Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .glossary/TERMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ This file names the *what*.
| bildirim | The notification **feature** (`features/bildirim`, epic #1666): the recipient-keyed store + read model behind the `/bildirimler` center page and the topbar **unread badge** — the `notification` D1 table, the `Notification` domain service, and the `bildirim.*` fate views/mutations (`list` / `unreadCount` / `markRead` / `markAllRead`). Ships dark behind the single default-off `phoenix-bildirim` flag every emitter sibling reuses. Turkish for "notification". | the **report (bildir)** feature (content-reporting for moderation — `bildir`, a different lexeme and module); an emitter (siblings #1695–#1699 produce notifications; this is the spine) |
| divan | The proving-ground reviewer surface (`features/divan`): the gated destination where the established community (yazar + moderatör) reviews a çaylak's sandboxed work and promotes (mod-direct) or vouches (kefil) them toward yazar — work "goes before the divan". Reads the `sandboxBacklogWhere` backlog over a yazar-OR-mod gate. Its topbar entry (`divanTo` NavLink, `Topbar.tsx`) is a **destination** under the ratified nav element taxonomy (destination / primary action / utility / signal — wayfinder map #2583, fork #2586). Turkish for "council/court". | a widening of inline çaylak visibility — it is a gated **destination** that reads the sandbox backlog, not a change to who sees sandboxed items inline; not a **primary action** (it goes nowhere near the `+ gönderi` CTA class) |
| divan section nav (boxed-pill tabs) | The intra-`/divan` section switcher (`.kp-divan__nav-tab`, `DivanPage.tsx` / `Divan.css`): the çaylaklar (roster) ↔ raporlar (mod queue) tab row shown when `raporlarVisible`. Each tab is a **boxed pill** — `1px` border + `r-sm` + a resting `surface` fill, switching to an `accent-faint` fill + `accent` border on `aria-current`. Under the ratified nav taxonomy (map #2583, fork #2586) these are intra-destination **destination** switchers, and the map flags the **resting boxed fill** as a containment-law violation candidate (the nav-surface inventory #2585): a section tab may paint accent on the *active* state (transient state paint is legal) but should not carry a boxed resting chrome. | the divan topbar entry (that is the destination-level `divanTo` link, one level up); a containment-legal transient accent (the *violation* is the resting boxed fill, not the active-state accent) |
| authoring brief | The GitHub issue a fresh `/skill-creator` session works out of to author one **fabrika** skill — the boot document that makes a stateless session workable (founder workflow ruling, #4650). Carries six fields: the skill + its destination directory, the v1 baseline path (reference only, #4638), the incidents from the 74-issue KEEP corpus the rebuild must not regress (#4642), the existing verbs it may assume (#4635), pointers to the two fabrika convention docs, and the output contract — `skill-reviewer` runs on the authored skill before the PR opens (runbook step 5.5, #4650), then one PR carrying `SKILL.md` + the derived contract spec, linked back with `Fixes #<brief>`. Format: [`claude-plugins/fabrika/docs/authoring-brief-contract.md`](../claude-plugins/fabrika/docs/authoring-brief-contract.md). | a `write-code` ticket (a brief is fired by a human starting a fresh session; what keeps it out of the coder pool is that it is emitted **assigned**, not label-absence — `review-plan` flips every planned child to `status:triaged` regardless — #4637-C's one-door rule, enforcement gap tracked at #4693); a design spec (it supplies ground, not architecture — the two-layer split is the session's judgment); an eval case (it cites incident issues; the bar, harness and corpus are #4649's) |
| authoring brief | The GitHub issue a fresh `/skill-creator` session works out of to author one **fabrika** skill — the boot document that makes a stateless session workable (founder workflow ruling, #4650). Carries six fields: the skill + its destination directory, the v1 baseline path (reference only, #4638), the incidents from the ruled KEEP corpus the rebuild must not regress — enumerated in [`packages/fabrika-cli/src/eval/incident-corpus/ruled-keeps.json`](../packages/fabrika-cli/src/eval/incident-corpus/ruled-keeps.json) at **66 members plus 1 pending** (#4642, corrected #4823), the existing verbs it may assume (#4635), pointers to the two fabrika convention docs, and the output contract — `skill-reviewer` runs on the authored skill before the PR opens (runbook step 5.5, #4650), then one PR carrying `SKILL.md` + the derived contract spec, linked back with `Fixes #<brief>`. Format: [`claude-plugins/fabrika/docs/authoring-brief-contract.md`](../claude-plugins/fabrika/docs/authoring-brief-contract.md). | a `write-code` ticket (a brief is fired by a human starting a fresh session; what keeps it out of the coder pool is that it is emitted **assigned**, not label-absence — `review-plan` flips every planned child to `status:triaged` regardless — #4637-C's one-door rule, enforcement gap tracked at #4693); a design spec (it supplies ground, not architecture — the two-layer split is the session's judgment); an eval case (it cites incident issues; the bar, harness and corpus are #4649's) |
| fabrika | The kamp.us **agent pipeline rebuilt from first principles** as its own plugin (`claude-plugins/fabrika/`, epic #4648) — thin skill wrappers over deterministic CLI verbs, every skill authored through `/skill-creator` and no other door (#4637-C). Grows beside the v1 baseline (`claude-plugins/kampus-pipeline/`), which stays frozen as the comparison target. Founder-sealed name (#4631), styled lowercase like the sibling brand nouns. Turkish for "factory". | the v1 `kampus-pipeline` plugin (that is the frozen baseline fabrika is measured against, not an earlier draft of fabrika); a rename or refactor of v1 (fabrika is net-new, built from first principles) |
| derived CLI contract (contract spec) | The per-skill specification an authoring session emits for the **fabrika** verbs its skill needs, and the artifact a `write-code` agent implements from: verb inventory, per-verb inputs/flags, output shape, exit-status semantics, error taxonomy, scope + zero-scope behavior, and literal examples. "Derived" is the method (#4638): the session works out which deterministic work belongs in the CLI and writes the contract, and **that spec is what the CLI implements** — so it must be readable without the authoring transcript. Lands as `contract.md` beside its skill; the format and the interface convention every verb meets are in [`claude-plugins/fabrika/docs/cli-interface-convention.md`](../claude-plugins/fabrika/docs/cli-interface-convention.md). | a description of an existing implementation (the contract comes first, the verb is built to it); a port of a v1 `kampus-pipeline` script (#4638 bans the v1 corpus as a source of truth — it is the frozen comparison baseline); the skill's own prose (judgment stays in the wrapper, determinism moves to the verb) |
| fabrika-cli | The deterministic verb package **fabrika**'s skills call: [`packages/fabrika-cli/`](../packages/fabrika-cli/), binary `fabrika`, invoked as `fabrika <group> <verb> …` — one registered group per skill, the first being `adr` (#4725). It is the deterministic half of fabrika's two-layer split: what a skill would otherwise re-derive per session lives here as a verb, and the skill fence is one invocation of it. **It calls `pipeline-cli` nowhere — no import, no subprocess ([ADR 0238](../.decisions/0238-fabrika-reimplements-v1-never-calls-it.md))**, which is why it is a separate package rather than a group inside the v1 router: a fabrika tethered to the pipeline it exists to replace can never replace it, so v1 is a reference for semantics and scars, never a dependency. | `pipeline-cli` (the v1 router — the frozen comparison baseline, and never an upstream); the **derived CLI contract** (that is the per-skill spec; this is the implementation built to it); a general-purpose CLI (internal pipeline tooling only) |
Expand Down
21 changes: 14 additions & 7 deletions claude-plugins/fabrika/docs/authoring-brief-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,18 @@ brief that says "rebuild this" has specified nothing; the fields below are the s

### 3. Incidents — what the rebuild must not regress

The rows from the **74-issue KEEP corpus** that bear on this skill, each as a number plus the
one-line behavior it records. The corpus is the fabrika eval feedstock ruled at 74 on
[#4642](https://github.com/kamp-us/phoenix/issues/4642) (the per-issue verdict table lives on
[#4634](https://github.com/kamp-us/phoenix/issues/4634)); it is the pipeline's observed scar tissue,
kept precisely because each row is a real incident encodable as a regression case.
The rows from the **ruled KEEP corpus** that bear on this skill, each as a number plus the
one-line behavior it records. The corpus is the fabrika eval feedstock — the pipeline's observed
scar tissue, kept precisely because each row is a real incident encodable as a regression case —
and it is a file you **read**, not a join you re-run:
[`ruled-keeps.json`](../../../packages/fabrika-cli/src/eval/incident-corpus/ruled-keeps.json)
enumerates it at **66 members plus 1 pending**, and `fabrika eval keeps` prints it with the cases
that already pin each row.
[#4642](https://github.com/kamp-us/phoenix/issues/4642) ruled the corpus and published its size as
74; that figure double-counts the 7 borderline items, and the enumeration supersedes it
([#4823](https://github.com/kamp-us/phoenix/issues/4823)). #4642 and the per-issue verdict table on
[#4634](https://github.com/kamp-us/phoenix/issues/4634) remain the enumeration's provenance, which
`ruled-keeps.json` records in its own `derivation`.

This is the field that makes a stateless session safe. Without it the session re-derives the skill
from its purpose alone and silently re-opens every hole the v1 skill closed the hard way — which is
Expand All @@ -67,7 +74,7 @@ the one thing a first-principles rebuild is most likely to do.
Two rules keep the field honest:

- **Cite by number, with the behavior stated.** "See the corpus" is not a list. A session cannot
open 74 issues and guess which four are its own.
open the whole corpus and guess which four are its own.
- **A skill with no corpus rows says so explicitly.** An empty list and an unwritten list read the
same on the page and mean opposite things.

Expand Down Expand Up @@ -223,7 +230,7 @@ doc, and this example pre-commits none of `/adr`'s design.
(181 lines, plus `scripts/`). Reference only — read it for the problem and the scars, not for the
shape. Do not port its scripts (#4638).

**Incidents this rebuild must not regress** (74-issue KEEP corpus, #4642):
**Incidents this rebuild must not regress** (ruled KEEP corpus, `ruled-keeps.json`; #4642):

| Issue | Recorded behavior |
|---|---|
Expand Down
6 changes: 3 additions & 3 deletions packages/fabrika-cli/src/eval/incident-corpus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ should carry its case before the week is out.

If the incident is one of the ruled KEEP issues, **read `ruled-keeps.json`** — or run
`fabrika eval keeps incident-corpus/ruled-keeps.json`, which prints each row with the eval cases
that already pin it. Membership is **66**, plus one pending row
([#4180](https://github.com/kamp-us/phoenix/issues/4180), whose sweep verdict the
[#4642](https://github.com/kamp-us/phoenix/issues/4642) ruling retracted and never replaced). Cite
that already pin it. The corpus is **66 members plus 1 pending** — the pending row is
[#4180](https://github.com/kamp-us/phoenix/issues/4180), whose sweep verdict the
[#4642](https://github.com/kamp-us/phoenix/issues/4642) ruling retracted and never replaced. Cite
the enumeration in the case's `verification`.

Do **not** re-run the two-artifact join by hand. That join — a `KEEP-AS-EVAL` row on the
Expand Down
37 changes: 36 additions & 1 deletion packages/fabrika-cli/src/eval/ruled-keeps.data.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {decodeIncidentProvenance} from "./incident-provenance.ts";
import {
decodeRuledKeeps,
KEEP_VERDICT,
publishedFigure,
publishedFigureViolations,
ruledKeepsViolations,
summarize,
withCoverage,
Expand Down Expand Up @@ -122,6 +124,39 @@ describe("ruled KEEP corpus — the corpus README points at the enumeration", ()

it("names the committed enumeration and the corrected figure", () => {
assert.include(readme(), "ruled-keeps.json");
assert.include(readme(), "66");
assert.include(readme(), publishedFigure(keeps()));
});
});

/**
* The corpus publishes its size to artifacts outside this package — the glossary entry a brief
* author reads first, then the fabrika authoring-brief contract that tells them which incident rows
* to cite — so a check confined to the package would go green while the page an author actually
* reads still said 74 (#4823, and #4838 for the same defect one file over). The surface list is the
* check: a page missing from it is unguarded however sound the assertion, so every page that
* publishes the size belongs here. Each surface is read by path: an unreadable surface throws,
* because "could not read it" and "it carries no stale figure" are different facts.
*/
describe("ruled KEEP corpus — every published cardinality is derived from the enumeration", () => {
const SURFACES = [
"./incident-corpus/README.md",
"../../../../.glossary/TERMS.md",
"../../../../claude-plugins/fabrika/docs/authoring-brief-contract.md",
];

it("checks a non-empty set of surfaces — zero scope reds (ADR 0092)", () => {
assert.isAbove(SURFACES.length, 0);
});

it("no surface publishes a figure the enumeration does not support", () => {
const figure = publishedFigure(keeps());
const violations = SURFACES.flatMap((surface) =>
publishedFigureViolations(
surface,
readFileSync(fileURLToPath(new URL(surface, import.meta.url)), "utf8"),
figure,
),
);
assert.deepStrictEqual(violations, []);
});
});
44 changes: 44 additions & 0 deletions packages/fabrika-cli/src/eval/ruled-keeps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,50 @@ export const ruledKeepsViolations = (keeps: RuledKeeps): ReadonlyArray<string> =
return violations;
};

/**
* The corpus size as the enumeration itself computes it — the one phrase every artifact that
* publishes this cardinality must carry verbatim.
*
* A published figure written as a literal is a second source of truth: the enumeration moves, the
* prose does not, and the reader gets a plausible number instead of an error (#4482, the class this
* corpus exists to record). Deriving the phrase from `rows` is what lets `publishedFigureViolations`
* red on drift rather than on a spelling.
*/
export const publishedFigure = (keeps: RuledKeeps): string => {
const members = keeps.rows.filter((row) => row.status === "member").length;
return `${members} members plus ${keeps.rows.length - members} pending`;
};

/**
* Phrasings that assert a corpus size the enumeration disproves. These are the exact strings the
* 74 was published as in-tree; the deny-list catches a copy of one, while the *positive* half of
* the check — the derived figure must be present — is what catches a figure that merely went stale.
*/
export const DISCREDITED_FIGURES: ReadonlyArray<string> = [
"74-issue KEEP corpus",
"74 KEEP",
"ruled at 74",
];

/**
* Check one artifact that publishes the corpus size against the enumeration. An empty array means
* the surface agrees with the file; each string names one way it does not.
*/
export const publishedFigureViolations = (
surface: string,
text: string,
figure: string,
): ReadonlyArray<string> => {
const violations: string[] = [];
if (!text.includes(figure)) {
violations.push(`${surface}: does not publish the enumerated figure '${figure}'`);
}
for (const stale of DISCREDITED_FIGURES) {
if (text.includes(stale)) violations.push(`${surface}: asserts the discredited '${stale}'`);
}
return violations;
};

/** A row joined with the corpus coverage the provenance ledger reports for it. */
export interface CoveredRow {
readonly row: KeepRow;
Expand Down
53 changes: 53 additions & 0 deletions packages/fabrika-cli/src/eval/ruled-keeps.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {Result} from "effect";
import type {IncidentProvenance} from "./incident-provenance.ts";
import {
decodeRuledKeeps,
publishedFigure,
publishedFigureViolations,
type RuledKeeps,
renderKeeps,
ruledKeepsViolations,
Expand Down Expand Up @@ -164,6 +166,57 @@ describe("summarize", () => {
});
});

describe("publishedFigure", () => {
it("is computed from the rows, so it moves when membership moves", () => {
const one = decode(file([member(1)]));
const two = decode(
file([
member(1),
member(2),
{...member(4180), status: "pending", pendingReason: "retracted"},
]),
);
assert.strictEqual(publishedFigure(one), "1 members plus 0 pending");
assert.strictEqual(publishedFigure(two), "2 members plus 1 pending");
});
});

describe("publishedFigureViolations", () => {
const figure = "66 members plus 1 pending";

it("holds on a surface that publishes the derived figure and no discredited one", () => {
assert.deepStrictEqual(
publishedFigureViolations("doc.md", `The corpus is ${figure} today.`, figure),
[],
);
});

it("reds when the surface publishes a figure the enumeration no longer supports", () => {
const violations = publishedFigureViolations(
"doc.md",
"The corpus is 65 members plus 2 pending today.",
figure,
);
assert.deepStrictEqual(violations, [
"doc.md: does not publish the enumerated figure '66 members plus 1 pending'",
]);
});

it("reds on a copy of the discredited cardinality even beside the right figure", () => {
const violations = publishedFigureViolations(
"doc.md",
`Pull rows from the 74-issue KEEP corpus. The corpus is ${figure}.`,
figure,
);
assert.deepStrictEqual(violations, ["doc.md: asserts the discredited '74-issue KEEP corpus'"]);
});

it("does not red on the ruling's size being narrated as superseded history", () => {
const text = `#4642 published its size as 74; the enumeration says ${figure}.`;
assert.deepStrictEqual(publishedFigureViolations("doc.md", text, figure), []);
});
});

describe("renderKeeps", () => {
it("marks a pending row and names the case that pins a covered one", () => {
const keeps = decode(
Expand Down
Loading