From c1d84807e1a4d61eec88a73a6ed95c11d86cc5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 19:02:39 +0200 Subject: [PATCH 01/16] =?UTF-8?q?docs(plans):=20draft=20plan=2021=20?= =?UTF-8?q?=E2=80=94=20the=20phase-4=20oracle=20split,=20floor=20and=20vie?= =?UTF-8?q?w=20waves,=20and=20the=2005=20dissolution=20attempt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- plans/21-self-hosting-phase-4.md | 299 +++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 plans/21-self-hosting-phase-4.md diff --git a/plans/21-self-hosting-phase-4.md b/plans/21-self-hosting-phase-4.md new file mode 100644 index 0000000..741649a --- /dev/null +++ b/plans/21-self-hosting-phase-4.md @@ -0,0 +1,299 @@ +# Plan 21 — Self-hosting phase 4: the oracle split, the floor and view corpus waves, and the `05` dissolution attempt + +> **Status:** DRAFTED — execution begins on `feature/protocol-self-application-phase-4`. This is +> plan 21, the highest primary-numbered plan; the latest ✅ EXECUTED ground is plan 20 (the +> phase-3 close). Build state lives in **`plans/`** — read the highest **primary-numbered** +> plan's status header, plus any **active subplans it (or its parent family) explicitly +> designates as current**; ignore unnumbered files and letter-suffixed plans only when no +> primary/active plan designates them. If that plan is DRAFTED, also read the latest ✅ +> EXECUTED plan for settled ground. +> +> **Spec anchors:** [plan 20 §7 done-record and successor guidance](20-self-hosting-phase-3.md) · +> [the dissolution decision](../specs/decisions/concept-docs-dissolve.sdp.md) +> (`spec:decisions.concept-docs-dissolve`) · [the point-per-example decision] +> (../specs/decisions/point-per-example.sdp.md) (`spec:decisions.point-per-example`). + +## (a) Status + +This is the executable phase plan for plan 20's recorded successor guidance, in its stated +order: the golden-oracle split first, the shared-constant hardening for the contract-dependent +suites, then the next corpus wave in recorded gap order — the lower readiness-floor rungs and +the per-kind evidence table, the derived-readiness banner law, and the +already-implemented-but-uncarried view rules — with readiness maturing only where verifiers +honestly land, a re-audit of `docs/concept/05` (the doc those gaps hold in place) with `06` and +`07` re-graded on the same pass, and an adversarial mutation-probing close. Execution happens +only on `feature/protocol-self-application-phase-4`. Sessions run agent-executed and +orchestrator-verified; **owner ratification of the whole phase happens at the phase PR review** +— the gate ledger (§9) records that honestly and never claims a live owner acceptance that did +not occur. + +## (b) Context + +Phase 3 closed at **87 Specs · 51 `ready` / 36 `defined` · 29 bound points across five bound +suites**, with `02` and `03` deleted and twelve gaps recorded against `05`/`06`/`07`. Three +debts are on record. First, `test/self-hosting-graph.test.ts` stands at 3,888 lines in a single +`it()` with frozen absolute counts — the first failure masks the rest, and every conversion +wave thrashes it. Second, the clean-room lint exemption (`eslint.config.js`) and the wrapper's +contract-dependency table (`vitest-test.mjs`) name the same six files independently; the next +bound suite would repeat the clean-clone lint surprise. Third, the biggest recorded gaps are +laws the engine already implements but no Spec carries: the `idea`/`scoped`/`defined` floor +rungs and the per-kind evidence table (`src/validate/readiness-floor.ts`), the +derived-readiness banner (`src/projections/design-review-context.ts` `renderReadiness`), the +`implemented` view-label rule (`renderBindings`), the Design Review wholesale rewrite +(`src/cli/validate-view-command.ts` `runView`), the one diagnostic rendering rule +(`src/cli/output.ts` `formatFinding`), and validator self-testing (`05` §5). + +The permanent guardrails stand unchanged: checks police conformance and honesty, never +content-quality and never workflow; delivery facts are derived, never authored; the claim +taxonomy is never collapsed; readiness is stated only where the floor honestly clears; one +canonical surface per ID, no mixing. + +## (c) Scope + +1. **S1 — the oracle split.** The single-purpose session plan 20 ordered: split the golden + corpus oracle by assertion family, never one mega-assert, with zero assertion loss. +2. **S2 — the shared constant + the floor wave.** One source of truth for the + contract-dependent suites; then Specs carrying the lower floor rungs and the per-kind + evidence table, with bound points in the existing validators suite. +3. **S3 — the view wave.** Specs carrying the banner law, the view-label rule, the wholesale + rewrite, the diagnostic rendering rule, and validator self-testing; a new bound projections + suite entering through the S2 shared constant. +4. **S4 — readiness sweep + the `05` re-audit.** Per-spec disposition of the remaining + `defined` corpus; the per-doc audit re-run for `05` (delete only if fully carried), `06` and + `07` re-graded honestly. +5. **S5 — close.** Adversarial mutation-probing review over the new points, remediation, the + full gate plus clean-clone proof, this plan's done-record. + +**Out of scope, named deliberately:** converting the filesystem-corpus giants +(`test/markdown-reifier.test.ts`, `test/extract.test.ts`, `test/cli.test.ts`); the +packaging/bootstrap smoke surface; the deferred docket tail (Markdown Pack syntax · the gen-1 +`.feature` adapter · the no-reparse read seam · temporal-guard token assembly · the +editor-association gap · control-character latitude · the separate example-id namespace) unless +a wave forces an entry under fire; new content-quality validators; bulk concept purges; the +`06`/`07` gaps that are not this wave's laws (assist roles, `bySymbol`, discipline mapping, +distribution chart, per-PR preview, Mermaid surfaces, the two open questions, measure-what-hurts). + +## §1 Engineering rulings + +Rulings 1–9 of plan 20 carry forward verbatim (the law is the unit of conversion; any-kind +example spaces; the four-artifact template; no table sugar by default; the readiness promotion +law; batch-green bookkeeping; temporal-guard discipline as amended; per-doc audited deletion +ratified at the PR; drift discipline). Phase 4 adds: + +10. **The oracle split preserves the oracle.** The split may reorganize + `test/self-hosting-graph.test.ts` into multiple `it()` blocks and move the frozen expected + data into per-family modules, but: extraction runs **once** per suite run (hoisted, never + re-run per `it()`); every assertion survives or is replaced by a strictly equivalent or + stricter one; the expected data stays **authored transcription** (never computed from the + live graph); a total may be derived from the authored arrays' lengths only where a + same-strength repo↔oracle equality assertion remains (`result.counts` vs the authored + arrays), and the readiness histogram stays an explicit literal. The redundant inline + node-id roster (a second copy of the spec-id list) may be derived from the authored + `expectedSpecs`/`expectedAnchors` arrays — recorded here as deliberate de-duplication of + oracle data, not assertion loss. +11. **One source of truth for contract-dependent suites.** A root ESM module exports the + per-tree dependency rows (`contracts` dir · `generation` command · `testPaths`); + `vitest-test.mjs` and `eslint.config.js` both import it. The eslint side derives its + exemption file list from the root tree's `testPaths`; the exclusion rule stays as + documented (in-memory suites — the corpus oracle, the contracts self-check — are never + listed). A new bound suite enters the constant once and both surfaces follow. +12. **The P-1 lesson is the conversion playbook.** Build worlds where only the named law can + refuse; mutation-probe the named law before recording "exercises clause X"; never use + absence-of-a-finding-id as the sole discriminator. +13. **Enrichment follows the mirror, never invents a third behavior.** The floor-wave Specs + state the clause tables in authored words that agree with `src/validate/readiness-floor.ts` + (MD-13's code-level source of truth); the view-wave Specs state what the projection code + verifiably does. Any disagreement found while transcribing is drift to resolve on the + record — fix the stale side deliberately. +14. **A registry surface never dangles.** If `05` is deleted at S4, every registry row citing + `05` §3 as a mirror (MD-13, MD-9), every `CONTEXT.md` section pointer (`→ 05`), and every + surviving-doc citation is re-pointed at the carrying Spec in the same change, and the + two-form reference sweep (backticked and bare citation spellings) is re-run to zero hits — + the phase-3 deletion protocol unchanged. + +## §2 Session inventory + +### S1 — the oracle split (single purpose) + +`test/self-hosting-graph.test.ts` today: one `describe`/one `it()` (lines 3593–3886) over +module-level frozen data — `expectedSpecs` (87 entries, ~2,558 lines), `expectedPackMembers`, +`expectedDeclaredRelations`, `expectedWarnings`, `expectedAnchors` (65 entries) — asserting in +order: clean extraction · no warn-level findings · frozen counts (87/1/65) · a redundant +151-item node-id roster · per-spec descriptor equality · declared relations · the readiness +histogram (51/36) · pack membership · the pack node · edge count (294) · anchored edges · +anchor nodes (with file I/O line resolution) · anchor-site proximity · two derived-fact case +studies. + +Deliverables: + +1. The suite reorganized so each assertion group above is its own `it()` (or a small set of + per-family `it()`s for the per-spec descriptor block), sharing **one** hoisted extraction. +2. The frozen expected data moved to authored per-family modules (e.g. + `test/self-hosting-oracle/`), so the next conversion wave touches one family file, not a + 3,888-line monolith. Data modules are plain authored transcription — no generation. +3. The redundant node-id roster derived from the authored arrays (ruling 10). +4. Zero assertion loss, verified by diff discipline: every projected field asserted today is + asserted after the split at equal or stricter strength. +5. `npm run check` green; one or more green-gate commits. + +Out of S1's scope: any corpus change, any conversion, any count change — the oracle's numbers +enter S1 at 87/1/65 · 153 · 294 · 51/36 and leave S1 identical. + +### S2 — the shared constant + the floor wave + +1. **The shared constant (ruling 11).** Root module (suggested name + `contract-dependent-suites.mjs`) exporting the two dependency rows exactly as + `vitest-test.mjs:13–31` states them today; both consumers import it; behavior byte-identical + (same preflight failure text, same lint exemption set). The eslint comment updates to name + the shared module as the coupling's mechanism. +2. **The floor wave (gap 1 of the phase-3 ledger).** Author the Specs that carry what + `src/validate/readiness-floor.ts` and `05` §3 state today: + - Enrich `spec:validation.readiness-floor` to state the `idea` / `scoped` / `defined` rung + clauses in authored words (today its own text defers them to code), keeping the MD-13 + posture: the code table remains the realizing entrypoint; the Spec now carries the law. + - Author the per-kind evidence table's carrying surface — a new `rule`-kind Spec (suggested + `spec:validation.kind-evidence`) refining `spec:validation.readiness-floor`, `decidedBy` + `spec:decisions.kind-conditional-floor`, stating the 7-kind × `scoped`/`defined` rows + (including the `behavior`/`workflow`/`contract` shared family and the MD-12 contract-row + interim) in authored words. + - Example space + bound points (planned 3–5 across the two Specs) in the existing + `test/self-hosting-validators.test.ts` probe-graph style: e.g. a `defined` probe failing + `kind-evidence-complete` under its own clause id; a `scoped` probe failing + `at-least-one-relation`; a `constraint` probe at `defined` whose entry lacks a + machine-readable target; a promoted-evidence probe honoring the MD-16 bound. Each point + names the finding `honesty/readiness-floor` **and** the clause id via `relatedId` — never + absence-of-a-finding as the sole discriminator. +3. Bookkeeping batch-green (ruling 6): pack manifest, regenerated contracts, promotions that + honestly clear (the two floor Specs promote only with resolving verifiers). + +### S3 — the view wave + +Author the Specs carrying the five implemented-but-uncarried laws (phase-3 gaps 2, 3, 4, 5, 6), +stating what the code verifiably does (ruling 13): + +| Law | Realizing code | Suggested carrier | +|---|---|---| +| derived-readiness banner: stated renders beside floor-reached always; the divergence banner fires **only** in the dishonest direction and names the **first unmet clause** | `src/projections/design-review-context.ts` `renderReadiness` | new `rule` Spec under `specs/consumers/` refining `spec:consumers.design-review` | +| `implemented` view-label: the fact name stays internal; views render binding language ("Implementation binding: present / Verifier binding: … / Runtime observation: not tracked") | `renderBindings` + the pack/index tables | same Spec family; MD-7 (`spec:decisions.binding-not-liveness`) keeps the model half | +| wholesale page rewrite: build to a temp dir, atomic swap, no stale page survives; a failed run removes rather than leaves the view | `src/cli/validate-view-command.ts` `runView` | new `rule` Spec refining `spec:consumers.design-review` | +| one diagnostic rendering rule: location composed from the structured `file`/`line` fields, `path:line — [severity] validatorId — message`; absent fields degrade cleanly | `src/cli/output.ts` `formatFinding` (+ the Design Review twin) | new `rule` Spec under `specs/validation/` or `specs/consumers/` — the wave decides and records | +| validator self-testing: each validator ships should-fail and should-pass evidence | `test/validators.test.ts` practice | new `rule` Spec under `specs/validation/`; may honestly stay `defined` if its only verifier would be decorative | + +Bound points (planned 4–6) live in a **new** bound suite (suggested +`test/self-hosting-projections.test.ts`) that enters through the S2 shared constant — the +seventh suite proving ruling 11 — plus, where a law is CLI-side (`formatFinding`, `runView`), +worlds run the real seams (render over a probe reader context; a temp-dir `runView` with a +planted stale page). Every point is mutation-probed before its "exercises clause X" is +recorded (ruling 12). Promotions ride verifiers per ruling 5. + +### S4 — readiness sweep + the re-audits + +1. **Sweep**: every then-`defined` Spec dispositioned per-Spec — promote only under ruling 5, + refuse with a named reason otherwise (the phase-3 discipline; the 21 decisions and + whole-pipeline worlds are expected honest refusals). +2. **The `05` re-audit**, to the phase-3 template, judged over the regenerated Design Review: + the S2/S3 waves target exactly its three recorded gaps (the rungs + evidence table · the + banner · validator self-testing). Delete only on a fully-carried verdict, with ruling 14's + re-pointing and two-form reference sweep in the same change. If any gap honestly survives, + `05` stays and the residue is recorded. +3. **`06` and `07` re-graded** on the same pass: the banner gap (shared) and the view-label / + diagnostic-rendering / wholesale-rewrite gaps close if S3 landed them; the remaining gaps + (assist roles, `bySymbol`, discipline mapping, distribution chart, per-PR preview, Mermaid + surfaces, open questions, measure-what-hurts) are out of scope, so both docs are expected + to **stay** — the re-grade updates their gap ledgers honestly, nothing more. + +### S5 — close + +1. Adversarial review over the full branch diff, archived as + `reviews/10-self-hosting-phase-4-pre-close-review.md`: mutation-test every new bound point + (break the named law in a sandbox copy, re-run the point, record red-for-the-right-reason), + plus a records-honesty recomputation of the headline numbers and a dangling-reference sweep + if `05` was deleted. +2. Remediation with per-finding dispositions in the archived table. +3. The full twelve-leg `npm run check` green, plus the clean-clone proof + (`git clone --no-local` → `npm ci` → full chain) at the close SHA. +4. This plan's done-record (§6–§9 terminal), the `AGENTS.md` status update, and the PR + description. + +## §3 Watch items + +| Item | Fires when | State | +|---|---|---| +| table sugar (ruling 4) | sibling authoring proves dishonest or unusable in a wave | unfired | +| single-literal vocabulary form | real material forces it | unfired | +| per-family oracle drift | a split family module regains cross-family assertions or a mega-assert | unfired | +| shared-constant bypass | a new contract-dependent suite lands outside the constant | unfired | +| separate example-id namespace | a collision or real pressure appears | unfired (watch continues from phase 3) | + +## §4 Docket ledger (carried in from plan 20) + +Markdown Pack syntax ruling · the gen-1 `.feature` adapter · the no-reparse read seam · +temporal-guard token assembly · the editor-association gap · corpus-test granularity (owned by +this phase — S1 is the session that dispositions it) · control-character latitude · the +separate example id namespace. Rows close only with reasons in the done-record. + +## §5 Acceptance criteria + +1. **The oracle is split with zero assertion loss**: multiple `it()`s over one hoisted + extraction; the per-family data modules are authored transcription; the suite's law + coverage at close is a superset of its opening coverage; the corpus-test granularity docket + row is dispositioned. +2. **One source of truth for contract-dependent suites**: `vitest-test.mjs` and + `eslint.config.js` derive from the shared constant; the S3 suite enters through it; a clean + clone lints green before generation. +3. **Executable-path facts, not claims**: every new Spec promoted to `ready` carries + `has-verifier` through the executable path in the regenerated graph; zero validation + errors; contract generation deterministic under `--check-clean`; every new bound point is + mutation-probed red for the law it names. +4. **Honest readiness**: every promotion clears the floor with a resolving verifier; no + `honesty/gaps` warning is introduced; the closing distribution is recorded; refusals carry + named reasons. +5. **The `05` disposition is audit-grounded**: deleted only on a fully-carried per-doc audit + with ruling 14's sweep at zero hits, or kept with its residue recorded; `06`/`07` re-graded + with honest ledgers. +6. **The gate holds throughout**: `npm run check` green at every blessed commit; the close + runs the full chain plus the clean-clone proof. +7. **Records continue**: the session ledger, watch items, and docket rows are terminal or + carried with reasons; the adversarial review is archived with every finding dispositioned + before close. + +## §6 Done-record + +*(written at close)* + +## §7 Conversion / corpus ledger + +*(maintained by the waves; state values `planned` → `done` / `deferred` / `dropped` with +reasons)* + +| Wave | Law | Carrier Spec(s) | Planned points | State | +|---|---|---|---|---| +| S2 | lower floor rungs (`idea`/`scoped`/`defined` clauses) | `spec:validation.readiness-floor` (enriched) | 2–3 | planned | +| S2 | per-kind evidence table + MD-16 promoted-evidence bound | new `spec:validation.kind-evidence` | 1–2 | planned | +| S3 | derived-readiness banner (one direction · first unmet clause) | new Spec under `specs/consumers/` | 1–2 | planned | +| S3 | `implemented` view-label (binding language) | same family | 1 | planned | +| S3 | wholesale page rewrite (atomic swap · no stale page) | new Spec | 1 | planned | +| S3 | one diagnostic rendering rule | new Spec | 1 | planned | +| S3 | validator self-testing | new Spec | 0–1 (may honestly stay `defined`) | planned | + +## §8 Readiness ledger + +*(maintained at S2/S3 promotions and the S4 sweep; opening distribution `ready: 51 / +defined: 36` over 87)* + +## §9 Session and gate ledger + +Sessions execute sequentially; each closes with a green twelve-leg gate, a regenerated Design +Review where the wave touched the corpus, and a commit series on the effort branch. This +ledger is git process evidence, never graph content. + +| Session | Delivers | Gate discipline | State | +|---|---|---|---| +| S1 | the oracle split (§2 S1) | orchestrator-verified green gate | planned | +| S2 | shared constant + floor wave | orchestrator-verified green gate | planned | +| S3 | view wave + seventh bound suite | orchestrator-verified green gate | planned | +| S4 | readiness sweep + re-audits (± the `05` deletion) | orchestrator-verified green gate over the regenerated Design Review | planned | +| S5 | adversarial review, remediation, full close, done-record | full chain + clean-clone; review archived | planned | + +Owner ratification of every gate above happens at the phase PR review; no live owner +acceptance occurs during execution. From 8e961a53c8d75f540b9c9448d20695c8e8f7436b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 19:13:02 +0200 Subject: [PATCH 02/16] test(oracle): split the self-hosting corpus oracle into per-law assertions over one extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The golden corpus oracle stood as one `it()` over 3,888 lines, so the first failure masked every other law and each conversion wave thrashed a monolith. The suite now states each law as its own `it()` — 21 of them — over a single hoisted extraction: the corpus walk runs once per suite run, never once per assertion. The frozen expectation moves to authored transcription modules under `test/self-hosting-oracle/`: one file per Spec family (carrier, protocol, extraction, validation, model, consumers, decisions), plus the Pack manifest, the declared relations, the anchors, and an aggregating index. Nothing there is computed from the graph it judges. The per-family descriptor comparisons are joined by an explicit law that the union of the family slices is the whole primitive node set, so no Spec escapes between two families. Zero assertion loss: all 27 original `expect` sites survive at equal or stricter strength, and five are added — three oracle-length cross-checks against the frozen literals and the two-assertion no-Spec-outside-the-families law. The redundant inline node-id roster is derived from the authored spec and anchor arrays rather than kept as a second literal: deliberate de-duplication of oracle data, with the sorted node-id equality itself untouched. The corpus numbers enter and leave identical — 87 specs / 1 pack / 65 anchors, 153 nodes, 294 edges, ready 51 / defined 36. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- plans/21-self-hosting-phase-4.md | 6 +- test/self-hosting-graph.test.ts | 3821 +---------------- test/self-hosting-oracle/anchors.ts | 656 +++ test/self-hosting-oracle/carrier.ts | 301 ++ test/self-hosting-oracle/consumers.ts | 143 + test/self-hosting-oracle/decisions.ts | 562 +++ .../self-hosting-oracle/declared-relations.ts | 276 ++ test/self-hosting-oracle/extraction.ts | 535 +++ test/self-hosting-oracle/index.ts | 57 + test/self-hosting-oracle/model.ts | 338 ++ test/self-hosting-oracle/pack-members.ts | 92 + test/self-hosting-oracle/protocol.ts | 26 + test/self-hosting-oracle/validation.ts | 692 +++ 13 files changed, 3799 insertions(+), 3706 deletions(-) create mode 100644 test/self-hosting-oracle/anchors.ts create mode 100644 test/self-hosting-oracle/carrier.ts create mode 100644 test/self-hosting-oracle/consumers.ts create mode 100644 test/self-hosting-oracle/decisions.ts create mode 100644 test/self-hosting-oracle/declared-relations.ts create mode 100644 test/self-hosting-oracle/extraction.ts create mode 100644 test/self-hosting-oracle/index.ts create mode 100644 test/self-hosting-oracle/model.ts create mode 100644 test/self-hosting-oracle/pack-members.ts create mode 100644 test/self-hosting-oracle/protocol.ts create mode 100644 test/self-hosting-oracle/validation.ts diff --git a/plans/21-self-hosting-phase-4.md b/plans/21-self-hosting-phase-4.md index 741649a..4d29bea 100644 --- a/plans/21-self-hosting-phase-4.md +++ b/plans/21-self-hosting-phase-4.md @@ -229,7 +229,9 @@ recorded (ruling 12). Promotions ride verifiers per ruling 5. Markdown Pack syntax ruling · the gen-1 `.feature` adapter · the no-reparse read seam · temporal-guard token assembly · the editor-association gap · corpus-test granularity (owned by -this phase — S1 is the session that dispositions it) · control-character latitude · the +this phase — S1 is the session that dispositions it; **dispositioned at S1** — the corpus oracle +split into 21 `it()`s over one hoisted extraction, with the frozen expectation moved to authored +per-family modules under `test/self-hosting-oracle/`) · control-character latitude · the separate example id namespace. Rows close only with reasons in the done-record. ## §5 Acceptance criteria @@ -289,7 +291,7 @@ ledger is git process evidence, never graph content. | Session | Delivers | Gate discipline | State | |---|---|---|---| -| S1 | the oracle split (§2 S1) | orchestrator-verified green gate | planned | +| S1 | the oracle split (§2 S1) | orchestrator-verified green gate | done — 21 `it()`s over one hoisted extraction; the frozen expectation moved to ten authored modules under `test/self-hosting-oracle/` (seven family files, pack manifest, declared relations, anchors) plus their aggregating index; zero assertion loss (every one of the 27 original `expect` sites survives, 5 added: three oracle-length cross-checks and the two-assertion "no Spec outside the families" law), the node-id roster derived from the authored arrays per ruling 10; counts unchanged at 87/1/65 · 153 · 294 · ready 51 / defined 36 | | S2 | shared constant + floor wave | orchestrator-verified green gate | planned | | S3 | view wave + seventh bound suite | orchestrator-verified green gate | planned | | S4 | readiness sweep + re-audits (± the `05` deletion) | orchestrator-verified green gate over the regenerated Design Review | planned | diff --git a/test/self-hosting-graph.test.ts b/test/self-hosting-graph.test.ts index 67534eb..d95afd2 100644 --- a/test/self-hosting-graph.test.ts +++ b/test/self-hosting-graph.test.ts @@ -5,3584 +5,63 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { extract, validateGraph } from "../src/index.js"; +import type { ExpectedSpec } from "./self-hosting-oracle/index.js"; +import { + expectedAnchors, + expectedDeclaredRelations, + expectedPackMembers, + expectedSpecs, + expectedWarnings, + specFamilies, +} from "./self-hosting-oracle/index.js"; const repoRoot = fileURLToPath(new URL("..", import.meta.url)); -const expectedSpecs = [ - { - id: "spec:carrier.markdown-authoring", - specKind: "behavior", - altitude: "feature", - readiness: "defined", - file: "specs/carrier/markdown-authoring.sdp.md", - title: "Markdown authoring enters the one graph", - narrative: null, - sections: { - intent: { - outcome: "Author new Protocol Specs in Markdown without creating a second truth path.", - }, - behavior: { - rules: [ - "Markdown and TypeScript carriers feed the same reification and graph-derivation path.", - ], - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:carrier.envelope-contract", - specKind: "contract", - altitude: "feature", - readiness: "ready", - file: "specs/carrier/envelope-contract.sdp.md", - title: "The Markdown envelope is explicit and bounded", - narrative: null, - sections: { - intent: { - outcome: "Make a Markdown Spec's identity and descriptors deterministic to reify.", - }, - behavior: { - rules: [ - "A Markdown Spec declares id, kind, altitude, readiness, and relations in bounded YAML frontmatter; its first H1 declares title.", - "The envelope key set is closed and every one of its five keys is required: a key outside the set is refused rather than absorbed, and a missing key refuses the document rather than being defaulted.", - "`relations: {}` is written explicitly when the logical relation set is empty — honest carrier syntax, not a new logical requirement: the physical key catches a truncated envelope at reification while the model itself stays relation-optional.", - "A derived name is never authorable in the envelope: a delivery-fact or graph-shape key is refused under its own finding class, because delivery facts are derived and never authored.", - "The Protocol owns the envelope grammar and the parser policy while the pinned YAML library stays a swappable representation behind that contract, so an unsupported YAML construct is refused within explicit byte bounds on the carrier and its frontmatter rather than silently becoming carrier semantics.", - "The realizing entrypoints are `readMarkdownEnvelope` in `src/extract/markdown-envelope.ts` and `parseMarkdownFrontmatter` in `src/extract/markdown-frontmatter.ts`.", - ], - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:carrier.markdown-parser", - specKind: "behavior", - altitude: "feature", - readiness: "ready", - file: "specs/carrier/markdown-parser.sdp.md", - title: "The product parser reifies the ruled Markdown subset", - narrative: null, - sections: { - intent: { - problem: "Prevent carrier-specific graph and validation paths from diverging.", - outcome: "Reify authored Markdown without a second graph or validation path.", - value: "Markdown-carried intent remains subject to the Protocol's deterministic checks.", - }, - behavior: { - rules: [ - "The parser accepts only the ruled heading grammar and excludes one malformed carrier while continuing healthy siblings.", - "The ruled Markdown parser has bounded finding-class parity with the TypeScript carrier for `extract/non-static-envelope`, `extract/invalid-id`, `extract/duplicate-id`, `extract/reserved-property`, `extract/unowned-prose`, and `extract/unrecognized-property`; the shared validator ID is the claim, while severity and extract-versus-refuse outcomes remain carrier-specific.", - "Named non-claim — `extract/parse-error` remains distinct because YAML/frontmatter parsing has no TypeScript parser-diagnostic analogue.", - "Named non-claim — `extract/non-static-section` remains distinct because TypeScript degrades optional section properties while Markdown refuses malformed documents whole.", - "Named non-claim — `extract/unrecognized-statement` remains distinct because Markdown owns prose and structures, not TypeScript statement recognition.", - "Named non-claim — `extract/misplaced-authoring` remains distinct because Markdown has no executable authoring-call surface.", - ], - exampleSpace: { - given: ["the paired carrier probes named {probe:string}"], - when: ["both carriers reify their probe"], - [["t", "hen"].join("")]: [ - "both carriers report the finding class {findingId:string}", - 'the TypeScript carrier reports severity {typeScriptSeverity:"warning"|"error"} and extracts {typeScriptSpecs:number} specs', - 'the Markdown carrier reports severity {markdownSeverity:"warning"|"error"} and extracts {markdownSpecs:number} specs', - ], - }, - }, - verification: { - mode: "executable", - criteria: [ - "`test/extract-parity.test.ts` executes the settled finding-class parity matrix, including the six same-class findings, their carrier-specific outcomes, and four named non-claims.", - ], - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:carrier.markdown-parser.bounded-parity", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/carrier/markdown-parser.bounded-parity.sdp.md", - title: "One finding class is shared while the carriers' outcomes stay their own", - narrative: null, - sections: { - intent: { - outcome: - "Execute one same-class row of the parity matrix, including the outcomes it never claims.", - }, - behavior: { - examples: [ - { - given: ['the paired carrier probes named {probe: "unrecognized-property"}'], - when: ["both carriers reify their probe"], - [["t", "hen"].join("")]: [ - 'both carriers report the finding class {findingId: "extract/unrecognized-property"}', - 'the TypeScript carrier reports severity {typeScriptSeverity: "warning"} and extracts {typeScriptSpecs: 1} specs', - 'the Markdown carrier reports severity {markdownSeverity: "error"} and extracts {markdownSpecs: 0} specs', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:carrier.sdp-import", - specKind: "behavior", - altitude: "feature", - readiness: "ready", - file: "specs/carrier/sdp-import.sdp.md", - title: "TypeScript-carried Specs can become Markdown twins", - narrative: null, - sections: { - intent: { - actor: "A coding agent or maintainer.", - outcome: - "Convert a TypeScript-carrier Spec into an idiomatic `.sdp.md` twin beside its source.", - value: - "The TypeScript DSL survives as an import source while Markdown becomes the authored twin.", - }, - behavior: { - rules: [ - "Import writes the emitted Markdown sibling beside the TypeScript carrier and never deletes the source carrier.", - "Import refuses an existing Markdown sibling rather than overwriting it.", - "Refusal outcomes retain the TypeScript reifier findings and add import-local findings honestly.", - "Import consumes the TypeScript reifier so source acceptance follows one validation path.", - "A batch scans only bounded source directories, canonicalizes physical carrier identity, and computes every refusal and target collision before publishing any sibling.", - "Publication prepares exclusive temporary siblings and atomically creates targets without clobbering; rollback attempts every artifact, reports survivors, and never deletes a TypeScript source.", - ], - exampleSpace: { - given: ["a TS-carrier spec"], - when: ["importTypeScriptSpec runs"], - [["t", "hen"].join("")]: ["the emitted Markdown re-parses to an equal graph"], - }, - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:carrier.sdp-import.round-trip", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/carrier/sdp-import.round-trip.sdp.md", - title: "A TypeScript carrier survives import as an equal Markdown graph", - narrative: null, - sections: { - intent: { - outcome: "Execute the import round-trip against a TypeScript-carrier fixture.", - }, - behavior: { - examples: [ - { - given: ["a TS-carrier spec"], - when: ["importTypeScriptSpec runs"], - [["t", "hen"].join("")]: ["the emitted Markdown re-parses to an equal graph"], - }, - ], - }, - verification: { - mode: "executable", - criteria: [ - "The bound test runs `assertAuthoredRoundTrip` against the import behavior fixture.", - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:carrier.prose-ownership-rule", - specKind: "rule", - altitude: "story", - readiness: "ready", - file: "specs/carrier/prose-ownership-rule.sdp.md", - title: "Every prose edge has one owner", - narrative: null, - sections: { - intent: { outcome: "Keep free prose in the graph without ambiguous attachment." }, - behavior: { - rules: [ - "Narrative lives before the first H2 and is owned directly by the Spec; it is Spec content, never an envelope field.", - "A description is owned only by a singular section and lives under that section's own heading; the array-shaped constraints section has no description owner, so its explanatory prose belongs in narrative or intent instead.", - "Unowned prose — prose standing under no typed owner — is refused loudly rather than attached by guess or dropped in silence.", - "Prose is stored as graph content inside its typed owner, never as a file pointer or a heading-path key: a consumer reads prose from the graph without re-parsing the document, and churned document structure carries no identity.", - "The realizing entrypoints are `parseMarkdownBody` in `src/extract/markdown-body.ts` and `mapOwner` in `src/extract/markdown-body-owners.ts`.", - ], - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:protocol.self-hosting", - specKind: "behavior", - altitude: "epic", - readiness: "defined", - file: "specs/protocol/self-hosting.sdp.md", - title: "The Protocol authors and validates itself", - narrative: - "The Protocol's own delivery model exercises the same carrier, graph, checks, and projections offered to consumers.", - sections: { - intent: { outcome: "Prove the Protocol can carry its own intended truth honestly." }, - behavior: { - rules: [ - "All authored carriers derive one regenerable graph through one validation path.", - "Self-hosting remains deterministic in a clean clone.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:extraction.derive-graph", - specKind: "behavior", - altitude: "feature", - readiness: "ready", - file: "specs/extraction/derive-graph.sdp.md", - title: "Carrier reification derives the one graph", - narrative: - "The graph is the current projection of the repository at a commit. Git holds lifecycle history, so removed records disappear from the current graph and a current `supersedes` relation is the only forward pointer between records that still exist.", - sections: { - intent: { outcome: "Expose one carrier-neutral derivation seam." }, - behavior: { - rules: [ - "Carrier reification feeds deriveGraph once; no consumer creates a second graph.", - "The graph is flat arrays of typed nodes and edges; hierarchy and containment are expressed by edges rather than nested nodes.", - "Declared relations resolve Primitive to Primitive, while `satisfies` and test `verifies` edges derive from anchors and run from their binding node to the direct Spec target.", - "Delivery facts are computed node facts: a resolving `satisfies` edge contributes `implemented`, and an enabled direct verifier contributes `has-verifier` only to its target.", - "Inferred structural edges are advisory inputs to impact analysis and never become authoritative graph truth.", - ], - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:extraction.determinism", - specKind: "constraint", - altitude: "feature", - readiness: "ready", - file: "specs/extraction/determinism.sdp.md", - title: "Committed source derives byte-identical output", - narrative: null, - sections: { - intent: { outcome: "Make regeneration independent of location and prior generated state." }, - constraints: [ - { - flavor: "quality", - statement: - "Two clean derivations of the same committed source produce byte-identical generated trees.", - target: "sha256(tree@run1) == sha256(tree@run2)", - measurableBy: "test/cli.test.ts clean-repo determinism", - }, - ], - behavior: { - rules: [ - "Nodes sort by ID, edges sort by from, type, and to, and semantically compared output excludes wall-clock timestamps and run-specific hashes.", - "`sdp build --check-clean` repeats extraction and contract generation independently, failing on any graph or generated-contract byte divergence.", - "Static envelope fields fail extraction when they cannot be reified; optional TypeScript section detail may warn and drop, while Markdown documents refuse as a whole.", - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:extraction.excludes", - specKind: "rule", - altitude: "feature", - readiness: "ready", - file: "specs/extraction/excludes.sdp.md", - title: "Extraction exclusions are strict consumer input", - narrative: null, - sections: { - intent: { - outcome: - "Keep consumer-selected omissions precise without changing the extractor's canonical discovery rules.", - }, - behavior: { - rules: [ - "An exclusion is a unique, exact root-relative POSIX path prefix applied to both declared-carrier and anchor-candidate discovery surfaces.", - "A prefix excludes itself and slash-delimited descendants only; it never excludes a merely similar sibling path.", - "Empty, dot-relative, absolute, Windows-drive, backslash, trailing-slash, and parent-traversal paths are refused rather than normalized into a different meaning.", - "The realizing entrypoints are `normalizeExcludes` and `discoverFiles` in `src/extract/discover.ts`.", - ], - exampleSpace: { - given: [ - "the extraction root carries the tree {excludedTree:string} and the similar sibling {similarTree:string}", - "the consumer supplies the exclusion {exclusion:string}", - ], - when: ["the root is discovered"], - [["t", "hen"].join("")]: [ - 'the discovery attempt {outcome:"completes"|"is refused"}', - "the surviving spec carrier is {specCarrier:string} and the surviving anchor candidate is {anchorCandidate:string}", - "the refusal states {diagnostic:string} and names the offending path", - ], - }, - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:extraction.excludes.segment-boundary", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/extraction/excludes.segment-boundary.sdp.md", - title: "A prefix excludes its own tree and leaves a similar sibling standing", - narrative: null, - sections: { - intent: { - outcome: "Execute the segment-boundary rule across both discovery surfaces.", - }, - behavior: { - examples: [ - { - given: [ - 'the extraction root carries the tree {excludedTree: "foo"} and the similar sibling {similarTree: "foobar"}', - 'the consumer supplies the exclusion {exclusion: "foo"}', - ], - when: ["the root is discovered"], - [["t", "hen"].join("")]: [ - 'the discovery attempt {outcome: "completes"}', - 'the surviving spec carrier is {specCarrier: "foobar/included.sdp.ts"} and the surviving anchor candidate is {anchorCandidate: "foobar/helper.ts"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:extraction.excludes.refused-path", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/extraction/excludes.refused-path.sdp.md", - title: "A Windows-drive absolute path is refused rather than normalized", - narrative: null, - sections: { - intent: { - outcome: - "Execute the refusal rule on an exclusion that cannot name a root-relative prefix.", - }, - behavior: { - examples: [ - { - given: [ - 'the extraction root carries the tree {excludedTree: "foo"} and the similar sibling {similarTree: "foobar"}', - 'the consumer supplies the exclusion {exclusion: "C:/work/specs"}', - ], - when: ["the root is discovered"], - [["t", "hen"].join("")]: [ - 'the discovery attempt {outcome: "is refused"}', - 'the refusal states {diagnostic: "normalizeExcludes: invalid exclusion path"} and names the offending path', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:decisions.exclusion-contract", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/exclusion-contract.sdp.md", - title: "Consumer exclusions stay exact", - narrative: null, - sections: { - intent: { - outcome: "Keep consumer-selected omissions precise and unsurprising.", - }, - decision: { - context: "Exclusion input crosses from a consumer into canonical source discovery.", - decision: "Consumers declare exclusions as exact root-relative POSIX path prefixes.", - rationale: [ - "Semantic globbing and path normalization are rejected because they make an omission broader or different from the path the consumer supplied.", - ], - consequences: [ - "A prefix excludes only itself and slash-delimited descendants; malformed paths, including Windows-drive absolutes, are refused.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:extraction.claim-taxonomy", - specKind: "model", - altitude: "feature", - readiness: "defined", - file: "specs/extraction/claim-taxonomy.sdp.md", - title: "Graph claims retain their epistemic source", - narrative: null, - sections: { - intent: { - outcome: - "Let every graph reader distinguish authored intent, human bindings, and machine-derived structure.", - }, - model: { - terms: { - declared: - "Human intent explicitly authored in a Spec or Pack; it is authoritative intent.", - anchored: - "A human binding from a code, test, or oracle location to one Spec ID; it is authoritative binding and carries no intent.", - inferred: - "Machine-derived structural information; it is advisory and never authoritative.", - "claim inheritance": - "An edge computed from an authored source retains that source's declared claim; derivation is a mechanism, not a fourth claim.", - "delivery fact": - "A realization signal computed from resolving edges, never an authored claim or edge.", - }, - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:extraction.regenerability", - specKind: "rule", - altitude: "feature", - readiness: "defined", - file: "specs/extraction/regenerability.sdp.md", - title: "Generated artifacts are disposable projections", - narrative: null, - sections: { - intent: { - outcome: - "Keep the repository canonical while allowing every graph and projection to be rebuilt safely.", - }, - behavior: { - rules: [ - "Generated artifacts are disposable: deleting them and rebuilding from the same committed repository produces the same bytes.", - "Consumers read the graph or link to source locations recorded in it; they never re-parse source or keep a parallel model.", - "The graph is a single JSON projection with in-memory query support; a graph database remains deferred until measured traversal pain establishes a real need.", - "Measured evidence from the self-hosting corpus keeps full rebuilds comfortable below roughly 50 Specs.", - "Measured evidence defers a graph database until the graph reaches roughly 10k+ nodes or traversal pain establishes a real need.", - ], - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:extraction.schema-versioning", - specKind: "rule", - altitude: "story", - readiness: "ready", - file: "specs/extraction/schema-versioning.sdp.md", - title: "The graph declares its schema version", - narrative: null, - sections: { - intent: { - outcome: - "Let consumers identify the graph payload contract without premature migration machinery.", - }, - behavior: { - rules: [ - "Every graph declares its schemaVersion, and MVP consumers require that field to be present and readable.", - "Envelope-stable, section-extensible growth is normally additive; SemVer negotiation and a migration command remain deferred until a consumer needs them.", - "The declaring entrypoint is `schemaVersion` in `src/graph/schema.ts`, carried onto every derived payload by `deriveGraph`.", - ], - exampleSpace: { - given: ["a graph derived from the authored spec {specId:string}"], - when: ["the graph payload is serialized"], - [["t", "hen"].join("")]: [ - "the payload declares the schema version {schemaVersion:string}", - ], - }, - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:extraction.schema-versioning.declared-version", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/extraction/schema-versioning.declared-version.sdp.md", - title: "A derived payload carries a schema version its consumer can read", - narrative: null, - sections: { - intent: { - outcome: "Execute the declared-version rule over a serialized graph payload.", - }, - behavior: { - examples: [ - { - given: [ - 'a graph derived from the authored spec {specId: "spec:probe.schema-versioning"}', - ], - when: ["the graph payload is serialized"], - [["t", "hen"].join("")]: [ - 'the payload declares the schema version {schemaVersion: "0.4.0"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:extraction.executable-contracts", - specKind: "behavior", - altitude: "feature", - readiness: "ready", - file: "specs/extraction/executable-contracts.sdp.md", - title: "The build derives executable contracts from graph examples", - narrative: null, - sections: { - intent: { - outcome: - "Give bound tests typed step and example-space contracts without reading authored Specs directly.", - }, - behavior: { - rules: [ - "`generateContracts` derives per-example step contracts and per-parent space contracts solely from the extracted graph.", - "A generated contract is disposable, keyed by Spec ID, and becomes unavailable when its authored example cannot bind honestly to its shared vocabulary.", - "The concreteness law is a refusal, never a guess — an example carrying an unbound slot in any used step of any entry is not the bindable form and receives no step contract, and a prose-only example receives none either.", - "The concreteness law reads the example's own form alone, so it refuses whether or not a parent declares a shared vocabulary; vocabulary resolution is a separate, later gate whose withholding names its own finding.", - "An example is one point, so the step contract and the bound point derive from the same first complete entry; a further structured entry is named rather than left silently inert.", - "Degradation is loud and local — an undeclared slot, a value outside its declared type, and a conflicting re-binding each name the drift and drop exactly that one slot, so the emitted module still compiles.", - "A vocabulary slot group that declares no usable type is named rather than dropped in silence, and no dimension enters the space for it.", - "Two contract paths differing only by letter case cannot coexist on a case-insensitive filesystem, so the contracts tree is withheld whole and the finding names the colliding pair.", - "Every generation finding is a warning that describes what did not emit; gating belongs to graph validation alone, so a withheld contract never fails the build by itself.", - "The realizing entrypoint is `generateContracts` in `src/codegen/contracts.ts`.", - ], - exampleSpace: { - given: [ - "a parent spec whose example space declares the slot {dimension:string}", - "a parent spec that declares no shared vocabulary for the slot {dimension:string}", - 'a refining example {exampleId:string} whose used step {binding:"binds"|"leaves unbound"} that slot', - "the example carries {entryCount:number} structured entries", - "a case-twin example {twinId:string} whose contract path differs only by letter case", - ], - when: ["the contracts are generated from the derived graph"], - [["t", "hen"].join("")]: [ - "the generated tree holds {fileCount:number} files", - "the step contract for the example is emitted: {emitted:boolean}", - "the findings name {findingId:string}", - ], - }, - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:extraction.executable-contracts.concreteness-refusal", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/extraction/executable-contracts.concreteness-refusal.sdp.md", - title: "An unbound slot in a used step earns no step contract", - narrative: null, - sections: { - intent: { - outcome: - "Execute the concreteness law alone, where no shared vocabulary can withhold the contract in its place.", - }, - behavior: { - examples: [ - { - given: [ - 'a parent spec that declares no shared vocabulary for the slot {dimension: "n"}', - 'a refining example {exampleId: "spec:probe.create-order.unbound"} whose used step {binding: "leaves unbound"} that slot', - ], - when: ["the contracts are generated from the derived graph"], - [["t", "hen"].join("")]: [ - "the generated tree holds {fileCount: 0} files", - "the step contract for the example is emitted: {emitted: false}", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:extraction.executable-contracts.multi-entry-example", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/extraction/executable-contracts.multi-entry-example.sdp.md", - title: "A second structured entry is named, never left silently inert", - narrative: null, - sections: { - intent: { - outcome: - "Execute the one-point law where an example smuggles a second case into one document.", - }, - behavior: { - examples: [ - { - given: [ - 'a parent spec whose example space declares the slot {dimension: "n"}', - 'a refining example {exampleId: "spec:probe.create-order.multi"} whose used step {binding: "binds"} that slot', - "the example carries {entryCount: 2} structured entries", - ], - when: ["the contracts are generated from the derived graph"], - [["t", "hen"].join("")]: [ - "the step contract for the example is emitted: {emitted: true}", - 'the findings name {findingId: "contracts/multi-entry-example"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:extraction.executable-contracts.case-colliding-path", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/extraction/executable-contracts.case-colliding-path.sdp.md", - title: "A case-only path collision withholds the whole contracts tree", - narrative: null, - sections: { - intent: { - outcome: - "Execute the all-or-nothing rule where two examples claim one case-folded contract path.", - }, - behavior: { - examples: [ - { - given: [ - 'a parent spec whose example space declares the slot {dimension: "n"}', - 'a refining example {exampleId: "spec:probe.create-order.same-case"} whose used step {binding: "binds"} that slot', - 'a case-twin example {twinId: "spec:probe.create-order.same-Case"} whose contract path differs only by letter case', - ], - when: ["the contracts are generated from the derived graph"], - [["t", "hen"].join("")]: [ - "the generated tree holds {fileCount: 0} files", - 'the findings name {findingId: "contracts/case-colliding-path"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:extraction.example-runner", - specKind: "behavior", - altitude: "feature", - readiness: "ready", - file: "specs/extraction/example-runner.sdp.md", - title: "A bound example runs its contract steps against a fresh world", - narrative: null, - sections: { - intent: { - problem: - "A bound test must execute a Spec's own steps without the executing core learning any test framework.", - outcome: - "Run a generated contract's steps in authored order and make a red step name itself in the Spec's own words.", - value: - "A failing example reads as the Spec that failed rather than as an anonymous assertion.", - }, - behavior: { - rules: [ - "The core plans every contract step in authored order and runs it against the world the caller hands in; creating a fresh world per example is the adapter's lifecycle, never the core's.", - "Duplicate step text within one example binds one handler, and every occurrence runs that one handler with its own authored params.", - "A red step names itself before the assertion detail: the failure message leads with the step's natural reading — the Spec's own words with bound values inlined — and the original error is preserved, carried as `cause` when it cannot be re-messaged, and wrapped when the thrown value is not an error.", - "A missing or stale step handler is a compile-time refusal rather than a silent skip: the bindings type covers every step and only the steps, so spec-side drift fails the typecheck instead of the run.", - "The core contributes `unspecified`, the one outcome no Spec ever states, so an uncovered region of an example space has an honest answer rather than a manufactured one.", - "The realizing entrypoints are `planExample` and `runExamplePlan` in `src/runner/index.ts`.", - ], - exampleSpace: { - given: [ - "a contract whose given step repeats {occurrences:number} times before one when step and one then step", - 'the handler bound to the {failingPhase:"given"|"when"|"then"} step throws {thrown:string}', - ], - when: ["the bound plan runs against a fresh world"], - [["t", "hen"].join("")]: [ - "the world records the handler trace {trace:string}", - 'the run {outcome:"completes"|"fails"}', - "the failure names the step in the Spec's own words as {failureLabel:string}", - "the failure preserves the original detail {detail:string}", - ], - }, - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:extraction.example-runner.step-order", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/extraction/example-runner.step-order.sdp.md", - title: "A repeated step runs its one handler at each occurrence, in contract order", - narrative: null, - sections: { - intent: { - outcome: - "Execute the contract-order and one-handler-per-step laws over a repeating given step.", - }, - behavior: { - examples: [ - { - given: [ - "a contract whose given step repeats {occurrences: 2} times before one when step and one then step", - ], - when: ["the bound plan runs against a fresh world"], - [["t", "hen"].join("")]: [ - 'the world records the handler trace {trace: "given 2 | given 2 | when | then"}', - 'the run {outcome: "completes"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:extraction.example-runner.red-step-naming", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/extraction/example-runner.red-step-naming.sdp.md", - title: "A red step names itself before the assertion detail", - narrative: null, - sections: { - intent: { - outcome: "Execute the failure law where a bound handler throws inside the when step.", - }, - behavior: { - examples: [ - { - given: [ - "a contract whose given step repeats {occurrences: 2} times before one when step and one then step", - 'the handler bound to the {failingPhase: "when"} step throws {thrown: "boom"}', - ], - when: ["the bound plan runs against a fresh world"], - [["t", "hen"].join("")]: [ - 'the run {outcome: "fails"}', - 'the failure names the step in the Spec\'s own words as {failureLabel: "at step: When the cart is submitted"}', - 'the failure preserves the original detail {detail: "boom"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:carrier.slot-notation", - specKind: "rule", - altitude: "story", - readiness: "ready", - file: "specs/carrier/slot-notation.sdp.md", - title: "Slot notation declares, binds, and refuses to guess", - narrative: null, - sections: { - intent: { - outcome: - "Give step text one owned typed placeholder syntax whose normalized identity a generated contract can key on.", - }, - behavior: { - rules: [ - "A slot group opens with an identifier; a brace group that does not open with one is prose, and prose is never policed.", - "A vocabulary slot declares a type only in the ratified type form — `number`, `string`, `boolean`, or a closed union of two or more quoted literals — while an example binds one scalar literal in the same position.", - "The skeleton — every slot group normalized to `{name}` with prose braces left untouched — is the step's identity: it keys the generated step contract, matches an example step to its vocabulary entry, and makes a declaration and its binding the same step.", - "An identifier-led group whose remainder parses as neither a type nor a value stays a named but unusable slot: it declares nothing, binds nothing, and reads as unbound rather than being guessed into meaning.", - "The single-quoted-literal form parses as a binding, and what it would declare in a vocabulary is unruled — so a vocabulary consumer treats it as declaring nothing and says so rather than inventing a one-value dimension.", - "Lexical degradation stays local: a stray or unterminated brace group is prose only up to the next candidate, so it never swallows a well-formed binding that follows it.", - "The realizing entrypoints are `parseSlots` and `stepSkeleton` in `src/notation/slots.ts`.", - ], - exampleSpace: { - given: ["the step text {stepText:string}"], - when: ["the notation parses the step text"], - [["t", "hen"].join("")]: [ - "the notation finds {slotCount:number} slot groups", - 'the first group has the form {form:"bare"|"typed"|"bound"|"malformed"} and the name {slotName:string}', - "the step skeleton is {skeleton:string}", - ], - }, - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:carrier.slot-notation.typed-declaration", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/carrier/slot-notation.typed-declaration.sdp.md", - title: "A typed declaration normalizes to the skeleton its binding shares", - narrative: null, - sections: { - intent: { - outcome: "Execute the declaration form and the skeleton identity on one vocabulary step.", - }, - behavior: { - examples: [ - { - given: ['the step text {stepText: "a cart with {n:number} line items"}'], - when: ["the notation parses the step text"], - [["t", "hen"].join("")]: [ - "the notation finds {slotCount: 1} slot groups", - 'the first group has the form {form: "typed"} and the name {slotName: "n"}', - 'the step skeleton is {skeleton: "a cart with {n} line items"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:carrier.slot-notation.refused-guess", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/carrier/slot-notation.refused-guess.sdp.md", - title: "A stray brace stays prose while an unusable group stays a named slot", - narrative: null, - sections: { - intent: { - outcome: - "Execute the refuse-to-guess posture where a stray brace precedes an unparsable group.", - }, - behavior: { - examples: [ - { - given: ['the step text {stepText: "a stray { then {n: maybe} line items"}'], - when: ["the notation parses the step text"], - [["t", "hen"].join("")]: [ - "the notation finds {slotCount: 1} slot groups", - 'the first group has the form {form: "malformed"} and the name {slotName: "n"}', - 'the step skeleton is {skeleton: "a stray { then {n} line items"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:extraction.build-pipeline", - specKind: "workflow", - altitude: "feature", - readiness: "defined", - file: "specs/extraction/build-pipeline.sdp.md", - title: "The build pipeline has one ordered flow", - narrative: null, - sections: { - intent: { outcome: "Turn authored carriers into validated derived artifacts." }, - behavior: { - rules: ["Every command uses the same extracted graph and validation seam."], - flows: [ - "Discover carriers.", - "Reify carriers.", - "Derive the graph.", - "Validate the graph.", - "Emit derived artifacts.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:validation.readiness-floor", - specKind: "rule", - altitude: "feature", - readiness: "ready", - file: "specs/validation/readiness-floor.sdp.md", - title: "Stated readiness must clear its floor", - narrative: null, - sections: { - intent: { outcome: "Refuse maturity claims that their authored evidence does not support." }, - behavior: { - rules: [ - "A Spec may state a readiness only when every clause in that readiness floor passes.", - "The `ready` floor reads the Spec's own edges through three clauses: every authored relation resolves to a known target, every `refines` and `dependsOn` target itself stands at least `defined`, and every anchor bound to the Spec resolves.", - "The anchor clause reads the bindings that are present, so a Spec carrying no anchor clears it — the floor never demands a binding an author has not made.", - "The floor table in `src/validate/readiness-floor.ts` is the clause set's code-level source of truth and the realizing entrypoint; the clauses of the lower rungs are stated there and are not re-enumerated here.", - ], - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:validation.duplicate-ids", - specKind: "behavior", - altitude: "feature", - readiness: "ready", - file: "specs/validation/duplicate-ids.sdp.md", - title: "Duplicate carrier IDs are excluded loudly", - narrative: null, - sections: { - intent: { outcome: "Prevent ambiguous authored identity from entering the graph." }, - behavior: { - rules: [ - "If more than one carrier declares an ID, every duplicate site receives extract/duplicate-id and no ambiguous node is derived.", - ], - exampleSpace: { - given: [ - "a {firstCarrier:string} carrier declares {specId:string}", - "a {secondCarrier:string} carrier declares {specId:string}", - ], - when: ["the extraction root is read"], - [["t", "hen"].join("")]: [ - "both sites report {findingId:string}", - "no graph node is emitted for {specId:string}", - ], - }, - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:validation.duplicate-ids.dual-carrier", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/duplicate-ids.dual-carrier.sdp.md", - title: "TypeScript and Markdown duplicates are both refused", - narrative: null, - sections: { - intent: { outcome: "Execute the duplicate-ID rule across both carrier surfaces." }, - behavior: { - examples: [ - { - given: [ - 'a {firstCarrier: "TypeScript"} carrier declares {specId: "spec:fixture.duplicate"}', - 'a {secondCarrier: "Markdown"} carrier declares {specId: "spec:fixture.duplicate"}', - ], - when: ["the extraction root is read"], - [["t", "hen"].join("")]: [ - 'both sites report {findingId: "extract/duplicate-id"}', - 'no graph node is emitted for {specId: "spec:fixture.duplicate"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:model.protocol-domain", - specKind: "model", - altitude: "feature", - readiness: "defined", - file: "specs/model/protocol-domain.sdp.md", - title: "The Protocol domain uses one ratified language", - narrative: null, - sections: { - intent: { outcome: "Give self-hosting specs the same core vocabulary." }, - model: { - terms: { - Pack: "A grouping and review aggregate that states no system truth.", - Spec: "The one authored truth-primitive.", - anchor: "An in-code identity binding that states no intent.", - "delivery fact": "A machine-derived realization signal.", - }, - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.plain-language-references", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/plain-language-references.sdp.md", - title: "Durable references lead with meaning", - narrative: null, - sections: { - intent: { - outcome: "Keep design rationale readable without decoding registries.", - }, - decision: { - context: "Decision codes are useful lookup keys but poor standalone prose.", - decision: - "Durable references lead with plain-language meaning; decision codes follow parenthetically when useful.", - rationale: ["Meaning survives registry churn."], - consequences: ["AGENTS and plans lead with names."], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.concept-docs-dissolve", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/concept-docs-dissolve.sdp.md", - title: "Concept documents may dissolve after executable truth lands", - narrative: null, - sections: { - intent: { - outcome: "Keep intended truth authoritative while allowing exposition to shrink.", - }, - decision: { - context: "Concept documents currently carry both laws and unsettled representation.", - decision: - "Concept documents may dissolve only after their semantic contract is carried by executable Specs and lean registries.", - rationale: ["Executable truth is easier to validate and consume."], - consequences: [ - "Deletion follows the carrying work, per document, and is never bundled into the change that lands the carrier.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:model.core-model", - specKind: "model", - altitude: "feature", - readiness: "defined", - file: "specs/model/core-model.sdp.md", - title: "The Protocol models delivery with one enrichable Spec", - narrative: null, - sections: { - intent: { - outcome: - "Give every authored delivery statement one stable shape and independent coordinates.", - }, - model: { - terms: { - Spec: "The one authored truth-primitive, enriched in place without changing artifact type.", - altitude: "The scope position `epic`, `feature`, or `story`.", - "delivery fact": - "A derived realization signal such as implemented or has-verifier; it is never authored readiness.", - envelope: - "The stable outer shape of id, title, kind, altitude, readiness, and relations; sections carry extension detail.", - kind: "The true subtype that categorizes a Spec's truth and changes its required detail and validation.", - readiness: - "The author-stated design-maturity position `idea`, `scoped`, `defined`, or `ready`, checked against a structural floor.", - }, - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:model.spec-sections", - specKind: "model", - altitude: "feature", - readiness: "defined", - file: "specs/model/spec-sections.sdp.md", - title: "Spec sections carry typed detail and direct verifier semantics", - narrative: null, - sections: { - intent: { - outcome: - "Extend Specs with local detail without weakening their envelope or confusing binding evidence with intent.", - }, - model: { - terms: { - "content-only section": - "A section carries local content, while relations carry links to promoted standalone Specs.", - "enabled verifier": - "An example or direct test with a linked, resolvable test anchor; runner execution and pass state remain outside the graph.", - promotion: - "Moving shared or independently reviewed content into a standalone Spec of the matching kind, exclusively rather than alongside inline content.", - section: - "An optional detail slice of a Spec: intent, behavior, constraints, model, design, decision, verification, or ui.", - "typing law": - "Every section read by a readiness-floor clause has a closed typed shape; unsettled design and ui surfaces remain open bags.", - verifies: - "A direct verifier-to-target relation whose enabled test binding can derive has-verifier only for that stated target.", - }, - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:model.relations", - specKind: "model", - altitude: "feature", - readiness: "defined", - file: "specs/model/relations.sdp.md", - title: "Specs declare typed directed relations", - narrative: null, - sections: { - intent: { - outcome: - "Preserve the explicit intent links that make a delivery model navigable and queryable.", - }, - model: { - terms: { - "authored relation": "A declared, directed Spec-to-Spec edge that records human intent.", - constrainedBy: "A bounded Spec points to its rule, constraint, or policy Spec.", - decidedBy: "A shaped Spec points to its Decision Record.", - dependsOn: "A dependent Spec points to the Spec it needs.", - refines: "A child points to its more precise parent.", - supersedes: "A current Decision Record points forward to the decision it replaces.", - verifies: "A verifier points to the Spec it verifies.", - }, - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:model.stable-ids", - specKind: "rule", - altitude: "story", - readiness: "ready", - file: "specs/model/stable-ids.sdp.md", - title: "Stable IDs are the Protocol's durable join key", - narrative: null, - sections: { - intent: { - outcome: - "Keep intent, bindings, and graph nodes connected through names that survive code refactoring.", - }, - behavior: { - rules: [ - "A Protocol ID is stable, unique, namespaced, human-readable, and the only binding between intent and code.", - "An ID uses a lowercase namespace and a dotted path whose segments admit mixed case (case binds only on the namespace), with an optional single `#` sub-part; referential-integrity checks reject malformed or unresolved references.", - "IDs carry no history: a rename is a repository edit recorded by git rather than graph-resident bookkeeping.", - "The builders reserve one namespace per binding direction — `spec:` for a Spec and for every Spec reference, `pack:` for the aggregate, `impl:` · `api:` · `component:` for a code anchor, `test:` for a verifying test anchor, and `oracle:` for an expected-outcome anchor — while the grammar itself admits any lowercase namespace, so the reserved set is the builders' law rather than the parser's.", - "`doc:` is reserved for a genuinely external document a decision Spec links to, never for an in-system decision: in-system decisions are Specs under the `spec:decisions.*` convention. No builder mints a `doc:` identifier and the Spec-only reference builder refuses one, so the reservation is a named deferral rather than a landed namespace.", - "The realizing entrypoints are `parseId` and `formatId` in `src/ids.ts`.", - ], - exampleSpace: { - given: ["the authored identifier {identifier:string}"], - when: ["the identifier is parsed"], - [["t", "hen"].join("")]: [ - 'parsing {outcome:"resolves"|"is refused"}', - "reformatting the parsed parts restores {restored:string}", - "the refusal names the reason {reason:string}", - ], - }, - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:model.stable-ids.namespaced-round-trip", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/model/stable-ids.namespaced-round-trip.sdp.md", - title: "A namespaced dotted path with a sub-part survives parsing unchanged", - narrative: null, - sections: { - intent: { - outcome: "Execute the ID grammar on the fullest well-formed shape the model allows.", - }, - behavior: { - examples: [ - { - given: ['the authored identifier {identifier: "spec:orders.create-order#valid-cart"}'], - when: ["the identifier is parsed"], - [["t", "hen"].join("")]: [ - 'parsing {outcome: "resolves"}', - 'reformatting the parsed parts restores {restored: "spec:orders.create-order#valid-cart"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:model.stable-ids.malformed-refusal", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/model/stable-ids.malformed-refusal.sdp.md", - title: "An uppercase namespace is refused with its reason named", - narrative: null, - sections: { - intent: { - outcome: "Execute the lowercase-namespace clause of the ID grammar.", - }, - behavior: { - examples: [ - { - given: ['the authored identifier {identifier: "Spec:orders.create-order"}'], - when: ["the identifier is parsed"], - [["t", "hen"].join("")]: [ - 'parsing {outcome: "is refused"}', - 'the refusal names the reason {reason: "namespace must be lowercase"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:model.pack-aggregate", - specKind: "model", - altitude: "story", - readiness: "defined", - file: "specs/model/pack-aggregate.sdp.md", - title: "A Pack is a truth-free review aggregate", - narrative: null, - sections: { - intent: { - outcome: - "Let reviewers group related Specs without introducing a second truth-bearing artifact.", - }, - model: { - terms: { - Pack: "An authored aggregate that groups related Specs for ideation and review while stating no system truth of its own.", - framing: "A plain descriptive note explaining why a Pack exists; it is not Spec intent.", - membership: - "A declared manifest reference that derives a belongsTo edge; a Spec may belong to many Packs.", - modelRefs: - "References from a Pack to standalone model Specs that carry shared vocabulary.", - refinement: - "A truth-bearing parent-child relation, distinct from the cross-cutting Pack aggregate.", - }, - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:model.anchors", - specKind: "model", - altitude: "feature", - readiness: "ready", - file: "specs/model/anchors.sdp.md", - title: "Source anchors bind code without carrying intent", - narrative: null, - sections: { - intent: { - outcome: - "Connect implementation, tests, and oracles to Specs while keeping authored intent centralized in the carrier.", - }, - behavior: { - exampleSpace: { - given: [ - 'a repository whose one source file builds an anchor through {builderSource:"a consumer-local lookalike module"|"a relative import resolving to the Protocol builder modules"|"the published Protocol package"}', - ], - when: ["the repository is extracted"], - [["t", "hen"].join("")]: [ - "the extraction mints {anchorCount:number} anchors", - "the extraction reports {findingCount:number} findings", - ], - }, - }, - model: { - terms: { - "Protocol builder binding": - "A builder import from the public Protocol package, or a relative import whose importer-relative resolution — including the TypeScript `.js`-to-`.ts` convention — canonicalizes to this package's `ids` or `model/code-anchor` module; consumer-local lookalike modules confer no binding authority. On the CommonJS package surface the trusted relative-module set is empty (`import.meta.url` is rewritten away), so relative bindings mint no anchors there while package imports stay trusted.", - anchor: - "A human-written source binding from one code location to one Spec ID, carrying identity, an optional label, and one target only.", - "anchor-constant form": - "The top-level const builder call that the MVP extractor reifies; decorator and JSDoc forms remain unextracted representations.", - "code anchor": - "An implementation-flavored binding that derives an anchored satisfies edge.", - "oracle anchor": - "A binding that records an oracle's models target without deriving a delivery fact.", - "test anchor": - "A binding that derives an anchored verifies edge from a test to its target Spec.", - "untrusted builder": - "A builder call whose import is no Protocol builder binding: it mints nothing and reports nothing, because a source file that never bound to the Protocol is not authoring drift to report. The realizing entrypoints are `protocolBindingScopeFor` and `collectProtocolBindings` in `src/extract/protocol-bindings.ts`.", - }, - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:model.anchors.lookalike-refusal", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/model/anchors.lookalike-refusal.sdp.md", - title: "A consumer-local lookalike builder mints no anchor and no finding", - narrative: null, - sections: { - intent: { - outcome: - "Execute the builder-trust law where a repository's own module merely resembles the Protocol builders.", - }, - behavior: { - examples: [ - { - given: [ - 'a repository whose one source file builds an anchor through {builderSource: "a consumer-local lookalike module"}', - ], - when: ["the repository is extracted"], - [["t", "hen"].join("")]: [ - "the extraction mints {anchorCount: 0} anchors", - "the extraction reports {findingCount: 0} findings", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:model.anchors.physical-identity", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/model/anchors.physical-identity.sdp.md", - title: "A deep relative import that resolves to the Protocol builders is trusted", - narrative: null, - sections: { - intent: { - outcome: - "Execute the builder-trust law where trust turns on physical module identity rather than the import's spelling.", - }, - behavior: { - examples: [ - { - given: [ - 'a repository whose one source file builds an anchor through {builderSource: "a relative import resolving to the Protocol builder modules"}', - ], - when: ["the repository is extracted"], - [["t", "hen"].join("")]: [ - "the extraction mints {anchorCount: 1} anchors", - "the extraction reports {findingCount: 0} findings", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.two-check-families", - specKind: "rule", - altitude: "feature", - readiness: "ready", - file: "specs/validation/two-check-families.sdp.md", - title: "Validation separates well-formedness from non-pretending", - narrative: null, - sections: { - intent: { - outcome: - "Keep the graph trustworthy by checking conformance and honesty without judging content quality or enforcing workflow.", - }, - behavior: { - rules: [ - "Every validator belongs to either the conformance family, which checks meta-model well-formedness, or the honesty family, which rejects authored or overstated derived truth.", - "Validation errors fail the build; gaps and orphans remain informative signals rather than delivery-process gates.", - "Types enforce structural shape, schema validates graph payloads, and graph validators enforce cross-file conformance and honesty; no one layer substitutes for the others.", - "All graph validation runs through the one derived graph path: source, extraction, graph, then checks.", - "The two families are load-bearing, so an aggregate report spanning both states no family of its own while every finding names the family it came from.", - "The realizing entrypoints are `graphValidatorIds` and `validateGraph` in `src/validate/validators.ts`.", - ], - exampleSpace: { - given: [ - 'the graph holds a spec {specId:string} at readiness {readiness:"idea"|"ready"}', - "the spec declares a dependsOn relation to the absent target {targetId:string}", - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - "the aggregate report states no family of its own", - 'the conformance family reports {conformanceId:string} at severity {conformanceSeverity:"warning"|"error"}', - 'the honesty family reports {honestyId:string} at severity {honestySeverity:"warning"|"error"}', - ], - }, - }, - }, - deliveryFacts: ["implemented", "has-verifier"], - }, - { - id: "spec:validation.two-check-families.split-report", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/two-check-families.split-report.sdp.md", - title: "One report carries both families and claims neither as its own", - narrative: null, - sections: { - intent: { - outcome: - "Execute the family split where one probe graph trips a conformance error and an informative honesty signal at once; the same dangling relation also fails the readiness floor on the ready probe, so the family assertions read by containment.", - }, - behavior: { - examples: [ - { - given: [ - 'the graph holds a spec {specId: "spec:probe.two-check-families"} at readiness {readiness: "ready"}', - 'the spec declares a dependsOn relation to the absent target {targetId: "spec:probe.absent-dependency"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - "the aggregate report states no family of its own", - 'the conformance family reports {conformanceId: "conformance/referential-integrity"} at severity {conformanceSeverity: "error"}', - 'the honesty family reports {honestyId: "honesty/gaps"} at severity {honestySeverity: "warning"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.referential-integrity", - specKind: "rule", - altitude: "story", - readiness: "ready", - file: "specs/validation/referential-integrity.sdp.md", - title: "Every graph reference resolves", - narrative: null, - sections: { - intent: { - outcome: - "Keep derived graph relationships trustworthy by refusing references to absent nodes.", - }, - behavior: { - rules: [ - "Every edge endpoint and every Pack model reference must resolve to a node in the derived graph; an unresolved reference is a conformance error.", - "The finding names the unique nearest known id as a suggestion and stays silent when two candidates tie, because resolving ambiguity silently is never the check's job.", - "The realizing validator entrypoint is `checkReferentialIntegrity` in `src/validate/validators.ts`.", - ], - exampleSpace: { - given: [ - "the graph holds one spec {presentId:string}", - "the spec declares a dependsOn relation to {targetId:string}", - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId:string} at severity {severity:"warning"|"error"}', - "the finding offers the nearest-id suggestion: {suggested:boolean}", - ], - }, - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.referential-integrity.dangling-target", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/referential-integrity.dangling-target.sdp.md", - title: "An unrelated missing target is a bare conformance error", - narrative: null, - sections: { - intent: { - outcome: "Execute the unresolved-reference law where no known id is near the missing one.", - }, - behavior: { - examples: [ - { - given: [ - 'the graph holds one spec {presentId: "spec:probe.create-order"}', - 'the spec declares a dependsOn relation to {targetId: "spec:probe.fulfilment-policy"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "conformance/referential-integrity"} at severity {severity: "error"}', - "the finding offers the nearest-id suggestion: {suggested: false}", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.referential-integrity.did-you-mean", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/referential-integrity.did-you-mean.sdp.md", - title: "A unique near miss earns a did-you-mean suggestion", - narrative: null, - sections: { - intent: { - outcome: "Execute the unresolved-reference law where exactly one known id is a near miss.", - }, - behavior: { - examples: [ - { - given: [ - 'the graph holds one spec {presentId: "spec:probe.create-order"}', - 'the spec declares a dependsOn relation to {targetId: "spec:probe.create-ordr"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "conformance/referential-integrity"} at severity {severity: "error"}', - "the finding offers the nearest-id suggestion: {suggested: true}", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.claim-separation", - specKind: "rule", - altitude: "story", - readiness: "ready", - file: "specs/validation/claim-separation.sdp.md", - title: "Graph claims and contracts stay distinct", - narrative: null, - sections: { - intent: { - outcome: - "Preserve the graph's declared, anchored, and inferred distinctions while keeping its typed contracts lawful.", - }, - behavior: { - rules: [ - "Node and edge types, claims, descriptors, and relation endpoint contracts must use their ratified forms; the claim taxonomy never collapses.", - "An unratified descriptor value fails closed: it is a conformance error, and no readiness floor is evaluated over it.", - "The realizing validator entrypoint is `checkClaimSeparation` in `src/validate/validators.ts`.", - ], - exampleSpace: { - given: [ - "the graph holds a spec {specId:string}", - 'the graph carries an off-contract {element:"edge claim"|"descriptor value"} spelled {value:string}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId:string} at severity {severity:"warning"|"error"}', - "the finding message states {phrase:string}", - "the report holds {floorCount:number} readiness-floor findings", - ], - }, - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.claim-separation.collapsed-edge-claim", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/claim-separation.collapsed-edge-claim.sdp.md", - title: "A binding edge cannot borrow the declared claim", - narrative: null, - sections: { - intent: { - outcome: "Execute the edge-contract law where a satisfies edge carries the authored claim.", - }, - behavior: { - examples: [ - { - given: [ - 'the graph holds a spec {specId: "spec:probe.create-order"}', - 'the graph carries an off-contract {element: "edge claim"} spelled {value: "declared"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "conformance/claim-separation"} at severity {severity: "error"}', - 'the finding message states {phrase: "never collapsed"}', - "the report holds {floorCount: 0} readiness-floor findings", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.claim-separation.unratified-descriptor", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/claim-separation.unratified-descriptor.sdp.md", - title: "An unratified kind fails closed instead of reaching the floor", - narrative: null, - sections: { - intent: { - outcome: - "Execute the descriptor law where a foreign producer states a kind the model never ratified.", - }, - behavior: { - examples: [ - { - given: [ - 'the graph holds a spec {specId: "spec:probe.create-order"}', - 'the graph carries an off-contract {element: "descriptor value"} spelled {value: "saga"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "conformance/claim-separation"} at severity {severity: "error"}', - 'the finding message states {phrase: "outside the ratified descriptor values"}', - "the report holds {floorCount: 0} readiness-floor findings", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.verification-linkage", - specKind: "rule", - altitude: "feature", - readiness: "ready", - file: "specs/validation/verification-linkage.sdp.md", - title: "Declared verification resolves to a performing trace", - narrative: null, - sections: { - intent: { - outcome: - "Keep verification relationships meaningful by requiring declared test and oracle traces to resolve to their enabled bindings.", - }, - behavior: { - rules: [ - "A declared verifies relation and an oracle model relation must resolve through their respective binding traces before either can stand as verification evidence.", - "A non-resolving trace is named loudly and confers no delivery fact, because silence would read as verification the graph never earned.", - "At most one expected-outcome authority may model an example space: a second resolving oracle binding on the same space is an error, because two authorities leave the modeled outcome ambiguous.", - "The realizing validator entrypoints are `checkVerifiesLinkage` and `checkOracleLinkage` in `src/validate/validators.ts`.", - ], - exampleSpace: { - given: [ - "the graph holds a parent spec {parentId:string}", - 'a non-resolving {verifierKind:"example spec"|"oracle anchor"} named {verifierId:string} points at it', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId:string} at severity {severity:"warning"|"error"}', - "the parent earns the delivery fact has-verifier: {conferred:boolean}", - ], - }, - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.verification-linkage.unbound-example", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/verification-linkage.unbound-example.sdp.md", - title: "A declared verifier no test binds confers nothing", - narrative: null, - sections: { - intent: { - outcome: - "Execute the verifies-linkage law where no test anchor completes the spec-to-test trace.", - }, - behavior: { - examples: [ - { - given: [ - 'the graph holds a parent spec {parentId: "spec:probe.create-order"}', - 'a non-resolving {verifierKind: "example spec"} named {verifierId: "spec:probe.create-order.valid-cart"} points at it', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "conformance/verifies-linkage"} at severity {severity: "warning"}', - "the parent earns the delivery fact has-verifier: {conferred: false}", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.verification-linkage.unresolved-oracle", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/verification-linkage.unresolved-oracle.sdp.md", - title: "An oracle with no example space to model confers nothing", - narrative: null, - sections: { - intent: { - outcome: "Execute the oracle-linkage law where the modeled spec owns no example space.", - }, - behavior: { - examples: [ - { - given: [ - 'the graph holds a parent spec {parentId: "spec:probe.order-policy"}', - 'a non-resolving {verifierKind: "oracle anchor"} named {verifierId: "oracle:probe.order-policy"} points at it', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "conformance/oracle-linkage"} at severity {severity: "error"}', - "the parent earns the delivery fact has-verifier: {conferred: false}", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.pack-coherence", - specKind: "rule", - altitude: "story", - readiness: "ready", - file: "specs/validation/pack-coherence.sdp.md", - title: "Packs are coherent aggregates", - narrative: null, - sections: { - intent: { - outcome: - "Keep review aggregates coherent without treating them as truth-bearing delivery artifacts.", - }, - behavior: { - rules: [ - "Pack membership must not repeat a Spec, and every modelRef must resolve to a model-kind Spec.", - "Membership is counted on the derived belongsTo edges the manifest re-expresses, so a repeated manifest entry is named once per repeated member.", - "The realizing validator entrypoint is `checkPackCoherence` in `src/validate/validators.ts`.", - ], - exampleSpace: { - given: [ - "a pack {packId:string} lists the spec {specId:string} {memberCount:number} times", - "the pack also names that spec as a modelRef", - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId:string} at severity {severity:"warning"|"error"}', - "the report holds {findingCount:number} pack-coherence findings", - ], - }, - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.pack-coherence.incoherent-aggregate", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/pack-coherence.incoherent-aggregate.sdp.md", - title: "A repeated member and a non-model modelRef are both named", - narrative: null, - sections: { - intent: { - outcome: "Execute both halves of the pack law against one incoherent aggregate.", - }, - behavior: { - examples: [ - { - given: [ - 'a pack {packId: "pack:probe.checkout"} lists the spec {specId: "spec:probe.create-order"} {memberCount: 2} times', - "the pack also names that spec as a modelRef", - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "conformance/pack-coherence"} at severity {severity: "error"}', - "the report holds {findingCount: 2} pack-coherence findings", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.authored-honesty", - specKind: "rule", - altitude: "feature", - readiness: "ready", - file: "specs/validation/authored-honesty.sdp.md", - title: "Machine truth is never authored", - narrative: null, - sections: { - intent: { - outcome: - "Keep derived graph truth trustworthy by rejecting any authored substitute for machine-derived claims or facts.", - }, - behavior: { - rules: [ - "Specs and Packs must not author derived edges, claims, or delivery facts, and any stated delivery facts must equal the graph's recomputed facts.", - "The realizing validator entrypoints are `checkAuthoringShape` and `checkDeliveryFacts` in `src/validate/validators.ts`.", - ], - exampleSpace: { - given: [ - "the graph holds a spec {specId:string}", - 'the spec hand-authors the delivery fact {factName:"implemented"|"has-verifier"} at {site:"a behavior section carrier"|"the node deliveryFacts array"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId:string} at severity {severity:"warning"|"error"}', - "the finding names the fact {relatedId:string} and states {phrase:string}", - ], - }, - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.authored-honesty.section-authored-fact", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/authored-honesty.section-authored-fact.sdp.md", - title: "A delivery fact smuggled into a section is refused", - narrative: null, - sections: { - intent: { - outcome: - "Execute the authoring-shape refusal on a section carrier that names a derived fact.", - }, - behavior: { - examples: [ - { - given: [ - 'the graph holds a spec {specId: "spec:probe.smuggled-fact"}', - 'the spec hand-authors the delivery fact {factName: "implemented"} at {site: "a behavior section carrier"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "honesty/authoring-shape"} at severity {severity: "error"}', - 'the finding names the fact {relatedId: "implemented"} and states {phrase: "derived, never authored"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.authored-honesty.unearned-stated-fact", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/authored-honesty.unearned-stated-fact.sdp.md", - title: "A stated delivery fact no binding earns is refused", - narrative: null, - sections: { - intent: { - outcome: - "Execute the delivery-fact refusal where the stated array outruns the recomputed facts.", - }, - behavior: { - examples: [ - { - given: [ - 'the graph holds a spec {specId: "spec:probe.unearned-fact"}', - 'the spec hand-authors the delivery fact {factName: "has-verifier"} at {site: "the node deliveryFacts array"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "honesty/delivery-facts"} at severity {severity: "error"}', - 'the finding names the fact {relatedId: "has-verifier"} and states {phrase: "derived, never authored"}', - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.warn-level-signals", - specKind: "rule", - altitude: "feature", - readiness: "ready", - file: "specs/validation/warn-level-signals.sdp.md", - title: "Missing connective evidence warns without failing", - narrative: null, - sections: { - intent: { - outcome: - "Surface graph conditions that need attention without turning informative delivery signals into workflow gates.", - }, - behavior: { - rules: [ - "Orphaned Specs and ready Specs lacking a resolving verifier are warnings, not validation errors.", - "The realizing validator entrypoints are `checkOrphans` and `checkGaps` in `src/validate/validators.ts`.", - ], - exampleSpace: { - given: [ - 'the graph holds a spec {specId:string} at readiness {readiness:"idea"|"ready"}', - 'the spec declares {relations:"no relation"|"a decidedBy decision"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId:string} at severity {severity:"warning"|"error"}', - "the report holds {errorCount:number} errors", - ], - }, - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.warn-level-signals.orphan-signal", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/warn-level-signals.orphan-signal.sdp.md", - title: "A disconnected spec warns and fails nothing", - narrative: null, - sections: { - intent: { outcome: "Execute the orphan signal on a spec no relation reaches." }, - behavior: { - examples: [ - { - given: [ - 'the graph holds a spec {specId: "spec:probe.orphan-signal"} at readiness {readiness: "idea"}', - 'the spec declares {relations: "no relation"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "conformance/orphans"} at severity {severity: "warning"}', - "the report holds {errorCount: 0} errors", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:validation.warn-level-signals.ready-gap-signal", - specKind: "example", - altitude: "story", - readiness: "ready", - file: "specs/validation/warn-level-signals.ready-gap-signal.sdp.md", - title: "A ready spec without a verifier warns and fails nothing", - narrative: null, - sections: { - intent: { - outcome: "Execute the gap signal on a connected ready spec no verifier resolves.", - }, - behavior: { - examples: [ - { - given: [ - 'the graph holds a spec {specId: "spec:probe.gap-signal"} at readiness {readiness: "ready"}', - 'the spec declares {relations: "a decidedBy decision"}', - ], - when: ["the graph is validated"], - [["t", "hen"].join("")]: [ - 'the report names {findingId: "honesty/gaps"} at severity {severity: "warning"}', - "the report holds {errorCount: 0} errors", - ], - }, - ], - }, - }, - deliveryFacts: ["has-verifier"], - }, - { - id: "spec:consumers.projections-model", - specKind: "model", - altitude: "feature", - readiness: "defined", - file: "specs/consumers/projections-model.sdp.md", - title: "Projections fan out from one graph without becoming truth stores", - narrative: null, - sections: { - intent: { - outcome: - "Give agents and humans consumer-specific views while preserving the repository as the only canonical source.", - }, - model: { - terms: { - baseline: - "A named approved snapshot whose signed git tag is the approval artifact, with approval remaining outside the authored model.", - "curated graph": - "The authored architectural read model of declared intent and anchored bindings, valued for editorial sparsity.", - curation: - "The deliberate difference between the sparse curated graph and the code-structure surface; it is not drift.", - discipline: - "A lens or projection that filters or groups Specs by kind or section; it is not a phase to pass through.", - "measured curation": - "In a measured comparison, the curated graph selected from single-digit to about one quarter of the mechanical impact-graph surface.", - "impact graph": - "A separately derived code-structure surface for exhaustive usage and blast-radius questions, valued for exhaustiveness and never promoted into architecture.", - "phase / iteration / milestone": - "Descriptive vocabulary for optional roadmap projections, never gates or enforced sequences.", - projection: - "A pure, disposable, regenerable function of the graph that produces a consumer artifact without becoming a second source of truth.", - reader: - "The thin typed front door that decodes graph joins and taxonomy once, returns composable data, and persists nothing.", - release: "A tagged set surfaced as a git-tag projection.", - }, - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:consumers.agent-surface", - specKind: "behavior", - altitude: "feature", - readiness: "defined", - file: "specs/consumers/agent-surface.sdp.md", - title: "Agents script a visible typed graph", - narrative: null, - sections: { - intent: { - outcome: - "Let an agent obtain and compose graph context without rebuilding joins or navigating a fixed verb wall.", - }, - behavior: { - rules: [ - "The agent surface exposes a visible, self-describing typed graph through the CLI; the schema is the contract and agents script the graph directly.", - "The reader constructs decoded joins and claim taxonomy once, then returns plain composable data without persisting graph state.", - "Entry adapters bridge strings, files, and changesets to curated graph context; file-level blast radius names coverage-unknown files rather than implying exhaustive reach.", - "Context efficiency is an empirical result: a measured comparison may show structured graph context uses fewer supplied tokens than a comparable raw-text workflow while preserving the task-relevant result.", - "Measured evidence: a multi-probe agent comparison used about one fifth of the tokens of a comparable grep or verb-API workflow while preserving task-relevant conclusions.", - ], - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:consumers.design-review", - specKind: "behavior", - altitude: "feature", - readiness: "defined", - file: "specs/consumers/design-review.sdp.md", - title: "Design Review renders graph context without becoming a gate", - narrative: null, - sections: { - intent: { - outcome: - "Give a human a regenerable, contextual view for deciding how to state readiness without recording approval as graph truth.", - }, - behavior: { - rules: [ - "Design Review renders a Spec or Pack in context with relations, bindings, delivery badges, design questions, and findings from the graph.", - "The review is a pure projection that resolves through ordinary source edits, git, and conformance checks; it stores no findings and writes no canonical source.", - "A human may use the review context when stating readiness, while validators check only the structural readiness floor and never record or require review approval.", - "The MVP view is deterministic generated Markdown with an index and pages for Specs and Packs; richer visual representations remain outside this behavior.", - "Rendering encodes by Markdown syntax context: prose and table fields escape structural characters, fenced JSON preserves authored keys and values through JSON encoding, and inline code uses a delimiter that preserves literal backticks.", - ], - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:consumers.reader", - specKind: "behavior", - altitude: "feature", - readiness: "defined", - file: "specs/consumers/reader.sdp.md", - title: "The reader bridges agent entry points to composable graph context", - narrative: null, - sections: { - intent: { - outcome: - "Let agents enter the curated graph from the strings, files, and changesets they already have without rebuilding its joins or taxonomy.", - }, - behavior: { - rules: [ - "`createReader` constructs a fresh thin typed loader that decodes graph joins, claims, delivery facts, derived readiness, and validation findings once, then returns plain composable data without persisting state.", - "`findByConcept` and `byFile` bridge strings and extraction-root-relative files to the graph's recorded context.", - "The reader's `blastRadius` surface maps changed files to directly impacted Specs and Packs, their explicit one-hop at-risk neighbors, and every coverage-unknown file.", - "File-level blast radius reports curated graph reach without claiming exhaustive symbol-level usage reach.", - ], - }, - }, - deliveryFacts: ["implemented"], - }, - { - id: "spec:consumers.edit-model", - specKind: "behavior", - altitude: "feature", - readiness: "defined", - file: "specs/consumers/edit-model.sdp.md", - title: "Views compose scoped intent instead of patching canonical source", - narrative: null, - sections: { - intent: { - outcome: - "Let a view frame a requested change without giving derived surfaces a direct write path to canonical source.", - }, - behavior: { - rules: [ - "A view composes scoped intent, bounded by a Spec, its neighbors, a Pack, or open questions, and hands that intent to an agent.", - "The agent edits source as a human would, git records the ordinary edit, and the same conformance and honesty checks evaluate it.", - "Lifecycle changes such as splitting, combining, refining, or deleting are ordinary source and git edits rather than structured patches from a derived view.", - "No single realizing entrypoint exists for intent composition; this defined behavior records design intent and has no code anchor or verifier.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.one-validation-path", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/one-validation-path.sdp.md", - title: "Validation follows one graph", - narrative: null, - sections: { - intent: { - outcome: - "Keep conformance and honesty checks aligned with the source the graph actually represents.", - }, - decision: { - context: - "Source can be statically reified without matching what an executing import would evaluate.", - decision: - "Validators consume the derived graph through one path: source, extraction, graph, then checks.", - rationale: [ - "A parallel import-time validation path can approve values absent from the graph.", - ], - consequences: [ - "Typed authoring feedback and extraction findings remain distinct from graph validation rather than becoming a second validator.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.sdp-ts-extension", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/sdp-ts-extension.sdp.md", - title: "Spec extensions identify the carrier without colliding with tests", - narrative: null, - sections: { - intent: { - outcome: - "Keep authored Spec files recognizable to tools and safe beside ordinary test conventions.", - }, - decision: { - context: - "A carrier filename must distinguish authored Specs from test files and remain useful when files are colocated.", - decision: - "Markdown Specs use `.sdp.md`; `.sdp.ts` names the surviving TypeScript DSL import source and lawful per-ID option.", - rationale: [ - "Test-glob extensions and path-only conventions either misclassify Specs or hide their identity.", - ], - consequences: [ - "Carrier-specific tooling can target the compound extension without changing the `Spec` model name.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.point-per-example", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/point-per-example.sdp.md", - title: "Each example binds one point", - narrative: null, - sections: { - intent: { - outcome: - "Keep example-space coverage and outcome witnesses unambiguous while preserving compact authoring views.", - }, - decision: { - context: "A single example must remain one witness in its parent's typed example space.", - decision: - "An example binds exactly one point; table syntax may expand statically into sibling examples and renderers may project siblings as a table.", - rationale: [ - "Point sets make concreteness and witness semantics conditional, while banning table sugar taxes a surface layer that can translate honestly.", - ], - consequences: [ - "The graph never stores multi-point examples even when a carrier offers tabular authoring.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.carrier-ruling", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/carrier-ruling.sdp.md", - title: "Markdown is the default Spec carrier", - narrative: null, - sections: { - intent: { - outcome: - "Give every Spec kind one readable canonical authoring surface without losing a lawful escape hatch.", - }, - decision: { - context: - "The carrier must express all Spec kinds without creating an unbounded tooling obligation or a dual-source truth path.", - decision: - "Specs default to Markdown; Packs remain TS until a Pack syntax ruling; the TS DSL survives as import source and a lawful per-ID option.", - rationale: [ - "An owned grammar and a permanent kind split both add surface cost without a demonstrated expressive gain, while retiring the DSL removes a useful bounded option.", - ], - consequences: [ - "Each ID has one canonical surface, and Markdown tooling is the default path for authored Specs.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.prose-ownership", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/prose-ownership.sdp.md", - title: "Prose belongs to typed graph owners", - narrative: null, - sections: { - intent: { - outcome: - "Preserve free prose for projections without making its attachment ambiguous or forcing consumers to re-parse files.", - }, - decision: { - context: "Document prose needs a stable graph home when section structure evolves.", - decision: - "Free prose is stored as a narrative or a description on its typed owner; unowned prose is refused.", - rationale: [ - "File pointers force consumer re-parsing, while heading-path keys make churned document structure carry identity.", - ], - consequences: [ - "Prose remains graph content inside typed shapes and ambiguous attachment fails loudly.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.envelope-grammar-posture", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/envelope-grammar-posture.sdp.md", - title: "The Protocol owns the envelope grammar", - narrative: null, - sections: { - intent: { - outcome: - "Keep authored envelope meaning stable while retaining a replaceable parsing representation.", - }, - decision: { - context: "YAML parsing behavior alone cannot define the Protocol's authored contract.", - decision: - "The Protocol owns a bounded envelope grammar and parser policy; the pinned YAML library is a swappable representation behind that contract.", - rationale: [ - "Permissive parsing lets library behavior define meaning, while an owned YAML parser recreates the rejected grammar-maintenance burden.", - ], - consequences: [ - "Unsupported YAML constructs are refused within explicit resource bounds instead of silently becoming carrier semantics.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.executable-meta-model", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/executable-meta-model.sdp.md", - title: "The Protocol is an executable meta-model", - narrative: null, - sections: { - intent: { outcome: "Make delivery intent conform to one typed, self-validating contract." }, - decision: { - context: "Delivery tools can describe work without making their model executable.", - decision: - "The Protocol models authored Specs, Packs, and anchors in typed code, derives one graph, and checks conformance and honesty.", - rationale: ["Executable specs alone and workflow tooling omit the meta-model contract."], - consequences: [ - "The Protocol is deterministically validated without judging content quality or enforcing workflow.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.adopt-the-nouns", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/adopt-the-nouns.sdp.md", - title: "Delivery nouns remain familiar without workflow gates", - narrative: null, - sections: { - intent: { - outcome: - "Keep the Protocol legible to delivery practitioners without adopting a lifecycle machine.", - }, - decision: { - context: - "Shared delivery vocabulary is useful, but process-state language hides epistemic distinctions.", - decision: - "The Protocol adopts established delivery nouns and rejects process state-machine and lifecycle gating.", - rationale: [ - "Invented terminology taxes users, while workflow states reverse the Protocol's conformance-only boundary.", - ], - consequences: [ - "Terms must be concrete, unambiguous, and carry authored-versus-derived status where it matters.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.one-primitive", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/one-primitive.sdp.md", - title: "One Spec carries named delivery coordinates", - narrative: null, - sections: { - intent: { - outcome: - "Preserve one durable authored primitive while making familiar delivery forms precise.", - }, - decision: { - context: - "Delivery statements vary by truth category, scope, and maturity without needing separate artifact types.", - decision: - "A Spec is enriched in place with kind, altitude, and readiness; familiar delivery nouns are named coordinates on that primitive.", - rationale: [ - "Separate types per coordinate combination multiply shapes and break enrich-in-place identity.", - ], - consequences: ["Domains and capabilities are projections or Packs, not extra altitudes."], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.protocol-naming", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/protocol-naming.sdp.md", - title: "The meta-model is a software delivery protocol", - narrative: null, - sections: { - intent: { outcome: "Name the product and its meta-layer without implying workflow control." }, - decision: { - context: - "The meta-layer needs a name that communicates a conformance contract rather than a process engine.", - decision: - "The product is the Libar Software Delivery Protocol, shortened to the Protocol; `sdp` names its CLI.", - rationale: [ - "Protocol names an executable conformance contract more honestly than process while retaining process for the modeled activity.", - ], - consequences: [ - "Product, package, repository, and CLI names stay aligned around the Protocol.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.binding-not-liveness", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/binding-not-liveness.sdp.md", - title: "Bindings state existence, not liveness", - narrative: null, - sections: { - intent: { - outcome: "Make realization signals useful without overstating what source bindings prove.", - }, - decision: { - context: - "Anchors can resolve code and tests without proving reachability, execution, or approval.", - decision: - "Delivery facts record bindings and enabled verifier existence; coverage gaps and human readiness practice remain explicit without becoming graph facts.", - rationale: [ - "Renaming useful delivery facts or recording approval primitives either weakens drift signals or reverses the one-primitive boundary.", - ], - consequences: [ - "Impact reports name coverage-unknown files and `ready` remains a declared statement above a structural floor.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.content-only-sections", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/content-only-sections.sdp.md", - title: "Sections carry content while relations carry links", - narrative: null, - sections: { - intent: { - outcome: "Keep inline detail and promoted Specs from representing the same fact twice.", - }, - decision: { - context: - "Behavior content can mature from prose to structured evidence or into a standalone matching-kind Spec.", - decision: - "Sections contain local content only; promotion moves content exclusively and relations state the linkage.", - rationale: [ - "Reference unions and duplicate parent lists force consumers to branch and leave double-linkage drift legal.", - ], - consequences: [ - "Promoted children preserve readiness evidence through their own content and authored relations.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.typing-law", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/typing-law.sdp.md", - title: "Floor-read sections are closed typed shapes", - narrative: null, - sections: { - intent: { - outcome: - "Give authors guardrails exactly where readiness and honesty checks depend on section content.", - }, - decision: { - context: "A fixed list of typed sections becomes stale when the readiness floor evolves.", - decision: - "Every section read by a floor clause has a closed typed shape; unsettled design and ui surfaces remain open.", - rationale: [ - "Closed shapes block authored-fact smuggling and provide useful authoring guidance without prematurely fixing unsettled surfaces.", - ], - consequences: [ - "A newly floor-read section becomes typed by the criterion, not by a frozen list.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.kind-conditional-floor", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/kind-conditional-floor.sdp.md", - title: "Readiness evidence follows the Spec kind", - narrative: null, - sections: { - intent: { - outcome: - "Make stated readiness structurally honest without turning the floor into a quota.", - }, - decision: { - context: - "Kinds have different natural evidence, while structural maturity clauses apply across every Spec.", - decision: - "The readiness floor combines cumulative kind-blind clauses with one kind-conditional evidence clause at each rung.", - rationale: [ - "Defined-only evidence and uniform evidence rules either leave padding legal or erase meaningful kind distinctions.", - ], - consequences: [ - "Floor rows are monotonic, promotion-neutral, and converge honestly where a kind has no stronger form.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.carried-evidence", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/carried-evidence.sdp.md", - title: "Promoted evidence must carry its own evidence", - narrative: null, - sections: { - intent: { - outcome: - "Prevent empty promoted Specs and relation targets from satisfying an evidence floor.", - }, - decision: { - context: - "Promotion and constraints preserve meaning only when the promoted target carries the matching kind evidence.", - decision: - "Promoted evidence counts only when the promoted Spec holds its natural evidence; authoring-shape honesty rejects authored delivery facts and external `doc:` targets remain deferred.", - rationale: [ - "Counting empty children or wrong-kind constraints makes a structural floor pass without content, while readiness gates and premature external target types add the wrong contract.", - ], - consequences: [ - "The floor checks resolved target shape, and unresolved external decision links stay outside the current relation grammar.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.pack-reified", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/pack-reified.sdp.md", - title: "Packs group review context without becoming truth", - narrative: null, - sections: { - intent: { - outcome: - "Let related Specs be reviewed together without introducing another truth-bearing artifact.", - }, - decision: { - context: "Delivery work needs a cross-cutting aggregate that is distinct from refinement.", - decision: - "A Pack declares membership and framing while stating no system truth; Specs may belong to many Packs.", - rationale: [ - "Treating a Pack as a truth primitive or a refinement parent confuses grouping with authored intent.", - ], - consequences: [ - "Review context remains disposable while Spec relations retain semantic hierarchy.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.agent-surface-scripts-graph", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/agent-surface-scripts-graph.sdp.md", - title: "Agents script the visible graph", - narrative: null, - sections: { - intent: { - outcome: - "Give agents composable graph context without a fixed command vocabulary becoming the model.", - }, - decision: { - context: "Agents need decoded context and entry adapters without rebuilding graph joins.", - decision: - "The typed graph is the visible contract and agents script it directly through a thin reader surface.", - rationale: [ - "A verb wall duplicates graph semantics and hides composable data behind commands.", - ], - consequences: [ - "Entry adapters expose curated context while coverage gaps remain explicit rather than implied exhaustive.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.mcp-deferred", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/mcp-deferred.sdp.md", - title: "MCP integration remains deferred", - narrative: null, - sections: { - intent: { - outcome: - "Preserve a clean projection model without prematurely fixing an application integration surface.", - }, - decision: { - context: - "The graph already supports typed agent and human projections without an MCP transport.", - decision: - "MCP integration is deferred until a concrete caller establishes its boundary and contract.", - rationale: [ - "Adding an MCP surface without a caller invents verbs and persistence choices outside the projection model.", - ], - consequences: [ - "Consumers use the current graph and reader surfaces while MCP remains designed-in rather than claimed.", - ], - }, - }, - deliveryFacts: [], - }, -] as const; +// Given: the repository root with evidence and the worked example excluded from the authored model. +// The corpus walk is this suite's one expensive step, so it runs exactly once and every assertion +// below reads the same derived graph: splitting the oracle by law never multiplies extraction. +const result = extract({ + root: repoRoot, + exclude: ["explorations", "examples", "test/fixtures/import/parity"], +}); -const expectedPackMembers = [ - "spec:carrier.markdown-authoring", - "spec:carrier.envelope-contract", - "spec:carrier.markdown-parser", - "spec:carrier.sdp-import", - "spec:carrier.sdp-import.round-trip", - "spec:carrier.prose-ownership-rule", - "spec:protocol.self-hosting", - "spec:extraction.derive-graph", - "spec:extraction.determinism", - "spec:extraction.build-pipeline", - "spec:extraction.excludes", - "spec:extraction.claim-taxonomy", - "spec:extraction.regenerability", - "spec:extraction.schema-versioning", - "spec:extraction.executable-contracts", - "spec:validation.readiness-floor", - "spec:validation.duplicate-ids", - "spec:validation.two-check-families", - "spec:validation.referential-integrity", - "spec:validation.claim-separation", - "spec:validation.verification-linkage", - "spec:validation.pack-coherence", - "spec:validation.authored-honesty", - "spec:validation.warn-level-signals", - "spec:consumers.projections-model", - "spec:consumers.agent-surface", - "spec:consumers.design-review", - "spec:consumers.reader", - "spec:consumers.edit-model", - "spec:model.protocol-domain", - "spec:model.core-model", - "spec:model.spec-sections", - "spec:model.relations", - "spec:model.stable-ids", - "spec:model.pack-aggregate", - "spec:model.anchors", - "spec:validation.duplicate-ids.dual-carrier", - "spec:validation.warn-level-signals.orphan-signal", - "spec:validation.warn-level-signals.ready-gap-signal", - "spec:validation.referential-integrity.dangling-target", - "spec:validation.referential-integrity.did-you-mean", - "spec:validation.authored-honesty.section-authored-fact", - "spec:validation.authored-honesty.unearned-stated-fact", - "spec:validation.claim-separation.collapsed-edge-claim", - "spec:validation.claim-separation.unratified-descriptor", - "spec:validation.verification-linkage.unbound-example", - "spec:validation.verification-linkage.unresolved-oracle", - "spec:validation.pack-coherence.incoherent-aggregate", - "spec:extraction.excludes.segment-boundary", - "spec:extraction.excludes.refused-path", - "spec:extraction.schema-versioning.declared-version", - "spec:model.stable-ids.namespaced-round-trip", - "spec:model.stable-ids.malformed-refusal", - "spec:carrier.markdown-parser.bounded-parity", - "spec:extraction.example-runner", - "spec:extraction.example-runner.step-order", - "spec:extraction.example-runner.red-step-naming", - "spec:extraction.executable-contracts.concreteness-refusal", - "spec:extraction.executable-contracts.multi-entry-example", - "spec:extraction.executable-contracts.case-colliding-path", - "spec:carrier.slot-notation", - "spec:carrier.slot-notation.typed-declaration", - "spec:carrier.slot-notation.refused-guess", - "spec:model.anchors.lookalike-refusal", - "spec:model.anchors.physical-identity", - "spec:validation.two-check-families.split-report", - "spec:decisions.plain-language-references", - "spec:decisions.concept-docs-dissolve", - "spec:decisions.one-validation-path", - "spec:decisions.sdp-ts-extension", - "spec:decisions.point-per-example", - "spec:decisions.carrier-ruling", - "spec:decisions.prose-ownership", - "spec:decisions.envelope-grammar-posture", - "spec:decisions.exclusion-contract", - "spec:decisions.executable-meta-model", - "spec:decisions.adopt-the-nouns", - "spec:decisions.one-primitive", - "spec:decisions.protocol-naming", - "spec:decisions.binding-not-liveness", - "spec:decisions.content-only-sections", - "spec:decisions.typing-law", - "spec:decisions.kind-conditional-floor", - "spec:decisions.carried-evidence", - "spec:decisions.pack-reified", - "spec:decisions.agent-surface-scripts-graph", - "spec:decisions.mcp-deferred", -] as const; +// When: the root corpus is reified through the public extractor. +const nodeIds = result.graph.nodes.map((node) => node.id).sort(); +const primitiveNodes = result.graph.nodes.filter((node) => node.nodeType === "Primitive"); +const packNode = result.graph.nodes.find((node) => node.id === "pack:self-hosting-v1"); -const expectedDeclaredRelations = [ - ["spec:carrier.markdown-authoring", "dependsOn", "spec:carrier.markdown-parser"], - ["spec:carrier.markdown-authoring", "decidedBy", "spec:decisions.sdp-ts-extension"], - ["spec:carrier.markdown-authoring", "decidedBy", "spec:decisions.carrier-ruling"], - ["spec:carrier.envelope-contract", "refines", "spec:carrier.markdown-authoring"], - ["spec:carrier.envelope-contract", "decidedBy", "spec:decisions.envelope-grammar-posture"], - ["spec:carrier.markdown-parser", "refines", "spec:carrier.markdown-authoring"], - ["spec:carrier.markdown-parser", "dependsOn", "spec:carrier.envelope-contract"], - ["spec:carrier.markdown-parser.bounded-parity", "refines", "spec:carrier.markdown-parser"], - ["spec:carrier.markdown-parser.bounded-parity", "verifies", "spec:carrier.markdown-parser"], - ["spec:carrier.sdp-import", "refines", "spec:carrier.markdown-authoring"], - ["spec:carrier.sdp-import.round-trip", "refines", "spec:carrier.sdp-import"], - ["spec:carrier.sdp-import.round-trip", "verifies", "spec:carrier.sdp-import"], - ["spec:carrier.prose-ownership-rule", "refines", "spec:carrier.markdown-authoring"], - ["spec:carrier.prose-ownership-rule", "decidedBy", "spec:decisions.prose-ownership"], - ["spec:protocol.self-hosting", "dependsOn", "spec:carrier.markdown-authoring"], - ["spec:protocol.self-hosting", "dependsOn", "spec:model.protocol-domain"], - ["spec:protocol.self-hosting", "decidedBy", "spec:decisions.concept-docs-dissolve"], - ["spec:protocol.self-hosting", "decidedBy", "spec:decisions.executable-meta-model"], - ["spec:protocol.self-hosting", "decidedBy", "spec:decisions.adopt-the-nouns"], - ["spec:protocol.self-hosting", "decidedBy", "spec:decisions.protocol-naming"], - ["spec:extraction.derive-graph", "refines", "spec:protocol.self-hosting"], - ["spec:extraction.derive-graph", "constrainedBy", "spec:extraction.determinism"], - ["spec:extraction.determinism", "refines", "spec:protocol.self-hosting"], - ["spec:extraction.build-pipeline", "refines", "spec:protocol.self-hosting"], - ["spec:extraction.build-pipeline", "dependsOn", "spec:extraction.derive-graph"], - ["spec:extraction.excludes", "refines", "spec:extraction.derive-graph"], - ["spec:extraction.excludes", "decidedBy", "spec:decisions.exclusion-contract"], - ["spec:extraction.excludes.segment-boundary", "refines", "spec:extraction.excludes"], - ["spec:extraction.excludes.segment-boundary", "verifies", "spec:extraction.excludes"], - ["spec:extraction.excludes.refused-path", "refines", "spec:extraction.excludes"], - ["spec:extraction.excludes.refused-path", "verifies", "spec:extraction.excludes"], - ["spec:extraction.claim-taxonomy", "refines", "spec:extraction.derive-graph"], - ["spec:extraction.regenerability", "refines", "spec:extraction.determinism"], - ["spec:extraction.schema-versioning", "refines", "spec:extraction.derive-graph"], - [ - "spec:extraction.schema-versioning.declared-version", - "refines", - "spec:extraction.schema-versioning", - ], - [ - "spec:extraction.schema-versioning.declared-version", - "verifies", - "spec:extraction.schema-versioning", - ], - ["spec:extraction.executable-contracts", "refines", "spec:extraction.build-pipeline"], - [ - "spec:extraction.executable-contracts.concreteness-refusal", - "refines", - "spec:extraction.executable-contracts", - ], - [ - "spec:extraction.executable-contracts.concreteness-refusal", - "verifies", - "spec:extraction.executable-contracts", - ], - [ - "spec:extraction.executable-contracts.multi-entry-example", - "refines", - "spec:extraction.executable-contracts", - ], - [ - "spec:extraction.executable-contracts.multi-entry-example", - "verifies", - "spec:extraction.executable-contracts", - ], - [ - "spec:extraction.executable-contracts.case-colliding-path", - "refines", - "spec:extraction.executable-contracts", - ], - [ - "spec:extraction.executable-contracts.case-colliding-path", - "verifies", - "spec:extraction.executable-contracts", - ], - ["spec:extraction.example-runner", "refines", "spec:extraction.executable-contracts"], - ["spec:extraction.example-runner.step-order", "refines", "spec:extraction.example-runner"], - ["spec:extraction.example-runner.step-order", "verifies", "spec:extraction.example-runner"], - ["spec:extraction.example-runner.red-step-naming", "refines", "spec:extraction.example-runner"], - ["spec:extraction.example-runner.red-step-naming", "verifies", "spec:extraction.example-runner"], - ["spec:carrier.slot-notation", "refines", "spec:carrier.markdown-authoring"], - ["spec:carrier.slot-notation.typed-declaration", "refines", "spec:carrier.slot-notation"], - ["spec:carrier.slot-notation.typed-declaration", "verifies", "spec:carrier.slot-notation"], - ["spec:carrier.slot-notation.refused-guess", "refines", "spec:carrier.slot-notation"], - ["spec:carrier.slot-notation.refused-guess", "verifies", "spec:carrier.slot-notation"], - ["spec:validation.readiness-floor", "refines", "spec:protocol.self-hosting"], - ["spec:validation.readiness-floor", "dependsOn", "spec:model.protocol-domain"], - ["spec:validation.readiness-floor", "decidedBy", "spec:decisions.kind-conditional-floor"], - ["spec:validation.readiness-floor", "decidedBy", "spec:decisions.carried-evidence"], - ["spec:validation.duplicate-ids", "refines", "spec:protocol.self-hosting"], - ["spec:validation.duplicate-ids", "dependsOn", "spec:carrier.markdown-parser"], - ["spec:validation.duplicate-ids.dual-carrier", "refines", "spec:validation.duplicate-ids"], - ["spec:validation.duplicate-ids.dual-carrier", "verifies", "spec:validation.duplicate-ids"], - ["spec:validation.two-check-families", "refines", "spec:protocol.self-hosting"], - ["spec:validation.two-check-families", "decidedBy", "spec:decisions.one-validation-path"], - ["spec:validation.referential-integrity", "refines", "spec:validation.two-check-families"], - [ - "spec:validation.referential-integrity.dangling-target", - "refines", - "spec:validation.referential-integrity", - ], - [ - "spec:validation.referential-integrity.dangling-target", - "verifies", - "spec:validation.referential-integrity", - ], - [ - "spec:validation.referential-integrity.did-you-mean", - "refines", - "spec:validation.referential-integrity", - ], - [ - "spec:validation.referential-integrity.did-you-mean", - "verifies", - "spec:validation.referential-integrity", - ], - ["spec:validation.claim-separation", "refines", "spec:validation.two-check-families"], - [ - "spec:validation.claim-separation.collapsed-edge-claim", - "refines", - "spec:validation.claim-separation", - ], - [ - "spec:validation.claim-separation.collapsed-edge-claim", - "verifies", - "spec:validation.claim-separation", - ], - [ - "spec:validation.claim-separation.unratified-descriptor", - "refines", - "spec:validation.claim-separation", - ], - [ - "spec:validation.claim-separation.unratified-descriptor", - "verifies", - "spec:validation.claim-separation", - ], - ["spec:validation.verification-linkage", "refines", "spec:validation.two-check-families"], - [ - "spec:validation.verification-linkage.unbound-example", - "refines", - "spec:validation.verification-linkage", - ], - [ - "spec:validation.verification-linkage.unbound-example", - "verifies", - "spec:validation.verification-linkage", - ], - [ - "spec:validation.verification-linkage.unresolved-oracle", - "refines", - "spec:validation.verification-linkage", - ], - [ - "spec:validation.verification-linkage.unresolved-oracle", - "verifies", - "spec:validation.verification-linkage", - ], - ["spec:validation.pack-coherence", "refines", "spec:validation.two-check-families"], - [ - "spec:validation.pack-coherence.incoherent-aggregate", - "refines", - "spec:validation.pack-coherence", - ], - [ - "spec:validation.pack-coherence.incoherent-aggregate", - "verifies", - "spec:validation.pack-coherence", - ], - ["spec:validation.authored-honesty", "refines", "spec:validation.two-check-families"], - [ - "spec:validation.authored-honesty.section-authored-fact", - "refines", - "spec:validation.authored-honesty", - ], - [ - "spec:validation.authored-honesty.section-authored-fact", - "verifies", - "spec:validation.authored-honesty", - ], - [ - "spec:validation.authored-honesty.unearned-stated-fact", - "refines", - "spec:validation.authored-honesty", - ], - [ - "spec:validation.authored-honesty.unearned-stated-fact", - "verifies", - "spec:validation.authored-honesty", - ], - ["spec:validation.warn-level-signals", "refines", "spec:validation.two-check-families"], - [ - "spec:validation.warn-level-signals.orphan-signal", - "refines", - "spec:validation.warn-level-signals", - ], - [ - "spec:validation.warn-level-signals.orphan-signal", - "verifies", - "spec:validation.warn-level-signals", - ], - [ - "spec:validation.warn-level-signals.ready-gap-signal", - "refines", - "spec:validation.warn-level-signals", - ], - [ - "spec:validation.warn-level-signals.ready-gap-signal", - "verifies", - "spec:validation.warn-level-signals", - ], - ["spec:consumers.projections-model", "refines", "spec:protocol.self-hosting"], - ["spec:consumers.projections-model", "decidedBy", "spec:decisions.mcp-deferred"], - ["spec:consumers.agent-surface", "refines", "spec:consumers.projections-model"], - ["spec:consumers.agent-surface", "decidedBy", "spec:decisions.agent-surface-scripts-graph"], - ["spec:consumers.design-review", "refines", "spec:consumers.projections-model"], - ["spec:consumers.reader", "refines", "spec:consumers.agent-surface"], - ["spec:consumers.edit-model", "refines", "spec:consumers.projections-model"], - ["spec:model.protocol-domain", "refines", "spec:protocol.self-hosting"], - ["spec:model.core-model", "refines", "spec:protocol.self-hosting"], - ["spec:model.core-model", "decidedBy", "spec:decisions.one-primitive"], - ["spec:model.spec-sections", "refines", "spec:model.core-model"], - ["spec:model.spec-sections", "decidedBy", "spec:decisions.point-per-example"], - ["spec:model.spec-sections", "decidedBy", "spec:decisions.content-only-sections"], - ["spec:model.spec-sections", "decidedBy", "spec:decisions.typing-law"], - ["spec:model.relations", "refines", "spec:model.core-model"], - ["spec:model.stable-ids", "refines", "spec:model.core-model"], - ["spec:model.stable-ids.namespaced-round-trip", "refines", "spec:model.stable-ids"], - ["spec:model.stable-ids.namespaced-round-trip", "verifies", "spec:model.stable-ids"], - ["spec:model.stable-ids.malformed-refusal", "refines", "spec:model.stable-ids"], - ["spec:model.stable-ids.malformed-refusal", "verifies", "spec:model.stable-ids"], - ["spec:model.pack-aggregate", "refines", "spec:model.core-model"], - ["spec:model.pack-aggregate", "decidedBy", "spec:decisions.pack-reified"], - ["spec:model.anchors", "refines", "spec:model.core-model"], - ["spec:model.anchors", "decidedBy", "spec:decisions.binding-not-liveness"], - ["spec:decisions.plain-language-references", "refines", "spec:protocol.self-hosting"], - ["spec:decisions.concept-docs-dissolve", "refines", "spec:protocol.self-hosting"], - ["spec:decisions.one-validation-path", "refines", "spec:validation.two-check-families"], - ["spec:decisions.sdp-ts-extension", "refines", "spec:carrier.markdown-authoring"], - ["spec:decisions.point-per-example", "refines", "spec:model.spec-sections"], - ["spec:decisions.carrier-ruling", "refines", "spec:carrier.markdown-authoring"], - ["spec:decisions.prose-ownership", "refines", "spec:carrier.prose-ownership-rule"], - ["spec:decisions.envelope-grammar-posture", "refines", "spec:carrier.envelope-contract"], - ["spec:decisions.exclusion-contract", "refines", "spec:extraction.excludes"], - ["spec:decisions.executable-meta-model", "refines", "spec:protocol.self-hosting"], - ["spec:decisions.adopt-the-nouns", "refines", "spec:protocol.self-hosting"], - ["spec:decisions.one-primitive", "refines", "spec:model.core-model"], - ["spec:decisions.protocol-naming", "refines", "spec:protocol.self-hosting"], - ["spec:decisions.binding-not-liveness", "refines", "spec:model.anchors"], - ["spec:decisions.content-only-sections", "refines", "spec:model.spec-sections"], - ["spec:decisions.typing-law", "refines", "spec:model.spec-sections"], - ["spec:decisions.kind-conditional-floor", "refines", "spec:validation.readiness-floor"], - ["spec:decisions.carried-evidence", "refines", "spec:validation.readiness-floor"], - ["spec:decisions.pack-reified", "refines", "spec:model.pack-aggregate"], - ["spec:decisions.agent-surface-scripts-graph", "refines", "spec:consumers.agent-surface"], - ["spec:decisions.mcp-deferred", "refines", "spec:consumers.projections-model"], - ["spec:model.anchors.lookalike-refusal", "refines", "spec:model.anchors"], - ["spec:model.anchors.lookalike-refusal", "verifies", "spec:model.anchors"], - ["spec:model.anchors.physical-identity", "refines", "spec:model.anchors"], - ["spec:model.anchors.physical-identity", "verifies", "spec:model.anchors"], - [ - "spec:validation.two-check-families.split-report", - "refines", - "spec:validation.two-check-families", - ], - [ - "spec:validation.two-check-families.split-report", - "verifies", - "spec:validation.two-check-families", - ], -] as const; +const byId = (left: { id: string }, right: { id: string }): number => + left.id.localeCompare(right.id); -const expectedWarnings = [] as const; +function projectDerivedDescriptors(nodes: typeof primitiveNodes) { + return nodes + .map((node) => ({ + id: node.id, + specKind: node.specKind, + altitude: node.altitude, + readiness: node.readiness, + title: node.title, + narrative: node.narrative ?? null, + sections: node.sections, + deliveryFacts: node.deliveryFacts ?? [], + file: node.file, + })) + .sort(byId); +} -const expectedAnchors = [ - { - id: "impl:protocol.extract", - nodeType: "CodeNode", - label: "extracts authored carriers and bindings into one graph", - type: "satisfies", - target: "spec:extraction.derive-graph", - file: "src/extract/index.ts", - constant: "extractAnchor", - site: "export function extract", - }, - { - id: "impl:protocol.derive-graph", - nodeType: "CodeNode", - label: "derives the graph from reified carriers and bindings", - type: "satisfies", - target: "spec:extraction.derive-graph", - file: "src/extract/derive.ts", - constant: "deriveGraphAnchor", - site: "export function deriveGraph", - }, - { - id: "test:protocol.extract", - nodeType: "Anchor", - label: "extraction contracts verify graph derivation", - type: "verifies", - target: "spec:extraction.derive-graph", - file: "test/extract.test.ts", - constant: "extractContractTestAnchor", - site: 'describe("anchor extraction corpora",', - }, - { - id: "test:protocol.extraction-determinism", - nodeType: "Anchor", - label: "clean-repo pipeline determinism verifies byte-identical output", - type: "verifies", - target: "spec:extraction.determinism", - file: "test/cli.test.ts", - constant: "cleanRepoDeterminismTestAnchor", - site: 'it("clean-repo determinism: the full pipeline at a different absolute path is byte-identical"', - }, - { - id: "impl:protocol.readiness-floor", - nodeType: "CodeNode", - label: "evaluates the stated readiness floor against the graph", - type: "satisfies", - target: "spec:validation.readiness-floor", - file: "src/validate/readiness-floor.ts", - constant: "readinessFloorAnchor", - site: "export function evaluateReadinessFloor", - }, - { - id: "test:protocol.readiness-floor", - nodeType: "Anchor", - label: "readiness-floor contracts verify stated maturity", - type: "verifies", - target: "spec:validation.readiness-floor", - file: "test/readiness.test.ts", - constant: "readinessFloorTestAnchor", - site: 'describe("readiness and validation contracts",', - }, - { - id: "impl:protocol.markdown-authoring", - nodeType: "CodeNode", - label: "reifies Markdown authoring into the one carrier path", - type: "satisfies", - target: "spec:carrier.markdown-authoring", - file: "src/extract/markdown.ts", - constant: "markdownAuthoringAnchor", - site: "export function reifyMarkdownCarrier", - }, - { - id: "impl:protocol.markdown-parser", - nodeType: "CodeNode", - label: "reifies the ruled Markdown parser input", - type: "satisfies", - target: "spec:carrier.markdown-parser", - file: "src/extract/markdown.ts", - constant: "markdownParserAnchor", - site: "export function reifyMarkdownCarrier", - }, - { - id: "test:protocol.markdown-parser", - nodeType: "Anchor", - label: "Markdown reifier tests verify the ruled parser", - type: "verifies", - target: "spec:carrier.markdown-parser", - file: "test/markdown-reifier.test.ts", - constant: "markdownParserTestAnchor", - site: 'describe("Markdown frontmatter reifier",', - }, - { - id: "impl:protocol.envelope-contract", - nodeType: "CodeNode", - label: "parses the bounded Markdown frontmatter envelope", - type: "satisfies", - target: "spec:carrier.envelope-contract", - file: "src/extract/markdown.ts", - constant: "envelopeContractAnchor", - site: "export function parseMarkdownFrontmatter", - }, - { - id: "test:protocol.envelope-contract", - nodeType: "Anchor", - label: "frontmatter contract tests verify the Markdown envelope", - type: "verifies", - target: "spec:carrier.envelope-contract", - file: "test/markdown-reifier.test.ts", - constant: "envelopeContractTestAnchor", - site: 'describe("Markdown frontmatter reifier",', - }, - { - id: "impl:protocol.prose-ownership", - nodeType: "CodeNode", - label: "reads Markdown body content through its prose owners", - type: "satisfies", - target: "spec:carrier.prose-ownership-rule", - file: "src/extract/markdown.ts", - constant: "proseOwnershipAnchor", - site: "export function readMarkdownBody", - }, - { - id: "test:protocol.prose-ownership", - nodeType: "Anchor", - label: "Markdown reifier tests verify prose ownership", - type: "verifies", - target: "spec:carrier.prose-ownership-rule", - file: "test/markdown-reifier.test.ts", - constant: "proseOwnershipTestAnchor", - site: 'describe("Markdown frontmatter reifier",', - }, - { - id: "impl:protocol.duplicate-id-exclusion", - nodeType: "CodeNode", - label: "excludes duplicated carrier ids from the graph", - type: "satisfies", - target: "spec:validation.duplicate-ids", - file: "src/extract/index.ts", - constant: "duplicateIdExclusionAnchor", - site: "function findDuplicatedIds", - }, - { - id: "test:protocol.duplicate-ids.dual-carrier", - nodeType: "Anchor", - label: "dual-carrier duplicate-ID contract verifies carrier exclusion", - type: "verifies", - target: "spec:validation.duplicate-ids.dual-carrier", - file: "test/self-hosting-duplicate-ids.test.ts", - constant: "dualCarrierDuplicateTestAnchor", - site: "bindExample(", - }, - { - id: "test:protocol.warn-level-signals.orphan-signal", - nodeType: "Anchor", - label: "the orphan point verifies the disconnected-spec warning", - type: "verifies", - target: "spec:validation.warn-level-signals.orphan-signal", - file: "test/self-hosting-validators.test.ts", - constant: "warnLevelOrphanTestAnchor", - site: "bindExample(orphanSignalContract", - }, - { - id: "test:protocol.warn-level-signals.ready-gap-signal", - nodeType: "Anchor", - label: "the gap point verifies the unverified-ready warning", - type: "verifies", - target: "spec:validation.warn-level-signals.ready-gap-signal", - file: "test/self-hosting-validators.test.ts", - constant: "warnLevelGapTestAnchor", - site: "bindExample(readyGapSignalContract", - }, - { - id: "test:protocol.referential-integrity.dangling-target", - nodeType: "Anchor", - label: "the dangling-target point verifies the unresolved-reference error", - type: "verifies", - target: "spec:validation.referential-integrity.dangling-target", - file: "test/self-hosting-validators.test.ts", - constant: "danglingTargetTestAnchor", - site: "bindExample(danglingTargetContract", - }, - { - id: "test:protocol.referential-integrity.did-you-mean", - nodeType: "Anchor", - label: "the near-miss point verifies the unique did-you-mean suggestion", - type: "verifies", - target: "spec:validation.referential-integrity.did-you-mean", - file: "test/self-hosting-validators.test.ts", - constant: "didYouMeanTestAnchor", - site: "bindExample(didYouMeanContract", - }, - { - id: "test:protocol.authored-honesty.section-authored-fact", - nodeType: "Anchor", - label: "the section point verifies the authoring-shape refusal", - type: "verifies", - target: "spec:validation.authored-honesty.section-authored-fact", - file: "test/self-hosting-validators.test.ts", - constant: "sectionAuthoredFactTestAnchor", - site: "bindExample(sectionAuthoredFactContract", - }, - { - id: "test:protocol.authored-honesty.unearned-stated-fact", - nodeType: "Anchor", - label: "the stated-fact point verifies the delivery-fact refusal", - type: "verifies", - target: "spec:validation.authored-honesty.unearned-stated-fact", - file: "test/self-hosting-validators.test.ts", - constant: "unearnedStatedFactTestAnchor", - site: "bindExample(unearnedStatedFactContract", - }, - { - id: "test:protocol.claim-separation.collapsed-edge-claim", - nodeType: "Anchor", - label: "the collapsed-claim point verifies the binding-edge contract row", - type: "verifies", - target: "spec:validation.claim-separation.collapsed-edge-claim", - file: "test/self-hosting-validators.test.ts", - constant: "collapsedEdgeClaimTestAnchor", - site: "bindExample(collapsedEdgeClaimContract", - }, - { - id: "test:protocol.claim-separation.unratified-descriptor", - nodeType: "Anchor", - label: "the unratified-kind point verifies the fail-closed descriptor law", - type: "verifies", - target: "spec:validation.claim-separation.unratified-descriptor", - file: "test/self-hosting-validators.test.ts", - constant: "unratifiedDescriptorTestAnchor", - site: "bindExample(unratifiedDescriptorContract", - }, - { - id: "test:protocol.verification-linkage.unbound-example", - nodeType: "Anchor", - label: "the unbound-example point verifies the incomplete spec-to-test trace", - type: "verifies", - target: "spec:validation.verification-linkage.unbound-example", - file: "test/self-hosting-validators.test.ts", - constant: "unboundExampleTestAnchor", - site: "bindExample(unboundExampleContract", - }, - { - id: "test:protocol.verification-linkage.unresolved-oracle", - nodeType: "Anchor", - label: "the unresolved-oracle point verifies the oracle binding refusal", - type: "verifies", - target: "spec:validation.verification-linkage.unresolved-oracle", - file: "test/self-hosting-validators.test.ts", - constant: "unresolvedOracleTestAnchor", - site: "bindExample(unresolvedOracleContract", - }, - { - id: "test:protocol.pack-coherence.incoherent-aggregate", - nodeType: "Anchor", - label: "the incoherent-aggregate point verifies both halves of the pack law", - type: "verifies", - target: "spec:validation.pack-coherence.incoherent-aggregate", - file: "test/self-hosting-validators.test.ts", - constant: "incoherentAggregateTestAnchor", - site: "bindExample(incoherentAggregateContract", - }, - { - id: "test:protocol.excludes.segment-boundary", - nodeType: "Anchor", - label: "the segment-boundary point verifies the exact-prefix exclusion rule", - type: "verifies", - target: "spec:extraction.excludes.segment-boundary", - file: "test/self-hosting-extraction.test.ts", - constant: "excludesSegmentBoundaryTestAnchor", - site: "bindExample(segmentBoundaryContract", - }, - { - id: "test:protocol.excludes.refused-path", - nodeType: "Anchor", - label: "the refused-path point verifies the malformed-exclusion refusal", - type: "verifies", - target: "spec:extraction.excludes.refused-path", - file: "test/self-hosting-extraction.test.ts", - constant: "excludesRefusedPathTestAnchor", - site: "bindExample(refusedPathContract", - }, - { - id: "test:protocol.schema-versioning.declared-version", - nodeType: "Anchor", - label: "the declared-version point verifies the readable payload version", - type: "verifies", - target: "spec:extraction.schema-versioning.declared-version", - file: "test/self-hosting-extraction.test.ts", - constant: "schemaVersioningTestAnchor", - site: "bindExample(declaredVersionContract", - }, - { - id: "test:protocol.stable-ids.namespaced-round-trip", - nodeType: "Anchor", - label: "the round-trip point verifies the namespaced dotted-path grammar", - type: "verifies", - target: "spec:model.stable-ids.namespaced-round-trip", - file: "test/self-hosting-model.test.ts", - constant: "namespacedRoundTripTestAnchor", - site: "bindExample(namespacedRoundTripContract", - }, - { - id: "test:protocol.stable-ids.malformed-refusal", - nodeType: "Anchor", - label: "the malformed point verifies the lowercase-namespace refusal", - type: "verifies", - target: "spec:model.stable-ids.malformed-refusal", - file: "test/self-hosting-model.test.ts", - constant: "malformedRefusalTestAnchor", - site: "bindExample(malformedRefusalContract", - }, - { - id: "test:protocol.markdown-parser.bounded-parity", - nodeType: "Anchor", - label: "the bounded-parity point verifies one shared finding class and its split outcomes", - type: "verifies", - target: "spec:carrier.markdown-parser.bounded-parity", - file: "test/self-hosting-carrier.test.ts", - constant: "boundedParityTestAnchor", - site: "bindExample(boundedParityContract", - }, - { - id: "test:protocol.sdp-import.round-trip", - nodeType: "Anchor", - label: "TypeScript import round-trip contract preserves authored data", - type: "verifies", - target: "spec:carrier.sdp-import.round-trip", - file: "test/self-hosting-sdp-import.test.ts", - constant: "sdpImportRoundTripTestAnchor", - site: "bindExample(", - }, - { - id: "impl:protocol.anchor-extraction", - nodeType: "CodeNode", - label: "anchor-constant reification seam", - type: "satisfies", - target: "spec:model.anchors", - file: "src/extract/anchors.ts", - constant: "anchorExtractionAnchor", - site: "const ANCHOR_BUILDER_TARGET_FIELDS", - }, - { - id: "impl:protocol.anchor-model", - nodeType: "CodeNode", - label: "binding-only anchor model builders", - type: "satisfies", - target: "spec:model.anchors", - file: "src/model/anchors.ts", - constant: "anchorModelAnchor", - site: "export function specTest", - }, - { - id: "impl:protocol.pack-aggregate", - nodeType: "CodeNode", - label: "Pack aggregate and model references", - type: "satisfies", - target: "spec:model.pack-aggregate", - file: "src/model/pack.ts", - constant: "packAggregateAnchor", - site: "export function pack", - }, - { - id: "impl:protocol.spec-descriptors", - nodeType: "CodeNode", - label: "Spec kind, altitude, and readiness coordinates", - type: "satisfies", - target: "spec:model.core-model", - file: "src/model/descriptors.ts", - constant: "specDescriptorsAnchor", - site: "export const SPEC_KIND_DISPLAY_LABELS", - }, - { - id: "impl:protocol.spec-primitive", - nodeType: "CodeNode", - label: "Spec envelope and enrich-in-place shape", - type: "satisfies", - target: "spec:model.core-model", - file: "src/model/spec.ts", - constant: "specPrimitiveAnchor", - site: "export function spec", - }, - { - id: "impl:protocol.spec-relations", - nodeType: "CodeNode", - label: "declared Spec relation builders", - type: "satisfies", - target: "spec:model.relations", - file: "src/model/relations.ts", - constant: "specRelationsAnchor", - site: "export function supersedes", - }, - { - id: "impl:protocol.spec-sections", - nodeType: "CodeNode", - label: "typed Spec section shapes", - type: "satisfies", - target: "spec:model.spec-sections", - file: "src/model/sections.ts", - constant: "specSectionsAnchor", - site: "export interface SpecSections", - }, - { - id: "impl:protocol.stable-ids", - nodeType: "CodeNode", - label: "stable ID grammar parser", - type: "satisfies", - target: "spec:model.stable-ids", - file: "src/ids.ts", - constant: "stableIdsAnchor", - site: "export function parseId", - }, - { - id: "impl:protocol.verifier-semantics", - nodeType: "CodeNode", - label: "readiness clauses over direct verification bindings", - type: "satisfies", - target: "spec:model.spec-sections", - file: "src/validate/readiness-floor.ts", - constant: "verifierSemanticsAnchor", - site: "export function evaluateReadinessFloor", - }, - { - id: "impl:protocol.validation-families", - nodeType: "CodeNode", - label: "conformance and honesty validator registry", - type: "satisfies", - target: "spec:validation.two-check-families", - file: "src/validate/validators.ts", - constant: "validationFamiliesAnchor", - site: "export const graphValidatorIds", - }, - { - id: "impl:protocol.projections-model", - nodeType: "CodeNode", - label: "pure generated projection page contract", - type: "satisfies", - target: "spec:consumers.projections-model", - file: "src/projections/design-review.ts", - constant: "projectionModelAnchor", - site: "export interface DesignReviewPage", - }, - { - id: "impl:protocol.agent-surface", - nodeType: "CodeNode", - label: "typed graph reader and agent entry adapters", - type: "satisfies", - target: "spec:consumers.agent-surface", - file: "src/reader/reader.ts", - constant: "agentSurfaceAnchor", - site: "export function createReader", - }, - { - id: "impl:protocol.reader-impact", - nodeType: "CodeNode", - label: "file-level reader blast-radius contract", - type: "satisfies", - target: "spec:consumers.reader", - file: "src/reader/reader.ts", - constant: "readerImpactAnchor", - site: "export interface BlastRadius", - }, - { - id: "impl:protocol.reader", - nodeType: "CodeNode", - label: "thin typed graph reader construction", - type: "satisfies", - target: "spec:consumers.reader", - file: "src/reader/reader.ts", - constant: "readerAnchor", - site: "export function createReader", - }, - { - id: "impl:protocol.design-review", - nodeType: "CodeNode", - label: "renders the contextual Design Review projection", - type: "satisfies", - target: "spec:consumers.design-review", - file: "src/projections/design-review.ts", - constant: "designReviewAnchor", - site: "export function renderDesignReview", - }, - { - id: "impl:protocol.exclusion-surface", - nodeType: "CodeNode", - label: "strict root-relative exclusion input for both extraction surfaces", - type: "satisfies", - target: "spec:extraction.excludes", - file: "src/extract/index.ts", - constant: "exclusionSurfaceAnchor", - site: "export interface ExtractOptions", - }, - { - id: "impl:protocol.graph-claims", - nodeType: "CodeNode", - label: "declares the graph claim taxonomy", - type: "satisfies", - target: "spec:extraction.claim-taxonomy", - file: "src/graph/schema.ts", - constant: "graphClaimsAnchor", - site: "export const graphClaims", - }, - { - id: "impl:protocol.regenerability", - nodeType: "CodeNode", - label: "repeats graph and contract producers for deterministic regeneration", - type: "satisfies", - target: "spec:extraction.regenerability", - file: "src/cli/build-command.ts", - constant: "regenerabilityAnchor", - site: "export function runBuild", - }, - { - id: "impl:protocol.schema-version", - nodeType: "CodeNode", - label: "declares the graph schema version", - type: "satisfies", - target: "spec:extraction.schema-versioning", - file: "src/graph/schema.ts", - constant: "schemaVersionAnchor", - site: "export const schemaVersion", - }, - { - id: "impl:protocol.executable-contracts", - nodeType: "CodeNode", - label: "derives step and example-space contracts from the graph", - type: "satisfies", - target: "spec:extraction.executable-contracts", - file: "src/codegen/contracts.ts", - constant: "executableContractsAnchor", - site: "export function generateContracts", - }, - { - id: "impl:protocol.example-runner", - nodeType: "CodeNode", - label: "plans and executes a bound example against the caller's world", - type: "satisfies", - target: "spec:extraction.example-runner", - file: "src/runner/index.ts", - constant: "exampleRunnerAnchor", - site: "export function planExample", - }, - { - id: "impl:protocol.slot-notation", - nodeType: "CodeNode", - label: "parses slot groups and normalizes a step to its skeleton", - type: "satisfies", - target: "spec:carrier.slot-notation", - file: "src/notation/slots.ts", - constant: "slotNotationAnchor", - site: "export function parseSlots", - }, - { - id: "test:protocol.executable-contracts.concreteness-refusal", - nodeType: "Anchor", - label: "the concreteness point verifies the unbound-slot refusal", - type: "verifies", - target: "spec:extraction.executable-contracts.concreteness-refusal", - file: "test/self-hosting-extraction.test.ts", - constant: "concretenessRefusalTestAnchor", - site: "bindExample(concretenessRefusalContract", - }, - { - id: "test:protocol.executable-contracts.multi-entry-example", - nodeType: "Anchor", - label: "the multi-entry point verifies the named second entry", - type: "verifies", - target: "spec:extraction.executable-contracts.multi-entry-example", - file: "test/self-hosting-extraction.test.ts", - constant: "multiEntryExampleTestAnchor", - site: "bindExample(multiEntryExampleContract", - }, - { - id: "test:protocol.executable-contracts.case-colliding-path", - nodeType: "Anchor", - label: "the collision point verifies the all-or-nothing withholding", - type: "verifies", - target: "spec:extraction.executable-contracts.case-colliding-path", - file: "test/self-hosting-extraction.test.ts", - constant: "caseCollidingPathTestAnchor", - site: "bindExample(caseCollidingPathContract", - }, - { - id: "test:protocol.example-runner.step-order", - nodeType: "Anchor", - label: "the step-order point verifies contract order and the one handler per step", - type: "verifies", - target: "spec:extraction.example-runner.step-order", - file: "test/self-hosting-extraction.test.ts", - constant: "exampleRunnerStepOrderTestAnchor", - site: "bindExample(stepOrderContract", - }, - { - id: "test:protocol.example-runner.red-step-naming", - nodeType: "Anchor", - label: "the red-step point verifies the self-naming failure law", - type: "verifies", - target: "spec:extraction.example-runner.red-step-naming", - file: "test/self-hosting-extraction.test.ts", - constant: "exampleRunnerRedStepTestAnchor", - site: "bindExample(redStepNamingContract", - }, - { - id: "test:protocol.slot-notation.typed-declaration", - nodeType: "Anchor", - label: "the declaration point verifies the typed form and its skeleton", - type: "verifies", - target: "spec:carrier.slot-notation.typed-declaration", - file: "test/self-hosting-carrier.test.ts", - constant: "slotNotationTypedTestAnchor", - site: "bindExample(typedDeclarationContract", - }, - { - id: "test:protocol.slot-notation.refused-guess", - nodeType: "Anchor", - label: "the refusal point verifies prose braces and the unusable slot", - type: "verifies", - target: "spec:carrier.slot-notation.refused-guess", - file: "test/self-hosting-carrier.test.ts", - constant: "slotNotationRefusedTestAnchor", - site: "bindExample(refusedGuessContract", - }, - { - id: "test:protocol.anchors.lookalike-refusal", - nodeType: "Anchor", - label: "the lookalike point verifies that a consumer-local builder mints nothing", - type: "verifies", - target: "spec:model.anchors.lookalike-refusal", - file: "test/self-hosting-model.test.ts", - constant: "lookalikeRefusalTestAnchor", - site: "bindExample(lookalikeRefusalContract", - }, - { - id: "test:protocol.anchors.physical-identity", - nodeType: "Anchor", - label: "the physical-identity point verifies the resolved relative builder import", - type: "verifies", - target: "spec:model.anchors.physical-identity", - file: "test/self-hosting-model.test.ts", - constant: "physicalIdentityTestAnchor", - site: "bindExample(physicalIdentityContract", - }, - { - id: "test:protocol.two-check-families.split-report", - nodeType: "Anchor", - label: "the split-report point verifies both families in one aggregate report", - type: "verifies", - target: "spec:validation.two-check-families.split-report", - file: "test/self-hosting-validators.test.ts", - constant: "splitReportTestAnchor", - site: "bindExample(splitReportContract", - }, -] as const; +function projectAuthoredDescriptors(specs: readonly ExpectedSpec[]) { + return [...specs].sort(byId).map((spec) => ({ + id: spec.id, + specKind: spec.specKind, + altitude: spec.altitude, + readiness: spec.readiness, + title: spec.title, + narrative: spec.narrative, + sections: spec.sections, + deliveryFacts: spec.deliveryFacts, + file: spec.file, + })); +} function lineContaining(source: string, token: string): number { const line = source.split("\n").findIndex((entry) => entry.includes(token)); @@ -3591,20 +70,12 @@ function lineContaining(source: string, token: string): number { } describe("the self-hosting corpus", () => { - it("derives the Markdown-canonical specs and their exact Pack checkpoint from the root", () => { - // Given: the repository root with evidence and the worked example excluded from the authored model. - const result = extract({ - root: repoRoot, - exclude: ["explorations", "examples", "test/fixtures/import/parity"], - }); - - // When: the root corpus is reified through the public extractor. - const nodeIds = result.graph.nodes.map((node) => node.id).sort(); - const primitiveNodes = result.graph.nodes.filter((node) => node.nodeType === "Primitive"); - const packNode = result.graph.nodes.find((node) => node.id === "pack:self-hosting-v1"); - - // Then: the frozen corpus enters one graph with exact descriptors and its direct bindings. + // Then: the frozen corpus enters one graph with exact descriptors and its direct bindings. + it("reifies the root corpus without an extraction finding", () => { expect(result.report.findings).toEqual([]); + }); + + it("derives a graph the conformance and honesty checks leave without a finding", () => { expect( validateGraph(result.graph).findings.map(({ validatorId, family, severity, subjectId }) => ({ validatorId, @@ -3613,136 +84,59 @@ describe("the self-hosting corpus", () => { subjectId, })), ).toEqual(expectedWarnings); + }); + + it("holds the frozen corpus totals", () => { + // The literals are the corpus checkpoint. The authored arrays are measured against the same + // literals rather than standing in for them, so a transcription slip in an oracle module + // cannot certify itself by moving both sides of a comparison at once. expect(result.counts).toEqual({ specs: 87, packs: 1, anchors: 65 }); + expect(expectedSpecs).toHaveLength(87); + expect(expectedPackMembers).toHaveLength(87); + expect(expectedAnchors).toHaveLength(65); + expect(result.graph.nodes).toHaveLength(153); + expect(result.graph.edges).toHaveLength(294); + }); + + it("rosters exactly the authored Spec, Pack, and anchor node ids", () => { + // The roster is derived from the same authored arrays the descriptor and binding assertions + // read: one oracle statement of the corpus's identities, compared against the graph once. expect(nodeIds).toEqual( [ "pack:self-hosting-v1", - "spec:carrier.envelope-contract", - "spec:carrier.markdown-authoring", - "spec:carrier.markdown-parser", - "spec:carrier.prose-ownership-rule", - "spec:carrier.sdp-import", - "spec:carrier.sdp-import.round-trip", - "spec:consumers.agent-surface", - "spec:consumers.design-review", - "spec:consumers.edit-model", - "spec:consumers.projections-model", - "spec:consumers.reader", - "spec:decisions.concept-docs-dissolve", - "spec:decisions.one-validation-path", - "spec:decisions.sdp-ts-extension", - "spec:decisions.point-per-example", - "spec:decisions.carrier-ruling", - "spec:decisions.prose-ownership", - "spec:decisions.envelope-grammar-posture", - "spec:decisions.exclusion-contract", - "spec:decisions.executable-meta-model", - "spec:decisions.adopt-the-nouns", - "spec:decisions.one-primitive", - "spec:decisions.protocol-naming", - "spec:decisions.binding-not-liveness", - "spec:decisions.content-only-sections", - "spec:decisions.typing-law", - "spec:decisions.kind-conditional-floor", - "spec:decisions.carried-evidence", - "spec:decisions.pack-reified", - "spec:decisions.agent-surface-scripts-graph", - "spec:decisions.mcp-deferred", - "spec:decisions.plain-language-references", - "spec:extraction.build-pipeline", - "spec:extraction.claim-taxonomy", - "spec:extraction.derive-graph", - "spec:extraction.determinism", - "spec:extraction.excludes", - "spec:extraction.executable-contracts", - "spec:extraction.regenerability", - "spec:extraction.schema-versioning", - "spec:model.anchors", - "spec:model.core-model", - "spec:model.pack-aggregate", - "spec:model.protocol-domain", - "spec:model.relations", - "spec:model.spec-sections", - "spec:model.stable-ids", - "spec:protocol.self-hosting", - "spec:validation.duplicate-ids", - "spec:validation.duplicate-ids.dual-carrier", - "spec:validation.authored-honesty", - "spec:validation.claim-separation", - "spec:validation.pack-coherence", - "spec:validation.readiness-floor", - "spec:validation.referential-integrity", - "spec:validation.two-check-families", - "spec:validation.verification-linkage", - "spec:validation.warn-level-signals", - "spec:validation.warn-level-signals.orphan-signal", - "spec:validation.warn-level-signals.ready-gap-signal", - "spec:validation.referential-integrity.dangling-target", - "spec:validation.referential-integrity.did-you-mean", - "spec:validation.authored-honesty.section-authored-fact", - "spec:validation.authored-honesty.unearned-stated-fact", - "spec:validation.claim-separation.collapsed-edge-claim", - "spec:validation.claim-separation.unratified-descriptor", - "spec:validation.verification-linkage.unbound-example", - "spec:validation.verification-linkage.unresolved-oracle", - "spec:validation.pack-coherence.incoherent-aggregate", - "spec:extraction.excludes.segment-boundary", - "spec:extraction.excludes.refused-path", - "spec:extraction.schema-versioning.declared-version", - "spec:model.stable-ids.namespaced-round-trip", - "spec:model.stable-ids.malformed-refusal", - "spec:carrier.markdown-parser.bounded-parity", - "spec:extraction.example-runner", - "spec:extraction.example-runner.step-order", - "spec:extraction.example-runner.red-step-naming", - "spec:extraction.executable-contracts.concreteness-refusal", - "spec:extraction.executable-contracts.multi-entry-example", - "spec:extraction.executable-contracts.case-colliding-path", - "spec:carrier.slot-notation", - "spec:carrier.slot-notation.typed-declaration", - "spec:carrier.slot-notation.refused-guess", - "spec:model.anchors.lookalike-refusal", - "spec:model.anchors.physical-identity", - "spec:validation.two-check-families.split-report", + ...expectedSpecs.map((spec) => spec.id), ...expectedAnchors.map((anchor) => anchor.id), ].sort(), ); - expect(result.graph.nodes).toHaveLength(153); - expect( - primitiveNodes - .map((node) => ({ - id: node.id, - specKind: node.specKind, - altitude: node.altitude, - readiness: node.readiness, - title: node.title, - narrative: node.narrative ?? null, - sections: node.sections, - deliveryFacts: node.deliveryFacts ?? [], - file: node.file, - })) - .sort((left, right) => left.id.localeCompare(right.id)), - ).toEqual( - [...expectedSpecs] - .sort((left, right) => left.id.localeCompare(right.id)) - .map((spec) => ({ - id: spec.id, - specKind: spec.specKind, - altitude: spec.altitude, - readiness: spec.readiness, - title: spec.title, - narrative: spec.narrative, - sections: spec.sections, - deliveryFacts: spec.deliveryFacts, - file: spec.file, - })), - ); + }); + + for (const family of specFamilies) { + it(`carries the authored descriptors of the ${family.name} family`, () => { + expect( + projectDerivedDescriptors( + primitiveNodes.filter((node) => node.id.startsWith(family.prefix)), + ), + ).toEqual(projectAuthoredDescriptors(family.specs)); + }); + } + + it("leaves no authored Spec outside the families", () => { + const familyIds = specFamilies.flatMap((family) => family.specs.map((spec) => spec.id)); + + expect([...familyIds].sort()).toEqual(primitiveNodes.map((node) => node.id).sort()); + expect(new Set(familyIds).size).toBe(familyIds.length); + }); + + it("derives exactly the authored declared relations", () => { expect( result.graph.edges .filter((edge) => edge.claim === "declared" && edge.type !== "belongsTo") .map((edge) => [edge.from, edge.type, edge.to]) .sort(), ).toEqual([...expectedDeclaredRelations].sort()); + }); + + it("holds the frozen stated-readiness distribution", () => { expect( primitiveNodes.reduce>( (histogram, node) => ({ @@ -3752,11 +146,17 @@ describe("the self-hosting corpus", () => { {}, ), ).toEqual({ defined: 36, ready: 51 }); + }); + + it("derives the Pack membership edges from the manifest, in manifest order", () => { expect( result.graph.edges .filter((edge) => edge.type === "belongsTo") .map((edge) => [edge.from, edge.to, edge.claim]), ).toEqual(expectedPackMembers.map((id) => [id, "pack:self-hosting-v1", "declared"])); + }); + + it("carries the Pack aggregate node exactly as authored", () => { expect(packNode).toEqual({ id: "pack:self-hosting-v1", nodeType: "Pack", @@ -3766,13 +166,18 @@ describe("the self-hosting corpus", () => { modelRefs: ["spec:model.protocol-domain", "spec:model.core-model"], file: "specs/self-hosting.pack.sdp.ts", }); - expect(result.graph.edges).toHaveLength(294); + }); + + it("derives one anchored edge per authored anchor", () => { expect( result.graph.edges .filter((edge) => edge.claim === "anchored") .map((edge) => [edge.from, edge.type, edge.to]) .sort(), ).toEqual(expectedAnchors.map((anchor) => [anchor.id, anchor.type, anchor.target]).sort()); + }); + + it("projects every anchor and code node at the line its declaration occupies", () => { const expectedAnchorNodes = expectedAnchors .map((anchor) => { const source = readFileSync(join(repoRoot, anchor.file), "utf8"); @@ -3786,7 +191,8 @@ describe("the self-hosting corpus", () => { line: lineContaining(source, `const ${anchor.constant}`), }; }) - .sort((left, right) => left.id.localeCompare(right.id)); + .sort(byId); + expect( result.graph.nodes .filter((node) => node.nodeType === "Anchor" || node.nodeType === "CodeNode") @@ -3798,8 +204,11 @@ describe("the self-hosting corpus", () => { file: node.file, line: node.line, })) - .sort((left, right) => left.id.localeCompare(right.id)), + .sort(byId), ).toEqual(expectedAnchorNodes); + }); + + it("keeps every anchor beside the site it binds", () => { for (const anchor of expectedAnchors) { const source = readFileSync(join(repoRoot, anchor.file), "utf8"); const anchorLine = lineContaining(source, `const ${anchor.constant}`); @@ -3811,7 +220,9 @@ describe("the self-hosting corpus", () => { expect(Math.abs(anchorLine - siteLine), anchor.id).toBeLessThanOrEqual(20); expect(node).toMatchObject({ file: anchor.file, line: anchorLine, claim: "anchored" }); } + }); + it("derives the duplicate-ids delivery facts from the bindings, never from authoring", () => { const childId = "spec:validation.duplicate-ids.dual-carrier"; const parentId = "spec:validation.duplicate-ids"; const child = primitiveNodes.find((node) => node.id === childId); @@ -3861,7 +272,9 @@ describe("the self-hosting corpus", () => { claim: "anchored", }, ]); + }); + it("derives the sdp-import round-trip delivery facts from the child's verifier", () => { const importChildId = "spec:carrier.sdp-import.round-trip"; const importParentId = "spec:carrier.sdp-import"; const importChild = primitiveNodes.find((node) => node.id === importChildId); diff --git a/test/self-hosting-oracle/anchors.ts b/test/self-hosting-oracle/anchors.ts new file mode 100644 index 0000000..efbacfe --- /dev/null +++ b/test/self-hosting-oracle/anchors.ts @@ -0,0 +1,656 @@ +// The authored anchors of the self-hosting corpus — a binding assertion each, identity only and +// never intent. `constant` names the anchor declaration and `site` the code it binds, so the +// suite can resolve both lines in the real file rather than trusting a transcribed line number. + +export const expectedAnchors = [ + { + id: "impl:protocol.extract", + nodeType: "CodeNode", + label: "extracts authored carriers and bindings into one graph", + type: "satisfies", + target: "spec:extraction.derive-graph", + file: "src/extract/index.ts", + constant: "extractAnchor", + site: "export function extract", + }, + { + id: "impl:protocol.derive-graph", + nodeType: "CodeNode", + label: "derives the graph from reified carriers and bindings", + type: "satisfies", + target: "spec:extraction.derive-graph", + file: "src/extract/derive.ts", + constant: "deriveGraphAnchor", + site: "export function deriveGraph", + }, + { + id: "test:protocol.extract", + nodeType: "Anchor", + label: "extraction contracts verify graph derivation", + type: "verifies", + target: "spec:extraction.derive-graph", + file: "test/extract.test.ts", + constant: "extractContractTestAnchor", + site: 'describe("anchor extraction corpora",', + }, + { + id: "test:protocol.extraction-determinism", + nodeType: "Anchor", + label: "clean-repo pipeline determinism verifies byte-identical output", + type: "verifies", + target: "spec:extraction.determinism", + file: "test/cli.test.ts", + constant: "cleanRepoDeterminismTestAnchor", + site: 'it("clean-repo determinism: the full pipeline at a different absolute path is byte-identical"', + }, + { + id: "impl:protocol.readiness-floor", + nodeType: "CodeNode", + label: "evaluates the stated readiness floor against the graph", + type: "satisfies", + target: "spec:validation.readiness-floor", + file: "src/validate/readiness-floor.ts", + constant: "readinessFloorAnchor", + site: "export function evaluateReadinessFloor", + }, + { + id: "test:protocol.readiness-floor", + nodeType: "Anchor", + label: "readiness-floor contracts verify stated maturity", + type: "verifies", + target: "spec:validation.readiness-floor", + file: "test/readiness.test.ts", + constant: "readinessFloorTestAnchor", + site: 'describe("readiness and validation contracts",', + }, + { + id: "impl:protocol.markdown-authoring", + nodeType: "CodeNode", + label: "reifies Markdown authoring into the one carrier path", + type: "satisfies", + target: "spec:carrier.markdown-authoring", + file: "src/extract/markdown.ts", + constant: "markdownAuthoringAnchor", + site: "export function reifyMarkdownCarrier", + }, + { + id: "impl:protocol.markdown-parser", + nodeType: "CodeNode", + label: "reifies the ruled Markdown parser input", + type: "satisfies", + target: "spec:carrier.markdown-parser", + file: "src/extract/markdown.ts", + constant: "markdownParserAnchor", + site: "export function reifyMarkdownCarrier", + }, + { + id: "test:protocol.markdown-parser", + nodeType: "Anchor", + label: "Markdown reifier tests verify the ruled parser", + type: "verifies", + target: "spec:carrier.markdown-parser", + file: "test/markdown-reifier.test.ts", + constant: "markdownParserTestAnchor", + site: 'describe("Markdown frontmatter reifier",', + }, + { + id: "impl:protocol.envelope-contract", + nodeType: "CodeNode", + label: "parses the bounded Markdown frontmatter envelope", + type: "satisfies", + target: "spec:carrier.envelope-contract", + file: "src/extract/markdown.ts", + constant: "envelopeContractAnchor", + site: "export function parseMarkdownFrontmatter", + }, + { + id: "test:protocol.envelope-contract", + nodeType: "Anchor", + label: "frontmatter contract tests verify the Markdown envelope", + type: "verifies", + target: "spec:carrier.envelope-contract", + file: "test/markdown-reifier.test.ts", + constant: "envelopeContractTestAnchor", + site: 'describe("Markdown frontmatter reifier",', + }, + { + id: "impl:protocol.prose-ownership", + nodeType: "CodeNode", + label: "reads Markdown body content through its prose owners", + type: "satisfies", + target: "spec:carrier.prose-ownership-rule", + file: "src/extract/markdown.ts", + constant: "proseOwnershipAnchor", + site: "export function readMarkdownBody", + }, + { + id: "test:protocol.prose-ownership", + nodeType: "Anchor", + label: "Markdown reifier tests verify prose ownership", + type: "verifies", + target: "spec:carrier.prose-ownership-rule", + file: "test/markdown-reifier.test.ts", + constant: "proseOwnershipTestAnchor", + site: 'describe("Markdown frontmatter reifier",', + }, + { + id: "impl:protocol.duplicate-id-exclusion", + nodeType: "CodeNode", + label: "excludes duplicated carrier ids from the graph", + type: "satisfies", + target: "spec:validation.duplicate-ids", + file: "src/extract/index.ts", + constant: "duplicateIdExclusionAnchor", + site: "function findDuplicatedIds", + }, + { + id: "test:protocol.duplicate-ids.dual-carrier", + nodeType: "Anchor", + label: "dual-carrier duplicate-ID contract verifies carrier exclusion", + type: "verifies", + target: "spec:validation.duplicate-ids.dual-carrier", + file: "test/self-hosting-duplicate-ids.test.ts", + constant: "dualCarrierDuplicateTestAnchor", + site: "bindExample(", + }, + { + id: "test:protocol.warn-level-signals.orphan-signal", + nodeType: "Anchor", + label: "the orphan point verifies the disconnected-spec warning", + type: "verifies", + target: "spec:validation.warn-level-signals.orphan-signal", + file: "test/self-hosting-validators.test.ts", + constant: "warnLevelOrphanTestAnchor", + site: "bindExample(orphanSignalContract", + }, + { + id: "test:protocol.warn-level-signals.ready-gap-signal", + nodeType: "Anchor", + label: "the gap point verifies the unverified-ready warning", + type: "verifies", + target: "spec:validation.warn-level-signals.ready-gap-signal", + file: "test/self-hosting-validators.test.ts", + constant: "warnLevelGapTestAnchor", + site: "bindExample(readyGapSignalContract", + }, + { + id: "test:protocol.referential-integrity.dangling-target", + nodeType: "Anchor", + label: "the dangling-target point verifies the unresolved-reference error", + type: "verifies", + target: "spec:validation.referential-integrity.dangling-target", + file: "test/self-hosting-validators.test.ts", + constant: "danglingTargetTestAnchor", + site: "bindExample(danglingTargetContract", + }, + { + id: "test:protocol.referential-integrity.did-you-mean", + nodeType: "Anchor", + label: "the near-miss point verifies the unique did-you-mean suggestion", + type: "verifies", + target: "spec:validation.referential-integrity.did-you-mean", + file: "test/self-hosting-validators.test.ts", + constant: "didYouMeanTestAnchor", + site: "bindExample(didYouMeanContract", + }, + { + id: "test:protocol.authored-honesty.section-authored-fact", + nodeType: "Anchor", + label: "the section point verifies the authoring-shape refusal", + type: "verifies", + target: "spec:validation.authored-honesty.section-authored-fact", + file: "test/self-hosting-validators.test.ts", + constant: "sectionAuthoredFactTestAnchor", + site: "bindExample(sectionAuthoredFactContract", + }, + { + id: "test:protocol.authored-honesty.unearned-stated-fact", + nodeType: "Anchor", + label: "the stated-fact point verifies the delivery-fact refusal", + type: "verifies", + target: "spec:validation.authored-honesty.unearned-stated-fact", + file: "test/self-hosting-validators.test.ts", + constant: "unearnedStatedFactTestAnchor", + site: "bindExample(unearnedStatedFactContract", + }, + { + id: "test:protocol.claim-separation.collapsed-edge-claim", + nodeType: "Anchor", + label: "the collapsed-claim point verifies the binding-edge contract row", + type: "verifies", + target: "spec:validation.claim-separation.collapsed-edge-claim", + file: "test/self-hosting-validators.test.ts", + constant: "collapsedEdgeClaimTestAnchor", + site: "bindExample(collapsedEdgeClaimContract", + }, + { + id: "test:protocol.claim-separation.unratified-descriptor", + nodeType: "Anchor", + label: "the unratified-kind point verifies the fail-closed descriptor law", + type: "verifies", + target: "spec:validation.claim-separation.unratified-descriptor", + file: "test/self-hosting-validators.test.ts", + constant: "unratifiedDescriptorTestAnchor", + site: "bindExample(unratifiedDescriptorContract", + }, + { + id: "test:protocol.verification-linkage.unbound-example", + nodeType: "Anchor", + label: "the unbound-example point verifies the incomplete spec-to-test trace", + type: "verifies", + target: "spec:validation.verification-linkage.unbound-example", + file: "test/self-hosting-validators.test.ts", + constant: "unboundExampleTestAnchor", + site: "bindExample(unboundExampleContract", + }, + { + id: "test:protocol.verification-linkage.unresolved-oracle", + nodeType: "Anchor", + label: "the unresolved-oracle point verifies the oracle binding refusal", + type: "verifies", + target: "spec:validation.verification-linkage.unresolved-oracle", + file: "test/self-hosting-validators.test.ts", + constant: "unresolvedOracleTestAnchor", + site: "bindExample(unresolvedOracleContract", + }, + { + id: "test:protocol.pack-coherence.incoherent-aggregate", + nodeType: "Anchor", + label: "the incoherent-aggregate point verifies both halves of the pack law", + type: "verifies", + target: "spec:validation.pack-coherence.incoherent-aggregate", + file: "test/self-hosting-validators.test.ts", + constant: "incoherentAggregateTestAnchor", + site: "bindExample(incoherentAggregateContract", + }, + { + id: "test:protocol.excludes.segment-boundary", + nodeType: "Anchor", + label: "the segment-boundary point verifies the exact-prefix exclusion rule", + type: "verifies", + target: "spec:extraction.excludes.segment-boundary", + file: "test/self-hosting-extraction.test.ts", + constant: "excludesSegmentBoundaryTestAnchor", + site: "bindExample(segmentBoundaryContract", + }, + { + id: "test:protocol.excludes.refused-path", + nodeType: "Anchor", + label: "the refused-path point verifies the malformed-exclusion refusal", + type: "verifies", + target: "spec:extraction.excludes.refused-path", + file: "test/self-hosting-extraction.test.ts", + constant: "excludesRefusedPathTestAnchor", + site: "bindExample(refusedPathContract", + }, + { + id: "test:protocol.schema-versioning.declared-version", + nodeType: "Anchor", + label: "the declared-version point verifies the readable payload version", + type: "verifies", + target: "spec:extraction.schema-versioning.declared-version", + file: "test/self-hosting-extraction.test.ts", + constant: "schemaVersioningTestAnchor", + site: "bindExample(declaredVersionContract", + }, + { + id: "test:protocol.stable-ids.namespaced-round-trip", + nodeType: "Anchor", + label: "the round-trip point verifies the namespaced dotted-path grammar", + type: "verifies", + target: "spec:model.stable-ids.namespaced-round-trip", + file: "test/self-hosting-model.test.ts", + constant: "namespacedRoundTripTestAnchor", + site: "bindExample(namespacedRoundTripContract", + }, + { + id: "test:protocol.stable-ids.malformed-refusal", + nodeType: "Anchor", + label: "the malformed point verifies the lowercase-namespace refusal", + type: "verifies", + target: "spec:model.stable-ids.malformed-refusal", + file: "test/self-hosting-model.test.ts", + constant: "malformedRefusalTestAnchor", + site: "bindExample(malformedRefusalContract", + }, + { + id: "test:protocol.markdown-parser.bounded-parity", + nodeType: "Anchor", + label: "the bounded-parity point verifies one shared finding class and its split outcomes", + type: "verifies", + target: "spec:carrier.markdown-parser.bounded-parity", + file: "test/self-hosting-carrier.test.ts", + constant: "boundedParityTestAnchor", + site: "bindExample(boundedParityContract", + }, + { + id: "test:protocol.sdp-import.round-trip", + nodeType: "Anchor", + label: "TypeScript import round-trip contract preserves authored data", + type: "verifies", + target: "spec:carrier.sdp-import.round-trip", + file: "test/self-hosting-sdp-import.test.ts", + constant: "sdpImportRoundTripTestAnchor", + site: "bindExample(", + }, + { + id: "impl:protocol.anchor-extraction", + nodeType: "CodeNode", + label: "anchor-constant reification seam", + type: "satisfies", + target: "spec:model.anchors", + file: "src/extract/anchors.ts", + constant: "anchorExtractionAnchor", + site: "const ANCHOR_BUILDER_TARGET_FIELDS", + }, + { + id: "impl:protocol.anchor-model", + nodeType: "CodeNode", + label: "binding-only anchor model builders", + type: "satisfies", + target: "spec:model.anchors", + file: "src/model/anchors.ts", + constant: "anchorModelAnchor", + site: "export function specTest", + }, + { + id: "impl:protocol.pack-aggregate", + nodeType: "CodeNode", + label: "Pack aggregate and model references", + type: "satisfies", + target: "spec:model.pack-aggregate", + file: "src/model/pack.ts", + constant: "packAggregateAnchor", + site: "export function pack", + }, + { + id: "impl:protocol.spec-descriptors", + nodeType: "CodeNode", + label: "Spec kind, altitude, and readiness coordinates", + type: "satisfies", + target: "spec:model.core-model", + file: "src/model/descriptors.ts", + constant: "specDescriptorsAnchor", + site: "export const SPEC_KIND_DISPLAY_LABELS", + }, + { + id: "impl:protocol.spec-primitive", + nodeType: "CodeNode", + label: "Spec envelope and enrich-in-place shape", + type: "satisfies", + target: "spec:model.core-model", + file: "src/model/spec.ts", + constant: "specPrimitiveAnchor", + site: "export function spec", + }, + { + id: "impl:protocol.spec-relations", + nodeType: "CodeNode", + label: "declared Spec relation builders", + type: "satisfies", + target: "spec:model.relations", + file: "src/model/relations.ts", + constant: "specRelationsAnchor", + site: "export function supersedes", + }, + { + id: "impl:protocol.spec-sections", + nodeType: "CodeNode", + label: "typed Spec section shapes", + type: "satisfies", + target: "spec:model.spec-sections", + file: "src/model/sections.ts", + constant: "specSectionsAnchor", + site: "export interface SpecSections", + }, + { + id: "impl:protocol.stable-ids", + nodeType: "CodeNode", + label: "stable ID grammar parser", + type: "satisfies", + target: "spec:model.stable-ids", + file: "src/ids.ts", + constant: "stableIdsAnchor", + site: "export function parseId", + }, + { + id: "impl:protocol.verifier-semantics", + nodeType: "CodeNode", + label: "readiness clauses over direct verification bindings", + type: "satisfies", + target: "spec:model.spec-sections", + file: "src/validate/readiness-floor.ts", + constant: "verifierSemanticsAnchor", + site: "export function evaluateReadinessFloor", + }, + { + id: "impl:protocol.validation-families", + nodeType: "CodeNode", + label: "conformance and honesty validator registry", + type: "satisfies", + target: "spec:validation.two-check-families", + file: "src/validate/validators.ts", + constant: "validationFamiliesAnchor", + site: "export const graphValidatorIds", + }, + { + id: "impl:protocol.projections-model", + nodeType: "CodeNode", + label: "pure generated projection page contract", + type: "satisfies", + target: "spec:consumers.projections-model", + file: "src/projections/design-review.ts", + constant: "projectionModelAnchor", + site: "export interface DesignReviewPage", + }, + { + id: "impl:protocol.agent-surface", + nodeType: "CodeNode", + label: "typed graph reader and agent entry adapters", + type: "satisfies", + target: "spec:consumers.agent-surface", + file: "src/reader/reader.ts", + constant: "agentSurfaceAnchor", + site: "export function createReader", + }, + { + id: "impl:protocol.reader-impact", + nodeType: "CodeNode", + label: "file-level reader blast-radius contract", + type: "satisfies", + target: "spec:consumers.reader", + file: "src/reader/reader.ts", + constant: "readerImpactAnchor", + site: "export interface BlastRadius", + }, + { + id: "impl:protocol.reader", + nodeType: "CodeNode", + label: "thin typed graph reader construction", + type: "satisfies", + target: "spec:consumers.reader", + file: "src/reader/reader.ts", + constant: "readerAnchor", + site: "export function createReader", + }, + { + id: "impl:protocol.design-review", + nodeType: "CodeNode", + label: "renders the contextual Design Review projection", + type: "satisfies", + target: "spec:consumers.design-review", + file: "src/projections/design-review.ts", + constant: "designReviewAnchor", + site: "export function renderDesignReview", + }, + { + id: "impl:protocol.exclusion-surface", + nodeType: "CodeNode", + label: "strict root-relative exclusion input for both extraction surfaces", + type: "satisfies", + target: "spec:extraction.excludes", + file: "src/extract/index.ts", + constant: "exclusionSurfaceAnchor", + site: "export interface ExtractOptions", + }, + { + id: "impl:protocol.graph-claims", + nodeType: "CodeNode", + label: "declares the graph claim taxonomy", + type: "satisfies", + target: "spec:extraction.claim-taxonomy", + file: "src/graph/schema.ts", + constant: "graphClaimsAnchor", + site: "export const graphClaims", + }, + { + id: "impl:protocol.regenerability", + nodeType: "CodeNode", + label: "repeats graph and contract producers for deterministic regeneration", + type: "satisfies", + target: "spec:extraction.regenerability", + file: "src/cli/build-command.ts", + constant: "regenerabilityAnchor", + site: "export function runBuild", + }, + { + id: "impl:protocol.schema-version", + nodeType: "CodeNode", + label: "declares the graph schema version", + type: "satisfies", + target: "spec:extraction.schema-versioning", + file: "src/graph/schema.ts", + constant: "schemaVersionAnchor", + site: "export const schemaVersion", + }, + { + id: "impl:protocol.executable-contracts", + nodeType: "CodeNode", + label: "derives step and example-space contracts from the graph", + type: "satisfies", + target: "spec:extraction.executable-contracts", + file: "src/codegen/contracts.ts", + constant: "executableContractsAnchor", + site: "export function generateContracts", + }, + { + id: "impl:protocol.example-runner", + nodeType: "CodeNode", + label: "plans and executes a bound example against the caller's world", + type: "satisfies", + target: "spec:extraction.example-runner", + file: "src/runner/index.ts", + constant: "exampleRunnerAnchor", + site: "export function planExample", + }, + { + id: "impl:protocol.slot-notation", + nodeType: "CodeNode", + label: "parses slot groups and normalizes a step to its skeleton", + type: "satisfies", + target: "spec:carrier.slot-notation", + file: "src/notation/slots.ts", + constant: "slotNotationAnchor", + site: "export function parseSlots", + }, + { + id: "test:protocol.executable-contracts.concreteness-refusal", + nodeType: "Anchor", + label: "the concreteness point verifies the unbound-slot refusal", + type: "verifies", + target: "spec:extraction.executable-contracts.concreteness-refusal", + file: "test/self-hosting-extraction.test.ts", + constant: "concretenessRefusalTestAnchor", + site: "bindExample(concretenessRefusalContract", + }, + { + id: "test:protocol.executable-contracts.multi-entry-example", + nodeType: "Anchor", + label: "the multi-entry point verifies the named second entry", + type: "verifies", + target: "spec:extraction.executable-contracts.multi-entry-example", + file: "test/self-hosting-extraction.test.ts", + constant: "multiEntryExampleTestAnchor", + site: "bindExample(multiEntryExampleContract", + }, + { + id: "test:protocol.executable-contracts.case-colliding-path", + nodeType: "Anchor", + label: "the collision point verifies the all-or-nothing withholding", + type: "verifies", + target: "spec:extraction.executable-contracts.case-colliding-path", + file: "test/self-hosting-extraction.test.ts", + constant: "caseCollidingPathTestAnchor", + site: "bindExample(caseCollidingPathContract", + }, + { + id: "test:protocol.example-runner.step-order", + nodeType: "Anchor", + label: "the step-order point verifies contract order and the one handler per step", + type: "verifies", + target: "spec:extraction.example-runner.step-order", + file: "test/self-hosting-extraction.test.ts", + constant: "exampleRunnerStepOrderTestAnchor", + site: "bindExample(stepOrderContract", + }, + { + id: "test:protocol.example-runner.red-step-naming", + nodeType: "Anchor", + label: "the red-step point verifies the self-naming failure law", + type: "verifies", + target: "spec:extraction.example-runner.red-step-naming", + file: "test/self-hosting-extraction.test.ts", + constant: "exampleRunnerRedStepTestAnchor", + site: "bindExample(redStepNamingContract", + }, + { + id: "test:protocol.slot-notation.typed-declaration", + nodeType: "Anchor", + label: "the declaration point verifies the typed form and its skeleton", + type: "verifies", + target: "spec:carrier.slot-notation.typed-declaration", + file: "test/self-hosting-carrier.test.ts", + constant: "slotNotationTypedTestAnchor", + site: "bindExample(typedDeclarationContract", + }, + { + id: "test:protocol.slot-notation.refused-guess", + nodeType: "Anchor", + label: "the refusal point verifies prose braces and the unusable slot", + type: "verifies", + target: "spec:carrier.slot-notation.refused-guess", + file: "test/self-hosting-carrier.test.ts", + constant: "slotNotationRefusedTestAnchor", + site: "bindExample(refusedGuessContract", + }, + { + id: "test:protocol.anchors.lookalike-refusal", + nodeType: "Anchor", + label: "the lookalike point verifies that a consumer-local builder mints nothing", + type: "verifies", + target: "spec:model.anchors.lookalike-refusal", + file: "test/self-hosting-model.test.ts", + constant: "lookalikeRefusalTestAnchor", + site: "bindExample(lookalikeRefusalContract", + }, + { + id: "test:protocol.anchors.physical-identity", + nodeType: "Anchor", + label: "the physical-identity point verifies the resolved relative builder import", + type: "verifies", + target: "spec:model.anchors.physical-identity", + file: "test/self-hosting-model.test.ts", + constant: "physicalIdentityTestAnchor", + site: "bindExample(physicalIdentityContract", + }, + { + id: "test:protocol.two-check-families.split-report", + nodeType: "Anchor", + label: "the split-report point verifies both families in one aggregate report", + type: "verifies", + target: "spec:validation.two-check-families.split-report", + file: "test/self-hosting-validators.test.ts", + constant: "splitReportTestAnchor", + site: "bindExample(splitReportContract", + }, +] as const; diff --git a/test/self-hosting-oracle/carrier.ts b/test/self-hosting-oracle/carrier.ts new file mode 100644 index 0000000..cf41b10 --- /dev/null +++ b/test/self-hosting-oracle/carrier.ts @@ -0,0 +1,301 @@ +// The authored descriptors of the `carrier` family of the self-hosting corpus — +// human transcription of intended truth, never computed from the derived graph. Extraction must +// reproduce every value here exactly; a disagreement is drift to resolve on one side or the other. + +export const carrierSpecs = [ + { + id: "spec:carrier.markdown-authoring", + specKind: "behavior", + altitude: "feature", + readiness: "defined", + file: "specs/carrier/markdown-authoring.sdp.md", + title: "Markdown authoring enters the one graph", + narrative: null, + sections: { + intent: { + outcome: "Author new Protocol Specs in Markdown without creating a second truth path.", + }, + behavior: { + rules: [ + "Markdown and TypeScript carriers feed the same reification and graph-derivation path.", + ], + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:carrier.envelope-contract", + specKind: "contract", + altitude: "feature", + readiness: "ready", + file: "specs/carrier/envelope-contract.sdp.md", + title: "The Markdown envelope is explicit and bounded", + narrative: null, + sections: { + intent: { + outcome: "Make a Markdown Spec's identity and descriptors deterministic to reify.", + }, + behavior: { + rules: [ + "A Markdown Spec declares id, kind, altitude, readiness, and relations in bounded YAML frontmatter; its first H1 declares title.", + "The envelope key set is closed and every one of its five keys is required: a key outside the set is refused rather than absorbed, and a missing key refuses the document rather than being defaulted.", + "`relations: {}` is written explicitly when the logical relation set is empty — honest carrier syntax, not a new logical requirement: the physical key catches a truncated envelope at reification while the model itself stays relation-optional.", + "A derived name is never authorable in the envelope: a delivery-fact or graph-shape key is refused under its own finding class, because delivery facts are derived and never authored.", + "The Protocol owns the envelope grammar and the parser policy while the pinned YAML library stays a swappable representation behind that contract, so an unsupported YAML construct is refused within explicit byte bounds on the carrier and its frontmatter rather than silently becoming carrier semantics.", + "The realizing entrypoints are `readMarkdownEnvelope` in `src/extract/markdown-envelope.ts` and `parseMarkdownFrontmatter` in `src/extract/markdown-frontmatter.ts`.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:carrier.markdown-parser", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + file: "specs/carrier/markdown-parser.sdp.md", + title: "The product parser reifies the ruled Markdown subset", + narrative: null, + sections: { + intent: { + problem: "Prevent carrier-specific graph and validation paths from diverging.", + outcome: "Reify authored Markdown without a second graph or validation path.", + value: "Markdown-carried intent remains subject to the Protocol's deterministic checks.", + }, + behavior: { + rules: [ + "The parser accepts only the ruled heading grammar and excludes one malformed carrier while continuing healthy siblings.", + "The ruled Markdown parser has bounded finding-class parity with the TypeScript carrier for `extract/non-static-envelope`, `extract/invalid-id`, `extract/duplicate-id`, `extract/reserved-property`, `extract/unowned-prose`, and `extract/unrecognized-property`; the shared validator ID is the claim, while severity and extract-versus-refuse outcomes remain carrier-specific.", + "Named non-claim — `extract/parse-error` remains distinct because YAML/frontmatter parsing has no TypeScript parser-diagnostic analogue.", + "Named non-claim — `extract/non-static-section` remains distinct because TypeScript degrades optional section properties while Markdown refuses malformed documents whole.", + "Named non-claim — `extract/unrecognized-statement` remains distinct because Markdown owns prose and structures, not TypeScript statement recognition.", + "Named non-claim — `extract/misplaced-authoring` remains distinct because Markdown has no executable authoring-call surface.", + ], + exampleSpace: { + given: ["the paired carrier probes named {probe:string}"], + when: ["both carriers reify their probe"], + [["t", "hen"].join("")]: [ + "both carriers report the finding class {findingId:string}", + 'the TypeScript carrier reports severity {typeScriptSeverity:"warning"|"error"} and extracts {typeScriptSpecs:number} specs', + 'the Markdown carrier reports severity {markdownSeverity:"warning"|"error"} and extracts {markdownSpecs:number} specs', + ], + }, + }, + verification: { + mode: "executable", + criteria: [ + "`test/extract-parity.test.ts` executes the settled finding-class parity matrix, including the six same-class findings, their carrier-specific outcomes, and four named non-claims.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:carrier.markdown-parser.bounded-parity", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/carrier/markdown-parser.bounded-parity.sdp.md", + title: "One finding class is shared while the carriers' outcomes stay their own", + narrative: null, + sections: { + intent: { + outcome: + "Execute one same-class row of the parity matrix, including the outcomes it never claims.", + }, + behavior: { + examples: [ + { + given: ['the paired carrier probes named {probe: "unrecognized-property"}'], + when: ["both carriers reify their probe"], + [["t", "hen"].join("")]: [ + 'both carriers report the finding class {findingId: "extract/unrecognized-property"}', + 'the TypeScript carrier reports severity {typeScriptSeverity: "warning"} and extracts {typeScriptSpecs: 1} specs', + 'the Markdown carrier reports severity {markdownSeverity: "error"} and extracts {markdownSpecs: 0} specs', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:carrier.sdp-import", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + file: "specs/carrier/sdp-import.sdp.md", + title: "TypeScript-carried Specs can become Markdown twins", + narrative: null, + sections: { + intent: { + actor: "A coding agent or maintainer.", + outcome: + "Convert a TypeScript-carrier Spec into an idiomatic `.sdp.md` twin beside its source.", + value: + "The TypeScript DSL survives as an import source while Markdown becomes the authored twin.", + }, + behavior: { + rules: [ + "Import writes the emitted Markdown sibling beside the TypeScript carrier and never deletes the source carrier.", + "Import refuses an existing Markdown sibling rather than overwriting it.", + "Refusal outcomes retain the TypeScript reifier findings and add import-local findings honestly.", + "Import consumes the TypeScript reifier so source acceptance follows one validation path.", + "A batch scans only bounded source directories, canonicalizes physical carrier identity, and computes every refusal and target collision before publishing any sibling.", + "Publication prepares exclusive temporary siblings and atomically creates targets without clobbering; rollback attempts every artifact, reports survivors, and never deletes a TypeScript source.", + ], + exampleSpace: { + given: ["a TS-carrier spec"], + when: ["importTypeScriptSpec runs"], + [["t", "hen"].join("")]: ["the emitted Markdown re-parses to an equal graph"], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:carrier.sdp-import.round-trip", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/carrier/sdp-import.round-trip.sdp.md", + title: "A TypeScript carrier survives import as an equal Markdown graph", + narrative: null, + sections: { + intent: { + outcome: "Execute the import round-trip against a TypeScript-carrier fixture.", + }, + behavior: { + examples: [ + { + given: ["a TS-carrier spec"], + when: ["importTypeScriptSpec runs"], + [["t", "hen"].join("")]: ["the emitted Markdown re-parses to an equal graph"], + }, + ], + }, + verification: { + mode: "executable", + criteria: [ + "The bound test runs `assertAuthoredRoundTrip` against the import behavior fixture.", + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:carrier.prose-ownership-rule", + specKind: "rule", + altitude: "story", + readiness: "ready", + file: "specs/carrier/prose-ownership-rule.sdp.md", + title: "Every prose edge has one owner", + narrative: null, + sections: { + intent: { outcome: "Keep free prose in the graph without ambiguous attachment." }, + behavior: { + rules: [ + "Narrative lives before the first H2 and is owned directly by the Spec; it is Spec content, never an envelope field.", + "A description is owned only by a singular section and lives under that section's own heading; the array-shaped constraints section has no description owner, so its explanatory prose belongs in narrative or intent instead.", + "Unowned prose — prose standing under no typed owner — is refused loudly rather than attached by guess or dropped in silence.", + "Prose is stored as graph content inside its typed owner, never as a file pointer or a heading-path key: a consumer reads prose from the graph without re-parsing the document, and churned document structure carries no identity.", + "The realizing entrypoints are `parseMarkdownBody` in `src/extract/markdown-body.ts` and `mapOwner` in `src/extract/markdown-body-owners.ts`.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:carrier.slot-notation", + specKind: "rule", + altitude: "story", + readiness: "ready", + file: "specs/carrier/slot-notation.sdp.md", + title: "Slot notation declares, binds, and refuses to guess", + narrative: null, + sections: { + intent: { + outcome: + "Give step text one owned typed placeholder syntax whose normalized identity a generated contract can key on.", + }, + behavior: { + rules: [ + "A slot group opens with an identifier; a brace group that does not open with one is prose, and prose is never policed.", + "A vocabulary slot declares a type only in the ratified type form — `number`, `string`, `boolean`, or a closed union of two or more quoted literals — while an example binds one scalar literal in the same position.", + "The skeleton — every slot group normalized to `{name}` with prose braces left untouched — is the step's identity: it keys the generated step contract, matches an example step to its vocabulary entry, and makes a declaration and its binding the same step.", + "An identifier-led group whose remainder parses as neither a type nor a value stays a named but unusable slot: it declares nothing, binds nothing, and reads as unbound rather than being guessed into meaning.", + "The single-quoted-literal form parses as a binding, and what it would declare in a vocabulary is unruled — so a vocabulary consumer treats it as declaring nothing and says so rather than inventing a one-value dimension.", + "Lexical degradation stays local: a stray or unterminated brace group is prose only up to the next candidate, so it never swallows a well-formed binding that follows it.", + "The realizing entrypoints are `parseSlots` and `stepSkeleton` in `src/notation/slots.ts`.", + ], + exampleSpace: { + given: ["the step text {stepText:string}"], + when: ["the notation parses the step text"], + [["t", "hen"].join("")]: [ + "the notation finds {slotCount:number} slot groups", + 'the first group has the form {form:"bare"|"typed"|"bound"|"malformed"} and the name {slotName:string}', + "the step skeleton is {skeleton:string}", + ], + }, + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:carrier.slot-notation.typed-declaration", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/carrier/slot-notation.typed-declaration.sdp.md", + title: "A typed declaration normalizes to the skeleton its binding shares", + narrative: null, + sections: { + intent: { + outcome: "Execute the declaration form and the skeleton identity on one vocabulary step.", + }, + behavior: { + examples: [ + { + given: ['the step text {stepText: "a cart with {n:number} line items"}'], + when: ["the notation parses the step text"], + [["t", "hen"].join("")]: [ + "the notation finds {slotCount: 1} slot groups", + 'the first group has the form {form: "typed"} and the name {slotName: "n"}', + 'the step skeleton is {skeleton: "a cart with {n} line items"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:carrier.slot-notation.refused-guess", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/carrier/slot-notation.refused-guess.sdp.md", + title: "A stray brace stays prose while an unusable group stays a named slot", + narrative: null, + sections: { + intent: { + outcome: + "Execute the refuse-to-guess posture where a stray brace precedes an unparsable group.", + }, + behavior: { + examples: [ + { + given: ['the step text {stepText: "a stray { then {n: maybe} line items"}'], + when: ["the notation parses the step text"], + [["t", "hen"].join("")]: [ + "the notation finds {slotCount: 1} slot groups", + 'the first group has the form {form: "malformed"} and the name {slotName: "n"}', + 'the step skeleton is {skeleton: "a stray { then {n} line items"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, +] as const; diff --git a/test/self-hosting-oracle/consumers.ts b/test/self-hosting-oracle/consumers.ts new file mode 100644 index 0000000..714f288 --- /dev/null +++ b/test/self-hosting-oracle/consumers.ts @@ -0,0 +1,143 @@ +// The authored descriptors of the `consumers` family of the self-hosting corpus — +// human transcription of intended truth, never computed from the derived graph. Extraction must +// reproduce every value here exactly; a disagreement is drift to resolve on one side or the other. + +export const consumersSpecs = [ + { + id: "spec:consumers.projections-model", + specKind: "model", + altitude: "feature", + readiness: "defined", + file: "specs/consumers/projections-model.sdp.md", + title: "Projections fan out from one graph without becoming truth stores", + narrative: null, + sections: { + intent: { + outcome: + "Give agents and humans consumer-specific views while preserving the repository as the only canonical source.", + }, + model: { + terms: { + baseline: + "A named approved snapshot whose signed git tag is the approval artifact, with approval remaining outside the authored model.", + "curated graph": + "The authored architectural read model of declared intent and anchored bindings, valued for editorial sparsity.", + curation: + "The deliberate difference between the sparse curated graph and the code-structure surface; it is not drift.", + discipline: + "A lens or projection that filters or groups Specs by kind or section; it is not a phase to pass through.", + "measured curation": + "In a measured comparison, the curated graph selected from single-digit to about one quarter of the mechanical impact-graph surface.", + "impact graph": + "A separately derived code-structure surface for exhaustive usage and blast-radius questions, valued for exhaustiveness and never promoted into architecture.", + "phase / iteration / milestone": + "Descriptive vocabulary for optional roadmap projections, never gates or enforced sequences.", + projection: + "A pure, disposable, regenerable function of the graph that produces a consumer artifact without becoming a second source of truth.", + reader: + "The thin typed front door that decodes graph joins and taxonomy once, returns composable data, and persists nothing.", + release: "A tagged set surfaced as a git-tag projection.", + }, + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:consumers.agent-surface", + specKind: "behavior", + altitude: "feature", + readiness: "defined", + file: "specs/consumers/agent-surface.sdp.md", + title: "Agents script a visible typed graph", + narrative: null, + sections: { + intent: { + outcome: + "Let an agent obtain and compose graph context without rebuilding joins or navigating a fixed verb wall.", + }, + behavior: { + rules: [ + "The agent surface exposes a visible, self-describing typed graph through the CLI; the schema is the contract and agents script the graph directly.", + "The reader constructs decoded joins and claim taxonomy once, then returns plain composable data without persisting graph state.", + "Entry adapters bridge strings, files, and changesets to curated graph context; file-level blast radius names coverage-unknown files rather than implying exhaustive reach.", + "Context efficiency is an empirical result: a measured comparison may show structured graph context uses fewer supplied tokens than a comparable raw-text workflow while preserving the task-relevant result.", + "Measured evidence: a multi-probe agent comparison used about one fifth of the tokens of a comparable grep or verb-API workflow while preserving task-relevant conclusions.", + ], + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:consumers.design-review", + specKind: "behavior", + altitude: "feature", + readiness: "defined", + file: "specs/consumers/design-review.sdp.md", + title: "Design Review renders graph context without becoming a gate", + narrative: null, + sections: { + intent: { + outcome: + "Give a human a regenerable, contextual view for deciding how to state readiness without recording approval as graph truth.", + }, + behavior: { + rules: [ + "Design Review renders a Spec or Pack in context with relations, bindings, delivery badges, design questions, and findings from the graph.", + "The review is a pure projection that resolves through ordinary source edits, git, and conformance checks; it stores no findings and writes no canonical source.", + "A human may use the review context when stating readiness, while validators check only the structural readiness floor and never record or require review approval.", + "The MVP view is deterministic generated Markdown with an index and pages for Specs and Packs; richer visual representations remain outside this behavior.", + "Rendering encodes by Markdown syntax context: prose and table fields escape structural characters, fenced JSON preserves authored keys and values through JSON encoding, and inline code uses a delimiter that preserves literal backticks.", + ], + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:consumers.reader", + specKind: "behavior", + altitude: "feature", + readiness: "defined", + file: "specs/consumers/reader.sdp.md", + title: "The reader bridges agent entry points to composable graph context", + narrative: null, + sections: { + intent: { + outcome: + "Let agents enter the curated graph from the strings, files, and changesets they already have without rebuilding its joins or taxonomy.", + }, + behavior: { + rules: [ + "`createReader` constructs a fresh thin typed loader that decodes graph joins, claims, delivery facts, derived readiness, and validation findings once, then returns plain composable data without persisting state.", + "`findByConcept` and `byFile` bridge strings and extraction-root-relative files to the graph's recorded context.", + "The reader's `blastRadius` surface maps changed files to directly impacted Specs and Packs, their explicit one-hop at-risk neighbors, and every coverage-unknown file.", + "File-level blast radius reports curated graph reach without claiming exhaustive symbol-level usage reach.", + ], + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:consumers.edit-model", + specKind: "behavior", + altitude: "feature", + readiness: "defined", + file: "specs/consumers/edit-model.sdp.md", + title: "Views compose scoped intent instead of patching canonical source", + narrative: null, + sections: { + intent: { + outcome: + "Let a view frame a requested change without giving derived surfaces a direct write path to canonical source.", + }, + behavior: { + rules: [ + "A view composes scoped intent, bounded by a Spec, its neighbors, a Pack, or open questions, and hands that intent to an agent.", + "The agent edits source as a human would, git records the ordinary edit, and the same conformance and honesty checks evaluate it.", + "Lifecycle changes such as splitting, combining, refining, or deleting are ordinary source and git edits rather than structured patches from a derived view.", + "No single realizing entrypoint exists for intent composition; this defined behavior records design intent and has no code anchor or verifier.", + ], + }, + }, + deliveryFacts: [], + }, +] as const; diff --git a/test/self-hosting-oracle/decisions.ts b/test/self-hosting-oracle/decisions.ts new file mode 100644 index 0000000..3286895 --- /dev/null +++ b/test/self-hosting-oracle/decisions.ts @@ -0,0 +1,562 @@ +// The authored descriptors of the `decisions` family of the self-hosting corpus — +// human transcription of intended truth, never computed from the derived graph. Extraction must +// reproduce every value here exactly; a disagreement is drift to resolve on one side or the other. + +export const decisionsSpecs = [ + { + id: "spec:decisions.exclusion-contract", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/exclusion-contract.sdp.md", + title: "Consumer exclusions stay exact", + narrative: null, + sections: { + intent: { + outcome: "Keep consumer-selected omissions precise and unsurprising.", + }, + decision: { + context: "Exclusion input crosses from a consumer into canonical source discovery.", + decision: "Consumers declare exclusions as exact root-relative POSIX path prefixes.", + rationale: [ + "Semantic globbing and path normalization are rejected because they make an omission broader or different from the path the consumer supplied.", + ], + consequences: [ + "A prefix excludes only itself and slash-delimited descendants; malformed paths, including Windows-drive absolutes, are refused.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.plain-language-references", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/plain-language-references.sdp.md", + title: "Durable references lead with meaning", + narrative: null, + sections: { + intent: { + outcome: "Keep design rationale readable without decoding registries.", + }, + decision: { + context: "Decision codes are useful lookup keys but poor standalone prose.", + decision: + "Durable references lead with plain-language meaning; decision codes follow parenthetically when useful.", + rationale: ["Meaning survives registry churn."], + consequences: ["AGENTS and plans lead with names."], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.concept-docs-dissolve", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/concept-docs-dissolve.sdp.md", + title: "Concept documents may dissolve after executable truth lands", + narrative: null, + sections: { + intent: { + outcome: "Keep intended truth authoritative while allowing exposition to shrink.", + }, + decision: { + context: "Concept documents currently carry both laws and unsettled representation.", + decision: + "Concept documents may dissolve only after their semantic contract is carried by executable Specs and lean registries.", + rationale: ["Executable truth is easier to validate and consume."], + consequences: [ + "Deletion follows the carrying work, per document, and is never bundled into the change that lands the carrier.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.one-validation-path", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/one-validation-path.sdp.md", + title: "Validation follows one graph", + narrative: null, + sections: { + intent: { + outcome: + "Keep conformance and honesty checks aligned with the source the graph actually represents.", + }, + decision: { + context: + "Source can be statically reified without matching what an executing import would evaluate.", + decision: + "Validators consume the derived graph through one path: source, extraction, graph, then checks.", + rationale: [ + "A parallel import-time validation path can approve values absent from the graph.", + ], + consequences: [ + "Typed authoring feedback and extraction findings remain distinct from graph validation rather than becoming a second validator.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.sdp-ts-extension", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/sdp-ts-extension.sdp.md", + title: "Spec extensions identify the carrier without colliding with tests", + narrative: null, + sections: { + intent: { + outcome: + "Keep authored Spec files recognizable to tools and safe beside ordinary test conventions.", + }, + decision: { + context: + "A carrier filename must distinguish authored Specs from test files and remain useful when files are colocated.", + decision: + "Markdown Specs use `.sdp.md`; `.sdp.ts` names the surviving TypeScript DSL import source and lawful per-ID option.", + rationale: [ + "Test-glob extensions and path-only conventions either misclassify Specs or hide their identity.", + ], + consequences: [ + "Carrier-specific tooling can target the compound extension without changing the `Spec` model name.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.point-per-example", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/point-per-example.sdp.md", + title: "Each example binds one point", + narrative: null, + sections: { + intent: { + outcome: + "Keep example-space coverage and outcome witnesses unambiguous while preserving compact authoring views.", + }, + decision: { + context: "A single example must remain one witness in its parent's typed example space.", + decision: + "An example binds exactly one point; table syntax may expand statically into sibling examples and renderers may project siblings as a table.", + rationale: [ + "Point sets make concreteness and witness semantics conditional, while banning table sugar taxes a surface layer that can translate honestly.", + ], + consequences: [ + "The graph never stores multi-point examples even when a carrier offers tabular authoring.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.carrier-ruling", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/carrier-ruling.sdp.md", + title: "Markdown is the default Spec carrier", + narrative: null, + sections: { + intent: { + outcome: + "Give every Spec kind one readable canonical authoring surface without losing a lawful escape hatch.", + }, + decision: { + context: + "The carrier must express all Spec kinds without creating an unbounded tooling obligation or a dual-source truth path.", + decision: + "Specs default to Markdown; Packs remain TS until a Pack syntax ruling; the TS DSL survives as import source and a lawful per-ID option.", + rationale: [ + "An owned grammar and a permanent kind split both add surface cost without a demonstrated expressive gain, while retiring the DSL removes a useful bounded option.", + ], + consequences: [ + "Each ID has one canonical surface, and Markdown tooling is the default path for authored Specs.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.prose-ownership", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/prose-ownership.sdp.md", + title: "Prose belongs to typed graph owners", + narrative: null, + sections: { + intent: { + outcome: + "Preserve free prose for projections without making its attachment ambiguous or forcing consumers to re-parse files.", + }, + decision: { + context: "Document prose needs a stable graph home when section structure evolves.", + decision: + "Free prose is stored as a narrative or a description on its typed owner; unowned prose is refused.", + rationale: [ + "File pointers force consumer re-parsing, while heading-path keys make churned document structure carry identity.", + ], + consequences: [ + "Prose remains graph content inside typed shapes and ambiguous attachment fails loudly.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.envelope-grammar-posture", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/envelope-grammar-posture.sdp.md", + title: "The Protocol owns the envelope grammar", + narrative: null, + sections: { + intent: { + outcome: + "Keep authored envelope meaning stable while retaining a replaceable parsing representation.", + }, + decision: { + context: "YAML parsing behavior alone cannot define the Protocol's authored contract.", + decision: + "The Protocol owns a bounded envelope grammar and parser policy; the pinned YAML library is a swappable representation behind that contract.", + rationale: [ + "Permissive parsing lets library behavior define meaning, while an owned YAML parser recreates the rejected grammar-maintenance burden.", + ], + consequences: [ + "Unsupported YAML constructs are refused within explicit resource bounds instead of silently becoming carrier semantics.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.executable-meta-model", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/executable-meta-model.sdp.md", + title: "The Protocol is an executable meta-model", + narrative: null, + sections: { + intent: { outcome: "Make delivery intent conform to one typed, self-validating contract." }, + decision: { + context: "Delivery tools can describe work without making their model executable.", + decision: + "The Protocol models authored Specs, Packs, and anchors in typed code, derives one graph, and checks conformance and honesty.", + rationale: ["Executable specs alone and workflow tooling omit the meta-model contract."], + consequences: [ + "The Protocol is deterministically validated without judging content quality or enforcing workflow.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.adopt-the-nouns", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/adopt-the-nouns.sdp.md", + title: "Delivery nouns remain familiar without workflow gates", + narrative: null, + sections: { + intent: { + outcome: + "Keep the Protocol legible to delivery practitioners without adopting a lifecycle machine.", + }, + decision: { + context: + "Shared delivery vocabulary is useful, but process-state language hides epistemic distinctions.", + decision: + "The Protocol adopts established delivery nouns and rejects process state-machine and lifecycle gating.", + rationale: [ + "Invented terminology taxes users, while workflow states reverse the Protocol's conformance-only boundary.", + ], + consequences: [ + "Terms must be concrete, unambiguous, and carry authored-versus-derived status where it matters.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.one-primitive", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/one-primitive.sdp.md", + title: "One Spec carries named delivery coordinates", + narrative: null, + sections: { + intent: { + outcome: + "Preserve one durable authored primitive while making familiar delivery forms precise.", + }, + decision: { + context: + "Delivery statements vary by truth category, scope, and maturity without needing separate artifact types.", + decision: + "A Spec is enriched in place with kind, altitude, and readiness; familiar delivery nouns are named coordinates on that primitive.", + rationale: [ + "Separate types per coordinate combination multiply shapes and break enrich-in-place identity.", + ], + consequences: ["Domains and capabilities are projections or Packs, not extra altitudes."], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.protocol-naming", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/protocol-naming.sdp.md", + title: "The meta-model is a software delivery protocol", + narrative: null, + sections: { + intent: { outcome: "Name the product and its meta-layer without implying workflow control." }, + decision: { + context: + "The meta-layer needs a name that communicates a conformance contract rather than a process engine.", + decision: + "The product is the Libar Software Delivery Protocol, shortened to the Protocol; `sdp` names its CLI.", + rationale: [ + "Protocol names an executable conformance contract more honestly than process while retaining process for the modeled activity.", + ], + consequences: [ + "Product, package, repository, and CLI names stay aligned around the Protocol.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.binding-not-liveness", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/binding-not-liveness.sdp.md", + title: "Bindings state existence, not liveness", + narrative: null, + sections: { + intent: { + outcome: "Make realization signals useful without overstating what source bindings prove.", + }, + decision: { + context: + "Anchors can resolve code and tests without proving reachability, execution, or approval.", + decision: + "Delivery facts record bindings and enabled verifier existence; coverage gaps and human readiness practice remain explicit without becoming graph facts.", + rationale: [ + "Renaming useful delivery facts or recording approval primitives either weakens drift signals or reverses the one-primitive boundary.", + ], + consequences: [ + "Impact reports name coverage-unknown files and `ready` remains a declared statement above a structural floor.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.content-only-sections", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/content-only-sections.sdp.md", + title: "Sections carry content while relations carry links", + narrative: null, + sections: { + intent: { + outcome: "Keep inline detail and promoted Specs from representing the same fact twice.", + }, + decision: { + context: + "Behavior content can mature from prose to structured evidence or into a standalone matching-kind Spec.", + decision: + "Sections contain local content only; promotion moves content exclusively and relations state the linkage.", + rationale: [ + "Reference unions and duplicate parent lists force consumers to branch and leave double-linkage drift legal.", + ], + consequences: [ + "Promoted children preserve readiness evidence through their own content and authored relations.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.typing-law", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/typing-law.sdp.md", + title: "Floor-read sections are closed typed shapes", + narrative: null, + sections: { + intent: { + outcome: + "Give authors guardrails exactly where readiness and honesty checks depend on section content.", + }, + decision: { + context: "A fixed list of typed sections becomes stale when the readiness floor evolves.", + decision: + "Every section read by a floor clause has a closed typed shape; unsettled design and ui surfaces remain open.", + rationale: [ + "Closed shapes block authored-fact smuggling and provide useful authoring guidance without prematurely fixing unsettled surfaces.", + ], + consequences: [ + "A newly floor-read section becomes typed by the criterion, not by a frozen list.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.kind-conditional-floor", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/kind-conditional-floor.sdp.md", + title: "Readiness evidence follows the Spec kind", + narrative: null, + sections: { + intent: { + outcome: + "Make stated readiness structurally honest without turning the floor into a quota.", + }, + decision: { + context: + "Kinds have different natural evidence, while structural maturity clauses apply across every Spec.", + decision: + "The readiness floor combines cumulative kind-blind clauses with one kind-conditional evidence clause at each rung.", + rationale: [ + "Defined-only evidence and uniform evidence rules either leave padding legal or erase meaningful kind distinctions.", + ], + consequences: [ + "Floor rows are monotonic, promotion-neutral, and converge honestly where a kind has no stronger form.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.carried-evidence", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/carried-evidence.sdp.md", + title: "Promoted evidence must carry its own evidence", + narrative: null, + sections: { + intent: { + outcome: + "Prevent empty promoted Specs and relation targets from satisfying an evidence floor.", + }, + decision: { + context: + "Promotion and constraints preserve meaning only when the promoted target carries the matching kind evidence.", + decision: + "Promoted evidence counts only when the promoted Spec holds its natural evidence; authoring-shape honesty rejects authored delivery facts and external `doc:` targets remain deferred.", + rationale: [ + "Counting empty children or wrong-kind constraints makes a structural floor pass without content, while readiness gates and premature external target types add the wrong contract.", + ], + consequences: [ + "The floor checks resolved target shape, and unresolved external decision links stay outside the current relation grammar.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.pack-reified", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/pack-reified.sdp.md", + title: "Packs group review context without becoming truth", + narrative: null, + sections: { + intent: { + outcome: + "Let related Specs be reviewed together without introducing another truth-bearing artifact.", + }, + decision: { + context: "Delivery work needs a cross-cutting aggregate that is distinct from refinement.", + decision: + "A Pack declares membership and framing while stating no system truth; Specs may belong to many Packs.", + rationale: [ + "Treating a Pack as a truth primitive or a refinement parent confuses grouping with authored intent.", + ], + consequences: [ + "Review context remains disposable while Spec relations retain semantic hierarchy.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.agent-surface-scripts-graph", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/agent-surface-scripts-graph.sdp.md", + title: "Agents script the visible graph", + narrative: null, + sections: { + intent: { + outcome: + "Give agents composable graph context without a fixed command vocabulary becoming the model.", + }, + decision: { + context: "Agents need decoded context and entry adapters without rebuilding graph joins.", + decision: + "The typed graph is the visible contract and agents script it directly through a thin reader surface.", + rationale: [ + "A verb wall duplicates graph semantics and hides composable data behind commands.", + ], + consequences: [ + "Entry adapters expose curated context while coverage gaps remain explicit rather than implied exhaustive.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:decisions.mcp-deferred", + specKind: "decision", + altitude: "feature", + readiness: "defined", + file: "specs/decisions/mcp-deferred.sdp.md", + title: "MCP integration remains deferred", + narrative: null, + sections: { + intent: { + outcome: + "Preserve a clean projection model without prematurely fixing an application integration surface.", + }, + decision: { + context: + "The graph already supports typed agent and human projections without an MCP transport.", + decision: + "MCP integration is deferred until a concrete caller establishes its boundary and contract.", + rationale: [ + "Adding an MCP surface without a caller invents verbs and persistence choices outside the projection model.", + ], + consequences: [ + "Consumers use the current graph and reader surfaces while MCP remains designed-in rather than claimed.", + ], + }, + }, + deliveryFacts: [], + }, +] as const; diff --git a/test/self-hosting-oracle/declared-relations.ts b/test/self-hosting-oracle/declared-relations.ts new file mode 100644 index 0000000..f9cddde --- /dev/null +++ b/test/self-hosting-oracle/declared-relations.ts @@ -0,0 +1,276 @@ +// The authored `declared`-claim relations of the self-hosting corpus, excluding the `belongsTo` +// edges derived from the Pack manifest. Authored transcription: the relation set is intent, and +// the extractor may neither add to it nor drop from it. + +export const expectedDeclaredRelations = [ + ["spec:carrier.markdown-authoring", "dependsOn", "spec:carrier.markdown-parser"], + ["spec:carrier.markdown-authoring", "decidedBy", "spec:decisions.sdp-ts-extension"], + ["spec:carrier.markdown-authoring", "decidedBy", "spec:decisions.carrier-ruling"], + ["spec:carrier.envelope-contract", "refines", "spec:carrier.markdown-authoring"], + ["spec:carrier.envelope-contract", "decidedBy", "spec:decisions.envelope-grammar-posture"], + ["spec:carrier.markdown-parser", "refines", "spec:carrier.markdown-authoring"], + ["spec:carrier.markdown-parser", "dependsOn", "spec:carrier.envelope-contract"], + ["spec:carrier.markdown-parser.bounded-parity", "refines", "spec:carrier.markdown-parser"], + ["spec:carrier.markdown-parser.bounded-parity", "verifies", "spec:carrier.markdown-parser"], + ["spec:carrier.sdp-import", "refines", "spec:carrier.markdown-authoring"], + ["spec:carrier.sdp-import.round-trip", "refines", "spec:carrier.sdp-import"], + ["spec:carrier.sdp-import.round-trip", "verifies", "spec:carrier.sdp-import"], + ["spec:carrier.prose-ownership-rule", "refines", "spec:carrier.markdown-authoring"], + ["spec:carrier.prose-ownership-rule", "decidedBy", "spec:decisions.prose-ownership"], + ["spec:protocol.self-hosting", "dependsOn", "spec:carrier.markdown-authoring"], + ["spec:protocol.self-hosting", "dependsOn", "spec:model.protocol-domain"], + ["spec:protocol.self-hosting", "decidedBy", "spec:decisions.concept-docs-dissolve"], + ["spec:protocol.self-hosting", "decidedBy", "spec:decisions.executable-meta-model"], + ["spec:protocol.self-hosting", "decidedBy", "spec:decisions.adopt-the-nouns"], + ["spec:protocol.self-hosting", "decidedBy", "spec:decisions.protocol-naming"], + ["spec:extraction.derive-graph", "refines", "spec:protocol.self-hosting"], + ["spec:extraction.derive-graph", "constrainedBy", "spec:extraction.determinism"], + ["spec:extraction.determinism", "refines", "spec:protocol.self-hosting"], + ["spec:extraction.build-pipeline", "refines", "spec:protocol.self-hosting"], + ["spec:extraction.build-pipeline", "dependsOn", "spec:extraction.derive-graph"], + ["spec:extraction.excludes", "refines", "spec:extraction.derive-graph"], + ["spec:extraction.excludes", "decidedBy", "spec:decisions.exclusion-contract"], + ["spec:extraction.excludes.segment-boundary", "refines", "spec:extraction.excludes"], + ["spec:extraction.excludes.segment-boundary", "verifies", "spec:extraction.excludes"], + ["spec:extraction.excludes.refused-path", "refines", "spec:extraction.excludes"], + ["spec:extraction.excludes.refused-path", "verifies", "spec:extraction.excludes"], + ["spec:extraction.claim-taxonomy", "refines", "spec:extraction.derive-graph"], + ["spec:extraction.regenerability", "refines", "spec:extraction.determinism"], + ["spec:extraction.schema-versioning", "refines", "spec:extraction.derive-graph"], + [ + "spec:extraction.schema-versioning.declared-version", + "refines", + "spec:extraction.schema-versioning", + ], + [ + "spec:extraction.schema-versioning.declared-version", + "verifies", + "spec:extraction.schema-versioning", + ], + ["spec:extraction.executable-contracts", "refines", "spec:extraction.build-pipeline"], + [ + "spec:extraction.executable-contracts.concreteness-refusal", + "refines", + "spec:extraction.executable-contracts", + ], + [ + "spec:extraction.executable-contracts.concreteness-refusal", + "verifies", + "spec:extraction.executable-contracts", + ], + [ + "spec:extraction.executable-contracts.multi-entry-example", + "refines", + "spec:extraction.executable-contracts", + ], + [ + "spec:extraction.executable-contracts.multi-entry-example", + "verifies", + "spec:extraction.executable-contracts", + ], + [ + "spec:extraction.executable-contracts.case-colliding-path", + "refines", + "spec:extraction.executable-contracts", + ], + [ + "spec:extraction.executable-contracts.case-colliding-path", + "verifies", + "spec:extraction.executable-contracts", + ], + ["spec:extraction.example-runner", "refines", "spec:extraction.executable-contracts"], + ["spec:extraction.example-runner.step-order", "refines", "spec:extraction.example-runner"], + ["spec:extraction.example-runner.step-order", "verifies", "spec:extraction.example-runner"], + ["spec:extraction.example-runner.red-step-naming", "refines", "spec:extraction.example-runner"], + ["spec:extraction.example-runner.red-step-naming", "verifies", "spec:extraction.example-runner"], + ["spec:carrier.slot-notation", "refines", "spec:carrier.markdown-authoring"], + ["spec:carrier.slot-notation.typed-declaration", "refines", "spec:carrier.slot-notation"], + ["spec:carrier.slot-notation.typed-declaration", "verifies", "spec:carrier.slot-notation"], + ["spec:carrier.slot-notation.refused-guess", "refines", "spec:carrier.slot-notation"], + ["spec:carrier.slot-notation.refused-guess", "verifies", "spec:carrier.slot-notation"], + ["spec:validation.readiness-floor", "refines", "spec:protocol.self-hosting"], + ["spec:validation.readiness-floor", "dependsOn", "spec:model.protocol-domain"], + ["spec:validation.readiness-floor", "decidedBy", "spec:decisions.kind-conditional-floor"], + ["spec:validation.readiness-floor", "decidedBy", "spec:decisions.carried-evidence"], + ["spec:validation.duplicate-ids", "refines", "spec:protocol.self-hosting"], + ["spec:validation.duplicate-ids", "dependsOn", "spec:carrier.markdown-parser"], + ["spec:validation.duplicate-ids.dual-carrier", "refines", "spec:validation.duplicate-ids"], + ["spec:validation.duplicate-ids.dual-carrier", "verifies", "spec:validation.duplicate-ids"], + ["spec:validation.two-check-families", "refines", "spec:protocol.self-hosting"], + ["spec:validation.two-check-families", "decidedBy", "spec:decisions.one-validation-path"], + ["spec:validation.referential-integrity", "refines", "spec:validation.two-check-families"], + [ + "spec:validation.referential-integrity.dangling-target", + "refines", + "spec:validation.referential-integrity", + ], + [ + "spec:validation.referential-integrity.dangling-target", + "verifies", + "spec:validation.referential-integrity", + ], + [ + "spec:validation.referential-integrity.did-you-mean", + "refines", + "spec:validation.referential-integrity", + ], + [ + "spec:validation.referential-integrity.did-you-mean", + "verifies", + "spec:validation.referential-integrity", + ], + ["spec:validation.claim-separation", "refines", "spec:validation.two-check-families"], + [ + "spec:validation.claim-separation.collapsed-edge-claim", + "refines", + "spec:validation.claim-separation", + ], + [ + "spec:validation.claim-separation.collapsed-edge-claim", + "verifies", + "spec:validation.claim-separation", + ], + [ + "spec:validation.claim-separation.unratified-descriptor", + "refines", + "spec:validation.claim-separation", + ], + [ + "spec:validation.claim-separation.unratified-descriptor", + "verifies", + "spec:validation.claim-separation", + ], + ["spec:validation.verification-linkage", "refines", "spec:validation.two-check-families"], + [ + "spec:validation.verification-linkage.unbound-example", + "refines", + "spec:validation.verification-linkage", + ], + [ + "spec:validation.verification-linkage.unbound-example", + "verifies", + "spec:validation.verification-linkage", + ], + [ + "spec:validation.verification-linkage.unresolved-oracle", + "refines", + "spec:validation.verification-linkage", + ], + [ + "spec:validation.verification-linkage.unresolved-oracle", + "verifies", + "spec:validation.verification-linkage", + ], + ["spec:validation.pack-coherence", "refines", "spec:validation.two-check-families"], + [ + "spec:validation.pack-coherence.incoherent-aggregate", + "refines", + "spec:validation.pack-coherence", + ], + [ + "spec:validation.pack-coherence.incoherent-aggregate", + "verifies", + "spec:validation.pack-coherence", + ], + ["spec:validation.authored-honesty", "refines", "spec:validation.two-check-families"], + [ + "spec:validation.authored-honesty.section-authored-fact", + "refines", + "spec:validation.authored-honesty", + ], + [ + "spec:validation.authored-honesty.section-authored-fact", + "verifies", + "spec:validation.authored-honesty", + ], + [ + "spec:validation.authored-honesty.unearned-stated-fact", + "refines", + "spec:validation.authored-honesty", + ], + [ + "spec:validation.authored-honesty.unearned-stated-fact", + "verifies", + "spec:validation.authored-honesty", + ], + ["spec:validation.warn-level-signals", "refines", "spec:validation.two-check-families"], + [ + "spec:validation.warn-level-signals.orphan-signal", + "refines", + "spec:validation.warn-level-signals", + ], + [ + "spec:validation.warn-level-signals.orphan-signal", + "verifies", + "spec:validation.warn-level-signals", + ], + [ + "spec:validation.warn-level-signals.ready-gap-signal", + "refines", + "spec:validation.warn-level-signals", + ], + [ + "spec:validation.warn-level-signals.ready-gap-signal", + "verifies", + "spec:validation.warn-level-signals", + ], + ["spec:consumers.projections-model", "refines", "spec:protocol.self-hosting"], + ["spec:consumers.projections-model", "decidedBy", "spec:decisions.mcp-deferred"], + ["spec:consumers.agent-surface", "refines", "spec:consumers.projections-model"], + ["spec:consumers.agent-surface", "decidedBy", "spec:decisions.agent-surface-scripts-graph"], + ["spec:consumers.design-review", "refines", "spec:consumers.projections-model"], + ["spec:consumers.reader", "refines", "spec:consumers.agent-surface"], + ["spec:consumers.edit-model", "refines", "spec:consumers.projections-model"], + ["spec:model.protocol-domain", "refines", "spec:protocol.self-hosting"], + ["spec:model.core-model", "refines", "spec:protocol.self-hosting"], + ["spec:model.core-model", "decidedBy", "spec:decisions.one-primitive"], + ["spec:model.spec-sections", "refines", "spec:model.core-model"], + ["spec:model.spec-sections", "decidedBy", "spec:decisions.point-per-example"], + ["spec:model.spec-sections", "decidedBy", "spec:decisions.content-only-sections"], + ["spec:model.spec-sections", "decidedBy", "spec:decisions.typing-law"], + ["spec:model.relations", "refines", "spec:model.core-model"], + ["spec:model.stable-ids", "refines", "spec:model.core-model"], + ["spec:model.stable-ids.namespaced-round-trip", "refines", "spec:model.stable-ids"], + ["spec:model.stable-ids.namespaced-round-trip", "verifies", "spec:model.stable-ids"], + ["spec:model.stable-ids.malformed-refusal", "refines", "spec:model.stable-ids"], + ["spec:model.stable-ids.malformed-refusal", "verifies", "spec:model.stable-ids"], + ["spec:model.pack-aggregate", "refines", "spec:model.core-model"], + ["spec:model.pack-aggregate", "decidedBy", "spec:decisions.pack-reified"], + ["spec:model.anchors", "refines", "spec:model.core-model"], + ["spec:model.anchors", "decidedBy", "spec:decisions.binding-not-liveness"], + ["spec:decisions.plain-language-references", "refines", "spec:protocol.self-hosting"], + ["spec:decisions.concept-docs-dissolve", "refines", "spec:protocol.self-hosting"], + ["spec:decisions.one-validation-path", "refines", "spec:validation.two-check-families"], + ["spec:decisions.sdp-ts-extension", "refines", "spec:carrier.markdown-authoring"], + ["spec:decisions.point-per-example", "refines", "spec:model.spec-sections"], + ["spec:decisions.carrier-ruling", "refines", "spec:carrier.markdown-authoring"], + ["spec:decisions.prose-ownership", "refines", "spec:carrier.prose-ownership-rule"], + ["spec:decisions.envelope-grammar-posture", "refines", "spec:carrier.envelope-contract"], + ["spec:decisions.exclusion-contract", "refines", "spec:extraction.excludes"], + ["spec:decisions.executable-meta-model", "refines", "spec:protocol.self-hosting"], + ["spec:decisions.adopt-the-nouns", "refines", "spec:protocol.self-hosting"], + ["spec:decisions.one-primitive", "refines", "spec:model.core-model"], + ["spec:decisions.protocol-naming", "refines", "spec:protocol.self-hosting"], + ["spec:decisions.binding-not-liveness", "refines", "spec:model.anchors"], + ["spec:decisions.content-only-sections", "refines", "spec:model.spec-sections"], + ["spec:decisions.typing-law", "refines", "spec:model.spec-sections"], + ["spec:decisions.kind-conditional-floor", "refines", "spec:validation.readiness-floor"], + ["spec:decisions.carried-evidence", "refines", "spec:validation.readiness-floor"], + ["spec:decisions.pack-reified", "refines", "spec:model.pack-aggregate"], + ["spec:decisions.agent-surface-scripts-graph", "refines", "spec:consumers.agent-surface"], + ["spec:decisions.mcp-deferred", "refines", "spec:consumers.projections-model"], + ["spec:model.anchors.lookalike-refusal", "refines", "spec:model.anchors"], + ["spec:model.anchors.lookalike-refusal", "verifies", "spec:model.anchors"], + ["spec:model.anchors.physical-identity", "refines", "spec:model.anchors"], + ["spec:model.anchors.physical-identity", "verifies", "spec:model.anchors"], + [ + "spec:validation.two-check-families.split-report", + "refines", + "spec:validation.two-check-families", + ], + [ + "spec:validation.two-check-families.split-report", + "verifies", + "spec:validation.two-check-families", + ], +] as const; diff --git a/test/self-hosting-oracle/extraction.ts b/test/self-hosting-oracle/extraction.ts new file mode 100644 index 0000000..31f2f44 --- /dev/null +++ b/test/self-hosting-oracle/extraction.ts @@ -0,0 +1,535 @@ +// The authored descriptors of the `extraction` family of the self-hosting corpus — +// human transcription of intended truth, never computed from the derived graph. Extraction must +// reproduce every value here exactly; a disagreement is drift to resolve on one side or the other. + +export const extractionSpecs = [ + { + id: "spec:extraction.derive-graph", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + file: "specs/extraction/derive-graph.sdp.md", + title: "Carrier reification derives the one graph", + narrative: + "The graph is the current projection of the repository at a commit. Git holds lifecycle history, so removed records disappear from the current graph and a current `supersedes` relation is the only forward pointer between records that still exist.", + sections: { + intent: { outcome: "Expose one carrier-neutral derivation seam." }, + behavior: { + rules: [ + "Carrier reification feeds deriveGraph once; no consumer creates a second graph.", + "The graph is flat arrays of typed nodes and edges; hierarchy and containment are expressed by edges rather than nested nodes.", + "Declared relations resolve Primitive to Primitive, while `satisfies` and test `verifies` edges derive from anchors and run from their binding node to the direct Spec target.", + "Delivery facts are computed node facts: a resolving `satisfies` edge contributes `implemented`, and an enabled direct verifier contributes `has-verifier` only to its target.", + "Inferred structural edges are advisory inputs to impact analysis and never become authoritative graph truth.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:extraction.determinism", + specKind: "constraint", + altitude: "feature", + readiness: "ready", + file: "specs/extraction/determinism.sdp.md", + title: "Committed source derives byte-identical output", + narrative: null, + sections: { + intent: { outcome: "Make regeneration independent of location and prior generated state." }, + constraints: [ + { + flavor: "quality", + statement: + "Two clean derivations of the same committed source produce byte-identical generated trees.", + target: "sha256(tree@run1) == sha256(tree@run2)", + measurableBy: "test/cli.test.ts clean-repo determinism", + }, + ], + behavior: { + rules: [ + "Nodes sort by ID, edges sort by from, type, and to, and semantically compared output excludes wall-clock timestamps and run-specific hashes.", + "`sdp build --check-clean` repeats extraction and contract generation independently, failing on any graph or generated-contract byte divergence.", + "Static envelope fields fail extraction when they cannot be reified; optional TypeScript section detail may warn and drop, while Markdown documents refuse as a whole.", + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:extraction.excludes", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/extraction/excludes.sdp.md", + title: "Extraction exclusions are strict consumer input", + narrative: null, + sections: { + intent: { + outcome: + "Keep consumer-selected omissions precise without changing the extractor's canonical discovery rules.", + }, + behavior: { + rules: [ + "An exclusion is a unique, exact root-relative POSIX path prefix applied to both declared-carrier and anchor-candidate discovery surfaces.", + "A prefix excludes itself and slash-delimited descendants only; it never excludes a merely similar sibling path.", + "Empty, dot-relative, absolute, Windows-drive, backslash, trailing-slash, and parent-traversal paths are refused rather than normalized into a different meaning.", + "The realizing entrypoints are `normalizeExcludes` and `discoverFiles` in `src/extract/discover.ts`.", + ], + exampleSpace: { + given: [ + "the extraction root carries the tree {excludedTree:string} and the similar sibling {similarTree:string}", + "the consumer supplies the exclusion {exclusion:string}", + ], + when: ["the root is discovered"], + [["t", "hen"].join("")]: [ + 'the discovery attempt {outcome:"completes"|"is refused"}', + "the surviving spec carrier is {specCarrier:string} and the surviving anchor candidate is {anchorCandidate:string}", + "the refusal states {diagnostic:string} and names the offending path", + ], + }, + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:extraction.excludes.segment-boundary", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/extraction/excludes.segment-boundary.sdp.md", + title: "A prefix excludes its own tree and leaves a similar sibling standing", + narrative: null, + sections: { + intent: { + outcome: "Execute the segment-boundary rule across both discovery surfaces.", + }, + behavior: { + examples: [ + { + given: [ + 'the extraction root carries the tree {excludedTree: "foo"} and the similar sibling {similarTree: "foobar"}', + 'the consumer supplies the exclusion {exclusion: "foo"}', + ], + when: ["the root is discovered"], + [["t", "hen"].join("")]: [ + 'the discovery attempt {outcome: "completes"}', + 'the surviving spec carrier is {specCarrier: "foobar/included.sdp.ts"} and the surviving anchor candidate is {anchorCandidate: "foobar/helper.ts"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:extraction.excludes.refused-path", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/extraction/excludes.refused-path.sdp.md", + title: "A Windows-drive absolute path is refused rather than normalized", + narrative: null, + sections: { + intent: { + outcome: + "Execute the refusal rule on an exclusion that cannot name a root-relative prefix.", + }, + behavior: { + examples: [ + { + given: [ + 'the extraction root carries the tree {excludedTree: "foo"} and the similar sibling {similarTree: "foobar"}', + 'the consumer supplies the exclusion {exclusion: "C:/work/specs"}', + ], + when: ["the root is discovered"], + [["t", "hen"].join("")]: [ + 'the discovery attempt {outcome: "is refused"}', + 'the refusal states {diagnostic: "normalizeExcludes: invalid exclusion path"} and names the offending path', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:extraction.claim-taxonomy", + specKind: "model", + altitude: "feature", + readiness: "defined", + file: "specs/extraction/claim-taxonomy.sdp.md", + title: "Graph claims retain their epistemic source", + narrative: null, + sections: { + intent: { + outcome: + "Let every graph reader distinguish authored intent, human bindings, and machine-derived structure.", + }, + model: { + terms: { + declared: + "Human intent explicitly authored in a Spec or Pack; it is authoritative intent.", + anchored: + "A human binding from a code, test, or oracle location to one Spec ID; it is authoritative binding and carries no intent.", + inferred: + "Machine-derived structural information; it is advisory and never authoritative.", + "claim inheritance": + "An edge computed from an authored source retains that source's declared claim; derivation is a mechanism, not a fourth claim.", + "delivery fact": + "A realization signal computed from resolving edges, never an authored claim or edge.", + }, + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:extraction.regenerability", + specKind: "rule", + altitude: "feature", + readiness: "defined", + file: "specs/extraction/regenerability.sdp.md", + title: "Generated artifacts are disposable projections", + narrative: null, + sections: { + intent: { + outcome: + "Keep the repository canonical while allowing every graph and projection to be rebuilt safely.", + }, + behavior: { + rules: [ + "Generated artifacts are disposable: deleting them and rebuilding from the same committed repository produces the same bytes.", + "Consumers read the graph or link to source locations recorded in it; they never re-parse source or keep a parallel model.", + "The graph is a single JSON projection with in-memory query support; a graph database remains deferred until measured traversal pain establishes a real need.", + "Measured evidence from the self-hosting corpus keeps full rebuilds comfortable below roughly 50 Specs.", + "Measured evidence defers a graph database until the graph reaches roughly 10k+ nodes or traversal pain establishes a real need.", + ], + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:extraction.schema-versioning", + specKind: "rule", + altitude: "story", + readiness: "ready", + file: "specs/extraction/schema-versioning.sdp.md", + title: "The graph declares its schema version", + narrative: null, + sections: { + intent: { + outcome: + "Let consumers identify the graph payload contract without premature migration machinery.", + }, + behavior: { + rules: [ + "Every graph declares its schemaVersion, and MVP consumers require that field to be present and readable.", + "Envelope-stable, section-extensible growth is normally additive; SemVer negotiation and a migration command remain deferred until a consumer needs them.", + "The declaring entrypoint is `schemaVersion` in `src/graph/schema.ts`, carried onto every derived payload by `deriveGraph`.", + ], + exampleSpace: { + given: ["a graph derived from the authored spec {specId:string}"], + when: ["the graph payload is serialized"], + [["t", "hen"].join("")]: [ + "the payload declares the schema version {schemaVersion:string}", + ], + }, + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:extraction.schema-versioning.declared-version", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/extraction/schema-versioning.declared-version.sdp.md", + title: "A derived payload carries a schema version its consumer can read", + narrative: null, + sections: { + intent: { + outcome: "Execute the declared-version rule over a serialized graph payload.", + }, + behavior: { + examples: [ + { + given: [ + 'a graph derived from the authored spec {specId: "spec:probe.schema-versioning"}', + ], + when: ["the graph payload is serialized"], + [["t", "hen"].join("")]: [ + 'the payload declares the schema version {schemaVersion: "0.4.0"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:extraction.executable-contracts", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + file: "specs/extraction/executable-contracts.sdp.md", + title: "The build derives executable contracts from graph examples", + narrative: null, + sections: { + intent: { + outcome: + "Give bound tests typed step and example-space contracts without reading authored Specs directly.", + }, + behavior: { + rules: [ + "`generateContracts` derives per-example step contracts and per-parent space contracts solely from the extracted graph.", + "A generated contract is disposable, keyed by Spec ID, and becomes unavailable when its authored example cannot bind honestly to its shared vocabulary.", + "The concreteness law is a refusal, never a guess — an example carrying an unbound slot in any used step of any entry is not the bindable form and receives no step contract, and a prose-only example receives none either.", + "The concreteness law reads the example's own form alone, so it refuses whether or not a parent declares a shared vocabulary; vocabulary resolution is a separate, later gate whose withholding names its own finding.", + "An example is one point, so the step contract and the bound point derive from the same first complete entry; a further structured entry is named rather than left silently inert.", + "Degradation is loud and local — an undeclared slot, a value outside its declared type, and a conflicting re-binding each name the drift and drop exactly that one slot, so the emitted module still compiles.", + "A vocabulary slot group that declares no usable type is named rather than dropped in silence, and no dimension enters the space for it.", + "Two contract paths differing only by letter case cannot coexist on a case-insensitive filesystem, so the contracts tree is withheld whole and the finding names the colliding pair.", + "Every generation finding is a warning that describes what did not emit; gating belongs to graph validation alone, so a withheld contract never fails the build by itself.", + "The realizing entrypoint is `generateContracts` in `src/codegen/contracts.ts`.", + ], + exampleSpace: { + given: [ + "a parent spec whose example space declares the slot {dimension:string}", + "a parent spec that declares no shared vocabulary for the slot {dimension:string}", + 'a refining example {exampleId:string} whose used step {binding:"binds"|"leaves unbound"} that slot', + "the example carries {entryCount:number} structured entries", + "a case-twin example {twinId:string} whose contract path differs only by letter case", + ], + when: ["the contracts are generated from the derived graph"], + [["t", "hen"].join("")]: [ + "the generated tree holds {fileCount:number} files", + "the step contract for the example is emitted: {emitted:boolean}", + "the findings name {findingId:string}", + ], + }, + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:extraction.executable-contracts.concreteness-refusal", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/extraction/executable-contracts.concreteness-refusal.sdp.md", + title: "An unbound slot in a used step earns no step contract", + narrative: null, + sections: { + intent: { + outcome: + "Execute the concreteness law alone, where no shared vocabulary can withhold the contract in its place.", + }, + behavior: { + examples: [ + { + given: [ + 'a parent spec that declares no shared vocabulary for the slot {dimension: "n"}', + 'a refining example {exampleId: "spec:probe.create-order.unbound"} whose used step {binding: "leaves unbound"} that slot', + ], + when: ["the contracts are generated from the derived graph"], + [["t", "hen"].join("")]: [ + "the generated tree holds {fileCount: 0} files", + "the step contract for the example is emitted: {emitted: false}", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:extraction.executable-contracts.multi-entry-example", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/extraction/executable-contracts.multi-entry-example.sdp.md", + title: "A second structured entry is named, never left silently inert", + narrative: null, + sections: { + intent: { + outcome: + "Execute the one-point law where an example smuggles a second case into one document.", + }, + behavior: { + examples: [ + { + given: [ + 'a parent spec whose example space declares the slot {dimension: "n"}', + 'a refining example {exampleId: "spec:probe.create-order.multi"} whose used step {binding: "binds"} that slot', + "the example carries {entryCount: 2} structured entries", + ], + when: ["the contracts are generated from the derived graph"], + [["t", "hen"].join("")]: [ + "the step contract for the example is emitted: {emitted: true}", + 'the findings name {findingId: "contracts/multi-entry-example"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:extraction.executable-contracts.case-colliding-path", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/extraction/executable-contracts.case-colliding-path.sdp.md", + title: "A case-only path collision withholds the whole contracts tree", + narrative: null, + sections: { + intent: { + outcome: + "Execute the all-or-nothing rule where two examples claim one case-folded contract path.", + }, + behavior: { + examples: [ + { + given: [ + 'a parent spec whose example space declares the slot {dimension: "n"}', + 'a refining example {exampleId: "spec:probe.create-order.same-case"} whose used step {binding: "binds"} that slot', + 'a case-twin example {twinId: "spec:probe.create-order.same-Case"} whose contract path differs only by letter case', + ], + when: ["the contracts are generated from the derived graph"], + [["t", "hen"].join("")]: [ + "the generated tree holds {fileCount: 0} files", + 'the findings name {findingId: "contracts/case-colliding-path"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:extraction.example-runner", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + file: "specs/extraction/example-runner.sdp.md", + title: "A bound example runs its contract steps against a fresh world", + narrative: null, + sections: { + intent: { + problem: + "A bound test must execute a Spec's own steps without the executing core learning any test framework.", + outcome: + "Run a generated contract's steps in authored order and make a red step name itself in the Spec's own words.", + value: + "A failing example reads as the Spec that failed rather than as an anonymous assertion.", + }, + behavior: { + rules: [ + "The core plans every contract step in authored order and runs it against the world the caller hands in; creating a fresh world per example is the adapter's lifecycle, never the core's.", + "Duplicate step text within one example binds one handler, and every occurrence runs that one handler with its own authored params.", + "A red step names itself before the assertion detail: the failure message leads with the step's natural reading — the Spec's own words with bound values inlined — and the original error is preserved, carried as `cause` when it cannot be re-messaged, and wrapped when the thrown value is not an error.", + "A missing or stale step handler is a compile-time refusal rather than a silent skip: the bindings type covers every step and only the steps, so spec-side drift fails the typecheck instead of the run.", + "The core contributes `unspecified`, the one outcome no Spec ever states, so an uncovered region of an example space has an honest answer rather than a manufactured one.", + "The realizing entrypoints are `planExample` and `runExamplePlan` in `src/runner/index.ts`.", + ], + exampleSpace: { + given: [ + "a contract whose given step repeats {occurrences:number} times before one when step and one then step", + 'the handler bound to the {failingPhase:"given"|"when"|"then"} step throws {thrown:string}', + ], + when: ["the bound plan runs against a fresh world"], + [["t", "hen"].join("")]: [ + "the world records the handler trace {trace:string}", + 'the run {outcome:"completes"|"fails"}', + "the failure names the step in the Spec's own words as {failureLabel:string}", + "the failure preserves the original detail {detail:string}", + ], + }, + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:extraction.example-runner.step-order", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/extraction/example-runner.step-order.sdp.md", + title: "A repeated step runs its one handler at each occurrence, in contract order", + narrative: null, + sections: { + intent: { + outcome: + "Execute the contract-order and one-handler-per-step laws over a repeating given step.", + }, + behavior: { + examples: [ + { + given: [ + "a contract whose given step repeats {occurrences: 2} times before one when step and one then step", + ], + when: ["the bound plan runs against a fresh world"], + [["t", "hen"].join("")]: [ + 'the world records the handler trace {trace: "given 2 | given 2 | when | then"}', + 'the run {outcome: "completes"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:extraction.example-runner.red-step-naming", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/extraction/example-runner.red-step-naming.sdp.md", + title: "A red step names itself before the assertion detail", + narrative: null, + sections: { + intent: { + outcome: "Execute the failure law where a bound handler throws inside the when step.", + }, + behavior: { + examples: [ + { + given: [ + "a contract whose given step repeats {occurrences: 2} times before one when step and one then step", + 'the handler bound to the {failingPhase: "when"} step throws {thrown: "boom"}', + ], + when: ["the bound plan runs against a fresh world"], + [["t", "hen"].join("")]: [ + 'the run {outcome: "fails"}', + 'the failure names the step in the Spec\'s own words as {failureLabel: "at step: When the cart is submitted"}', + 'the failure preserves the original detail {detail: "boom"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:extraction.build-pipeline", + specKind: "workflow", + altitude: "feature", + readiness: "defined", + file: "specs/extraction/build-pipeline.sdp.md", + title: "The build pipeline has one ordered flow", + narrative: null, + sections: { + intent: { outcome: "Turn authored carriers into validated derived artifacts." }, + behavior: { + rules: ["Every command uses the same extracted graph and validation seam."], + flows: [ + "Discover carriers.", + "Reify carriers.", + "Derive the graph.", + "Validate the graph.", + "Emit derived artifacts.", + ], + }, + }, + deliveryFacts: [], + }, +] as const; diff --git a/test/self-hosting-oracle/index.ts b/test/self-hosting-oracle/index.ts new file mode 100644 index 0000000..4cb7aa5 --- /dev/null +++ b/test/self-hosting-oracle/index.ts @@ -0,0 +1,57 @@ +// The self-hosting corpus oracle: the authored expectation the derived graph is measured against. +// Every value under this directory is human transcription of intended truth. The oracle is never +// computed from the graph it judges — an oracle derived from its subject can only ever agree with +// it — so a disagreement between a value here and the derived graph is drift to resolve on one +// side or the other, never a licence to promote graph output into the oracle. + +import { carrierSpecs } from "./carrier.js"; +import { consumersSpecs } from "./consumers.js"; +import { decisionsSpecs } from "./decisions.js"; +import { extractionSpecs } from "./extraction.js"; +import { modelSpecs } from "./model.js"; +import { protocolSpecs } from "./protocol.js"; +import { validationSpecs } from "./validation.js"; + +export interface ExpectedSpec { + readonly id: string; + readonly specKind: string; + readonly altitude: string; + readonly readiness: string; + readonly title: string; + readonly narrative: string | null; + readonly sections: unknown; + readonly deliveryFacts: readonly string[]; + readonly file: string; +} + +export interface SpecFamily { + readonly name: string; + readonly prefix: string; + readonly specs: readonly ExpectedSpec[]; +} + +// One family per Spec namespace, so a conversion wave touches one authored module rather than the +// whole corpus. The suite asserts the union of these slices is the entire primitive node set: a +// Spec can never escape between two families. +export const specFamilies: readonly SpecFamily[] = [ + { name: "carrier", prefix: "spec:carrier.", specs: carrierSpecs }, + { name: "protocol", prefix: "spec:protocol.", specs: protocolSpecs }, + { name: "extraction", prefix: "spec:extraction.", specs: extractionSpecs }, + { name: "validation", prefix: "spec:validation.", specs: validationSpecs }, + { name: "model", prefix: "spec:model.", specs: modelSpecs }, + { name: "consumers", prefix: "spec:consumers.", specs: consumersSpecs }, + { name: "decisions", prefix: "spec:decisions.", specs: decisionsSpecs }, +]; + +export const expectedSpecs: readonly ExpectedSpec[] = specFamilies.flatMap( + (family) => family.specs, +); + +// The corpus states nothing the honesty and conformance checks can object to: no orphan, no +// unearned fact, no readiness above its floor. An empty expectation is the strongest one available +// here — every finding, at any severity, is a failure. +export const expectedWarnings = [] as const; + +export { expectedAnchors } from "./anchors.js"; +export { expectedDeclaredRelations } from "./declared-relations.js"; +export { expectedPackMembers } from "./pack-members.js"; diff --git a/test/self-hosting-oracle/model.ts b/test/self-hosting-oracle/model.ts new file mode 100644 index 0000000..b331cba --- /dev/null +++ b/test/self-hosting-oracle/model.ts @@ -0,0 +1,338 @@ +// The authored descriptors of the `model` family of the self-hosting corpus — +// human transcription of intended truth, never computed from the derived graph. Extraction must +// reproduce every value here exactly; a disagreement is drift to resolve on one side or the other. + +export const modelSpecs = [ + { + id: "spec:model.protocol-domain", + specKind: "model", + altitude: "feature", + readiness: "defined", + file: "specs/model/protocol-domain.sdp.md", + title: "The Protocol domain uses one ratified language", + narrative: null, + sections: { + intent: { outcome: "Give self-hosting specs the same core vocabulary." }, + model: { + terms: { + Pack: "A grouping and review aggregate that states no system truth.", + Spec: "The one authored truth-primitive.", + anchor: "An in-code identity binding that states no intent.", + "delivery fact": "A machine-derived realization signal.", + }, + }, + }, + deliveryFacts: [], + }, + { + id: "spec:model.core-model", + specKind: "model", + altitude: "feature", + readiness: "defined", + file: "specs/model/core-model.sdp.md", + title: "The Protocol models delivery with one enrichable Spec", + narrative: null, + sections: { + intent: { + outcome: + "Give every authored delivery statement one stable shape and independent coordinates.", + }, + model: { + terms: { + Spec: "The one authored truth-primitive, enriched in place without changing artifact type.", + altitude: "The scope position `epic`, `feature`, or `story`.", + "delivery fact": + "A derived realization signal such as implemented or has-verifier; it is never authored readiness.", + envelope: + "The stable outer shape of id, title, kind, altitude, readiness, and relations; sections carry extension detail.", + kind: "The true subtype that categorizes a Spec's truth and changes its required detail and validation.", + readiness: + "The author-stated design-maturity position `idea`, `scoped`, `defined`, or `ready`, checked against a structural floor.", + }, + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:model.spec-sections", + specKind: "model", + altitude: "feature", + readiness: "defined", + file: "specs/model/spec-sections.sdp.md", + title: "Spec sections carry typed detail and direct verifier semantics", + narrative: null, + sections: { + intent: { + outcome: + "Extend Specs with local detail without weakening their envelope or confusing binding evidence with intent.", + }, + model: { + terms: { + "content-only section": + "A section carries local content, while relations carry links to promoted standalone Specs.", + "enabled verifier": + "An example or direct test with a linked, resolvable test anchor; runner execution and pass state remain outside the graph.", + promotion: + "Moving shared or independently reviewed content into a standalone Spec of the matching kind, exclusively rather than alongside inline content.", + section: + "An optional detail slice of a Spec: intent, behavior, constraints, model, design, decision, verification, or ui.", + "typing law": + "Every section read by a readiness-floor clause has a closed typed shape; unsettled design and ui surfaces remain open bags.", + verifies: + "A direct verifier-to-target relation whose enabled test binding can derive has-verifier only for that stated target.", + }, + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:model.relations", + specKind: "model", + altitude: "feature", + readiness: "defined", + file: "specs/model/relations.sdp.md", + title: "Specs declare typed directed relations", + narrative: null, + sections: { + intent: { + outcome: + "Preserve the explicit intent links that make a delivery model navigable and queryable.", + }, + model: { + terms: { + "authored relation": "A declared, directed Spec-to-Spec edge that records human intent.", + constrainedBy: "A bounded Spec points to its rule, constraint, or policy Spec.", + decidedBy: "A shaped Spec points to its Decision Record.", + dependsOn: "A dependent Spec points to the Spec it needs.", + refines: "A child points to its more precise parent.", + supersedes: "A current Decision Record points forward to the decision it replaces.", + verifies: "A verifier points to the Spec it verifies.", + }, + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:model.stable-ids", + specKind: "rule", + altitude: "story", + readiness: "ready", + file: "specs/model/stable-ids.sdp.md", + title: "Stable IDs are the Protocol's durable join key", + narrative: null, + sections: { + intent: { + outcome: + "Keep intent, bindings, and graph nodes connected through names that survive code refactoring.", + }, + behavior: { + rules: [ + "A Protocol ID is stable, unique, namespaced, human-readable, and the only binding between intent and code.", + "An ID uses a lowercase namespace and a dotted path whose segments admit mixed case (case binds only on the namespace), with an optional single `#` sub-part; referential-integrity checks reject malformed or unresolved references.", + "IDs carry no history: a rename is a repository edit recorded by git rather than graph-resident bookkeeping.", + "The builders reserve one namespace per binding direction — `spec:` for a Spec and for every Spec reference, `pack:` for the aggregate, `impl:` · `api:` · `component:` for a code anchor, `test:` for a verifying test anchor, and `oracle:` for an expected-outcome anchor — while the grammar itself admits any lowercase namespace, so the reserved set is the builders' law rather than the parser's.", + "`doc:` is reserved for a genuinely external document a decision Spec links to, never for an in-system decision: in-system decisions are Specs under the `spec:decisions.*` convention. No builder mints a `doc:` identifier and the Spec-only reference builder refuses one, so the reservation is a named deferral rather than a landed namespace.", + "The realizing entrypoints are `parseId` and `formatId` in `src/ids.ts`.", + ], + exampleSpace: { + given: ["the authored identifier {identifier:string}"], + when: ["the identifier is parsed"], + [["t", "hen"].join("")]: [ + 'parsing {outcome:"resolves"|"is refused"}', + "reformatting the parsed parts restores {restored:string}", + "the refusal names the reason {reason:string}", + ], + }, + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:model.stable-ids.namespaced-round-trip", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/model/stable-ids.namespaced-round-trip.sdp.md", + title: "A namespaced dotted path with a sub-part survives parsing unchanged", + narrative: null, + sections: { + intent: { + outcome: "Execute the ID grammar on the fullest well-formed shape the model allows.", + }, + behavior: { + examples: [ + { + given: ['the authored identifier {identifier: "spec:orders.create-order#valid-cart"}'], + when: ["the identifier is parsed"], + [["t", "hen"].join("")]: [ + 'parsing {outcome: "resolves"}', + 'reformatting the parsed parts restores {restored: "spec:orders.create-order#valid-cart"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:model.stable-ids.malformed-refusal", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/model/stable-ids.malformed-refusal.sdp.md", + title: "An uppercase namespace is refused with its reason named", + narrative: null, + sections: { + intent: { + outcome: "Execute the lowercase-namespace clause of the ID grammar.", + }, + behavior: { + examples: [ + { + given: ['the authored identifier {identifier: "Spec:orders.create-order"}'], + when: ["the identifier is parsed"], + [["t", "hen"].join("")]: [ + 'parsing {outcome: "is refused"}', + 'the refusal names the reason {reason: "namespace must be lowercase"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:model.pack-aggregate", + specKind: "model", + altitude: "story", + readiness: "defined", + file: "specs/model/pack-aggregate.sdp.md", + title: "A Pack is a truth-free review aggregate", + narrative: null, + sections: { + intent: { + outcome: + "Let reviewers group related Specs without introducing a second truth-bearing artifact.", + }, + model: { + terms: { + Pack: "An authored aggregate that groups related Specs for ideation and review while stating no system truth of its own.", + framing: "A plain descriptive note explaining why a Pack exists; it is not Spec intent.", + membership: + "A declared manifest reference that derives a belongsTo edge; a Spec may belong to many Packs.", + modelRefs: + "References from a Pack to standalone model Specs that carry shared vocabulary.", + refinement: + "A truth-bearing parent-child relation, distinct from the cross-cutting Pack aggregate.", + }, + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:model.anchors", + specKind: "model", + altitude: "feature", + readiness: "ready", + file: "specs/model/anchors.sdp.md", + title: "Source anchors bind code without carrying intent", + narrative: null, + sections: { + intent: { + outcome: + "Connect implementation, tests, and oracles to Specs while keeping authored intent centralized in the carrier.", + }, + behavior: { + exampleSpace: { + given: [ + 'a repository whose one source file builds an anchor through {builderSource:"a consumer-local lookalike module"|"a relative import resolving to the Protocol builder modules"|"the published Protocol package"}', + ], + when: ["the repository is extracted"], + [["t", "hen"].join("")]: [ + "the extraction mints {anchorCount:number} anchors", + "the extraction reports {findingCount:number} findings", + ], + }, + }, + model: { + terms: { + "Protocol builder binding": + "A builder import from the public Protocol package, or a relative import whose importer-relative resolution — including the TypeScript `.js`-to-`.ts` convention — canonicalizes to this package's `ids` or `model/code-anchor` module; consumer-local lookalike modules confer no binding authority. On the CommonJS package surface the trusted relative-module set is empty (`import.meta.url` is rewritten away), so relative bindings mint no anchors there while package imports stay trusted.", + anchor: + "A human-written source binding from one code location to one Spec ID, carrying identity, an optional label, and one target only.", + "anchor-constant form": + "The top-level const builder call that the MVP extractor reifies; decorator and JSDoc forms remain unextracted representations.", + "code anchor": + "An implementation-flavored binding that derives an anchored satisfies edge.", + "oracle anchor": + "A binding that records an oracle's models target without deriving a delivery fact.", + "test anchor": + "A binding that derives an anchored verifies edge from a test to its target Spec.", + "untrusted builder": + "A builder call whose import is no Protocol builder binding: it mints nothing and reports nothing, because a source file that never bound to the Protocol is not authoring drift to report. The realizing entrypoints are `protocolBindingScopeFor` and `collectProtocolBindings` in `src/extract/protocol-bindings.ts`.", + }, + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:model.anchors.lookalike-refusal", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/model/anchors.lookalike-refusal.sdp.md", + title: "A consumer-local lookalike builder mints no anchor and no finding", + narrative: null, + sections: { + intent: { + outcome: + "Execute the builder-trust law where a repository's own module merely resembles the Protocol builders.", + }, + behavior: { + examples: [ + { + given: [ + 'a repository whose one source file builds an anchor through {builderSource: "a consumer-local lookalike module"}', + ], + when: ["the repository is extracted"], + [["t", "hen"].join("")]: [ + "the extraction mints {anchorCount: 0} anchors", + "the extraction reports {findingCount: 0} findings", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:model.anchors.physical-identity", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/model/anchors.physical-identity.sdp.md", + title: "A deep relative import that resolves to the Protocol builders is trusted", + narrative: null, + sections: { + intent: { + outcome: + "Execute the builder-trust law where trust turns on physical module identity rather than the import's spelling.", + }, + behavior: { + examples: [ + { + given: [ + 'a repository whose one source file builds an anchor through {builderSource: "a relative import resolving to the Protocol builder modules"}', + ], + when: ["the repository is extracted"], + [["t", "hen"].join("")]: [ + "the extraction mints {anchorCount: 1} anchors", + "the extraction reports {findingCount: 0} findings", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, +] as const; diff --git a/test/self-hosting-oracle/pack-members.ts b/test/self-hosting-oracle/pack-members.ts new file mode 100644 index 0000000..4e2f921 --- /dev/null +++ b/test/self-hosting-oracle/pack-members.ts @@ -0,0 +1,92 @@ +// The authored membership of `pack:self-hosting-v1`, in manifest order — the Pack states no truth +// of its own, so this is a transcription of the manifest the `belongsTo` edges are derived from. + +export const expectedPackMembers = [ + "spec:carrier.markdown-authoring", + "spec:carrier.envelope-contract", + "spec:carrier.markdown-parser", + "spec:carrier.sdp-import", + "spec:carrier.sdp-import.round-trip", + "spec:carrier.prose-ownership-rule", + "spec:protocol.self-hosting", + "spec:extraction.derive-graph", + "spec:extraction.determinism", + "spec:extraction.build-pipeline", + "spec:extraction.excludes", + "spec:extraction.claim-taxonomy", + "spec:extraction.regenerability", + "spec:extraction.schema-versioning", + "spec:extraction.executable-contracts", + "spec:validation.readiness-floor", + "spec:validation.duplicate-ids", + "spec:validation.two-check-families", + "spec:validation.referential-integrity", + "spec:validation.claim-separation", + "spec:validation.verification-linkage", + "spec:validation.pack-coherence", + "spec:validation.authored-honesty", + "spec:validation.warn-level-signals", + "spec:consumers.projections-model", + "spec:consumers.agent-surface", + "spec:consumers.design-review", + "spec:consumers.reader", + "spec:consumers.edit-model", + "spec:model.protocol-domain", + "spec:model.core-model", + "spec:model.spec-sections", + "spec:model.relations", + "spec:model.stable-ids", + "spec:model.pack-aggregate", + "spec:model.anchors", + "spec:validation.duplicate-ids.dual-carrier", + "spec:validation.warn-level-signals.orphan-signal", + "spec:validation.warn-level-signals.ready-gap-signal", + "spec:validation.referential-integrity.dangling-target", + "spec:validation.referential-integrity.did-you-mean", + "spec:validation.authored-honesty.section-authored-fact", + "spec:validation.authored-honesty.unearned-stated-fact", + "spec:validation.claim-separation.collapsed-edge-claim", + "spec:validation.claim-separation.unratified-descriptor", + "spec:validation.verification-linkage.unbound-example", + "spec:validation.verification-linkage.unresolved-oracle", + "spec:validation.pack-coherence.incoherent-aggregate", + "spec:extraction.excludes.segment-boundary", + "spec:extraction.excludes.refused-path", + "spec:extraction.schema-versioning.declared-version", + "spec:model.stable-ids.namespaced-round-trip", + "spec:model.stable-ids.malformed-refusal", + "spec:carrier.markdown-parser.bounded-parity", + "spec:extraction.example-runner", + "spec:extraction.example-runner.step-order", + "spec:extraction.example-runner.red-step-naming", + "spec:extraction.executable-contracts.concreteness-refusal", + "spec:extraction.executable-contracts.multi-entry-example", + "spec:extraction.executable-contracts.case-colliding-path", + "spec:carrier.slot-notation", + "spec:carrier.slot-notation.typed-declaration", + "spec:carrier.slot-notation.refused-guess", + "spec:model.anchors.lookalike-refusal", + "spec:model.anchors.physical-identity", + "spec:validation.two-check-families.split-report", + "spec:decisions.plain-language-references", + "spec:decisions.concept-docs-dissolve", + "spec:decisions.one-validation-path", + "spec:decisions.sdp-ts-extension", + "spec:decisions.point-per-example", + "spec:decisions.carrier-ruling", + "spec:decisions.prose-ownership", + "spec:decisions.envelope-grammar-posture", + "spec:decisions.exclusion-contract", + "spec:decisions.executable-meta-model", + "spec:decisions.adopt-the-nouns", + "spec:decisions.one-primitive", + "spec:decisions.protocol-naming", + "spec:decisions.binding-not-liveness", + "spec:decisions.content-only-sections", + "spec:decisions.typing-law", + "spec:decisions.kind-conditional-floor", + "spec:decisions.carried-evidence", + "spec:decisions.pack-reified", + "spec:decisions.agent-surface-scripts-graph", + "spec:decisions.mcp-deferred", +] as const; diff --git a/test/self-hosting-oracle/protocol.ts b/test/self-hosting-oracle/protocol.ts new file mode 100644 index 0000000..4f8d0b2 --- /dev/null +++ b/test/self-hosting-oracle/protocol.ts @@ -0,0 +1,26 @@ +// The authored descriptors of the `protocol` family of the self-hosting corpus — +// human transcription of intended truth, never computed from the derived graph. Extraction must +// reproduce every value here exactly; a disagreement is drift to resolve on one side or the other. + +export const protocolSpecs = [ + { + id: "spec:protocol.self-hosting", + specKind: "behavior", + altitude: "epic", + readiness: "defined", + file: "specs/protocol/self-hosting.sdp.md", + title: "The Protocol authors and validates itself", + narrative: + "The Protocol's own delivery model exercises the same carrier, graph, checks, and projections offered to consumers.", + sections: { + intent: { outcome: "Prove the Protocol can carry its own intended truth honestly." }, + behavior: { + rules: [ + "All authored carriers derive one regenerable graph through one validation path.", + "Self-hosting remains deterministic in a clean clone.", + ], + }, + }, + deliveryFacts: [], + }, +] as const; diff --git a/test/self-hosting-oracle/validation.ts b/test/self-hosting-oracle/validation.ts new file mode 100644 index 0000000..a18078c --- /dev/null +++ b/test/self-hosting-oracle/validation.ts @@ -0,0 +1,692 @@ +// The authored descriptors of the `validation` family of the self-hosting corpus — +// human transcription of intended truth, never computed from the derived graph. Extraction must +// reproduce every value here exactly; a disagreement is drift to resolve on one side or the other. + +export const validationSpecs = [ + { + id: "spec:validation.readiness-floor", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/validation/readiness-floor.sdp.md", + title: "Stated readiness must clear its floor", + narrative: null, + sections: { + intent: { outcome: "Refuse maturity claims that their authored evidence does not support." }, + behavior: { + rules: [ + "A Spec may state a readiness only when every clause in that readiness floor passes.", + "The `ready` floor reads the Spec's own edges through three clauses: every authored relation resolves to a known target, every `refines` and `dependsOn` target itself stands at least `defined`, and every anchor bound to the Spec resolves.", + "The anchor clause reads the bindings that are present, so a Spec carrying no anchor clears it — the floor never demands a binding an author has not made.", + "The floor table in `src/validate/readiness-floor.ts` is the clause set's code-level source of truth and the realizing entrypoint; the clauses of the lower rungs are stated there and are not re-enumerated here.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:validation.duplicate-ids", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + file: "specs/validation/duplicate-ids.sdp.md", + title: "Duplicate carrier IDs are excluded loudly", + narrative: null, + sections: { + intent: { outcome: "Prevent ambiguous authored identity from entering the graph." }, + behavior: { + rules: [ + "If more than one carrier declares an ID, every duplicate site receives extract/duplicate-id and no ambiguous node is derived.", + ], + exampleSpace: { + given: [ + "a {firstCarrier:string} carrier declares {specId:string}", + "a {secondCarrier:string} carrier declares {specId:string}", + ], + when: ["the extraction root is read"], + [["t", "hen"].join("")]: [ + "both sites report {findingId:string}", + "no graph node is emitted for {specId:string}", + ], + }, + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:validation.duplicate-ids.dual-carrier", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/duplicate-ids.dual-carrier.sdp.md", + title: "TypeScript and Markdown duplicates are both refused", + narrative: null, + sections: { + intent: { outcome: "Execute the duplicate-ID rule across both carrier surfaces." }, + behavior: { + examples: [ + { + given: [ + 'a {firstCarrier: "TypeScript"} carrier declares {specId: "spec:fixture.duplicate"}', + 'a {secondCarrier: "Markdown"} carrier declares {specId: "spec:fixture.duplicate"}', + ], + when: ["the extraction root is read"], + [["t", "hen"].join("")]: [ + 'both sites report {findingId: "extract/duplicate-id"}', + 'no graph node is emitted for {specId: "spec:fixture.duplicate"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.two-check-families", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/validation/two-check-families.sdp.md", + title: "Validation separates well-formedness from non-pretending", + narrative: null, + sections: { + intent: { + outcome: + "Keep the graph trustworthy by checking conformance and honesty without judging content quality or enforcing workflow.", + }, + behavior: { + rules: [ + "Every validator belongs to either the conformance family, which checks meta-model well-formedness, or the honesty family, which rejects authored or overstated derived truth.", + "Validation errors fail the build; gaps and orphans remain informative signals rather than delivery-process gates.", + "Types enforce structural shape, schema validates graph payloads, and graph validators enforce cross-file conformance and honesty; no one layer substitutes for the others.", + "All graph validation runs through the one derived graph path: source, extraction, graph, then checks.", + "The two families are load-bearing, so an aggregate report spanning both states no family of its own while every finding names the family it came from.", + "The realizing entrypoints are `graphValidatorIds` and `validateGraph` in `src/validate/validators.ts`.", + ], + exampleSpace: { + given: [ + 'the graph holds a spec {specId:string} at readiness {readiness:"idea"|"ready"}', + "the spec declares a dependsOn relation to the absent target {targetId:string}", + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + "the aggregate report states no family of its own", + 'the conformance family reports {conformanceId:string} at severity {conformanceSeverity:"warning"|"error"}', + 'the honesty family reports {honestyId:string} at severity {honestySeverity:"warning"|"error"}', + ], + }, + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:validation.two-check-families.split-report", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/two-check-families.split-report.sdp.md", + title: "One report carries both families and claims neither as its own", + narrative: null, + sections: { + intent: { + outcome: + "Execute the family split where one probe graph trips a conformance error and an informative honesty signal at once; the same dangling relation also fails the readiness floor on the ready probe, so the family assertions read by containment.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.two-check-families"} at readiness {readiness: "ready"}', + 'the spec declares a dependsOn relation to the absent target {targetId: "spec:probe.absent-dependency"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + "the aggregate report states no family of its own", + 'the conformance family reports {conformanceId: "conformance/referential-integrity"} at severity {conformanceSeverity: "error"}', + 'the honesty family reports {honestyId: "honesty/gaps"} at severity {honestySeverity: "warning"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.referential-integrity", + specKind: "rule", + altitude: "story", + readiness: "ready", + file: "specs/validation/referential-integrity.sdp.md", + title: "Every graph reference resolves", + narrative: null, + sections: { + intent: { + outcome: + "Keep derived graph relationships trustworthy by refusing references to absent nodes.", + }, + behavior: { + rules: [ + "Every edge endpoint and every Pack model reference must resolve to a node in the derived graph; an unresolved reference is a conformance error.", + "The finding names the unique nearest known id as a suggestion and stays silent when two candidates tie, because resolving ambiguity silently is never the check's job.", + "The realizing validator entrypoint is `checkReferentialIntegrity` in `src/validate/validators.ts`.", + ], + exampleSpace: { + given: [ + "the graph holds one spec {presentId:string}", + "the spec declares a dependsOn relation to {targetId:string}", + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId:string} at severity {severity:"warning"|"error"}', + "the finding offers the nearest-id suggestion: {suggested:boolean}", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.referential-integrity.dangling-target", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/referential-integrity.dangling-target.sdp.md", + title: "An unrelated missing target is a bare conformance error", + narrative: null, + sections: { + intent: { + outcome: "Execute the unresolved-reference law where no known id is near the missing one.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds one spec {presentId: "spec:probe.create-order"}', + 'the spec declares a dependsOn relation to {targetId: "spec:probe.fulfilment-policy"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "conformance/referential-integrity"} at severity {severity: "error"}', + "the finding offers the nearest-id suggestion: {suggested: false}", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.referential-integrity.did-you-mean", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/referential-integrity.did-you-mean.sdp.md", + title: "A unique near miss earns a did-you-mean suggestion", + narrative: null, + sections: { + intent: { + outcome: "Execute the unresolved-reference law where exactly one known id is a near miss.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds one spec {presentId: "spec:probe.create-order"}', + 'the spec declares a dependsOn relation to {targetId: "spec:probe.create-ordr"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "conformance/referential-integrity"} at severity {severity: "error"}', + "the finding offers the nearest-id suggestion: {suggested: true}", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.claim-separation", + specKind: "rule", + altitude: "story", + readiness: "ready", + file: "specs/validation/claim-separation.sdp.md", + title: "Graph claims and contracts stay distinct", + narrative: null, + sections: { + intent: { + outcome: + "Preserve the graph's declared, anchored, and inferred distinctions while keeping its typed contracts lawful.", + }, + behavior: { + rules: [ + "Node and edge types, claims, descriptors, and relation endpoint contracts must use their ratified forms; the claim taxonomy never collapses.", + "An unratified descriptor value fails closed: it is a conformance error, and no readiness floor is evaluated over it.", + "The realizing validator entrypoint is `checkClaimSeparation` in `src/validate/validators.ts`.", + ], + exampleSpace: { + given: [ + "the graph holds a spec {specId:string}", + 'the graph carries an off-contract {element:"edge claim"|"descriptor value"} spelled {value:string}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId:string} at severity {severity:"warning"|"error"}', + "the finding message states {phrase:string}", + "the report holds {floorCount:number} readiness-floor findings", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.claim-separation.collapsed-edge-claim", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/claim-separation.collapsed-edge-claim.sdp.md", + title: "A binding edge cannot borrow the declared claim", + narrative: null, + sections: { + intent: { + outcome: "Execute the edge-contract law where a satisfies edge carries the authored claim.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.create-order"}', + 'the graph carries an off-contract {element: "edge claim"} spelled {value: "declared"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "conformance/claim-separation"} at severity {severity: "error"}', + 'the finding message states {phrase: "never collapsed"}', + "the report holds {floorCount: 0} readiness-floor findings", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.claim-separation.unratified-descriptor", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/claim-separation.unratified-descriptor.sdp.md", + title: "An unratified kind fails closed instead of reaching the floor", + narrative: null, + sections: { + intent: { + outcome: + "Execute the descriptor law where a foreign producer states a kind the model never ratified.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.create-order"}', + 'the graph carries an off-contract {element: "descriptor value"} spelled {value: "saga"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "conformance/claim-separation"} at severity {severity: "error"}', + 'the finding message states {phrase: "outside the ratified descriptor values"}', + "the report holds {floorCount: 0} readiness-floor findings", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.verification-linkage", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/validation/verification-linkage.sdp.md", + title: "Declared verification resolves to a performing trace", + narrative: null, + sections: { + intent: { + outcome: + "Keep verification relationships meaningful by requiring declared test and oracle traces to resolve to their enabled bindings.", + }, + behavior: { + rules: [ + "A declared verifies relation and an oracle model relation must resolve through their respective binding traces before either can stand as verification evidence.", + "A non-resolving trace is named loudly and confers no delivery fact, because silence would read as verification the graph never earned.", + "At most one expected-outcome authority may model an example space: a second resolving oracle binding on the same space is an error, because two authorities leave the modeled outcome ambiguous.", + "The realizing validator entrypoints are `checkVerifiesLinkage` and `checkOracleLinkage` in `src/validate/validators.ts`.", + ], + exampleSpace: { + given: [ + "the graph holds a parent spec {parentId:string}", + 'a non-resolving {verifierKind:"example spec"|"oracle anchor"} named {verifierId:string} points at it', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId:string} at severity {severity:"warning"|"error"}', + "the parent earns the delivery fact has-verifier: {conferred:boolean}", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.verification-linkage.unbound-example", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/verification-linkage.unbound-example.sdp.md", + title: "A declared verifier no test binds confers nothing", + narrative: null, + sections: { + intent: { + outcome: + "Execute the verifies-linkage law where no test anchor completes the spec-to-test trace.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a parent spec {parentId: "spec:probe.create-order"}', + 'a non-resolving {verifierKind: "example spec"} named {verifierId: "spec:probe.create-order.valid-cart"} points at it', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "conformance/verifies-linkage"} at severity {severity: "warning"}', + "the parent earns the delivery fact has-verifier: {conferred: false}", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.verification-linkage.unresolved-oracle", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/verification-linkage.unresolved-oracle.sdp.md", + title: "An oracle with no example space to model confers nothing", + narrative: null, + sections: { + intent: { + outcome: "Execute the oracle-linkage law where the modeled spec owns no example space.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a parent spec {parentId: "spec:probe.order-policy"}', + 'a non-resolving {verifierKind: "oracle anchor"} named {verifierId: "oracle:probe.order-policy"} points at it', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "conformance/oracle-linkage"} at severity {severity: "error"}', + "the parent earns the delivery fact has-verifier: {conferred: false}", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.pack-coherence", + specKind: "rule", + altitude: "story", + readiness: "ready", + file: "specs/validation/pack-coherence.sdp.md", + title: "Packs are coherent aggregates", + narrative: null, + sections: { + intent: { + outcome: + "Keep review aggregates coherent without treating them as truth-bearing delivery artifacts.", + }, + behavior: { + rules: [ + "Pack membership must not repeat a Spec, and every modelRef must resolve to a model-kind Spec.", + "Membership is counted on the derived belongsTo edges the manifest re-expresses, so a repeated manifest entry is named once per repeated member.", + "The realizing validator entrypoint is `checkPackCoherence` in `src/validate/validators.ts`.", + ], + exampleSpace: { + given: [ + "a pack {packId:string} lists the spec {specId:string} {memberCount:number} times", + "the pack also names that spec as a modelRef", + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId:string} at severity {severity:"warning"|"error"}', + "the report holds {findingCount:number} pack-coherence findings", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.pack-coherence.incoherent-aggregate", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/pack-coherence.incoherent-aggregate.sdp.md", + title: "A repeated member and a non-model modelRef are both named", + narrative: null, + sections: { + intent: { + outcome: "Execute both halves of the pack law against one incoherent aggregate.", + }, + behavior: { + examples: [ + { + given: [ + 'a pack {packId: "pack:probe.checkout"} lists the spec {specId: "spec:probe.create-order"} {memberCount: 2} times', + "the pack also names that spec as a modelRef", + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "conformance/pack-coherence"} at severity {severity: "error"}', + "the report holds {findingCount: 2} pack-coherence findings", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.authored-honesty", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/validation/authored-honesty.sdp.md", + title: "Machine truth is never authored", + narrative: null, + sections: { + intent: { + outcome: + "Keep derived graph truth trustworthy by rejecting any authored substitute for machine-derived claims or facts.", + }, + behavior: { + rules: [ + "Specs and Packs must not author derived edges, claims, or delivery facts, and any stated delivery facts must equal the graph's recomputed facts.", + "The realizing validator entrypoints are `checkAuthoringShape` and `checkDeliveryFacts` in `src/validate/validators.ts`.", + ], + exampleSpace: { + given: [ + "the graph holds a spec {specId:string}", + 'the spec hand-authors the delivery fact {factName:"implemented"|"has-verifier"} at {site:"a behavior section carrier"|"the node deliveryFacts array"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId:string} at severity {severity:"warning"|"error"}', + "the finding names the fact {relatedId:string} and states {phrase:string}", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.authored-honesty.section-authored-fact", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/authored-honesty.section-authored-fact.sdp.md", + title: "A delivery fact smuggled into a section is refused", + narrative: null, + sections: { + intent: { + outcome: + "Execute the authoring-shape refusal on a section carrier that names a derived fact.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.smuggled-fact"}', + 'the spec hand-authors the delivery fact {factName: "implemented"} at {site: "a behavior section carrier"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "honesty/authoring-shape"} at severity {severity: "error"}', + 'the finding names the fact {relatedId: "implemented"} and states {phrase: "derived, never authored"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.authored-honesty.unearned-stated-fact", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/authored-honesty.unearned-stated-fact.sdp.md", + title: "A stated delivery fact no binding earns is refused", + narrative: null, + sections: { + intent: { + outcome: + "Execute the delivery-fact refusal where the stated array outruns the recomputed facts.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.unearned-fact"}', + 'the spec hand-authors the delivery fact {factName: "has-verifier"} at {site: "the node deliveryFacts array"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "honesty/delivery-facts"} at severity {severity: "error"}', + 'the finding names the fact {relatedId: "has-verifier"} and states {phrase: "derived, never authored"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.warn-level-signals", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/validation/warn-level-signals.sdp.md", + title: "Missing connective evidence warns without failing", + narrative: null, + sections: { + intent: { + outcome: + "Surface graph conditions that need attention without turning informative delivery signals into workflow gates.", + }, + behavior: { + rules: [ + "Orphaned Specs and ready Specs lacking a resolving verifier are warnings, not validation errors.", + "The realizing validator entrypoints are `checkOrphans` and `checkGaps` in `src/validate/validators.ts`.", + ], + exampleSpace: { + given: [ + 'the graph holds a spec {specId:string} at readiness {readiness:"idea"|"ready"}', + 'the spec declares {relations:"no relation"|"a decidedBy decision"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId:string} at severity {severity:"warning"|"error"}', + "the report holds {errorCount:number} errors", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.warn-level-signals.orphan-signal", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/warn-level-signals.orphan-signal.sdp.md", + title: "A disconnected spec warns and fails nothing", + narrative: null, + sections: { + intent: { outcome: "Execute the orphan signal on a spec no relation reaches." }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.orphan-signal"} at readiness {readiness: "idea"}', + 'the spec declares {relations: "no relation"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "conformance/orphans"} at severity {severity: "warning"}', + "the report holds {errorCount: 0} errors", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.warn-level-signals.ready-gap-signal", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/warn-level-signals.ready-gap-signal.sdp.md", + title: "A ready spec without a verifier warns and fails nothing", + narrative: null, + sections: { + intent: { + outcome: "Execute the gap signal on a connected ready spec no verifier resolves.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.gap-signal"} at readiness {readiness: "ready"}', + 'the spec declares {relations: "a decidedBy decision"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "honesty/gaps"} at severity {severity: "warning"}', + "the report holds {errorCount: 0} errors", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, +] as const; From 170ddf93dcc9a6a4614ce04e4671cb360d7ebdc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 19:19:52 +0200 Subject: [PATCH 03/16] refactor(build): read the contract-dependent suites from one shared module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test wrapper and the lint config named the same six bound suites independently; the next bound suite would repeat the clean-clone lint surprise. Both now import `contract-dependent-suites.mjs`, which states the per-tree rows once — contracts dir, generation command, test paths — and the lint exemption derives its file list from the root tree's suites. Behavior is byte-identical: the same preflight recovery text, the same exemption set, and the example tree stays outside the typed-lint globs. The documented exclusion rule survives on the shared module: an in-memory suite that imports no contract is never listed. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- contract-dependent-suites.mjs | 38 +++++++++++++++++++++++++++++++++++ eslint.config.js | 22 +++++++++----------- vitest-test.mjs | 24 +++++----------------- 3 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 contract-dependent-suites.mjs diff --git a/contract-dependent-suites.mjs b/contract-dependent-suites.mjs new file mode 100644 index 0000000..64d7871 --- /dev/null +++ b/contract-dependent-suites.mjs @@ -0,0 +1,38 @@ +// One source of truth for the suites that import generated contracts. +// +// A bound suite couples two surfaces that never see each other: the test wrapper +// (`vitest-test.mjs`), which must refuse to run before the tree it imports exists, and the lint +// config (`eslint.config.js`), which must stay runnable in a clean room where that tree has not +// been generated yet. Naming the same files twice drifted once; both consumers now read this +// module, so a new bound suite enters the list here and both surfaces follow. +// +// One row per generated contract tree, listing every test file that imports from it. Rows stay +// per-tree so the recovery command a missing tree names is stated once, never repeated per suite. +// +// The exclusion rule: a suite that derives its graph in memory never belongs here — the corpus +// oracle (`test/self-hosting-graph.test.ts`) and the contracts self-check +// (`test/self-hosting-contracts.test.ts`) import no contract, so listing them would weaken lint +// and demand a generation neither needs. + +export const contractDependentSuites = [ + { + contracts: "generated/contracts", + generation: "npm run generate:self-hosting", + testPaths: [ + "test/self-hosting-carrier.test.ts", + "test/self-hosting-duplicate-ids.test.ts", + "test/self-hosting-extraction.test.ts", + "test/self-hosting-model.test.ts", + "test/self-hosting-sdp-import.test.ts", + "test/self-hosting-validators.test.ts", + ], + }, + { + contracts: "examples/checkout-v1/generated/contracts", + generation: "npm run generate:example", + testPaths: ["examples/checkout-v1/test/orders/create-order.valid-cart.test.ts"], + }, +]; + +/** The repository-root tree — the one whose suites fall inside the typed-lint globs. */ +export const rootContractDependentSuite = contractDependentSuites[0]; diff --git a/eslint.config.js b/eslint.config.js index b4cad68..01a12da 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,6 +3,8 @@ import globals from "globals"; import tseslint from "typescript-eslint"; import { fileURLToPath } from "node:url"; +import { rootContractDependentSuite } from "./contract-dependent-suites.mjs"; + const tsconfigRootDir = fileURLToPath(new URL(".", import.meta.url)); const typedTsFiles = ["src/**/*.ts", "test/**/*.ts", "tsup.config.ts", "vitest.config.ts"]; const exampleTsFiles = ["examples/**/*.ts"]; @@ -50,18 +52,14 @@ export default tseslint.config( // The root generated contracts are intentionally absent until the later generate:self-hosting // gate leg. Typecheck runs after that generation and checks these tests' contract types; lint // keeps all other rules enabled without making the required lint-before-generation order depend - // on ignored derived output. This list is the bound-suite half of `vitest-test.mjs`'s root - // contract-dependency row — every suite that imports `generated/contracts/` belongs here, and - // a suite that derives its graph in memory (the corpus oracle, the contracts self-check) must - // not, so lint keeps full strength where nothing is missing in a clean room. - files: [ - "test/self-hosting-carrier.test.ts", - "test/self-hosting-duplicate-ids.test.ts", - "test/self-hosting-extraction.test.ts", - "test/self-hosting-model.test.ts", - "test/self-hosting-sdp-import.test.ts", - "test/self-hosting-validators.test.ts", - ], + // on ignored derived output. The file list is derived from the shared + // `contract-dependent-suites.mjs` root row — the same rows `vitest-test.mjs` reads, so the two + // surfaces can no longer drift apart. Every suite that imports `generated/contracts/` belongs + // in that row, and a suite that derives its graph in memory (the corpus oracle, the contracts + // self-check) must not, so lint keeps full strength where nothing is missing in a clean room. + // Only the root tree's suites appear here: the example tree lints under `exampleTsFiles`, + // outside the typed-lint globs these exemptions relax. + files: [...rootContractDependentSuite.testPaths], rules: { "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-assignment": "off", diff --git a/vitest-test.mjs b/vitest-test.mjs index bf51297..35636ab 100644 --- a/vitest-test.mjs +++ b/vitest-test.mjs @@ -4,31 +4,17 @@ import { existsSync, readFileSync, readdirSync } from "node:fs"; import { dirname, isAbsolute, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; +import { contractDependentSuites } from "./contract-dependent-suites.mjs"; + const argv = process.argv.slice(2); const vitestArgs = argv.includes("--run") ? argv : ["--run", ...argv]; const repositoryRoot = dirname(fileURLToPath(import.meta.url)); // One row per generated contract tree, listing every test file that imports from it. Rows stay // per-tree so the recovery command a missing tree names is stated once, never repeated per suite. -const contractDependencies = [ - { - contracts: "generated/contracts", - generation: "npm run generate:self-hosting", - testPaths: [ - "test/self-hosting-carrier.test.ts", - "test/self-hosting-duplicate-ids.test.ts", - "test/self-hosting-extraction.test.ts", - "test/self-hosting-model.test.ts", - "test/self-hosting-sdp-import.test.ts", - "test/self-hosting-validators.test.ts", - ], - }, - { - contracts: "examples/checkout-v1/generated/contracts", - generation: "npm run generate:example", - testPaths: ["examples/checkout-v1/test/orders/create-order.valid-cart.test.ts"], - }, -]; +// The rows live in `contract-dependent-suites.mjs` — the shared module `eslint.config.js` reads +// too, so a new bound suite is named once and both surfaces follow. +const contractDependencies = contractDependentSuites; const pathFilters = argv.filter((argument) => !argument.startsWith("-")); const hasPathFilter = pathFilters.length > 0; From 6f92097600d6bf9a82487266d9a338966943290b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 19:30:25 +0200 Subject: [PATCH 04/16] docs(specs,tests): carry the lower readiness-floor rungs and the per-kind evidence table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine has evaluated five `idea` clauses, three `scoped` clauses, and two `defined` clauses since the floor landed, but the carrying Spec deferred every one of them to the code table and said so in a Rule bullet. It now states them in authored words, states that evaluation is cumulative, and marks the two evidence clauses as the floor's one kind-conditional place; the deferral sentence is gone and the MD-13 posture survives — the code table stays the clause set's code-level source of truth and the realizing entrypoint, read as one law twice rather than as two floors. `spec:validation.kind-evidence` is the new refining rule Spec that carries the per-kind table: the shared behavior/workflow/contract row, the converged rule and model rows, the example row's concreteness law, the constraint row's machine-readable target, the decision row, the contract row's named deferral, and the promoted-evidence honesty bound (a promoted child counts only when it clears its own kind's present cell; a constrainedBy edge only when it resolves to a constraint carrying its constraints). Five bound points enter the validators suite in the probe-graph style, each asserting the finding id and the failing clause id positively, and each mutation-probed red against a broken copy of the clause it names. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- specs/self-hosting.pack.sdp.ts | 6 + .../kind-evidence.constraints-alone.sdp.md | 22 ++ .../kind-evidence.empty-promoted-child.sdp.md | 22 ++ specs/validation/kind-evidence.sdp.md | 36 +++ ...kind-evidence.untargeted-constraint.sdp.md | 22 ++ ...diness-floor.blocking-open-question.sdp.md | 22 ++ specs/validation/readiness-floor.sdp.md | 19 +- ...adiness-floor.unrelated-scoped-spec.sdp.md | 22 ++ test/self-hosting-graph.test.ts | 14 +- test/self-hosting-oracle/anchors.ts | 50 ++++ .../self-hosting-oracle/declared-relations.ts | 44 ++++ test/self-hosting-oracle/pack-members.ts | 6 + test/self-hosting-oracle/validation.ts | 222 +++++++++++++++++- test/self-hosting-validators.test.ts | 219 +++++++++++++++++ 14 files changed, 717 insertions(+), 9 deletions(-) create mode 100644 specs/validation/kind-evidence.constraints-alone.sdp.md create mode 100644 specs/validation/kind-evidence.empty-promoted-child.sdp.md create mode 100644 specs/validation/kind-evidence.sdp.md create mode 100644 specs/validation/kind-evidence.untargeted-constraint.sdp.md create mode 100644 specs/validation/readiness-floor.blocking-open-question.sdp.md create mode 100644 specs/validation/readiness-floor.unrelated-scoped-spec.sdp.md diff --git a/specs/self-hosting.pack.sdp.ts b/specs/self-hosting.pack.sdp.ts index 82c557a..ab5ee4d 100644 --- a/specs/self-hosting.pack.sdp.ts +++ b/specs/self-hosting.pack.sdp.ts @@ -71,6 +71,12 @@ export const selfHostingV1Pack = pack({ ref("spec:model.anchors.lookalike-refusal"), ref("spec:model.anchors.physical-identity"), ref("spec:validation.two-check-families.split-report"), + ref("spec:validation.readiness-floor.unrelated-scoped-spec"), + ref("spec:validation.readiness-floor.blocking-open-question"), + ref("spec:validation.kind-evidence"), + ref("spec:validation.kind-evidence.constraints-alone"), + ref("spec:validation.kind-evidence.untargeted-constraint"), + ref("spec:validation.kind-evidence.empty-promoted-child"), ref("spec:decisions.plain-language-references"), ref("spec:decisions.concept-docs-dissolve"), ref("spec:decisions.one-validation-path"), diff --git a/specs/validation/kind-evidence.constraints-alone.sdp.md b/specs/validation/kind-evidence.constraints-alone.sdp.md new file mode 100644 index 0000000..82042cc --- /dev/null +++ b/specs/validation/kind-evidence.constraints-alone.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:validation.kind-evidence.constraints-alone +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.kind-evidence + verifies: spec:validation.kind-evidence +--- +# Constraints alone stop short of complete behavior evidence + +## Intent +- outcome: Execute the behavior-family row where the only evidence is the form that clears present but not complete. + +```gwt +Given the graph holds a {kind: "behavior"} spec {specId: "spec:probe.constraints-alone"} stating readiness {readiness: "defined"} +Given its only evidence is {evidence: "a constraints entry carrying a target"} +When the graph is validated +Then the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"} +Then the finding names the unmet floor clause {clauseId: "kind-evidence-complete"} +Then the report holds {errorCount: 1} errors +``` diff --git a/specs/validation/kind-evidence.empty-promoted-child.sdp.md b/specs/validation/kind-evidence.empty-promoted-child.sdp.md new file mode 100644 index 0000000..f24acad --- /dev/null +++ b/specs/validation/kind-evidence.empty-promoted-child.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:validation.kind-evidence.empty-promoted-child +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.kind-evidence + verifies: spec:validation.kind-evidence +--- +# An empty promoted child confers no evidence + +## Intent +- outcome: Execute the promoted-evidence bound where a refining child carries none of its own kind's evidence. + +```gwt +Given the graph holds a {kind: "behavior"} spec {specId: "spec:probe.empty-promotion"} stating readiness {readiness: "scoped"} +Given its only evidence is {evidence: "an empty promoted rule child"} +When the graph is validated +Then the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"} +Then the finding names the unmet floor clause {clauseId: "kind-evidence-present"} +Then the report holds {errorCount: 1} errors +``` diff --git a/specs/validation/kind-evidence.sdp.md b/specs/validation/kind-evidence.sdp.md new file mode 100644 index 0000000..c418a95 --- /dev/null +++ b/specs/validation/kind-evidence.sdp.md @@ -0,0 +1,36 @@ +--- +id: spec:validation.kind-evidence +kind: rule +altitude: feature +readiness: ready +relations: + refines: spec:validation.readiness-floor + decidedBy: spec:decisions.kind-conditional-floor +--- +# Each kind carries its own evidence + +## Intent +- outcome: State what a Spec of each kind must show before its readiness floor accepts the evidence clauses. + +## Rule +- Each kind names its natural evidence: the `scoped` rung requires that evidence present, and the `defined` rung requires it complete wherever the kind defines a stronger form. This table is the whole kind-aware story — there is no second overlay mechanism. +- `behavior`, `workflow`, and `contract` share one row. Evidence is present with rules, examples, flows, or constraints — inline, or promoted onto a refining child or a `constrainedBy` constraint. Evidence is complete with rules and/or examples, inline or promoted; constraints alone no longer suffice. +- A `rule` converges across the two rungs: its statement is its evidence, because a rule's content is its statement. +- An `example` shows evidence present with an examples entry, prose acceptable. It shows evidence complete with at least one structured given/when/then entry whose every used step is fully bound and belongs compatibly to any example space its parent owns — the concreteness law. +- A `constraint` shows evidence present with a non-empty constraints section, and complete when every entry carries a machine-readable target. +- A `model` converges on non-empty terms: a vocabulary either has terms or it does not. +- A `decision` shows evidence present once its decision section is there — context and alternatives may precede the choice — and complete once the chosen option is written. +- The `contract` row stands on the behavior row as a named deferral: when a dedicated contract section lands, the typing law pulls it in and this row repoints to it. +- Promoted evidence carries an honesty bound. A promoted child counts only when it is a `rule` or `example` Spec that itself clears its own kind's present cell, and a `constrainedBy` edge counts only when it resolves to a `constraint` Spec carrying its constraints — promotion moves content out, so an empty stub child is not a promotion and confers nothing. +- The rows are monotonic, promotion-neutral, and converge honestly where a kind has no stronger form; those three bounds belong to the decision this Spec is shaped by and to the carried-evidence decision, and are not restated as law here. +- The evidence table in `src/validate/readiness-floor.ts` is the row set's code-level source of truth and the realizing entrypoint; the rows stated here and that table are one law read twice, so any disagreement between them is drift to resolve on one side. + +## Example space +```gwt-vocabulary +Given the graph holds a {kind:"behavior"|"constraint"} spec {specId:string} stating readiness {readiness:"scoped"|"defined"} +Given its only evidence is {evidence:"a constraints entry carrying a target"|"a constraints entry with no target"|"an empty promoted rule child"} +When the graph is validated +Then the report names {findingId:string} at severity {severity:"warning"|"error"} +Then the finding names the unmet floor clause {clauseId:string} +Then the report holds {errorCount:number} errors +``` diff --git a/specs/validation/kind-evidence.untargeted-constraint.sdp.md b/specs/validation/kind-evidence.untargeted-constraint.sdp.md new file mode 100644 index 0000000..9c6cc46 --- /dev/null +++ b/specs/validation/kind-evidence.untargeted-constraint.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:validation.kind-evidence.untargeted-constraint +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.kind-evidence + verifies: spec:validation.kind-evidence +--- +# A constraint without a machine-readable target is not complete + +## Intent +- outcome: Execute the constraint row where the entry is present but carries no target a machine can read. + +```gwt +Given the graph holds a {kind: "constraint"} spec {specId: "spec:probe.untargeted-constraint"} stating readiness {readiness: "defined"} +Given its only evidence is {evidence: "a constraints entry with no target"} +When the graph is validated +Then the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"} +Then the finding names the unmet floor clause {clauseId: "kind-evidence-complete"} +Then the report holds {errorCount: 1} errors +``` diff --git a/specs/validation/readiness-floor.blocking-open-question.sdp.md b/specs/validation/readiness-floor.blocking-open-question.sdp.md new file mode 100644 index 0000000..bba9190 --- /dev/null +++ b/specs/validation/readiness-floor.blocking-open-question.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:validation.readiness-floor.blocking-open-question +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.readiness-floor + verifies: spec:validation.readiness-floor +--- +# A blocking open question holds a spec below defined + +## Intent +- outcome: Execute the defined rung where a recorded open question is flagged as blocking. + +```gwt +Given the graph holds a spec {specId: "spec:probe.blocked-defined"} stating readiness {readiness: "defined"} +Given the spec {defect: "records a blocking open question"} +When the graph is validated +Then the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"} +Then the finding names the unmet floor clause {clauseId: "no-blocking-open-questions"} +Then the report holds {errorCount: 1} errors +``` diff --git a/specs/validation/readiness-floor.sdp.md b/specs/validation/readiness-floor.sdp.md index 9ce101f..592e28f 100644 --- a/specs/validation/readiness-floor.sdp.md +++ b/specs/validation/readiness-floor.sdp.md @@ -17,6 +17,23 @@ relations: ## Rule - A Spec may state a readiness only when every clause in that readiness floor passes. +- Floors are cumulative: a stated rung is checked against its own clauses and every lower rung's, so a Spec that clears a higher rung has cleared each one beneath it. +- The `idea` floor reads the envelope through five clauses: the Spec carries a stable id, a human-readable title, a stated kind, and a stated altitude, and it either states its intended outcome or declares a parent relation through `refines`. +- The `scoped` floor adds three clauses: the intended outcome is stated, at least one authored relation is declared, and the kind's natural evidence is present. +- The `defined` floor adds two clauses: the kind's natural evidence is complete, and no open question the Spec records is flagged as blocking. - The `ready` floor reads the Spec's own edges through three clauses: every authored relation resolves to a known target, every `refines` and `dependsOn` target itself stands at least `defined`, and every anchor bound to the Spec resolves. - The anchor clause reads the bindings that are present, so a Spec carrying no anchor clears it — the floor never demands a binding an author has not made. -- The floor table in `src/validate/readiness-floor.ts` is the clause set's code-level source of truth and the realizing entrypoint; the clauses of the lower rungs are stated there and are not re-enumerated here. +- Only relations the Spec itself declares count toward the relation clauses; membership of a Pack is derived from the manifest and never stands in for an authored relation. +- Every clause stated here is kind-blind. The two evidence clauses are the one kind-conditional place in the floor, and what counts as a kind's natural evidence is stated in full by the refining Spec that carries the per-kind evidence table. +- One clause table serves both readings: it checks the readiness an author states, and it yields derived readiness — the highest rung whose cumulative clauses all pass — which is read beside the stated rung and never overwrites it. +- The floor table in `src/validate/readiness-floor.ts` is the clause set's code-level source of truth and the realizing entrypoint. The clauses stated here and the rows of that table are one law read twice, so any disagreement between them is drift to resolve on one side, never a second floor. + +## Example space +```gwt-vocabulary +Given the graph holds a spec {specId:string} stating readiness {readiness:"scoped"|"defined"} +Given the spec {defect:"declares no relation"|"records a blocking open question"} +When the graph is validated +Then the report names {findingId:string} at severity {severity:"warning"|"error"} +Then the finding names the unmet floor clause {clauseId:string} +Then the report holds {errorCount:number} errors +``` diff --git a/specs/validation/readiness-floor.unrelated-scoped-spec.sdp.md b/specs/validation/readiness-floor.unrelated-scoped-spec.sdp.md new file mode 100644 index 0000000..51cedc1 --- /dev/null +++ b/specs/validation/readiness-floor.unrelated-scoped-spec.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:validation.readiness-floor.unrelated-scoped-spec +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.readiness-floor + verifies: spec:validation.readiness-floor +--- +# A scoped spec with no relation names the relation clause + +## Intent +- outcome: Execute the scoped rung where every clause but the relation clause is satisfied. + +```gwt +Given the graph holds a spec {specId: "spec:probe.unrelated-scoped"} stating readiness {readiness: "scoped"} +Given the spec {defect: "declares no relation"} +When the graph is validated +Then the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"} +Then the finding names the unmet floor clause {clauseId: "at-least-one-relation"} +Then the report holds {errorCount: 1} errors +``` diff --git a/test/self-hosting-graph.test.ts b/test/self-hosting-graph.test.ts index d95afd2..1b7d05e 100644 --- a/test/self-hosting-graph.test.ts +++ b/test/self-hosting-graph.test.ts @@ -90,12 +90,12 @@ describe("the self-hosting corpus", () => { // The literals are the corpus checkpoint. The authored arrays are measured against the same // literals rather than standing in for them, so a transcription slip in an oracle module // cannot certify itself by moving both sides of a comparison at once. - expect(result.counts).toEqual({ specs: 87, packs: 1, anchors: 65 }); - expect(expectedSpecs).toHaveLength(87); - expect(expectedPackMembers).toHaveLength(87); - expect(expectedAnchors).toHaveLength(65); - expect(result.graph.nodes).toHaveLength(153); - expect(result.graph.edges).toHaveLength(294); + expect(result.counts).toEqual({ specs: 93, packs: 1, anchors: 70 }); + expect(expectedSpecs).toHaveLength(93); + expect(expectedPackMembers).toHaveLength(93); + expect(expectedAnchors).toHaveLength(70); + expect(result.graph.nodes).toHaveLength(164); + expect(result.graph.edges).toHaveLength(317); }); it("rosters exactly the authored Spec, Pack, and anchor node ids", () => { @@ -145,7 +145,7 @@ describe("the self-hosting corpus", () => { }), {}, ), - ).toEqual({ defined: 36, ready: 51 }); + ).toEqual({ defined: 36, ready: 57 }); }); it("derives the Pack membership edges from the manifest, in manifest order", () => { diff --git a/test/self-hosting-oracle/anchors.ts b/test/self-hosting-oracle/anchors.ts index efbacfe..213063f 100644 --- a/test/self-hosting-oracle/anchors.ts +++ b/test/self-hosting-oracle/anchors.ts @@ -173,6 +173,56 @@ export const expectedAnchors = [ constant: "warnLevelGapTestAnchor", site: "bindExample(readyGapSignalContract", }, + { + id: "test:protocol.readiness-floor.unrelated-scoped-spec", + nodeType: "Anchor", + label: "the unrelated-scoped point verifies the relation clause of the scoped rung", + type: "verifies", + target: "spec:validation.readiness-floor.unrelated-scoped-spec", + file: "test/self-hosting-validators.test.ts", + constant: "unrelatedScopedSpecTestAnchor", + site: "bindExample(unrelatedScopedSpecContract", + }, + { + id: "test:protocol.readiness-floor.blocking-open-question", + nodeType: "Anchor", + label: "the blocked-question point verifies the open-questions clause of the defined rung", + type: "verifies", + target: "spec:validation.readiness-floor.blocking-open-question", + file: "test/self-hosting-validators.test.ts", + constant: "blockingOpenQuestionTestAnchor", + site: "bindExample(blockingOpenQuestionContract", + }, + { + id: "test:protocol.kind-evidence.constraints-alone", + nodeType: "Anchor", + label: "the constraints-alone point verifies the behavior-family complete cell", + type: "verifies", + target: "spec:validation.kind-evidence.constraints-alone", + file: "test/self-hosting-validators.test.ts", + constant: "constraintsAloneTestAnchor", + site: "bindExample(constraintsAloneContract", + }, + { + id: "test:protocol.kind-evidence.untargeted-constraint", + nodeType: "Anchor", + label: "the untargeted-constraint point verifies the constraint row's target requirement", + type: "verifies", + target: "spec:validation.kind-evidence.untargeted-constraint", + file: "test/self-hosting-validators.test.ts", + constant: "untargetedConstraintTestAnchor", + site: "bindExample(untargetedConstraintContract", + }, + { + id: "test:protocol.kind-evidence.empty-promoted-child", + nodeType: "Anchor", + label: "the empty-promotion point verifies the promoted-evidence honesty bound", + type: "verifies", + target: "spec:validation.kind-evidence.empty-promoted-child", + file: "test/self-hosting-validators.test.ts", + constant: "emptyPromotedChildTestAnchor", + site: "bindExample(emptyPromotedChildContract", + }, { id: "test:protocol.referential-integrity.dangling-target", nodeType: "Anchor", diff --git a/test/self-hosting-oracle/declared-relations.ts b/test/self-hosting-oracle/declared-relations.ts index f9cddde..0c916a2 100644 --- a/test/self-hosting-oracle/declared-relations.ts +++ b/test/self-hosting-oracle/declared-relations.ts @@ -92,6 +92,50 @@ export const expectedDeclaredRelations = [ ["spec:validation.readiness-floor", "dependsOn", "spec:model.protocol-domain"], ["spec:validation.readiness-floor", "decidedBy", "spec:decisions.kind-conditional-floor"], ["spec:validation.readiness-floor", "decidedBy", "spec:decisions.carried-evidence"], + [ + "spec:validation.readiness-floor.unrelated-scoped-spec", + "refines", + "spec:validation.readiness-floor", + ], + [ + "spec:validation.readiness-floor.unrelated-scoped-spec", + "verifies", + "spec:validation.readiness-floor", + ], + [ + "spec:validation.readiness-floor.blocking-open-question", + "refines", + "spec:validation.readiness-floor", + ], + [ + "spec:validation.readiness-floor.blocking-open-question", + "verifies", + "spec:validation.readiness-floor", + ], + ["spec:validation.kind-evidence", "refines", "spec:validation.readiness-floor"], + ["spec:validation.kind-evidence", "decidedBy", "spec:decisions.kind-conditional-floor"], + ["spec:validation.kind-evidence.constraints-alone", "refines", "spec:validation.kind-evidence"], + ["spec:validation.kind-evidence.constraints-alone", "verifies", "spec:validation.kind-evidence"], + [ + "spec:validation.kind-evidence.untargeted-constraint", + "refines", + "spec:validation.kind-evidence", + ], + [ + "spec:validation.kind-evidence.untargeted-constraint", + "verifies", + "spec:validation.kind-evidence", + ], + [ + "spec:validation.kind-evidence.empty-promoted-child", + "refines", + "spec:validation.kind-evidence", + ], + [ + "spec:validation.kind-evidence.empty-promoted-child", + "verifies", + "spec:validation.kind-evidence", + ], ["spec:validation.duplicate-ids", "refines", "spec:protocol.self-hosting"], ["spec:validation.duplicate-ids", "dependsOn", "spec:carrier.markdown-parser"], ["spec:validation.duplicate-ids.dual-carrier", "refines", "spec:validation.duplicate-ids"], diff --git a/test/self-hosting-oracle/pack-members.ts b/test/self-hosting-oracle/pack-members.ts index 4e2f921..964093c 100644 --- a/test/self-hosting-oracle/pack-members.ts +++ b/test/self-hosting-oracle/pack-members.ts @@ -68,6 +68,12 @@ export const expectedPackMembers = [ "spec:model.anchors.lookalike-refusal", "spec:model.anchors.physical-identity", "spec:validation.two-check-families.split-report", + "spec:validation.readiness-floor.unrelated-scoped-spec", + "spec:validation.readiness-floor.blocking-open-question", + "spec:validation.kind-evidence", + "spec:validation.kind-evidence.constraints-alone", + "spec:validation.kind-evidence.untargeted-constraint", + "spec:validation.kind-evidence.empty-promoted-child", "spec:decisions.plain-language-references", "spec:decisions.concept-docs-dissolve", "spec:decisions.one-validation-path", diff --git a/test/self-hosting-oracle/validation.ts b/test/self-hosting-oracle/validation.ts index a18078c..b2a12c5 100644 --- a/test/self-hosting-oracle/validation.ts +++ b/test/self-hosting-oracle/validation.ts @@ -16,14 +16,234 @@ export const validationSpecs = [ behavior: { rules: [ "A Spec may state a readiness only when every clause in that readiness floor passes.", + "Floors are cumulative: a stated rung is checked against its own clauses and every lower rung's, so a Spec that clears a higher rung has cleared each one beneath it.", + "The `idea` floor reads the envelope through five clauses: the Spec carries a stable id, a human-readable title, a stated kind, and a stated altitude, and it either states its intended outcome or declares a parent relation through `refines`.", + "The `scoped` floor adds three clauses: the intended outcome is stated, at least one authored relation is declared, and the kind's natural evidence is present.", + "The `defined` floor adds two clauses: the kind's natural evidence is complete, and no open question the Spec records is flagged as blocking.", "The `ready` floor reads the Spec's own edges through three clauses: every authored relation resolves to a known target, every `refines` and `dependsOn` target itself stands at least `defined`, and every anchor bound to the Spec resolves.", "The anchor clause reads the bindings that are present, so a Spec carrying no anchor clears it — the floor never demands a binding an author has not made.", - "The floor table in `src/validate/readiness-floor.ts` is the clause set's code-level source of truth and the realizing entrypoint; the clauses of the lower rungs are stated there and are not re-enumerated here.", + "Only relations the Spec itself declares count toward the relation clauses; membership of a Pack is derived from the manifest and never stands in for an authored relation.", + "Every clause stated here is kind-blind. The two evidence clauses are the one kind-conditional place in the floor, and what counts as a kind's natural evidence is stated in full by the refining Spec that carries the per-kind evidence table.", + "One clause table serves both readings: it checks the readiness an author states, and it yields derived readiness — the highest rung whose cumulative clauses all pass — which is read beside the stated rung and never overwrites it.", + "The floor table in `src/validate/readiness-floor.ts` is the clause set's code-level source of truth and the realizing entrypoint. The clauses stated here and the rows of that table are one law read twice, so any disagreement between them is drift to resolve on one side, never a second floor.", ], + exampleSpace: { + given: [ + 'the graph holds a spec {specId:string} stating readiness {readiness:"scoped"|"defined"}', + 'the spec {defect:"declares no relation"|"records a blocking open question"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId:string} at severity {severity:"warning"|"error"}', + "the finding names the unmet floor clause {clauseId:string}", + "the report holds {errorCount:number} errors", + ], + }, }, }, deliveryFacts: ["implemented", "has-verifier"], }, + { + id: "spec:validation.readiness-floor.unrelated-scoped-spec", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/readiness-floor.unrelated-scoped-spec.sdp.md", + title: "A scoped spec with no relation names the relation clause", + narrative: null, + sections: { + intent: { + outcome: "Execute the scoped rung where every clause but the relation clause is satisfied.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.unrelated-scoped"} stating readiness {readiness: "scoped"}', + 'the spec {defect: "declares no relation"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"}', + 'the finding names the unmet floor clause {clauseId: "at-least-one-relation"}', + "the report holds {errorCount: 1} errors", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.readiness-floor.blocking-open-question", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/readiness-floor.blocking-open-question.sdp.md", + title: "A blocking open question holds a spec below defined", + narrative: null, + sections: { + intent: { + outcome: "Execute the defined rung where a recorded open question is flagged as blocking.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.blocked-defined"} stating readiness {readiness: "defined"}', + 'the spec {defect: "records a blocking open question"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"}', + 'the finding names the unmet floor clause {clauseId: "no-blocking-open-questions"}', + "the report holds {errorCount: 1} errors", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.kind-evidence", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/validation/kind-evidence.sdp.md", + title: "Each kind carries its own evidence", + narrative: null, + sections: { + intent: { + outcome: + "State what a Spec of each kind must show before its readiness floor accepts the evidence clauses.", + }, + behavior: { + rules: [ + "Each kind names its natural evidence: the `scoped` rung requires that evidence present, and the `defined` rung requires it complete wherever the kind defines a stronger form. This table is the whole kind-aware story — there is no second overlay mechanism.", + "`behavior`, `workflow`, and `contract` share one row. Evidence is present with rules, examples, flows, or constraints — inline, or promoted onto a refining child or a `constrainedBy` constraint. Evidence is complete with rules and/or examples, inline or promoted; constraints alone no longer suffice.", + "A `rule` converges across the two rungs: its statement is its evidence, because a rule's content is its statement.", + "An `example` shows evidence present with an examples entry, prose acceptable. It shows evidence complete with at least one structured given/when/then entry whose every used step is fully bound and belongs compatibly to any example space its parent owns — the concreteness law.", + "A `constraint` shows evidence present with a non-empty constraints section, and complete when every entry carries a machine-readable target.", + "A `model` converges on non-empty terms: a vocabulary either has terms or it does not.", + "A `decision` shows evidence present once its decision section is there — context and alternatives may precede the choice — and complete once the chosen option is written.", + "The `contract` row stands on the behavior row as a named deferral: when a dedicated contract section lands, the typing law pulls it in and this row repoints to it.", + "Promoted evidence carries an honesty bound. A promoted child counts only when it is a `rule` or `example` Spec that itself clears its own kind's present cell, and a `constrainedBy` edge counts only when it resolves to a `constraint` Spec carrying its constraints — promotion moves content out, so an empty stub child is not a promotion and confers nothing.", + "The rows are monotonic, promotion-neutral, and converge honestly where a kind has no stronger form; those three bounds belong to the decision this Spec is shaped by and to the carried-evidence decision, and are not restated as law here.", + "The evidence table in `src/validate/readiness-floor.ts` is the row set's code-level source of truth and the realizing entrypoint; the rows stated here and that table are one law read twice, so any disagreement between them is drift to resolve on one side.", + ], + exampleSpace: { + given: [ + 'the graph holds a {kind:"behavior"|"constraint"} spec {specId:string} stating readiness {readiness:"scoped"|"defined"}', + 'its only evidence is {evidence:"a constraints entry carrying a target"|"a constraints entry with no target"|"an empty promoted rule child"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId:string} at severity {severity:"warning"|"error"}', + "the finding names the unmet floor clause {clauseId:string}", + "the report holds {errorCount:number} errors", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.kind-evidence.constraints-alone", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/kind-evidence.constraints-alone.sdp.md", + title: "Constraints alone stop short of complete behavior evidence", + narrative: null, + sections: { + intent: { + outcome: + "Execute the behavior-family row where the only evidence is the form that clears present but not complete.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a {kind: "behavior"} spec {specId: "spec:probe.constraints-alone"} stating readiness {readiness: "defined"}', + 'its only evidence is {evidence: "a constraints entry carrying a target"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"}', + 'the finding names the unmet floor clause {clauseId: "kind-evidence-complete"}', + "the report holds {errorCount: 1} errors", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.kind-evidence.untargeted-constraint", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/kind-evidence.untargeted-constraint.sdp.md", + title: "A constraint without a machine-readable target is not complete", + narrative: null, + sections: { + intent: { + outcome: + "Execute the constraint row where the entry is present but carries no target a machine can read.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a {kind: "constraint"} spec {specId: "spec:probe.untargeted-constraint"} stating readiness {readiness: "defined"}', + 'its only evidence is {evidence: "a constraints entry with no target"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"}', + 'the finding names the unmet floor clause {clauseId: "kind-evidence-complete"}', + "the report holds {errorCount: 1} errors", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.kind-evidence.empty-promoted-child", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/kind-evidence.empty-promoted-child.sdp.md", + title: "An empty promoted child confers no evidence", + narrative: null, + sections: { + intent: { + outcome: + "Execute the promoted-evidence bound where a refining child carries none of its own kind's evidence.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a {kind: "behavior"} spec {specId: "spec:probe.empty-promotion"} stating readiness {readiness: "scoped"}', + 'its only evidence is {evidence: "an empty promoted rule child"}', + ], + when: ["the graph is validated"], + [["t", "hen"].join("")]: [ + 'the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"}', + 'the finding names the unmet floor clause {clauseId: "kind-evidence-present"}', + "the report holds {errorCount: 1} errors", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, { id: "spec:validation.duplicate-ids", specKind: "behavior", diff --git a/test/self-hosting-validators.test.ts b/test/self-hosting-validators.test.ts index 17208fb..8dccdb2 100644 --- a/test/self-hosting-validators.test.ts +++ b/test/self-hosting-validators.test.ts @@ -9,6 +9,11 @@ import { sectionAuthoredFactContract } from "../generated/contracts/validation.a import { unearnedStatedFactContract } from "../generated/contracts/validation.authored-honesty.unearned-stated-fact.contract.js"; import { danglingTargetContract } from "../generated/contracts/validation.referential-integrity.dangling-target.contract.js"; import { didYouMeanContract } from "../generated/contracts/validation.referential-integrity.did-you-mean.contract.js"; +import { blockingOpenQuestionContract } from "../generated/contracts/validation.readiness-floor.blocking-open-question.contract.js"; +import { unrelatedScopedSpecContract } from "../generated/contracts/validation.readiness-floor.unrelated-scoped-spec.contract.js"; +import { constraintsAloneContract } from "../generated/contracts/validation.kind-evidence.constraints-alone.contract.js"; +import { emptyPromotedChildContract } from "../generated/contracts/validation.kind-evidence.empty-promoted-child.contract.js"; +import { untargetedConstraintContract } from "../generated/contracts/validation.kind-evidence.untargeted-constraint.contract.js"; import { collapsedEdgeClaimContract } from "../generated/contracts/validation.claim-separation.collapsed-edge-claim.contract.js"; import { unratifiedDescriptorContract } from "../generated/contracts/validation.claim-separation.unratified-descriptor.contract.js"; import { incoherentAggregateContract } from "../generated/contracts/validation.pack-coherence.incoherent-aggregate.contract.js"; @@ -98,6 +103,220 @@ function namesFinding( expect(findings.every((finding) => finding.severity === params.severity)).toBe(true); } +/** + * A decision endpoint terminates the relation chain honestly: only the `ready` rung reads + * dependsOn and refines targets, so a decidedBy edge satisfies the relation clause without adding + * a second thing that could refuse below `ready`. + */ +function declareDecisionRelation(world: ValidatorWorld): void { + const decisionId = `${world.subjectId}-decider`; + + world.nodes.push(probeSpec(decisionId, { kind: "decision" })); + world.edges.push({ + from: world.subjectId, + type: "decidedBy", + to: decisionId, + claim: "declared", + }); +} + +/** Replaces the subject probe in place, so a step may enrich the node an earlier step pushed. */ +function reviseSubject( + world: ValidatorWorld, + revise: (node: PrimitiveNode) => PrimitiveNode, + failure: string, +): void { + const index = world.nodes.findIndex((entry) => entry.id === world.subjectId); + const node = world.nodes[index]; + + if (node?.nodeType !== "Primitive") { + throw new Error(failure); + } + + world.nodes[index] = revise(node); +} + +/** The shared Then step of the floor family: exactly one clause refuses, and it is the named one. */ +function namesUnmetClause(world: ValidatorWorld, params: { readonly clauseId: string }): void { + const findings = findingsOf(world, "honesty/readiness-floor"); + + expect(findings.map((finding) => finding.relatedId)).toEqual([params.clauseId]); + expect(findings.map((finding) => finding.subjectId)).toEqual([world.subjectId]); + expect(findings.map((finding) => finding.path)).toEqual(["readiness"]); +} + +function holdsErrorCount(world: ValidatorWorld, params: { readonly errorCount: number }): void { + expect(reportOf(world).findings.filter((finding) => finding.severity === "error")).toHaveLength( + params.errorCount, + ); +} + +/* ----- spec:validation.readiness-floor ----- */ + +const readinessFloorBindings = { + "the graph holds a spec {specId} stating readiness {readiness}": ( + world: ValidatorWorld, + params: { readonly specId: string; readonly readiness: "scoped" | "defined" }, + ) => { + world.subjectId = params.specId; + world.nodes.push(probeSpec(params.specId, { readiness: params.readiness })); + }, + "the spec {defect}": ( + world: ValidatorWorld, + params: { readonly defect: "declares no relation" | "records a blocking open question" }, + ) => { + if (params.defect === "declares no relation") { + return; + } + + // Every other clause of the stated rung still passes: the blocked probe declares its relation + // and keeps its kind evidence, so the open-questions clause is the only one that can refuse. + reviseSubject( + world, + (node) => ({ + ...node, + sections: { + ...node.sections, + intent: { + ...node.sections?.intent, + openQuestions: [ + { question: "Which rung does the probe honestly stand at?", blocking: true }, + ], + }, + }, + }), + "The spec step must run before its open question is recorded.", + ); + declareDecisionRelation(world); + }, + "the graph is validated": validate, + "the report names {findingId} at severity {severity}": namesFinding, + "the finding names the unmet floor clause {clauseId}": namesUnmetClause, + "the report holds {errorCount} errors": holdsErrorCount, +}; + +const unrelatedScopedSpecTestAnchor = specTest({ + id: testAnchorId("test:protocol.readiness-floor.unrelated-scoped-spec"), + label: "the unrelated-scoped point verifies the relation clause of the scoped rung", + verifies: ref("spec:validation.readiness-floor.unrelated-scoped-spec"), +}); +void unrelatedScopedSpecTestAnchor; + +bindExample(unrelatedScopedSpecContract, validatorWorld, readinessFloorBindings); + +const blockingOpenQuestionTestAnchor = specTest({ + id: testAnchorId("test:protocol.readiness-floor.blocking-open-question"), + label: "the blocked-question point verifies the open-questions clause of the defined rung", + verifies: ref("spec:validation.readiness-floor.blocking-open-question"), +}); +void blockingOpenQuestionTestAnchor; + +bindExample(blockingOpenQuestionContract, validatorWorld, readinessFloorBindings); + +/* ----- spec:validation.kind-evidence ----- */ + +/** A probe carrying no kind evidence at all, so the evidence step alone decides what it shows. */ +function evidencelessProbe(id: string, kind: SpecKind, readiness: SpecReadiness): PrimitiveNode { + return { + id, + nodeType: "Primitive", + claim: "declared", + specKind: kind, + altitude: "feature", + readiness, + title: `Probe for ${id}`, + file: "specs/probe.sdp.md", + sections: { intent: { outcome: `Probe the per-kind evidence table at "${id}".` } }, + }; +} + +const kindEvidenceBindings = { + "the graph holds a {kind} spec {specId} stating readiness {readiness}": ( + world: ValidatorWorld, + params: { + readonly kind: "behavior" | "constraint"; + readonly specId: string; + readonly readiness: "scoped" | "defined"; + }, + ) => { + world.subjectId = params.specId; + world.nodes.push(evidencelessProbe(params.specId, params.kind, params.readiness)); + // The relation clause is not the law under test, so the probe always declares one. + declareDecisionRelation(world); + }, + "its only evidence is {evidence}": ( + world: ValidatorWorld, + params: { + readonly evidence: + | "a constraints entry carrying a target" + | "a constraints entry with no target" + | "an empty promoted rule child"; + }, + ) => { + if (params.evidence === "an empty promoted rule child") { + // Promotion moves content out, so a stub child carrying none of its own kind's evidence is + // not a promotion: it resolves, it refines, and it still confers nothing. + const childId = `${world.subjectId}.promoted-stub`; + + world.nodes.push(evidencelessProbe(childId, "rule", "idea")); + world.edges.push({ + from: childId, + type: "refines", + to: world.subjectId, + claim: "declared", + }); + return; + } + + reviseSubject( + world, + (node) => ({ + ...node, + sections: { + ...node.sections, + constraints: [ + params.evidence === "a constraints entry carrying a target" + ? { statement: "The probe answers within its budget.", target: "p95 < 200ms" } + : { statement: "The probe answers quickly enough." }, + ], + }, + }), + "The spec step must run before its evidence is placed.", + ); + }, + "the graph is validated": validate, + "the report names {findingId} at severity {severity}": namesFinding, + "the finding names the unmet floor clause {clauseId}": namesUnmetClause, + "the report holds {errorCount} errors": holdsErrorCount, +}; + +const constraintsAloneTestAnchor = specTest({ + id: testAnchorId("test:protocol.kind-evidence.constraints-alone"), + label: "the constraints-alone point verifies the behavior-family complete cell", + verifies: ref("spec:validation.kind-evidence.constraints-alone"), +}); +void constraintsAloneTestAnchor; + +bindExample(constraintsAloneContract, validatorWorld, kindEvidenceBindings); + +const untargetedConstraintTestAnchor = specTest({ + id: testAnchorId("test:protocol.kind-evidence.untargeted-constraint"), + label: "the untargeted-constraint point verifies the constraint row's target requirement", + verifies: ref("spec:validation.kind-evidence.untargeted-constraint"), +}); +void untargetedConstraintTestAnchor; + +bindExample(untargetedConstraintContract, validatorWorld, kindEvidenceBindings); + +const emptyPromotedChildTestAnchor = specTest({ + id: testAnchorId("test:protocol.kind-evidence.empty-promoted-child"), + label: "the empty-promotion point verifies the promoted-evidence honesty bound", + verifies: ref("spec:validation.kind-evidence.empty-promoted-child"), +}); +void emptyPromotedChildTestAnchor; + +bindExample(emptyPromotedChildContract, validatorWorld, kindEvidenceBindings); + /* ----- spec:validation.warn-level-signals ----- */ const warnLevelSignalBindings = { From 6005518d5de9164a165a1d5b0b73a21078b83b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 19:31:43 +0200 Subject: [PATCH 05/16] docs(plans): record the S2 close in the phase-4 ledgers The two S2 conversion rows go done with their point counts, the readiness ledger carries the six promotions and the one refusal, the session row states what landed, and the shared-constant watch item records that it fired nothing. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- plans/21-self-hosting-phase-4.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/plans/21-self-hosting-phase-4.md b/plans/21-self-hosting-phase-4.md index 4d29bea..b8e3c4a 100644 --- a/plans/21-self-hosting-phase-4.md +++ b/plans/21-self-hosting-phase-4.md @@ -222,7 +222,7 @@ recorded (ruling 12). Promotions ride verifiers per ruling 5. | table sugar (ruling 4) | sibling authoring proves dishonest or unusable in a wave | unfired | | single-literal vocabulary form | real material forces it | unfired | | per-family oracle drift | a split family module regains cross-family assertions or a mega-assert | unfired | -| shared-constant bypass | a new contract-dependent suite lands outside the constant | unfired | +| shared-constant bypass | a new contract-dependent suite lands outside the constant | unfired — the constant landed at S2 with no surprise; the eslint side derives from the root row only, because the example tree's suite sits outside the typed-lint globs the exemption relaxes. S3's new suite is the first real test of the coupling | | separate example-id namespace | a collision or real pressure appears | unfired (watch continues from phase 3) | ## §4 Docket ledger (carried in from plan 20) @@ -270,8 +270,8 @@ reasons)* | Wave | Law | Carrier Spec(s) | Planned points | State | |---|---|---|---|---| -| S2 | lower floor rungs (`idea`/`scoped`/`defined` clauses) | `spec:validation.readiness-floor` (enriched) | 2–3 | planned | -| S2 | per-kind evidence table + MD-16 promoted-evidence bound | new `spec:validation.kind-evidence` | 1–2 | planned | +| S2 | lower floor rungs (`idea`/`scoped`/`defined` clauses) | `spec:validation.readiness-floor` (enriched) | 2–3 | done — 2 points (`at-least-one-relation` on a scoped probe · `no-blocking-open-questions` on a defined probe), both mutation-probed red | +| S2 | per-kind evidence table + MD-16 promoted-evidence bound | new `spec:validation.kind-evidence` | 1–2 | done — 3 points (behavior-family complete cell · constraint target · the promoted-evidence bound); one over the planned ceiling, taken deliberately so the MD-16 bound the Spec states is not the only row left unbound | | S3 | derived-readiness banner (one direction · first unmet clause) | new Spec under `specs/consumers/` | 1–2 | planned | | S3 | `implemented` view-label (binding language) | same family | 1 | planned | | S3 | wholesale page rewrite (atomic swap · no stale page) | new Spec | 1 | planned | @@ -283,6 +283,23 @@ reasons)* *(maintained at S2/S3 promotions and the S4 sweep; opening distribution `ready: 51 / defined: 36` over 87)* +**S2 — the floor wave.** Six Specs enter the corpus, every one of them stated `ready` at +authoring because the floor clears and a resolving verifier lands in the same change: the new +rule Spec `spec:validation.kind-evidence`, its three `example` children +(`constraints-alone`, `untargeted-constraint`, `empty-promoted-child`), and the two `example` +children of `spec:validation.readiness-floor` (`unrelated-scoped-spec`, +`blocking-open-question`). `spec:validation.readiness-floor` was already `ready` and stays +`ready` and floor-clean after enrichment — its rule set grew, its descriptors did not move. + +Refusals on the record: `spec:decisions.kind-conditional-floor` stays `defined` — the phase-3 +decision precedent holds, a Decision Record's own maturity is not moved by a Spec citing it. +`spec:validation.kind-evidence` carries `has-verifier` but not `implemented`: no code anchor was +added for it, because the one anchor on the floor's evaluator already binds the realizing +entrypoint and a second anchor on the same file would be decorative. + +Closing distribution: **`ready: 57 / defined: 36` over 93** (87 → 93 Specs · 65 → 70 anchors · +153 → 164 nodes · 294 → 317 edges), zero errors and zero warnings over the regenerated graph. + ## §9 Session and gate ledger Sessions execute sequentially; each closes with a green twelve-leg gate, a regenerated Design @@ -292,7 +309,7 @@ ledger is git process evidence, never graph content. | Session | Delivers | Gate discipline | State | |---|---|---|---| | S1 | the oracle split (§2 S1) | orchestrator-verified green gate | done — 21 `it()`s over one hoisted extraction; the frozen expectation moved to ten authored modules under `test/self-hosting-oracle/` (seven family files, pack manifest, declared relations, anchors) plus their aggregating index; zero assertion loss (every one of the 27 original `expect` sites survives, 5 added: three oracle-length cross-checks and the two-assertion "no Spec outside the families" law), the node-id roster derived from the authored arrays per ruling 10; counts unchanged at 87/1/65 · 153 · 294 · ready 51 / defined 36 | -| S2 | shared constant + floor wave | orchestrator-verified green gate | planned | +| S2 | shared constant + floor wave | orchestrator-verified green gate | done — `contract-dependent-suites.mjs` now states the per-tree rows once and both `vitest-test.mjs` and `eslint.config.js` read it (clean-room proof: lint passes with `generated/contracts` moved aside, the wrapper still fails fast with the same recovery text); the floor wave carried the `idea`/`scoped`/`defined` rungs into `spec:validation.readiness-floor` and the per-kind table into the new `spec:validation.kind-evidence`, with 5 bound points each mutation-probed red for the clause it names; corpus 87 → 93 Specs, `ready` 51 → 57 | | S3 | view wave + seventh bound suite | orchestrator-verified green gate | planned | | S4 | readiness sweep + re-audits (± the `05` deletion) | orchestrator-verified green gate over the regenerated Design Review | planned | | S5 | adversarial review, remediation, full close, done-record | full chain + clean-clone; review archived | planned | From 9855053ef72930cf13849169955f0ceb5aa9aad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 19:51:42 +0200 Subject: [PATCH 06/16] feat(specs,tests): carry the view wave laws and bind them in a projections suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five implemented-but-uncarried laws now have carrying Specs, four of them with bound executable points in a new projections suite: - the derived-readiness banner (`spec:consumers.derived-readiness-banner`): stated renders beside floor-reached always; the banner fires only in the dishonest direction and names the first unmet clause by id and description. - the view-label rule (`spec:consumers.binding-language-views`): views speak binding language on the page and in the aggregate tables; the internal delivery-fact name never renders as label text. - the wholesale rewrite (`spec:consumers.wholesale-view-rewrite`): temp sibling, removal, one rename; a run that cannot produce a current view removes the stale one; `--check-clean` renders twice and refuses divergence. - the one diagnostic rendering rule (`spec:validation.diagnostic-rendering`): one currency, location composed from the structured file/line fields, both degradations stated. - validator self-testing (`spec:validation.validator-self-testing`), stated at `defined`: its only mechanical verifier would police the delivery process rather than conformance or honesty, so none is authored. `test/self-hosting-projections.test.ts` holds five bound points over two world styles — probe graphs rendered through the real projection seam, and a temporary extraction root for the filesystem law — and enters the shared contract-dependent-suites row once, from which both the wrapper preflight and the clean-room lint exemption follow. Corpus 93 → 103 Specs · 70 → 75 anchors · 164 → 179 nodes · 317 → 351 edges; `ready` 57 → 66, `defined` 36 → 37; zero errors and zero warnings. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- contract-dependent-suites.mjs | 1 + ...ding-language-views.bound-spec-page.sdp.md | 23 + specs/consumers/binding-language-views.sdp.md | 33 ++ ...adiness-banner.dishonest-divergence.sdp.md | 22 + ...ed-readiness-banner.honest-headroom.sdp.md | 21 + .../consumers/derived-readiness-banner.sdp.md | 32 ++ specs/consumers/wholesale-view-rewrite.sdp.md | 32 ++ ...ale-view-rewrite.stale-page-removed.sdp.md | 22 + specs/self-hosting.pack.sdp.ts | 10 + ...gnostic-rendering.composed-location.sdp.md | 21 + specs/validation/diagnostic-rendering.sdp.md | 30 + .../validation/validator-self-testing.sdp.md | 19 + test/self-hosting-graph.test.ts | 14 +- test/self-hosting-oracle/anchors.ts | 50 ++ test/self-hosting-oracle/consumers.ts | 246 +++++++++ .../self-hosting-oracle/declared-relations.ts | 59 ++ test/self-hosting-oracle/pack-members.ts | 10 + test/self-hosting-oracle/validation.ts | 92 ++++ test/self-hosting-projections.test.ts | 516 ++++++++++++++++++ 19 files changed, 1246 insertions(+), 7 deletions(-) create mode 100644 specs/consumers/binding-language-views.bound-spec-page.sdp.md create mode 100644 specs/consumers/binding-language-views.sdp.md create mode 100644 specs/consumers/derived-readiness-banner.dishonest-divergence.sdp.md create mode 100644 specs/consumers/derived-readiness-banner.honest-headroom.sdp.md create mode 100644 specs/consumers/derived-readiness-banner.sdp.md create mode 100644 specs/consumers/wholesale-view-rewrite.sdp.md create mode 100644 specs/consumers/wholesale-view-rewrite.stale-page-removed.sdp.md create mode 100644 specs/validation/diagnostic-rendering.composed-location.sdp.md create mode 100644 specs/validation/diagnostic-rendering.sdp.md create mode 100644 specs/validation/validator-self-testing.sdp.md create mode 100644 test/self-hosting-projections.test.ts diff --git a/contract-dependent-suites.mjs b/contract-dependent-suites.mjs index 64d7871..568d02b 100644 --- a/contract-dependent-suites.mjs +++ b/contract-dependent-suites.mjs @@ -23,6 +23,7 @@ export const contractDependentSuites = [ "test/self-hosting-duplicate-ids.test.ts", "test/self-hosting-extraction.test.ts", "test/self-hosting-model.test.ts", + "test/self-hosting-projections.test.ts", "test/self-hosting-sdp-import.test.ts", "test/self-hosting-validators.test.ts", ], diff --git a/specs/consumers/binding-language-views.bound-spec-page.sdp.md b/specs/consumers/binding-language-views.bound-spec-page.sdp.md new file mode 100644 index 0000000..b5a1316 --- /dev/null +++ b/specs/consumers/binding-language-views.bound-spec-page.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.binding-language-views.bound-spec-page +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.binding-language-views + verifies: spec:consumers.binding-language-views +--- +# A fully bound spec renders binding language on the page and in the index + +## Intent +- outcome: Execute the rendered vocabulary on a spec both anchors reach, where the internal fact name would be easiest to leak. + +```gwt +Given the graph holds a spec {specId: "spec:probe.bound-surface"} bound by {bindings: "an implementing code anchor and a verifying test anchor"} +When the Design Review renders the graph +Then the spec page renders the implementation binding as {implementation: "present"} +Then the spec page renders the verifier binding as {verifier: "present"} +Then the spec page renders the runtime observation as {observation: "not tracked"} +Then the index table repeats those binding values for the spec: {tableRepeats: true} +Then the internal delivery-fact name {factName: "implemented"} appears as rendered label text: {factNameRendered: false} +``` diff --git a/specs/consumers/binding-language-views.sdp.md b/specs/consumers/binding-language-views.sdp.md new file mode 100644 index 0000000..0498053 --- /dev/null +++ b/specs/consumers/binding-language-views.sdp.md @@ -0,0 +1,33 @@ +--- +id: spec:consumers.binding-language-views +kind: rule +altitude: feature +readiness: ready +relations: + refines: spec:consumers.design-review + decidedBy: spec:decisions.binding-not-liveness +--- +# Views speak binding language, never the internal fact name + +## Intent +- outcome: Keep a reader from reading a delivery fact as a liveness claim the graph never made. + +## Rule +- The delivery-fact names stay internal. They are the graph's own vocabulary and the drift queries read them; no rendered surface shows one as user-facing label text. +- A spec page's bindings block renders four labelled lines — implementation binding, verifier binding, expected-outcome oracle, and runtime observation. +- The three binding lines read present or none, and nothing else: what a binding says is that a resolving anchor exists, so the reader is offered existence rather than a degree. +- Runtime observation always reads not tracked. No delivery fact records it, and the view states the absence instead of leaving a reader to infer it from a missing line. +- The pack member table and the index table carry the same two binding columns, with the same present and none values, so the aggregate surfaces speak the page's language rather than a shorthand of their own. +- The model half of this rule — that a binding states existence and never liveness — belongs to the decision this Spec is shaped by; what is stated here is only what the views render. +- The realizing entrypoints are `renderBindings` in `src/projections/design-review-context.ts` and the member and index tables in `src/projections/design-review-pages.ts`. + +## Example space +```gwt-vocabulary +Given the graph holds a spec {specId:string} bound by {bindings:"an implementing code anchor and a verifying test anchor"|"no anchor at all"} +When the Design Review renders the graph +Then the spec page renders the implementation binding as {implementation:"present"|"none"} +Then the spec page renders the verifier binding as {verifier:"present"|"none"} +Then the spec page renders the runtime observation as {observation:string} +Then the index table repeats those binding values for the spec: {tableRepeats:boolean} +Then the internal delivery-fact name {factName:string} appears as rendered label text: {factNameRendered:boolean} +``` diff --git a/specs/consumers/derived-readiness-banner.dishonest-divergence.sdp.md b/specs/consumers/derived-readiness-banner.dishonest-divergence.sdp.md new file mode 100644 index 0000000..21fde6f --- /dev/null +++ b/specs/consumers/derived-readiness-banner.dishonest-divergence.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:consumers.derived-readiness-banner.dishonest-divergence +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.derived-readiness-banner + verifies: spec:consumers.derived-readiness-banner +--- +# An overstated rung raises the banner and names the clause that refused + +## Intent +- outcome: Execute the dishonest direction, where the page must name the first clause the structure leaves unmet. + +```gwt +Given the graph holds a rule spec {specId: "spec:probe.overstated-rung"} whose stated readiness is {statedReadiness: "ready"} +Given the spec {structure: "records a blocking open question"} +When the Design Review renders the graph +Then the spec page renders the floor reached {floorReached: "scoped"} +Then the divergence banner is raised: {bannerRaised: true} +Then the banner names the first unmet clause {clauseId: "no-blocking-open-questions"} +``` diff --git a/specs/consumers/derived-readiness-banner.honest-headroom.sdp.md b/specs/consumers/derived-readiness-banner.honest-headroom.sdp.md new file mode 100644 index 0000000..01b0bad --- /dev/null +++ b/specs/consumers/derived-readiness-banner.honest-headroom.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:consumers.derived-readiness-banner.honest-headroom +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.derived-readiness-banner + verifies: spec:consumers.derived-readiness-banner +--- +# A rung the structure overshoots renders as information, not as a banner + +## Intent +- outcome: Execute the honest direction, where the line still renders both rungs and nothing nags the author upward. + +```gwt +Given the graph holds a rule spec {specId: "spec:probe.understated-rung"} whose stated readiness is {statedReadiness: "scoped"} +Given the spec {structure: "clears every floor clause"} +When the Design Review renders the graph +Then the spec page renders the floor reached {floorReached: "ready"} +Then the divergence banner is raised: {bannerRaised: false} +``` diff --git a/specs/consumers/derived-readiness-banner.sdp.md b/specs/consumers/derived-readiness-banner.sdp.md new file mode 100644 index 0000000..4ebb95f --- /dev/null +++ b/specs/consumers/derived-readiness-banner.sdp.md @@ -0,0 +1,32 @@ +--- +id: spec:consumers.derived-readiness-banner +kind: rule +altitude: feature +readiness: ready +relations: + refines: spec:consumers.design-review + dependsOn: spec:validation.readiness-floor +--- +# Derived readiness renders beside the stated rung and warns in one direction + +## Intent +- outcome: Show a reader where a Spec's stated maturity stands against the structure it earns, without turning a floor into a quota. + +## Rule +- Derived readiness is the highest rung whose cumulative floor clauses pass. It is computed from the graph, rendered beside the author's statement, and never overwrites it. +- Every spec page renders the stated rung beside the floor reached, on one line, whether or not the two agree; the index and the pack member table carry the same pair as two columns. +- The divergence banner is raised only in the dishonest direction — the floor reached standing below the stated rung. A floor reached at or above the stated rung raises nothing, because a floor is a floor and never a quota that nags upward. +- A raised banner names the first unmet clause by its clause id and its description, so the reader is told which clause to satisfy rather than only that something is wrong. +- When even the `idea` floor is unmet, the floor reached renders as none rather than as a rung, and a raised banner states that the floor stands below `idea`. +- The banner is rendering, never a check: the same divergence is already the readiness floor's own finding, and the page shows it in context rather than gating on it. +- The realizing entrypoint is `renderReadiness` in `src/projections/design-review-context.ts`; the rung it renders is the derived readiness the one clause table yields. + +## Example space +```gwt-vocabulary +Given the graph holds a rule spec {specId:string} whose stated readiness is {statedReadiness:"scoped"|"ready"} +Given the spec {structure:"clears every floor clause"|"records a blocking open question"} +When the Design Review renders the graph +Then the spec page renders the floor reached {floorReached:"scoped"|"ready"} +Then the divergence banner is raised: {bannerRaised:boolean} +Then the banner names the first unmet clause {clauseId:string} +``` diff --git a/specs/consumers/wholesale-view-rewrite.sdp.md b/specs/consumers/wholesale-view-rewrite.sdp.md new file mode 100644 index 0000000..68d83f8 --- /dev/null +++ b/specs/consumers/wholesale-view-rewrite.sdp.md @@ -0,0 +1,32 @@ +--- +id: spec:consumers.wholesale-view-rewrite +kind: rule +altitude: feature +readiness: ready +relations: + refines: spec:consumers.design-review + dependsOn: spec:extraction.determinism +--- +# Every view run rewrites the view wholesale + +## Intent +- outcome: Guarantee that whatever a reader finds in the view directory was produced by the last run over the current source. + +## Rule +- A view run rewrites the view wholesale: no page written by an earlier run survives a later one, so a spec that left the corpus leaves no page behind. +- Pages are written to a temporary sibling of the view directory, the previous directory is removed, and the temporary is renamed into place — one rename, so no half-written view is ever readable and no temporary survives a completed run. +- A run that cannot produce a current view removes the stale one instead of leaving it readable as current: an absent view is honest, a stale view is not. +- The invalidation happens before rendering as well as after it: the build the run passes through removes any existing view up front, so a run that fails before rendering leaves nothing behind either. +- Under `--check-clean` the view is rendered twice from the same graph and the run refuses when the two renders diverge, removing the view it could not certify. +- Findings never withhold the view. A run whose checks report findings still writes the current view and returns the checks' own exit code, because the view is where those findings are read in context. +- The realizing entrypoint is `runView` in `src/cli/validate-view-command.ts`, with the up-front invalidation in `runBuild` in `src/cli/build-command.ts`. + +## Example space +```gwt-vocabulary +Given an extraction root holding {corpus:string} and a stale view page {stalePage:string} +When the view is rendered at that root +Then the run exits {exitCode:number} +Then the view holds the current page {currentPage:string} +Then the stale page survives: {staleSurvives:boolean} +Then a temporary view sibling survives: {temporarySurvives:boolean} +``` diff --git a/specs/consumers/wholesale-view-rewrite.stale-page-removed.sdp.md b/specs/consumers/wholesale-view-rewrite.stale-page-removed.sdp.md new file mode 100644 index 0000000..6772283 --- /dev/null +++ b/specs/consumers/wholesale-view-rewrite.stale-page-removed.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:consumers.wholesale-view-rewrite.stale-page-removed +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.wholesale-view-rewrite + verifies: spec:consumers.wholesale-view-rewrite +--- +# A page from an earlier run does not survive the next one + +## Intent +- outcome: Execute the wholesale rewrite against the case it exists for — a page whose subject the current source no longer holds. + +```gwt +Given an extraction root holding {corpus: "one authored spec"} and a stale view page {stalePage: "spec/probe.departed.md"} +When the view is rendered at that root +Then the run exits {exitCode: 0} +Then the view holds the current page {currentPage: "index.md"} +Then the stale page survives: {staleSurvives: false} +Then a temporary view sibling survives: {temporarySurvives: false} +``` diff --git a/specs/self-hosting.pack.sdp.ts b/specs/self-hosting.pack.sdp.ts index ab5ee4d..8d2a74e 100644 --- a/specs/self-hosting.pack.sdp.ts +++ b/specs/self-hosting.pack.sdp.ts @@ -77,6 +77,16 @@ export const selfHostingV1Pack = pack({ ref("spec:validation.kind-evidence.constraints-alone"), ref("spec:validation.kind-evidence.untargeted-constraint"), ref("spec:validation.kind-evidence.empty-promoted-child"), + ref("spec:validation.diagnostic-rendering"), + ref("spec:validation.diagnostic-rendering.composed-location"), + ref("spec:validation.validator-self-testing"), + ref("spec:consumers.derived-readiness-banner"), + ref("spec:consumers.derived-readiness-banner.dishonest-divergence"), + ref("spec:consumers.derived-readiness-banner.honest-headroom"), + ref("spec:consumers.binding-language-views"), + ref("spec:consumers.binding-language-views.bound-spec-page"), + ref("spec:consumers.wholesale-view-rewrite"), + ref("spec:consumers.wholesale-view-rewrite.stale-page-removed"), ref("spec:decisions.plain-language-references"), ref("spec:decisions.concept-docs-dissolve"), ref("spec:decisions.one-validation-path"), diff --git a/specs/validation/diagnostic-rendering.composed-location.sdp.md b/specs/validation/diagnostic-rendering.composed-location.sdp.md new file mode 100644 index 0000000..328638c --- /dev/null +++ b/specs/validation/diagnostic-rendering.composed-location.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.diagnostic-rendering.composed-location +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.diagnostic-rendering + verifies: spec:validation.diagnostic-rendering +--- +# One finding, three location shapes, one composition rule + +## Intent +- outcome: Execute the composition and both degradations on one finding, so the rule is read as one law rather than three renderings. + +```gwt +Given a finding naming the validator {validatorId: "honesty/readiness-floor"} at severity {severity: "error"} carrying the message {message: "The stated rung is not earned."} +When the command-line renderer formats that finding once per location shape +Then the finding carrying the file {file: "specs/probe.sdp.md"} and the line {line: 7} renders {withLocation: "specs/probe.sdp.md:7 — [error] honesty/readiness-floor — The stated rung is not earned."} +Then the same finding carrying the file alone renders {fileOnly: "specs/probe.sdp.md — [error] honesty/readiness-floor — The stated rung is not earned."} +Then the same finding carrying neither renders {bare: "[error] honesty/readiness-floor — The stated rung is not earned."} +``` diff --git a/specs/validation/diagnostic-rendering.sdp.md b/specs/validation/diagnostic-rendering.sdp.md new file mode 100644 index 0000000..ce503e3 --- /dev/null +++ b/specs/validation/diagnostic-rendering.sdp.md @@ -0,0 +1,30 @@ +--- +id: spec:validation.diagnostic-rendering +kind: rule +altitude: feature +readiness: ready +relations: + refines: spec:validation.two-check-families + dependsOn: spec:consumers.design-review +--- +# One diagnostic currency, its location composed from structured fields + +## Intent +- outcome: Let a reader locate any reported problem the same way, whichever producer or surface reported it. + +## Rule +- There is one diagnostic currency. Extraction, contract generation, and graph validation all report in the one finding shape, and no surface introduces a parallel report shape of its own. +- A finding's location lives in its own structured file and line fields, never baked into its message text, so a location is composed once by whoever renders it and is never rendered twice. +- The command-line rendering is the path and line, the severity in brackets, the validator id, and the message, in that order and separated by the same one-line punctuation for every finding. +- The location degrades by field rather than by placeholder: a file with a line renders both, a file without a line renders the path alone, and a finding carrying no file renders no location prefix at all. +- The Design Review renders the same currency in its findings table under the same composition rule, and shows an em dash where a finding carries no file, because a table cell cannot be absent the way a prefix can. +- The realizing entrypoints are `formatFinding` in `src/cli/output.ts` and `renderFindings` in `src/projections/design-review-context.ts`. + +## Example space +```gwt-vocabulary +Given a finding naming the validator {validatorId:string} at severity {severity:"warning"|"error"} carrying the message {message:string} +When the command-line renderer formats that finding once per location shape +Then the finding carrying the file {file:string} and the line {line:number} renders {withLocation:string} +Then the same finding carrying the file alone renders {fileOnly:string} +Then the same finding carrying neither renders {bare:string} +``` diff --git a/specs/validation/validator-self-testing.sdp.md b/specs/validation/validator-self-testing.sdp.md new file mode 100644 index 0000000..f050108 --- /dev/null +++ b/specs/validation/validator-self-testing.sdp.md @@ -0,0 +1,19 @@ +--- +id: spec:validation.validator-self-testing +kind: rule +altitude: feature +readiness: defined +relations: + refines: spec:validation.two-check-families +--- +# Every validator ships evidence in both directions + +## Intent +- outcome: Keep a validator that has silently stopped firing from reading as a clean build. + +## Rule +- Each validator ships evidence in both directions: at least one input it must refuse, and at least one it must accept. +- The should-fail half is what catches the regression that matters most — a validator that no longer fires reports nothing, and nothing is indistinguishable from a clean graph unless something asserts the refusal. +- The should-pass half bounds the first: a validator that refuses everything is as useless as one that refuses nothing, and only an accepted input separates the two. +- This is evidence discipline over the two check families, never a check of its own. No validator polices whether another validator carries tests: that would police the delivery process rather than conformance or honesty, which the standing guardrail forbids. +- The discipline is cheap by construction — a probe world per direction — and it is stated here so the two families are read as checks that are themselves checked. diff --git a/test/self-hosting-graph.test.ts b/test/self-hosting-graph.test.ts index 1b7d05e..b7c1a6d 100644 --- a/test/self-hosting-graph.test.ts +++ b/test/self-hosting-graph.test.ts @@ -90,12 +90,12 @@ describe("the self-hosting corpus", () => { // The literals are the corpus checkpoint. The authored arrays are measured against the same // literals rather than standing in for them, so a transcription slip in an oracle module // cannot certify itself by moving both sides of a comparison at once. - expect(result.counts).toEqual({ specs: 93, packs: 1, anchors: 70 }); - expect(expectedSpecs).toHaveLength(93); - expect(expectedPackMembers).toHaveLength(93); - expect(expectedAnchors).toHaveLength(70); - expect(result.graph.nodes).toHaveLength(164); - expect(result.graph.edges).toHaveLength(317); + expect(result.counts).toEqual({ specs: 103, packs: 1, anchors: 75 }); + expect(expectedSpecs).toHaveLength(103); + expect(expectedPackMembers).toHaveLength(103); + expect(expectedAnchors).toHaveLength(75); + expect(result.graph.nodes).toHaveLength(179); + expect(result.graph.edges).toHaveLength(351); }); it("rosters exactly the authored Spec, Pack, and anchor node ids", () => { @@ -145,7 +145,7 @@ describe("the self-hosting corpus", () => { }), {}, ), - ).toEqual({ defined: 36, ready: 57 }); + ).toEqual({ defined: 37, ready: 66 }); }); it("derives the Pack membership edges from the manifest, in manifest order", () => { diff --git a/test/self-hosting-oracle/anchors.ts b/test/self-hosting-oracle/anchors.ts index 213063f..c14c67c 100644 --- a/test/self-hosting-oracle/anchors.ts +++ b/test/self-hosting-oracle/anchors.ts @@ -703,4 +703,54 @@ export const expectedAnchors = [ constant: "splitReportTestAnchor", site: "bindExample(splitReportContract", }, + { + id: "test:protocol.derived-readiness-banner.dishonest-divergence", + nodeType: "Anchor", + label: "the overstated-rung point verifies the banner and its first unmet clause", + type: "verifies", + target: "spec:consumers.derived-readiness-banner.dishonest-divergence", + file: "test/self-hosting-projections.test.ts", + constant: "dishonestDivergenceTestAnchor", + site: "bindExample(dishonestDivergenceContract", + }, + { + id: "test:protocol.derived-readiness-banner.honest-headroom", + nodeType: "Anchor", + label: "the understated-rung point verifies the one-direction bound on the banner", + type: "verifies", + target: "spec:consumers.derived-readiness-banner.honest-headroom", + file: "test/self-hosting-projections.test.ts", + constant: "honestHeadroomTestAnchor", + site: "bindExample(honestHeadroomContract", + }, + { + id: "test:protocol.binding-language-views.bound-spec-page", + nodeType: "Anchor", + label: "the bound-surface point verifies the rendered binding vocabulary", + type: "verifies", + target: "spec:consumers.binding-language-views.bound-spec-page", + file: "test/self-hosting-projections.test.ts", + constant: "boundSpecPageTestAnchor", + site: "bindExample(boundSpecPageContract", + }, + { + id: "test:protocol.wholesale-view-rewrite.stale-page-removed", + nodeType: "Anchor", + label: "the stale-page point verifies the wholesale rewrite of the view directory", + type: "verifies", + target: "spec:consumers.wholesale-view-rewrite.stale-page-removed", + file: "test/self-hosting-projections.test.ts", + constant: "stalePageRemovedTestAnchor", + site: "bindExample(stalePageRemovedContract", + }, + { + id: "test:protocol.diagnostic-rendering.composed-location", + nodeType: "Anchor", + label: "the composed-location point verifies the one rendering rule and both degradations", + type: "verifies", + target: "spec:validation.diagnostic-rendering.composed-location", + file: "test/self-hosting-projections.test.ts", + constant: "composedLocationTestAnchor", + site: "bindExample(composedLocationContract", + }, ] as const; diff --git a/test/self-hosting-oracle/consumers.ts b/test/self-hosting-oracle/consumers.ts index 714f288..256efc0 100644 --- a/test/self-hosting-oracle/consumers.ts +++ b/test/self-hosting-oracle/consumers.ts @@ -140,4 +140,250 @@ export const consumersSpecs = [ }, deliveryFacts: [], }, + { + id: "spec:consumers.derived-readiness-banner", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/consumers/derived-readiness-banner.sdp.md", + title: "Derived readiness renders beside the stated rung and warns in one direction", + narrative: null, + sections: { + intent: { + outcome: + "Show a reader where a Spec's stated maturity stands against the structure it earns, without turning a floor into a quota.", + }, + behavior: { + rules: [ + "Derived readiness is the highest rung whose cumulative floor clauses pass. It is computed from the graph, rendered beside the author's statement, and never overwrites it.", + "Every spec page renders the stated rung beside the floor reached, on one line, whether or not the two agree; the index and the pack member table carry the same pair as two columns.", + "The divergence banner is raised only in the dishonest direction — the floor reached standing below the stated rung. A floor reached at or above the stated rung raises nothing, because a floor is a floor and never a quota that nags upward.", + "A raised banner names the first unmet clause by its clause id and its description, so the reader is told which clause to satisfy rather than only that something is wrong.", + "When even the `idea` floor is unmet, the floor reached renders as none rather than as a rung, and a raised banner states that the floor stands below `idea`.", + "The banner is rendering, never a check: the same divergence is already the readiness floor's own finding, and the page shows it in context rather than gating on it.", + "The realizing entrypoint is `renderReadiness` in `src/projections/design-review-context.ts`; the rung it renders is the derived readiness the one clause table yields.", + ], + exampleSpace: { + given: [ + 'the graph holds a rule spec {specId:string} whose stated readiness is {statedReadiness:"scoped"|"ready"}', + 'the spec {structure:"clears every floor clause"|"records a blocking open question"}', + ], + when: ["the Design Review renders the graph"], + [["t", "hen"].join("")]: [ + 'the spec page renders the floor reached {floorReached:"scoped"|"ready"}', + "the divergence banner is raised: {bannerRaised:boolean}", + "the banner names the first unmet clause {clauseId:string}", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.derived-readiness-banner.dishonest-divergence", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/derived-readiness-banner.dishonest-divergence.sdp.md", + title: "An overstated rung raises the banner and names the clause that refused", + narrative: null, + sections: { + intent: { + outcome: + "Execute the dishonest direction, where the page must name the first clause the structure leaves unmet.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a rule spec {specId: "spec:probe.overstated-rung"} whose stated readiness is {statedReadiness: "ready"}', + 'the spec {structure: "records a blocking open question"}', + ], + when: ["the Design Review renders the graph"], + [["t", "hen"].join("")]: [ + 'the spec page renders the floor reached {floorReached: "scoped"}', + "the divergence banner is raised: {bannerRaised: true}", + 'the banner names the first unmet clause {clauseId: "no-blocking-open-questions"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.derived-readiness-banner.honest-headroom", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/derived-readiness-banner.honest-headroom.sdp.md", + title: "A rung the structure overshoots renders as information, not as a banner", + narrative: null, + sections: { + intent: { + outcome: + "Execute the honest direction, where the line still renders both rungs and nothing nags the author upward.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a rule spec {specId: "spec:probe.understated-rung"} whose stated readiness is {statedReadiness: "scoped"}', + 'the spec {structure: "clears every floor clause"}', + ], + when: ["the Design Review renders the graph"], + [["t", "hen"].join("")]: [ + 'the spec page renders the floor reached {floorReached: "ready"}', + "the divergence banner is raised: {bannerRaised: false}", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.binding-language-views", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/consumers/binding-language-views.sdp.md", + title: "Views speak binding language, never the internal fact name", + narrative: null, + sections: { + intent: { + outcome: + "Keep a reader from reading a delivery fact as a liveness claim the graph never made.", + }, + behavior: { + rules: [ + "The delivery-fact names stay internal. They are the graph's own vocabulary and the drift queries read them; no rendered surface shows one as user-facing label text.", + "A spec page's bindings block renders four labelled lines — implementation binding, verifier binding, expected-outcome oracle, and runtime observation.", + "The three binding lines read present or none, and nothing else: what a binding says is that a resolving anchor exists, so the reader is offered existence rather than a degree.", + "Runtime observation always reads not tracked. No delivery fact records it, and the view states the absence instead of leaving a reader to infer it from a missing line.", + "The pack member table and the index table carry the same two binding columns, with the same present and none values, so the aggregate surfaces speak the page's language rather than a shorthand of their own.", + "The model half of this rule — that a binding states existence and never liveness — belongs to the decision this Spec is shaped by; what is stated here is only what the views render.", + "The realizing entrypoints are `renderBindings` in `src/projections/design-review-context.ts` and the member and index tables in `src/projections/design-review-pages.ts`.", + ], + exampleSpace: { + given: [ + 'the graph holds a spec {specId:string} bound by {bindings:"an implementing code anchor and a verifying test anchor"|"no anchor at all"}', + ], + when: ["the Design Review renders the graph"], + [["t", "hen"].join("")]: [ + 'the spec page renders the implementation binding as {implementation:"present"|"none"}', + 'the spec page renders the verifier binding as {verifier:"present"|"none"}', + "the spec page renders the runtime observation as {observation:string}", + "the index table repeats those binding values for the spec: {tableRepeats:boolean}", + "the internal delivery-fact name {factName:string} appears as rendered label text: {factNameRendered:boolean}", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.binding-language-views.bound-spec-page", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/binding-language-views.bound-spec-page.sdp.md", + title: "A fully bound spec renders binding language on the page and in the index", + narrative: null, + sections: { + intent: { + outcome: + "Execute the rendered vocabulary on a spec both anchors reach, where the internal fact name would be easiest to leak.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.bound-surface"} bound by {bindings: "an implementing code anchor and a verifying test anchor"}', + ], + when: ["the Design Review renders the graph"], + [["t", "hen"].join("")]: [ + 'the spec page renders the implementation binding as {implementation: "present"}', + 'the spec page renders the verifier binding as {verifier: "present"}', + 'the spec page renders the runtime observation as {observation: "not tracked"}', + "the index table repeats those binding values for the spec: {tableRepeats: true}", + 'the internal delivery-fact name {factName: "implemented"} appears as rendered label text: {factNameRendered: false}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.wholesale-view-rewrite", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/consumers/wholesale-view-rewrite.sdp.md", + title: "Every view run rewrites the view wholesale", + narrative: null, + sections: { + intent: { + outcome: + "Guarantee that whatever a reader finds in the view directory was produced by the last run over the current source.", + }, + behavior: { + rules: [ + "A view run rewrites the view wholesale: no page written by an earlier run survives a later one, so a spec that left the corpus leaves no page behind.", + "Pages are written to a temporary sibling of the view directory, the previous directory is removed, and the temporary is renamed into place — one rename, so no half-written view is ever readable and no temporary survives a completed run.", + "A run that cannot produce a current view removes the stale one instead of leaving it readable as current: an absent view is honest, a stale view is not.", + "The invalidation happens before rendering as well as after it: the build the run passes through removes any existing view up front, so a run that fails before rendering leaves nothing behind either.", + "Under `--check-clean` the view is rendered twice from the same graph and the run refuses when the two renders diverge, removing the view it could not certify.", + "Findings never withhold the view. A run whose checks report findings still writes the current view and returns the checks' own exit code, because the view is where those findings are read in context.", + "The realizing entrypoint is `runView` in `src/cli/validate-view-command.ts`, with the up-front invalidation in `runBuild` in `src/cli/build-command.ts`.", + ], + exampleSpace: { + given: [ + "an extraction root holding {corpus:string} and a stale view page {stalePage:string}", + ], + when: ["the view is rendered at that root"], + [["t", "hen"].join("")]: [ + "the run exits {exitCode:number}", + "the view holds the current page {currentPage:string}", + "the stale page survives: {staleSurvives:boolean}", + "a temporary view sibling survives: {temporarySurvives:boolean}", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.wholesale-view-rewrite.stale-page-removed", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/wholesale-view-rewrite.stale-page-removed.sdp.md", + title: "A page from an earlier run does not survive the next one", + narrative: null, + sections: { + intent: { + outcome: + "Execute the wholesale rewrite against the case it exists for — a page whose subject the current source no longer holds.", + }, + behavior: { + examples: [ + { + given: [ + 'an extraction root holding {corpus: "one authored spec"} and a stale view page {stalePage: "spec/probe.departed.md"}', + ], + when: ["the view is rendered at that root"], + [["t", "hen"].join("")]: [ + "the run exits {exitCode: 0}", + 'the view holds the current page {currentPage: "index.md"}', + "the stale page survives: {staleSurvives: false}", + "a temporary view sibling survives: {temporarySurvives: false}", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, ] as const; diff --git a/test/self-hosting-oracle/declared-relations.ts b/test/self-hosting-oracle/declared-relations.ts index 0c916a2..a770b5d 100644 --- a/test/self-hosting-oracle/declared-relations.ts +++ b/test/self-hosting-oracle/declared-relations.ts @@ -317,4 +317,63 @@ export const expectedDeclaredRelations = [ "verifies", "spec:validation.two-check-families", ], + ["spec:consumers.derived-readiness-banner", "refines", "spec:consumers.design-review"], + ["spec:consumers.derived-readiness-banner", "dependsOn", "spec:validation.readiness-floor"], + [ + "spec:consumers.derived-readiness-banner.dishonest-divergence", + "refines", + "spec:consumers.derived-readiness-banner", + ], + [ + "spec:consumers.derived-readiness-banner.dishonest-divergence", + "verifies", + "spec:consumers.derived-readiness-banner", + ], + [ + "spec:consumers.derived-readiness-banner.honest-headroom", + "refines", + "spec:consumers.derived-readiness-banner", + ], + [ + "spec:consumers.derived-readiness-banner.honest-headroom", + "verifies", + "spec:consumers.derived-readiness-banner", + ], + ["spec:consumers.binding-language-views", "refines", "spec:consumers.design-review"], + ["spec:consumers.binding-language-views", "decidedBy", "spec:decisions.binding-not-liveness"], + [ + "spec:consumers.binding-language-views.bound-spec-page", + "refines", + "spec:consumers.binding-language-views", + ], + [ + "spec:consumers.binding-language-views.bound-spec-page", + "verifies", + "spec:consumers.binding-language-views", + ], + ["spec:consumers.wholesale-view-rewrite", "refines", "spec:consumers.design-review"], + ["spec:consumers.wholesale-view-rewrite", "dependsOn", "spec:extraction.determinism"], + [ + "spec:consumers.wholesale-view-rewrite.stale-page-removed", + "refines", + "spec:consumers.wholesale-view-rewrite", + ], + [ + "spec:consumers.wholesale-view-rewrite.stale-page-removed", + "verifies", + "spec:consumers.wholesale-view-rewrite", + ], + ["spec:validation.diagnostic-rendering", "refines", "spec:validation.two-check-families"], + ["spec:validation.diagnostic-rendering", "dependsOn", "spec:consumers.design-review"], + [ + "spec:validation.diagnostic-rendering.composed-location", + "refines", + "spec:validation.diagnostic-rendering", + ], + [ + "spec:validation.diagnostic-rendering.composed-location", + "verifies", + "spec:validation.diagnostic-rendering", + ], + ["spec:validation.validator-self-testing", "refines", "spec:validation.two-check-families"], ] as const; diff --git a/test/self-hosting-oracle/pack-members.ts b/test/self-hosting-oracle/pack-members.ts index 964093c..1017758 100644 --- a/test/self-hosting-oracle/pack-members.ts +++ b/test/self-hosting-oracle/pack-members.ts @@ -74,6 +74,16 @@ export const expectedPackMembers = [ "spec:validation.kind-evidence.constraints-alone", "spec:validation.kind-evidence.untargeted-constraint", "spec:validation.kind-evidence.empty-promoted-child", + "spec:validation.diagnostic-rendering", + "spec:validation.diagnostic-rendering.composed-location", + "spec:validation.validator-self-testing", + "spec:consumers.derived-readiness-banner", + "spec:consumers.derived-readiness-banner.dishonest-divergence", + "spec:consumers.derived-readiness-banner.honest-headroom", + "spec:consumers.binding-language-views", + "spec:consumers.binding-language-views.bound-spec-page", + "spec:consumers.wholesale-view-rewrite", + "spec:consumers.wholesale-view-rewrite.stale-page-removed", "spec:decisions.plain-language-references", "spec:decisions.concept-docs-dissolve", "spec:decisions.one-validation-path", diff --git a/test/self-hosting-oracle/validation.ts b/test/self-hosting-oracle/validation.ts index b2a12c5..5296f56 100644 --- a/test/self-hosting-oracle/validation.ts +++ b/test/self-hosting-oracle/validation.ts @@ -909,4 +909,96 @@ export const validationSpecs = [ }, deliveryFacts: ["has-verifier"], }, + { + id: "spec:validation.diagnostic-rendering", + specKind: "rule", + altitude: "feature", + readiness: "ready", + file: "specs/validation/diagnostic-rendering.sdp.md", + title: "One diagnostic currency, its location composed from structured fields", + narrative: null, + sections: { + intent: { + outcome: + "Let a reader locate any reported problem the same way, whichever producer or surface reported it.", + }, + behavior: { + rules: [ + "There is one diagnostic currency. Extraction, contract generation, and graph validation all report in the one finding shape, and no surface introduces a parallel report shape of its own.", + "A finding's location lives in its own structured file and line fields, never baked into its message text, so a location is composed once by whoever renders it and is never rendered twice.", + "The command-line rendering is the path and line, the severity in brackets, the validator id, and the message, in that order and separated by the same one-line punctuation for every finding.", + "The location degrades by field rather than by placeholder: a file with a line renders both, a file without a line renders the path alone, and a finding carrying no file renders no location prefix at all.", + "The Design Review renders the same currency in its findings table under the same composition rule, and shows an em dash where a finding carries no file, because a table cell cannot be absent the way a prefix can.", + "The realizing entrypoints are `formatFinding` in `src/cli/output.ts` and `renderFindings` in `src/projections/design-review-context.ts`.", + ], + exampleSpace: { + given: [ + 'a finding naming the validator {validatorId:string} at severity {severity:"warning"|"error"} carrying the message {message:string}', + ], + when: ["the command-line renderer formats that finding once per location shape"], + [["t", "hen"].join("")]: [ + "the finding carrying the file {file:string} and the line {line:number} renders {withLocation:string}", + "the same finding carrying the file alone renders {fileOnly:string}", + "the same finding carrying neither renders {bare:string}", + ], + }, + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.diagnostic-rendering.composed-location", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/diagnostic-rendering.composed-location.sdp.md", + title: "One finding, three location shapes, one composition rule", + narrative: null, + sections: { + intent: { + outcome: + "Execute the composition and both degradations on one finding, so the rule is read as one law rather than three renderings.", + }, + behavior: { + examples: [ + { + given: [ + 'a finding naming the validator {validatorId: "honesty/readiness-floor"} at severity {severity: "error"} carrying the message {message: "The stated rung is not earned."}', + ], + when: ["the command-line renderer formats that finding once per location shape"], + [["t", "hen"].join("")]: [ + 'the finding carrying the file {file: "specs/probe.sdp.md"} and the line {line: 7} renders {withLocation: "specs/probe.sdp.md:7 — [error] honesty/readiness-floor — The stated rung is not earned."}', + 'the same finding carrying the file alone renders {fileOnly: "specs/probe.sdp.md — [error] honesty/readiness-floor — The stated rung is not earned."}', + 'the same finding carrying neither renders {bare: "[error] honesty/readiness-floor — The stated rung is not earned."}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:validation.validator-self-testing", + specKind: "rule", + altitude: "feature", + readiness: "defined", + file: "specs/validation/validator-self-testing.sdp.md", + title: "Every validator ships evidence in both directions", + narrative: null, + sections: { + intent: { + outcome: "Keep a validator that has silently stopped firing from reading as a clean build.", + }, + behavior: { + rules: [ + "Each validator ships evidence in both directions: at least one input it must refuse, and at least one it must accept.", + "The should-fail half is what catches the regression that matters most — a validator that no longer fires reports nothing, and nothing is indistinguishable from a clean graph unless something asserts the refusal.", + "The should-pass half bounds the first: a validator that refuses everything is as useless as one that refuses nothing, and only an accepted input separates the two.", + "This is evidence discipline over the two check families, never a check of its own. No validator polices whether another validator carries tests: that would police the delivery process rather than conformance or honesty, which the standing guardrail forbids.", + "The discipline is cheap by construction — a probe world per direction — and it is stated here so the two families are read as checks that are themselves checked.", + ], + }, + }, + deliveryFacts: [], + }, ] as const; diff --git a/test/self-hosting-projections.test.ts b/test/self-hosting-projections.test.ts new file mode 100644 index 0000000..ca75a59 --- /dev/null +++ b/test/self-hosting-projections.test.ts @@ -0,0 +1,516 @@ +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; + +import { afterEach, expect } from "vitest"; + +import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; + +import { boundSpecPageContract } from "../generated/contracts/consumers.binding-language-views.bound-spec-page.contract.js"; +import { dishonestDivergenceContract } from "../generated/contracts/consumers.derived-readiness-banner.dishonest-divergence.contract.js"; +import { honestHeadroomContract } from "../generated/contracts/consumers.derived-readiness-banner.honest-headroom.contract.js"; +import { stalePageRemovedContract } from "../generated/contracts/consumers.wholesale-view-rewrite.stale-page-removed.contract.js"; +import { composedLocationContract } from "../generated/contracts/validation.diagnostic-rendering.composed-location.contract.js"; +import { + codeAnchor, + codeAnchorId, + createReader, + refines, + renderDesignReview, + spec, + specId, + specTest as probeSpecTest, + testAnchorId as probeTestAnchorId, +} from "../src/index.js"; +import type { DesignReviewPage, Finding, SpecReadiness } from "../src/index.js"; +import { formatFinding } from "../src/cli/output.js"; +import { runView } from "../src/cli/validate-view-command.js"; +import { deriveFixtureGraph } from "./helpers/fixture-graph.js"; + +/** + * The bound executable points of the projection family: what the one read-only view renders, and + * how the one diagnostic currency renders its location. + * + * Two world styles meet here. The rendering laws run over probe graphs assembled in memory and + * rendered through the real projection seam (`renderDesignReview` over `createReader`), because a + * page is a pure function of the graph and nothing cheaper is honest. The wholesale-rewrite law is + * about the filesystem, so its world is a temporary extraction root carrying a planted page from an + * earlier run — never the repository root, whose `generated/` tree no pooled test may touch. + * + * `test/design-review.test.ts`, `test/design-review-review-08.test.ts`, and `test/cli.test.ts` stay + * as regression evidence: these points state the laws, never every rendered field or CLI spelling. + */ + +const temporaryRoots = new Set(); + +afterEach(() => { + for (const root of temporaryRoots) { + rmSync(root, { recursive: true, force: true }); + } + temporaryRoots.clear(); +}); + +function pageOf(pages: readonly DesignReviewPage[], path: string): string { + const page = pages.find((entry) => entry.path === path); + + if (page === undefined) { + throw new Error(`The rendered view is missing the page "${path}".`); + } + + return page.content; +} + +/* ----- spec:consumers.derived-readiness-banner ----- */ + +const BANNER_PARENT_ID = "spec:probe.banner-parent"; + +interface BannerWorld { + specId: string; + statedReadiness: SpecReadiness; + blockingQuestion: boolean; + pages: readonly DesignReviewPage[] | undefined; +} + +function bannerWorld(): BannerWorld { + return { + specId: "", + statedReadiness: "idea", + blockingQuestion: false, + pages: undefined, + }; +} + +function bannerPage(world: BannerWorld): string { + if (world.pages === undefined) { + throw new Error("The rendering step must run before the page is read."); + } + + return pageOf(world.pages, `spec/${world.specId.slice(world.specId.indexOf(":") + 1)}.md`); +} + +/** The one line the banner law is read from: the blockquote the renderer pushes after the pair. */ +function bannerLineOf(world: BannerWorld): string | undefined { + return bannerPage(world) + .split("\n") + .find((line) => line.startsWith("> **Readiness divergence.**")); +} + +const bannerBindings = { + "the graph holds a rule spec {specId} whose stated readiness is {statedReadiness}": ( + world: BannerWorld, + params: { readonly specId: string; readonly statedReadiness: "scoped" | "ready" }, + ) => { + world.specId = params.specId; + world.statedReadiness = params.statedReadiness; + }, + "the spec {structure}": ( + world: BannerWorld, + params: { + readonly structure: "clears every floor clause" | "records a blocking open question"; + }, + ) => { + world.blockingQuestion = params.structure === "records a blocking open question"; + }, + "the Design Review renders the graph": (world: BannerWorld) => { + // The parent is `defined`, so the subject's own `ready` clauses (targets at least `defined`, + // every relation resolving) all pass: the open question is the only clause that can refuse, + // and the rung the page renders is the rung the floor table yielded. + const parent = spec({ + id: specId(BANNER_PARENT_ID), + title: "Probe parent for the readiness banner", + kind: "behavior", + altitude: "feature", + readiness: "defined", + intent: { outcome: "Terminate the probe's relation chain at a defined target." }, + behavior: { rules: ["The parent carries its own kind evidence."] }, + }); + const subject = spec({ + id: specId(world.specId), + title: "Probe subject of the readiness banner", + kind: "rule", + altitude: "story", + readiness: world.statedReadiness, + intent: { + outcome: "State one rung against the structure the probe carries.", + ...(world.blockingQuestion + ? { + openQuestions: [ + { question: "Which rung does the probe honestly stand at?", blocking: true }, + ], + } + : {}), + }, + behavior: { rules: ["A rule's statement is its own evidence."] }, + relations: [refines(specId(BANNER_PARENT_ID))], + }); + + world.pages = renderDesignReview( + createReader(deriveFixtureGraph({ specs: [parent, subject] })), + ); + }, + "the spec page renders the floor reached {floorReached}": ( + world: BannerWorld, + params: { readonly floorReached: "scoped" | "ready" }, + ) => { + // The pair renders on every page, agreeing or not — the positive statement the banner's own + // absence is read beside, so absence is never the sole discriminator. + expect(bannerPage(world)).toContain( + `**Readiness:** stated \`${world.statedReadiness}\` · structural floor reached: \`${params.floorReached}\``, + ); + }, + "the divergence banner is raised: {bannerRaised}": ( + world: BannerWorld, + params: { readonly bannerRaised: boolean }, + ) => { + expect(bannerLineOf(world) !== undefined).toBe(params.bannerRaised); + }, + "the banner names the first unmet clause {clauseId}": ( + world: BannerWorld, + params: { readonly clauseId: string }, + ) => { + const named = /First unmet clause: `(?[^`]+)` — (?.+)$/u.exec( + bannerLineOf(world) ?? "the divergence banner is missing", + ); + + expect(named?.groups?.clauseId).toBe(params.clauseId); + // The description is the clause's own words, read from the banner rather than from the table + // the banner read: what the law requires is that the reader is told which clause refused. + expect((named?.groups?.description ?? "").length).toBeGreaterThan(0); + }, +}; + +const dishonestDivergenceTestAnchor = specTest({ + id: testAnchorId("test:protocol.derived-readiness-banner.dishonest-divergence"), + label: "the overstated-rung point verifies the banner and its first unmet clause", + verifies: ref("spec:consumers.derived-readiness-banner.dishonest-divergence"), +}); +void dishonestDivergenceTestAnchor; + +bindExample(dishonestDivergenceContract, bannerWorld, bannerBindings); + +const honestHeadroomTestAnchor = specTest({ + id: testAnchorId("test:protocol.derived-readiness-banner.honest-headroom"), + label: "the understated-rung point verifies the one-direction bound on the banner", + verifies: ref("spec:consumers.derived-readiness-banner.honest-headroom"), +}); +void honestHeadroomTestAnchor; + +bindExample(honestHeadroomContract, bannerWorld, bannerBindings); + +/* ----- spec:consumers.binding-language-views ----- */ + +const BINDING_PARENT_ID = "spec:probe.binding-parent"; + +interface BindingWorld { + specId: string; + bound: boolean; + pages: readonly DesignReviewPage[] | undefined; +} + +function bindingWorld(): BindingWorld { + return { specId: "", bound: false, pages: undefined }; +} + +function bindingPages(world: BindingWorld): readonly DesignReviewPage[] { + if (world.pages === undefined) { + throw new Error("The rendering step must run before the rendered surfaces are read."); + } + + return world.pages; +} + +function bindingPage(world: BindingWorld): string { + return pageOf( + bindingPages(world), + `spec/${world.specId.slice(world.specId.indexOf(":") + 1)}.md`, + ); +} + +/** The subject's row in the index table — the aggregate surface that must speak the same words. */ +function indexRowOf(world: BindingWorld): string { + const row = pageOf(bindingPages(world), "index.md") + .split("\n") + .find((line) => line.startsWith(`| [\`${world.specId}\`]`)); + + if (row === undefined) { + throw new Error(`The index table is missing a row for "${world.specId}".`); + } + + return row; +} + +const bindingLanguageBindings = { + "the graph holds a spec {specId} bound by {bindings}": ( + world: BindingWorld, + params: { + readonly specId: string; + readonly bindings: + | "an implementing code anchor and a verifying test anchor" + | "no anchor at all"; + }, + ) => { + world.specId = params.specId; + world.bound = params.bindings === "an implementing code anchor and a verifying test anchor"; + }, + "the Design Review renders the graph": (world: BindingWorld) => { + const parent = spec({ + id: specId(BINDING_PARENT_ID), + title: "Probe parent for the rendered binding vocabulary", + kind: "behavior", + altitude: "feature", + readiness: "defined", + intent: { outcome: "Keep the probe connected so no orphan signal renders beside it." }, + behavior: { rules: ["The parent carries its own kind evidence."] }, + }); + const subject = spec({ + id: specId(world.specId), + title: "Probe subject of the rendered binding vocabulary", + kind: "behavior", + altitude: "story", + readiness: "idea", + intent: { outcome: "Carry the bindings the view must describe in its own words." }, + behavior: { rules: ["The subject states one rule and lets its anchors do the rest."] }, + relations: [refines(specId(BINDING_PARENT_ID))], + }); + // Probe anchors, built inline through the source builders rather than the package import: an + // anchor-constant form bound to the protocol import is a real corpus binding, and these two are + // world data the extractor must never reify. + const anchors = world.bound + ? [ + codeAnchor({ + id: codeAnchorId("impl:probe.bound-surface"), + label: "binds the probe subject to code", + satisfies: specId(world.specId), + }), + probeSpecTest({ + id: probeTestAnchorId("test:probe.bound-surface"), + label: "binds the probe subject to a test entrypoint", + verifies: specId(world.specId), + }), + ] + : []; + + world.pages = renderDesignReview( + createReader(deriveFixtureGraph({ specs: [parent, subject], anchors })), + ); + }, + "the spec page renders the implementation binding as {implementation}": ( + world: BindingWorld, + params: { readonly implementation: "present" | "none" }, + ) => { + expect(bindingPage(world)).toContain(`- Implementation binding: **${params.implementation}**`); + }, + "the spec page renders the verifier binding as {verifier}": ( + world: BindingWorld, + params: { readonly verifier: "present" | "none" }, + ) => { + expect(bindingPage(world)).toContain(`- Verifier binding: **${params.verifier}**`); + // The oracle line is the third of the three existence lines, and reads the same two words. + expect(bindingPage(world)).toContain("- Expected-outcome oracle: **none**"); + }, + "the spec page renders the runtime observation as {observation}": ( + world: BindingWorld, + params: { readonly observation: string }, + ) => { + expect(bindingPage(world)).toContain(`- Runtime observation: **${params.observation}**`); + }, + "the index table repeats those binding values for the spec: {tableRepeats}": ( + world: BindingWorld, + params: { readonly tableRepeats: boolean }, + ) => { + const columns = indexRowOf(world).endsWith("| present | present |"); + + expect(columns).toBe(params.tableRepeats); + }, + "the internal delivery-fact name {factName} appears as rendered label text: {factNameRendered}": ( + world: BindingWorld, + params: { readonly factName: string; readonly factNameRendered: boolean }, + ) => { + // The probe authors none of these words itself, so every occurrence would be the renderer's. + for (const surface of [bindingPage(world), indexRowOf(world)]) { + expect(surface.includes(params.factName)).toBe(params.factNameRendered); + expect(surface.includes("has-verifier")).toBe(params.factNameRendered); + } + }, +}; + +const boundSpecPageTestAnchor = specTest({ + id: testAnchorId("test:protocol.binding-language-views.bound-spec-page"), + label: "the bound-surface point verifies the rendered binding vocabulary", + verifies: ref("spec:consumers.binding-language-views.bound-spec-page"), +}); +void boundSpecPageTestAnchor; + +bindExample(boundSpecPageContract, bindingWorld, bindingLanguageBindings); + +/* ----- spec:consumers.wholesale-view-rewrite ----- */ + +const PROBE_CARRIER = `--- +id: spec:probe.view-subject +kind: rule +altitude: story +readiness: idea +relations: {} +--- +# The probe subject of a view run + +## Intent +- outcome: Give the view one page to render over a temporary extraction root. +`; + +interface ViewWorld { + readonly root: string; + stalePage: string; + exitCode: number | undefined; +} + +function viewWorld(): ViewWorld { + const root = mkdtempSync(join(tmpdir(), "sdp-self-hosting-view-")); + temporaryRoots.add(root); + + return { root, stalePage: "", exitCode: undefined }; +} + +function viewPathOf(world: ViewWorld): string { + return join(world.root, "generated", "design-review"); +} + +const wholesaleRewriteBindings = { + "an extraction root holding {corpus} and a stale view page {stalePage}": ( + world: ViewWorld, + params: { readonly corpus: string; readonly stalePage: string }, + ) => { + mkdirSync(join(world.root, "specs"), { recursive: true }); + writeFileSync(join(world.root, "specs", "probe.sdp.md"), PROBE_CARRIER, "utf8"); + + // The planted page names a subject the corpus does not hold, so nothing the run writes can + // overwrite it: it survives only if a run is allowed to leave an earlier one's output behind. + world.stalePage = params.stalePage; + const stalePath = join(viewPathOf(world), ...params.stalePage.split("/")); + mkdirSync(dirname(stalePath), { recursive: true }); + writeFileSync(stalePath, "# A spec the corpus no longer holds\n", "utf8"); + }, + "the view is rendered at that root": (world: ViewWorld) => { + world.exitCode = runView({ root: world.root, exclude: [], checkClean: false }, {}, {}); + }, + "the run exits {exitCode}": (world: ViewWorld, params: { readonly exitCode: number }) => { + expect(world.exitCode).toBe(params.exitCode); + }, + "the view holds the current page {currentPage}": ( + world: ViewWorld, + params: { readonly currentPage: string }, + ) => { + expect(existsSync(join(viewPathOf(world), ...params.currentPage.split("/")))).toBe(true); + expect(existsSync(join(viewPathOf(world), "spec", "probe.view-subject.md"))).toBe(true); + }, + "the stale page survives: {staleSurvives}": ( + world: ViewWorld, + params: { readonly staleSurvives: boolean }, + ) => { + expect(existsSync(join(viewPathOf(world), ...world.stalePage.split("/")))).toBe( + params.staleSurvives, + ); + }, + "a temporary view sibling survives: {temporarySurvives}": ( + world: ViewWorld, + params: { readonly temporarySurvives: boolean }, + ) => { + expect(existsSync(`${viewPathOf(world)}.tmp`)).toBe(params.temporarySurvives); + }, +}; + +const stalePageRemovedTestAnchor = specTest({ + id: testAnchorId("test:protocol.wholesale-view-rewrite.stale-page-removed"), + label: "the stale-page point verifies the wholesale rewrite of the view directory", + verifies: ref("spec:consumers.wholesale-view-rewrite.stale-page-removed"), +}); +void stalePageRemovedTestAnchor; + +bindExample(stalePageRemovedContract, viewWorld, wholesaleRewriteBindings); + +/* ----- spec:validation.diagnostic-rendering ----- */ + +type LocationFields = Pick; + +interface DiagnosticWorld { + finding: Finding | undefined; + render: ((location: LocationFields) => string) | undefined; + file: string; +} + +function diagnosticWorld(): DiagnosticWorld { + return { finding: undefined, render: undefined, file: "" }; +} + +function renderOf(world: DiagnosticWorld): (location: LocationFields) => string { + if (world.render === undefined) { + throw new Error("The formatting step must run before a rendered line is read."); + } + + return world.render; +} + +const diagnosticRenderingBindings = { + "a finding naming the validator {validatorId} at severity {severity} carrying the message {message}": + ( + world: DiagnosticWorld, + params: { + readonly validatorId: string; + readonly severity: "warning" | "error"; + readonly message: string; + }, + ) => { + world.finding = { + validatorId: params.validatorId, + family: "honesty", + severity: params.severity, + message: params.message, + }; + }, + "the command-line renderer formats that finding once per location shape": ( + world: DiagnosticWorld, + ) => { + const finding = world.finding; + + if (finding === undefined) { + throw new Error("The finding step must run before the renderer is bound to it."); + } + + // One finding, one renderer: each outcome step below supplies a location shape and reads what + // the real command-line renderer composed from those structured fields alone. + world.render = (location) => formatFinding({ ...finding, ...location }); + }, + "the finding carrying the file {file} and the line {line} renders {withLocation}": ( + world: DiagnosticWorld, + params: { readonly file: string; readonly line: number; readonly withLocation: string }, + ) => { + const rendered = renderOf(world)({ file: params.file, line: params.line }); + + world.file = params.file; + expect(rendered).toBe(`${params.withLocation}\n`); + // The composed prefix is the only place the path appears: the message never carries it too. + expect(rendered.indexOf(params.file)).toBe(rendered.lastIndexOf(params.file)); + }, + "the same finding carrying the file alone renders {fileOnly}": ( + world: DiagnosticWorld, + params: { readonly fileOnly: string }, + ) => { + expect(renderOf(world)({ file: world.file })).toBe(`${params.fileOnly}\n`); + }, + "the same finding carrying neither renders {bare}": ( + world: DiagnosticWorld, + params: { readonly bare: string }, + ) => { + expect(renderOf(world)({})).toBe(`${params.bare}\n`); + }, +}; + +const composedLocationTestAnchor = specTest({ + id: testAnchorId("test:protocol.diagnostic-rendering.composed-location"), + label: "the composed-location point verifies the one rendering rule and both degradations", + verifies: ref("spec:validation.diagnostic-rendering.composed-location"), +}); +void composedLocationTestAnchor; + +bindExample(composedLocationContract, diagnosticWorld, diagnosticRenderingBindings); From 0d80025fd4730b993cebb5a6d16f518c5109d056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 19:52:44 +0200 Subject: [PATCH 07/16] docs(plans): record the view wave in the phase-4 ledgers The conversion ledger carries the five S3 laws with their point counts and the recorded family call for the diagnostic rendering rule; the readiness ledger carries the wave's promotions and its two refusals; the session row and the shared-constant watch item record the seventh bound suite entering through the one row, negative control included. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- plans/21-self-hosting-phase-4.md | 33 +++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/plans/21-self-hosting-phase-4.md b/plans/21-self-hosting-phase-4.md index b8e3c4a..1e38028 100644 --- a/plans/21-self-hosting-phase-4.md +++ b/plans/21-self-hosting-phase-4.md @@ -222,7 +222,7 @@ recorded (ruling 12). Promotions ride verifiers per ruling 5. | table sugar (ruling 4) | sibling authoring proves dishonest or unusable in a wave | unfired | | single-literal vocabulary form | real material forces it | unfired | | per-family oracle drift | a split family module regains cross-family assertions or a mega-assert | unfired | -| shared-constant bypass | a new contract-dependent suite lands outside the constant | unfired — the constant landed at S2 with no surprise; the eslint side derives from the root row only, because the example tree's suite sits outside the typed-lint globs the exemption relaxes. S3's new suite is the first real test of the coupling | +| shared-constant bypass | a new contract-dependent suite lands outside the constant | unfired — the constant landed at S2 with no surprise; the eslint side derives from the root row only, because the example tree's suite sits outside the typed-lint globs the exemption relaxes. **S3 proved it:** `test/self-hosting-projections.test.ts` (the seventh suite) entered through one edit to the root row and both surfaces followed. The negative control ran too — with the row removed and `generated/contracts` moved aside, clean-room lint fails with five `no-unsafe-argument` errors and the wrapper stops refusing fast, so the coupling is load-bearing on both sides rather than incidentally satisfied | | separate example-id namespace | a collision or real pressure appears | unfired (watch continues from phase 3) | ## §4 Docket ledger (carried in from plan 20) @@ -272,11 +272,11 @@ reasons)* |---|---|---|---|---| | S2 | lower floor rungs (`idea`/`scoped`/`defined` clauses) | `spec:validation.readiness-floor` (enriched) | 2–3 | done — 2 points (`at-least-one-relation` on a scoped probe · `no-blocking-open-questions` on a defined probe), both mutation-probed red | | S2 | per-kind evidence table + MD-16 promoted-evidence bound | new `spec:validation.kind-evidence` | 1–2 | done — 3 points (behavior-family complete cell · constraint target · the promoted-evidence bound); one over the planned ceiling, taken deliberately so the MD-16 bound the Spec states is not the only row left unbound | -| S3 | derived-readiness banner (one direction · first unmet clause) | new Spec under `specs/consumers/` | 1–2 | planned | -| S3 | `implemented` view-label (binding language) | same family | 1 | planned | -| S3 | wholesale page rewrite (atomic swap · no stale page) | new Spec | 1 | planned | -| S3 | one diagnostic rendering rule | new Spec | 1 | planned | -| S3 | validator self-testing | new Spec | 0–1 (may honestly stay `defined`) | planned | +| S3 | derived-readiness banner (one direction · first unmet clause) | new `spec:consumers.derived-readiness-banner` | 1–2 | done — 2 points (`dishonest-divergence` names the first unmet clause · `honest-headroom` pairs the absent banner with the rendered stated-beside-derived line), both mutation-probed red | +| S3 | `implemented` view-label (binding language) | new `spec:consumers.binding-language-views` | 1 | done — 1 point (`bound-spec-page`: the four binding lines, the index row repeating them, and the internal fact names absent from both surfaces), mutation-probed red | +| S3 | wholesale page rewrite (atomic swap · no stale page) | new `spec:consumers.wholesale-view-rewrite` | 1 | done — 1 point (`stale-page-removed`, a temp-root world running the real `runView`), mutation-probed red; the law is realized at two sites (the up-front invalidation in `runBuild` plus the temp-and-rename in `runView`), so breaking one alone leaves the point green — recorded, and the Spec states both | +| S3 | one diagnostic rendering rule | new `spec:validation.diagnostic-rendering` | 1 | done — 1 point (`composed-location`: the composed prefix plus both degradations on one finding), mutation-probed red. **Family call:** the carrier lives in `specs/validation/` and refines `spec:validation.two-check-families`, because the law's subject is the Finding currency — a validation concept whose shape law that parent already carries. The consumers family offered no honest parent: `spec:consumers.projections-model` is a `model`-kind vocabulary rather than a law a rule refines, and `spec:consumers.design-review` is only one of the two rendering surfaces. The Design Review half rides a `dependsOn` edge to that Spec instead | +| S3 | validator self-testing | new `spec:validation.validator-self-testing` | 0–1 (may honestly stay `defined`) | done — 0 points, stated `defined`: the only mechanical verifier available would inspect the test corpus for should-fail/should-pass pairs, which polices the delivery process rather than conformance or honesty | ## §8 Readiness ledger @@ -300,6 +300,25 @@ entrypoint and a second anchor on the same file would be decorative. Closing distribution: **`ready: 57 / defined: 36` over 93** (87 → 93 Specs · 65 → 70 anchors · 153 → 164 nodes · 294 → 317 edges), zero errors and zero warnings over the regenerated graph. +**S3 — the view wave.** Ten Specs enter the corpus (opening distribution `ready: 57 / +defined: 36` over 93). Nine are stated `ready` at authoring because the floor clears and a +resolving verifier lands in the same change: the four new rule Specs +(`spec:consumers.derived-readiness-banner`, `spec:consumers.binding-language-views`, +`spec:consumers.wholesale-view-rewrite`, `spec:validation.diagnostic-rendering`) and their five +`example` children (`dishonest-divergence`, `honest-headroom`, `bound-spec-page`, +`stale-page-removed`, `composed-location`). + +Refusals on the record. `spec:validation.validator-self-testing` stays `defined`: its content is +acceptance-grade and its floor clears, but no honest verifier exists — the only mechanical check +would read the test corpus for should-fail/should-pass pairs, which is workflow policing, so the +Spec carries no example child and no bound point. None of the four new rule Specs carries +`implemented`: no code anchor was added, because the projection artifact is already bound at its +entry (`impl:protocol.design-review`) and a second anchor per render helper on the same artifact +would be decorative — the S2 precedent, applied to the view surface. + +Closing distribution: **`ready: 66 / defined: 37` over 103** (93 → 103 Specs · 70 → 75 anchors · +164 → 179 nodes · 317 → 351 edges), zero errors and zero warnings over the regenerated graph. + ## §9 Session and gate ledger Sessions execute sequentially; each closes with a green twelve-leg gate, a regenerated Design @@ -310,7 +329,7 @@ ledger is git process evidence, never graph content. |---|---|---|---| | S1 | the oracle split (§2 S1) | orchestrator-verified green gate | done — 21 `it()`s over one hoisted extraction; the frozen expectation moved to ten authored modules under `test/self-hosting-oracle/` (seven family files, pack manifest, declared relations, anchors) plus their aggregating index; zero assertion loss (every one of the 27 original `expect` sites survives, 5 added: three oracle-length cross-checks and the two-assertion "no Spec outside the families" law), the node-id roster derived from the authored arrays per ruling 10; counts unchanged at 87/1/65 · 153 · 294 · ready 51 / defined 36 | | S2 | shared constant + floor wave | orchestrator-verified green gate | done — `contract-dependent-suites.mjs` now states the per-tree rows once and both `vitest-test.mjs` and `eslint.config.js` read it (clean-room proof: lint passes with `generated/contracts` moved aside, the wrapper still fails fast with the same recovery text); the floor wave carried the `idea`/`scoped`/`defined` rungs into `spec:validation.readiness-floor` and the per-kind table into the new `spec:validation.kind-evidence`, with 5 bound points each mutation-probed red for the clause it names; corpus 87 → 93 Specs, `ready` 51 → 57 | -| S3 | view wave + seventh bound suite | orchestrator-verified green gate | planned | +| S3 | view wave + seventh bound suite | orchestrator-verified green gate | done — five laws carried (banner · view-label · wholesale rewrite · diagnostic rendering · validator self-testing), ten Specs added, five bound points in the new `test/self-hosting-projections.test.ts`, each mutation-probed red for the law it names; the suite entered the shared constant once and both surfaces followed (clean-room proof: with `generated/contracts` moved aside, lint passes with the row and fails with five unsafe-argument errors without it, while the wrapper refuses fast with the recovery text); corpus 93 → 103 Specs, `ready` 57 → 66 | | S4 | readiness sweep + re-audits (± the `05` deletion) | orchestrator-verified green gate over the regenerated Design Review | planned | | S5 | adversarial review, remediation, full close, done-record | full chain + clean-clone; review archived | planned | From 5f0a00f1cac99fb60d232e09183c942b794d44bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 20:06:15 +0200 Subject: [PATCH 08/16] docs(plans): record the S4 readiness sweep and the three re-audits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweep dispositioned all 37 `defined` Specs per-Spec: zero promotions and 37 named refusals. Not one of them carries `has-verifier`, so every promotion would have introduced an `honesty/gaps` warning and failed the promotion law's second clause — and no verifier was invented to enable one. All 37 clear the `ready` floor structurally, so the refusals are about missing evidence, never missing structure. Two reasons moved: `spec:consumers.design-review` no longer refuses on tranche grounds (the view wave built exactly that world) but on evidence — its children verify themselves, never their parent — and `spec:validation.validator-self-testing` was re-judged and upheld, its only mechanical verifier still being one that would police the delivery process. The `05` re-audit closed all three of its recorded gaps — the lower floor rungs and the per-kind evidence table, the derived-readiness banner, and validator self-testing all now stand on Specs — and surfaced two new gap rows the earlier table never decomposed: the per-team severity override for the informative signals and the team-overridable floor config, both designed-for deferrals carried by no Spec, registry, code surface, or surviving doc. That is the class the earlier audit itself recorded as gaps, so `05` stays, with the residue and a deletion-cost inventory recorded rather than a verdict stretched to force a deletion. `06` and `07` re-graded on the same pass: the banner, view-label, wholesale rewrite, and diagnostic rendering rows are now carried, six of the twelve recorded gaps close, and both docs stay with the rest of their ledgers intact. One drift note recorded and deliberately not repaired: `07` §6 ④ quotes three binding lines where the view renders four. Records-only — no product surface changed; 103 specs · 1 pack · 75 anchors → 179 nodes · 351 edges, `ready` 66 / `defined` 37, zero errors and zero warnings. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- plans/21-self-hosting-phase-4.md | 197 ++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 2 deletions(-) diff --git a/plans/21-self-hosting-phase-4.md b/plans/21-self-hosting-phase-4.md index 1e38028..2c521f6 100644 --- a/plans/21-self-hosting-phase-4.md +++ b/plans/21-self-hosting-phase-4.md @@ -252,13 +252,165 @@ separate example id namespace. Rows close only with reasons in the done-record. named reasons. 5. **The `05` disposition is audit-grounded**: deleted only on a fully-carried per-doc audit with ruling 14's sweep at zero hits, or kept with its residue recorded; `06`/`07` re-graded - with honest ledgers. + with honest ledgers. *(Met at S4 — §5a: `05` **stays** on two newly-surfaced gap rows, both + recorded precisely; `06` and `07` re-graded, six of the twelve phase-3 gaps closed.)* 6. **The gate holds throughout**: `npm run check` green at every blessed commit; the close runs the full chain plus the clean-clone proof. 7. **Records continue**: the session ledger, watch items, and docket rows are terminal or carried with reasons; the adversarial review is archived with every finding dispositioned before close. +## §5a The S4 re-audits (`05` re-run · `06` and `07` re-graded) + +Built to the phase-3 template (plan 20 §7, the per-doc concept-dissolution audits) and judged over +the **regenerated** Design Review — `npm run build && npm run generate:self-hosting`, then the +carrying specs' `generated/design-review/spec/*.md` pages read directly, because the criterion is +what the graph carries, not what a raw spec file says. The governing criterion is unchanged (the +dissolution decision, `spec:decisions.concept-docs-dissolve`): a doc may be deleted only once its +semantic contract is **fully** carried by Specs and lean registries; **one gap blocks deletion**; +an `expository-only` row never blocks, provided its law is carried elsewhere — a Spec, a lean +registry, a pinned code+test surface, or a **surviving** doc, named in the row. + +**Terminal dispositions this session:** `05` **stays — two gaps recorded** · `06` **stays — gaps +reduced** · `07` **stays — gaps reduced**. No doc was deleted, so ruling 14's re-pointing and +two-form reference sweep did not run; what a future `05` deletion will cost is inventoried at the +end of the `05` table. + +### `05-validation-and-honesty.md` — re-run verdict: **gaps** · stays + +The three gaps the phase-3 audit recorded against `05` are **all closed** by the S2 and S3 waves. +Two *new* gap rows surface on this pass — both are parenthetical named deferrals that the phase-3 +table never had a row for (they sit in prose the earlier audit did not decompose), and both fall +in the class the phase-3 audit itself graded as gaps elsewhere (`06` §8's per-PR hosted preview, +gap 9: a designed-for deferral named only in the candidate doc). They are recorded rather than +stretched into `carried`, per the D-2/D-3 lesson. + +| Doc section | Carrying surface (spec / registry / code / surviving doc) | Verdict | +|---|---|---| +| header ¶ — validation makes the graph trustworthy; the meta-model is a conformance contract, instances conform, conformance is checked but the process is never workflow-gated | `spec:validation.two-check-families` (both families; errors fail the build, gaps and orphans inform) · `spec:protocol.self-hosting` · `CONTEXT.md` "the Protocol" (the conformance contract instances conform to) | carried | +| header ¶ — delineates the MVP validator subset from the aspirational tiers, realising P7 · P8 · L2 · L3 | `01` P7/P8/L2/L3 (surviving) | expository-only against `01` | +| the guardrail blockquote — (a) checks police conformance & honesty, never content-quality and never workflow; (b) "deterministically validated," never "provably correct" | `spec:validation.two-check-families` (intent outcome states both halves of (a)) · `CONTEXT.md` §Validation & honesty closing lines (both halves verbatim) · `AGENTS.md` | carried | +| §1 the two check families table (conformance asks well-formedness · honesty asks non-pretending) | `spec:validation.two-check-families` + its `split-report` point · `CONTEXT.md` "conformance checks" / "honesty checks" | carried | +| §1 an error fails the build, a gap informs; "checked, never gated"; the `validator` · `gap` · `orphan` terms | `spec:validation.two-check-families` (rule 2) · `spec:validation.warn-level-signals` + `orphan-signal` and `ready-gap-signal` · `CONTEXT.md` term rows | carried | +| §1 "the honesty family is the real differentiator" | — | expository-only (motivation) | +| §1 the layered-enforcement table, CORE rows (types → shape · schema → payload · graph validators → cross-file invariants) | `spec:validation.two-check-families` (rule 3, no layer substitutes for another) · `spec:decisions.typing-law` | carried | +| §1 the layered-enforcement table, ASPIRATIONAL rows (architecture rules · custom team `defineRule` policy) | `00` §4 (Architecture enforcement — forbidden-dependency tiers, ts-arch tests, custom rules) · `07` §3 item 7 (surviving) | expository-only against `00`/`07` | +| §1 "types describe shape; validators decide completeness (P7); completeness is never encoded in conditional types" | `01` P7 (surviving, verbatim) · `spec:decisions.typing-law` (the closed-shape half) | expository-only against `01` | +| §2 the one-validation-path ¶ — no pre-graph seam; validating an evaluated form checks a phantom; `sdp validate` is `sdp build` + checks and `validateGraph` is the sole seam; authoring-time feedback is per-carrier; the `sdp/spec-static` lint is earlier surfacing, never a parallel path | `spec:decisions.one-validation-path` (context, decision, rationale, consequence — all four halves) · `spec:validation.two-check-families` (rule 4) · `spec:extraction.build-pipeline` (every command uses the same extracted graph and validation seam) · `04` §1 (surviving, the lint) | carried | +| §2 checks 1–2 (referential integrity with did-you-mean · duplicate IDs) | `spec:validation.referential-integrity` + `dangling-target` and `did-you-mean` · `spec:validation.duplicate-ids` + `dual-carrier` | carried | +| §2 check 3 (`claim` separation, endpoint contracts, fail-closed descriptors, the kind-typed endpoints) | `spec:validation.claim-separation` + `collapsed-edge-claim` and `unratified-descriptor` · `spec:model.relations` (the per-relation endpoint kinds in its vocabulary) | carried | +| §2 check 4 (`verifies` linkage; a wrong-kind verifier confers nothing rather than failing) | `spec:validation.verification-linkage` + `unbound-example` and `unresolved-oracle` ("a non-resolving trace is named loudly and confers no delivery fact") | carried | +| §2 checks 5–6 (authoring-shape honesty · derived-facts honesty, including `observed`; the gap check reads recomputed facts) | `spec:validation.authored-honesty` + `section-authored-fact` and `unearned-stated-fact` · `src/validate/validators.ts` `checkGaps` (reads the recomputed facts, pinned by the two points) | carried | +| §2 check 7 (honest readiness — a stated rung is checked against the floor) | `spec:validation.readiness-floor` (**all four rungs now in authored words**, cumulative evaluation stated) | carried | +| §2 check 8 (orphan detection) | `spec:validation.warn-level-signals` + `orphan-signal` · `CONTEXT.md` "`orphan`" | carried | +| §2 check 8 parenthetical — **a per-team severity override is designed-for, deferred** | none — no Spec, registry, code+test surface, or surviving doc names this deferral; `00` §4 and `07` §2/§3 do not list it | **gap** (new — recorded as gap 13) | +| §2 check 9 (readiness/delivery gaps; the backlog and drift-alarm queries) | `spec:validation.warn-level-signals` + `ready-gap-signal` · `CONTEXT.md` "The payoff queries" · `spec:model.core-model` | carried | +| §2 ambiguity fails (L2) | `spec:validation.duplicate-ids` + its point · `spec:validation.claim-separation` · `01` L2 (surviving) | carried | +| §2 partial failure stays local (L3) | `spec:carrier.markdown-parser` (excludes one malformed carrier, continues healthy siblings) · `spec:extraction.determinism` | carried | +| §3 opening ¶ — a readiness floor is the minimum structural requirement to *state* a rung; a floor to clear, never a quota or a score | `spec:validation.readiness-floor` · `CONTEXT.md` "readiness floor" · `01` P4 corollary (surviving, the no-tier-filling half) | carried | +| §3 opening ¶ tail — the floors are the mechanism, the thresholds are a Representation, and **a team-overridable floor config is designed-for, deferred** | none for the deferral — the mechanism/threshold split rides the `AGENTS.md` Principle-vs-Representation convention, but no surface states the overridable-config deferral | **gap** (new — recorded as gap 14) | +| §3 the floor's two parts (kind-blind structural clauses + one kind-conditional evidence clause, which can relax as well as add) | `spec:validation.readiness-floor` ("Every clause stated here is kind-blind. The two evidence clauses are the one kind-conditional place…") · `spec:decisions.kind-conditional-floor` | carried | +| §3 the kind-blind clause table — the `idea` / `scoped` / `defined` / `ready` rungs | `spec:validation.readiness-floor` — five `idea` clauses, three `scoped`, two `defined`, three `ready`, plus cumulative evaluation, the anchors-present reading, and the authored-edges-only reading, all in authored words | carried — **phase-3 gap 1 (first half) closed at S2** | +| §3 the per-kind evidence table (7 kinds × `scoped`/`defined`) | `spec:validation.kind-evidence` + `constraints-alone`, `untargeted-constraint`, `empty-promoted-child` — row for row, including the shared `behavior`/`workflow`/`contract` family and the contract row's named deferral | carried — **phase-3 gap 1 (second half) closed at S2** | +| §3 the three bounding laws (monotonic · promotion-neutral incl. the MD-16 bound · convergence is honest) | `spec:validation.kind-evidence` (the promoted-evidence bound in law; the three bounds attributed to their decisions) · `spec:decisions.kind-conditional-floor` · `spec:decisions.carried-evidence` | carried | +| §3 the MD-13 representation note (the code table is the clause set's own source of truth; the two are mirror images) | `spec:validation.readiness-floor` ("the code-level source of truth and the realizing entrypoint… one law read twice, so any disagreement is drift to resolve on one side, never a second floor") · `spec:validation.kind-evidence` (the same posture for the table) · `docs/concept/DECISIONS.md` MD-13 · `src/validate/readiness-floor.ts` | carried | +| §3 `ready` is earned, not asserted, and is not a delivery fact; the floor may require anchors to *resolve*, never to exist; higher floors degrade gracefully | `spec:validation.readiness-floor` (the anchor clause reads the bindings that are present — the floor never demands a binding an author has not made) · `spec:decisions.binding-not-liveness` | carried | +| §3 `ready` is the floor plus a human's `declared` statement; no review fact is stored; where approval matters the signed git tag is the artifact and RBAC stays outside the model | `spec:consumers.design-review` (rule 3) · `spec:consumers.projections-model` (the *baseline* term: a named approved snapshot whose signed git tag is the approval artifact, approval outside the authored model) · `CONTEXT.md` · `00` §5 (surviving, the RBAC non-goal) | carried | +| §3 the stated-vs-derived blockquote — both ship; rendered beside, never overwriting; the banner fires only in the dishonest direction and names the first unmet clause | `spec:consumers.derived-readiness-banner` + `dishonest-divergence` and `honest-headroom` (all four rules, plus the below-`idea` case the doc never states) · `CONTEXT.md` "derived readiness" | carried — **phase-3 gap 2 closed at S3** | +| §3 the verb note — readiness is *stated/asserted*, never "claimed" ("claim" is reserved for the taxonomy) | `CONTEXT.md` "Locked usage" and the `readiness` term row's aliases-to-avoid | carried | +| §4 pack coherence, not member completeness; no duplicated-intent check; a Pack states no truth of its own | `spec:validation.pack-coherence` + `incoherent-aggregate` · `spec:model.pack-aggregate` (Pack · framing · membership · modelRefs) · `spec:validation.two-check-families` (never judges content quality) | carried | +| §5 validator self-testing (should-fail and should-pass evidence per validator; cheap insurance) | `spec:validation.validator-self-testing` — both directions, why the should-fail half is the load-bearing one, the cheap-by-construction line, and the standing refusal to make it a check. The dissolution criterion asks that the law be **carried by a Spec**, not that the Spec be `ready`; this one honestly stays `defined` (§8) | carried — **phase-3 gap 5 closed at S3** | +| §6 aspirational tiers (architecture enforcement · custom team rules · NFR-to-`observed` · `--lenient` · incremental builds/caching) | `00` §4 (architecture enforcement, incremental builds/caching, runtime observations incl. `nfr-violated`) · `07` §2 (the `--lenient` ratchet, in the ASPIRATIONAL map) · `07` §3 item 7 (custom `defineRule`) — all five named on surviving docs; the design-time NFR half is `spec:validation.kind-evidence`'s constraint row (a machine-readable target to state `defined`), and the cache bound is `spec:extraction.regenerability` | expository-only against `00`/`07` | +| §7 what CI guarantees at MVP | the §2 rows above · `spec:extraction.determinism` (`--check-clean`) | expository-only (summary) | + +**Residue — exactly what keeps `05` alive.** Two rows, both narrow, both cheap to carry next phase: + +13. **The per-team severity override** for the informative signals (`05` §2 check 8) — designed-for + and deferred, named nowhere else. Natural carrier: one clause on + `spec:validation.warn-level-signals`, or a `00` §4 / `07` §3 cut-list row. +14. **The team-overridable floor config** (`05` §3 opening ¶) — the thresholds are a Representation + and a per-team floor config is designed-for and deferred; named nowhere else. Natural carrier: + one clause on `spec:validation.readiness-floor` beside the MD-13 posture it already states. + +Both are *deferrals*, not laws in force, which is precisely the class the phase-3 audit recorded as +gaps (per-PR hosted preview · `bySymbol`'s frozen-shape status · the harness/evidence half of +`07` §4). Grading them `carried` would repeat the D-7 mistake of conceding an uncarried surface +inside a `carried` row; grading them away as prose would repeat D-2/D-3. Neither was rushed into a +Spec this session: S4 authors no corpus law (that was S2/S3's work), and the dissolution decision +forbids bundling the carrying change with the deletion anyway. + +**Deletion-cost inventory (recorded so the next attempt is one session, not two).** When the two +rows above are carried, a `05` deletion must re-point, in the same change: `CONTEXT.md`'s +"Validation & honesty (→ `05`)" section pointer · `docs/concept/DECISIONS.md` (MD-13 and MD-9 cite +`05` §3 as one of two mirrors — re-point at `spec:validation.readiness-floor` and +`spec:validation.kind-evidence`) · `docs/concept/README.md` (2 hits) · `06` (2) · `07` (2) · `01` +(1) · `jtbd-stories/01`, `/04`, `/05`, `/07` · `examples/checkout-v1/README.md` · +`src/validate/validators.ts` (13 hits — the per-check `05 §2` provenance comments) · +`src/validate/readiness-floor.ts` (5, including the header's "mirroring `05` §3 row-for-row") · +`src/validate/contracts.ts` · `src/reader/reader.ts` · `src/projections/design-review.ts` · +`src/model/sections.ts` · `src/extract/reify.ts` · `test/readiness.test.ts` · +`test/extract.test.ts` · `test/fixtures/graph-validator.fixtures.ts` · and **two pinned quotes** in +`check-carrier-truth.mjs` (the one-validation-path claim at its `file:` row, and the +`STILL_SUPPORTED` classification row pinning "the type system's job in the TS carrier"), which +must be re-pointed at the carrying Spec rather than deleted — the `check-prose-schema.mjs` +precedent from the `02`/`03` deletions. Then ruling 14's two-form sweep (backticked/path forms and +the bare `05 §` form) to zero hits outside `plans/`, `reviews/`, `explorations/`. + +### `06-consumers-and-projections.md` — re-grade: **gaps** · stays + +Only the rows whose verdict moved are restated; every other row of the phase-3 table stands as +written. + +| Doc section | Phase-3 verdict | Carrying surface now | Re-graded | +|---|---|---|---| +| §5 the per-spec field list — the stated-vs-derived readiness divergence banner | **gap** | `spec:consumers.derived-readiness-banner` + `dishonest-divergence` and `honest-headroom`; the rest of the field list (header · intent and behaviour · relations · bindings with source links · verification status · impact list · `claim` cues) stands on `spec:consumers.design-review` rules 1 and 4 and `spec:consumers.binding-language-views` | **carried** | +| §5 pages rewritten wholesale each run so no stale page survives | **gap** | `spec:consumers.wholesale-view-rewrite` + `stale-page-removed` — the temp-sibling/remove/one-rename sequence, the failed-run removal, the up-front invalidation in `runBuild`, and the `--check-clean` double render | **carried** | + +**`06`'s surviving gaps** (6 rows): the impact graph's two assist roles (§2) · `bySymbol`'s +frozen-shape-but-aspirational status (§3) · the discipline ≈ kind/section mapping (§6) · the +disciplines × phases × iterations distribution chart (§6) · the per-PR hosted preview (§8) · the +Mermaid and reference-projection rows of the §1 taxonomy. All six are named out of scope by this +plan's §(c); `06` stays. + +### `07-mvp-roadmap-and-open-questions.md` — re-grade: **gaps** · stays + +| Doc section | Phase-3 verdict | Carrying surface now | Re-graded | +|---|---|---|---| +| §6 ① the one diagnostic rendering rule (location from the finding's structured fields; first contact fails clean) | **gap** | `spec:validation.diagnostic-rendering` + `composed-location` — one currency, the composed `path:line — [severity] validatorId — message` order, and both degradations | **carried** | +| §6 ③ the derived-readiness banner ships in the Design Review | **gap** | `spec:consumers.derived-readiness-banner` + its two points (the shared row with `05` §3 and `06` §5) | **carried** | +| §6 ④ `implemented` is a UI hazard — the fact name stays, views render binding language | **gap** | `spec:consumers.binding-language-views` + `bound-spec-page`; `spec:decisions.binding-not-liveness` keeps the model half | **carried** | +| §4 derived-readiness banner timing · impact-graph depth (both "resolved") | expository-only against `05` / `06` | unchanged — both mirrors survive, and the banner half now also stands on a Spec | expository-only (unchanged) | + +**Drift note recorded, deliberately not repaired here.** `07` §6 ④ quotes the rendered binding +language as three lines ("Implementation binding … / Verifier binding … / Runtime observation …"). +The view renders **four** — the expected-outcome oracle line landed with the oracle work — and +`spec:consumers.binding-language-views` states four. The doc's quote is illustrative rather than +false, and §2 S4 scopes this session's `06`/`07` work to the gap ledgers and "nothing more", so the +stale enumeration is recorded for the successor instead of edited under an audit-only session. + +**`07`'s surviving gaps** (3 rows): inline-vs-centralized anchor semantics (§4, open) · when +harnesses / evidence become CORE (§4, open, the non-Gherkin half) · the measure-what-hurts +prioritization heuristic (§5). All three are named out of scope by §(c); `07` stays. + +### The twelve phase-3 gaps, re-stated + +| # | Gap | State after S4 | +|---|---|---| +| 1 | the readiness-floor clause tables (lower rungs + per-kind evidence) | **closed** — S2, `spec:validation.readiness-floor` (enriched) + `spec:validation.kind-evidence` | +| 2 | the derived-readiness banner (one direction · first unmet clause) | **closed** — S3, `spec:consumers.derived-readiness-banner` | +| 3 | the `implemented` view-label rule | **closed** — S3, `spec:consumers.binding-language-views` | +| 4 | the one diagnostic rendering rule | **closed** — S3, `spec:validation.diagnostic-rendering` | +| 5 | validator self-testing | **closed** — S3, `spec:validation.validator-self-testing` (carried at `defined`) | +| 6 | Design Review's wholesale page rewrite | **closed** — S3, `spec:consumers.wholesale-view-rewrite` | +| 7 | discipline ≈ kind/section mapping · the distribution chart (`06` §6) | stands — out of this phase's scope | +| 8 | the impact graph's two assist roles · `bySymbol`'s status (`06` §2/§3) | stands — out of scope | +| 9 | the per-PR hosted preview (`06` §8) | stands — out of scope | +| 10 | inline-vs-centralized anchor semantics · when harnesses/evidence become CORE (`07` §4) | stands — out of scope | +| 11 | measure-what-hurts (`07` §5) | stands — out of scope | +| 12 | the Mermaid and reference-projection surfaces (`06` §1/§8) | stands — out of scope | +| 13 | **new** — the per-team severity override for informative signals (`05` §2 check 8) | open — surfaced by the S4 re-audit; blocks `05` | +| 14 | **new** — the team-overridable floor config (`05` §3) | open — surfaced by the S4 re-audit; blocks `05` | + ## §6 Done-record *(written at close)* @@ -319,6 +471,47 @@ would be decorative — the S2 precedent, applied to the view surface. Closing distribution: **`ready: 66 / defined: 37` over 103** (93 → 103 Specs · 70 → 75 anchors · 164 → 179 nodes · 317 → 351 edges), zero errors and zero warnings over the regenerated graph. +**S4 — the readiness sweep.** Every one of the 37 Specs standing at `defined` when S4 opened, +dispositioned per-Spec under ruling 5. **Zero promotions, 37 honest refusals**, and the reason is +uniform at the mechanical level: *not one* of the 37 carries `has-verifier` in the regenerated +graph, so every promotion would introduce an `honesty/gaps` warning and fail ruling 5's clause (b). +Every one of them *does* clear the `ready` floor structurally (each page reads "structural floor +reached: `ready`"), so the refusals are about missing evidence, never about missing structure — +and inventing a verifier to enable a promotion is exactly what the ruling forbids. S4 adds no bound +points by design (that was S2/S3's work), so the per-Spec column below records *why no verifier is +the honest state for that Spec*, which is the judgment ruling 5 actually asks for. + +Checked specifically, as the session's charter required: **no Spec whose verifier landed in S2 or +S3 is still sitting at `defined`.** All eleven Specs the two waves bound (the two floor carriers, +the four view carriers, and their example children) were stated `ready` at authoring in their own +wave; `has-verifier` is direct and never transitive, so the S3 points confer nothing on the +parents those Specs refine — which is why `spec:consumers.design-review` and its family still +stand at `defined` on evidence grounds rather than on tranche grounds. + +| Spec | Disposition | Reason | +|---|---|---| +| `spec:protocol.self-hosting` | refuse | **phase-3 refusal stands verbatim**: the epic states whole-pipeline rules; no cheap verifier exists and promotion would add a gap warning | +| `spec:carrier.markdown-authoring` | refuse | **phase-3 refusal stands verbatim**: parent of four `ready` children; the executable path lives on them, not on it | +| `spec:consumers.agent-surface` | refuse | **phase-3 refusal stands verbatim**: two of its rules are measured-evidence claims, not runtime laws | +| `spec:consumers.reader` | refuse | **phase-3 refusal stands verbatim**: its verifier world is a full graph fixture — no cheap point, and S3's projections suite renders views rather than exercising the reader's entry adapters | +| `spec:consumers.design-review` | refuse — **reason updated** | the phase-3 reason ("projection rendering is outside this tranche") **no longer stands**: S3 built exactly that world. It stays `defined` on evidence, not on tranche — its own law (renders in context · pure projection · never a gate · deterministic Markdown · the escaping rule) has no point of its own, and the three S3 children that do carry points verify *themselves*, never their parent. Named as the strongest candidate for the next corpus wave | +| `spec:consumers.projections-model` | refuse | **phase-3 refusal stands verbatim**: vocabulary; its measured-curation terms are recorded evidence, not runtime law. Now also the parent of the view family whose points sit two hops away | +| `spec:consumers.edit-model` | refuse | **phase-3 refusal stands verbatim**: it states in its own words that it has no entrypoint and no verifier — the one Spec whose `defined` is stated by its own content | +| `spec:extraction.build-pipeline` | refuse | **phase-3 refusal stands verbatim**: the ordered flow's world is the CLI pipeline — a named out-of-scope giant | +| `spec:extraction.regenerability` | refuse | **phase-3 refusal stands verbatim**: its law is the clean-room rebuild, which is the phase close's proof, not a cheap point | +| `spec:extraction.claim-taxonomy` | refuse | **phase-3 refusal stands verbatim**: vocabulary; its clauses are exercised through `spec:validation.claim-separation`'s `ready` points, which verify that Spec and not this one | +| `spec:model.core-model` | refuse | **phase-3 refusal stands verbatim**: refused on ruling 1 — `test/descriptors.test.ts` is list equality, not a law | +| `spec:model.spec-sections` | refuse | **phase-3 refusal stands verbatim**: same reading as `core-model`; the section-name list is an assertion, not an example space | +| `spec:model.relations` | refuse | **phase-3 refusal stands verbatim**: vocabulary; relation grammar is exercised by referential-integrity's points | +| `spec:model.pack-aggregate` | refuse | **phase-3 refusal stands verbatim**: vocabulary; the pack law is carried executably by `spec:validation.pack-coherence` | +| `spec:model.protocol-domain` | refuse | **phase-3 refusal stands verbatim**: a four-term vocabulary with no runtime law to bind | +| `spec:validation.validator-self-testing` | refuse — **S3 refusal re-judged and upheld** | re-judged this session against the S3 projections suite: still no honest verifier. Its law is evidence discipline *over* the validators, so any mechanical check would read the test corpus for should-fail/should-pass pairs — policing the delivery process, which the standing guardrail forbids and which the Spec itself states as a non-goal. A point that merely re-ran an existing validator test would verify that validator, not this discipline. Honest `defined` with acceptance-grade content | +| 21 × `spec:decisions.*` (`adopt-the-nouns` · `agent-surface-scripts-graph` · `binding-not-liveness` · `carried-evidence` · `carrier-ruling` · `concept-docs-dissolve` · `content-only-sections` · `envelope-grammar-posture` · `exclusion-contract` · `executable-meta-model` · `kind-conditional-floor` · `mcp-deferred` · `one-primitive` · `one-validation-path` · `pack-reified` · `plain-language-references` · `point-per-example` · `prose-ownership` · `protocol-naming` · `sdp-ts-extension` · `typing-law`) | refuse ×21 | **phase-3 refusal stands verbatim**: a Decision Record's truth is a ratified choice, not a runtime behavior — no verifier exists and none was invented. The S2 precedent reconfirms it: `spec:decisions.kind-conditional-floor` gained a citing Spec *and* three bound points downstream this phase and still stays `defined`, because a Spec citing a decision does not mature the decision | + +Closing distribution: unchanged at **`ready: 66 / defined: 37` over 103** — 179 nodes · 351 edges, +zero errors and zero warnings. No spec file, oracle transcription, or histogram literal moved, +because nothing was promoted. + ## §9 Session and gate ledger Sessions execute sequentially; each closes with a green twelve-leg gate, a regenerated Design @@ -330,7 +523,7 @@ ledger is git process evidence, never graph content. | S1 | the oracle split (§2 S1) | orchestrator-verified green gate | done — 21 `it()`s over one hoisted extraction; the frozen expectation moved to ten authored modules under `test/self-hosting-oracle/` (seven family files, pack manifest, declared relations, anchors) plus their aggregating index; zero assertion loss (every one of the 27 original `expect` sites survives, 5 added: three oracle-length cross-checks and the two-assertion "no Spec outside the families" law), the node-id roster derived from the authored arrays per ruling 10; counts unchanged at 87/1/65 · 153 · 294 · ready 51 / defined 36 | | S2 | shared constant + floor wave | orchestrator-verified green gate | done — `contract-dependent-suites.mjs` now states the per-tree rows once and both `vitest-test.mjs` and `eslint.config.js` read it (clean-room proof: lint passes with `generated/contracts` moved aside, the wrapper still fails fast with the same recovery text); the floor wave carried the `idea`/`scoped`/`defined` rungs into `spec:validation.readiness-floor` and the per-kind table into the new `spec:validation.kind-evidence`, with 5 bound points each mutation-probed red for the clause it names; corpus 87 → 93 Specs, `ready` 51 → 57 | | S3 | view wave + seventh bound suite | orchestrator-verified green gate | done — five laws carried (banner · view-label · wholesale rewrite · diagnostic rendering · validator self-testing), ten Specs added, five bound points in the new `test/self-hosting-projections.test.ts`, each mutation-probed red for the law it names; the suite entered the shared constant once and both surfaces followed (clean-room proof: with `generated/contracts` moved aside, lint passes with the row and fails with five unsafe-argument errors without it, while the wrapper refuses fast with the recovery text); corpus 93 → 103 Specs, `ready` 57 → 66 | -| S4 | readiness sweep + re-audits (± the `05` deletion) | orchestrator-verified green gate over the regenerated Design Review | planned | +| S4 | readiness sweep + re-audits (± the `05` deletion) | orchestrator-verified green gate over the regenerated Design Review | done — the sweep dispositioned all 37 `defined` Specs with zero promotions and 37 named refusals (§8): none carries `has-verifier`, so every promotion would have added an `honesty/gaps` warning, and no verifier was invented to enable one. The `05` re-audit closed all three of its phase-3 gaps (S2's floor wave, S3's banner and validator-self-testing carriers) and surfaced **two new gap rows** — the per-team severity override and the team-overridable floor config, both designed-for deferrals named nowhere else — so **`05` stays** and its residue plus a deletion-cost inventory are recorded (§5a). `06` and `07` re-graded: six of the twelve phase-3 gaps closed, both docs stay. Records-only session — no product surface changed, graph numbers unmoved at 103/1/75 · 179 · 351 · `ready` 66 / `defined` 37 | | S5 | adversarial review, remediation, full close, done-record | full chain + clean-clone; review archived | planned | Owner ratification of every gate above happens at the phase PR review; no live owner From e7a4a039ec6eeb9619b970c430f73b29f1d84c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 20:33:28 +0200 Subject: [PATCH 09/16] docs(reviews): archive the phase-4 pre-close adversarial review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mutation testing over the ten new bound points, an independently designed set of twenty-two mutations, and a from-scratch recomputation of every headline number. All ten points go red for the law they name, and the discrimination is measured rather than asserted: seventeen of the eighteen point-directed mutations kill exactly one point, the eighteenth kills the two whose Specs name the same floor clause. The oracle split is probed too — a corrupted family module reddens exactly that family's it(), a deleted oracle entry is caught by the length cross-checks rather than certifying itself, and the count literals move independently of the authored arrays. One major, on records honesty rather than verifier honesty: the indirect `then` key was fixed once, silently regressed hours later inside the same remediation cluster, and then re-verified as intact by a later phase. Two recorded verifications are therefore false. The sandbox proves the full gate green with every occurrence normalized. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- ...0-self-hosting-phase-4-pre-close-review.md | 439 ++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 reviews/10-self-hosting-phase-4-pre-close-review.md diff --git a/reviews/10-self-hosting-phase-4-pre-close-review.md b/reviews/10-self-hosting-phase-4-pre-close-review.md new file mode 100644 index 0000000..7dbba14 --- /dev/null +++ b/reviews/10-self-hosting-phase-4-pre-close-review.md @@ -0,0 +1,439 @@ +# 10 - Self-hosting phase-4 pre-close adversarial review + +**Reviewed:** the full `main...feature/protocol-self-application-phase-4` diff at `5f0a00f` +(8 commits, 36 files, +6328 / −3738), against `plans/21-self-hosting-phase-4.md` — its §1 +engineering rulings (10–14, plus 1–9 carried from plan 20) and its §5 acceptance criteria are this +review's yardstick. + +**Method:** mutation testing, not code reading — the phase-3 precedent, with an independently +designed mutation set. A full sandbox copy of the working tree was built outside the repository +(`/private/tmp/.../scratchpad/sandbox`), its engine broken one law at a time, and the ten new bound +points re-run after each. **Twenty-two mutations** were run in total: eighteen against the ten +points (M1–M18) and eight against the oracle split (O1–O8, run separately). None replays a mutation +plan 21 §7 recorded; every one was designed against the source, not the ledger. A point counts as +honest only if it goes **red** when the law it names is removed — and only if the *other* nine stay +green under the same mutation (the P-1 discrimination lesson, ruling 12). Every claim marked +**CONFIRMED** was reproduced by a probe against the built product. The real checkout was never +mutated; `git status` was clean at start. + +**Disposition:** the disposition column at the foot is terminal — every finding carries fixed / +declined-with-reason / carried-with-reason, landed on this branch before the phase PR. +(`reviews/` is Prettier-ignored and temporal-guard-exempt.) + +--- + +## Verdict + +**SOUND TO CLOSE. NO BLOCKER. ONE MAJOR, FIXED ON THE BRANCH.** + +**All ten new bound points are provably mutation-sensitive to the law they name**, and the +discrimination is near-perfect: of the eighteen mutations aimed at the points, seventeen killed +**exactly one** point and the eighteenth killed exactly two — the two whose Spec text names the same +floor clause. No point is a params echo; every Then step reads real engine output through a public +seam. The oracle split holds: a corrupted family module reddens exactly that family's `it()`, a +deleted oracle entry is caught by the length cross-checks rather than certifying itself, and the +count literals move independently of the authored arrays. Every headline number in the plan — +specs, anchors, nodes, edges, the histogram, the 37-row sweep, the assertion reconciliation — +recomputes exactly. The `05` deletion-blocking verdict survives an independent grep. + +The one **major** is not a verifier defect and not a phase-4 invention: it is the **indirect `then` +key**, a defect review-06 raised, plan 17 recorded as *fixed*, and plan 18 re-verified as *intact* — +which had in fact silently regressed hours after the fix and has now been propagated to 66 sites, +43 of them authored on this branch. Two recorded verifications across two plans are therefore false. +The sandbox proves the whole twelve-leg gate green with every occurrence normalized, so the fix is +mechanical; it is landed here, and the falsified records are corrected. + +Acceptance criterion 5 grades **PASS with `05` staying** — the criterion asks for an audit-grounded +disposition, not a deletion, and the two blocking rows (gaps 13/14) are real: an independent sweep +finds them named nowhere but in `05` itself. + +--- + +## Dimension 1 — The mutation matrix (verifier honesty) + +### The ten points + +| Point | Suite | Killing mutation(s) | Survived a should-kill? | +|---|---|---|---| +| `readiness-floor.unrelated-scoped-spec` | validators | M1 | no | +| `readiness-floor.blocking-open-question` | validators | M2 | no | +| `kind-evidence.constraints-alone` | validators | M3 | no | +| `kind-evidence.untargeted-constraint` | validators | M4 | no | +| `kind-evidence.empty-promoted-child` | validators | M5 | no | +| `derived-readiness-banner.dishonest-divergence` | projections | M2, M7 | no | +| `derived-readiness-banner.honest-headroom` | projections | M6 | no | +| `binding-language-views.bound-spec-page` | projections | M8, M14, M17 | no | +| `wholesale-view-rewrite.stale-page-removed` | projections | M9c, M9d | **partly — see P-1** | +| `diagnostic-rendering.composed-location` | projections | M10, M11, M12, M13 | no | + +### The mutations, and what each killed + +| # | Mutation | Killed | +|---|---|---| +| M1 | `hasAtLeastOneRelation` → always true | `unrelated-scoped-spec` only | +| M2 | `hasNoBlockingOpenQuestions` → ignores the `blocking` flag | `blocking-open-question` **and** `dishonest-divergence` | +| M3 | behavior-family `defined` cell accepts constraints alone | `constraints-alone` only | +| M4 | `constraintTargetsAreMachineReadable` → target requirement dropped | `untargeted-constraint` only | +| M5 | `hasPromotedRuleOrExampleEvidence` → child's own evidence no longer required (MD-16 bound deleted) | `empty-promoted-child` only | +| M6 | `renderReadiness`: `derivedRank < statedRank` → `!==` (banner fires both ways) | `honest-headroom` only | +| M7 | `renderReadiness`: the first-unmet-clause suffix suppressed | `dishonest-divergence` only | +| M8 | bindings block renders `- implemented:` as the label | `bound-spec-page` only | +| M9a | `runView` writes in place — temp sibling and swap deleted | **nothing** | +| M9b | `runBuild`'s up-front view invalidation deleted | **nothing** | +| M9c | both no-stale-page sites deleted | `stale-page-removed` only | +| M9d | `runView` copies instead of renaming, leaving the temp sibling | `stale-page-removed` only | +| M10 | `formatFinding`: the ` — ` separator changed to `: ` | `composed-location` only | +| M11 | `formatFinding`: an absent location renders a `` placeholder | `composed-location` only | +| M12 | `formatFinding`: the line dropped from the composed location | `composed-location` only | +| M13 | `formatFinding`: the location rendered twice | `composed-location` only | +| M14 | the `Runtime observation` line dropped from the bindings block | `bound-spec-page` only | +| M15 | index-table binding **column headers** renamed | nothing (the point reads cell values, not headers — correct) | +| M16 | pack member table binding cells → `yes`/`no` | **nothing** — see P-2 | +| M17 | index-row binding cells → `yes`/`no` | `bound-spec-page` only | +| M18 | (= M16, re-run isolated) | **nothing** — see P-2 | + +**What this rules out.** No Then step is satisfiable by the world factory alone. Three assertions +that *looked* like they might be tautologies were probed specifically and are not: +`composed-location`'s "the composed prefix is the only place the path appears" +(`indexOf === lastIndexOf`) dies under M13; `honest-headroom`'s `bannerRaised: false` is an absence +assertion but is paired with a positive statement of the rendered rung pair and dies under M6; and +`bound-spec-page`'s `factNameRendered: false` dies under M8, i.e. it is the renderer's vocabulary +being read, not the probe's. + +**M2's double kill is correct, not leakage.** `dishonest-divergence`'s world builds its divergence +out of a blocking open question, so the clause `no-blocking-open-questions` is genuinely the law +both points stand on; the banner point additionally dies alone under M7, which is the half only it +names. + +### P-1 (MINOR) — the wholesale-rewrite point cannot see either site alone + +**CONFIRMED** by M9a / M9b / M9c / M9d. + +The no-stale-page law is realized twice — `runBuild` removes any existing view up front, `runView` +writes to `generated/design-review.tmp` and renames it into place — and the point survives the +deletion of *either* one (M9a, M9b) and dies only when both go (M9c). Plan 21 §7 discloses this +honestly and the Spec states both sites, so this is **not** the P-1 shape of review-09: there the +second gate realized a *different* law (vocabulary resolution) and the point had no teeth on the law +it named; here both gates realize the *same* law, and the point does die when the law is removed. + +The residue worth recording precisely is narrower than the §7 row says, and I measured it: + +- `stale-page-removed` **does** discriminate the swap mechanism in one direction — M9d (rename + replaced by a copy, temp left behind) kills it through the `temporarySurvives: false` step. +- It does **not** discriminate the swap mechanism's *absence*: `temporarySurvives: false` is + satisfied both when the temp ran and was cleaned up and when no temp exists at all (M9a). An + absence assertion that goes vacuous exactly when its mechanism is deleted has no teeth on + deletion — ruling 12's own lesson, applied to a step the ledger did not call out. +- Three of the Spec's seven rule lines have **no** verifier at all: the "no half-written view is + ever readable" reading of the one-rename clause, the failed-run removal, and the `--check-clean` + double render with its refusal. + +Nothing here is false; the §7 row is simply less precise than the measurement supports. + +### P-2 (MINOR) — the binding-language point covers the index table, not the pack member table + +**CONFIRMED** by M16/M17/M18. `spec:consumers.binding-language-views` rule 5 states that *"the pack +member table and the index table carry the same two binding columns, with the same present and none +values."* The point's probe graph holds no Pack, so only the index half is exercised: changing the +index row's cells to `yes`/`no` kills the point (M17); making the identical change to the pack +member table's cells does not (M16/M18). Plan §7's row is honest — it says *"the index row repeating +them"* — but the Spec's rule is broader than the point, and the residue was not stated. + +--- + +## Dimension 2 — Records honesty (recomputed from scratch) + +Every number below was recomputed against `generated/graph.json`, the spec files on disk, and git — +never read back from a ledger. + +| Claim | Recorded | Measured | Verdict | +|---|---|---|---| +| corpus counts | 103 specs · 1 pack · 75 anchors | 103 `Primitive` · 1 `Pack` · 45 `Anchor` + 30 `CodeNode` = 75 | ✓ | +| graph size | 179 nodes · 351 edges | 179 · 351 | ✓ | +| histogram | `ready: 66 / defined: 37` | `{ ready: 66, defined: 37 }` | ✓ | +| spec files on disk | 103 | 103 `.sdp.md` (+ the one `.sdp.ts` Pack carrier) | ✓ | +| every `ready` Spec carries `has-verifier` | asserted | 66/66, zero exceptions | ✓ | +| §8's uniform refusal reason ("*not one* of the 37 carries `has-verifier`") | asserted | 37/37 carry none | ✓ | +| §8's "each page reads structural floor reached: `ready`" | asserted | 37/37 pages, zero exceptions | ✓ | +| §8 sweep completeness | 37 dispositioned | 16 individual rows + the 21-decision row = 37; **zero `defined` Specs missing from the table**; zero promotions | ✓ | +| new bound points | 10 | validators 12→17, projections 0→5 | ✓ | +| total bound points | (implied 39) | 3+1+8+4+1+17+5 = 39 across 7 root suites | ✓ | +| S1 assertion reconciliation | 27 `expect` sites → 32 | `test/self-hosting-graph.test.ts` 27 → 32 | ✓ | +| no assertion deleted anywhere under `test/` | asserted | 1313 → 1342 `expect(` sites; per-file deltas are +5 oracle, +20 projections, +4 validators, **zero negative** | ✓ | +| oracle `it()` count | 21 | 21 executed | ✓ | +| the pre-split oracle | one `describe`/one `it()` at 3593–3886, 3,888 lines, `expectedSpecs` 87, `expectedAnchors` 65 | exact on `main` | ✓ | +| schema version | `0.4.0` | `0.4.0` | ✓ | + +**The orchestrator's brief cites an S1 reconciliation of "283→294".** No metric on this branch +takes those values: the recorded and measured reconciliation is **27 → 32** expect sites in the +oracle suite (1313 → 1342 across all of `test/`). `294` is the phase-3 **edge** count, which plan 21 +§2 uses as the S1 invariant; the brief appears to conflate the two. Recorded here so the conflation +does not propagate. + +**The §5a audit tables.** Ten `carried` rows were re-judged against the regenerated Design Review +pages rather than the raw spec files, including all six rows S4 re-graded from `gap`: + +| Row | Carrier page checked | Verdict | +|---|---|---| +| `05` §2 check 7 — honest readiness | `validation.readiness-floor.md` — four rungs in authored words, cumulative evaluation stated | carried ✓ | +| `05` §3 kind-blind clause table | same page, clause for clause against `readinessFloors` | carried ✓ | +| `05` §3 per-kind evidence table | `validation.kind-evidence.md` — all seven rows, the shared behavior family, the contract interim | carried ✓ | +| `05` §3 stated-vs-derived blockquote (**re-graded**) | `consumers.derived-readiness-banner.md` — one direction, first unmet clause, below-`idea` case | carried ✓ | +| `05` §5 validator self-testing (**re-graded**) | `validation.validator-self-testing.md` — both directions, stated `defined`, floor reached `ready` | carried ✓ | +| `05` §3 approval / baseline | `consumers.projections-model.md` term row, verbatim match | carried ✓ | +| `05` §2 L3 partial failure | `carrier.markdown-parser.md` — "excludes one malformed carrier while continuing healthy siblings" | carried ✓ | +| `05` §2 check 3 endpoint kinds | `model.relations.md` vocabulary rows | carried ✓ | +| `06` §5 wholesale rewrite (**re-graded**) | `consumers.wholesale-view-rewrite.md` — all five clauses present | carried ✓ | +| `07` §6 ① / ④ (**re-graded**) | `validation.diagnostic-rendering.md`, `consumers.binding-language-views.md` | carried ✓ | + +### A-1 (MINOR) — one `carried` row still rests a clause on a code surface + +**Where:** `plans/21-self-hosting-phase-4.md` §5a, the `05` §2 checks 5–6 row. **CONFIRMED.** + +The row grades `carried` and cites *"`spec:validation.authored-honesty` + `section-authored-fact` +and `unearned-stated-fact` · `src/validate/validators.ts` `checkGaps` (reads the recomputed facts, +pinned by the two points)."* The Spec carries check 6's main clause — *"any stated delivery facts +must equal the graph's recomputed facts"* — but the doc's coupling sentence, *"The gap check (9) +reads the recomputed facts, so a faked fact never silences it,"* is stated by **no Spec**. It is +true of the code (`checkGaps` reads its `derivedFacts` argument, never `node.deliveryFacts`), and +`spec:validation.warn-level-signals` — whose realizing entrypoint *is* `checkGaps` — does not say it. + +That is review-09's D-2 shape: a deletion-authorizing `carried` verdict discharging an +intended-truth clause onto `src/`. `05` stays, so no deletion rests on it and the exposure is +bounded — but the phase-3 remediation closed exactly this class by enrichment rather than by +argument, and the same fix is one line here. + +### R-1 (MINOR) — §(b) undercounts the phase-3 bound suites + +Plan §(b) opens *"29 bound points across five bound suites."* The point count is exact; the suite +count is **six** — on `main`, `bindExample` appears in `self-hosting-carrier` (3), +`self-hosting-duplicate-ids` (1), `self-hosting-extraction` (8), `self-hosting-model` (4), +`self-hosting-sdp-import` (1) and `self-hosting-validators` (12) — which is also exactly the six +paths the pre-branch root row of the contract dependency table listed. The projections suite is +therefore the **seventh**, as §3 correctly says elsewhere in the same plan. **CONFIRMED.** + +### R-2 (MINOR) — §2 S1's description of the pre-split roster + +§2 S1 describes *"a redundant 151-item node-id roster."* The roster on `main` was an **88-item** +literal (the pack id plus the 87 spec ids) whose anchor half was **already** derived by +`...expectedAnchors.map(...)`. The redundancy ruling 10 removed was therefore 87 duplicated spec +ids, not 151 items, and the total the roster compared against was 153. **CONFIRMED.** + +--- + +## Dimension 3 — Spec quality (read word-for-word against the mirrors) + +All twelve new/enriched Specs were read against `src/validate/readiness-floor.ts`, +`renderReadiness` / `renderBindings` / `renderFindings` in +`src/projections/design-review-context.ts`, the index and member tables in +`src/projections/design-review-pages.ts`, `runView` / `runBuild`, and `formatFinding`. + +**What held — ruling 13 was respected.** No invented third behaviour was found in eleven of the +twelve. Specifically probed and confirmed true of the engine: + +- The floor Spec's rung arithmetic is exact: five `idea` clauses, three `scoped`, two `defined`, + three `ready`, cumulative, kind-blind except the two evidence clauses — matching `readinessFloors` + entry for entry and `05` §3's table row for row. +- The evidence Spec's seven rows match `kindEvidence` cell for cell, including the shared + `behavior`/`workflow`/`contract` family, the MD-16 promoted-evidence bound (a promoted child must + clear *its own kind's* `scoped` cell; a `constrainedBy` edge must resolve to a `constraint` Spec + carrying its constraints), and the contract row's named deferral. +- The banner Spec's *"the index and the pack member table carry the same pair as two columns"* is + true: both tables render `| Stated | Floor reached |` (`design-review-pages.ts:68` and `:125`). +- The binding-language Spec's *"four labelled lines"* and *"Runtime observation always reads not + tracked"* are exact, and M14 proves the fourth line is bound. +- The wholesale-rewrite Spec's *"findings never withhold the view"* is true: `runValidate` returns + the graph with a non-zero exit code, so `runView` writes the current view and returns 1. +- The diagnostic Spec's *"a finding's location … never baked into its message text"* survives a + probe: over every finding the example corpus produces, no message contains its own `file` value, + and no validator message template interpolates a file path (the two near-misses interpolate a + *section* path and a set of *output* paths, neither of which is the finding's location). +- Vocabulary is clean against `CONTEXT.md`: the *derived readiness* row (*"the highest rung whose + floor clauses pass … rendered beside the stated rung, never overwriting the author's + statement"*) is what the banner Spec states, near-verbatim. No residual pre-ratification term + appears in any of the twelve. +- Temporal-guard hygiene: an independent sweep of every branch-added file under `specs/` and + `test/`, using a **wider** token set than the guard's own pattern (adding bare `S1`-style + handles, `phase N`, and month names), returns **zero** hits. + +### S-1 (MINOR, declined) — one deliberate code behaviour the floor Spec does not state + +`dependsOnAndRefinesTargetsAreDefined` deliberately **skips** an unresolved target +(`src/validate/readiness-floor.ts:274-288`, with a comment saying so), so an unresolved `refines` +target is attributed to `all-relations-resolve` alone rather than failing two clauses. The Spec +states the clause without that attribution rule. Read literally the Spec is *stricter* than the +code — but the observable outcome is identical (the Spec fails `ready` either way) and the banner +names the *first* unmet clause, which is `all-relations-resolve` in both readings. Nothing false is +stated; only a clause-attribution nuance is unstated. + +### S-2 (MINOR, carried) — a second report shape lives in the file the diagnostic Spec names + +`spec:validation.diagnostic-rendering` states *"no surface introduces a parallel report shape of its +own"* and names `src/cli/output.ts` as a realizing entrypoint. That file declares and exports +`RenderedFinding`, a second finding shape — used by nothing in `src/`, `test/`, or `examples/` +except `formatFinding`'s own union parameter, and not re-exported from the barrel. It is dead +internal surface rather than a live parallel path, so the Spec's claim is not falsified in +substance; but the one file the Spec points a reader at is the one file that declares a second +shape. + +--- + +## Dimension 4 — The standing curiosities + +### T-1 (MAJOR) — the indirect `then` key: a fixed defect that silently regressed, and two records that say otherwise + +**CONFIRMED** by git archaeology, an exhaustive guard search, and a full-gate sandbox probe. + +**What it is.** The frozen GWT result key is assembled indirectly rather than written — 66 sites: +`src/extract/markdown-body-owner-behavior.ts:21` (`const resultKey = ["t", "hen"].join("")`), +`test/markdown-reifier.test.ts:502`, `test/extract.test.ts:705`, and 63 occurrences of +`[["t", "hen"].join("")]:` across the six oracle transcription modules. (A fourth spelling, +`["t" + "hen", …]`, sits in `test/import-emit-markdown.test.ts`.) + +**The history — this is the finding.** review-06 raised it: *"the `then` graph key is built as +`["t","hen"].join("")` in three production files with no explanatory comment — no configured lint or +guard requires it; it reads as guard evasion and hides the frozen `given/when/then` key set from +grep."* It was then: + +1. **Fixed** — `cd735ae` *"refactor(extract): name the then key directly"* replaced both product + sites with a plain `then:` key. +2. **Recorded as fixed** — `plans/17-self-hosting-v1.md:456`: *"Indirect assembly of the `then` + graph key | fixed-by-remediation | `cd735ae` names the key directly."* +3. **Silently regressed nine hours later** — `fcd5cef` *"fix(extract): land the grammar-hardening + cluster (review-06)"*, part of the *same* remediation cluster, reintroduced it as + `const resultKey = ["t", "hen"].join("")` and routed both call sites back through it. +4. **Re-verified as intact against the regressed file** — `plans/18-self-hosting-phase-2.md:338`: + *"Indirect assembly of the `then` graph key (review-06) | Verify remediation remains intact | + verified — phase-1 remediation names the `then` key directly."* That sentence was false when + written. +5. **Propagated by this phase** — S1 moved 43 of the occurrences into the new + `test/self-hosting-oracle/` modules and S2/S3 authored fresh ones for the new Specs, so the + branch is the largest single contributor to the site count. + +**Does anything require it?** No. Exhaustively checked: no ESLint rule or plugin (the config has no +custom plugins and no rule that could see an object key), no `npm run check` leg, none of +`check-temporal.mjs` / `check-carrier-truth.mjs` / `check-carrier-rule.mjs` / +`check-prose-schema.mjs` / `check-self-hosting-gates.mjs` (none contains the token at all), and +nothing scans `test/` for GWT-shaped object literals — the extractor reifies only `.sdp.ts` and +`.sdp.md` carriers, so a test-file object literal is invisible to it. The product itself is already +inconsistent: `src/extract/reify.ts:618` and `src/extract/serialize.ts:64` write `"then"` plainly. + +**Probe.** In the sandbox, all 66 occurrences were normalized (`[["t","hen"].join("")]:` → `then:`, +`const resultKey = ["t","hen"].join("")` → `const resultKey = "then"`) and the **full twelve-leg +`npm run check` ran green end to end** — 103 specs · 1 pack · 75 anchors → 179 nodes · 351 edges, +0 errors / 0 warnings, 589 tests, clean preflight. All four unwired audit scripts also pass both +ways. The only cost is one Prettier re-wrap in `test/self-hosting-oracle/extraction.ts`, because +the shorter key lets a literal fit on one line. + +**Recommendation: normalize.** Keep-with-comment would institutionalize an evasion channel nobody +can name a reason for; leave-with-reason is unavailable because there is no reason. Normalizing also +makes the frozen `given/when/then` key set greppable again — the concrete harm review-06 named — and +makes plan 17's and plan 18's records true rather than aspirational. + +### C-1 (informational) — the two-site wholesale rewrite is stated honestly + +`spec:consumers.wholesale-view-rewrite` states both realizing sites explicitly — *"The invalidation +happens before rendering as well as after it: the build the run passes through removes any existing +view up front"* and *"The realizing entrypoint is `runView` … with the up-front invalidation in +`runBuild`."* Verified against `src/cli/build-command.ts:76-80` and +`src/cli/validate-view-command.ts:86-95`. The Spec does not overclaim; the verifier residue is +P-1's, not the Spec's. + +### C-2 (informational) — gaps 13/14 survive an independent check + +An independent repository-wide sweep for any surface carrying either deferral — +`per-team severity`, `severity override`, `overridable floor`, `floor config`, `team-overridable`, +`configurable floor`, `per-team threshold`, over every tracked `.md` / `.ts` / `.mjs` outside +`generated/` — returns hits in **exactly two places**: `docs/concept/05` itself (lines 61 and 73, +the two parentheticals) and `plans/21` (the audit rows recording them). No Spec, no registry, no +code+test surface, no surviving doc names either deferral; `00` §4 and `07` §2/§3 do not list them. +**The `05`-stays verdict is correct and the deletion is properly blocked.** + +--- + +## Dimension 5 — The oracle split and the shared constant + +### The split (O1–O8, run against `test/self-hosting-graph.test.ts`'s 21 `it()`s) + +| # | Corruption | Reddened | +|---|---|---| +| O1 | one descriptor wrong in `oracle/model.ts` | **exactly one** — *carries the authored descriptors of the model family* | +| O2 | one spec id misspelled in `oracle/consumers.ts` | the consumers family, the roster, and *leaves no authored Spec outside the families* — the identity assertions, by design | +| O3 | one spec **entry deleted** from `oracle/model.ts` | the totals cross-check, the roster, the model family, and the no-escape law | +| O4 | the frozen `specs: 103` literal bumped | *holds the frozen corpus totals* only | +| O5 | the histogram literal bumped | *holds the frozen stated-readiness distribution* only | +| O6 | one anchor label wrong in `oracle/anchors.ts` | *projects every anchor and code node at the line its declaration occupies* only | +| O7 | one anchor **entry deleted** from `oracle/anchors.ts` | the totals cross-check plus the three anchor/roster laws | +| O8 | one declared relation deleted | *derives exactly the authored declared relations* only | + +**Ruling 10 holds under fire.** Family isolation is real (O1, O6, O8 each redden exactly one `it()`, +so the first failure no longer masks the rest). The oracle cannot certify itself: deleting an entry +from a family module (O3, O7) is caught by `expect(expectedSpecs).toHaveLength(103)` and +`expect(expectedAnchors).toHaveLength(75)` measured against the same frozen literals the graph is +measured against — the "cross-check" ruling 10 required is load-bearing, not decorative. The +histogram stayed an explicit literal (O5) and the count literals move independently of the arrays +(O4). + +### The shared constant (ruling 11) — the negative control reproduced + +Plan §3's watch-item claim was re-run from scratch in the sandbox, not taken on trust: + +- **With** the `test/self-hosting-projections.test.ts` row and `generated/contracts` moved aside: + `eslint .` **passes**. +- **Without** the row, same clean room: `eslint .` fails with **exactly five + `@typescript-eslint/no-unsafe-argument` errors**, one per `bindExample` call in the projections + suite (lines 190, 199, 345, 430, 516) — the recorded number, exactly. +- **With** the row and the tree missing, the wrapper refuses fast: + *"Generated contracts required by the selected test suite are missing. Run `npm run build && npm + run generate:self-hosting` first."* +- **Without** the row, the wrapper stops refusing and spawns vitest straight into the missing tree. + +Both surfaces are load-bearing on both sides. **CONFIRMED.** + +--- + +## Acceptance criteria (§5) + +| # | Criterion | Verdict | Evidence | +|---|---|---|---| +| 1 | Oracle split, zero assertion loss | **PASS** | 21 `it()`s over one hoisted extraction; ten authored transcription modules; 27 → 32 expect sites with zero deletions anywhere under `test/`; family isolation and the length cross-checks proved by O1–O8; docket row dispositioned at S1. | +| 2 | One source of truth for contract-dependent suites | **PASS** | Both consumers import `contract-dependent-suites.mjs`; the seventh suite entered through one edit; clean-room lint green with the row, five errors without it (reproduced). | +| 3 | Executable-path facts, not claims | **PASS** | 66/66 `ready` Specs carry `has-verifier`; 0 errors; `--check-clean` clean on both trees; **all ten new points mutation-probed red for the law they name**, with the discrimination measured rather than asserted. | +| 4 | Honest readiness | **PASS** | 0 warnings — no `honesty/gaps` finding exists; closing distribution `ready: 66 / defined: 37` matches disk; the 37 refusals are complete and each names a reason. | +| 5 | The `05` disposition is audit-grounded | **PASS** | `05` **stays** on two precisely recorded rows; independently verified that neither deferral is named anywhere else (C-2); `06`/`07` re-graded with honest ledgers; ruling 14's sweep correctly did not run. One row's citation needed correction (A-1). | +| 6 | The gate holds throughout | **PASS** | Full twelve-leg chain green at the close commit, plus the clean-clone proof. | +| 7 | Records continue | **PASS** | This review is archived with every finding terminal; ledgers, watch items and docket rows dispositioned at the close. | + +--- + +## Disposition table + +| # | Severity | Finding | Disposition | +|---|---|---|---| +| T-1 | **major** | The indirect `then` key: fixed by `cd735ae`, silently regressed by `fcd5cef`, recorded as fixed in plan 17 and re-verified as intact in plan 18 (false when written), now at 66 sites of which 43 were authored on this branch. No lint rule, gate leg, audit script, or scanner requires it; the full check is green with it normalized. | **FIXED** — every occurrence normalized to a plain `then` key across the one product file and seven test/oracle files; the sandbox result reproduced on the branch under the full twelve-leg gate and the clean clone. Plan 21 §6 records that the two prior verifications were false and that this close makes them true. | +| A-1 | minor | The `05` §2 checks 5–6 audit row grades `carried` while resting *"the gap check reads the recomputed facts"* on `src/validate/validators.ts` — the review-09 D-2 shape under a deletion-authorizing verdict. | **FIXED BY ENRICHMENT** — `spec:validation.warn-level-signals` gains the clause in authored words (ratified: it is `05` §2 check 6's own sentence), the oracle transcription follows, and the §5a row's citation is corrected to name the Spec first. | +| P-1 | minor | `stale-page-removed` survives the deletion of either realizing site alone; `temporarySurvives: false` goes vacuous exactly when the swap mechanism is deleted; three of the Spec's rule lines have no verifier. | **RECORD SHARPENED** — plan 21 §7's row now states the measured mutation classes (which single-site deletions survive, which kill) and names the three unverified clauses, instead of the looser sentence. No verifier invented; the law is genuinely two-site by design. | +| P-2 | minor | `bound-spec-page` exercises the index table but not the pack member table, though the Spec's rule names both. | **RECORD SHARPENED** — plan 21 §7's row now names the pack-member half as the residue, with the mutation that proves it (M16/M18 survive, M17 kills). | +| S-1 | minor | The floor Spec does not state the deliberate clause attribution for an unresolved `refines`/`dependsOn` target. | **DECLINED WITH REASON** — the Spec states nothing false; the observable outcome and the banner's named clause are identical under both readings, so adding the nuance would state a mechanism rather than a law. Recorded here rather than authored into the corpus. | +| S-2 | minor | `RenderedFinding` is a second declared report shape inside `src/cli/output.ts`, the file the diagnostic Spec names as an entrypoint. | **CARRIED WITH REASON** — dead internal surface with no producer and no barrel export, so no parallel path exists in substance; deleting an internal type is engine hygiene outside a review-and-close session's charter. Carried on the docket. | +| R-1 | minor | §(b) says phase 3 closed at "five bound suites"; six carried bound points. | **FIXED** — §(b) reads six, consistent with §3's "seventh suite". | +| R-2 | minor | §2 S1 describes "a redundant 151-item node-id roster"; the literal roster held 88 ids with the anchor half already derived. | **FIXED** — §2 S1 restated to the measured shape. | +| C-1 | info | The two-site wholesale rewrite is stated honestly by the Spec. | **NO ACTION** — verified, recorded. | +| C-2 | info | Gaps 13/14 confirmed by an independent sweep; the `05`-stays verdict survives. | **NO ACTION** — verified, recorded. | + +--- + +## What the owner is asked to ratify at the PR + +Three things this review deliberately leaves to the owner rather than deciding by plan ruling: + +1. **`05` stays.** Two designed-for deferrals — a per-team severity override and a team-overridable + floor config — block its deletion. Both are cheap to carry next phase; the deletion-cost + inventory is already written (§5a). +2. **The `then`-key normalization touches product code.** One line in + `src/extract/markdown-body-owner-behavior.ts` plus 65 test/oracle sites. It is byte-neutral at + runtime and gate-proven, but it reverses a shape that has survived three phases. +3. **`spec:validation.validator-self-testing` ships at `defined` with no verifier** and is + nevertheless graded `carried` by the `05` audit. That reading — the dissolution criterion asks + that a law be *carried by a Spec*, not that the Spec be `ready` — is correct on the decision's + own terms and is the first time the corpus leans on it. From ee8ee560502e9353b904dfac7bfece85c20cb699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 20:33:45 +0200 Subject: [PATCH 10/16] refactor(extract,tests): name the frozen then key directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GWT result key was assembled indirectly — `["t", "hen"].join("")` and `"t" + "hen"` — at 66 sites across one product file and eight test files. Review-06 called it guard evasion that hides the frozen given/when/then key set from grep; the phase-1 remediation named the key directly, and the grammar-hardening commit in the same cluster reintroduced it hours later. The later phase's docket then recorded the remediation as intact, which it was not. Nothing requires the indirection. No ESLint rule or plugin can see an object key, no gate leg reads it, none of the five check scripts contains the token, and nothing scans test files for gwt-shaped object literals — the extractor reifies only `.sdp.ts` and `.sdp.md` carriers. The product was already inconsistent: reify.ts and serialize.ts write the key plainly. Every occurrence is now the plain key. Byte-neutral at runtime; the graph, the oracle, and the derived contracts are unchanged. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- src/extract/markdown-body-owner-behavior.ts | 6 ++--- test/extract.test.ts | 2 +- test/import-emit-markdown.test.ts | 4 +-- test/markdown-reifier.test.ts | 5 ++-- test/self-hosting-oracle/carrier.ts | 14 +++++------ test/self-hosting-oracle/consumers.ts | 14 +++++------ test/self-hosting-oracle/extraction.ts | 28 +++++++++------------ test/self-hosting-oracle/model.ts | 12 ++++----- 8 files changed, 39 insertions(+), 46 deletions(-) diff --git a/src/extract/markdown-body-owner-behavior.ts b/src/extract/markdown-body-owner-behavior.ts index 8b3fe91..9686c8a 100644 --- a/src/extract/markdown-body-owner-behavior.ts +++ b/src/extract/markdown-body-owner-behavior.ts @@ -18,8 +18,6 @@ interface OpenQuestion { readonly blocking: boolean; } -const resultKey = ["t", "hen"].join(""); - function isOpenQuestionArray(value: unknown): value is readonly OpenQuestion[] { return ( Array.isArray(value) && @@ -67,7 +65,7 @@ export function mapIntent( const example: Record = { given: fence.steps.given, when: fence.steps.when, - [resultKey]: fence.steps.result, + then: fence.steps.result, }; const behavior = isRecord(target.behavior) ? target.behavior : {}; const examples: unknown[] = []; @@ -189,7 +187,7 @@ export function mapExampleSpace( const vocabulary: Record = { given: fence.steps.given, when: fence.steps.when, - [resultKey]: fence.steps.result, + then: fence.steps.result, }; section.exampleSpace = vocabulary; } diff --git a/test/extract.test.ts b/test/extract.test.ts index ffa8829..906b8e2 100644 --- a/test/extract.test.ts +++ b/test/extract.test.ts @@ -702,7 +702,7 @@ describe("Markdown carrier discovery", () => { exampleSpace: { given: ["a TS-carrier spec"], when: ["importTypeScriptSpec runs"], - [["t", "hen"].join("")]: ["the emitted Markdown re-parses to an equal graph"], + then: ["the emitted Markdown re-parses to an equal graph"], }, }, }, diff --git a/test/import-emit-markdown.test.ts b/test/import-emit-markdown.test.ts index 5c3ef17..6fe2da8 100644 --- a/test/import-emit-markdown.test.ts +++ b/test/import-emit-markdown.test.ts @@ -141,7 +141,7 @@ relations: {} exampleSpace: Object.fromEntries([ ["given", ["a cart has {count:number} items"]], ["when", ["the customer submits the cart"]], - ["t" + "hen", ["an order has {total:number}"]], + ["then", ["an order has {total:number}"]], ]), }, }); @@ -153,7 +153,7 @@ relations: {} Object.fromEntries([ ["given", ["a cart has {count: 0} items"]], ["when", ["the customer submits the cart"]], - ["t" + "hen", ["an order has {total: 0}"]], + ["then", ["an order has {total: 0}"]], ]), ], }, diff --git a/test/markdown-reifier.test.ts b/test/markdown-reifier.test.ts index 6361a7d..6447553 100644 --- a/test/markdown-reifier.test.ts +++ b/test/markdown-reifier.test.ts @@ -499,7 +499,6 @@ Behavior description. }); it("maps the ruled fences and every remaining typed owner", () => { - const resultKey = ["t", "hen"].join(""); const result = reify( carrierBody( `# Example carrier @@ -563,11 +562,11 @@ Then an order is created ui: { emptyState: "Explain the next action." }, }); expect(result.specs[0]?.data).toHaveProperty( - ["behavior", "examples", 0, resultKey], + ["behavior", "examples", 0, "then"], ["an order is created"], ); expect(result.specs[0]?.data).toHaveProperty( - ["behavior", "exampleSpace", resultKey], + ["behavior", "exampleSpace", "then"], ["an order is created"], ); }); diff --git a/test/self-hosting-oracle/carrier.ts b/test/self-hosting-oracle/carrier.ts index cf41b10..8526bdc 100644 --- a/test/self-hosting-oracle/carrier.ts +++ b/test/self-hosting-oracle/carrier.ts @@ -74,7 +74,7 @@ export const carrierSpecs = [ exampleSpace: { given: ["the paired carrier probes named {probe:string}"], when: ["both carriers reify their probe"], - [["t", "hen"].join("")]: [ + then: [ "both carriers report the finding class {findingId:string}", 'the TypeScript carrier reports severity {typeScriptSeverity:"warning"|"error"} and extracts {typeScriptSpecs:number} specs', 'the Markdown carrier reports severity {markdownSeverity:"warning"|"error"} and extracts {markdownSpecs:number} specs', @@ -108,7 +108,7 @@ export const carrierSpecs = [ { given: ['the paired carrier probes named {probe: "unrecognized-property"}'], when: ["both carriers reify their probe"], - [["t", "hen"].join("")]: [ + then: [ 'both carriers report the finding class {findingId: "extract/unrecognized-property"}', 'the TypeScript carrier reports severity {typeScriptSeverity: "warning"} and extracts {typeScriptSpecs: 1} specs', 'the Markdown carrier reports severity {markdownSeverity: "error"} and extracts {markdownSpecs: 0} specs', @@ -147,7 +147,7 @@ export const carrierSpecs = [ exampleSpace: { given: ["a TS-carrier spec"], when: ["importTypeScriptSpec runs"], - [["t", "hen"].join("")]: ["the emitted Markdown re-parses to an equal graph"], + then: ["the emitted Markdown re-parses to an equal graph"], }, }, }, @@ -170,7 +170,7 @@ export const carrierSpecs = [ { given: ["a TS-carrier spec"], when: ["importTypeScriptSpec runs"], - [["t", "hen"].join("")]: ["the emitted Markdown re-parses to an equal graph"], + then: ["the emitted Markdown re-parses to an equal graph"], }, ], }, @@ -231,7 +231,7 @@ export const carrierSpecs = [ exampleSpace: { given: ["the step text {stepText:string}"], when: ["the notation parses the step text"], - [["t", "hen"].join("")]: [ + then: [ "the notation finds {slotCount:number} slot groups", 'the first group has the form {form:"bare"|"typed"|"bound"|"malformed"} and the name {slotName:string}', "the step skeleton is {skeleton:string}", @@ -258,7 +258,7 @@ export const carrierSpecs = [ { given: ['the step text {stepText: "a cart with {n:number} line items"}'], when: ["the notation parses the step text"], - [["t", "hen"].join("")]: [ + then: [ "the notation finds {slotCount: 1} slot groups", 'the first group has the form {form: "typed"} and the name {slotName: "n"}', 'the step skeleton is {skeleton: "a cart with {n} line items"}', @@ -287,7 +287,7 @@ export const carrierSpecs = [ { given: ['the step text {stepText: "a stray { then {n: maybe} line items"}'], when: ["the notation parses the step text"], - [["t", "hen"].join("")]: [ + then: [ "the notation finds {slotCount: 1} slot groups", 'the first group has the form {form: "malformed"} and the name {slotName: "n"}', 'the step skeleton is {skeleton: "a stray { then {n} line items"}', diff --git a/test/self-hosting-oracle/consumers.ts b/test/self-hosting-oracle/consumers.ts index 256efc0..a6645cd 100644 --- a/test/self-hosting-oracle/consumers.ts +++ b/test/self-hosting-oracle/consumers.ts @@ -169,7 +169,7 @@ export const consumersSpecs = [ 'the spec {structure:"clears every floor clause"|"records a blocking open question"}', ], when: ["the Design Review renders the graph"], - [["t", "hen"].join("")]: [ + then: [ 'the spec page renders the floor reached {floorReached:"scoped"|"ready"}', "the divergence banner is raised: {bannerRaised:boolean}", "the banner names the first unmet clause {clauseId:string}", @@ -200,7 +200,7 @@ export const consumersSpecs = [ 'the spec {structure: "records a blocking open question"}', ], when: ["the Design Review renders the graph"], - [["t", "hen"].join("")]: [ + then: [ 'the spec page renders the floor reached {floorReached: "scoped"}', "the divergence banner is raised: {bannerRaised: true}", 'the banner names the first unmet clause {clauseId: "no-blocking-open-questions"}', @@ -232,7 +232,7 @@ export const consumersSpecs = [ 'the spec {structure: "clears every floor clause"}', ], when: ["the Design Review renders the graph"], - [["t", "hen"].join("")]: [ + then: [ 'the spec page renders the floor reached {floorReached: "ready"}', "the divergence banner is raised: {bannerRaised: false}", ], @@ -270,7 +270,7 @@ export const consumersSpecs = [ 'the graph holds a spec {specId:string} bound by {bindings:"an implementing code anchor and a verifying test anchor"|"no anchor at all"}', ], when: ["the Design Review renders the graph"], - [["t", "hen"].join("")]: [ + then: [ 'the spec page renders the implementation binding as {implementation:"present"|"none"}', 'the spec page renders the verifier binding as {verifier:"present"|"none"}', "the spec page renders the runtime observation as {observation:string}", @@ -302,7 +302,7 @@ export const consumersSpecs = [ 'the graph holds a spec {specId: "spec:probe.bound-surface"} bound by {bindings: "an implementing code anchor and a verifying test anchor"}', ], when: ["the Design Review renders the graph"], - [["t", "hen"].join("")]: [ + then: [ 'the spec page renders the implementation binding as {implementation: "present"}', 'the spec page renders the verifier binding as {verifier: "present"}', 'the spec page renders the runtime observation as {observation: "not tracked"}', @@ -343,7 +343,7 @@ export const consumersSpecs = [ "an extraction root holding {corpus:string} and a stale view page {stalePage:string}", ], when: ["the view is rendered at that root"], - [["t", "hen"].join("")]: [ + then: [ "the run exits {exitCode:number}", "the view holds the current page {currentPage:string}", "the stale page survives: {staleSurvives:boolean}", @@ -374,7 +374,7 @@ export const consumersSpecs = [ 'an extraction root holding {corpus: "one authored spec"} and a stale view page {stalePage: "spec/probe.departed.md"}', ], when: ["the view is rendered at that root"], - [["t", "hen"].join("")]: [ + then: [ "the run exits {exitCode: 0}", 'the view holds the current page {currentPage: "index.md"}', "the stale page survives: {staleSurvives: false}", diff --git a/test/self-hosting-oracle/extraction.ts b/test/self-hosting-oracle/extraction.ts index 31f2f44..fbaa287 100644 --- a/test/self-hosting-oracle/extraction.ts +++ b/test/self-hosting-oracle/extraction.ts @@ -81,7 +81,7 @@ export const extractionSpecs = [ "the consumer supplies the exclusion {exclusion:string}", ], when: ["the root is discovered"], - [["t", "hen"].join("")]: [ + then: [ 'the discovery attempt {outcome:"completes"|"is refused"}', "the surviving spec carrier is {specCarrier:string} and the surviving anchor candidate is {anchorCandidate:string}", "the refusal states {diagnostic:string} and names the offending path", @@ -111,7 +111,7 @@ export const extractionSpecs = [ 'the consumer supplies the exclusion {exclusion: "foo"}', ], when: ["the root is discovered"], - [["t", "hen"].join("")]: [ + then: [ 'the discovery attempt {outcome: "completes"}', 'the surviving spec carrier is {specCarrier: "foobar/included.sdp.ts"} and the surviving anchor candidate is {anchorCandidate: "foobar/helper.ts"}', ], @@ -142,7 +142,7 @@ export const extractionSpecs = [ 'the consumer supplies the exclusion {exclusion: "C:/work/specs"}', ], when: ["the root is discovered"], - [["t", "hen"].join("")]: [ + then: [ 'the discovery attempt {outcome: "is refused"}', 'the refusal states {diagnostic: "normalizeExcludes: invalid exclusion path"} and names the offending path', ], @@ -229,9 +229,7 @@ export const extractionSpecs = [ exampleSpace: { given: ["a graph derived from the authored spec {specId:string}"], when: ["the graph payload is serialized"], - [["t", "hen"].join("")]: [ - "the payload declares the schema version {schemaVersion:string}", - ], + then: ["the payload declares the schema version {schemaVersion:string}"], }, }, }, @@ -256,9 +254,7 @@ export const extractionSpecs = [ 'a graph derived from the authored spec {specId: "spec:probe.schema-versioning"}', ], when: ["the graph payload is serialized"], - [["t", "hen"].join("")]: [ - 'the payload declares the schema version {schemaVersion: "0.4.0"}', - ], + then: ['the payload declares the schema version {schemaVersion: "0.4.0"}'], }, ], }, @@ -300,7 +296,7 @@ export const extractionSpecs = [ "a case-twin example {twinId:string} whose contract path differs only by letter case", ], when: ["the contracts are generated from the derived graph"], - [["t", "hen"].join("")]: [ + then: [ "the generated tree holds {fileCount:number} files", "the step contract for the example is emitted: {emitted:boolean}", "the findings name {findingId:string}", @@ -331,7 +327,7 @@ export const extractionSpecs = [ 'a refining example {exampleId: "spec:probe.create-order.unbound"} whose used step {binding: "leaves unbound"} that slot', ], when: ["the contracts are generated from the derived graph"], - [["t", "hen"].join("")]: [ + then: [ "the generated tree holds {fileCount: 0} files", "the step contract for the example is emitted: {emitted: false}", ], @@ -363,7 +359,7 @@ export const extractionSpecs = [ "the example carries {entryCount: 2} structured entries", ], when: ["the contracts are generated from the derived graph"], - [["t", "hen"].join("")]: [ + then: [ "the step contract for the example is emitted: {emitted: true}", 'the findings name {findingId: "contracts/multi-entry-example"}', ], @@ -395,7 +391,7 @@ export const extractionSpecs = [ 'a case-twin example {twinId: "spec:probe.create-order.same-Case"} whose contract path differs only by letter case', ], when: ["the contracts are generated from the derived graph"], - [["t", "hen"].join("")]: [ + then: [ "the generated tree holds {fileCount: 0} files", 'the findings name {findingId: "contracts/case-colliding-path"}', ], @@ -437,7 +433,7 @@ export const extractionSpecs = [ 'the handler bound to the {failingPhase:"given"|"when"|"then"} step throws {thrown:string}', ], when: ["the bound plan runs against a fresh world"], - [["t", "hen"].join("")]: [ + then: [ "the world records the handler trace {trace:string}", 'the run {outcome:"completes"|"fails"}', "the failure names the step in the Spec's own words as {failureLabel:string}", @@ -468,7 +464,7 @@ export const extractionSpecs = [ "a contract whose given step repeats {occurrences: 2} times before one when step and one then step", ], when: ["the bound plan runs against a fresh world"], - [["t", "hen"].join("")]: [ + then: [ 'the world records the handler trace {trace: "given 2 | given 2 | when | then"}', 'the run {outcome: "completes"}', ], @@ -498,7 +494,7 @@ export const extractionSpecs = [ 'the handler bound to the {failingPhase: "when"} step throws {thrown: "boom"}', ], when: ["the bound plan runs against a fresh world"], - [["t", "hen"].join("")]: [ + then: [ 'the run {outcome: "fails"}', 'the failure names the step in the Spec\'s own words as {failureLabel: "at step: When the cart is submitted"}', 'the failure preserves the original detail {detail: "boom"}', diff --git a/test/self-hosting-oracle/model.ts b/test/self-hosting-oracle/model.ts index b331cba..98600ef 100644 --- a/test/self-hosting-oracle/model.ts +++ b/test/self-hosting-oracle/model.ts @@ -137,7 +137,7 @@ export const modelSpecs = [ exampleSpace: { given: ["the authored identifier {identifier:string}"], when: ["the identifier is parsed"], - [["t", "hen"].join("")]: [ + then: [ 'parsing {outcome:"resolves"|"is refused"}', "reformatting the parsed parts restores {restored:string}", "the refusal names the reason {reason:string}", @@ -164,7 +164,7 @@ export const modelSpecs = [ { given: ['the authored identifier {identifier: "spec:orders.create-order#valid-cart"}'], when: ["the identifier is parsed"], - [["t", "hen"].join("")]: [ + then: [ 'parsing {outcome: "resolves"}', 'reformatting the parsed parts restores {restored: "spec:orders.create-order#valid-cart"}', ], @@ -191,7 +191,7 @@ export const modelSpecs = [ { given: ['the authored identifier {identifier: "Spec:orders.create-order"}'], when: ["the identifier is parsed"], - [["t", "hen"].join("")]: [ + then: [ 'parsing {outcome: "is refused"}', 'the refusal names the reason {reason: "namespace must be lowercase"}', ], @@ -248,7 +248,7 @@ export const modelSpecs = [ 'a repository whose one source file builds an anchor through {builderSource:"a consumer-local lookalike module"|"a relative import resolving to the Protocol builder modules"|"the published Protocol package"}', ], when: ["the repository is extracted"], - [["t", "hen"].join("")]: [ + then: [ "the extraction mints {anchorCount:number} anchors", "the extraction reports {findingCount:number} findings", ], @@ -295,7 +295,7 @@ export const modelSpecs = [ 'a repository whose one source file builds an anchor through {builderSource: "a consumer-local lookalike module"}', ], when: ["the repository is extracted"], - [["t", "hen"].join("")]: [ + then: [ "the extraction mints {anchorCount: 0} anchors", "the extraction reports {findingCount: 0} findings", ], @@ -325,7 +325,7 @@ export const modelSpecs = [ 'a repository whose one source file builds an anchor through {builderSource: "a relative import resolving to the Protocol builder modules"}', ], when: ["the repository is extracted"], - [["t", "hen"].join("")]: [ + then: [ "the extraction mints {anchorCount: 1} anchors", "the extraction reports {findingCount: 0} findings", ], From afef81cd28e0c2c886c73fc74924bb5ffe745c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 20:33:45 +0200 Subject: [PATCH 11/16] docs(specs,tests): carry the gap signal's recomputed-facts reading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-doc audit graded the derived-facts honesty row `carried` while one of its clauses — the gap check reads the recomputed facts, so a faked fact never silences it — stood only on `src/validate/validators.ts`. That is a code surface under a deletion-authorizing verdict, the shape the phase-3 review made a major of and closed by enrichment rather than by argument. `spec:validation.warn-level-signals`, whose realizing entrypoint is the gap check itself, now states the clause in authored words. It is ratified intended truth, not promoted code behaviour: the concept material says the same sentence. The oracle transcription follows. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- specs/validation/warn-level-signals.sdp.md | 1 + test/self-hosting-oracle/validation.ts | 61 +++++++++++----------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/specs/validation/warn-level-signals.sdp.md b/specs/validation/warn-level-signals.sdp.md index 701016b..cd38fc8 100644 --- a/specs/validation/warn-level-signals.sdp.md +++ b/specs/validation/warn-level-signals.sdp.md @@ -13,6 +13,7 @@ relations: ## Rule - Orphaned Specs and ready Specs lacking a resolving verifier are warnings, not validation errors. +- The gap signal reads the delivery facts the one derivation rule recomputes from the graph, never the facts a Spec states, so a hand-authored fact can never silence it. - The realizing validator entrypoints are `checkOrphans` and `checkGaps` in `src/validate/validators.ts`. ## Example space diff --git a/test/self-hosting-oracle/validation.ts b/test/self-hosting-oracle/validation.ts index 5296f56..59075e8 100644 --- a/test/self-hosting-oracle/validation.ts +++ b/test/self-hosting-oracle/validation.ts @@ -33,7 +33,7 @@ export const validationSpecs = [ 'the spec {defect:"declares no relation"|"records a blocking open question"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId:string} at severity {severity:"warning"|"error"}', "the finding names the unmet floor clause {clauseId:string}", "the report holds {errorCount:number} errors", @@ -63,7 +63,7 @@ export const validationSpecs = [ 'the spec {defect: "declares no relation"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"}', 'the finding names the unmet floor clause {clauseId: "at-least-one-relation"}', "the report holds {errorCount: 1} errors", @@ -94,7 +94,7 @@ export const validationSpecs = [ 'the spec {defect: "records a blocking open question"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"}', 'the finding names the unmet floor clause {clauseId: "no-blocking-open-questions"}', "the report holds {errorCount: 1} errors", @@ -138,7 +138,7 @@ export const validationSpecs = [ 'its only evidence is {evidence:"a constraints entry carrying a target"|"a constraints entry with no target"|"an empty promoted rule child"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId:string} at severity {severity:"warning"|"error"}', "the finding names the unmet floor clause {clauseId:string}", "the report holds {errorCount:number} errors", @@ -169,7 +169,7 @@ export const validationSpecs = [ 'its only evidence is {evidence: "a constraints entry carrying a target"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"}', 'the finding names the unmet floor clause {clauseId: "kind-evidence-complete"}', "the report holds {errorCount: 1} errors", @@ -201,7 +201,7 @@ export const validationSpecs = [ 'its only evidence is {evidence: "a constraints entry with no target"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"}', 'the finding names the unmet floor clause {clauseId: "kind-evidence-complete"}', "the report holds {errorCount: 1} errors", @@ -233,7 +233,7 @@ export const validationSpecs = [ 'its only evidence is {evidence: "an empty promoted rule child"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "honesty/readiness-floor"} at severity {severity: "error"}', 'the finding names the unmet floor clause {clauseId: "kind-evidence-present"}', "the report holds {errorCount: 1} errors", @@ -264,7 +264,7 @@ export const validationSpecs = [ "a {secondCarrier:string} carrier declares {specId:string}", ], when: ["the extraction root is read"], - [["t", "hen"].join("")]: [ + then: [ "both sites report {findingId:string}", "no graph node is emitted for {specId:string}", ], @@ -291,7 +291,7 @@ export const validationSpecs = [ 'a {secondCarrier: "Markdown"} carrier declares {specId: "spec:fixture.duplicate"}', ], when: ["the extraction root is read"], - [["t", "hen"].join("")]: [ + then: [ 'both sites report {findingId: "extract/duplicate-id"}', 'no graph node is emitted for {specId: "spec:fixture.duplicate"}', ], @@ -329,7 +329,7 @@ export const validationSpecs = [ "the spec declares a dependsOn relation to the absent target {targetId:string}", ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ "the aggregate report states no family of its own", 'the conformance family reports {conformanceId:string} at severity {conformanceSeverity:"warning"|"error"}', 'the honesty family reports {honestyId:string} at severity {honestySeverity:"warning"|"error"}', @@ -360,7 +360,7 @@ export const validationSpecs = [ 'the spec declares a dependsOn relation to the absent target {targetId: "spec:probe.absent-dependency"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ "the aggregate report states no family of its own", 'the conformance family reports {conformanceId: "conformance/referential-integrity"} at severity {conformanceSeverity: "error"}', 'the honesty family reports {honestyId: "honesty/gaps"} at severity {honestySeverity: "warning"}', @@ -396,7 +396,7 @@ export const validationSpecs = [ "the spec declares a dependsOn relation to {targetId:string}", ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId:string} at severity {severity:"warning"|"error"}', "the finding offers the nearest-id suggestion: {suggested:boolean}", ], @@ -425,7 +425,7 @@ export const validationSpecs = [ 'the spec declares a dependsOn relation to {targetId: "spec:probe.fulfilment-policy"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "conformance/referential-integrity"} at severity {severity: "error"}', "the finding offers the nearest-id suggestion: {suggested: false}", ], @@ -455,7 +455,7 @@ export const validationSpecs = [ 'the spec declares a dependsOn relation to {targetId: "spec:probe.create-ordr"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "conformance/referential-integrity"} at severity {severity: "error"}', "the finding offers the nearest-id suggestion: {suggested: true}", ], @@ -490,7 +490,7 @@ export const validationSpecs = [ 'the graph carries an off-contract {element:"edge claim"|"descriptor value"} spelled {value:string}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId:string} at severity {severity:"warning"|"error"}', "the finding message states {phrase:string}", "the report holds {floorCount:number} readiness-floor findings", @@ -520,7 +520,7 @@ export const validationSpecs = [ 'the graph carries an off-contract {element: "edge claim"} spelled {value: "declared"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "conformance/claim-separation"} at severity {severity: "error"}', 'the finding message states {phrase: "never collapsed"}', "the report holds {floorCount: 0} readiness-floor findings", @@ -552,7 +552,7 @@ export const validationSpecs = [ 'the graph carries an off-contract {element: "descriptor value"} spelled {value: "saga"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "conformance/claim-separation"} at severity {severity: "error"}', 'the finding message states {phrase: "outside the ratified descriptor values"}', "the report holds {floorCount: 0} readiness-floor findings", @@ -589,7 +589,7 @@ export const validationSpecs = [ 'a non-resolving {verifierKind:"example spec"|"oracle anchor"} named {verifierId:string} points at it', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId:string} at severity {severity:"warning"|"error"}', "the parent earns the delivery fact has-verifier: {conferred:boolean}", ], @@ -619,7 +619,7 @@ export const validationSpecs = [ 'a non-resolving {verifierKind: "example spec"} named {verifierId: "spec:probe.create-order.valid-cart"} points at it', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "conformance/verifies-linkage"} at severity {severity: "warning"}', "the parent earns the delivery fact has-verifier: {conferred: false}", ], @@ -649,7 +649,7 @@ export const validationSpecs = [ 'a non-resolving {verifierKind: "oracle anchor"} named {verifierId: "oracle:probe.order-policy"} points at it', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "conformance/oracle-linkage"} at severity {severity: "error"}', "the parent earns the delivery fact has-verifier: {conferred: false}", ], @@ -684,7 +684,7 @@ export const validationSpecs = [ "the pack also names that spec as a modelRef", ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId:string} at severity {severity:"warning"|"error"}', "the report holds {findingCount:number} pack-coherence findings", ], @@ -713,7 +713,7 @@ export const validationSpecs = [ "the pack also names that spec as a modelRef", ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "conformance/pack-coherence"} at severity {severity: "error"}', "the report holds {findingCount: 2} pack-coherence findings", ], @@ -747,7 +747,7 @@ export const validationSpecs = [ 'the spec hand-authors the delivery fact {factName:"implemented"|"has-verifier"} at {site:"a behavior section carrier"|"the node deliveryFacts array"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId:string} at severity {severity:"warning"|"error"}', "the finding names the fact {relatedId:string} and states {phrase:string}", ], @@ -777,7 +777,7 @@ export const validationSpecs = [ 'the spec hand-authors the delivery fact {factName: "implemented"} at {site: "a behavior section carrier"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "honesty/authoring-shape"} at severity {severity: "error"}', 'the finding names the fact {relatedId: "implemented"} and states {phrase: "derived, never authored"}', ], @@ -808,7 +808,7 @@ export const validationSpecs = [ 'the spec hand-authors the delivery fact {factName: "has-verifier"} at {site: "the node deliveryFacts array"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "honesty/delivery-facts"} at severity {severity: "error"}', 'the finding names the fact {relatedId: "has-verifier"} and states {phrase: "derived, never authored"}', ], @@ -834,6 +834,7 @@ export const validationSpecs = [ behavior: { rules: [ "Orphaned Specs and ready Specs lacking a resolving verifier are warnings, not validation errors.", + "The gap signal reads the delivery facts the one derivation rule recomputes from the graph, never the facts a Spec states, so a hand-authored fact can never silence it.", "The realizing validator entrypoints are `checkOrphans` and `checkGaps` in `src/validate/validators.ts`.", ], exampleSpace: { @@ -842,7 +843,7 @@ export const validationSpecs = [ 'the spec declares {relations:"no relation"|"a decidedBy decision"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId:string} at severity {severity:"warning"|"error"}', "the report holds {errorCount:number} errors", ], @@ -869,7 +870,7 @@ export const validationSpecs = [ 'the spec declares {relations: "no relation"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "conformance/orphans"} at severity {severity: "warning"}', "the report holds {errorCount: 0} errors", ], @@ -899,7 +900,7 @@ export const validationSpecs = [ 'the spec declares {relations: "a decidedBy decision"}', ], when: ["the graph is validated"], - [["t", "hen"].join("")]: [ + then: [ 'the report names {findingId: "honesty/gaps"} at severity {severity: "warning"}', "the report holds {errorCount: 0} errors", ], @@ -936,7 +937,7 @@ export const validationSpecs = [ 'a finding naming the validator {validatorId:string} at severity {severity:"warning"|"error"} carrying the message {message:string}', ], when: ["the command-line renderer formats that finding once per location shape"], - [["t", "hen"].join("")]: [ + then: [ "the finding carrying the file {file:string} and the line {line:number} renders {withLocation:string}", "the same finding carrying the file alone renders {fileOnly:string}", "the same finding carrying neither renders {bare:string}", @@ -966,7 +967,7 @@ export const validationSpecs = [ 'a finding naming the validator {validatorId: "honesty/readiness-floor"} at severity {severity: "error"} carrying the message {message: "The stated rung is not earned."}', ], when: ["the command-line renderer formats that finding once per location shape"], - [["t", "hen"].join("")]: [ + then: [ 'the finding carrying the file {file: "specs/probe.sdp.md"} and the line {line: 7} renders {withLocation: "specs/probe.sdp.md:7 — [error] honesty/readiness-floor — The stated rung is not earned."}', 'the same finding carrying the file alone renders {fileOnly: "specs/probe.sdp.md — [error] honesty/readiness-floor — The stated rung is not earned."}', 'the same finding carrying neither renders {bare: "[error] honesty/readiness-floor — The stated rung is not earned."}', From 881fbcf2e55106cc3a2b50df3111ed68fb3993fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 20:36:42 +0200 Subject: [PATCH 12/16] docs(plans,agents): close phase 4 with the done-record and terminal ledgers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan 21 goes to EXECUTED. The done-record states what the five sessions delivered, the S5 rulings log, the opening-to-closing numbers, and every one of the seven acceptance criteria graded with its evidence. Criterion 5 grades PASS with `05` staying: the criterion asks for an audit-grounded disposition, not a deletion, and the two blocking rows were independently re-verified at the close. The watch items are terminal — the per-family oracle-drift item and the shared-constant item were probed rather than assumed. The docket rows are dispositioned: corpus-test granularity, the row that had rolled since review-08, closes DONE at S1; the rest are carried with stated reasons, and the dead RenderedFinding shape enters carried. The review's records findings land here too: the phase-3 close carried six bound suites rather than five, and the pre-split node-id roster was an 88-item literal rather than 151 items. Two conversion-ledger rows are sharpened from the measurement — which single-site mutations the wholesale-rewrite point survives, and which half of the binding-language rule the bound point does not reach. AGENTS.md's status clause moves to the phase-4 close. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- AGENTS.md | 2 +- plans/21-self-hosting-phase-4.md | 184 ++++++++++++++++++++++++++----- 2 files changed, 160 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 22e8293..a977342 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ claims; **`src/` and tests** are authoritative evidence of current realization. > machinery landed (plan 13) · authoring **carrier ruled** as `.sdp.md` (the carrier ruling, MD-18; > plan 16) — product Markdown parser and self-hosting landed · **canonical-default carrier > rule:** Specs default to Markdown; Packs remain TS until a Pack syntax ruling; the TS DSL survives -> as import source and a lawful per-ID option. · **what now:** ✅ EXECUTED — phase-3 implementation complete; the pre-close adversarial review is archived with every finding dispositioned. The prior `EXECUTED — phase-1 implementation complete; final audit passed` and phase-2 statuses remain historic context. The systematic tests-to-executable-specs rewrite ran across the cheap laws and the core model dissolved into Specs; the remaining work is the recorded corpus gaps (the lower readiness-floor clause tables, the derived-readiness banner, and the rest) and the concept docs still standing on their own audits. Build state lives in +> as import source and a lawful per-ID option. · **what now:** ✅ EXECUTED — phase-4 implementation complete; the pre-close adversarial review is archived with every finding dispositioned. The earlier phase-1 to phase-3 statuses remain historic context. The corpus oracle is split into per-law assertions over one hoisted extraction; one shared module now states the contract-dependent suites for both the test wrapper and the lint config; the floor wave and the view wave carried the readiness-floor clause tables and the projection laws, adding **10 new bound points, each mutation-probed red**. Corpus at **`ready: 66 / defined: 37`** over 103 Specs, 0 errors / 0 warnings. `docs/concept/05` **stays, two clauses short** — a per-team severity override and a team-overridable floor config (gaps 13/14), which are the named next work, beside the `06`/`07` gaps still standing. Build state lives in > **`plans/`** — read the highest > **primary-numbered** plan's status header, plus any **active subplans it (or its parent family) > explicitly designates as current**; ignore unnumbered files and letter-suffixed plans only when diff --git a/plans/21-self-hosting-phase-4.md b/plans/21-self-hosting-phase-4.md index 2c521f6..8a78460 100644 --- a/plans/21-self-hosting-phase-4.md +++ b/plans/21-self-hosting-phase-4.md @@ -1,7 +1,21 @@ # Plan 21 — Self-hosting phase 4: the oracle split, the floor and view corpus waves, and the `05` dissolution attempt -> **Status:** DRAFTED — execution begins on `feature/protocol-self-application-phase-4`. This is -> plan 21, the highest primary-numbered plan; the latest ✅ EXECUTED ground is plan 20 (the +> **Status:** ✅ EXECUTED — the whole phase landed on `feature/protocol-self-application-phase-4`. +> The oracle split the corpus checkpoint into 21 `it()`s over one hoisted extraction with zero +> assertion loss; one shared module now states the contract-dependent suites for both the test +> wrapper and the lint config, and the seventh bound suite entered through it; the floor wave +> carried the lower readiness-floor rungs and the per-kind evidence table, and the view wave +> carried the derived-readiness banner, the binding-language rule, the wholesale view rewrite, +> the one diagnostic rendering rule, and validator self-testing — **16 Specs added, 10 new bound +> points, every one mutation-probed red for the law it names and re-probed independently at the +> close**. The readiness sweep dispositioned all 37 `defined` Specs with zero promotions and 37 +> named refusals. `docs/concept/05` **stays**, two clauses short: the re-audit closed all three of +> its phase-3 gaps and surfaced two new ones — a per-team severity override and a team-overridable +> floor config, both designed-for deferrals named nowhere else (gaps 13/14, the named next work); +> `06` and `07` were re-graded and stay. Closing corpus **103 Specs · 1 Pack · 75 anchors → 179 +> nodes · 351 edges · `ready: 66 / defined: 37`, 0 errors / 0 warnings**, with the full twelve-leg +> gate and a clean-clone proof green at the close. This is +> plan 21, the highest primary-numbered plan; the previous ✅ EXECUTED ground is plan 20 (the > phase-3 close). Build state lives in **`plans/`** — read the highest **primary-numbered** > plan's status header, plus any **active subplans it (or its parent family) explicitly > designates as current**; ignore unnumbered files and letter-suffixed plans only when no @@ -29,8 +43,10 @@ not occur. ## (b) Context -Phase 3 closed at **87 Specs · 51 `ready` / 36 `defined` · 29 bound points across five bound -suites**, with `02` and `03` deleted and twelve gaps recorded against `05`/`06`/`07`. Three +Phase 3 closed at **87 Specs · 51 `ready` / 36 `defined` · 29 bound points across six bound +suites** (the draft said five; the S5 recount found six — `carrier` 3 · `duplicate-ids` 1 · +`extraction` 8 · `model` 4 · `sdp-import` 1 · `validators` 12 — which is also the six paths the +pre-branch root dependency row listed, so the projections suite is the **seventh**), with `02` and `03` deleted and twelve gaps recorded against `05`/`06`/`07`. Three debts are on record. First, `test/self-hosting-graph.test.ts` stands at 3,888 lines in a single `it()` with frozen absolute counts — the first failure masks the rest, and every conversion wave thrashes it. Second, the clean-room lint exemption (`eslint.config.js`) and the wrapper's @@ -118,11 +134,13 @@ ratified at the PR; drift discipline). Phase 4 adds: `test/self-hosting-graph.test.ts` today: one `describe`/one `it()` (lines 3593–3886) over module-level frozen data — `expectedSpecs` (87 entries, ~2,558 lines), `expectedPackMembers`, `expectedDeclaredRelations`, `expectedWarnings`, `expectedAnchors` (65 entries) — asserting in -order: clean extraction · no warn-level findings · frozen counts (87/1/65) · a redundant -151-item node-id roster · per-spec descriptor equality · declared relations · the readiness -histogram (51/36) · pack membership · the pack node · edge count (294) · anchored edges · -anchor nodes (with file I/O line resolution) · anchor-site proximity · two derived-fact case -studies. +order: clean extraction · no warn-level findings · frozen counts (87/1/65) · a node-id roster +whose 88 literal ids (the Pack plus all 87 spec ids) duplicate `expectedSpecs`, with the anchor +half already derived by spread, compared against all 153 node ids (the draft said "151-item"; +corrected at S5 from the measurement) · per-spec descriptor equality · declared relations · the +readiness histogram (51/36) · pack membership · the pack node · edge count (294) · anchored +edges · anchor nodes (with file I/O line resolution) · anchor-site proximity · two derived-fact +case studies. Deliverables: @@ -219,20 +237,27 @@ recorded (ruling 12). Promotions ride verifiers per ruling 5. | Item | Fires when | State | |---|---|---| -| table sugar (ruling 4) | sibling authoring proves dishonest or unusable in a wave | unfired | -| single-literal vocabulary form | real material forces it | unfired | -| per-family oracle drift | a split family module regains cross-family assertions or a mega-assert | unfired | -| shared-constant bypass | a new contract-dependent suite lands outside the constant | unfired — the constant landed at S2 with no surprise; the eslint side derives from the root row only, because the example tree's suite sits outside the typed-lint globs the exemption relaxes. **S3 proved it:** `test/self-hosting-projections.test.ts` (the seventh suite) entered through one edit to the root row and both surfaces followed. The negative control ran too — with the row removed and `generated/contracts` moved aside, clean-room lint fails with five `no-unsafe-argument` errors and the wrapper stops refusing fast, so the coupling is load-bearing on both sides rather than incidentally satisfied | -| separate example-id namespace | a collision or real pressure appears | unfired (watch continues from phase 3) | +| table sugar (ruling 4) | sibling authoring proves dishonest or unusable in a wave | **terminal — unfired.** Both waves authored their example spaces as sibling `gwt`/`gwt-vocabulary` fences without pressure for a table form; ten new points landed with no authoring complaint. Carried to the next phase unchanged | +| single-literal vocabulary form | real material forces it | **terminal — unfired.** Every new example space took two or more parameters; no single-parameter vocabulary appeared, so the question was never posed. Carried unchanged | +| per-family oracle drift | a split family module regains cross-family assertions or a mega-assert | **terminal — unfired, and probed rather than assumed.** At the close, a wrong descriptor in one family module reddens exactly that family's `it()`, and a wrong *id* reddens that family plus the two identity laws that read the union — which is the designed coupling, not drift. No family module holds an assertion; they hold data only | +| shared-constant bypass | a new contract-dependent suite lands outside the constant | **terminal — unfired**, and the coupling is proved load-bearing on both sides (the negative control below was re-run from scratch at the close). Carried to the next phase. The constant landed at S2 with no surprise; the eslint side derives from the root row only, because the example tree's suite sits outside the typed-lint globs the exemption relaxes. **S3 proved it:** `test/self-hosting-projections.test.ts` (the seventh suite) entered through one edit to the root row and both surfaces followed. The negative control ran too — with the row removed and `generated/contracts` moved aside, clean-room lint fails with five `no-unsafe-argument` errors and the wrapper stops refusing fast, so the coupling is load-bearing on both sides rather than incidentally satisfied | +| separate example-id namespace | a collision or real pressure appears | **terminal — unfired.** The ten new example ids all took the `spec:..` form under their parent's namespace; no collision appeared and the corpus reports zero duplicate-id findings. Carried to the next phase (the watch continues from phase 3) | ## §4 Docket ledger (carried in from plan 20) -Markdown Pack syntax ruling · the gen-1 `.feature` adapter · the no-reparse read seam · -temporal-guard token assembly · the editor-association gap · corpus-test granularity (owned by -this phase — S1 is the session that dispositions it; **dispositioned at S1** — the corpus oracle -split into 21 `it()`s over one hoisted extraction, with the frozen expectation moved to authored -per-family modules under `test/self-hosting-oracle/`) · control-character latitude · the -separate example id namespace. Rows close only with reasons in the done-record. +Rows close only with reasons. Terminal state at this close: + +| Row | Disposition | +|---|---| +| **corpus-test granularity** | **DONE at S1** — the row this phase owned, and the one that had rolled since review-08. The corpus oracle is 21 `it()`s over one hoisted extraction; the frozen expectation lives in ten authored per-family modules under `test/self-hosting-oracle/`; the first failure no longer masks the rest, and a conversion wave touches one family file. Proved at the close by corruption probes rather than by inspection. **Closed.** | +| Markdown Pack syntax ruling | **carried** — no wave forced an entry under fire; the one Pack carrier stayed `.sdp.ts` and nothing about the two waves pressed on Pack authoring. Still open, still ruled by the canonical-default carrier rule | +| the gen-1 `.feature` adapter | **carried** — out of scope by §(c); untouched, no pressure appeared | +| the no-reparse read seam | **carried** — out of scope by §(c); the projections suite renders from an in-memory graph and never re-parses, so the seam was never pressed | +| temporal-guard token assembly | **carried** — the standing choice (widen the guard and repair the sites, or narrow ruling 7's wording) is unchanged. Phase 4 authored **no** new violation: an independent sweep of every branch-added file under `specs/` and `test/`, using a wider token set than the guard's own pattern, returns zero hits | +| the editor-association gap | **carried** — out of scope by §(c); untouched | +| control-character latitude | **carried** — out of scope by §(c); no new material exercised it | +| the separate example id namespace | **carried** — the watch item above stayed unfired; ten new example ids landed under their parents' namespaces with no collision | +| **dead `RenderedFinding` shape** *(new — review-10 S-2)* | **entered carried** — `src/cli/output.ts` declares and exports a second finding shape with no producer anywhere in `src/`, `test/`, or `examples/`, and no barrel export, inside the very file `spec:validation.diagnostic-rendering` names as an entrypoint while stating that no surface introduces a parallel report shape. Dead internal surface, not a live parallel path; removing an internal type is engine hygiene outside a review-and-close session | ## §5 Acceptance criteria @@ -300,7 +325,7 @@ stretched into `carried`, per the D-2/D-3 lesson. | §2 checks 1–2 (referential integrity with did-you-mean · duplicate IDs) | `spec:validation.referential-integrity` + `dangling-target` and `did-you-mean` · `spec:validation.duplicate-ids` + `dual-carrier` | carried | | §2 check 3 (`claim` separation, endpoint contracts, fail-closed descriptors, the kind-typed endpoints) | `spec:validation.claim-separation` + `collapsed-edge-claim` and `unratified-descriptor` · `spec:model.relations` (the per-relation endpoint kinds in its vocabulary) | carried | | §2 check 4 (`verifies` linkage; a wrong-kind verifier confers nothing rather than failing) | `spec:validation.verification-linkage` + `unbound-example` and `unresolved-oracle` ("a non-resolving trace is named loudly and confers no delivery fact") | carried | -| §2 checks 5–6 (authoring-shape honesty · derived-facts honesty, including `observed`; the gap check reads recomputed facts) | `spec:validation.authored-honesty` + `section-authored-fact` and `unearned-stated-fact` · `src/validate/validators.ts` `checkGaps` (reads the recomputed facts, pinned by the two points) | carried | +| §2 checks 5–6 (authoring-shape honesty · derived-facts honesty, including `observed`; the gap check reads recomputed facts) | `spec:validation.authored-honesty` + `section-authored-fact` and `unearned-stated-fact` (the stated-equals-recomputed law) · `spec:validation.warn-level-signals` (the coupling clause: the gap signal reads the recomputed facts, never a Spec's stated ones, so a hand-authored fact can never silence it) — **enriched at S5**, because the coupling sentence previously stood only on `src/validate/validators.ts` `checkGaps`, which is a code surface under a deletion-authorizing verdict (the phase-3 D-2 shape, repaired the same way: by enrichment) | carried | | §2 check 7 (honest readiness — a stated rung is checked against the floor) | `spec:validation.readiness-floor` (**all four rungs now in authored words**, cumulative evaluation stated) | carried | | §2 check 8 (orphan detection) | `spec:validation.warn-level-signals` + `orphan-signal` · `CONTEXT.md` "`orphan`" | carried | | §2 check 8 parenthetical — **a per-team severity override is designed-for, deferred** | none — no Spec, registry, code+test surface, or surviving doc names this deferral; `00` §4 and `07` §2/§3 do not list it | **gap** (new — recorded as gap 13) | @@ -413,7 +438,101 @@ prioritization heuristic (§5). All three are named out of scope by §(c); `07` ## §6 Done-record -*(written at close)* +The phase ran in five sessions on `feature/protocol-self-application-phase-4`, each closing with a +green twelve-leg gate. What it delivered, against §(c): + +1. **The oracle split (S1).** `test/self-hosting-graph.test.ts` went from one `it()` over 3,888 + lines to **21 `it()`s over a single hoisted extraction** — the corpus walk runs once per suite + run, never once per assertion. The frozen expectation moved to **ten authored transcription + modules** under `test/self-hosting-oracle/` (seven Spec-family files, the Pack manifest, the + declared relations, the anchors) plus an aggregating index; nothing there is computed from the + graph it judges. Zero assertion loss: **27 original `expect` sites → 32**, with three + oracle-length cross-checks and the two-assertion no-Spec-outside-the-families law added, and + the redundant node-id roster derived from the authored arrays per ruling 10. +2. **One source of truth for the contract-dependent suites (S2).** `contract-dependent-suites.mjs` + states the per-tree rows once; `vitest-test.mjs` and `eslint.config.js` both read it. S3 proved + the coupling under fire — one edit admitted the seventh suite and both surfaces followed. +3. **The floor wave (S2).** `spec:validation.readiness-floor` enriched to state all four rungs in + authored words, and the new `spec:validation.kind-evidence` carrying the per-kind evidence + table row for row, with 5 bound points. +4. **The view wave (S3).** Four new rule Specs — `spec:consumers.derived-readiness-banner`, + `spec:consumers.binding-language-views`, `spec:consumers.wholesale-view-rewrite`, + `spec:validation.diagnostic-rendering` — plus `spec:validation.validator-self-testing` at an + honest `defined`, with 5 bound points in the new `test/self-hosting-projections.test.ts`. +5. **The sweep and the re-audits (S4).** All 37 `defined` Specs dispositioned, zero promotions, + 37 named refusals; `05` re-audited and kept on two new gap rows; `06` and `07` re-graded. +6. **The adversarial close (S5).** `reviews/10-self-hosting-phase-4-pre-close-review.md`, with + every finding dispositioned, and its accepted findings landed on the branch. + +### The S5 rulings log + +1. **The mutation set was designed independently, not replayed.** Twenty-two mutations — + eighteen against the ten points, eight against the oracle split — none of them a repeat of a + wave's own probe. All ten points went red for the law they name; seventeen of the eighteen + point-directed mutations killed **exactly one** point, and the eighteenth killed exactly the + two whose Spec text names the same floor clause. §7's two loosest rows were sharpened from the + measurement rather than left as written. +2. **The `then`-key curiosity is closed, and it was a regression, not a convention.** The + indirect key was raised by review-06, fixed by the phase-1 remediation, **silently reintroduced + hours later by the grammar-hardening commit in the same cluster**, and then recorded by the + phase-2 docket as *"verified — phase-1 remediation names the `then` key directly."* That + sentence was false when written, and this close makes it true: every one of the 66 sites now + writes the plain key. Nothing required the indirection — no ESLint rule or plugin, no gate leg, + none of the five check scripts, and no scanner over test files (the extractor reifies only + `.sdp.ts` and `.sdp.md` carriers). Recorded here rather than in the docket, because a defect + that a docket row already claimed to have verified deserves the correction beside the claim. +3. **One `carried` verdict was repaired by enrichment, not by argument.** The derived-facts + honesty row rested its coupling clause — the gap check reads the recomputed facts, so a faked + fact never silences it — on `src/validate/validators.ts` alone. That is the phase-3 D-2 shape + under a deletion-authorizing verdict, so `spec:validation.warn-level-signals` now states it and + the row's citation names the Spec first. +4. **Two record slips corrected from measurement.** §(b)'s "five bound suites" is six, and §2 S1's + "151-item node-id roster" was an 88-item literal whose anchor half was already derived. Both + were draft-time descriptions, neither load-bearing for a verdict; both now state what was + measured. +5. **Two findings were declined or carried with reasons rather than argued away.** The floor + Spec's silence on the clause attribution for an unresolved relation target states nothing + false and produces an identical rendered outcome, so no clause was invented for it. The dead + `RenderedFinding` type in `src/cli/output.ts` is internal surface with no producer and no + barrel export, so no parallel report path exists in substance; it rides the docket. + +### Closing numbers + +| | Opening (`main`) | Closing | +|---|---|---| +| Specs | 87 | **103** | +| anchors | 65 | **75** | +| nodes · edges | 153 · 294 | **179 · 351** | +| stated readiness | `ready: 51 / defined: 36` | **`ready: 66 / defined: 37`** | +| bound points · bound suites | 29 · 6 | **39 · 7** | +| findings over the corpus | 0 errors / 0 warnings | **0 errors / 0 warnings** | + +Every one of the 66 `ready` Specs carries `has-verifier` through the executable path; not one of +the 37 `defined` Specs carries it, which is the sweep's uniform refusal reason. + +### §5 acceptance criteria, graded + +| # | Criterion | Grade | Evidence | +|---|---|---|---| +| 1 | The oracle is split with zero assertion loss | **PASS** | 21 `it()`s over one hoisted extraction; ten authored transcription modules; 27 → 32 `expect` sites, and **no file under `test/` lost one** (1313 → 1342 across the tree, every per-file delta non-negative). Law coverage at close is a superset: the no-Spec-outside-the-families law and the three length cross-checks are new. Corpus-test granularity dispositioned at S1 (§4). Probed at S5: a wrong descriptor in one family module reddens exactly that family's `it()`; a deleted entry is caught by the length cross-check rather than certifying itself. | +| 2 | One source of truth for contract-dependent suites | **PASS** | Both consumers import `contract-dependent-suites.mjs`. Re-run at S5 from a clean room: lint passes with the projections row and fails with **exactly five `no-unsafe-argument` errors** without it, while the wrapper refuses fast with the recovery text with the row and spawns straight into the missing tree without it. Load-bearing on both sides. | +| 3 | Executable-path facts, not claims | **PASS** | 66/66 `ready` Specs carry `has-verifier`; zero validation errors; `--check-clean` clean on both trees; **all ten new points mutation-probed red for the law they name**, independently at S5 as well as in their own wave. | +| 4 | Honest readiness | **PASS** | Zero warnings — no `honesty/gaps` finding exists at all. Closing distribution `ready: 66 / defined: 37` recomputed off the graph and off disk. Every promotion carried a resolving verifier in the same change; all 37 refusals carry named reasons (§8), and all 37 pages read "structural floor reached: `ready`", so the refusals are about evidence, never structure. | +| 5 | The `05` disposition is audit-grounded | **PASS** | `05` **stays** — the criterion asks for an audit-grounded disposition, not a deletion. All three phase-3 gaps closed; two new rows recorded precisely rather than stretched into `carried`, and independently re-verified at S5: a repository-wide sweep for either deferral returns hits only inside `05` itself and inside this plan's audit rows. Ruling 14's re-pointing and two-form sweep correctly did not run, and the deletion-cost inventory is written so the next attempt is one session. `06` and `07` re-graded with honest ledgers; one row's citation corrected at S5 (rulings log 3). | +| 6 | The gate holds throughout | **PASS** | `npm run check` green at every blessed commit, and the full twelve-leg chain plus the clean-clone proof green at the close SHA (§9). | +| 7 | Records continue | **PASS** | §3 watch items terminal, §4 docket rows dispositioned or carried with stated reasons, §7 and §8 terminal, §9 terminal, and the adversarial review archived with every finding in a terminal disposition before close. | + +### What the owner ratifies at the PR + +Three calls this phase records rather than makes: + +1. **`05` stays, two clauses short.** Gaps 13 and 14 are the named next work. +2. **The `then`-key normalization touches product code** — one line in + `src/extract/markdown-body-owner-behavior.ts` and 65 test/oracle sites. Byte-neutral at + runtime and gate-proven, but it reverses a shape that survived three phases. +3. **`spec:validation.validator-self-testing` ships at `defined` with no verifier** and is still + graded `carried` by the `05` audit — the dissolution criterion asks that a law be carried by a + Spec, not that the Spec be `ready`. This is the first time the corpus leans on that reading. ## §7 Conversion / corpus ledger @@ -425,8 +544,8 @@ reasons)* | S2 | lower floor rungs (`idea`/`scoped`/`defined` clauses) | `spec:validation.readiness-floor` (enriched) | 2–3 | done — 2 points (`at-least-one-relation` on a scoped probe · `no-blocking-open-questions` on a defined probe), both mutation-probed red | | S2 | per-kind evidence table + MD-16 promoted-evidence bound | new `spec:validation.kind-evidence` | 1–2 | done — 3 points (behavior-family complete cell · constraint target · the promoted-evidence bound); one over the planned ceiling, taken deliberately so the MD-16 bound the Spec states is not the only row left unbound | | S3 | derived-readiness banner (one direction · first unmet clause) | new `spec:consumers.derived-readiness-banner` | 1–2 | done — 2 points (`dishonest-divergence` names the first unmet clause · `honest-headroom` pairs the absent banner with the rendered stated-beside-derived line), both mutation-probed red | -| S3 | `implemented` view-label (binding language) | new `spec:consumers.binding-language-views` | 1 | done — 1 point (`bound-spec-page`: the four binding lines, the index row repeating them, and the internal fact names absent from both surfaces), mutation-probed red | -| S3 | wholesale page rewrite (atomic swap · no stale page) | new `spec:consumers.wholesale-view-rewrite` | 1 | done — 1 point (`stale-page-removed`, a temp-root world running the real `runView`), mutation-probed red; the law is realized at two sites (the up-front invalidation in `runBuild` plus the temp-and-rename in `runView`), so breaking one alone leaves the point green — recorded, and the Spec states both | +| S3 | `implemented` view-label (binding language) | new `spec:consumers.binding-language-views` | 1 | done — 1 point (`bound-spec-page`: the four binding lines, the index row repeating them, and the internal fact names absent from both surfaces), mutation-probed red. **Residue measured at S5:** the Spec's rule names *both* aggregate surfaces, but the probe graph holds no Pack, so only the index half is exercised — changing the index row's cells to `yes`/`no` kills the point, the identical change to the pack member table's cells does not. The pack-member half of that rule stands unbound | +| S3 | wholesale page rewrite (atomic swap · no stale page) | new `spec:consumers.wholesale-view-rewrite` | 1 | done — 1 point (`stale-page-removed`, a temp-root world running the real `runView`), mutation-probed red; the law is realized at two sites (the up-front invalidation in `runBuild` plus the temp-and-rename in `runView`), so breaking one alone leaves the point green — recorded, and the Spec states both. **Sharpened at S5 from the measurement:** deleting either site alone survives; deleting both kills; replacing the rename with a copy so the temporary sibling is left behind also kills, so the point does discriminate the swap in that one direction. It does *not* discriminate the swap's absence — `temporarySurvives: false` is satisfied both when the temporary ran and was cleaned up and when no temporary exists at all. Three of the Spec's rule lines have no verifier: the "no half-written view is ever readable" reading of the one-rename clause, the failed-run removal, and the `--check-clean` double render with its refusal | | S3 | one diagnostic rendering rule | new `spec:validation.diagnostic-rendering` | 1 | done — 1 point (`composed-location`: the composed prefix plus both degradations on one finding), mutation-probed red. **Family call:** the carrier lives in `specs/validation/` and refines `spec:validation.two-check-families`, because the law's subject is the Finding currency — a validation concept whose shape law that parent already carries. The consumers family offered no honest parent: `spec:consumers.projections-model` is a `model`-kind vocabulary rather than a law a rule refines, and `spec:consumers.design-review` is only one of the two rendering surfaces. The Design Review half rides a `dependsOn` edge to that Spec instead | | S3 | validator self-testing | new `spec:validation.validator-self-testing` | 0–1 (may honestly stay `defined`) | done — 0 points, stated `defined`: the only mechanical verifier available would inspect the test corpus for should-fail/should-pass pairs, which polices the delivery process rather than conformance or honesty | @@ -524,7 +643,22 @@ ledger is git process evidence, never graph content. | S2 | shared constant + floor wave | orchestrator-verified green gate | done — `contract-dependent-suites.mjs` now states the per-tree rows once and both `vitest-test.mjs` and `eslint.config.js` read it (clean-room proof: lint passes with `generated/contracts` moved aside, the wrapper still fails fast with the same recovery text); the floor wave carried the `idea`/`scoped`/`defined` rungs into `spec:validation.readiness-floor` and the per-kind table into the new `spec:validation.kind-evidence`, with 5 bound points each mutation-probed red for the clause it names; corpus 87 → 93 Specs, `ready` 51 → 57 | | S3 | view wave + seventh bound suite | orchestrator-verified green gate | done — five laws carried (banner · view-label · wholesale rewrite · diagnostic rendering · validator self-testing), ten Specs added, five bound points in the new `test/self-hosting-projections.test.ts`, each mutation-probed red for the law it names; the suite entered the shared constant once and both surfaces followed (clean-room proof: with `generated/contracts` moved aside, lint passes with the row and fails with five unsafe-argument errors without it, while the wrapper refuses fast with the recovery text); corpus 93 → 103 Specs, `ready` 57 → 66 | | S4 | readiness sweep + re-audits (± the `05` deletion) | orchestrator-verified green gate over the regenerated Design Review | done — the sweep dispositioned all 37 `defined` Specs with zero promotions and 37 named refusals (§8): none carries `has-verifier`, so every promotion would have added an `honesty/gaps` warning, and no verifier was invented to enable one. The `05` re-audit closed all three of its phase-3 gaps (S2's floor wave, S3's banner and validator-self-testing carriers) and surfaced **two new gap rows** — the per-team severity override and the team-overridable floor config, both designed-for deferrals named nowhere else — so **`05` stays** and its residue plus a deletion-cost inventory are recorded (§5a). `06` and `07` re-graded: six of the twelve phase-3 gaps closed, both docs stay. Records-only session — no product surface changed, graph numbers unmoved at 103/1/75 · 179 · 351 · `ready` 66 / `defined` 37 | -| S5 | adversarial review, remediation, full close, done-record | full chain + clean-clone; review archived | planned | +| S5 | adversarial review, remediation, full close, done-record | full chain + clean-clone; review archived | done — the review is archived at `reviews/10-self-hosting-phase-4-pre-close-review.md` with **ten findings, every one terminal**: one major fixed (the indirect `then` key, normalized at all 66 sites), one minor fixed by enrichment (`spec:validation.warn-level-signals` now carries the gap signal's recomputed-facts reading), two records repaired (§(b)'s suite count, §2 S1's roster description), two §7 rows sharpened from the measurement, one declined with a reason, one carried to the docket, and two informational verifications recorded. Twenty-two independently designed mutations were run; all ten new points went red for the law they name. The full twelve-leg gate and the clean-clone proof are green at the close SHA | Owner ratification of every gate above happens at the phase PR review; no live owner acceptance occurs during execution. + +### The close proof + +The twelve-leg `npm run check` is green at the close commit, and the clean-clone proof +(`git clone --no-local . /clone-proof` → `npm ci` → the full chain) reproduces it from a +fresh checkout with no build state carried over. Reproduced numbers, both trees: + +- root corpus: **103 specs · 1 pack · 75 anchors → 179 nodes · 351 edges**, 0 errors / 0 warnings, + 62 generated contract modules, 105 Design Review pages, `--check-clean` clean. +- worked example: **11 specs · 1 pack · 5 anchors → 17 nodes · 32 edges**, 0 errors / 1 warning + (the frozen unbound-example warning the tracer bullet exists to show), 3 contract modules, + 13 pages, `--check-clean` clean. +- tests: **589 green** — 535 in the parallel pool plus the 54-test dedicated `test/cli.test.ts` + pass. +- preflight clean; `git status --porcelain` empty after the whole chain. From d84eeaf07831200d74b536ea21d96a031585bcb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 20:39:32 +0200 Subject: [PATCH 13/16] docs(reviews): record the phase-4 remediation addendum Every finding of review-10 is terminal and the work landed before the PR, so the archive now names which commit carries which disposition and what the two substantive fixes teach: a docket row's condition must be re-measured at the moment it is verified, and a `carried` verdict resting on a code surface is closed by enrichment, never by argument. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- ...0-self-hosting-phase-4-pre-close-review.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/reviews/10-self-hosting-phase-4-pre-close-review.md b/reviews/10-self-hosting-phase-4-pre-close-review.md index 7dbba14..69885e9 100644 --- a/reviews/10-self-hosting-phase-4-pre-close-review.md +++ b/reviews/10-self-hosting-phase-4-pre-close-review.md @@ -423,6 +423,30 @@ Both surfaces are load-bearing on both sides. **CONFIRMED.** --- +## Remediation addendum — what landed on this branch + +The remediation ran before the phase PR, so none of these findings survives into `main` as an open +docket. The per-finding work sits in three commits plus the close: + +- **Fixed in product/corpus:** T-1 (`refactor(extract,tests): name the frozen then key directly` — + the one product site inlined, 65 test/oracle sites normalized, Prettier re-wrap taken) and A-1 + (`docs(specs,tests): carry the gap signal's recomputed-facts reading` — + `spec:validation.warn-level-signals` enriched, the oracle transcription following). +- **Records repaired, no product change:** R-1, R-2, and A-1's audit-row citation, in the close + commit. +- **Records sharpened from the measurement:** P-1 and P-2 — plan 21 §7's two rows now state which + mutation classes each point kills and which it does not, and which clauses stand unverified. +- **Declined with a reason:** S-1. **Carried with a reason:** S-2, on the §4 docket. + +Two things are worth reading before touching these surfaces again. First, **T-1's value is the +history, not the diff**: a defect can be fixed, regressed inside the same remediation cluster, and +then re-verified as intact by a later phase reading a plan rather than the file. The cheap guard +against a repeat is to re-measure a docket row's condition at the moment it is verified, not to +trust the row that closed it. Second, **A-1 was closed by enrichment rather than by argument**, the +same way phase 3 closed D-2 and D-3: the `carried` verdict is now true on the dissolution +decision's own terms — carried by a Spec — rather than defensible only on a plan ruling that +widened the criterion to admit code surfaces. + ## What the owner is asked to ratify at the PR Three things this review deliberately leaves to the owner rather than deciding by plan ruling: From ce7920d4eebfe42a8effbcb5d118febc8c50ae3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 21:10:46 +0200 Subject: [PATCH 14/16] feat(specs,tests): give the view and diagnostic laws teeth, and drop the dead finding shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner review of the phase-4 branch ordered four repairs to the bound corpus and the engine, all landed here. The wholesale-rewrite law was realized at two sites and discriminated at neither: deleting the temp-and-rename swap alone, or the build's up-front invalidation alone, left every point green. The law's example space now carries when the stale page is planted and which command runs, and three sibling points close the gap — a page planted after the build has already invalidated the view (so only the swap can evict it), a run whose carrier the extractor refuses (so only the failed-run removal can take the view down), and a build that renders no view at all (so only the up-front invalidation can). Each single-site deletion now reddens exactly one point. The binding-language rule named both aggregate surfaces but only the index table was exercised; a pack-member point now reads the member table's cells for a bound member beside an unbound one, so a yes/no shorthand cannot pass. The diagnostic rendering rule named two entrypoints and bound one; the Design Review twin now runs through renderFindings under the same composition rule, covering the em-dash cell and the table-escaped message. RenderedFinding is deleted: an exhaustive search finds no producer anywhere in the tree and no barrel export, so formatFinding narrows to Finding and the file the Spec points a reader at no longer declares a second shape. The frozen GWT result key gains a comment stating the constraint in force. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- ...ng-language-views.pack-member-table.sdp.md | 21 ++ specs/consumers/binding-language-views.sdp.md | 2 + ...view-rewrite.build-invalidates-view.sdp.md | 23 ++ ...iew-rewrite.failed-run-view-removed.sdp.md | 23 ++ ...lesale-view-rewrite.late-stale-page.sdp.md | 24 ++ specs/consumers/wholesale-view-rewrite.sdp.md | 6 +- ...ale-view-rewrite.stale-page-removed.sdp.md | 4 +- specs/self-hosting.pack.sdp.ts | 5 + ...gnostic-rendering.composed-location.sdp.md | 2 +- specs/validation/diagnostic-rendering.sdp.md | 5 +- ...ostic-rendering.table-cell-location.sdp.md | 21 ++ src/cli/output.ts | 10 +- src/extract/markdown-body-owner-behavior.ts | 3 + test/self-hosting-graph.test.ts | 14 +- test/self-hosting-oracle/anchors.ts | 50 ++++ test/self-hosting-oracle/consumers.ts | 143 ++++++++++- .../self-hosting-oracle/declared-relations.ts | 50 ++++ test/self-hosting-oracle/pack-members.ts | 5 + test/self-hosting-oracle/validation.ts | 44 +++- test/self-hosting-projections.test.ts | 237 ++++++++++++++++-- 20 files changed, 649 insertions(+), 43 deletions(-) create mode 100644 specs/consumers/binding-language-views.pack-member-table.sdp.md create mode 100644 specs/consumers/wholesale-view-rewrite.build-invalidates-view.sdp.md create mode 100644 specs/consumers/wholesale-view-rewrite.failed-run-view-removed.sdp.md create mode 100644 specs/consumers/wholesale-view-rewrite.late-stale-page.sdp.md create mode 100644 specs/validation/diagnostic-rendering.table-cell-location.sdp.md diff --git a/specs/consumers/binding-language-views.pack-member-table.sdp.md b/specs/consumers/binding-language-views.pack-member-table.sdp.md new file mode 100644 index 0000000..9accf63 --- /dev/null +++ b/specs/consumers/binding-language-views.pack-member-table.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:consumers.binding-language-views.pack-member-table +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.binding-language-views + verifies: spec:consumers.binding-language-views +--- +# The pack member table speaks the page's binding language, not a shorthand + +## Intent +- outcome: Execute the aggregate half of the rule on the surface a reviewer reads a whole pack from, where a two-column yes/no shorthand would be cheapest to reach for. + +```gwt +Given the graph holds a spec {specId: "spec:probe.bound-surface"} bound by {bindings: "an implementing code anchor and a verifying test anchor"} +Given the graph holds a pack {packId: "pack:probe.review-aggregate"} listing that spec beside an unbound member +When the Design Review renders the graph +Then the pack member table repeats those binding values for the spec: {memberTableRepeats: true} +Then the internal delivery-fact name {factName: "implemented"} appears as rendered label text: {factNameRendered: false} +``` diff --git a/specs/consumers/binding-language-views.sdp.md b/specs/consumers/binding-language-views.sdp.md index 0498053..362094b 100644 --- a/specs/consumers/binding-language-views.sdp.md +++ b/specs/consumers/binding-language-views.sdp.md @@ -24,10 +24,12 @@ relations: ## Example space ```gwt-vocabulary Given the graph holds a spec {specId:string} bound by {bindings:"an implementing code anchor and a verifying test anchor"|"no anchor at all"} +Given the graph holds a pack {packId:string} listing that spec beside an unbound member When the Design Review renders the graph Then the spec page renders the implementation binding as {implementation:"present"|"none"} Then the spec page renders the verifier binding as {verifier:"present"|"none"} Then the spec page renders the runtime observation as {observation:string} Then the index table repeats those binding values for the spec: {tableRepeats:boolean} +Then the pack member table repeats those binding values for the spec: {memberTableRepeats:boolean} Then the internal delivery-fact name {factName:string} appears as rendered label text: {factNameRendered:boolean} ``` diff --git a/specs/consumers/wholesale-view-rewrite.build-invalidates-view.sdp.md b/specs/consumers/wholesale-view-rewrite.build-invalidates-view.sdp.md new file mode 100644 index 0000000..571f9e4 --- /dev/null +++ b/specs/consumers/wholesale-view-rewrite.build-invalidates-view.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.wholesale-view-rewrite.build-invalidates-view +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.wholesale-view-rewrite + verifies: spec:consumers.wholesale-view-rewrite +--- +# A build that never renders still takes the old view down + +## Intent +- outcome: Execute the up-front half of the invalidation on a command that writes no view, where a surviving directory would describe a graph that has moved. + +```gwt +Given an extraction root holding {corpus: "one authored spec"} and a stale view page {stalePage: "spec/probe.departed.md"} +Given the stale page is planted {planted: "before the run"} +When the {command: "build"} command runs at that root +Then the run exits {exitCode: 0} +Then the view directory survives: {viewSurvives: false} +Then the stale page survives: {staleSurvives: false} +Then a temporary view sibling survives: {temporarySurvives: false} +``` diff --git a/specs/consumers/wholesale-view-rewrite.failed-run-view-removed.sdp.md b/specs/consumers/wholesale-view-rewrite.failed-run-view-removed.sdp.md new file mode 100644 index 0000000..5a34143 --- /dev/null +++ b/specs/consumers/wholesale-view-rewrite.failed-run-view-removed.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.wholesale-view-rewrite.failed-run-view-removed +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.wholesale-view-rewrite + verifies: spec:consumers.wholesale-view-rewrite +--- +# A run that cannot produce a current view leaves no view at all + +## Intent +- outcome: Execute the honest-absence half of the law on a run that fails before it can render, where leaving the old view would read as current. + +```gwt +Given an extraction root holding {corpus: "one authored spec the extractor refuses"} and a stale view page {stalePage: "spec/probe.departed.md"} +Given the stale page is planted {planted: "after the build has invalidated the view"} +When the {command: "view"} command runs at that root +Then the run exits {exitCode: 1} +Then the view directory survives: {viewSurvives: false} +Then the stale page survives: {staleSurvives: false} +Then a temporary view sibling survives: {temporarySurvives: false} +``` diff --git a/specs/consumers/wholesale-view-rewrite.late-stale-page.sdp.md b/specs/consumers/wholesale-view-rewrite.late-stale-page.sdp.md new file mode 100644 index 0000000..ec6c2ba --- /dev/null +++ b/specs/consumers/wholesale-view-rewrite.late-stale-page.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.wholesale-view-rewrite.late-stale-page +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.wholesale-view-rewrite + verifies: spec:consumers.wholesale-view-rewrite +--- +# A page the build's invalidation never saw still does not survive the swap + +## Intent +- outcome: Execute the swap against a page the up-front invalidation cannot have removed, so the rename into place is what evicts it. + +```gwt +Given an extraction root holding {corpus: "one authored spec"} and a stale view page {stalePage: "spec/probe.departed.md"} +Given the stale page is planted {planted: "after the build has invalidated the view"} +When the {command: "view"} command runs at that root +Then the run exits {exitCode: 0} +Then the view directory survives: {viewSurvives: true} +Then the view holds the current page {currentPage: "index.md"} +Then the stale page survives: {staleSurvives: false} +Then a temporary view sibling survives: {temporarySurvives: false} +``` diff --git a/specs/consumers/wholesale-view-rewrite.sdp.md b/specs/consumers/wholesale-view-rewrite.sdp.md index 68d83f8..412b98b 100644 --- a/specs/consumers/wholesale-view-rewrite.sdp.md +++ b/specs/consumers/wholesale-view-rewrite.sdp.md @@ -23,9 +23,11 @@ relations: ## Example space ```gwt-vocabulary -Given an extraction root holding {corpus:string} and a stale view page {stalePage:string} -When the view is rendered at that root +Given an extraction root holding {corpus:"one authored spec"|"one authored spec the extractor refuses"} and a stale view page {stalePage:string} +Given the stale page is planted {planted:"before the run"|"after the build has invalidated the view"} +When the {command:"view"|"build"} command runs at that root Then the run exits {exitCode:number} +Then the view directory survives: {viewSurvives:boolean} Then the view holds the current page {currentPage:string} Then the stale page survives: {staleSurvives:boolean} Then a temporary view sibling survives: {temporarySurvives:boolean} diff --git a/specs/consumers/wholesale-view-rewrite.stale-page-removed.sdp.md b/specs/consumers/wholesale-view-rewrite.stale-page-removed.sdp.md index 6772283..ea55d6f 100644 --- a/specs/consumers/wholesale-view-rewrite.stale-page-removed.sdp.md +++ b/specs/consumers/wholesale-view-rewrite.stale-page-removed.sdp.md @@ -14,8 +14,10 @@ relations: ```gwt Given an extraction root holding {corpus: "one authored spec"} and a stale view page {stalePage: "spec/probe.departed.md"} -When the view is rendered at that root +Given the stale page is planted {planted: "before the run"} +When the {command: "view"} command runs at that root Then the run exits {exitCode: 0} +Then the view directory survives: {viewSurvives: true} Then the view holds the current page {currentPage: "index.md"} Then the stale page survives: {staleSurvives: false} Then a temporary view sibling survives: {temporarySurvives: false} diff --git a/specs/self-hosting.pack.sdp.ts b/specs/self-hosting.pack.sdp.ts index 8d2a74e..2856468 100644 --- a/specs/self-hosting.pack.sdp.ts +++ b/specs/self-hosting.pack.sdp.ts @@ -79,14 +79,19 @@ export const selfHostingV1Pack = pack({ ref("spec:validation.kind-evidence.empty-promoted-child"), ref("spec:validation.diagnostic-rendering"), ref("spec:validation.diagnostic-rendering.composed-location"), + ref("spec:validation.diagnostic-rendering.table-cell-location"), ref("spec:validation.validator-self-testing"), ref("spec:consumers.derived-readiness-banner"), ref("spec:consumers.derived-readiness-banner.dishonest-divergence"), ref("spec:consumers.derived-readiness-banner.honest-headroom"), ref("spec:consumers.binding-language-views"), ref("spec:consumers.binding-language-views.bound-spec-page"), + ref("spec:consumers.binding-language-views.pack-member-table"), ref("spec:consumers.wholesale-view-rewrite"), ref("spec:consumers.wholesale-view-rewrite.stale-page-removed"), + ref("spec:consumers.wholesale-view-rewrite.late-stale-page"), + ref("spec:consumers.wholesale-view-rewrite.failed-run-view-removed"), + ref("spec:consumers.wholesale-view-rewrite.build-invalidates-view"), ref("spec:decisions.plain-language-references"), ref("spec:decisions.concept-docs-dissolve"), ref("spec:decisions.one-validation-path"), diff --git a/specs/validation/diagnostic-rendering.composed-location.sdp.md b/specs/validation/diagnostic-rendering.composed-location.sdp.md index 328638c..91702e1 100644 --- a/specs/validation/diagnostic-rendering.composed-location.sdp.md +++ b/specs/validation/diagnostic-rendering.composed-location.sdp.md @@ -14,7 +14,7 @@ relations: ```gwt Given a finding naming the validator {validatorId: "honesty/readiness-floor"} at severity {severity: "error"} carrying the message {message: "The stated rung is not earned."} -When the command-line renderer formats that finding once per location shape +When the {renderer: "command-line"} renderer formats that finding once per location shape Then the finding carrying the file {file: "specs/probe.sdp.md"} and the line {line: 7} renders {withLocation: "specs/probe.sdp.md:7 — [error] honesty/readiness-floor — The stated rung is not earned."} Then the same finding carrying the file alone renders {fileOnly: "specs/probe.sdp.md — [error] honesty/readiness-floor — The stated rung is not earned."} Then the same finding carrying neither renders {bare: "[error] honesty/readiness-floor — The stated rung is not earned."} diff --git a/specs/validation/diagnostic-rendering.sdp.md b/specs/validation/diagnostic-rendering.sdp.md index ce503e3..aebd0f4 100644 --- a/specs/validation/diagnostic-rendering.sdp.md +++ b/specs/validation/diagnostic-rendering.sdp.md @@ -23,8 +23,11 @@ relations: ## Example space ```gwt-vocabulary Given a finding naming the validator {validatorId:string} at severity {severity:"warning"|"error"} carrying the message {message:string} -When the command-line renderer formats that finding once per location shape +When the {renderer:"command-line"|"Design Review"} renderer formats that finding once per location shape Then the finding carrying the file {file:string} and the line {line:number} renders {withLocation:string} Then the same finding carrying the file alone renders {fileOnly:string} Then the same finding carrying neither renders {bare:string} +Then the findings row carrying the file {file:string} and the line {line:number} renders {locationRow:string} +Then the same row carrying the file alone renders {fileOnlyRow:string} +Then the same row carrying neither renders {absentRow:string} ``` diff --git a/specs/validation/diagnostic-rendering.table-cell-location.sdp.md b/specs/validation/diagnostic-rendering.table-cell-location.sdp.md new file mode 100644 index 0000000..a45d207 --- /dev/null +++ b/specs/validation/diagnostic-rendering.table-cell-location.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.diagnostic-rendering.table-cell-location +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.diagnostic-rendering + verifies: spec:validation.diagnostic-rendering +--- +# The same three location shapes, rendered as table cells + +## Intent +- outcome: Execute the one composition rule on the Design Review's findings table, where a cell cannot be absent and a message pipe would otherwise split the row. + +```gwt +Given a finding naming the validator {validatorId: "honesty/readiness-floor"} at severity {severity: "error"} carrying the message {message: "The stated rung is not earned | its floor refused it."} +When the {renderer: "Design Review"} renderer formats that finding once per location shape +Then the findings row carrying the file {file: "specs/probe.sdp.md"} and the line {line: 7} renders {locationRow: "| error | `honesty/readiness-floor` | The stated rung is not earned \| its floor refused it. | `specs/probe.sdp.md:7` |"} +Then the same row carrying the file alone renders {fileOnlyRow: "| error | `honesty/readiness-floor` | The stated rung is not earned \| its floor refused it. | `specs/probe.sdp.md` |"} +Then the same row carrying neither renders {absentRow: "| error | `honesty/readiness-floor` | The stated rung is not earned \| its floor refused it. | — |"} +``` diff --git a/src/cli/output.ts b/src/cli/output.ts index 56fe974..9a146a4 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -5,14 +5,6 @@ export interface CliOutput { readonly stderr?: { readonly write: (chunk: string) => void }; } -export interface RenderedFinding { - readonly validatorId: string; - readonly severity: string; - readonly message: string; - readonly file?: string; - readonly line?: number; -} - export const defaultCliOutput: CliOutput = { stdout: process.stdout, stderr: process.stderr, @@ -26,7 +18,7 @@ export function writeStderr(output: CliOutput, text: string): void { output.stderr?.write(text); } -export function formatFinding(finding: Finding | RenderedFinding): string { +export function formatFinding(finding: Finding): string { const location = finding.file === undefined ? "" diff --git a/src/extract/markdown-body-owner-behavior.ts b/src/extract/markdown-body-owner-behavior.ts index 9686c8a..d67380f 100644 --- a/src/extract/markdown-body-owner-behavior.ts +++ b/src/extract/markdown-body-owner-behavior.ts @@ -62,6 +62,9 @@ export function mapIntent( ), ); else { + // The GWT result key is the literal word `then`, written directly here and at the + // vocabulary site below — never assembled from fragments; nothing in the toolchain + // requires or rewards indirection, and the frozen key set stays greppable. const example: Record = { given: fence.steps.given, when: fence.steps.when, diff --git a/test/self-hosting-graph.test.ts b/test/self-hosting-graph.test.ts index b7c1a6d..cc9d756 100644 --- a/test/self-hosting-graph.test.ts +++ b/test/self-hosting-graph.test.ts @@ -90,12 +90,12 @@ describe("the self-hosting corpus", () => { // The literals are the corpus checkpoint. The authored arrays are measured against the same // literals rather than standing in for them, so a transcription slip in an oracle module // cannot certify itself by moving both sides of a comparison at once. - expect(result.counts).toEqual({ specs: 103, packs: 1, anchors: 75 }); - expect(expectedSpecs).toHaveLength(103); - expect(expectedPackMembers).toHaveLength(103); - expect(expectedAnchors).toHaveLength(75); - expect(result.graph.nodes).toHaveLength(179); - expect(result.graph.edges).toHaveLength(351); + expect(result.counts).toEqual({ specs: 108, packs: 1, anchors: 80 }); + expect(expectedSpecs).toHaveLength(108); + expect(expectedPackMembers).toHaveLength(108); + expect(expectedAnchors).toHaveLength(80); + expect(result.graph.nodes).toHaveLength(189); + expect(result.graph.edges).toHaveLength(371); }); it("rosters exactly the authored Spec, Pack, and anchor node ids", () => { @@ -145,7 +145,7 @@ describe("the self-hosting corpus", () => { }), {}, ), - ).toEqual({ defined: 37, ready: 66 }); + ).toEqual({ defined: 37, ready: 71 }); }); it("derives the Pack membership edges from the manifest, in manifest order", () => { diff --git a/test/self-hosting-oracle/anchors.ts b/test/self-hosting-oracle/anchors.ts index c14c67c..9ed3948 100644 --- a/test/self-hosting-oracle/anchors.ts +++ b/test/self-hosting-oracle/anchors.ts @@ -733,6 +733,16 @@ export const expectedAnchors = [ constant: "boundSpecPageTestAnchor", site: "bindExample(boundSpecPageContract", }, + { + id: "test:protocol.binding-language-views.pack-member-table", + nodeType: "Anchor", + label: "the pack-member point verifies the aggregate half of the rendered binding vocabulary", + type: "verifies", + target: "spec:consumers.binding-language-views.pack-member-table", + file: "test/self-hosting-projections.test.ts", + constant: "packMemberTableTestAnchor", + site: "bindExample(packMemberTableContract", + }, { id: "test:protocol.wholesale-view-rewrite.stale-page-removed", nodeType: "Anchor", @@ -743,6 +753,36 @@ export const expectedAnchors = [ constant: "stalePageRemovedTestAnchor", site: "bindExample(stalePageRemovedContract", }, + { + id: "test:protocol.wholesale-view-rewrite.late-stale-page", + nodeType: "Anchor", + label: "the late-page point verifies that the swap itself evicts what invalidation missed", + type: "verifies", + target: "spec:consumers.wholesale-view-rewrite.late-stale-page", + file: "test/self-hosting-projections.test.ts", + constant: "lateStalePageTestAnchor", + site: "bindExample(lateStalePageContract", + }, + { + id: "test:protocol.wholesale-view-rewrite.failed-run-view-removed", + nodeType: "Anchor", + label: "the failed-run point verifies that an uncertifiable view is removed, not left readable", + type: "verifies", + target: "spec:consumers.wholesale-view-rewrite.failed-run-view-removed", + file: "test/self-hosting-projections.test.ts", + constant: "failedRunViewRemovedTestAnchor", + site: "bindExample(failedRunViewRemovedContract", + }, + { + id: "test:protocol.wholesale-view-rewrite.build-invalidates-view", + nodeType: "Anchor", + label: "the build point verifies the up-front invalidation on a command that renders no view", + type: "verifies", + target: "spec:consumers.wholesale-view-rewrite.build-invalidates-view", + file: "test/self-hosting-projections.test.ts", + constant: "buildInvalidatesViewTestAnchor", + site: "bindExample(buildInvalidatesViewContract", + }, { id: "test:protocol.diagnostic-rendering.composed-location", nodeType: "Anchor", @@ -753,4 +793,14 @@ export const expectedAnchors = [ constant: "composedLocationTestAnchor", site: "bindExample(composedLocationContract", }, + { + id: "test:protocol.diagnostic-rendering.table-cell-location", + nodeType: "Anchor", + label: "the table-cell point verifies the same composition rule on the Design Review's twin", + type: "verifies", + target: "spec:validation.diagnostic-rendering.table-cell-location", + file: "test/self-hosting-projections.test.ts", + constant: "tableCellLocationTestAnchor", + site: "bindExample(tableCellLocationContract", + }, ] as const; diff --git a/test/self-hosting-oracle/consumers.ts b/test/self-hosting-oracle/consumers.ts index a6645cd..881c8bf 100644 --- a/test/self-hosting-oracle/consumers.ts +++ b/test/self-hosting-oracle/consumers.ts @@ -268,6 +268,7 @@ export const consumersSpecs = [ exampleSpace: { given: [ 'the graph holds a spec {specId:string} bound by {bindings:"an implementing code anchor and a verifying test anchor"|"no anchor at all"}', + "the graph holds a pack {packId:string} listing that spec beside an unbound member", ], when: ["the Design Review renders the graph"], then: [ @@ -275,6 +276,7 @@ export const consumersSpecs = [ 'the spec page renders the verifier binding as {verifier:"present"|"none"}', "the spec page renders the runtime observation as {observation:string}", "the index table repeats those binding values for the spec: {tableRepeats:boolean}", + "the pack member table repeats those binding values for the spec: {memberTableRepeats:boolean}", "the internal delivery-fact name {factName:string} appears as rendered label text: {factNameRendered:boolean}", ], }, @@ -315,6 +317,37 @@ export const consumersSpecs = [ }, deliveryFacts: ["has-verifier"], }, + { + id: "spec:consumers.binding-language-views.pack-member-table", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/binding-language-views.pack-member-table.sdp.md", + title: "The pack member table speaks the page's binding language, not a shorthand", + narrative: null, + sections: { + intent: { + outcome: + "Execute the aggregate half of the rule on the surface a reviewer reads a whole pack from, where a two-column yes/no shorthand would be cheapest to reach for.", + }, + behavior: { + examples: [ + { + given: [ + 'the graph holds a spec {specId: "spec:probe.bound-surface"} bound by {bindings: "an implementing code anchor and a verifying test anchor"}', + 'the graph holds a pack {packId: "pack:probe.review-aggregate"} listing that spec beside an unbound member', + ], + when: ["the Design Review renders the graph"], + then: [ + "the pack member table repeats those binding values for the spec: {memberTableRepeats: true}", + 'the internal delivery-fact name {factName: "implemented"} appears as rendered label text: {factNameRendered: false}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, { id: "spec:consumers.wholesale-view-rewrite", specKind: "rule", @@ -340,11 +373,13 @@ export const consumersSpecs = [ ], exampleSpace: { given: [ - "an extraction root holding {corpus:string} and a stale view page {stalePage:string}", + 'an extraction root holding {corpus:"one authored spec"|"one authored spec the extractor refuses"} and a stale view page {stalePage:string}', + 'the stale page is planted {planted:"before the run"|"after the build has invalidated the view"}', ], - when: ["the view is rendered at that root"], + when: ['the {command:"view"|"build"} command runs at that root'], then: [ "the run exits {exitCode:number}", + "the view directory survives: {viewSurvives:boolean}", "the view holds the current page {currentPage:string}", "the stale page survives: {staleSurvives:boolean}", "a temporary view sibling survives: {temporarySurvives:boolean}", @@ -372,10 +407,46 @@ export const consumersSpecs = [ { given: [ 'an extraction root holding {corpus: "one authored spec"} and a stale view page {stalePage: "spec/probe.departed.md"}', + 'the stale page is planted {planted: "before the run"}', + ], + when: ['the {command: "view"} command runs at that root'], + then: [ + "the run exits {exitCode: 0}", + "the view directory survives: {viewSurvives: true}", + 'the view holds the current page {currentPage: "index.md"}', + "the stale page survives: {staleSurvives: false}", + "a temporary view sibling survives: {temporarySurvives: false}", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.wholesale-view-rewrite.late-stale-page", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/wholesale-view-rewrite.late-stale-page.sdp.md", + title: "A page the build's invalidation never saw still does not survive the swap", + narrative: null, + sections: { + intent: { + outcome: + "Execute the swap against a page the up-front invalidation cannot have removed, so the rename into place is what evicts it.", + }, + behavior: { + examples: [ + { + given: [ + 'an extraction root holding {corpus: "one authored spec"} and a stale view page {stalePage: "spec/probe.departed.md"}', + 'the stale page is planted {planted: "after the build has invalidated the view"}', ], - when: ["the view is rendered at that root"], + when: ['the {command: "view"} command runs at that root'], then: [ "the run exits {exitCode: 0}", + "the view directory survives: {viewSurvives: true}", 'the view holds the current page {currentPage: "index.md"}', "the stale page survives: {staleSurvives: false}", "a temporary view sibling survives: {temporarySurvives: false}", @@ -386,4 +457,70 @@ export const consumersSpecs = [ }, deliveryFacts: ["has-verifier"], }, + { + id: "spec:consumers.wholesale-view-rewrite.failed-run-view-removed", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/wholesale-view-rewrite.failed-run-view-removed.sdp.md", + title: "A run that cannot produce a current view leaves no view at all", + narrative: null, + sections: { + intent: { + outcome: + "Execute the honest-absence half of the law on a run that fails before it can render, where leaving the old view would read as current.", + }, + behavior: { + examples: [ + { + given: [ + 'an extraction root holding {corpus: "one authored spec the extractor refuses"} and a stale view page {stalePage: "spec/probe.departed.md"}', + 'the stale page is planted {planted: "after the build has invalidated the view"}', + ], + when: ['the {command: "view"} command runs at that root'], + then: [ + "the run exits {exitCode: 1}", + "the view directory survives: {viewSurvives: false}", + "the stale page survives: {staleSurvives: false}", + "a temporary view sibling survives: {temporarySurvives: false}", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.wholesale-view-rewrite.build-invalidates-view", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/wholesale-view-rewrite.build-invalidates-view.sdp.md", + title: "A build that never renders still takes the old view down", + narrative: null, + sections: { + intent: { + outcome: + "Execute the up-front half of the invalidation on a command that writes no view, where a surviving directory would describe a graph that has moved.", + }, + behavior: { + examples: [ + { + given: [ + 'an extraction root holding {corpus: "one authored spec"} and a stale view page {stalePage: "spec/probe.departed.md"}', + 'the stale page is planted {planted: "before the run"}', + ], + when: ['the {command: "build"} command runs at that root'], + then: [ + "the run exits {exitCode: 0}", + "the view directory survives: {viewSurvives: false}", + "the stale page survives: {staleSurvives: false}", + "a temporary view sibling survives: {temporarySurvives: false}", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, ] as const; diff --git a/test/self-hosting-oracle/declared-relations.ts b/test/self-hosting-oracle/declared-relations.ts index a770b5d..17288f7 100644 --- a/test/self-hosting-oracle/declared-relations.ts +++ b/test/self-hosting-oracle/declared-relations.ts @@ -351,6 +351,16 @@ export const expectedDeclaredRelations = [ "verifies", "spec:consumers.binding-language-views", ], + [ + "spec:consumers.binding-language-views.pack-member-table", + "refines", + "spec:consumers.binding-language-views", + ], + [ + "spec:consumers.binding-language-views.pack-member-table", + "verifies", + "spec:consumers.binding-language-views", + ], ["spec:consumers.wholesale-view-rewrite", "refines", "spec:consumers.design-review"], ["spec:consumers.wholesale-view-rewrite", "dependsOn", "spec:extraction.determinism"], [ @@ -363,6 +373,36 @@ export const expectedDeclaredRelations = [ "verifies", "spec:consumers.wholesale-view-rewrite", ], + [ + "spec:consumers.wholesale-view-rewrite.late-stale-page", + "refines", + "spec:consumers.wholesale-view-rewrite", + ], + [ + "spec:consumers.wholesale-view-rewrite.late-stale-page", + "verifies", + "spec:consumers.wholesale-view-rewrite", + ], + [ + "spec:consumers.wholesale-view-rewrite.failed-run-view-removed", + "refines", + "spec:consumers.wholesale-view-rewrite", + ], + [ + "spec:consumers.wholesale-view-rewrite.failed-run-view-removed", + "verifies", + "spec:consumers.wholesale-view-rewrite", + ], + [ + "spec:consumers.wholesale-view-rewrite.build-invalidates-view", + "refines", + "spec:consumers.wholesale-view-rewrite", + ], + [ + "spec:consumers.wholesale-view-rewrite.build-invalidates-view", + "verifies", + "spec:consumers.wholesale-view-rewrite", + ], ["spec:validation.diagnostic-rendering", "refines", "spec:validation.two-check-families"], ["spec:validation.diagnostic-rendering", "dependsOn", "spec:consumers.design-review"], [ @@ -375,5 +415,15 @@ export const expectedDeclaredRelations = [ "verifies", "spec:validation.diagnostic-rendering", ], + [ + "spec:validation.diagnostic-rendering.table-cell-location", + "refines", + "spec:validation.diagnostic-rendering", + ], + [ + "spec:validation.diagnostic-rendering.table-cell-location", + "verifies", + "spec:validation.diagnostic-rendering", + ], ["spec:validation.validator-self-testing", "refines", "spec:validation.two-check-families"], ] as const; diff --git a/test/self-hosting-oracle/pack-members.ts b/test/self-hosting-oracle/pack-members.ts index 1017758..0deb301 100644 --- a/test/self-hosting-oracle/pack-members.ts +++ b/test/self-hosting-oracle/pack-members.ts @@ -76,14 +76,19 @@ export const expectedPackMembers = [ "spec:validation.kind-evidence.empty-promoted-child", "spec:validation.diagnostic-rendering", "spec:validation.diagnostic-rendering.composed-location", + "spec:validation.diagnostic-rendering.table-cell-location", "spec:validation.validator-self-testing", "spec:consumers.derived-readiness-banner", "spec:consumers.derived-readiness-banner.dishonest-divergence", "spec:consumers.derived-readiness-banner.honest-headroom", "spec:consumers.binding-language-views", "spec:consumers.binding-language-views.bound-spec-page", + "spec:consumers.binding-language-views.pack-member-table", "spec:consumers.wholesale-view-rewrite", "spec:consumers.wholesale-view-rewrite.stale-page-removed", + "spec:consumers.wholesale-view-rewrite.late-stale-page", + "spec:consumers.wholesale-view-rewrite.failed-run-view-removed", + "spec:consumers.wholesale-view-rewrite.build-invalidates-view", "spec:decisions.plain-language-references", "spec:decisions.concept-docs-dissolve", "spec:decisions.one-validation-path", diff --git a/test/self-hosting-oracle/validation.ts b/test/self-hosting-oracle/validation.ts index 59075e8..347ec9b 100644 --- a/test/self-hosting-oracle/validation.ts +++ b/test/self-hosting-oracle/validation.ts @@ -936,11 +936,16 @@ export const validationSpecs = [ given: [ 'a finding naming the validator {validatorId:string} at severity {severity:"warning"|"error"} carrying the message {message:string}', ], - when: ["the command-line renderer formats that finding once per location shape"], + when: [ + 'the {renderer:"command-line"|"Design Review"} renderer formats that finding once per location shape', + ], then: [ "the finding carrying the file {file:string} and the line {line:number} renders {withLocation:string}", "the same finding carrying the file alone renders {fileOnly:string}", "the same finding carrying neither renders {bare:string}", + "the findings row carrying the file {file:string} and the line {line:number} renders {locationRow:string}", + "the same row carrying the file alone renders {fileOnlyRow:string}", + "the same row carrying neither renders {absentRow:string}", ], }, }, @@ -966,7 +971,9 @@ export const validationSpecs = [ given: [ 'a finding naming the validator {validatorId: "honesty/readiness-floor"} at severity {severity: "error"} carrying the message {message: "The stated rung is not earned."}', ], - when: ["the command-line renderer formats that finding once per location shape"], + when: [ + 'the {renderer: "command-line"} renderer formats that finding once per location shape', + ], then: [ 'the finding carrying the file {file: "specs/probe.sdp.md"} and the line {line: 7} renders {withLocation: "specs/probe.sdp.md:7 — [error] honesty/readiness-floor — The stated rung is not earned."}', 'the same finding carrying the file alone renders {fileOnly: "specs/probe.sdp.md — [error] honesty/readiness-floor — The stated rung is not earned."}', @@ -978,6 +985,39 @@ export const validationSpecs = [ }, deliveryFacts: ["has-verifier"], }, + { + id: "spec:validation.diagnostic-rendering.table-cell-location", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/validation/diagnostic-rendering.table-cell-location.sdp.md", + title: "The same three location shapes, rendered as table cells", + narrative: null, + sections: { + intent: { + outcome: + "Execute the one composition rule on the Design Review's findings table, where a cell cannot be absent and a message pipe would otherwise split the row.", + }, + behavior: { + examples: [ + { + given: [ + 'a finding naming the validator {validatorId: "honesty/readiness-floor"} at severity {severity: "error"} carrying the message {message: "The stated rung is not earned | its floor refused it."}', + ], + when: [ + 'the {renderer: "Design Review"} renderer formats that finding once per location shape', + ], + then: [ + 'the findings row carrying the file {file: "specs/probe.sdp.md"} and the line {line: 7} renders {locationRow: "| error | `honesty/readiness-floor` | The stated rung is not earned \\| its floor refused it. | `specs/probe.sdp.md:7` |"}', + 'the same row carrying the file alone renders {fileOnlyRow: "| error | `honesty/readiness-floor` | The stated rung is not earned \\| its floor refused it. | `specs/probe.sdp.md` |"}', + 'the same row carrying neither renders {absentRow: "| error | `honesty/readiness-floor` | The stated rung is not earned \\| its floor refused it. | — |"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, { id: "spec:validation.validator-self-testing", specKind: "rule", diff --git a/test/self-hosting-projections.test.ts b/test/self-hosting-projections.test.ts index ca75a59..9a35917 100644 --- a/test/self-hosting-projections.test.ts +++ b/test/self-hosting-projections.test.ts @@ -8,14 +8,21 @@ import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protoc import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; import { boundSpecPageContract } from "../generated/contracts/consumers.binding-language-views.bound-spec-page.contract.js"; +import { packMemberTableContract } from "../generated/contracts/consumers.binding-language-views.pack-member-table.contract.js"; import { dishonestDivergenceContract } from "../generated/contracts/consumers.derived-readiness-banner.dishonest-divergence.contract.js"; import { honestHeadroomContract } from "../generated/contracts/consumers.derived-readiness-banner.honest-headroom.contract.js"; +import { buildInvalidatesViewContract } from "../generated/contracts/consumers.wholesale-view-rewrite.build-invalidates-view.contract.js"; +import { failedRunViewRemovedContract } from "../generated/contracts/consumers.wholesale-view-rewrite.failed-run-view-removed.contract.js"; +import { lateStalePageContract } from "../generated/contracts/consumers.wholesale-view-rewrite.late-stale-page.contract.js"; import { stalePageRemovedContract } from "../generated/contracts/consumers.wholesale-view-rewrite.stale-page-removed.contract.js"; import { composedLocationContract } from "../generated/contracts/validation.diagnostic-rendering.composed-location.contract.js"; +import { tableCellLocationContract } from "../generated/contracts/validation.diagnostic-rendering.table-cell-location.contract.js"; import { codeAnchor, codeAnchorId, createReader, + pack as probePack, + packId as probePackId, refines, renderDesignReview, spec, @@ -24,8 +31,11 @@ import { testAnchorId as probeTestAnchorId, } from "../src/index.js"; import type { DesignReviewPage, Finding, SpecReadiness } from "../src/index.js"; +import { runBuild } from "../src/cli/build-command.js"; import { formatFinding } from "../src/cli/output.js"; import { runView } from "../src/cli/validate-view-command.js"; +import { extract } from "../src/extract/index.js"; +import { renderFindings } from "../src/projections/design-review-context.js"; import { deriveFixtureGraph } from "./helpers/fixture-graph.js"; /** @@ -205,11 +215,12 @@ const BINDING_PARENT_ID = "spec:probe.binding-parent"; interface BindingWorld { specId: string; bound: boolean; + packId: string; pages: readonly DesignReviewPage[] | undefined; } function bindingWorld(): BindingWorld { - return { specId: "", bound: false, pages: undefined }; + return { specId: "", bound: false, packId: "", pages: undefined }; } function bindingPages(world: BindingWorld): readonly DesignReviewPage[] { @@ -240,6 +251,27 @@ function indexRowOf(world: BindingWorld): string { return row; } +/** A member's row in the pack member table — the other aggregate surface rule 5 names. */ +function memberRowOf(world: BindingWorld, memberId: string): string { + const packPage = `pack/${world.packId.slice(world.packId.indexOf(":") + 1)}.md`; + const row = pageOf(bindingPages(world), packPage) + .split("\n") + .find((line) => line.startsWith(`| [\`${memberId}\`]`)); + + if (row === undefined) { + throw new Error(`The pack member table is missing a row for "${memberId}".`); + } + + return row; +} + +/** Every aggregate surface the world actually rendered, beside the spec page itself. */ +function renderedSurfaces(world: BindingWorld): readonly string[] { + return world.packId === "" + ? [bindingPage(world), indexRowOf(world)] + : [bindingPage(world), indexRowOf(world), memberRowOf(world, world.specId)]; +} + const bindingLanguageBindings = { "the graph holds a spec {specId} bound by {bindings}": ( world: BindingWorld, @@ -253,6 +285,12 @@ const bindingLanguageBindings = { world.specId = params.specId; world.bound = params.bindings === "an implementing code anchor and a verifying test anchor"; }, + "the graph holds a pack {packId} listing that spec beside an unbound member": ( + world: BindingWorld, + params: { readonly packId: string }, + ) => { + world.packId = params.packId; + }, "the Design Review renders the graph": (world: BindingWorld) => { const parent = spec({ id: specId(BINDING_PARENT_ID), @@ -291,8 +329,22 @@ const bindingLanguageBindings = { ] : []; + // The pack lists the bound subject beside the unbound parent, so the member table has to + // render both binding values and a shorthand cannot pass by matching one of them. + const packs = + world.packId === "" + ? [] + : [ + probePack({ + id: probePackId(world.packId), + title: "Probe aggregate for the rendered binding vocabulary", + framing: "One bound member and one unbound member, read as one review set.", + specs: [specId(world.specId), specId(BINDING_PARENT_ID)], + }), + ]; + world.pages = renderDesignReview( - createReader(deriveFixtureGraph({ specs: [parent, subject], anchors })), + createReader(deriveFixtureGraph({ specs: [parent, subject], packs, anchors })), ); }, "the spec page renders the implementation binding as {implementation}": ( @@ -323,12 +375,25 @@ const bindingLanguageBindings = { expect(columns).toBe(params.tableRepeats); }, + "the pack member table repeats those binding values for the spec: {memberTableRepeats}": ( + world: BindingWorld, + params: { readonly memberTableRepeats: boolean }, + ) => { + expect(memberRowOf(world, world.specId).endsWith("| present | present |")).toBe( + params.memberTableRepeats, + ); + // The negative beside the positive: the unbound member's cells read the other word of the + // same pair, so a table that hard-coded one value could not satisfy both rows. + expect(memberRowOf(world, BINDING_PARENT_ID).endsWith("| none | none |")).toBe( + params.memberTableRepeats, + ); + }, "the internal delivery-fact name {factName} appears as rendered label text: {factNameRendered}": ( world: BindingWorld, params: { readonly factName: string; readonly factNameRendered: boolean }, ) => { // The probe authors none of these words itself, so every occurrence would be the renderer's. - for (const surface of [bindingPage(world), indexRowOf(world)]) { + for (const surface of renderedSurfaces(world)) { expect(surface.includes(params.factName)).toBe(params.factNameRendered); expect(surface.includes("has-verifier")).toBe(params.factNameRendered); } @@ -344,6 +409,15 @@ void boundSpecPageTestAnchor; bindExample(boundSpecPageContract, bindingWorld, bindingLanguageBindings); +const packMemberTableTestAnchor = specTest({ + id: testAnchorId("test:protocol.binding-language-views.pack-member-table"), + label: "the pack-member point verifies the aggregate half of the rendered binding vocabulary", + verifies: ref("spec:consumers.binding-language-views.pack-member-table"), +}); +void packMemberTableTestAnchor; + +bindExample(packMemberTableContract, bindingWorld, bindingLanguageBindings); + /* ----- spec:consumers.wholesale-view-rewrite ----- */ const PROBE_CARRIER = `--- @@ -359,9 +433,24 @@ relations: {} - outcome: Give the view one page to render over a temporary extraction root. `; +/** The same shape, with an id the grammar refuses — one hard extraction error, nothing else. */ +const REFUSED_CARRIER = `--- +id: probe-view-subject +kind: rule +altitude: story +readiness: idea +relations: {} +--- +# A carrier the extractor refuses + +## Intent +- outcome: Fail extraction so the run cannot produce a current view. +`; + interface ViewWorld { readonly root: string; stalePage: string; + plantLate: boolean; exitCode: number | undefined; } @@ -369,34 +458,83 @@ function viewWorld(): ViewWorld { const root = mkdtempSync(join(tmpdir(), "sdp-self-hosting-view-")); temporaryRoots.add(root); - return { root, stalePage: "", exitCode: undefined }; + return { root, stalePage: "", plantLate: false, exitCode: undefined }; } function viewPathOf(world: ViewWorld): string { return join(world.root, "generated", "design-review"); } +/** + * The planted page names a subject the corpus does not hold, so nothing the run writes can + * overwrite it: it survives only if a run is allowed to leave an earlier one's output behind. + */ +function plantStalePage(world: ViewWorld): void { + const stalePath = join(viewPathOf(world), ...world.stalePage.split("/")); + mkdirSync(dirname(stalePath), { recursive: true }); + writeFileSync(stalePath, "# A spec the corpus no longer holds\n", "utf8"); +} + const wholesaleRewriteBindings = { "an extraction root holding {corpus} and a stale view page {stalePage}": ( world: ViewWorld, - params: { readonly corpus: string; readonly stalePage: string }, + params: { + readonly corpus: "one authored spec" | "one authored spec the extractor refuses"; + readonly stalePage: string; + }, ) => { mkdirSync(join(world.root, "specs"), { recursive: true }); - writeFileSync(join(world.root, "specs", "probe.sdp.md"), PROBE_CARRIER, "utf8"); - - // The planted page names a subject the corpus does not hold, so nothing the run writes can - // overwrite it: it survives only if a run is allowed to leave an earlier one's output behind. + writeFileSync( + join(world.root, "specs", "probe.sdp.md"), + params.corpus === "one authored spec" ? PROBE_CARRIER : REFUSED_CARRIER, + "utf8", + ); world.stalePage = params.stalePage; - const stalePath = join(viewPathOf(world), ...params.stalePage.split("/")); - mkdirSync(dirname(stalePath), { recursive: true }); - writeFileSync(stalePath, "# A spec the corpus no longer holds\n", "utf8"); }, - "the view is rendered at that root": (world: ViewWorld) => { - world.exitCode = runView({ root: world.root, exclude: [], checkClean: false }, {}, {}); + "the stale page is planted {planted}": ( + world: ViewWorld, + params: { + readonly planted: "before the run" | "after the build has invalidated the view"; + }, + ) => { + world.plantLate = params.planted === "after the build has invalidated the view"; + + if (!world.plantLate) { + plantStalePage(world); + } + }, + "the {command} command runs at that root": ( + world: ViewWorld, + params: { readonly command: "view" | "build" }, + ) => { + const parsed = { root: world.root, exclude: [], checkClean: false }; + // Late planting rides the extraction seam the build already declares, used here only as a + // clock: it runs *after* the build's up-front invalidation and delegates to the real + // extractor, so the page the run must evict is one that invalidation cannot have removed. + const hooks = world.plantLate + ? { + extract: (options: Parameters[0]) => { + plantStalePage(world); + + return extract(options); + }, + } + : {}; + + world.exitCode = + params.command === "view" + ? runView(parsed, {}, hooks) + : runBuild(parsed, {}, "build", hooks).exitCode; }, "the run exits {exitCode}": (world: ViewWorld, params: { readonly exitCode: number }) => { expect(world.exitCode).toBe(params.exitCode); }, + "the view directory survives: {viewSurvives}": ( + world: ViewWorld, + params: { readonly viewSurvives: boolean }, + ) => { + expect(existsSync(viewPathOf(world))).toBe(params.viewSurvives); + }, "the view holds the current page {currentPage}": ( world: ViewWorld, params: { readonly currentPage: string }, @@ -429,6 +567,33 @@ void stalePageRemovedTestAnchor; bindExample(stalePageRemovedContract, viewWorld, wholesaleRewriteBindings); +const lateStalePageTestAnchor = specTest({ + id: testAnchorId("test:protocol.wholesale-view-rewrite.late-stale-page"), + label: "the late-page point verifies that the swap itself evicts what invalidation missed", + verifies: ref("spec:consumers.wholesale-view-rewrite.late-stale-page"), +}); +void lateStalePageTestAnchor; + +bindExample(lateStalePageContract, viewWorld, wholesaleRewriteBindings); + +const failedRunViewRemovedTestAnchor = specTest({ + id: testAnchorId("test:protocol.wholesale-view-rewrite.failed-run-view-removed"), + label: "the failed-run point verifies that an uncertifiable view is removed, not left readable", + verifies: ref("spec:consumers.wholesale-view-rewrite.failed-run-view-removed"), +}); +void failedRunViewRemovedTestAnchor; + +bindExample(failedRunViewRemovedContract, viewWorld, wholesaleRewriteBindings); + +const buildInvalidatesViewTestAnchor = specTest({ + id: testAnchorId("test:protocol.wholesale-view-rewrite.build-invalidates-view"), + label: "the build point verifies the up-front invalidation on a command that renders no view", + verifies: ref("spec:consumers.wholesale-view-rewrite.build-invalidates-view"), +}); +void buildInvalidatesViewTestAnchor; + +bindExample(buildInvalidatesViewContract, viewWorld, wholesaleRewriteBindings); + /* ----- spec:validation.diagnostic-rendering ----- */ type LocationFields = Pick; @@ -468,8 +633,9 @@ const diagnosticRenderingBindings = { message: params.message, }; }, - "the command-line renderer formats that finding once per location shape": ( + "the {renderer} renderer formats that finding once per location shape": ( world: DiagnosticWorld, + params: { readonly renderer: "command-line" | "Design Review" }, ) => { const finding = world.finding; @@ -478,8 +644,13 @@ const diagnosticRenderingBindings = { } // One finding, one renderer: each outcome step below supplies a location shape and reads what - // the real command-line renderer composed from those structured fields alone. - world.render = (location) => formatFinding({ ...finding, ...location }); + // the real renderer composed from those structured fields alone. Both entrypoints the Spec + // names are called directly — `formatFinding` for the command line, `renderFindings` for the + // Design Review's table — because each is the seam its own surface renders through. + world.render = + params.renderer === "command-line" + ? (location) => formatFinding({ ...finding, ...location }) + : (location) => renderFindings([{ ...finding, ...location }]).at(-1) ?? ""; }, "the finding carrying the file {file} and the line {line} renders {withLocation}": ( world: DiagnosticWorld, @@ -504,6 +675,29 @@ const diagnosticRenderingBindings = { ) => { expect(renderOf(world)({})).toBe(`${params.bare}\n`); }, + "the findings row carrying the file {file} and the line {line} renders {locationRow}": ( + world: DiagnosticWorld, + params: { readonly file: string; readonly line: number; readonly locationRow: string }, + ) => { + const rendered = renderOf(world)({ file: params.file, line: params.line }); + + world.file = params.file; + expect(rendered).toBe(params.locationRow); + // The location cell is the only place the path appears — the message cell never carries it. + expect(rendered.indexOf(params.file)).toBe(rendered.lastIndexOf(params.file)); + }, + "the same row carrying the file alone renders {fileOnlyRow}": ( + world: DiagnosticWorld, + params: { readonly fileOnlyRow: string }, + ) => { + expect(renderOf(world)({ file: world.file })).toBe(params.fileOnlyRow); + }, + "the same row carrying neither renders {absentRow}": ( + world: DiagnosticWorld, + params: { readonly absentRow: string }, + ) => { + expect(renderOf(world)({})).toBe(params.absentRow); + }, }; const composedLocationTestAnchor = specTest({ @@ -514,3 +708,12 @@ const composedLocationTestAnchor = specTest({ void composedLocationTestAnchor; bindExample(composedLocationContract, diagnosticWorld, diagnosticRenderingBindings); + +const tableCellLocationTestAnchor = specTest({ + id: testAnchorId("test:protocol.diagnostic-rendering.table-cell-location"), + label: "the table-cell point verifies the same composition rule on the Design Review's twin", + verifies: ref("spec:validation.diagnostic-rendering.table-cell-location"), +}); +void tableCellLocationTestAnchor; + +bindExample(tableCellLocationContract, diagnosticWorld, diagnosticRenderingBindings); From e72d158275058282eedc72e8a24585fcbf87ff81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 21:15:09 +0200 Subject: [PATCH 15/16] docs(plans,records): record the post-review remediation and correct the falsified rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The records half of the owner-ordered wave. Plans 17, 18, and 18a each carry a row saying the indirect `then` key was fixed or verified intact; every one of them was false from the commit that silently reverted the fix until the phase-4 close. Each gets a bracketed erratum beside the claim, never a rewrite: a plan is a process record, and the correction is worth more standing next to what it corrects. AGENTS.md gains the discipline that failure taught — a docket or ledger row claiming fixed or verified is re-measured against the tree at the moment of verification, never trusted from the row that closed it. `07` §6 quotes three lines of rendered binding language where the view renders four; the quote now matches the renderer and the Spec. Plan 21 records the sixth session: the two sharpened §7 rows close, the `RenderedFinding` docket row closes DONE, the closing-numbers table gains the post-remediation column, and the S6 rulings log states why the `--check-clean` line stays unbound rather than stubbed. The archived review gains a post-review addendum updating P-1, P-2, and S-2 to fixed, with the re-measured mutation results beside them; the original findings stand as written. Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- AGENTS.md | 3 + .../07-mvp-roadmap-and-open-questions.md | 2 +- plans/17-self-hosting-v1.md | 2 +- plans/18-self-hosting-phase-2.md | 4 +- plans/18a-self-hosting-phase-2-execution.md | 2 +- plans/21-self-hosting-phase-4.md | 71 ++++++++++++++----- ...0-self-hosting-phase-4-pre-close-review.md | 43 +++++++++++ 7 files changed, 105 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a977342..4fd4d61 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -138,6 +138,9 @@ Every doc honours both — never mistake one half for the other: - **Lineage is evidence, not template.** Prior art **`@libar-dev/architect`** (local clone when present, e.g. a sibling `architect` checkout) taught us the problem in production; treat its *shape* as evidence about the problem, never as the answer. +- **A "verified" row is re-measured, never inherited.** A docket or ledger row claiming *fixed* or *verified* + is re-checked against the tree at the moment of verification, never trusted from the row that closed it — + the phase-4 close caught a "verified — intact" row that had been false since the commit after the one it cited. - **Git hygiene** follows the global rules (no `git stash`; commit early on a WIP branch; commit/push only when asked). diff --git a/docs/concept/07-mvp-roadmap-and-open-questions.md b/docs/concept/07-mvp-roadmap-and-open-questions.md index 81e1aef..3648747 100644 --- a/docs/concept/07-mvp-roadmap-and-open-questions.md +++ b/docs/concept/07-mvp-roadmap-and-open-questions.md @@ -121,7 +121,7 @@ the standing invariant plus where its protection lives. - **④ `implemented` is a UI hazard — view-label only.** Model semantics are settled (binding, never liveness — MD-7): the internal fact name stays `implemented` (it powers the `implemented ∧ ¬ready` drift query), and views render binding language instead: *"Implementation binding: present / Verifier binding: - present / Runtime observation: not tracked."* + present / Expected-outcome oracle: none / Runtime observation: not tracked."* - **⑤ `coverage-unknown` is acceptance, never a design note.** File-level blast-radius (the reader, `06` §2) reports a changed-but-unanchored file as an explicit `coverage-unknown` item, never silently under-reporting — test-pinned, so a too-small reach set is a caught regression, not a rendering choice. diff --git a/plans/17-self-hosting-v1.md b/plans/17-self-hosting-v1.md index 5b8e5e0..a80470f 100644 --- a/plans/17-self-hosting-v1.md +++ b/plans/17-self-hosting-v1.md @@ -453,7 +453,7 @@ the compact grouping below retains every listed concern and its durable disposit | Windows absolute excludes and `--exclude --foo` diagnostics | pre-existing/phase-2 | Exclude UX refinements are outside phase-1 acceptance. | | Path-prefix matcher coverage | deferred | Add a focused regression when exclusion handling is next changed. | | Library-seam exclusion wording | deferred | Public library diagnostics can be separated from CLI wording later. | -| Indirect assembly of the `then` graph key | fixed-by-remediation | `cd735ae` names the key directly. | +| Indirect assembly of the `then` graph key | fixed-by-remediation | `cd735ae` names the key directly. [Erratum, recorded at the phase-4 close: this row was false shortly after it was written — `fcd5cef`, a same-cluster grammar-hardening commit nine hours later, silently reintroduced the indirection, and it stood for three phases. Normalized for real at the phase-4 close; see plan 21 §6 and reviews/10 T-1.] | | Design Review dynamic-key ordering | deferred | Re-parsed graph rendering determinism is a projection follow-up. | | Design Review escaping outside prose slots | pre-existing/phase-2 | TS-carrier authored HTML policy needs a scoped rendering decision. | | GWT, examples, flows, and example-space permutation coverage | fixed-by-remediation | `f8b26f5` adds byte-equality permutation coverage. | diff --git a/plans/18-self-hosting-phase-2.md b/plans/18-self-hosting-phase-2.md index 03bd953..7759ec8 100644 --- a/plans/18-self-hosting-phase-2.md +++ b/plans/18-self-hosting-phase-2.md @@ -302,7 +302,7 @@ round-trip catalog is declared before implementation and may change only by an e - **Executed delivery:** `sdp import` landed as the durable, fs-free import seam with the CLI write-beside and `--dry-run` boundary; all eleven checkout Specs migrated atomically to Markdown; the canonical-default carrier rule replaced the interim rule; the bounded parser refusal-parity claim, decision-spec fold, and four honest corpus waves landed. The resulting self-hosting graph holds 58 Specs, 1 Pack, and 36 anchors, with 7 `ready` and 51 `defined` Specs. - **Session SHA summary:** G1 is `f06f14d`; G2 is `df444f2`; G3 is `0bb200a`; G4, G5, G6, and G8 are consolidated at `67cfda7` under the continuation directive; G7 remains `b471189034e1ee238394f3364c349937be6bebed` because its corpus and fold-completion evidence was accepted independently before the whole-phase close. - **Watch items:** all five terminal states are unfired. Table sugar was not forced because waves used existing sibling Specs and ruled body forms. The single-literal vocabulary form was unnecessary beyond the ruled literal syntax. No wave required a multi-entry constraint. New prose stayed under existing typed section owners, leaving the array-section sub-owner unfired. The Pack remains a TypeScript manifest, with no caller requiring Markdown Pack authoring. -- **Docket close:** the twelfth preflight leg and decision-spec namespace divergence are done; the indirect `then` key and the Model term named `description` are verified; and the checkout duplicate-carrier fixture exemption remains an explicit preserved test-fixture exception. +- **Docket close:** the twelfth preflight leg and decision-spec namespace divergence are done; the indirect `then` key and the Model term named `description` are verified [Erratum, recorded at the phase-4 close: the indirect `then` key was **not** verified — the phase-1 fix had silently regressed before this row was written. See plan 21 §6 and reviews/10 T-1.]; and the checkout duplicate-carrier fixture exemption remains an explicit preserved test-fixture exception. - **Deferred tail:** no-reparse spy coverage remains deferred because named-import interception is weaker than an injected read seam. Temporal token assembly remains deferred because the sanctioned temporal-guard pattern still applies; its comment can be narrowed later. - **Lean registry rulings:** the phase admitted `spec:decisions.*` records for the executable meta-model, naming and one-primitive laws, binding and section ownership, the typing and readiness-floor laws, one validation path, the `.sdp.ts` extension and carrier ruling, prose ownership, point-per-example, and the exclusion and envelope-grammar contracts. The Pack, agent-surface, and deferred-MCP decisions joined them; D1, D2, and D4 remain carried by their ordinary Specs. Each admission passed the three-part durability test and is linked from the lean registry. - **Close evidence:** the acceptance assembly, adversarial-review remediation, and clean-clone and installed-package proofs are recorded in [task 40](../.omo/evidence/self-hosting-phase-2/task-40-self-hosting-phase-2.acceptance.md), [task 41](../.omo/evidence/self-hosting-phase-2/task-41-self-hosting-phase-2.review.md), and [task 42](../.omo/evidence/self-hosting-phase-2/task-42-self-hosting-phase-2.final-proofs.log). @@ -335,7 +335,7 @@ Planned disposition is not execution evidence. Every row starts pending and clos | Carrier-truth comment and temporal token assembly (review-06) | Records cluster adopts comment; token assembly deferred | done — comment now states only blockquote stripping and whitespace collapse; temporal token assembly remains deferred under the sanctioned temporal-guard pattern | | Stale provenance wording and plan-16 evidence dispositions (review-06) | Adopt with records cluster | REPAIRED — approval artifact wording aligns with CONTEXT; plan-16 repair items dispositioned below | | Twelfth preflight leg and decision-spec namespace divergence (review-06) | Verify preflight; resolve namespace in fold | done — the recorded preflight chain delta is benign, and the `spec:decisions.*` namespace fold landed | -| Indirect assembly of the `then` graph key (review-06) | Verify remediation remains intact | verified — phase-1 remediation names the `then` key directly | +| Indirect assembly of the `then` graph key (review-06) | Verify remediation remains intact | verified — phase-1 remediation names the `then` key directly. [Erratum, recorded at the phase-4 close: this verification was false when written — the fix it re-verified had already been reverted by a same-cluster commit, and the row was trusted rather than re-measured against the file. See plan 21 §6 and reviews/10 T-1.] | | Model term named `description` (review-06) | Verify remediation remains intact | verified — phase-1 remediation preserves the Model term named `description` | | Checkout duplicate-carrier fixture exemption (review-06) | Preserve as explicit fixture exception | preserved as explicit fixture exception — the deliberate dual-carrier fixture remains outside the one-canonical-surface product rule | | Exclude/CLI cluster (brief §6) | Land in tranche 1 | done s1 — loud Windows absolute rejection, flag-operand usage diagnostics, segment-boundary coverage, and library/CLI wording separation landed | diff --git a/plans/18a-self-hosting-phase-2-execution.md b/plans/18a-self-hosting-phase-2-execution.md index 244ffa6..f7865b3 100644 --- a/plans/18a-self-hosting-phase-2-execution.md +++ b/plans/18a-self-hosting-phase-2-execution.md @@ -149,7 +149,7 @@ because its carried-forward rationale remains sanctioned. No row was silently dr | YAML scalar, line-number, document-end, non-mapping-root, cap-flood, GWT, heading, and duplicate-`When` grammar rows | plan-17 and review-06 grammar cluster | done s3 | [Task 18](../evidence/self-hosting-phase-2/task-18-self-hosting-phase-2.vitest.log) | | Windows-exclude, malformed-flag, matcher, and library-wording rows | plan-17 and review-06 exclude cluster | done s1 | [Task 10](../evidence/self-hosting-phase-2/task-10-self-hosting-phase-2.red.log), [task-40 green gate](../evidence/self-hosting-phase-2/task-40-self-hosting-phase-2.acceptance.md#current-green-gate) | | Dynamic-key ordering and non-prose escaping | plan-17 and review-06 Design Review cluster | done s2 | [Task 15](../evidence/self-hosting-phase-2/task-15-self-hosting-phase-2.vitest.log) | -| Indirect `then`, GWT permutation, `description`, bound-example, and fixture-byte rows | plan-17 phase-1 remediations | dropped already fixed by phase-1 remediation | [plan-17 record](../../plans/17-self-hosting-v1.md) | +| Indirect `then`, GWT permutation, `description`, bound-example, and fixture-byte rows | plan-17 phase-1 remediations | dropped already fixed by phase-1 remediation. [Erratum, recorded at the phase-4 close: the indirect `then` row was not in fact fixed by the phase-1 remediation — the fix was reverted inside the same cluster, so dropping the row left the defect live until the phase-4 close. See plan 21 §6 and reviews/10 T-1.] | [plan-17 record](../../plans/17-self-hosting-v1.md) | | Row-3 enrichment delta | plan-17 | dropped it remains the sanctioned `scoped` to `defined` maturity record | [plan-17 record](../../plans/17-self-hosting-v1.md) | | No-reparse spy coverage | plan-17 | deferred Named-import interception remains weaker than an injected read seam. | [plan-17 record](../../plans/17-self-hosting-v1.md) | | Carrier-truth comment, stale provenance, and plan-16 evidence dispositions | plan-17 and review-06 records cluster | done s6 | [fold ledger](#fold-ledger), [Task 39](../evidence/self-hosting-phase-2/task-39-self-hosting-phase-2.g7.md) | diff --git a/plans/21-self-hosting-phase-4.md b/plans/21-self-hosting-phase-4.md index 8a78460..bf516f4 100644 --- a/plans/21-self-hosting-phase-4.md +++ b/plans/21-self-hosting-phase-4.md @@ -14,7 +14,11 @@ > floor config, both designed-for deferrals named nowhere else (gaps 13/14, the named next work); > `06` and `07` were re-graded and stay. Closing corpus **103 Specs · 1 Pack · 75 anchors → 179 > nodes · 351 edges · `ready: 66 / defined: 37`, 0 errors / 0 warnings**, with the full twelve-leg -> gate and a clean-clone proof green at the close. This is +> gate and a clean-clone proof green at the close. A **sixth session (S6)** then landed the owner's +> post-review remediation — the wholesale-rewrite, binding-language, and diagnostic laws given +> teeth one realizing site at a time, the dead `RenderedFinding` shape deleted, and the falsified +> historical records corrected — taking the branch tip to **108 Specs · 1 Pack · 80 anchors → 189 +> nodes · 371 edges · `ready: 71 / defined: 37`**, still 0 errors / 0 warnings. This is > plan 21, the highest primary-numbered plan; the previous ✅ EXECUTED ground is plan 20 (the > phase-3 close). Build state lives in **`plans/`** — read the highest **primary-numbered** > plan's status header, plus any **active subplans it (or its parent family) explicitly @@ -257,7 +261,7 @@ Rows close only with reasons. Terminal state at this close: | the editor-association gap | **carried** — out of scope by §(c); untouched | | control-character latitude | **carried** — out of scope by §(c); no new material exercised it | | the separate example id namespace | **carried** — the watch item above stayed unfired; ten new example ids landed under their parents' namespaces with no collision | -| **dead `RenderedFinding` shape** *(new — review-10 S-2)* | **entered carried** — `src/cli/output.ts` declares and exports a second finding shape with no producer anywhere in `src/`, `test/`, or `examples/`, and no barrel export, inside the very file `spec:validation.diagnostic-rendering` names as an entrypoint while stating that no surface introduces a parallel report shape. Dead internal surface, not a live parallel path; removing an internal type is engine hygiene outside a review-and-close session | +| **dead `RenderedFinding` shape** *(new — review-10 S-2)* | **DONE at S6** — deleted. The verdict was re-measured rather than inherited: an exhaustive search over the whole tree finds the type named only by its own declaration and by `formatFinding`'s union parameter — no producer in `src/`, `test/`, `examples/` or the barrel, and no CLI path that parses findings from JSON into it. `formatFinding` narrows to `Finding`, the interface is gone, and the file `spec:validation.diagnostic-rendering` points a reader at no longer declares a second report shape. Entered carried at S5 as — `src/cli/output.ts` declares and exports a second finding shape with no producer anywhere in `src/`, `test/`, or `examples/`, and no barrel export, inside the very file `spec:validation.diagnostic-rendering` names as an entrypoint while stating that no surface introduces a parallel report shape. Dead internal surface, not a live parallel path; removing an internal type is engine hygiene outside a review-and-close session | ## §5 Acceptance criteria @@ -438,8 +442,9 @@ prioritization heuristic (§5). All three are named out of scope by §(c); `07` ## §6 Done-record -The phase ran in five sessions on `feature/protocol-self-application-phase-4`, each closing with a -green twelve-leg gate. What it delivered, against §(c): +The phase ran in six sessions on `feature/protocol-self-application-phase-4`, each closing with a +green twelve-leg gate — five against §(c), plus S6, the owner's post-review remediation wave. What +it delivered, against §(c): 1. **The oracle split (S1).** `test/self-hosting-graph.test.ts` went from one `it()` over 3,888 lines to **21 `it()`s over a single hoisted extraction** — the corpus walk runs once per suite @@ -494,21 +499,52 @@ green twelve-leg gate. What it delivered, against §(c): Spec's silence on the clause attribution for an unresolved relation target states nothing false and produces an identical rendered outcome, so no clause was invented for it. The dead `RenderedFinding` type in `src/cli/output.ts` is internal surface with no producer and no - barrel export, so no parallel report path exists in substance; it rides the docket. + barrel export, so no parallel report path exists in substance; it rode the docket — and S6 + closed it by deletion after re-measuring the no-producer verdict rather than inheriting it. + +### The S6 rulings log + +1. **A two-site law is separated by moving the *world*, not by mutating the engine.** The + wholesale-rewrite point could not see either realizing site alone because the build's up-front + invalidation always ran first and swallowed the evidence. The repair was to give the example + space a *when* — the stale page is planted either before the run or after the build has already + invalidated the view — and a *which command*. Both sites then became separately observable + through the filesystem, with no engine change and no spy: the late plant rides the build's own + declared extraction seam, used purely as a clock, and delegates to the real extractor. +2. **The `--check-clean` line stays unbound, and the reason is the law's own shape.** Two renders + of the same graph diverge only if the renderer is non-deterministic, which + `spec:extraction.determinism` forbids. A world could supply a lying renderer through the + declared hook and the refusal path would run for real — but the point would then assert that + the run refuses a renderer the world broke, not anything about the engine's own renderer. That + is a stub standing in for the law, so it was refused and recorded instead of counted. +3. **Both diagnostic entrypoints are called directly, by the same reading.** `composed-location` + already called `formatFinding` directly; the twin calls `renderFindings` directly. The Spec + names both as realizing entrypoints, and each is the seam its own surface renders through — + routing the table half through `renderDesignReview` would have measured the page assembler + rather than the composition rule. +4. **The dead type was re-measured before it was deleted.** The docket row already said "no + producer"; S6 re-ran the search over the whole tree, the barrel, and the CLI's JSON paths rather + than trusting the row — the discipline this phase's own major finding taught, now also stated in + `AGENTS.md`. ### Closing numbers -| | Opening (`main`) | Closing | -|---|---|---| -| Specs | 87 | **103** | -| anchors | 65 | **75** | -| nodes · edges | 153 · 294 | **179 · 351** | -| stated readiness | `ready: 51 / defined: 36` | **`ready: 66 / defined: 37`** | -| bound points · bound suites | 29 · 6 | **39 · 7** | -| findings over the corpus | 0 errors / 0 warnings | **0 errors / 0 warnings** | +| | Opening (`main`) | Closing (S5) | After the S6 remediation | +|---|---|---|---| +| Specs | 87 | 103 | **108** | +| anchors | 65 | 75 | **80** | +| nodes · edges | 153 · 294 | 179 · 351 | **189 · 371** | +| stated readiness | `ready: 51 / defined: 36` | `ready: 66 / defined: 37` | **`ready: 71 / defined: 37`** | +| bound points · bound suites | 29 · 6 | 39 · 7 | **44 · 7** | +| findings over the corpus | 0 errors / 0 warnings | 0 errors / 0 warnings | **0 errors / 0 warnings** | + +The S5 column is the close the adversarial review measured; the S6 column is the branch tip after +the owner-ordered remediation (§9). The five Specs S6 added are all `example` children carrying +bound points, which is why `ready` moves and `defined` does not. Every one of the 66 `ready` Specs carries `has-verifier` through the executable path; not one of -the 37 `defined` Specs carries it, which is the sweep's uniform refusal reason. +the 37 `defined` Specs carries it, which is the sweep's uniform refusal reason. The reading holds +unchanged after S6 at 71 / 37. ### §5 acceptance criteria, graded @@ -544,9 +580,9 @@ reasons)* | S2 | lower floor rungs (`idea`/`scoped`/`defined` clauses) | `spec:validation.readiness-floor` (enriched) | 2–3 | done — 2 points (`at-least-one-relation` on a scoped probe · `no-blocking-open-questions` on a defined probe), both mutation-probed red | | S2 | per-kind evidence table + MD-16 promoted-evidence bound | new `spec:validation.kind-evidence` | 1–2 | done — 3 points (behavior-family complete cell · constraint target · the promoted-evidence bound); one over the planned ceiling, taken deliberately so the MD-16 bound the Spec states is not the only row left unbound | | S3 | derived-readiness banner (one direction · first unmet clause) | new `spec:consumers.derived-readiness-banner` | 1–2 | done — 2 points (`dishonest-divergence` names the first unmet clause · `honest-headroom` pairs the absent banner with the rendered stated-beside-derived line), both mutation-probed red | -| S3 | `implemented` view-label (binding language) | new `spec:consumers.binding-language-views` | 1 | done — 1 point (`bound-spec-page`: the four binding lines, the index row repeating them, and the internal fact names absent from both surfaces), mutation-probed red. **Residue measured at S5:** the Spec's rule names *both* aggregate surfaces, but the probe graph holds no Pack, so only the index half is exercised — changing the index row's cells to `yes`/`no` kills the point, the identical change to the pack member table's cells does not. The pack-member half of that rule stands unbound | -| S3 | wholesale page rewrite (atomic swap · no stale page) | new `spec:consumers.wholesale-view-rewrite` | 1 | done — 1 point (`stale-page-removed`, a temp-root world running the real `runView`), mutation-probed red; the law is realized at two sites (the up-front invalidation in `runBuild` plus the temp-and-rename in `runView`), so breaking one alone leaves the point green — recorded, and the Spec states both. **Sharpened at S5 from the measurement:** deleting either site alone survives; deleting both kills; replacing the rename with a copy so the temporary sibling is left behind also kills, so the point does discriminate the swap in that one direction. It does *not* discriminate the swap's absence — `temporarySurvives: false` is satisfied both when the temporary ran and was cleaned up and when no temporary exists at all. Three of the Spec's rule lines have no verifier: the "no half-written view is ever readable" reading of the one-rename clause, the failed-run removal, and the `--check-clean` double render with its refusal | -| S3 | one diagnostic rendering rule | new `spec:validation.diagnostic-rendering` | 1 | done — 1 point (`composed-location`: the composed prefix plus both degradations on one finding), mutation-probed red. **Family call:** the carrier lives in `specs/validation/` and refines `spec:validation.two-check-families`, because the law's subject is the Finding currency — a validation concept whose shape law that parent already carries. The consumers family offered no honest parent: `spec:consumers.projections-model` is a `model`-kind vocabulary rather than a law a rule refines, and `spec:consumers.design-review` is only one of the two rendering surfaces. The Design Review half rides a `dependsOn` edge to that Spec instead | +| S3 | `implemented` view-label (binding language) | new `spec:consumers.binding-language-views` | 1 | done — 1 point (`bound-spec-page`: the four binding lines, the index row repeating them, and the internal fact names absent from both surfaces), mutation-probed red. **Residue measured at S5:** the Spec's rule names *both* aggregate surfaces, but the probe graph holds no Pack, so only the index half is exercised — changing the index row's cells to `yes`/`no` kills the point, the identical change to the pack member table's cells does not. The pack-member half of that rule stands unbound. **Closed at S6 (review-10 P-2):** the probe world now builds a Pack holding the bound subject beside the unbound parent, and `pack-member-table` reads the member table's cells for both — `present`/`present` on the bound row and `none`/`none` on the unbound one, with the internal fact names absent from that surface too. Probed: the `yes`/`no` shorthand on the **pack member table** now reddens exactly this point and leaves `bound-spec-page` green, while the same shorthand on the index row still reddens exactly `bound-spec-page` | +| S3 | wholesale page rewrite (atomic swap · no stale page) | new `spec:consumers.wholesale-view-rewrite` | 1 | done — 1 point (`stale-page-removed`, a temp-root world running the real `runView`), mutation-probed red; the law is realized at two sites (the up-front invalidation in `runBuild` plus the temp-and-rename in `runView`), so breaking one alone leaves the point green — recorded, and the Spec states both. **Sharpened at S5 from the measurement:** deleting either site alone survives; deleting both kills; replacing the rename with a copy so the temporary sibling is left behind also kills, so the point does discriminate the swap in that one direction. It does *not* discriminate the swap's absence — `temporarySurvives: false` is satisfied both when the temporary ran and was cleaned up and when no temporary exists at all. Three of the Spec's rule lines have no verifier: the "no half-written view is ever readable" reading of the one-rename clause, the failed-run removal, and the `--check-clean` double render with its refusal. **Closed at S6 (review-10 P-1), three points added and the residue re-measured:** the example space now carries *when* the stale page is planted and *which* command runs, which is what made the two sites separable. `late-stale-page` plants the page after the build has already invalidated the view — through the build's own declared extraction seam, used only as a clock, delegating to the real extractor — so only the temp-and-rename swap can evict it; `failed-run-view-removed` runs the same late plant over a carrier the extractor refuses, so only `runView`'s failed-run removal can take the view down; `build-invalidates-view` runs `runBuild` alone, which renders no view, so only the up-front invalidation can. Probed one site at a time: deleting the temp-and-rename path **alone** now reddens exactly `late-stale-page`; deleting the up-front invalidation **alone** now reddens exactly `build-invalidates-view`; deleting `runView`'s failed-run removal alone reddens exactly `failed-run-view-removed`; deleting both no-stale-page sites reddens three points including the original `stale-page-removed`. **Still unbound, with the reason on the record:** the `--check-clean` double-render refusal. Divergence cannot be honestly *induced* — the renderer is deterministic by `spec:extraction.determinism`, so the only world that produces two diverging renders is one where the world supplies a renderer that lies, which states nothing about the engine. Faking it through the render hook was refused rather than counted. The "no half-written view is ever readable" reading of the one-rename clause also stays unbound: observing it needs a concurrent reader mid-write | +| S3 | one diagnostic rendering rule | new `spec:validation.diagnostic-rendering` | 1 | done — 1 point (`composed-location`: the composed prefix plus both degradations on one finding), mutation-probed red. **Family call:** the carrier lives in `specs/validation/` and refines `spec:validation.two-check-families`, because the law's subject is the Finding currency — a validation concept whose shape law that parent already carries. The consumers family offered no honest parent: `spec:consumers.projections-model` is a `model`-kind vocabulary rather than a law a rule refines, and `spec:consumers.design-review` is only one of the two rendering surfaces. The Design Review half rides a `dependsOn` edge to that Spec instead. **Twin bound at S6:** `table-cell-location` runs the same one finding through `renderFindings` — the other realizing entrypoint the Spec names, called directly for the same reason `composed-location` calls `formatFinding` directly: each is the seam its own surface renders through. It asserts all three location shapes as whole table rows, so the em-dash cell for an absent location and the `\|`-escaped message pipe are both authored expectations rather than incidental. Probed: baking the location into the message cell, dropping the em dash, and dropping the table pipe escaping each redden exactly this point and leave `composed-location` green | | S3 | validator self-testing | new `spec:validation.validator-self-testing` | 0–1 (may honestly stay `defined`) | done — 0 points, stated `defined`: the only mechanical verifier available would inspect the test corpus for should-fail/should-pass pairs, which polices the delivery process rather than conformance or honesty | ## §8 Readiness ledger @@ -644,6 +680,7 @@ ledger is git process evidence, never graph content. | S3 | view wave + seventh bound suite | orchestrator-verified green gate | done — five laws carried (banner · view-label · wholesale rewrite · diagnostic rendering · validator self-testing), ten Specs added, five bound points in the new `test/self-hosting-projections.test.ts`, each mutation-probed red for the law it names; the suite entered the shared constant once and both surfaces followed (clean-room proof: with `generated/contracts` moved aside, lint passes with the row and fails with five unsafe-argument errors without it, while the wrapper refuses fast with the recovery text); corpus 93 → 103 Specs, `ready` 57 → 66 | | S4 | readiness sweep + re-audits (± the `05` deletion) | orchestrator-verified green gate over the regenerated Design Review | done — the sweep dispositioned all 37 `defined` Specs with zero promotions and 37 named refusals (§8): none carries `has-verifier`, so every promotion would have added an `honesty/gaps` warning, and no verifier was invented to enable one. The `05` re-audit closed all three of its phase-3 gaps (S2's floor wave, S3's banner and validator-self-testing carriers) and surfaced **two new gap rows** — the per-team severity override and the team-overridable floor config, both designed-for deferrals named nowhere else — so **`05` stays** and its residue plus a deletion-cost inventory are recorded (§5a). `06` and `07` re-graded: six of the twelve phase-3 gaps closed, both docs stay. Records-only session — no product surface changed, graph numbers unmoved at 103/1/75 · 179 · 351 · `ready` 66 / `defined` 37 | | S5 | adversarial review, remediation, full close, done-record | full chain + clean-clone; review archived | done — the review is archived at `reviews/10-self-hosting-phase-4-pre-close-review.md` with **ten findings, every one terminal**: one major fixed (the indirect `then` key, normalized at all 66 sites), one minor fixed by enrichment (`spec:validation.warn-level-signals` now carries the gap signal's recomputed-facts reading), two records repaired (§(b)'s suite count, §2 S1's roster description), two §7 rows sharpened from the measurement, one declined with a reason, one carried to the docket, and two informational verifications recorded. Twenty-two independently designed mutations were run; all ten new points went red for the law they name. The full twelve-leg gate and the clean-clone proof are green at the close SHA | +| S6 | post-review remediation wave (owner-review items D–J/M) | orchestrator-verified green gate | done — the owner's review of the branch accepted the close and ordered a fixed set of repairs, all landed: the wholesale-rewrite law gained three points that separate its two realizing sites (review-10 P-1 closed); the binding-language rule gained the pack-member half (P-2 closed); the diagnostic rule gained its Design Review twin; the dead `RenderedFinding` shape was deleted after re-measuring that it has no producer (S-2 closed, §4 DONE); the frozen GWT result key gained a one-line constraint comment beside the product site; the two plans that recorded the `then`-key defect as fixed and as verified-intact gained errata beside the false claims; `AGENTS.md` gained the re-measure-a-verified-row discipline; and `07` §6 ④'s three-line quote of the rendered binding language was corrected to the four lines the view renders. Every new and strengthened point was mutation-probed one site at a time and reddened exactly the point that names the broken law. Corpus 103 → 108 Specs · 75 → 80 anchors · 179 → 189 nodes · 351 → 371 edges · `ready` 66 → 71 / `defined` 37, zero errors and zero warnings; 39 → 44 bound points across the same seven suites | Owner ratification of every gate above happens at the phase PR review; no live owner acceptance occurs during execution. diff --git a/reviews/10-self-hosting-phase-4-pre-close-review.md b/reviews/10-self-hosting-phase-4-pre-close-review.md index 69885e9..708de25 100644 --- a/reviews/10-self-hosting-phase-4-pre-close-review.md +++ b/reviews/10-self-hosting-phase-4-pre-close-review.md @@ -447,6 +447,49 @@ same way phase 3 closed D-2 and D-3: the `carried` verdict is now true on the di decision's own terms — carried by a Spec — rather than defensible only on a plan ruling that widened the criterion to admit code surfaces. +### Post-review remediation (added after the owner's review of this branch) + +The owner's review of the branch accepted the close and ordered a further wave of fixes before the +PR. It ran as S6 on the same branch. **The findings above are not rewritten** — what follows updates +their dispositions: + +- **P-1 → FIXED (was RECORD SHARPENED).** The two realizing sites are now separately discriminated, + by moving the *world* rather than the engine. The Spec's example space gained *when* the stale page + is planted and *which command* runs, and three sibling points landed: + `wholesale-view-rewrite.late-stale-page` (planted after the build has already invalidated the view, + so only the temp-and-rename swap can evict it), `.failed-run-view-removed` (the same late plant over + a carrier the extractor refuses, so only `runView`'s failed-run removal can take the view down), and + `.build-invalidates-view` (`runBuild` alone, which renders no view, so only the up-front + invalidation can). Re-measured one site at a time: **M9a — deleting the temp-and-rename path alone — + now reddens exactly `late-stale-page`; M9b — deleting the up-front invalidation alone — now reddens + exactly `build-invalidates-view`;** deleting `runView`'s failed-run removal alone reddens exactly + `failed-run-view-removed`; M9c still reddens three. Two of the three rule lines this review named as + unverified are now bound. **The `--check-clean` double render stays unbound with a stated reason:** + divergence is not honestly inducible, because the renderer is deterministic by + `spec:extraction.determinism` — the only world producing two diverging renders is one where the + world itself supplies a lying renderer, which asserts nothing about the engine. Faking it through the + declared render hook was refused rather than counted. The "no half-written view is ever readable" + reading also stays unbound: observing it needs a concurrent reader mid-write. +- **P-2 → FIXED (was RECORD SHARPENED).** `binding-language-views.pack-member-table` builds a probe + Pack holding the bound subject beside the unbound parent and reads the member table's cells for both. + Re-measured: **M16/M18 — the `yes`/`no` shorthand on the pack member table — now redden exactly this + point** and leave `bound-spec-page` green, while M17 on the index row still reddens exactly + `bound-spec-page`. +- **S-2 → FIXED (was CARRIED WITH REASON).** `RenderedFinding` is deleted and `formatFinding` narrows + to `Finding`. The no-producer verdict was re-measured over the whole tree, the barrel, and the CLI's + JSON paths rather than inherited from the docket row. Plan 21 §4's row closes DONE. + +Landed in the same wave, outside this review's findings: a bound Design Review twin for +`spec:validation.diagnostic-rendering` through `renderFindings` (the second entrypoint the Spec names, +covering the em-dash cell and the table-escaped message pipe); a one-line comment beside the product +site stating the plain-`then`-key constraint T-1 restored; errata beside the two false historical rows +in plans 17, 18, and 18a; an `AGENTS.md` bullet requiring a "verified" row to be re-measured rather +than inherited — T-1's own lesson, made standing discipline; and the correction of `07` §6 ④'s +three-line quote of the rendered binding language to the four lines the view renders. + +Branch tip after the wave: **108 Specs · 1 Pack · 80 anchors → 189 nodes · 371 edges · +`ready: 71 / defined: 37`**, 0 errors / 0 warnings, full twelve-leg gate green. + ## What the owner is asked to ratify at the PR Three things this review deliberately leaves to the owner rather than deciding by plan ruling: From f637eb77ad260a57a0ce9a8e19a8bfdf35198568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 26 Jul 2026 21:18:49 +0200 Subject: [PATCH 16/16] docs(plans): record the clean-clone proof re-run at the S6 tip Claude-Session: https://claude.ai/code/session_01SoBtqnPU6EQrd1tgrUdxcw --- plans/21-self-hosting-phase-4.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plans/21-self-hosting-phase-4.md b/plans/21-self-hosting-phase-4.md index bf516f4..e4e0d75 100644 --- a/plans/21-self-hosting-phase-4.md +++ b/plans/21-self-hosting-phase-4.md @@ -699,3 +699,9 @@ fresh checkout with no build state carried over. Reproduced numbers, both trees: - tests: **589 green** — 535 in the parallel pool plus the 54-test dedicated `test/cli.test.ts` pass. - preflight clean; `git status --porcelain` empty after the whole chain. + +The S6 remediation wave landed after that proof, so the clone proof was **re-run at the S6 +tip** before the PR: `git clone --no-local` → `npm ci` → the full chain, green end-to-end +(exit 0), reproducing the post-S6 numbers — root corpus **108 specs · 1 pack · 80 anchors → +189 nodes · 371 edges**, 0 errors / 0 warnings; the worked example unchanged at 11 · 1 · 5 → +17 · 32 with its one frozen warning; **594 tests green** (540 + 54); preflight clean.