diff --git a/.glossary/TERMS.md b/.glossary/TERMS.md index a560e41a4..9fb5b8d4d 100644 --- a/.glossary/TERMS.md +++ b/.glossary/TERMS.md @@ -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 #`. 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 #`. 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 …` — 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) | diff --git a/claude-plugins/fabrika/docs/authoring-brief-contract.md b/claude-plugins/fabrika/docs/authoring-brief-contract.md index d84ce67ae..1df706c28 100644 --- a/claude-plugins/fabrika/docs/authoring-brief-contract.md +++ b/claude-plugins/fabrika/docs/authoring-brief-contract.md @@ -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 @@ -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. @@ -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 | |---|---| diff --git a/packages/fabrika-cli/src/eval/incident-corpus/README.md b/packages/fabrika-cli/src/eval/incident-corpus/README.md index 228f7da16..5b457db4d 100644 --- a/packages/fabrika-cli/src/eval/incident-corpus/README.md +++ b/packages/fabrika-cli/src/eval/incident-corpus/README.md @@ -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 diff --git a/packages/fabrika-cli/src/eval/ruled-keeps.data.unit.test.ts b/packages/fabrika-cli/src/eval/ruled-keeps.data.unit.test.ts index 265abd477..d8de22536 100644 --- a/packages/fabrika-cli/src/eval/ruled-keeps.data.unit.test.ts +++ b/packages/fabrika-cli/src/eval/ruled-keeps.data.unit.test.ts @@ -6,6 +6,8 @@ import {decodeIncidentProvenance} from "./incident-provenance.ts"; import { decodeRuledKeeps, KEEP_VERDICT, + publishedFigure, + publishedFigureViolations, ruledKeepsViolations, summarize, withCoverage, @@ -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, []); }); }); diff --git a/packages/fabrika-cli/src/eval/ruled-keeps.ts b/packages/fabrika-cli/src/eval/ruled-keeps.ts index 13ff1a551..a0b16cef5 100644 --- a/packages/fabrika-cli/src/eval/ruled-keeps.ts +++ b/packages/fabrika-cli/src/eval/ruled-keeps.ts @@ -148,6 +148,50 @@ export const ruledKeepsViolations = (keeps: RuledKeeps): ReadonlyArray = 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 = [ + "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 => { + 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; diff --git a/packages/fabrika-cli/src/eval/ruled-keeps.unit.test.ts b/packages/fabrika-cli/src/eval/ruled-keeps.unit.test.ts index e7059efac..86263fa20 100644 --- a/packages/fabrika-cli/src/eval/ruled-keeps.unit.test.ts +++ b/packages/fabrika-cli/src/eval/ruled-keeps.unit.test.ts @@ -3,6 +3,8 @@ import {Result} from "effect"; import type {IncidentProvenance} from "./incident-provenance.ts"; import { decodeRuledKeeps, + publishedFigure, + publishedFigureViolations, type RuledKeeps, renderKeeps, ruledKeepsViolations, @@ -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(