diff --git a/AGENTS.md b/AGENTS.md index 30f649a..22e8293 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-2 implementation complete; final audit passed. The prior `EXECUTED — phase-1 implementation complete; final audit passed` status remains historic context. The remaining work is the systematic tests-to-executable-specs rewrite and per-doc concept deletion. Build state lives in +> 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 > **`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 @@ -53,9 +53,9 @@ Progressive disclosure — start at the top, follow the pointers down. | Look here | What you get | Read | |---|---|---| -| `CONTEXT.md` (repo root) | **the vocabulary** — the ratified lean glossary (terms · relations · a worked dialogue · flagged ambiguities); sole source of truth for terminology; the model exposition lives in `00`–`07` | **first, always** | +| `CONTEXT.md` (repo root) | **the vocabulary** — the ratified lean glossary (terms · relations · a worked dialogue · flagged ambiguities); sole source of truth for terminology; the model exposition lives in the Specs under `specs/` and in the surviving concept docs | **first, always** | | `jtbd-stories/` | **the jobs (functional spec)** — stable `When / I want / so I can` stories (themes A–H); no personas, because consumers are heterogeneous (humans, CI, CLIs, **AI agents**) | to know *what* we serve | -| `docs/concept/00`–`07` (+ README) | **the technical design and the model** — 9 principle-led docs: vision & MVP boundary, founding principles (P1–P10), core model (`02` — the primitive, descriptors, sections, relations), the one graph, authoring & binding, validation & honesty, consumers, roadmap | to know *how* it is designed | +| `docs/concept/` (+ README) | **the technical design** — the surviving principle-led docs: vision & MVP boundary, founding principles (P1–P10), authoring & binding, validation & honesty, consumers, roadmap; the core model dissolved into the model Specs (`spec:model.core-model`, `spec:model.spec-sections`, `spec:model.relations`, `spec:model.stable-ids`, `spec:model.pack-aggregate`) and the one graph into the extraction Specs (`spec:extraction.derive-graph`, `spec:extraction.determinism`, `spec:extraction.claim-taxonomy`, `spec:extraction.regenerability`, `spec:extraction.schema-versioning`) | to know *how* it is designed | | `docs/concept/DECISIONS.md` | **the lean decision registry** — ratified names, one-line glosses, carrying Specs, and the D1–D6 lookup; historical rationale lives in git, plans, and the Specs themselves | when resolving a decision name or following its canonical pointer | | `src/` | **the engine** — `model` (Spec/descriptors/pack/anchors) · `extract` · `graph` · `validate` · `reader` (agent surface) · `projections` (Design Review) · `cli` (`sdp`) · `runner` / `codegen` / `notation` / `adapters` | when implementing or verifying **current engine** behavior | | `examples/checkout-v1` | **the worked MVP example** (TS DSL tracer bullet) — specs, anchors, untracked `generated/` (regenerated in-pipeline); walkthrough in its README | when proving the loop end-to-end | diff --git a/CONTEXT.md b/CONTEXT.md index f826760..abff41a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -3,9 +3,9 @@ > **Status: RATIFIED · LEAN GLOSSARY.** The terminology base of the Libar Software Delivery Protocol — our > bounded context's vocabulary: **one concept → one word**; the rest are *aliases to avoid*. > -> This document carries **terms only**; the model exposition lives in the design docs -> (`docs/concept/00`–`07`). The lean decision registry points to carrying Specs; historical -> rationale lives in git, plans, and those Specs. +> This document carries **terms only**; the model exposition lives in the authored Specs under +> `specs/` and in the surviving design docs under `docs/concept/`. The lean decision registry +> points to carrying Specs; historical rationale lives in git, plans, and those Specs. ## Governing rubric [SETTLED] @@ -28,7 +28,7 @@ Self-hosting — the Protocol's own repo conforming to its own meta-model — is a later milestone, never a Phase-0 claim (`00` §3). -## The primitive & its descriptors (→ `02` §1–§2) +## The primitive & its descriptors (→ `spec:model.core-model`) | Term | Definition | Aliases to avoid | |---|---|---| @@ -51,14 +51,14 @@ The familiar delivery nouns are **named coordinates on the one primitive, never | **Executable Spec** | an `example` that **has a verifier** (a delivery fact) | a readiness rung | | **capability / domain** | a **projection** (Capability Map over high-altitude `behavior` `Spec`s) and/or a **`Pack` grouping** | a kind or an altitude | -## Sections (→ `02` §3) +## Sections (→ `spec:model.spec-sections`) | Term | Definition | Aliases to avoid | |---|---|---| | **section** | the typed, optional detail-slice of a `Spec` — the **extension surface**; one concern each: `intent` · `behavior` · `constraints` · `model` · `design` · `decision` · `verification` · `ui` | "Facet" · "aspect" | | **section ⟷ kind duality** | `constraints`/`model`/`decision` — and `behavior.rules`/`behavior.examples` vs the `rule`/`example` kinds — have a `kind` twin: keep **inline** when local detail; **promote** to a standalone `Spec` when shared or needing its own identity/lifecycle/review. **Sections carry content, relations carry linkage** (never a ref inside a section); **promotion is exclusive** (inline XOR promoted — content moves out, MD-10) | — | -## The other authored things (no system truth — → `02` §4, `04` §2) +## The other authored things (no system truth — → `spec:model.pack-aggregate`, `04` §2) | Term | Definition | Aliases to avoid | |---|---|---| @@ -68,7 +68,7 @@ The familiar delivery nouns are **named coordinates on the one primitive, never **Two grouping mechanisms, kept distinct:** *refinement* (parent `Spec` → children — authored truth with descendants) vs *the aggregate* (the `Pack` — a cross-cutting review collection, no truth of its own). -## Claims & the authored/derived split (→ `01`, `03` §3) +## Claims & the authored/derived split (→ `01`, `spec:extraction.claim-taxonomy`) | Term | Definition | Aliases to avoid | |---|---|---| @@ -81,7 +81,7 @@ descendants) vs *the aggregate* (the `Pack` — a cross-cutting review collectio No 4th `claim`: an edge computed deterministically from an authored source is a derivation **mechanism, not a claim category** — it **inherits** its source's `claim` (so `belongsTo` carries `declared`). -## Delivery facts (derived realization signals — → `02` §2) +## Delivery facts (derived realization signals — → `spec:model.core-model`) | Term | Definition | Aliases to avoid | |---|---|---| @@ -94,7 +94,7 @@ claim category** — it **inherits** its source's `claim` (so `belongsTo` carrie The payoff queries: `ready ∧ ¬implemented` = the **build backlog**; `implemented ∧ ¬ready` = the **drift alarm**. -## The graph & extraction (→ `03`) +## The graph & extraction (→ `spec:extraction.derive-graph`) | Term | Definition | Aliases to avoid | |---|---|---| @@ -141,13 +141,15 @@ validated," never "provably correct."** | Term | Definition | Aliases to avoid | |---|---|---| | **step contract** | a derived, regenerable typed module emitted per `example` spec from the graph (the generated-union pattern): the union of the example's literal step strings; tests bind handlers against it, so spec-side drift is a compile error — derived, never authored, importable *because* it is a projection | importing the authored spec | -| **example space** | the typed parameter vocabulary a parent `behavior` spec's steps declare; the space its child examples bind points in — the sibling set shares one vocabulary | "variables" | +| **example space** | the typed parameter vocabulary a parent spec's steps declare — typically a `behavior` spec; any kind may own the space when the vocabulary parameterizes its own law (`rule` and `model` parents are lawful); the space its child examples bind points in — the sibling set shares one vocabulary | "variables" | | **parameter slot** (short: **slot**) | one typed placeholder in a step's text | — | | **bound point** | the concrete slot values an `example` child binds for the steps it uses; partial points are honest — an unbound slot in a *used* step caps the example below `defined` (the **concreteness law**, one structural floor clause) | — | | **space contract** | the per-parent derived sibling of the step contract: the typed dimensions of the example space, every child's bound point, and the Outcome union derived from the parent's Then vocabulary | — | | **oracle** | the authored expected-outcome semantics for a parent's example space — implementation-side, beside the tests, bound by the space's zero-or-one resolving `specOracle` anchor, never extracted; typed against the space contract on input and the derived Outcome union on output (`unspecified` is a first-class answer); rendered surfaces say **"expected outcome"** | a `model`-kind spec · a derived fact | | **witness** | an example whose bound point falls in an outcome class — the evidence that class is covered | — | | **coverage gap** | a region of the example space with no witness (or where the oracle answers `unspecified`) — an informative absence, never a gate | — | +| **world** | the state a bound point's handlers construct and assert against — built by the Given steps, exercised by the When, read by the Then; created fresh per example by the test adapter (the runner core plans and runs steps, never owns world lifecycle) | "fixture" · "test context" | +| **probe** | a synthetic spec, graph, or repository a world constructs so the law under test is the only thing that can refuse (`spec:probe.*` by convention); probes live inside bound suites and never enter the shipped corpus | "dummy" · "mock" | | **`sdp import`** | one import verb with many source adapters, sharing the document emitter authored once in the winning carrier; the TS→`.sdp.md` adapter landed, the gen-1 `.feature` adapter designed-in and deferred | round-trip sync | **Structural law (point-per-example, MD-17):** an `example` binds exactly **one** point; a table of cases is @@ -161,7 +163,7 @@ a sibling set back as a table — the graph never holds a multi-point example. | **carrier** | the text format that carries the authored `Spec` document — the envelope, the prose, and the owned notation; **ruled: Markdown (`.sdp.md`) for all eight kinds**, the TS DSL remaining the import source and a lawful per-ID option (one canonical surface per ID, `04` §1) | "format"/"file type" (say which layer) · a carrier is never a second store | | **notation** | the Protocol-**owned** typed step language inside the carrier's fenced blocks — Given/When/Then step text and the slot vocabulary; owned by the Protocol whatever the carrier | "grammar" (the dismissed own-language direction) · "DSL" (reserved for the TS DSL) | -## Relations (authored, typed, directed `Spec`→`Spec` edges — → `02` §6) +## Relations (authored, typed, directed `Spec`→`Spec` edges — → `spec:model.relations`) | Relation | Direction | Means | Industry anchor | |---|---|---|---| @@ -202,7 +204,8 @@ artifact** — approval remains outside the model, never an authored primitive). division (`01`); a friendlier Studio-facing name is a minor open item. - **`ref()`** in the DSL is a **spec-only** reference builder wearing a generic name (it rejects `pack:`/`doc:` targets) — documented on the export (`src/ids.ts`). Consequently `decidedBy` → an - external `doc:` ADR is a **named deferral** (MD-16, stated in `02` §6); revisit when `doc:`-target + external `doc:` ADR is a **named deferral** (MD-16, stated in `spec:decisions.carried-evidence`); + revisit when `doc:`-target relations or pack-targeting arrive. - ~~Candidate vocabulary from the executable-spec exploration: *notation* · *carrier*~~ — **ratified by the carrier ruling (MD-18)**; see **The authoring carrier** above. The rest diff --git a/check-carrier-rule.mjs b/check-carrier-rule.mjs index 9aafe79..60b8b1c 100644 --- a/check-carrier-rule.mjs +++ b/check-carrier-rule.mjs @@ -12,10 +12,13 @@ import { fileURLToPath } from "node:url"; // 2. the obsolete sole-canonical-TS wording gone from the diary and the glossary, and the // surviving law (one canonical surface per ID, no mixing) still present in the diary; // 3. the extension-law re-point (the `.sdp.ts` extension law, MD-15) present in the diary; -// 4. the SAME logical/physical relations distinction in the core-model doc and JS-A1: -// relations stay OPTIONAL in the logical Spec; the physical Markdown envelope writes -// `relations: {}` when the logical set is empty — honest carrier syntax, never a new -// logical relation requirement. +// 4. the logical/physical relations distinction present on BOTH surviving surfaces, each in its +// own words — the carrying Spec (the Markdown envelope contract) and JS-A1: relations stay +// OPTIONAL in the logical Spec; the physical Markdown envelope writes `relations: {}` when the +// logical set is empty — honest carrier syntax, never a new logical relation requirement. The +// concept doc that used to hold the JS-A1-verbatim twin dissolved into its carrying Specs, so +// this leg proves co-presence of the law, not verbatim sameness across the two surfaces: +// JS-A1 keeps the verbatim sentence, the Spec states the same law as it authored it. // // Usage: node check-carrier-rule.mjs [rootDir] // rootDir — tree holding the operative surfaces; defaults to this repo root. QA passes a @@ -27,7 +30,7 @@ const SURFACES = { decisions: "docs/concept/DECISIONS.md", glossary: "CONTEXT.md", agents: "AGENTS.md", - coreModel: "docs/concept/02-core-model.md", + envelopeContract: "specs/carrier/envelope-contract.sdp.md", jsA1: "jtbd-stories/01-capture-and-evolve-intent.md", }; @@ -37,18 +40,21 @@ const SURFACES = { const 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"; -// The owner-confirmed logical/physical relations distinction. Verbatim in the core-model doc -// and JS-A1. +// The owner-confirmed logical/physical relations distinction. Verbatim in JS-A1. const RELATIONS_DISTINCTION = "Relations are optional in the logical `Spec` model. A physical Markdown envelope writes `relations: {}` when the logical set is empty: honest carrier syntax, not a new logical relation requirement."; +// The same law in the carrying Spec's own authored words. +const ENVELOPE_RELATIONS_CLAUSE = + "`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."; + const SURVIVING_LAW = "one canonical surface per ID, no mixing"; const OBSOLETE_SOLE_CANONICAL = "stays sole-canonical"; const EXTENSION_LAW_REPAIR = "re-pointed, not repealed"; // Sameness is about WORDS, so markdown quoting furniture is stripped before whitespace -// normalization: a sentence wrapped inside a `>` blockquote (the AGENTS status row, the -// core-model carrier note) must read identically to its plain-paragraph twins. +// normalization: a sentence wrapped inside a `>` blockquote (the AGENTS status row) must read +// identically to its plain-paragraph twins. const norm = (text) => text.replace(/^>\s?/gm, "").replace(/\s+/g, " "); const read = (rel) => norm(readFileSync(join(rootDir, rel), "utf8")); @@ -88,8 +94,8 @@ expectContains( "missing the extension-law re-point (the `.sdp.ts` extension law, MD-15)", ); expectContains( - SURFACES.coreModel, - RELATIONS_DISTINCTION, + SURFACES.envelopeContract, + ENVELOPE_RELATIONS_CLAUSE, "missing the logical/physical relations distinction", ); expectContains( diff --git a/check-carrier-truth.mjs b/check-carrier-truth.mjs index 06b3e99..4b36314 100644 --- a/check-carrier-truth.mjs +++ b/check-carrier-truth.mjs @@ -103,24 +103,6 @@ const CLAIMS = [ present: ["The carrier extractors — `ts-morph` (the TS carrier) and the ruled Markdown parser"], absent: ["| `ts-morph` as the extractor |"], }, - { - file: "docs/concept/03-the-one-graph.md", - label: "derivation diagram names the carrier extractors", - present: ["carrier extractors"], - absent: ["ts-morph extractor"], - }, - { - file: "docs/concept/03-the-one-graph.md", - label: "spec discovery covers both carrier suffixes", - present: ["every `*.sdp.ts` and `*.sdp.md` under the extraction root"], - absent: ["every `*.sdp.ts` under the extraction root"], - }, - { - file: "docs/concept/03-the-one-graph.md", - label: "the two-tier echo points at the per-carrier asymmetry", - present: ["the per-carrier asymmetry is named at `04` §1"], - absent: [], - }, { file: "docs/concept/04-authoring-and-binding.md", label: "intro names two ruled carriers, not exactly two surfaces with an open competition", @@ -482,33 +464,6 @@ const RULES = [ category: PLURAL, }, - // 03 — the one graph. - { - file: "docs/concept/03-the-one-graph.md", - includes: "ts-morph for .sdp.ts · the ruled Markdown parser for .sdp.md", - category: PLURAL, - }, - { - file: "docs/concept/03-the-one-graph.md", - includes: "every `*.sdp.ts` and `*.sdp.md` under the extraction root", - category: PLURAL, - }, - { - file: "docs/concept/03-the-one-graph.md", - includes: '"file": "specs/orders/create-order.sdp.ts"', - category: CHECKOUT_HISTORY, - }, - { - file: "docs/concept/03-the-one-graph.md", - includes: "the TS carrier's granularity", - category: STILL_SUPPORTED, - }, - { - file: "docs/concept/03-the-one-graph.md", - includes: "(from `ts-morph`)", - category: CODE_LINKAGE, - }, - // 04 — authoring & binding. { file: "docs/concept/04-authoring-and-binding.md", diff --git a/check-prose-schema.mjs b/check-prose-schema.mjs index f5c0d8b..5e4f812 100644 --- a/check-prose-schema.mjs +++ b/check-prose-schema.mjs @@ -32,18 +32,27 @@ expectContains( "narrative is not searchable", ); expectContains( - "docs/concept/02-core-model.md", - "`narrative` is content owned directly", + "specs/carrier/prose-ownership-rule.sdp.md", + "owned directly by the Spec; it is Spec content, never an envelope field", "missing narrative model contract", ); +// The schema-version literal is pinned on its declaring surface above (`src/graph/schema.ts`); +// the carrying Spec states the law that a graph declares its version, without a version literal. +// Its bound example point does carry the literal as a slot value — pinned here too, so a +// version bump names both authored surfaces instead of failing only at the bound point. expectContains( - "docs/concept/03-the-one-graph.md", - '"schemaVersion": "0.4.0"', - "missing 0.4.0 payload", + "specs/extraction/schema-versioning.sdp.md", + "Every graph declares its schemaVersion", + "missing schema-version declaration law", ); expectContains( - "docs/concept/03-the-one-graph.md", - "`constraints` has no description field", + "specs/extraction/schema-versioning.declared-version.sdp.md", + '{schemaVersion: "0.4.0"}', + "missing declared-version example literal", +); +expectContains( + "specs/carrier/prose-ownership-rule.sdp.md", + "the array-shaped constraints section has no description owner", "missing constraint omission contract", ); expectContains( diff --git a/docs/concept/01-founding-principles-and-invariants.md b/docs/concept/01-founding-principles-and-invariants.md index b05c96c..ed80232 100644 --- a/docs/concept/01-founding-principles-and-invariants.md +++ b/docs/concept/01-founding-principles-and-invariants.md @@ -25,7 +25,7 @@ If a successor kept only the essentials, the design stands or falls on these. Ea **Principle · CORE.** Derivation is reproducible: `graph = f(repo)` and `view = f(graph)`, with stable ordering (specs sorted by ID, edges by `(from, type, to)`) and non-determinism (timestamps, hashes) excluded from the semantic comparison. Delete `generated/` and rebuild byte-identically. Without determinism, "derived" is unfalsifiable and the no-second-store rule cannot be enforced. ### P4 — One enrichable primitive -**Principle · CORE.** A single object shape — the `Spec` — carries every durable delivery artifact; the familiar delivery nouns (Use Case, NFR, Decision Record; Epic, Feature, Story) are **named coordinates** on it, not separate artifact types. Realization is a **derived delivery fact** (`implemented` / `has-verifier` / `observed`), never an authored artifact. Refinement is *enrichment of the same identified object*, never migration into a different artifact type (which loses information). This is the central reframing of the whole design. *(The concrete field set is Representation; the "one primitive, enrich not convert" thesis is the Principle.)* See [`02-core-model.md`](./02-core-model.md). +**Principle · CORE.** A single object shape — the `Spec` — carries every durable delivery artifact; the familiar delivery nouns (Use Case, NFR, Decision Record; Epic, Feature, Story) are **named coordinates** on it, not separate artifact types. Realization is a **derived delivery fact** (`implemented` / `has-verifier` / `observed`), never an authored artifact. Refinement is *enrichment of the same identified object*, never migration into a different artifact type (which loses information). This is the central reframing of the whole design. *(The concrete field set is Representation; the "one primitive, enrich not convert" thesis is the Principle.)* See [the core-model spec](../../specs/model/core-model.sdp.md) (`spec:model.core-model`). **Corollary — significance governs detail, not a tier.** Because refinement is enrichment of one object — not advancement through fixed readiness stages — a spec carries detail only where the work is *architecturally significant or genuinely novel*. The Nth instance of an established shape (a CRUD endpoint, a standard codec, a barrel) earns a **reference, not a re-derivation**; padding a spec to "fill a level" destroys signal and is pure cleanup debt. Readiness (P8) is **stated by the author and checked against a floor — never a quota of artifacts to produce.** Fixed tiers pressure an author to fill the tier rather than the need — AI authors especially, which replicate the prevailing detail level even where it does not fit. *Enrich what matters; reference what is standard; pad nothing.* @@ -39,10 +39,10 @@ If a successor kept only the essentials, the design stands or falls on these. Ea **Principle · CORE.** The static type system encodes only structural *shape* (all sections optional). Whether a thing is *complete enough* for its stated readiness is a separate property, decided by validators — never encoded in conditional types. "Types describe shape; validators decide completeness." This is what lets the descriptors move freely without type gymnastics. ### P8 — Three authored descriptors; readiness is a stated, checked position -**Principle · CORE.** A spec is positioned by **three authored descriptors**: **`kind`** (a *true subtype* — the category of truth), **`altitude`** (size / scope), and **`readiness`** (design maturity). `altitude` and `readiness` move independently — a high-`altitude` feature can be barely `scoped` while a low-`altitude` example is fully `ready`. A linear idea→requirement→implementation pipeline cannot represent this. **Delivery facts** (`implemented` / `has-verifier` / `observed`) are **derived**, never readiness rungs — what the machine observes, not what the human states. And: **readiness is stated by the author; validators verify it** — the build refuses if a spec lacks the sections or relations its stated readiness floor requires. *(The independence of the descriptors, the kind-as-subtype distinction, and the floor-is-checked rule are the Principle; the specific enum members are Representation — see `02`.)* +**Principle · CORE.** A spec is positioned by **three authored descriptors**: **`kind`** (a *true subtype* — the category of truth), **`altitude`** (size / scope), and **`readiness`** (design maturity). `altitude` and `readiness` move independently — a high-`altitude` feature can be barely `scoped` while a low-`altitude` example is fully `ready`. A linear idea→requirement→implementation pipeline cannot represent this. **Delivery facts** (`implemented` / `has-verifier` / `observed`) are **derived**, never readiness rungs — what the machine observes, not what the human states. And: **readiness is stated by the author; validators verify it** — the build refuses if a spec lacks the sections or relations its stated readiness floor requires. *(The independence of the descriptors, the kind-as-subtype distinction, and the floor-is-checked rule are the Principle; the specific enum members are Representation — see `spec:model.core-model`.)* ### P9 — The `claim` is never collapsed -**Principle · CORE.** Every edge and node records its **`claim`** — whether it is **declared** (human intent, explicit in specs/packs), **anchored** (a human *binding* — bound by an in-code **anchor**), or **inferred** (machine-derived structure). The three are kept strictly distinct, forever. Every edge carries its `claim`. *(Founding Principle #3. The precedence ordering among them is Representation; the never-collapse rule is the Principle. `claim` replaces the old "provenance"; an edge computed deterministically from an authored source inherits that source's `claim` — see `03`/`04`.)* +**Principle · CORE.** Every edge and node records its **`claim`** — whether it is **declared** (human intent, explicit in specs/packs), **anchored** (a human *binding* — bound by an in-code **anchor**), or **inferred** (machine-derived structure). The three are kept strictly distinct, forever. Every edge carries its `claim`. *(Founding Principle #3. The precedence ordering among them is Representation; the never-collapse rule is the Principle. `claim` replaces the old "provenance"; an edge computed deterministically from an authored source inherits that source's `claim` — see `spec:extraction.claim-taxonomy` and `04`.)* ### P10 — Inference is never authoritative **Principle · CORE.** Static analysis recovers structural and dataflow facts, but *intent* — "this implements that," "this decision constrains those" — must be explicitly authored. Inferred edges carry confidence, are shown differently, never trigger validator errors on their own, and can only become "declared" through an explicit human act — never silent promotion. This is a permanent design boundary. @@ -113,7 +113,7 @@ These appear in the design and matter, but none is a Principle. Swapping any of | The ID grammar `:#` and namespace list | P6 (identity is the join key) | CORE | | Specific anchor syntaxes (decorator / JSDoc / anchor const) | P9/P10 (declared vs anchored) | CORE | | Flat node/edge arrays (hierarchy as edges, not nesting) | P2 (one queryable read model) | CORE | -| The enum member sets on each descriptor | P8 (three descriptors) | CORE — reconciled in `02` | +| The enum member sets on each descriptor | P8 (three descriptors) | CORE — reconciled in `spec:model.core-model` | | Generated `spec-ids` union for compile-time ref checks | early referential integrity | ASPIRATIONAL (optional convenience, L8) | | The runtime-composition adapter (Effect / Awilix / manual wiring) | "read architecture from live wiring" | ASPIRATIONAL | | HTML / Web Components for the rich view | "rich, shareable read surface from the graph" | ASPIRATIONAL | diff --git a/docs/concept/02-core-model.md b/docs/concept/02-core-model.md deleted file mode 100644 index 8d8dbc3..0000000 --- a/docs/concept/02-core-model.md +++ /dev/null @@ -1,322 +0,0 @@ -# 02 — Core Model - -The whole delivery lifecycle is modelled with one primitive: the `Spec`. This document defines its shape, the three descriptors that position it, the sections that carry detail, stable IDs, and relations. It realises principles **P4** (one enrichable primitive), **P6** (identity is the join key), **P7** (shape vs completeness), **P8** (three independent descriptors), and **L9** (envelope is a stability contract, sections are the extension surface). - -Running domain throughout: Order Management. - ---- - -## 1. The `Spec` primitive - -A `Spec` is a persistent statement of desired system truth — the one kind of thing a human authors directly. It can be tiny and vague, then progressively enriched **in place** with examples, rules, constraints, decisions, and verification, and bound to code by **anchors** — **without changing its artifact type**. Maturity changes *required completeness*, not the object format. It is the only authored **truth**-primitive; everything the machine reports about its realization is **derived** (§2). - -```ts -type Spec = { - id: SpecId; // stable, namespaced, unique (P6) - title: string; - kind: SpecKind; // the category of truth — a true subtype - altitude: SpecAltitude; // size / scope - readiness: SpecReadiness; // design maturity - narrative?: string; // prose owned by the Spec, outside the envelope - - // all sections optional — types describe shape, validators decide completeness (P7) - intent?: IntentSection; - behavior?: BehaviorSection; - constraints?: ConstraintSection[]; - model?: ModelSection; - design?: DesignSection; - decision?: DecisionSection; - verification?: VerificationSection; - ui?: UiSection; // references design artifacts; aspirational rendering - - relations?: SpecRelation[]; -}; -``` - -The **envelope** — `id`, `title`, `kind`, `altitude`, `readiness`, `relations` — is intentionally minimal and stable; it changes almost never. Variability lives in the **sections**. New capability is added by adding sections or extending enums (a MINOR change), not by reshaping the envelope (L9). - -> **Carrier note (the carrier ruling, MD-18).** The envelope above is the *logical* shape; a carrier -> serializes it physically. Relations are optional in the logical `Spec` model. A physical Markdown -> envelope writes `relations: {}` when the logical set is empty: honest carrier syntax, not a new -> logical relation requirement. The explicit physical key catches a truncated envelope at -> reification — the model itself stays relation-optional exactly as this section states. - -The same shape serves an early idea, a semi-refined behavior, a business rule, an acceptance example, an NFR, an API contract, and an example with a verifier. There is no `Requirement` / `UnimplementedRequirement` / `ImplementedRequirement` split. Two *other* things are authored but are **not** truth-primitives — the **`Pack`** (§4) and the in-code **`anchor`** (§6, and `04`); everything the machine reports about realization is **derived** (§2). - ---- - -## 2. The three descriptors - -A single maturity field is not enough. Three **descriptors** position every `Spec` — one true subtype and two positional descriptors (P8). The familiar delivery nouns (Use Case, NFR, Decision Record; Epic, Feature, Story) are **named coordinates** on these descriptors, never separate authored types. - -| Descriptor | Behaves like | Values | -|---|---|---| -| `kind` — category of truth | a **true subtype** (discriminated union — changes required detail + validation) | the 8 below | -| `altitude` — size | a **position** — same shape, different scope | `epic → feature → story` | -| `readiness` — design maturity | a **position** — same shape, different completeness | `idea → scoped → defined → ready` | - -### `SpecKind` — the category of truth (a true subtype) - -`SpecKind` answers a different question from altitude: not *how big*, but *what category of statement*. Each literal carries a recognized display label. - -```ts -type SpecKind = - | "behavior" // a use case / behavior (display: Use Case / Behavior) - | "workflow" // a multi-step process / journey (display: Workflow) - | "example" // a concrete Given/When/Then (display: Example / Scenario) - | "rule" // a business or invariant rule (display: Business Rule) - | "constraint" // a quality attribute / NFR / policy (display: Constraint (NFR)) - | "model" // domain vocabulary (display: Domain Model) - | "decision" // an architectural / product decision (display: Decision Record) - | "contract"; // an API contract, schema, or event contract -``` - -A spec is exactly one kind. If a fact straddles kinds, model it as two specs with a relation between them. `kind` is the genuine specialization — it changes which sections are required and how the spec is validated. - -### `SpecAltitude` — size / scope (a position) - -```ts -type SpecAltitude = - | "epic" // a large outcome spanning many features - | "feature" // a coherent user- or system-facing slice - | "story"; // a single, concrete unit of delivered behavior -``` - -Altitude is a clean 3-rung ladder. `epic` is the ceiling; above-epic framing (initiative / theme) is deferred, additive later. A **Scenario** is *not* an altitude — it is a low-altitude `example`-kind spec that `refines`/`verifies` a Story. - -### `SpecReadiness` — design maturity (a position) - -```ts -type SpecReadiness = - | "idea" // title + minimal intent; open questions allowed - | "scoped" // intent + a relation; the kind's natural evidence *present* (kind-conditional floor, `05` §3) - | "defined" // the kind's natural evidence *complete* (structured GWT, measurable targets, the written choice…) - | "ready"; // implementation-ready: clears the readiness floor (`05`), stated by a human (typically out of a Design Review, `06`) — the floor is checked; the review is practice, not a recorded fact -``` - -The descriptors move freely. A `feature`-altitude spec can sit at `scoped` forever (it is not failing CI as long as nothing *states* it is `ready`). A `story`-altitude spec can be `ready`. A linear idea→requirement→implementation pipeline cannot express these states; independent descriptors can. - -> Progression is *expected* but not strictly linear: a child spec can be born at a higher readiness than its parent (e.g. a low-altitude example added inside an already-`ready` feature). Reaching `ready` itself, however, requires the child's own `refines`/`dependsOn` targets to be ≥ `defined` (the `ready` floor, `05`) — so a child cannot be `ready` while a parent it refines is still `scoped`. - -### Why the enums look the way they do - -`kind`, `altitude`, and `readiness` are kept as three non-overlapping vocabularies: - -1. **`altitude` is only `epic → feature → story`.** It does not contain `rule` (a *kind* of truth, not a level), nor `contract` (a kind), nor `scenario` (a low-altitude `example`). A clean ladder of altitudes — nothing else smuggled in. -2. **`kind` has no `quality` alias.** There is one member — `constraint` — and the *flavor* (quality / security / performance / compliance / operational / policy) lives on the `ConstraintSection` (§3), where a `flavor: "performance"` constraint with a measurable `target` *is* an NFR. One concept, one place. -3. **`capability` and `domain` are neither a `kind` nor an `altitude`.** A capability is a **projection** (the *Capability Map* over high-altitude `behavior` specs); a bounded context / domain is a **`Pack` grouping** and/or projection. Both were removed as descriptors entirely — there is no "capability appears in multiple descriptors" question, because capability is on *none* of them. -4. **NFR and Scenario are labels, not kinds.** An NFR is a `constraint` with a measurable target; a Scenario is an `example`. The recognizable delivery nouns survive as display labels and projections, never as separate authored truth-primitives. - -### Readiness vs delivery facts (the derived realization signals) - -`readiness` is **authored** — what the human *states* about design maturity. It is distinct from **delivery facts**, which are **derived** — what the machine *observes* about realization, computed from graph edges and shown as badges. Authoring a delivery fact by hand is a `05` honesty violation. - -| Delivery fact | Derived when | Means — and does **not** mean | -|---|---|---| -| `implemented` | ≥1 `satisfies` edge (from an `anchor`) resolves to the spec | code **claims** to realise it — *not* that it works, is reachable, or is live | -| `has-verifier` | ≥1 `verifies` edge from an enabled verifier resolves to the spec | a verifier **exists** — *not* that it passed (pass/fail is CI's, not the graph's) | -| `observed` *(aspirational)* | runtime evidence links to the spec's target | seen live in production / telemetry | - -Because readiness and delivery facts answer *separate* questions, two high-value queries fall out: - -- `ready ∧ ¬implemented` → the **build backlog** (designed, not yet built). -- `implemented ∧ ¬ready` → the **drift alarm** (code ran ahead of the design). - -A readiness floor (`05`) checks the **structure required to *derive* a fact**, never the fact itself: stating `ready` may require that bindings *resolve* (so `implemented` is *derivable*) — it never requires the spec to *be* `implemented`. Readiness is a *stated position* about the design; delivery facts are observations about the code. - -> **Liveness is a separate, higher rung.** `implemented` and `has-verifier` are *binding / existence* facts: neither asserts the code is reachable or that the linked test runs. Runtime liveness is the **`observed`** fact (aspirational, `07`). The ladder — **`implemented`** (a binding exists) → **`has-verifier`** (a verifier exists) → **`observed`** (it ran / was seen live) — is deliberate: an anchor on dead code, or a linked-but-skipped/excluded test, is a human honesty error caught in review, not something the graph's linkage check detects (skip / quarantine / glob-exclusion is CI's concern, exactly as pass/fail is). The graph checks *binding*, never *liveness*. - ---- - -## 3. Sections - -Sections carry the detail. They are the **extension surface**: the system grows by adding sections (or enum members), never by reshaping the envelope (L9). All are optional on the type (P7); validators require the right ones to *state* a given readiness (`05`). - -| Section | Carries | Notes | -|---|---|---| -| `intent` | optional description; actor, problem, outcome, value, risks, assumptions, open questions | `openQuestions` may be flagged `blocking` to prevent stating `defined` or `ready` (the open-questions home, MD-9). | -| `behavior` | optional description; rules (prose), examples (prose or structured Given/When/Then), flows | **Content only — never refs** (the duality rule below). An example entry matures *in place*: prose → a structured `{ given, when, then }` entry → (promoted) a child `example` spec backed by a verifier. | -| `constraints[]` | a `flavor` (quality / security / performance / compliance / operational / policy), a statement, an optional `target`, optional `measurableBy` | A `performance` constraint with a measurable `target` is an NFR. `target` must be machine-readable (`p95 < 300ms`, not "fast enough") to state `defined`+. | -| `model` | optional description; domain terms (vocabulary only) — `terms: Record` | Used for pack-level coherence checks. Richer concept structures (typed concepts, attributes) are a **named deferral**; when one lands, the typing law below pulls it into the closed shape. | -| `design` | optional description; components, ports, dependencies, decisions, tradeoffs | Referenced by ID; decision bodies are linked, not parsed for semantics. | -| `decision` | optional description; context, chosen option, rationale, alternatives, consequences | The inline form of a decision; promote to a `kind:"decision"` spec when shared (see the duality rule below). | -| `verification` | optional description; mode (manual / reviewed / contract / executable) + criteria | A verifying test *existing and enabled* is the derived `has-verifier` delivery fact (§2), not an authored field here. Pass/fail is **not** in the graph — it is CI's, operational. | -| `ui` | optional description; references to component stories, design-tool nodes, visual baselines, accessibility status | **Aspirational.** Always links, never owns or renders. | - -`narrative` is content owned directly by the `Spec`; it is not an envelope field. `description` is owned only by the seven singular sections named above. `constraints` remains a machine-readable array with no description owner; explanatory prose belongs in narrative or intent. - -The `decision` section carries **no `status` field** (rejected by the typing law, MD-11): a decision's adoption arc is the envelope's `readiness` (`idea` raised → `scoped` explored → `defined` written → `ready` ratified), checked against the floor like any spec; replacement is the `supersedes` relation; a *rejected* path is not a truth-spec at all — it lives in the chosen decision's `alternatives` / `consequences`. One concept, one place. - -### The typing law — which sections have typed shapes (MD-11) - -**Every floor-bearing section is typed, as a closed shape.** A section is *floor-bearing* when a readiness-floor clause reads it as evidence (`05` §3): `intent`, `behavior`, `constraints`, `model`, `verification`, `decision`. Closed means no index signature — which is what closes the in-section honesty bypass (`behavior: { "has-verifier": true }`) at the type level, and gives authors (human and agent) autocomplete + shape guardrails, the highest-value adoption lever. The genuinely-unsettled surfaces — `design`, `ui` — stay **open bags** so they keep breathing (L9). The criterion outlives the list: when a floor clause starts reading a section, that section gets typed. - -### Section ⟷ kind — when a concern is inline vs. its own primitive - -Five section concerns have a same-named (or same-natured) `kind` twin, so the same concern can live two ways: `constraints` ⟷ `constraint`, `model` ⟷ `model`, `decision` ⟷ `decision`, and `behavior.rules` / `behavior.examples` ⟷ the `rule` / `example` kinds. **The rule** (stated here once, not scattered across the other docs): - -- Keep it **inline as a section** when it is **local detail** of its host `Spec`. -- **Promote it to a standalone `Spec`** of the matching `kind` when it is **referenced by more than one `Spec`, or needs its own identity / lifecycle / review.** The promoted spec is linked by **relations** — `constrainedBy` / `decidedBy` / `modelRefs` for the truth-section twins; `refines` (+ `verifies` for verifying examples) from the promoted `rule` / `example` child to its parent. - -Two laws make the duality safe (content-only sections — DECISIONS MD-10): - -- **Sections carry content; relations carry linkage.** A section never holds a spec ref — `behavior.rules` and `behavior.examples` are prose / structured entries only. "Which examples does this spec have?" is a reverse-edge query over the children's `refines`/`verifies`, never an authored list. -- **Promotion is exclusive.** An entry lives inline **or** as a promoted child — never both. Promotion *moves* the content out; nothing is left behind to drift. (The readiness floor counts promoted children as evidence, `05` §3 — promotion never costs a spec its earned rung.) - -`modelRefs` on a `Pack` always points at standalone `kind:"model"` specs — shared vocabulary is never inlined twice. - -### Worked example — one spec enriching in place - -```ts -// idea -export const CreateOrder = spec({ - id: specId("spec:orders.create-order"), - title: "Customer creates an order", - kind: "behavior", - altitude: "feature", - readiness: "idea", - intent: { - actor: "customer", - outcome: "turn a valid cart into an order", - value: "customers can complete purchases", - }, - relations: [refines(specId("spec:orders.order-management"))], -}); -``` - -Later, the *same* spec — same ID — gains sections and climbs readiness: - -```ts -// ready — same object, enriched (no artifact migration) -export const CreateOrder = spec({ - id: specId("spec:orders.create-order"), - title: "Customer creates an order", - kind: "behavior", - altitude: "feature", - readiness: "ready", - intent: { actor: "customer", outcome: "turn a valid cart into an order", value: "customers can complete purchases" }, - // content only — the valid-cart example is a promoted child (it refines + verifies this spec); no refs here - behavior: { rules: ["only valid carts can become orders", "creating an order emits OrderCreated"] }, - constraints: [ - { flavor: "performance", statement: "order creation is fast enough for checkout", target: "p95 < 300ms" }, - ], - relations: [ - refines(specId("spec:orders.order-management")), - decidedBy(specId("spec:decisions.order-lifecycle")), - ], -}); -``` - -The spec carries **no** code or test fields. The link to code is an **anchor** in the source (see `04`), which the extractor derives into a `satisfies` edge and an `implemented` delivery fact. A `ready` spec with no resolving anchor is the **build backlog**; once the anchor exists, the spec is `implemented ∧ ready` — the healthy state. - -A low-altitude example becomes executable the same way — it is not a separate "test object," it is a low-altitude `example`-kind spec that `verifies` its parent: - -```ts -export const ValidCartCreatesOrder = spec({ - id: specId("spec:orders.create-order.valid-cart"), - title: "Valid cart creates an order", - kind: "example", - altitude: "story", - readiness: "ready", - behavior: { - examples: [{ - given: ["a customer has a valid cart", "all cart items are available"], - when: ["the customer submits the order"], - then: ["an order is created", "an OrderCreated event is emitted"], - }], - }, - verification: { mode: "executable", criteria: ["an order row exists", "an OrderCreated event is published"] }, - relations: [refines(specId("spec:orders.create-order")), verifies(specId("spec:orders.create-order"))], -}); -``` - -The old `exemplifies` relation is gone: an example simply uses `kind:"example"` + `refines` + `verifies`. `verification` carries `mode` + `criteria` only — the *existence* of an enabled verifying test is the derived `has-verifier` fact, observed from a test **anchor**, never authored here. - -### Verifier semantics — direct, per-spec, not transitive - -`verifies` is a **direct** edge from a verifier to *its stated target*, and `has-verifier` is computed **per spec** from the verifies edges that resolve to **that** spec — it is **not** propagated transitively up `refines`. Two pieces matter: - -- **An *enabled verifier*** is an `example`/scenario `Spec` backed by a **linked, resolvable test anchor** (a `specTest`/test **anchor**, `04`), or a test anchored directly to the spec. The example expresses *what* is verified (declared); the test **binds** it to runnable code (anchored). "Enabled" is **structural — a non-dangling test binding, not a runner verdict**: whether that test is skipped, quarantined, or glob-excluded is **CI's concern, exactly as pass/fail is**. A `verifies` edge from a verifier that is **not** enabled (no resolving test binding) does not yield `has-verifier`. -- **Two edges, two targets — no transitive shortcut.** In the worked example, the `example` `verifies` its **parent** (`spec:orders.create-order`), and the backing test `verifies` the **example** (`spec:orders.create-order.valid-cart`) — *not* the parent. The parent earns `has-verifier` because an *enabled* example verifies it; the example earns `has-verifier` because its test verifies it. A spec never inherits `has-verifier` from a child merely because the child is verified. - ---- - -## 4. `Pack` — grouped coherent ideation - -A `Pack` clusters related specs (a feature initiative, a bounded slice) so a team can ideate at the group level before drilling down. It is an authored **aggregate** that states **no truth of its own** — it *references* specs, never owns vocabulary. Its `framing` is **a plain descriptive note** for ideation and review (why the group exists), **not** authored system truth: deliberately *not* the truth-bearing `intent: { outcome, value }` shape a `Spec` carries, so it can never be mistaken for one. It is never checked for completeness and never reconciled against its members — only the member `Spec`s state truth. - -```ts -export const CheckoutV1 = pack({ - id: packId("pack:checkout-v1"), - title: "Checkout v1", - framing: "let customers complete purchases reliably — lifts conversion, cuts failed orders", - modelRefs: [ref("spec:checkout.glossary")], // → standalone kind:"model" specs; shared vocabulary is never inlined twice - specs: [ - ref("spec:orders.create-order"), - ref("spec:payments.authorize-payment"), - ref("spec:inventory.reserve"), - ], -}); -``` - -A spec may belong to **many** packs. Membership is authored on the `Pack` manifest; the `belongsTo` edge is **derived** from it (it carries `claim:"declared"`, because it is a deterministic re-expression of the declared manifest — see §6 and `04`). A pack is checked for **coherence** (referenced terms and `modelRefs` resolve, membership resolves, no duplicate members) — *not* for the completeness of any individual member, and *not* for "duplicated intent" (a pack has no truth of its own to duplicate). This supports "a large coherent group of low-detail specs" as a first-class state. - -**Two grouping mechanisms stay distinct:** *refinement* (a parent spec → its children — authored truth that happens to have descendants) vs *the aggregate* (the `Pack` — a cross-cutting review collection with no truth of its own, which may span or sub-slice the refinement hierarchy). A `Pack` is the unit a **Design Review** (`06`) renders "in context." - ---- - -## 5. Stable IDs (P6) - -IDs are the load-bearing linkage of the entire graph. The grammar is a Representation; the principle is that the ID is stable, unique, namespaced, human-readable, and the *only* binding between intent and code. - -``` -: spec:orders.create-order -:# spec:orders.create-order#valid-cart (optional sub-part) -``` - -Namespaces in the MVP: `spec`, `pack`, `impl`, `api`, `test`, `oracle`, `component`, `doc`. Examples from the running domain: - -- `pack:checkout-v1` -- `spec:orders.create-order` -- `spec:orders.order-management` — a high-altitude `behavior` spec; the *Capability Map* projection is computed over these -- `spec:decisions.order-lifecycle` — a `kind:"decision"` spec; in-system decisions live under the `spec:decisions.*` convention -- `impl:orders.create-order-use-case` -- `api:orders.post` -- `test:orders.create-order.valid-cart` -- `oracle:orders.create-order` - -`doc:` is reserved for a genuinely *external* document linked from a decision spec (e.g. `doc:adr-order-lifecycle`) — never for an in-system decision, which is a `spec:decisions.*` spec. - -Code and specs link by these IDs *in strings*, never by import edges (P6) — which is the only thing that lets either side survive heavy refactoring. The cost of string IDs is typos; that is what referential-integrity checks exist to catch (`05`). An optional generated `spec-ids` union type could push some of those checks to `tsc` — a deferred convenience, never a load-bearing gate (L8). - -> **IDs carry no history.** IDs are stable by convention and survive refactors; renaming one is a repo edit that git records. The graph does not bookkeep ID history (see `01`, git is the event log). - ---- - -## 6. Relations - -Authored relations are typed, directed, **declared** edges between specs (and from specs to decision/model nodes). They are the human-asserted *intent* half of the graph (the authored/derived split, `01`/`04`). - -MVP relation vocabulary (a Representation; extensible): - -| Relation | Direction | Meaning | -|---|---|---| -| `refines(parent)` | child → parent | a more precise child; also how altitude descends (Story refines Feature refines Epic) | -| `dependsOn(spec)` | dependent → dependency | needs another spec to hold | -| `constrainedBy(specOrConstraint)` | bounded → constraint | bounded by a rule / NFR / policy spec (a *typed* dependency) | -| `decidedBy(decision)` | shaped → decision | shaped by a Decision Record (`doc:` only for a genuinely external ADR) | -| `verifies(spec)` | verifier → target | an example/scenario verifies a target | -| `supersedes(decision)` | new → old | current forward-pointer between two Decision Records that both still exist | - -`supersedes` is permitted **only** on decision specs, as a current authored statement about two records that both still exist in the repo — not as graph-resident history (see git is the event log in `01`). - -Two notes carried from the language ratification: the verb forms align with UML where a standard stereotype exists (`refines` ≈ «refine», `dependsOn` ≈ UML *Dependency*, `decidedBy` ≈ «trace», `verifies` ≈ «verify») — adopted nouns, per the governing rubric. And `constrainedBy` / `decidedBy` are deliberately kept **distinct** from a generic `dependsOn`: "bounded by an NFR" and "shaped by a decision" are high-value, separately-queryable intents that a generic dependency edge would flatten. - -> **`doc:` targets are a named deferral (MD-16).** The DSL's relation builders and `ref()` accept only `spec:` targets, so `decidedBy` → an external `doc:` ADR is designed-for but not yet representable; until the need arrives, an external ADR is referenced from the decision spec's body, not by a typed edge. (The glossary's flagged-ambiguities entry and the notes in `src/ids.ts` / `src/model/relations.ts` carry the same flag.) - -**Derived edges — never authored:** - -- `satisfies` (code → spec) — emitted from an **anchor** in source; carries `claim:"anchored"`. This is the inverse of the old `satisfiedBy`: the direction is **code → spec**, and it is **derived from anchors**, never hand-authored. -- `belongsTo` (spec → pack) — a deterministic re-expression of the `Pack` manifest; carries `claim:"declared"` (it inherits the authored manifest's authority — it is *not* advisory `inferred`). - -All authored relations carry `claim:"declared"` like every authored edge (P9, and the `claim` taxonomy in `04`). The extractor adds *anchored* and *inferred* edges separately; they are never merged into declared relations. diff --git a/docs/concept/03-the-one-graph.md b/docs/concept/03-the-one-graph.md deleted file mode 100644 index a462257..0000000 --- a/docs/concept/03-the-one-graph.md +++ /dev/null @@ -1,141 +0,0 @@ -# 03 — The One Graph - -The graph is the heart of the trust model. This document defines how it is derived, why it is deterministic, how the `claim` taxonomy stays honest, why it is regenerable, and why git — not the graph — is the event log. It realises **P1** (canonical repo), **P2** (one read model), **P3** (determinism), **P9** (the `claim` never collapsed), **P10** (inference never authoritative), and **git is the event log** from `01`. - -> The graph is a *projection of the repository at a commit*. It is never a second source of truth. - ---- - -## 1. Derivation - -``` -repo (specs + source + structural facts) - │ carrier extractors (ts-morph for .sdp.ts · the ruled Markdown parser for .sdp.md; - │ pure: reads source, writes graph + report) - ▼ -graph.json (the single read model — P2) - │ generators (pure: graph → output) - ▼ -generated/ (views, exports, slices — all disposable, L8) -``` - -Two pure steps: `graph = f(repo)` and `output = f(graph)`. The extractor is the *only* thing that reads source to build the graph; everything else reads the graph and only the graph (P2). The extractor has no database, no remote calls, and no side effects beyond emitting the graph and the validation report. - -### What the extractor reads - -- **Authored spec documents** — every `*.sdp.ts` and `*.sdp.md` under the extraction root (conventionally `/specs/`; discovery is by suffix alone, outside tooling-output and dot-directories — the `.sdp.ts`/`.sdp.md` extension law, MD-15 re-pointed by the carrier ruling, MD-18) — the declared layer: specs, packs, relations. -- **Source-code anchors** — the anchored layer: an **anchor** binds a code, test, or oracle location to a spec ID — identity, an optional label, and one `satisfies` / `verifies` / `models` target; richer structural facts are aspirational (`04` §2). Anchors carry *no* intent (see `04`). -- **Structural facts** — the inferred layer: machine-derived structure (imports, calls, symbol identity). Designed-in — the `claim` value and the advisory edge row exist, and every consumer decodes them — but **empty in the MVP**: the entry adapters and file-level impact resolve off the curated layers (`06` §2), so nothing yet needs an inferred edge; the first producer is the aspirational impact graph. - -The graph is **flat**: arrays of nodes and arrays of edges. Hierarchy and containment are expressed as edges (`belongsTo`, `refines`), never by nesting nodes inside nodes. This is a Representation (chosen for uniform querying and cheap diffs), held as a hard rule. - -```jsonc -{ - "schemaVersion": "0.4.0", - "nodes": [ - { "id": "spec:orders.create-order", "nodeType": "Primitive", "claim": "declared", "specKind": "behavior", "altitude": "feature", "readiness": "ready", "title": "Customer creates an order", "narrative": "A customer completes a valid cart.", "file": "specs/orders/create-order.sdp.ts", "sections": { "intent": { "description": "The customer needs a deterministic order outcome.", "outcome": "Create an order." } }, "deliveryFacts": ["implemented"] }, - { "id": "impl:orders.create-order-use-case", "nodeType": "CodeNode", "claim": "anchored", "file": "src/orders/create-order.use-case.ts", "line": 12 } - ], - "edges": [ - { "from": "impl:orders.create-order-use-case", "type": "satisfies", "to": "spec:orders.create-order", "claim": "anchored" }, - { "from": "test:orders.create-order.valid-cart", "type": "verifies", "to": "spec:orders.create-order.valid-cart", "claim": "anchored" } - ] -} -``` - -Node typing keeps concerns separate: every node carries `nodeType` (`Primitive` / `Pack` / `Anchor` / `CodeNode` / …), and `Primitive` nodes additionally carry `specKind` (the truth-category from `02`). This split prevents the old single `kind` field from colliding between structural class and domain truth-category. **Delivery facts** (`implemented` here) are **computed from edges** — the `satisfies` edge resolving to the spec — not authored on the node; readiness (`ready`) is the authored maturity, kept separate (`02` §2). The `satisfies` edge runs **code → spec** and is **anchored** (derived from an anchor), never hand-authored. The `verifies` edge runs **verifier → its direct target** — here the test verifies the *example* (`spec:orders.create-order.valid-cart`), not the parent; `has-verifier` is then computed per spec from the edges resolving to each, never propagated transitively up `refines` (`02` §2, *Verifier semantics*). - -Schema `0.4.0` carries authored prose in canonical payload fields: `Primitive.narrative` and `description` on the seven singular section owners. The serializer emits them in fixed recursive key order and omits absent fields; `constraints` has no description field. - -### The edge contract — one row per edge type - -Every edge in the graph has a fixed contract: where it comes from, what `claim` it carries, which surface authors it (if any), how a validator treats a broken one, and what it feeds. This table is the single reference (the per-relation detail lives in `02` §3 and `04`): - -| Edge | From → To (`nodeType`) | Authored / Derived | `claim` | Authoring surface | Validator severity | Readiness effect | Delivery-fact effect | -|---|---|---|---|---|---|---|---| -| `refines` | Primitive → Primitive | authored | declared | spec `relations[]` | **error** if target missing | `ready` floor: target ≥ `defined` | — | -| `dependsOn` | Primitive → Primitive | authored | declared | spec `relations[]` | **error** if target missing | `ready` floor: target ≥ `defined` | — | -| `constrainedBy` | Primitive → Primitive (`constraint`) | authored | declared | spec `relations[]` | **error** if target missing | — | — | -| `decidedBy` | Primitive → Primitive (`decision`) | authored | declared | spec `relations[]` | **error** if target missing (a `doc:` target is a named deferral — carried evidence, MD-16; `02` §6) | — | — | -| `verifies` | Primitive (`example`) **or** Anchor (test) → Primitive | authored | `declared` (from an example) / `anchored` (from a test anchor) | spec `relations[]` **or** `specTest` anchor | **error** if target missing | — | contributes `has-verifier` to the **target** (if the verifier is *enabled*) | -| `supersedes` | Primitive (`decision`) → Primitive (`decision`) | authored | declared | spec `relations[]` | **error** if target missing | — | — | -| `satisfies` | CodeNode → Primitive | derived (from an anchor) | anchored | source **anchor** | **error** if target missing | `ready` floor: present anchors must *resolve* | contributes `implemented` to the **target** | -| `models` | Anchor (`oracle:`) → Primitive (`behavior`, with an example space) | derived (from an anchor) | anchored | `specOracle` anchor | **error** if target missing/ineligible or more than one oracle models the same space | — | — (existence is an anchor query, never a delivery fact) | -| `belongsTo` | Primitive → Pack | derived (from the manifest) | declared (inherited) | `Pack` manifest `specs[]` | **error** if member missing/duplicate | — | — | -| calls / imports | CodeNode → CodeNode | derived (analysis) | inferred | — (machine) | **advisory** — never errors on its own | — | feeds the impact graph only | - -**Delivery facts are node facts, not edges.** `implemented` / `has-verifier` / `observed` are computed *from* the `satisfies` / `verifies` / runtime edges above and shown as badges on the node (`02` §2) — they are never authored and never themselves edges. - ---- - -## 2. Determinism (P3) - -Given the same repo at a commit, the extractor emits a **byte-identical** graph: - -- nodes sorted by `id`, edges sorted by `(from, type, to)`; -- no wall-clock timestamps, no run-specific hashes in the semantically-compared output (any metadata like a build hash is contained and excluded from comparison); -- a determinism check (`sdp build --check-clean`) rebuilds and asserts the result is byte-identical to an independent rebuild from the same commit — never a comparison against a committed `generated/` artifact, which is gitignored (L8). A divergence is a detectable, failable condition. - -Determinism is what makes "derived" *falsifiable*. Without it, the no-second-store rule (below) cannot be enforced, because you could not prove the graph is a function of the repo. - -A consequence: the authoring surface must be statically extractable (P5). A non-static expression in a spec file would make derivation non-deterministic, so the extractor responds in two tiers (`04` §1): a non-static **envelope** field (`id` · `kind` · `altitude` · `readiness` · any relation target) is a **hard error that fails the build** — the graph cannot be keyed or typed without it — while non-static **optional section** detail is dropped with a warning (graceful partial extraction, L3) rather than guessed. Those tiers are the TS carrier's granularity; the Markdown carrier is deliberately all-or-nothing per document — the per-carrier asymmetry is named at `04` §1. - ---- - -## 3. The `claim` taxonomy — declared / anchored / inferred (P9, P10) - -Every node and edge records its **`claim`** — its epistemic status. The three are kept strictly distinct, forever, and never collapsed. - -| `claim` | Means | Authority | Example | -|---|---|---|---| -| **declared** | human intent, explicit in a spec/pack file | authoritative intent | `spec:orders.create-order refines spec:checkout.complete-purchase` | -| **anchored** | a human *binding* — an **anchor** points code → a spec ID | authoritative binding (no intent) | `impl:* satisfies spec:*` via an anchor on the code | -| **inferred** | machine-derived structure | *advisory* — never authoritative | `impl:A calls impl:B` (from `ts-morph`) | - -Rules that keep this honest: - -- **Inferred edges are never authoritative (P10).** They carry confidence, are rendered differently in views, never trigger a validator *error* on their own, and can become "declared" only by a human editing the repo — never by silent promotion. -- **A computed-from-authored edge inherits its source's `claim`.** There is no 4th `claim`; `inferred` is the *advisory* kind, derived by analysis (calls, imports). An edge computed deterministically from an authored source is a derivation *mechanism*, not a claim category — so `belongsTo` (a re-expression of the `Pack`'s declared manifest) carries `claim:"declared"`, **not** advisory `inferred`. -- **Ambiguity is loud (L2).** A genuine conflict — two declarations that contradict, a duplicate ID, an anchor that disagrees with a declaration — is a build error, not a silent merge. A deterministic precedence (declared over anchored over inferred) resolves *layering*, but never a true contradiction. -- **Delivery facts are derived, not declared (epistemic boundary).** A spec's realization (`implemented` / `has-verifier` / `observed`) is computed from edges, never authored by a human. In the MVP, `has-verifier` is *structural* — a linked, enabled verifying spec/test exists — and test run results are **not** ingested (operational, CI's domain). The runtime path that would carry truly observed data (`observed`) is aspirational. *(See `06`, `07`.)* - -> This is the difference between the Protocol and a documentation generator: a doc generator treats every edge as an equal "fact." The Protocol knows which edges are *intent a human asserted*, which are *a binding a human anchored*, and which are *structure a machine guessed*, and never lets one masquerade as another. - ---- - -## 4. Regenerability and the no-second-store rule (P1, P2) - -- `generated/` is gitignored and disposable (L8). Delete it, run `sdp build`, get the same bytes back. -- No consumer reads source directly or keeps a parallel model (P2). The view reads the graph; the trace query reads the graph; an AI agent reads the graph (as JSON). One read model, many readers. Consumers may *link to* source locations recorded in the graph (a Design Review linking to the anchored file/line is legitimate); what is forbidden is independently *re-parsing* source to derive a parallel model (DECISIONS R2). -- The MVP graph is a **single JSON file** plus an in-memory graph for queries. A property-graph database is deferred until measured traversal pain — and the schema is designed to map cleanly to one later, so the deferral has forethought. - -There is no "is the graph in sync with the repo?" question, because the graph is *defined* as a function of the repo. The only thing to trust is `git` and the code. - ---- - -## 5. Git is the event log - -Founding Principle #5: **git history IS the event log; the graph and all views are projections of the repo at a commit.** Applied to the graph: - -- **The graph carries only current state.** It is the projection of the repo *as it is now*. It does not carry change history, audit trails, or superseded/deprecated bookkeeping as live state. -- **History is a `git` operation.** To see what a spec used to say, check out the prior commit and regenerate the graph. `git log` / `git blame` *is* the lifecycle history. The graph never reimplements it. -- **Removed means gone.** A spec deleted from the current repo is absent from the current graph. Its past lives in git, not in a "deleted" flag on a graph node. -- **The one kept forward-pointer** — a decision spec declaring it `supersedes` another — is a *current* authored relationship between two records both present in the repo, not the graph storing a timeline. (Full rationale in `01`, the git-is-the-event-log section.) - -Anything reconstructable from git is left to git. - -### A graph diff is just two projections - -Because the graph is a deterministic projection, comparing two commits is comparing two graphs: - -``` -graph(commitA) vs graph(commitB) → added / removed / changed nodes and edges -``` - -This gives change-impact and history reconstruction *without* a second store and without history-in-graph bookkeeping — it is git plus determinism. *(A rich impact UI is aspirational; the underlying diff is a natural consequence of the model.)* - ---- - -## 6. Schema versioning (minimal) - -The graph self-describes its `schemaVersion`. The MVP needs only that consumers can read the version and the field is present; full SemVer negotiation and a `migrate` command are aspirational. The envelope-stable / section-extensible shape (L9) means most growth is additive (MINOR), so heavyweight migration machinery is not an MVP concern. diff --git a/docs/concept/04-authoring-and-binding.md b/docs/concept/04-authoring-and-binding.md index a31382c..172e4b4 100644 --- a/docs/concept/04-authoring-and-binding.md +++ b/docs/concept/04-authoring-and-binding.md @@ -8,7 +8,7 @@ Realises **P5** (statically extractable), **P6** (ID-linked), **P9/P10** (anchor ## 1. The TypeScript Spec DSL — the TS carrier (CORE) -In the TS carrier, specs are authored as typed TypeScript in `*.sdp.ts` files, discovered by suffix anywhere under the extraction root (conventionally `/specs/`) — the Protocol's own compound extension (MD-15; the `.stories.tsx` pattern), deliberately **not** `.spec.ts`, which every JS test runner's default glob would try to execute. The DSL is a thin set of helpers (`spec`, `pack`, the branded-ID builders, relation builders) over the `Spec` shape from `02`. +In the TS carrier, specs are authored as typed TypeScript in `*.sdp.ts` files, discovered by suffix anywhere under the extraction root (conventionally `/specs/`) — the Protocol's own compound extension (MD-15; the `.stories.tsx` pattern), deliberately **not** `.spec.ts`, which every JS test runner's default glob would try to execute. The DSL is a thin set of helpers (`spec`, `pack`, the branded-ID builders, relation builders) over the `Spec` shape from `spec:model.core-model`. ```ts import { dependsOn, refines, spec, specId } from "@libar-dev/software-delivery-protocol"; @@ -26,7 +26,7 @@ export const CreateOrder = spec({ openQuestions: ["should stock reservation happen before or after order creation?"], }, behavior: { - // content only — never refs (02 §3): a promoted example is a child spec that refines/verifies this one + // content only — never refs (`spec:model.spec-sections`): a promoted example is a child spec that refines/verifies this one rules: ["only valid carts can become orders", "creating an order emits OrderCreated"], examples: ["an expired payment card is declined before any order is created"], }, @@ -42,7 +42,7 @@ A spec file in this carrier is **"a JSON file that TypeScript happens to validat - no IO, async, or imports of *product* code (only `@libar-dev/software-delivery-protocol` helpers); - relation arguments are string-literal IDs, not expressions. -If a non-static expression appears, the extractor responds in **two tiers**, drawn along the same envelope/section line the model is built on (`02` §2): +If a non-static expression appears, the extractor responds in **two tiers**, drawn along the same envelope/section line the model is built on (`spec:model.core-model`): - **Envelope fields are hard errors.** A non-static `id`, `kind`, `altitude`, `readiness`, or any **relation target** **fails the build** — these are the keys the graph is built on, so the extractor must never guess, drop, or anonymise them. A spec whose identity or position cannot be reified deterministically is not extracted at all. - **Optional section detail degrades gracefully.** A non-static expression *inside an optional section* drops *that one property* with a warning, keeping the rest of the spec (graceful partial extraction, L3). It never aborts the build for section detail. @@ -119,7 +119,7 @@ export const createOrderValidCartTest = specTest({ // ... the real test (plain Vitest/Jest/etc.) lives alongside ... ``` -Here the test `verifies` the **example** it backs (`spec:orders.create-order.valid-cart`); that test anchor is exactly what makes the example an **enabled verifier**, so the example's own `verifies` edge can confer `has-verifier` on the parent it targets (the direct, per-spec, non-transitive rule in `02` §2, *Verifier semantics*). This produces the bidirectional spec↔test trace that is a core MVP deliverable: query "what verifies this spec?" and "what does this test cover?" from the graph. The test's *result and its runner status* (pass/fail, skipped, quarantined, glob-excluded) are operational — CI's, never in the graph; the graph records only that an enabled verifier — a **resolvable test binding** — *exists*, never that it ran (the derived `has-verifier` delivery fact, `02` §2). +Here the test `verifies` the **example** it backs (`spec:orders.create-order.valid-cart`); that test anchor is exactly what makes the example an **enabled verifier**, so the example's own `verifies` edge can confer `has-verifier` on the parent it targets (the direct, per-spec, non-transitive rule in `spec:model.spec-sections`). This produces the bidirectional spec↔test trace that is a core MVP deliverable: query "what verifies this spec?" and "what does this test cover?" from the graph. The test's *result and its runner status* (pass/fail, skipped, quarantined, glob-excluded) are operational — CI's, never in the graph; the graph records only that an enabled verifier — a **resolvable test binding** — *exists*, never that it ran (the derived `has-verifier` delivery fact, `spec:model.core-model`). The third builder beside `codeAnchor` and `specTest` is **`specOracle`** (`oracle:` namespace), under the same binding-only law: identity plus one `models` target, emitted as an **anchored** Anchor → `Spec` edge. It records that an **oracle** — the authored expected-outcome semantics for a parent's example space (§4) — *exists*; the graph never records what the oracle says (the function beside the anchor is never extracted, never authoritative), and the anchor confers **no delivery fact** — discovery is an anchor query. A behavior example space has **zero or one** resolving oracle binding: a non-behavior/no-space target or competing oracle is a conformance error, and consumers fail closed rather than selecting an authority. @@ -144,7 +144,7 @@ The MVP records runtime bindings *generically*: **anchors** name routes/handlers The **carrier-independent executable machinery is landed (CORE)**; what stays deferred is named per surface below. The landed half — identical under whichever richer surface arrives, so none of it waits on one: - **Generated contracts.** `sdp build` emits a per-example **step contract** (the literal-union module a test binds handlers against — spec-side drift is a compile error naming the exact step) and a per-parent **space contract** (the typed dimensions of the example space · every child's bound point · the Outcome union derived from the parent's Then vocabulary). Both derive from the extracted graph, never the evaluated spec module (one validation path, MD-14), and a test may import them *because* they are projections — never the authored spec. -- **The typed example space.** A parent `behavior` spec's `exampleSpace` declares the parameter-slot vocabulary its steps use; each `example` child binds one **point** (point-per-example, MD-17 — a table of cases is authoring-surface sugar). The **concreteness law** is one structural cell in the example kind's `defined` floor: an unbound slot in a used step caps the example below `defined`. +- **The typed example space.** A parent spec's `exampleSpace` declares the parameter-slot vocabulary its steps use (typically a `behavior` spec; any kind may own the space when the vocabulary parameterizes its own law); each `example` child binds one **point** (point-per-example, MD-17 — a table of cases is authoring-surface sugar). The **concreteness law** is one structural cell in the example kind's `defined` floor: an unbound slot in a used step caps the example below `defined`. - **The oracle.** The authored expected-outcome semantics for a parent's example space — implementation-side, beside the tests, bound by the `specOracle` anchor (§2), never extracted. Typed against the generated space contract on both sides: a renamed slot fails to compile, claiming an outcome the specs never stated is a `tsc` error, and `unspecified` is a first-class answer. - **The execution half.** The framework-neutral `/runner` core plus the `/vitest` adapter subpath (vitest an optional peer of the adapter alone); failure messages render in the spec's own language. diff --git a/docs/concept/05-validation-and-honesty.md b/docs/concept/05-validation-and-honesty.md index c8d0ac4..be3a476 100644 --- a/docs/concept/05-validation-and-honesty.md +++ b/docs/concept/05-validation-and-honesty.md @@ -27,7 +27,7 @@ An **error fails the build**; a **gap informs**. This is the one sense in which | Layer | Checks | Status | |---|---|---| -| Types | structural shape of `Spec` (all sections optional, branded IDs; floor-bearing sections closed-typed — the typing law, `02` §3 / MD-11) | CORE | +| Types | structural shape of `Spec` (all sections optional, branded IDs; floor-bearing sections closed-typed — the typing law, `spec:model.spec-sections` / MD-11) | CORE | | Schema | the graph JSON validates against its schema | CORE | | Graph validators | cross-file conformance + honesty invariants (§2) | CORE | | Architecture rules | forbidden-dependency, layer boundaries, domain purity | ASPIRATIONAL | @@ -41,13 +41,13 @@ Types describe **shape**; validators decide **completeness** (P7). Completeness These are the non-negotiable core. CI fails on any error. They split across the two families. -**They run over the one graph — there is exactly one validation path** (MD-14): source → extract (static reification, P5) → graph → checks; `sdp validate` is `sdp build` + checks, and `validateGraph` is the sole validation seam. Validating any *evaluated* form (importing spec modules and checking the resulting objects) would check a phantom — a non-static expression evaluates to a value on import but is dropped by static reification, so the checks could pass a spec the graph doesn't actually hold. For the same reason there is no pre-graph validation seam of any kind: a check that reads anything but the derived graph is a second validation path, forbidden. Authoring-time feedback is per-carrier: the type system's job in the TS carrier (typed sections, `02` §3), the ruled parser's hard diagnostics in the Markdown carrier — static reification (P5) is what rejects non-static authoring, and the `sdp/spec-static` lint is a designed-for earlier surfacing of the same tiers (`04` §1), never a parallel validator path. +**They run over the one graph — there is exactly one validation path** (MD-14): source → extract (static reification, P5) → graph → checks; `sdp validate` is `sdp build` + checks, and `validateGraph` is the sole validation seam. Validating any *evaluated* form (importing spec modules and checking the resulting objects) would check a phantom — a non-static expression evaluates to a value on import but is dropped by static reification, so the checks could pass a spec the graph doesn't actually hold. For the same reason there is no pre-graph validation seam of any kind: a check that reads anything but the derived graph is a second validation path, forbidden. Authoring-time feedback is per-carrier: the type system's job in the TS carrier (typed sections, `spec:model.spec-sections`), the ruled parser's hard diagnostics in the Markdown carrier — static reification (P5) is what rejects non-static authoring, and the `sdp/spec-static` lint is a designed-for earlier surfacing of the same tiers (`04` §1), never a parallel validator path. **Conformance checks:** 1. **Referential integrity.** Every ID referenced (in relations, `modelRefs`, anchors) resolves to a node that exists. A dangling reference is an error — with a "did you mean…?" suggestion where possible. *(This is the cost of string-ID linkage from P6, paid down at build time.)* 2. **Duplicate-ID detection.** No two nodes share an ID. A duplicate is an error, never an auto-resolved merge (L2 — ambiguity is loud). -3. **`claim` separation never collapsed.** A `declared` edge is never silently "satisfied" by an `inferred` one; node/edge typing stays valid and distinct (`03`, `04`) — `nodeType` / `claim` / the edge-contract rows, and the three descriptors (`specKind` · `altitude` · `readiness`) carry their ratified values. The floor is never evaluated over an unratified descriptor (fail closed, never a crash or a silent skip). The edge-contract rows include the kind-typed endpoints (`03` §1): `constrainedBy` → a `rule`/`constraint`-kind spec (`02` §6's "a rule / NFR / policy spec") · `decidedBy` → a `decision`-kind spec · `supersedes` only between `decision`-kind specs; the declared-`verifies`-from-an-example row stays check 4's informative warning (a wrong-kind verifier confers nothing rather than failing the build). +3. **`claim` separation never collapsed.** A `declared` edge is never silently "satisfied" by an `inferred` one; node/edge typing stays valid and distinct (`spec:extraction.derive-graph`, `04`) — `nodeType` / `claim` / the edge-contract rows, and the three descriptors (`specKind` · `altitude` · `readiness`) carry their ratified values. The floor is never evaluated over an unratified descriptor (fail closed, never a crash or a silent skip). The edge-contract rows include the kind-typed endpoints (`spec:extraction.derive-graph`): `constrainedBy` → a `rule`/`constraint`-kind spec (`spec:model.relations`'s "a rule, constraint, or policy Spec") · `decidedBy` → a `decision`-kind spec · `supersedes` only between `decision`-kind specs; the declared-`verifies`-from-an-example row stays check 4's informative warning (a wrong-kind verifier confers nothing rather than failing the build). 4. **`verifies` linkage.** The bidirectional spec↔test trace resolves: a test anchored `verifies: spec:X` must point at an existing spec. **Honesty checks:** @@ -59,7 +59,7 @@ These are the non-negotiable core. CI fails on any error. They split across the **Informative (a `gap` or `orphan`, not an error by default):** 8. **Orphan detection.** A spec with no relations and nothing pointing at it is surfaced as a warning — it has fallen out of the graph's connective tissue. (A per-team severity override is designed-for, deferred.) -9. **Readiness/delivery gaps.** A `ready` spec with no resolving verifier is a surfaced `gap` (the build backlog and drift-alarm queries, `02` §2), not an error. +9. **Readiness/delivery gaps.** A `ready` spec with no resolving verifier is a surfaced `gap` (the build backlog and drift-alarm queries, `spec:model.core-model`), not an error. Two cross-cutting honesty rules apply to all validators: @@ -91,19 +91,19 @@ The floor has two parts: **kind-blind structural clauses** (the same for every k | `constraint` | `constraints[]` non-empty | every entry has a machine-readable `target` | | `model` | `model.terms` non-empty | same — a vocabulary either has terms or it doesn't | | `decision` | `decision` section present (context / alternatives may precede the choice) | `decision.decision` — the chosen option — is written | -| `contract` *(interim)* | as `behavior` (examples read naturally as sample payloads) | as `behavior` — **named deferral:** when a dedicated contract section lands, the typing law (`02` §3) pulls it in and this row repoints to it | +| `contract` *(interim)* | as `behavior` (examples read naturally as sample payloads) | as `behavior` — **named deferral:** when a dedicated contract section lands, the typing law (`spec:model.spec-sections`) pulls it in and this row repoints to it | Three laws bound the table: - **Monotonic by construction.** Every `defined` cell implies its `scoped` cell — clearing the higher rung always clears the lower. (The pre-MD-12 floor failed this for `constraint`: its natural evidence stopped counting between `scoped` and `defined`.) -- **Promotion-neutral.** Promoted forms count wherever one exists (`02` §3) — de-composing a spec into children never costs it its earned rung. The converse honesty bound (MD-16): a promoted form counts **only when it itself carries its kind's evidence** — promotion *moves content out* (`02` §3), so an empty stub child, or a `constrainedBy` edge that does not resolve to a `constraint`-kind spec carrying its constraints, is not evidence. +- **Promotion-neutral.** Promoted forms count wherever one exists (`spec:model.spec-sections`) — de-composing a spec into children never costs it its earned rung. The converse honesty bound (MD-16): a promoted form counts **only when it itself carries its kind's evidence** — promotion *moves content out* (`spec:model.spec-sections`), so an empty stub child, or a `constrainedBy` edge that does not resolve to a `constraint`-kind spec carrying its constraints, is not evidence. - **Convergence is honest.** Where the rungs converge (`rule`, `model`), the floor refuses to become a quota ("more terms," "more rules" — the tier-filling P4 forbids); those rungs then differ only by the author's stated confidence plus the kind-blind clauses. > **Representation note (MD-13).** In code, the floor *table* is the single source of truth: rows carry the clause id, description, and a named predicate; the evaluator is one generic loop and the clause-id type is derived from the table. The tables above and the data in `readiness-floor.ts` are intended to be reviewable as mirror images. -**The `ready` floor is earned, not asserted** — and it is **not** a delivery fact. The floor may require that anchors *resolve* (so `implemented` is *derivable*); it **never** requires the spec to *be* `implemented`. Readiness is a *stated position* about the design; delivery facts are observations about the code (`02` §2). Higher floors degrade gracefully if their structural inputs are absent. +**The `ready` floor is earned, not asserted** — and it is **not** a delivery fact. The floor may require that anchors *resolve* (so `implemented` is *derivable*); it **never** requires the spec to *be* `implemented`. Readiness is a *stated position* about the design; delivery facts are observations about the code (`spec:model.core-model`). Higher floors degrade gracefully if their structural inputs are absent. -**`ready` is the structural floor plus a human's `declared` statement — not a record that a review occurred.** The Design Review (`06` §5) is *where* a human typically decides, but the graph stores **no** review/approval fact, and the validator never checks one — that would be the workflow-gating the honesty guardrail forbids (§1). Where approval matters — a baseline — the signed git tag is the approval artifact (authorship + tag, `03`), not an authored primitive (approval / RBAC stays outside the model, `07`). +**`ready` is the structural floor plus a human's `declared` statement — not a record that a review occurred.** The Design Review (`06` §5) is *where* a human typically decides, but the graph stores **no** review/approval fact, and the validator never checks one — that would be the workflow-gating the honesty guardrail forbids (§1). Where approval matters — a baseline — the signed git tag is the approval artifact (authorship + tag, `spec:extraction.derive-graph`), not an authored primitive (approval / RBAC stays outside the model, `07`). > **Stated vs derived readiness.** The author *states* a `readiness`; the same floor table also yields a *derived* readiness — the highest rung whose cumulative clauses pass. Both ship in the MVP: the floor check fails a stated rung the structure does not earn, and the Design Review renders stated beside the floor reached, naming the first unmet clause (the evaluator reports which clause fails). The divergence banner fires only in the dishonest direction — derived *at-or-above* stated is ordinary information, because the floor is a floor, never a quota that nags upward. The principle: *the stated rung is not trusted, it is verified.* (Note the verb: readiness is **stated/asserted**, never "claimed" — "claim" is reserved for the `claim` taxonomy in `04`.) @@ -111,7 +111,7 @@ Three laws bound the table: ## 4. Pack-level coherence (CORE-adjacent) -A `Pack` is validated for **coherence**, not member completeness, and **deterministically only**: referenced terms and `modelRefs` resolve, membership resolves, no duplicate member IDs. There is **no** "duplicated intent" check — a `Pack` states no truth of its own (only a plain `framing` note, `02` §4), so there is nothing to duplicate; semantic duplication is a human/agent judgment, not a validator's. This lets a team hold "a large coherent group of low-detail specs" without the build demanding implementation. Coherence-vs-completeness is a distinct check from the readiness floors above. +A `Pack` is validated for **coherence**, not member completeness, and **deterministically only**: referenced terms and `modelRefs` resolve, membership resolves, no duplicate member IDs. There is **no** "duplicated intent" check — a `Pack` states no truth of its own (only a plain `framing` note, `spec:model.pack-aggregate`), so there is nothing to duplicate; semantic duplication is a human/agent judgment, not a validator's. This lets a team hold "a large coherent group of low-detail specs" without the build demanding implementation. Coherence-vs-completeness is a distinct check from the readiness floors above. --- diff --git a/docs/concept/06-consumers-and-projections.md b/docs/concept/06-consumers-and-projections.md index 35662a4..51a5276 100644 --- a/docs/concept/06-consumers-and-projections.md +++ b/docs/concept/06-consumers-and-projections.md @@ -131,7 +131,7 @@ Why patching dissolves: **Principle · CORE (concept).** The flagship curated surface is the **Design Review**: a `Spec` (or a `Pack`) rendered **in context** — its neighbors, relations, `claim`/delivery badges, auto-generated **design questions** (from blocking open questions + `gap`s), and a **findings** table. It adopts the recognized SDLC noun. -- It is the context in which a human **decides** to state `ready`: a spec is reviewed *in context* (alone and in its related set / `Pack`), and stating `ready` is the human's call coming out of that review. The review is **never an automated gate** — validators check only the structural **readiness floor** (`05`); they do not adjudicate the review or state `ready` on the author's behalf. This keeps the honesty guardrail from `00`/`05` (checks police conformance & honesty, never workflow) intact (`02` §2, `05`). So `ready` is an **authored `declared` statement** — its *checkable* content is the floor; that a review actually happened is **not a fact the graph records**, so where review evidence matters it rides **git** (authorship, commit, the baseline tag — `03` §5), never an authored approval primitive. +- It is the context in which a human **decides** to state `ready`: a spec is reviewed *in context* (alone and in its related set / `Pack`), and stating `ready` is the human's call coming out of that review. The review is **never an automated gate** — validators check only the structural **readiness floor** (`05`); they do not adjudicate the review or state `ready` on the author's behalf. This keeps the honesty guardrail from `00`/`05` (checks police conformance & honesty, never workflow) intact (`spec:model.core-model`, `05`). So `ready` is an **authored `declared` statement** — its *checkable* content is the floor; that a review actually happened is **not a fact the graph records**, so where review evidence matters it rides **git** (authorship, commit, the baseline tag — `spec:extraction.derive-graph`), never an authored approval primitive. - It is a **pure projection** — findings resolve through the edit loop (§4); there is **no stored `Finding` type**, no second store. - *Concept is core; rich diagrams grow later* — the MVP renders the relationship slice; heatmaps and interactive trees are aspirational (Spec Studio, §8). diff --git a/docs/concept/07-mvp-roadmap-and-open-questions.md b/docs/concept/07-mvp-roadmap-and-open-questions.md index 6a672e1..81e1aef 100644 --- a/docs/concept/07-mvp-roadmap-and-open-questions.md +++ b/docs/concept/07-mvp-roadmap-and-open-questions.md @@ -71,7 +71,7 @@ Deferred deliberately; recorded so they are not lost. None blocks the MVP. - **Derived-readiness banner timing (resolved, recorded here for traceability).** Floor enforcement and the banner both ship in the MVP: the Design Review renders stated readiness beside the structurally-reached floor and names the first unmet clause — cheaply, because the floor evaluator reports which clause fails. *(`05` §3.)* - **Impact-graph depth (resolved, recorded here for traceability).** The boundary is decided, not open: **file-level** impact ships in the MVP (`git diff` → `byFile` → a curated-graph walk gives changeset blast-radius with no symbol index, surfacing an explicit `coverage-unknown` item for any changed file that has no anchor so a too-small set is never mistaken for complete), while the **exhaustive** impact graph — `bySymbol`, symbol-level identity, cross-package find-all-usages, drift/fan-in tooling — is deferred (Iterate). What remains genuinely open is only *when* the exhaustive graph earns its way in, driven by measured pain (§5), never the MVP boundary itself. *(`06` §2.)* - **Inline-vs-centralized anchor semantics.** Anchors carry no intent in the MVP. How much *structural* semantics an anchor may carry beyond the landed binding contract (`id` · optional `label` · one `satisfies`/`verifies` target) — e.g. a future `component`/`implements` — is left configurable later. *(`04` §2.)* -- **Graph-DB timing.** File-based until measured traversal pain; the schema is designed to map to a property graph later. *(`03` §4.)* +- **Graph-DB timing.** File-based until measured traversal pain; the schema is designed to map to a property graph later. *(`spec:extraction.regenerability`.)* - **Trace-link recovery.** Permitted later only as an assistive *suggestion* engine (the impact graph's "propose candidates" assist role), never a declared edge — bounded permanently by P10. *(`01`, `06` §2.)* - **When (if ever) harnesses / evidence become CORE** (the Gherkin half is answered — the carrier ruling, MD-18, declined a Gherkin-like surface). Driven by measured pain after the MVP loop holds, not by the roadmap. @@ -107,11 +107,11 @@ the standing invariant plus where its protection lives. is the committed golden fixture: `test/fixtures/checkout-v1/expected-graph.json` plus the `expected-design-review/` golden tree (legitimate because it lives under `test/fixtures/`, never gitignored `generated/`, L8; paths repo-relative / POSIX; binding line numbers deliberately included — the line *is* - the binding location, `03` §1). The **determinism self-checks** — "is the output a pure function of the + the binding location, `spec:extraction.derive-graph`). The **determinism self-checks** — "is the output a pure function of the repo" — live separately in the CLI tests: `--check-clean` (two independent extractions, byte-identical), delete-`generated/`-and-rebuild, and the clean-repo determinism test (the full pipeline at a different absolute path is byte-identical) — each a self-comparison, **never** a diff against a committed - `generated/` artifact (`03` §2). + `generated/` artifact (`spec:extraction.determinism`). - **③ The derived-readiness banner ships in the Design Review.** The view renders stated readiness beside the structurally-reached floor and names the first unmet clause — cheap because the floor evaluator reports *which* clause fails (`05` §3, with the open-questions home, MD-9: the floor reads diff --git a/docs/concept/DECISIONS.md b/docs/concept/DECISIONS.md index 10077be..8f559a8 100644 --- a/docs/concept/DECISIONS.md +++ b/docs/concept/DECISIONS.md @@ -43,8 +43,8 @@ positions and are never reused. | Label | One line | Canonical in | |---|---|---| -| **D1** | readiness is separate from delivery facts | `02` §2 | -| **D2** | sections are typed to their evidence role | `02` §3 | +| **D1** | readiness is separate from delivery facts | [Core model spec](../../specs/model/core-model.sdp.md) (`spec:model.core-model`) | +| **D2** | sections are typed to their evidence role | [Spec sections spec](../../specs/model/spec-sections.sdp.md) (`spec:model.spec-sections`) | | **D3** | Pack is a reified grouping/aggregate | [Pack reified decision](../../specs/decisions/pack-reified.sdp.md) (`spec:decisions.pack-reified`) | | **D4** | Design Review is the flagship curated projection | `06` §5 | | **D5** | the agent surface is a visible graph the agent scripts | [Agent surface scripts the graph decision](../../specs/decisions/agent-surface-scripts-graph.sdp.md) (`spec:decisions.agent-surface-scripts-graph`) | diff --git a/docs/concept/README.md b/docs/concept/README.md index 72d509a..8fb8232 100644 --- a/docs/concept/README.md +++ b/docs/concept/README.md @@ -37,8 +37,6 @@ Every load-bearing claim is named as one or the other, on purpose — so a Repre | — | [Ubiquitous Language](../../CONTEXT.md) | The ratified **glossary** (`CONTEXT.md`, repo root): terms, descriptor values, relations, a worked dialogue, flagged ambiguities. The terminology base for every doc below. | | 00 | [Vision, Scope & MVP Boundary](./00-vision-scope-and-mvp-boundary.md) | The honest full ambition **and** the sharp MVP line, in one place. | | 01 | [Founding Principles & Invariants](./01-founding-principles-and-invariants.md) | The load-bearing laws, each tagged Principle/Representation and CORE/ASPIRATIONAL. Git-as-event-log. The `claim` epistemics. | -| 02 | [Core Model](./02-core-model.md) | The `Spec` primitive, the three descriptors (`kind` · `altitude` · `readiness`), sections, delivery facts, stable IDs, relations. | -| 03 | [The One Graph](./03-the-one-graph.md) | Derivation, determinism, the `claim` taxonomy, regenerability, git as the event log, the no-second-store rule. | | 04 | [Authoring & Binding](./04-authoring-and-binding.md) | The two ruled authoring carriers — Markdown (`.sdp.md`, the default) and the TypeScript DSL (`.sdp.ts`, import source and lawful per-ID option) — plus generic source anchors (framework-neutral). Packs remain TS until a Pack syntax ruling; harnesses named but deferred; the carrier competition is ruled (MD-18). | | 05 | [Validation & Honesty](./05-validation-and-honesty.md) | Conformance + honesty checks and readiness floors; the MVP subset sharply separated from aspirational tiers. | | 06 | [Consumers & Projections](./06-consumers-and-projections.md) | MVP: the agent surface (a typed graph the agent scripts) + Design Review + reader; edits via intent→agent→git (no patch loop). Two surfaces (curated graph vs impact graph). Aspirational: Studio, exports, MCP surface. | @@ -46,8 +44,8 @@ Every load-bearing claim is named as one or the other, on purpose — so a Repre **Reading paths:** - Evaluating the idea → `00`, then `01`. -- Building the MVP → `07`, then `02`, `04`, `05`. -- Understanding the trust model → `01`, `03`. +- Building the MVP → `07`, then the model Specs (`spec:model.core-model` and siblings), `04`, `05`. +- Understanding the trust model → `01`, then the extraction Specs (`spec:extraction.derive-graph`, `spec:extraction.determinism`, `spec:extraction.claim-taxonomy`, `spec:extraction.regenerability`). - Reasoning about the long-term shape → `00`, `06`. --- diff --git a/eslint.config.js b/eslint.config.js index 3aa34e2..b4cad68 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -47,11 +47,21 @@ export default tseslint.config( }, }, { - // The root generated contract is intentionally absent until the later generate:self-hosting - // gate leg. Typecheck runs after that generation and checks this test's contract types; lint + // 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. - files: ["test/self-hosting-duplicate-ids.test.ts", "test/self-hosting-sdp-import.test.ts"], + // 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", + ], rules: { "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-assignment": "off", diff --git a/examples/checkout-v1/README.md b/examples/checkout-v1/README.md index 4fb3e5d..1b6cd88 100644 --- a/examples/checkout-v1/README.md +++ b/examples/checkout-v1/README.md @@ -137,12 +137,12 @@ Revert the edit afterwards. The next `build` regenerates `generated/`. ## Where the concepts live -| Concept | Read | -| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| the vocabulary (every term used above) | [ubiquitous language](../../CONTEXT.md) | -| the executable half (contracts · example space · oracle) | [ubiquitous language — "The executable half"](../../CONTEXT.md) | -| the `Spec` primitive, descriptors, sections, relations | [`02` core model](../../docs/concept/02-core-model.md) | -| the one graph, determinism, the `claim` taxonomy | [`03` the one graph](../../docs/concept/03-the-one-graph.md) | -| the DSL and anchors (authoring & binding) | [`04` authoring & binding](../../docs/concept/04-authoring-and-binding.md) | -| the checks and the readiness floor | [`05` validation & honesty](../../docs/concept/05-validation-and-honesty.md) | -| the reader, the Design Review, the agent surface | [`06` consumers & projections](../../docs/concept/06-consumers-and-projections.md) | +| Concept | Read | +| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| the vocabulary (every term used above) | [ubiquitous language](../../CONTEXT.md) | +| the executable half (contracts · example space · oracle) | [ubiquitous language — "The executable half"](../../CONTEXT.md) | +| the `Spec` primitive, descriptors, sections, relations | [`spec:model.core-model`](../../specs/model/core-model.sdp.md) and the model Specs beside it | +| the one graph, determinism, the `claim` taxonomy | [`spec:extraction.derive-graph`](../../specs/extraction/derive-graph.sdp.md) · [determinism](../../specs/extraction/determinism.sdp.md) · [claim taxonomy](../../specs/extraction/claim-taxonomy.sdp.md) | +| the DSL and anchors (authoring & binding) | [`04` authoring & binding](../../docs/concept/04-authoring-and-binding.md) | +| the checks and the readiness floor | [`05` validation & honesty](../../docs/concept/05-validation-and-honesty.md) | +| the reader, the Design Review, the agent surface | [`06` consumers & projections](../../docs/concept/06-consumers-and-projections.md) | diff --git a/jtbd-stories/01-capture-and-evolve-intent.md b/jtbd-stories/01-capture-and-evolve-intent.md index 116af21..08d25e0 100644 --- a/jtbd-stories/01-capture-and-evolve-intent.md +++ b/jtbd-stories/01-capture-and-evolve-intent.md @@ -8,7 +8,7 @@ The job here is to get a thought into the system and let it grow without ever fo ### Capture a rough idea with zero ceremony **Phase:** MVP -**References:** [02 — Core Model](../docs/concept/02-core-model.md) +**References:** [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`) > **When** a half-formed idea for a behaviour or capability shows up mid-work, **I want to** record it as a spec in seconds with only an `id`, a `title`, and a sentence of intent, **so I can** keep the thought in the canonical repo instead of losing it to a side channel. @@ -30,7 +30,7 @@ The job here is to get a thought into the system and let it grow without ever fo ### Enrich a spec in place as it matures **Phase:** MVP -**References:** [02 — Core Model](../docs/concept/02-core-model.md), [04 — Authoring & Binding](../docs/concept/04-authoring-and-binding.md) +**References:** [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`), [04 — Authoring & Binding](../docs/concept/04-authoring-and-binding.md) > **When** an idea sharpens into rules, examples, constraints, and decisions, **I want to** add detail and raise readiness on the *same* spec object rather than convert it into a new "requirement" or "test" artifact, **so I can** avoid information loss, broken links, and duplicate sources of truth. @@ -51,7 +51,7 @@ The job here is to get a thought into the system and let it grow without ever fo ### Refine a big idea into child specs without losing the parent **Phase:** MVP -**References:** [02 — Core Model](../docs/concept/02-core-model.md) +**References:** [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`) > **When** a large, coherent idea needs to be broken into concrete pieces, **I want to** author child specs that `refine` the parent, **so I can** drill into detail while the parent stays useful for roadmap, architecture, and shared understanding. @@ -72,7 +72,7 @@ The job here is to get a thought into the system and let it grow without ever fo ### Position any spec on independent descriptors **Phase:** MVP -**References:** [02 — Core Model](../docs/concept/02-core-model.md) +**References:** [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`) > **When** I'm describing anything from a large epic down to a single example with a verifier, **I want to** use the same spec shape positioned by three authored descriptors — `kind` (category of truth), `altitude` (size / scope), and `readiness` (design maturity) — **so I can** model the whole delivery surface without a zoo of artifact types. @@ -93,7 +93,7 @@ The job here is to get a thought into the system and let it grow without ever fo ### Group related specs into a coherent pack **Phase:** MVP -**References:** [02 — Core Model](../docs/concept/02-core-model.md), [05 — Validation & Honesty](../docs/concept/05-validation-and-honesty.md) +**References:** [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`), [05 — Validation & Honesty](../docs/concept/05-validation-and-honesty.md) > **When** I'm ideating a feature initiative or a bounded slice as a cluster of related specs, **I want to** group them in a `Pack` with shared model references, **so I can** hold a large coherent group of low-detail specs and reason at the group level before drilling into any single member. @@ -114,7 +114,7 @@ The job here is to get a thought into the system and let it grow without ever fo ### Survey the landscape to decide where to invest next **Phase:** Iterate -**References:** [02 — Core Model](../docs/concept/02-core-model.md), [06 — Consumers & Projections](../docs/concept/06-consumers-and-projections.md) +**References:** [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`), [06 — Consumers & Projections](../docs/concept/06-consumers-and-projections.md) > **When** I'm scoping a new initiative and deciding where to put effort, **I want to** see the pack/capability landscape and where intent is still thin (specs at `idea`/`scoped`), **so I can** choose what to flesh out next instead of guessing from memory or scattered docs. diff --git a/jtbd-stories/02-bind-code-to-intent.md b/jtbd-stories/02-bind-code-to-intent.md index cc9b33b..07abecb 100644 --- a/jtbd-stories/02-bind-code-to-intent.md +++ b/jtbd-stories/02-bind-code-to-intent.md @@ -29,7 +29,7 @@ The job here is to connect real implementation to the spec that justifies it — ### Link by stable ID so specs and code survive refactors **Phase:** MVP -**References:** [04 — Authoring & Binding](../docs/concept/04-authoring-and-binding.md), [02 — Core Model](../docs/concept/02-core-model.md) +**References:** [04 — Authoring & Binding](../docs/concept/04-authoring-and-binding.md), [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`) > **When** I rename files, move modules, or reorganise spec folders, **I want to** keep the spec-to-code linkage intact because it is bound by stable IDs rather than file paths or imports, **so I can** refactor freely without breaking the graph. diff --git a/jtbd-stories/03-one-graph.md b/jtbd-stories/03-one-graph.md index fe658bc..2a27f36 100644 --- a/jtbd-stories/03-one-graph.md +++ b/jtbd-stories/03-one-graph.md @@ -8,7 +8,7 @@ This theme *is* the founding principle expressed as jobs. There is one graph; it ### Derive one canonical graph from the repo **Phase:** MVP -**References:** [03 — The One Graph](../docs/concept/03-the-one-graph.md), [02 — Core Model](../docs/concept/02-core-model.md) +**References:** [the derive-graph spec](../specs/extraction/derive-graph.sdp.md) (`spec:extraction.derive-graph`), [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`) > **When** I have authored specs and anchored code, **I want to** build one canonical graph of nodes and edges directly from the repo, **so I can** query the whole delivery state without standing up or syncing a second store. @@ -29,7 +29,7 @@ This theme *is* the founding principle expressed as jobs. There is one graph; it ### Trust every edge's `claim` **Phase:** MVP -**References:** [03 — The One Graph](../docs/concept/03-the-one-graph.md) +**References:** [the derive-graph spec](../specs/extraction/derive-graph.sdp.md) (`spec:extraction.derive-graph`) > **When** I look at a relationship in the graph, **I want to** know whether it was **declared** by an author, **anchored** by an in-code pointer (an anchor), or **inferred** by structural analysis, **so I can** trust intentional facts and treat guesses as guesses. @@ -50,7 +50,7 @@ This theme *is* the founding principle expressed as jobs. There is one graph; it ### Regenerate the graph as a pure function of the repo **Phase:** MVP -**References:** [03 — The One Graph](../docs/concept/03-the-one-graph.md) +**References:** [the derive-graph spec](../specs/extraction/derive-graph.sdp.md) (`spec:extraction.derive-graph`) > **When** I suspect the graph is stale or just want a clean slate, **I want to** delete it and rebuild it byte-identically from the repo, **so I can** treat the graph as disposable and never worry about it diverging from the code. @@ -71,7 +71,7 @@ This theme *is* the founding principle expressed as jobs. There is one graph; it ### Reconstruct history from git, not a second store **Phase:** MVP -**References:** [03 — The One Graph](../docs/concept/03-the-one-graph.md), [01 — Founding Principles & Invariants](../docs/concept/01-founding-principles-and-invariants.md) +**References:** [the derive-graph spec](../specs/extraction/derive-graph.sdp.md) (`spec:extraction.derive-graph`), [01 — Founding Principles & Invariants](../docs/concept/01-founding-principles-and-invariants.md) > **When** I need to know how a spec or decision evolved, **I want to** reconstruct its history from git commits, **so I can** audit and time-travel with the tools I already have instead of a separate event store. diff --git a/jtbd-stories/04-keep-it-honest.md b/jtbd-stories/04-keep-it-honest.md index 5319c5f..1de23f6 100644 --- a/jtbd-stories/04-keep-it-honest.md +++ b/jtbd-stories/04-keep-it-honest.md @@ -30,7 +30,7 @@ A graph nobody trusts is worthless. The job here is to make the delivery state * ### Enforce completeness rules per readiness level **Phase:** MVP -**References:** [05 — Validation & Honesty](../docs/concept/05-validation-and-honesty.md), [02 — Core Model](../docs/concept/02-core-model.md) +**References:** [05 — Validation & Honesty](../docs/concept/05-validation-and-honesty.md), [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`) > **When** a spec states a readiness level, **I want to** have the system require the sections and links that level's floor demands, **so I can** rely on readiness as a real signal rather than a self-asserted label. diff --git a/jtbd-stories/05-see-and-share.md b/jtbd-stories/05-see-and-share.md index f77d5f4..ad7e971 100644 --- a/jtbd-stories/05-see-and-share.md +++ b/jtbd-stories/05-see-and-share.md @@ -74,7 +74,7 @@ The graph is only valuable if humans and agents can actually consume it. The job ### Conduct a Design Review and decide readiness **Phase:** MVP -**References:** [06 — Consumers & Projections](../docs/concept/06-consumers-and-projections.md) (§5), [02 — Core Model](../docs/concept/02-core-model.md), [05 — Validation & Honesty](../docs/concept/05-validation-and-honesty.md) +**References:** [06 — Consumers & Projections](../docs/concept/06-consumers-and-projections.md) (§5), [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`), [05 — Validation & Honesty](../docs/concept/05-validation-and-honesty.md) > **When** a spec or `Pack` is mature enough to consider stating `ready`, **I want to** review it in context — its neighbors, relations, `claim`/delivery badges, open questions, and gaps — **so I can** decide whether to state `ready`, with the structural floor visible but the judgment mine. diff --git a/jtbd-stories/07-trace-and-impact.md b/jtbd-stories/07-trace-and-impact.md index 6bb124b..48bbeac 100644 --- a/jtbd-stories/07-trace-and-impact.md +++ b/jtbd-stories/07-trace-and-impact.md @@ -8,7 +8,7 @@ Once everything is one graph, the payoff is navigation: what does a change touch ### See what a change impacts before making it **Phase:** MVP -**References:** [06 — Consumers & Projections](../docs/concept/06-consumers-and-projections.md), [03 — The One Graph](../docs/concept/03-the-one-graph.md) +**References:** [06 — Consumers & Projections](../docs/concept/06-consumers-and-projections.md), [the derive-graph spec](../specs/extraction/derive-graph.sdp.md) (`spec:extraction.derive-graph`) > **When** I'm about to change a spec or a piece of code, **I want to** see what depends on it upstream and downstream, **so I can** refactor or re-scope with confidence instead of discovering the blast radius after the fact. @@ -30,7 +30,7 @@ Once everything is one graph, the payoff is navigation: what does a change touch ### Trace a spec to its verification and back **Phase:** MVP -**References:** [03 — The One Graph](../docs/concept/03-the-one-graph.md), [05 — Validation & Honesty](../docs/concept/05-validation-and-honesty.md) +**References:** [the derive-graph spec](../specs/extraction/derive-graph.sdp.md) (`spec:extraction.derive-graph`), [05 — Validation & Honesty](../docs/concept/05-validation-and-honesty.md) > **When** I need to know whether a spec is actually backed by a test, **I want to** trace from the spec to the tests that verify it and back from any test to the specs it covers, **so I can** trust coverage as a structural fact instead of a filename convention or an act of faith. @@ -72,15 +72,15 @@ Once everything is one graph, the payoff is navigation: what does a change touch ### Find the specs that still need a verifier **Phase:** MVP -**References:** [02 — Core Model](../docs/concept/02-core-model.md) (§2 delivery facts), [05 — Validation & Honesty](../docs/concept/05-validation-and-honesty.md) +**References:** [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`), [05 — Validation & Honesty](../docs/concept/05-validation-and-honesty.md) > **When** I'm planning verification coverage for a `Pack` or release, **I want to** list the specs that lack an enabled verifier (the `has-verifier` `gap`s), **so I can** target exactly what needs a test instead of guessing or auditing files by hand. -**Essence:** Coverage is a structural fact in the one graph, not a spreadsheet. `has-verifier` is derived per spec (`02` §2); its absence on specs that warrant one is a surfaced `gap` — so "what is unverified" is a query, and a common QA situation is served without a second tool. +**Essence:** Coverage is a structural fact in the one graph, not a spreadsheet. `has-verifier` is derived per spec (`spec:model.core-model`); its absence on specs that warrant one is a surfaced `gap` — so "what is unverified" is a query, and a common QA situation is served without a second tool. **Acceptance criteria:** 1. The graph answers "which specs in this `Pack` (or across the graph) have no resolving `has-verifier`" directly, as a list, not a count. -2. The answer reflects the structural fact only — a verifier *exists and is enabled* — never a pass/fail verdict, which stays CI's (`02` §2, JS-G2). +2. The answer reflects the structural fact only — a verifier *exists and is enabled* — never a pass/fail verdict, which stays CI's (`spec:model.spec-sections`, JS-G2). 3. `ready` specs missing a verifier are highlighted as the priority slice (the honest "designed, stated done, unverified" gap), distinct from low-readiness specs that are not expected to have one yet. 4. Each gap is reachable to its spec in context (the Design Review, JS-E1/JS-E4), so planning a test starts from the spec, not a bare ID. 5. The result is stable across refactors because it is keyed on stable IDs and `verifies` edges, not test file locations. @@ -92,16 +92,16 @@ Once everything is one graph, the payoff is navigation: what does a change touch ### Trace a decision to what it shaped, for an audit trail **Phase:** Iterate -**References:** [02 — Core Model](../docs/concept/02-core-model.md), [03 — The One Graph](../docs/concept/03-the-one-graph.md) (§5 git is the event log) +**References:** [the core-model spec](../specs/model/core-model.sdp.md) (`spec:model.core-model`), [the derive-graph spec](../specs/extraction/derive-graph.sdp.md) (`spec:extraction.derive-graph` — git is the event log) > **When** I need to justify why the system is built the way it is, **I want to** trace a Decision Record to the specs and code it shaped — and back — **so I can** produce an audit trail from the graph and git instead of maintaining a separate compliance document. -**Essence:** `decidedBy` is an explicit, queryable edge, and git is the event log (`03` §5). Together they answer "what decision shaped this, and what did that decision touch" without a bespoke audit store — an auditor/compliance situation served by the same one graph. +**Essence:** `decidedBy` is an explicit, queryable edge, and git is the event log (`spec:extraction.derive-graph`). Together they answer "what decision shaped this, and what did that decision touch" without a bespoke audit store — an auditor/compliance situation served by the same one graph. **Acceptance criteria:** 1. From a `kind:"decision"` spec, I can list every spec that declares `decidedBy` it; from any spec, I can see the decisions that shaped it — both directions, from the graph. 2. The trace continues into code through the shaped specs' `satisfies` anchors, so a decision reaches the implementation it influenced. 3. The `supersedes` forward-pointer between decisions is followable, so the *current* decision and the ones it replaced (both still in the repo) are both visible, with history left to git. 4. Every step carries its `claim`, so an auditor can tell asserted intent (`declared`) from a binding (`anchored`) from machine-derived structure (`inferred`). -5. The trail is reconstructable at any commit — `git` plus a deterministic rebuild — so "what did this look like when the decision was made" needs no second store (`03` §5). +5. The trail is reconstructable at any commit — `git` plus a deterministic rebuild — so "what did this look like when the decision was made" needs no second store (`spec:extraction.derive-graph`). 6. The trace is a pure projection: producing it never authors truth and never gives a write path to canonical source. diff --git a/plans/20-self-hosting-phase-3.md b/plans/20-self-hosting-phase-3.md new file mode 100644 index 0000000..dc5ff6d --- /dev/null +++ b/plans/20-self-hosting-phase-3.md @@ -0,0 +1,1021 @@ +# Plan 20 — Self-hosting phase 3: the executable-specs rewrite, readiness maturation, and the first concept-doc dissolutions + +> **Status:** ✅ EXECUTED — phase-3 implementation complete; the pre-close adversarial review is +> archived with every finding dispositioned and its four majors remediated. This is plan 20, the +> highest primary-numbered plan and the latest ✅ EXECUTED ground (superseding plan 18, whose +> phase-2 close carried plan 19's remediation). 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 18 §7 done-record](18-self-hosting-phase-2.md) · [the phase-2 brief's +> remaining-work enumeration](18-self-hosting-phase-2-brief.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 the remaining-work enumeration recorded at the phase-2 +close: the systematic tests-to-executable-specs rewrite (item 1), readiness maturation riding it +(item 2), and the first per-doc concept audits under the dissolution decision (item 3). The +deferred tail (item 4) stays deferred unless a wave forces an entry under fire. Execution happens +only on `feature/protocol-self-application-phase-3`. Sessions run agent-executed and +orchestrator-verified; **owner ratification of the whole phase happens at the phase PR review** — +the gate ledger (§10) records that honestly and never claims a live owner acceptance that did not +occur. + +## (b) Context + +Phase 2 left one carrier rule, 58 interlinked specs, and a green twelve-leg gate — but the thesis +is not yet fully applied to the repo itself: 529 of 532 repository tests are plain vitest +assertions whose verification the graph cannot see, and `has-verifier` is earned through the +executable path for only two spec pairs (the duplicate-IDs tracer and the `sdp import` +round-trip). The corpus sits at 7 `ready` / 51 `defined`; the concept docs still duplicate +contract the specs now carry. Phase 3 makes the rewrite systematic where the verify loop is +honestly cheap, matures readiness only as verifiers land, and performs the first per-doc +dissolution audits — deleting a concept doc only where its semantic contract is fully carried. + +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. **Tranche 1 — the executable-specs rewrite, first systematic waves.** Convert the cheap, + law-shaped test suites to bound executable specs under the tracer template: the validation + family (S1), the extraction/model cheap wins (S2), and the uncovered engine areas — contract + codegen, the runner core, slot notation — which first need honest corpus additions (S3). +2. **Tranche 2 — readiness maturation (S4).** Promote `defined` specs to `ready` only where the + floor clears **and** a resolving verifier exists; sweep the specs whose verifiers landed in + S1–S3 plus the three carrier specs that already carry anchored verifiers. +3. **Tranche 3 — per-doc concept audits and the first dissolutions (S5).** Audit `02`, `03`, + `05`, `06`, and `07` against the coverage trail; repair the last prose ties; delete only the + docs whose contract is fully carried, per-doc, judged over the regenerated Design Review. +4. **Close (S6).** Adversarial review over the full branch diff, remediation, the full gate + (including clean-clone), and 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`) and the golden +corpus oracle (`test/self-hosting-graph.test.ts`) — their specs already hold anchored verifiers, +and their worlds are not cheap; the packaging/bootstrap smoke surface; the deferred tail +(Markdown Pack syntax · the gen-1 `.feature` adapter · the no-reparse read seam · temporal-guard +token assembly) unless a wave forces an entry; the editor-association gap; new content-quality +validators; bulk concept purges. + +## (d) §1 Engineering rulings + +1. **The unit of conversion is the law, never the assertion.** A wave converts a spec's law into + an example space plus 1–3 bound points that honestly exercise it. Row-for-row expansion of + `it.each` tables is refused: the residual plain-vitest assertions survive as regression + evidence (checks never police workflow), and a residual test is deleted only when a bound + contract honestly carries its assertion. Conversion must never weaken coverage. +2. **A `rule`-kind parent may own an Example space.** The parser routes `## Example space` for + every kind, codegen keys on `sections.behavior.exampleSpace` alone, and sections are optional + detail slices under the section/kind duality — so the tracer parent's `behavior` kind was + convention, not law. Recorded here as an under-fire ruling; it does not pass the three-part + test (the base already forced it), so it stays a plan record, not a decision spec. +3. **The four-artifact template is the only conversion path.** Parent `## Example space` + (`gwt-vocabulary` fence) → `example`-kind child spec (`refines` + `verifies` the parent, one + immediate `gwt` point, fully bound, vocabulary-compatible) → regenerated + `generated/contracts/` → `bindExample` handlers beside a top-level `specTest` anchor whose + `verifies` names the child. Anchors bind real test entrypoints only — no decorative anchors. +4. **Table sugar does not fire by default.** Where a law's honest coverage needs more points than + 1–3 siblings, prefer sibling `example` specs (the point-per-example decision's static + expansion is already the ruled semantics); the Markdown table syntax is ruled only if a wave + shows sibling authoring is dishonest or unusable in practice, and any such ruling lands as its + own recorded grammar session, never a wave side-effect. Same discipline for the single-literal + vocabulary form and the multi-entry constraint form. +5. **The readiness promotion law.** `ready` is stated only where (a) the `ready` floor clears, + (b) a resolving verifier exists — so the promotion introduces no `honesty/gaps` warning — and + (c) the spec's content is honestly acceptance-grade. Promotion is per-spec judgment, never a + quota; a spec may honestly stay `defined` with a verifier. +6. **Bookkeeping moves with each wave, batch-green.** Every new spec ID enters + `specs/self-hosting.pack.sdp.ts` in the same change; contract-dependent test files enter the + `vitest-test.mjs` dependency table; `generated/` is regenerated and committed in-wave; + `npm run check` is green at every commit a wave blesses. No blessed commit holds a generated + contract whose bound test is planned-but-missing unless the wave records the gap deliberately + (the checkout `invalid-cart` precedent). +7. **Temporal-guard discipline.** Specs never carry dates, plan references, or wave handles; + source and test surfaces never carry the guard's enforced tokens (ISO dates, `plans/` + path references, session/wave/fold handles). Pre-existing prose provenance comments naming a + plan in source are lawful pointers into the process record — the guard's token list is the + law's boundary, deliberately. Wave prompts must repeat this — the guard enumerates untracked + files too. *(Amended at the post-close records sweep: the drafted wording claimed a broader + source/test ban than the guard enforces or the tree honors — see the docket's + temporal-guard row for the decided disposition.)* +8. **Concept deletion is per-doc, audited, and ratified at the PR.** Each candidate doc gets an + audit row (§4): every section mapped to its carrying surface or the doc is not deleted. The + audit is judged over the regenerated Design Review; the deletion commit carries the audit + pointer; the owner's ratification is the phase PR review. `CONTEXT.md`, the lean + `DECISIONS.md` registry, and `plans/` never dissolve. +9. **Drift discipline.** Any disagreement a wave finds between a spec's stated law and engine + behavior is drift to resolve deliberately — fix the stale side on the record; never silently + promote code behavior into intent to make a contract bind. + +## (e) §2 Wave inventory — the conversion ledger + +State values: `planned` → `done` / `deferred` / `dropped` (with reasons, §7). "Points" are bound +example children; residual tests stay per ruling 1. + +| Wave | Source tests | Parent spec (vocabulary) | Planned points | State | +|---|---|---|---|---| +| S1 | `test/validators.test.ts` orphan/gap cases | `spec:validation.warn-level-signals` | 1–2 | done — 2 points (`orphan-signal`, `ready-gap-signal`) | +| S1 | `test/validators.test.ts` referential cases | `spec:validation.referential-integrity` | 1–2 | done — 2 points (`dangling-target`, `did-you-mean`) | +| S1 | `test/validators.test.ts` + fixture rows | `spec:validation.authored-honesty` | 1–2 | done — 2 points (`section-authored-fact`, `unearned-stated-fact`) | +| S1 | `test/validators.test.ts` claim cases | `spec:validation.claim-separation` | 1–2 | done — 2 points (`collapsed-edge-claim`, `unratified-descriptor`) | +| S1 | `test/validators.test.ts` verifies/models cases | `spec:validation.verification-linkage` | 1–2 | done — 2 points (`unbound-example`, `unresolved-oracle`) | +| S1 | `test/validators.test.ts` pack cases (coverage gap) | `spec:validation.pack-coherence` | 1 | done — 1 point (`incoherent-aggregate`) | +| S2 | `test/exclude-diagnostics.test.ts` | `spec:extraction.excludes` | 1–2 | done — 2 points (`segment-boundary`, `refused-path`) | +| S2 | `test/graph-schema.test.ts` | `spec:extraction.schema-versioning` | 1 | done — 1 point (`declared-version`) | +| S2 | `test/ids.test.ts` (representative points) | `spec:model.stable-ids` | 1–2 | done — 2 points (`namespaced-round-trip`, `malformed-refusal`) | +| S2 | `test/extract-parity.test.ts` (representative) | `spec:carrier.markdown-parser` | 1 | done — 1 point (`bounded-parity`) | +| S3 | `test/codegen.test.ts` (representative laws) | `spec:extraction.executable-contracts` (enriched) | 2–3 | done — 3 points (`concreteness-refusal`, `multi-entry-example`, `case-colliding-path`) | +| S3 | `test/runner.test.ts` | `spec:extraction.example-runner` (new, §3 ID confirmed) | 1–2 | done — 2 points (`step-order`, `red-step-naming`) | +| S3 | `test/notation.test.ts` | `spec:carrier.slot-notation` (new, §3 ID confirmed) | 1–2 | done — 2 points (`typed-declaration`, `refused-guess`) | + +The readiness sweep (S4) carries its own ledger in §6, beside the rulings that produced it; the +audit tranche (S5) carries its tables in §4 and §7. + +## (f) §3 Modeling policy for the S3 corpus additions + +- **The runner core** (`src/runner/`): one `behavior`-kind spec, feature altitude, under the + extraction family (the executable-contracts chain) — proposed `spec:extraction.example-runner` + — `refines spec:extraction.executable-contracts`, anchored to `planExample`/`runExamplePlan`. + Its law: a bound example runs steps in contract order against a fresh world; a red step names + itself in the spec's own words; an unbound handler is a compile-time refusal. +- **Slot notation** (`src/notation/slots.ts`): one `rule`-kind spec, story altitude — proposed + `spec:carrier.slot-notation` — `refines spec:carrier.markdown-authoring`, anchored to + `parseSlots`/`stepSkeleton`. Its law: the typed slot forms, the skeleton identity, and the + refuse-to-guess posture for malformed groups. +- **Codegen enrichment**: `spec:extraction.executable-contracts` gains the concreteness law, the + refusal catalog (`contracts/*` findings), and an example space over contract generation; its + existing anchors stand. +- Kinds and altitudes follow the corpus conventions; IDs are two-segment; every new spec enters + the pack manifest and the §4 coverage trail. Thin filler specs are a defect — if a law cannot + be stated with honest content, it is not authored this phase. + +## (g) §4 The concept-doc audit design + +Per-doc criterion (the dissolution decision): a doc may be deleted only once its semantic +contract is fully carried by specs and lean registries, judged at a Design Review — never a bulk +purge. Audit table template (one per candidate doc, recorded in §7 at execution): + +`| Doc section | Carrying surface (spec / registry / code) | Verdict (carried · gap · expository-only) |` + +Candidate ranking from the phase-2 coverage trail, with known blockers to repair or record: + +1. **`02-core-model.md`** — strongest: all six sections have carrying specs. Known tie: the + D1/D2 rows in `DECISIONS.md` still point at `02` §2/§3 as canonical — re-point to + `spec:model.core-model` / `spec:model.spec-sections` before deletion. +2. **`07-roadmap...`** — docket-superseded at phase 2; likeliest deletion on supersession + grounds; sweep inbound references (`AGENTS.md` names `07` as the historical slice roadmap). +3. **`03-the-one-graph.md`** — near: only §5 ("git is the event log", duplicated in `01`) lacks + a carrier; either carry it or record the doc as not-yet-deletable. +4. **`05-validation-and-honesty.md`** — blocked unless repaired: §3's floor tables are the + recorded mirror of `readiness-floor.ts` and are cited by MD-13/MD-9 as carrying surfaces; + §5–§7 have no carriers. Expect audit-recorded, not deleted, unless the wave honestly closes + the gaps. +5. **`06-consumers-and-projections.md`** — partial (§6, §8–§10 uncarried); expect + audit-recorded, not deleted. + +`00`, `01`, `04`, and the concept README are out of audit scope this phase. Every deletion +sweeps inbound references (`AGENTS.md`, `README.md`, `CONTEXT.md` pointers, other concept docs, +`check-carrier-truth.mjs`'s audit map) in the same commit. + +## (h) §5 Watch items carried into phase 3 + +Terminal states are recorded at the close; every entry survives with a reason, fired or not. + +| Watch item | Trigger | Ruling rule | Entry state | Terminal state (close) | +|---|---|---|---|---| +| table-sugar syntax | sibling-point authoring proves dishonest for a real law | rule Markdown syntax; preserve static sibling expansion semantics | watch — unfired | **unfired, survives** — 29 points were authored as siblings across fifteen laws and the 1–3 point budget was never pressed (S2 ruling 3, S3 ruling 4); no law needed more points than siblings gave it | +| single-literal vocabulary form | a real vocabulary slot needs one literal | rule only with a concrete fixture | watch — unfired | **unfired, survives** — S3 met the closed-union boundary in `spec:carrier.slot-notation` and stated it as a refusal in authored words instead of ruling syntax under fire (S3 ruling 4) | +| multi-entry constraint form | a real constraint needs more than one entry | rule carrier syntax + parity evidence together | watch — unfired | **unfired, survives** — no wave authored a constraint at all; the corpus holds one `constraint`-kind spec and it was untouched, so nothing forced the form | +| array-section prose sub-owner | a real spec needs prose beside array entries | rule with the forcing spec | watch — unfired | **unfired, survives** — every enrichment this phase fitted the existing owners (rule lines, model terms, `## Example space`); the prose-ownership rule was stated, never stretched | +| Markdown Pack syntax | a Pack needs Markdown authoring for a real caller | rule separately; Pack is not a kind | watch — unfired | **unfired, survives** — the pack manifest absorbed 29 new member ids as TS with no friction; no caller needed Markdown authoring, so the carrier rule's "Packs remain TS" default held on its own terms | + +## (i) §6 Rulings-under-fire running log (execution) + +Appended as waves execute; entries name the forcing material and the disposition. + +### 2026-07-26 — S1 (the validation family) + +1. **Bound handlers live in a dedicated `test/self-hosting-validators.test.ts`, not inside + `test/validators.test.ts`.** *Forcing material:* the anchor scanner's file-level gate. A + `specTest` anchor is extracted only from a file the scanner reads, and the scanner reads a file + only when it imports the package specifier (or a trusted relative builder module). Adding that + import to `test/validators.test.ts` turned the scan on for the whole file, and its two + pre-existing **inline** `specTest(…)` fixture calls — arguments to `deriveFixtureGraph`, not + anchor constants — immediately raised two `extract/misplaced-authoring` warnings the corpus must + not carry. The alternatives were both worse: hoisting those fixture calls to top level would + mint real graph anchors pointing at deliberately dangling ids, and leaving the warnings in place + would break the corpus's zero-finding oracle. *Disposition:* the wave's bound points go in a + dedicated suite, matching the repository's existing convention for bound self-hosting examples + (`test/self-hosting-duplicate-ids.test.ts`, `test/self-hosting-sdp-import.test.ts`); the + plain-vitest suite is untouched and survives whole as regression evidence (ruling 1). No ADR + test is met — the base already forced this — so it stays a plan record. +2. **The wrapper's contract-dependency table becomes one row per generated tree.** *Forcing + material:* a second self-hosting suite. The old table keyed one row per test file, and a second + row for the same tree would have made the missing-tree stderr repeat + `npm run generate:self-hosting` — a message + `test/self-hosting-duplicate-ids.test.ts` pins byte-exactly. *Disposition:* rows now carry a + `testPaths` list, so the recovery command a missing tree names is stated once regardless of how + many suites depend on it; the pinned messages are unchanged. +3. **Rule-kind parents own their Example space (§1 ruling 2, exercised).** All six S1 parents are + `rule` kind; the parser, codegen, and floor treated them exactly like the `behavior`-kind + tracer. No grammar change was needed and no watch item fired. +4. **Three unstated laws were promoted into spec text — from intended truth, never from code.** + The did-you-mean suggestion (`05` §2 check 1), the fail-closed descriptor posture (`05` §2 + check 3), and "a non-resolving trace confers nothing" (`02` §2, `04` §2) were all already + ratified in the concept material and merely absent from the carrying specs; each is now a rule + line on its parent so its bound point reads against stated intent. **Corrected at S6 (review-09 + S-2):** that sentence over-claims for one of the three. The did-you-mean *suggestion* is + ratified verbatim (`05` §2 check 1, "where possible"), but the **tie rule** the spec line adds + ("names the unique nearest known id … and stays silent when two candidates tie") is not stated + anywhere in the concept material; it is a legitimate reading of `05` §2's cross-cutting + ambiguity-is-loud rule, and the spec line states that rationale, but it was authored here + rather than carried in. Recorded as a derivation from a ratified principle, not as + pre-ratified text; the clause stands and `suggestNearestId` already agreed with it. + `pack-coherence` gained one + mechanism sentence (membership is counted on the derived `belongsTo` edges the manifest + re-expresses) so its `{memberCount}` dimension is readable; the validator's behavior was found + to agree with the stated law, so no code moved. +5. **Six parents and eleven children promoted to `ready` under §1 ruling 5.** Every promotion + clears the floor, carries a resolving verifier through the executable path (the children by + their bound `specTest` anchors, the parents by the enabled-example rule), and introduces no + `honesty/gaps` warning. The closing distribution is `defined: 45 / ready: 24` over 69 specs. + +### 2026-07-26 — S2 (the extraction, model, and carrier cheap wins) + +1. **Three dedicated suites, one per family, rather than one S2 suite.** *Forcing material:* the + four converted laws span three families whose worlds share nothing — a temporary filesystem + root, an in-memory derived payload, in-process ID parsing, and a fixture-pair reification. + *Disposition:* `test/self-hosting-extraction.test.ts` (excludes · schema-versioning), + `test/self-hosting-model.test.ts` (stable-ids), and `test/self-hosting-carrier.test.ts` + (markdown-parser). S1's per-tree wrapper row absorbed all three as `testPaths` entries with no + change to the pinned recovery messages, which is exactly what that restructuring was for. No + ADR test is met; it stays a plan record. +2. **A behavior-kind parent owns an Example space beside a `Verification — executable` section.** + *Forcing material:* `spec:carrier.markdown-parser` already carried an anchored verifier + (`test:protocol.markdown-parser`) and its executable-verification criteria. Adding + `## Example space` between `## Behavior` and `## Verification — executable` routes into + `sections.behavior.exampleSpace` exactly as for a `rule` parent; the existing anchor and the + verification section are untouched. *Disposition:* the executable path now stands **beside** + the anchored one on the same spec — both bindings resolve, and the spec's bounded parity claim + is unchanged in wording. +3. **Partial points are the honest shape for refusal-versus-success pairs.** *Forcing material:* + three of the four vocabularies branch on an outcome (discovery completes vs. is refused, + parsing resolves vs. is refused), so each sibling uses only the Then steps its branch reaches. + The concreteness law reads used steps only, so both siblings clear `defined` and each contract + carries exactly its own steps. *Disposition:* no new grammar; the table-sugar watch item stayed + unfired — two siblings per law were enough, and the ledger's 1–2 point budget was never + pressed. +4. **No drift found between the four stated laws and engine behavior.** Every clause the points + exercise — exact-prefix exclusion across both discovery surfaces, refusal rather than + normalization, the payload-carried schema version, the lowercase-namespace and `#` sub-part + grammar, and the shared-validator-ID parity with carrier-specific severity and extraction + outcomes — held as authored. Four mechanism lines were added (`normalizeExcludes` / + `discoverFiles`, `schemaVersion` / `deriveGraph`, `parseId` / `formatId`) purely to name the + realizing entrypoints in the corpus's established style; **no code moved and no claim was + widened**. +5. **Four parents and six children promoted to `ready` under §1 ruling 5.** Each parent's floor + clears (every `refines` / `dependsOn` target is at least `defined`), each child carries a + resolving verifier through its bound `specTest` anchor, and each parent earns `has-verifier` + from its enabled example — the promotion introduces no `honesty/gaps` warning. + `spec:carrier.markdown-parser` was judged acceptance-grade on its own terms: its parity claim + is explicitly bounded and its four named non-claims are stated exclusions, not open questions. + The closing distribution is `defined: 41 / ready: 34` over 75 specs; the corpus stays at 0 + errors and 0 warnings. + +### 2026-07-26 — S3 (the corpus additions: codegen, the runner core, slot notation) + +1. **The bound points join the existing per-family suites rather than three new files.** + *Forcing material:* S2's ruling 1 settled the convention as one dedicated suite **per family**, + and all three S3 laws fall inside families that already own one — extraction + (`spec:extraction.executable-contracts`, `spec:extraction.example-runner`) and carrier + (`spec:carrier.slot-notation`). Their worlds share nothing with the suites' existing worlds, but + that was already true inside `test/self-hosting-extraction.test.ts` (a temporary filesystem root + beside an in-memory derived payload), so world-sharing was never the criterion the convention + turned on. *Disposition:* `test/self-hosting-extraction.test.ts` absorbs the codegen and runner + points, `test/self-hosting-carrier.test.ts` the notation points; the wrapper's per-tree + `testPaths` row and the pinned missing-tree stderr are untouched, which is exactly what S1's + restructuring was for. No ADR test is met — it stays a plan record. +2. **Two new `impl:` anchors landed in source, in the canonical anchor-constant form.** + `impl:protocol.example-runner` (`src/runner/index.ts`, beside `planExample`) and + `impl:protocol.slot-notation` (`src/notation/slots.ts`, beside `parseSlots`) each import their + builders from this package's canonical `ids` / `model/code-anchor` modules, so the anchor-trust + rule holds. Both modules are leaves in the import graph, so the added imports introduce no cycle + and the `/runner` subpath keeps its dependency-light shape. +3. **Inline authoring builders in a bound suite stay silent, so the codegen world is honest.** + *Forcing material:* the codegen law needs probe graphs, and the natural way to build one is the + typed `spec(…)` builder inside a step handler — the exact shape S1 had to relocate for + `specTest(…)`. *Disposition:* the misplaced-authoring scan resolves builders through **protocol + bindings only** (the package specifier, or a relative import canonicalizing to this package's + `ids` / `model/code-anchor`), and the probe helpers import `spec` / `specId` / `refines` from + `../src/index.js`, which is neither. The scan is on for the file (the `specTest` anchors need + it) and reports nothing; the corpus keeps its zero-finding oracle. No grammar or code moved. +4. **Two vocabulary shapes were bounded by the notation itself, and the boundary was recorded, not + worked around.** A bound slot value is one double-quoted literal with no interior quote, so a + step text carrying a closed union (`{a:"x"|"y"}`) cannot itself be bound as a slot value. The + closed-union clause therefore stays a stated rule with `test/notation.test.ts` as its regression + evidence, and the two bound points cover the typed form, the skeleton identity, the prose brace, + and the unusable group. The single-literal vocabulary form is stated as a refusal in + `spec:carrier.slot-notation` **without ruling new syntax** — the watch item stays unfired. +5. **No drift found between the three stated laws and engine behavior.** Every clause the points + exercise held as authored: contract order with one handler per repeated step, the `at step:` + failure prefix with the original error preserved by identity, the concreteness refusal (space + contract emitted, step contract withheld), the named second entry, and the whole-tree + withholding on a case-folded collision with a warning that never gates. Where the specs state + more than the points exercise — the compile-time bindings law, the closed-union declaration + form, the loud local degradations — the clause is authored from intended truth (the runner and + codegen module contracts and the ratified executable-half vocabulary) and the plain-vitest + suites remain its evidence. **No code moved and no claim was widened.** + + **Corrected at S6 (review-09 P-1).** One item in that list was not true as written: the + concreteness refusal was *not* among the clauses the S3 points exercised. Mutation testing + showed `concreteness-refusal` stayed green with the unbound-slot refusal deleted, because its + world ran under a parent owning an example space, where vocabulary resolution withholds the + step contract for the same input — the point verified a disjunction of two gates and could not + name which one fired. The honest statement for S3 is: the drift sweep confirmed the *other* + four clauses; the concreteness clause was carried by `test/codegen.test.ts` alone, so no + coverage was ever lost, but the executable claim was over-stated. Remediated in S6 by running + the point under a parent that declares no shared vocabulary, which removes the second gate + structurally rather than asserting around it — re-probed both ways (see the S6 log). +6. **Three parents and seven children promoted to `ready` under §1 ruling 5.** Each parent's floor + clears (`spec:extraction.executable-contracts` → `spec:extraction.build-pipeline`, + `spec:extraction.example-runner` → `spec:extraction.executable-contracts`, + `spec:carrier.slot-notation` → `spec:carrier.markdown-authoring`, every target at least + `defined`), each child carries a resolving verifier through its bound `specTest` anchor, and + each parent earns `has-verifier` from its enabled examples — the promotions introduce no + `honesty/gaps` warning. `spec:extraction.executable-contracts` was judged acceptance-grade only + after the enrichment: the two-rule stub it was could not honestly carry `ready`. The closing + distribution is `defined: 40 / ready: 44` over 84 specs; the corpus stays at 0 errors and 0 + warnings. + +### 2026-07-26 — S4 (the readiness maturation sweep) + +1. **Enrichment precedes promotion, or the promotion does not happen.** *Forcing material:* the two + carrier specs that already carried anchored verifiers — `spec:carrier.envelope-contract` and + `spec:carrier.prose-ownership-rule` — were one-line stubs. Each cleared the `ready` floor + structurally and carried `has-verifier`, so both would have promoted silently; neither was + acceptance-grade. *Disposition:* the S3 precedent applied (`executable-contracts` was promoted + only after enrichment). The envelope contract gained the closed-key-set law, the explicit + `relations: {}` carrier rule (the carrier ruling's own carrier note), the derived-name refusal + (delivery facts are derived, never authored), and the owned-grammar bounded-refusal posture (the + envelope-grammar-posture decision's stated consequence); the prose rule gained the + singular-section description owner, the constraints-array exception, and the + graph-content-never-a-file-pointer clause (the prose-ownership decision's stated rationale). + **Every added clause came from ratified intent; no code moved and no claim was widened.** +2. **A `model`-kind parent may own an Example space — ruling 2 exercised on a fourth kind.** + *Forcing material:* the anchor-trust law lives on `spec:model.anchors`, whose kind evidence is + `model.terms`, not `behavior`. Adding `## Example space` routed into `sections.behavior.exampleSpace` + beside an untouched `model` section exactly as for the `rule` and `behavior` parents, and the + floor read the terms as before. *Disposition:* no grammar change and no watch item fired; the + section/kind duality carried it, as ruling 2 predicted. +3. **Two laws were converted, and both were chosen for law-shape rather than for cheapness.** + `spec:model.anchors` — builder trust is physical module identity — took two sibling points + (`lookalike-refusal`, `physical-identity`); `spec:validation.two-check-families` took one + (`split-report`), because the family split is one law and one probe graph exhibits both halves + at once. `test/anchor-trust.test.ts` and `test/validators.test.ts` survive whole as regression + evidence (ruling 1). The bound points joined the existing per-family suites per S3's ruling 1; + the wrapper's per-tree `testPaths` row and the pinned missing-tree stderr are untouched. +4. **The second-tier model candidates were refused on ruling 1, not on budget.** + `spec:model.core-model` and `spec:model.spec-sections` were examined against + `test/descriptors.test.ts`: every assertion there is a literal-list equality over the ratified + enums, which is an assertion, not a law with an example space. Converting it would have been + row-for-row expansion wearing a contract, so both stay `defined` with their vocabularies intact. + The same reading kept `spec:model.relations`, `spec:model.pack-aggregate`, + `spec:model.protocol-domain`, and `spec:extraction.claim-taxonomy` at `defined`: each is a + vocabulary whose clauses are already exercised through a *different* spec's ready points. +5. **The epic stays `defined`, deliberately.** `spec:protocol.self-hosting` has no resolving + verifier and no honestly cheap one — its two rules are whole-pipeline claims whose worlds + (the full build chain, the clean-clone rebuild) are named out of scope for this tranche. Its + children now carry heavy verification, but `has-verifier` is direct and never propagates up + `refines` (binding, never liveness), so promoting the epic would have introduced exactly the + `honesty/gaps` warning ruling 5 forbids. Recorded as an honest `defined`, not an oversight. +6. **Decisions get no verifiers, and none was invented.** All 21 `decision`-kind specs stay + `defined`: a decision's truth is a ratified choice, not a runtime behavior, so no resolving + verifier can exist for it and any promotion would be a `honesty/gaps` warning by construction. + `spec:consumers.edit-model` stays `defined` on its own stated terms — it says in its own rules + that it has no realizing entrypoint and no verifier, which is the honest shape, not a gap. +7. **Frozen-fixture bookkeeping moved with the enrichment.** Two of the five self-hosting carrier + fixtures are byte-identical copies of live carriers (`test/markdown-reifier.test.ts` pins that), + and `test/extract.test.ts` asserts their reified sections. Both were refreshed in the same + change, so the carrier specs' anchored verifiers keep verifying the text that is actually + authored. The closing distribution is `defined: 36 / ready: 51` over 87 specs; the corpus stays + at 0 errors and 0 warnings. + +### 2026-07-26 — S5 (the per-doc concept-dissolution audits) + +1. **An audit-script surface map follows its content to the carrying Spec; it is never blanket + disabled.** *Forcing material:* three standalone audit scripts pin concept-doc text. + `check-carrier-truth.mjs` enumerates `docs/concept/*.md` from disk, so a deleted doc drops out + of the scan by itself — but its CLAIMS entries would fail unreadable and its classification + RULES would fail as stale audit entries, so both sets are retired with the doc. + `check-prose-schema.mjs` and `check-carrier-rule.mjs` instead pin *laws*, and a law that moved + into a Spec is still pinnable. *Disposition:* the narrative-ownership and constraints-omission + pins re-point to `spec:carrier.prose-ownership-rule`; the schema-version pin re-points to + `spec:extraction.schema-versioning` (the version literal was already pinned independently on + `src/graph/schema.ts`, so nothing weakened); the logical/physical relations pin re-points to + `spec:carrier.envelope-contract`. That last one is the only case where a **verbatim-sameness** + assertion across two surfaces became one verbatim surface (JS-A1) plus one surface stating the + same law in its own authored words — recorded here because it is a real, deliberate change in + what that check proves, not a silent map edit. Two surfaces remain pinned; no check lost a leg. + No ADR test is met — the base already forced it — so it stays a plan record. +2. **A doc-level law duplicated into a *surviving* concept doc is expository, not a gap.** *Forcing + material:* `03` §5 duplicates `01`'s git-is-the-event-log principle, and `07` §§1–4 duplicate + `00`'s MVP boundary and cut list. The §4 audit design already anticipated this shape for `03` + §5. *Disposition:* a row whose law is carried by a surviving surface reads `expository-only` and + names that surface; a row whose law is carried **nowhere** reads `gap`, whether or not the + prose is duplicated. Applied uniformly across all five audits. +3. **The phase-2 prediction for `03` §5 was closed before this session, not by it.** The plan's §4 + ranking expected §5 ("git is the event log") to be `03`'s one uncarried section. The audit found + it carried by the `spec:extraction.derive-graph` narrative — current projection, removed means + gone, `supersedes` the one forward pointer — **landed in phase 2 on `main`** and visible on the + regenerated Design Review page. Recorded so the ranking is reconciled rather than quietly + overtaken. (**Corrected at S6, review-09 D-6:** this entry first read "the phase's earlier + corpus work," which in a phase-3 plan reads as phase 3. This branch never edited + `specs/extraction/derive-graph.sdp.md`; the carriage predates it. The headline — closed before + this session, not by it — was and remains accurate.) +4. **`07` is not deletable on supersession grounds.** The phase-2 docket's `SUPERSEDED` row is a + *doc-repair* disposition, not a statement about the document, and the dissolution decision + admits only one basis: the semantic contract is fully carried. Audited on content, `07` carries + five gaps (§4's two open questions, §5's prioritization heuristic, §6 ①'s diagnostic rendering + rule, §6 ③'s banner, §6 ④'s view-label rule), so it stays. Recorded because the plan named it + the likeliest deletion. +5. **No drift found, and no Spec was edited.** Every law the audits traced agreed with its carrying + Spec; no concept-doc statement contradicted a Spec, so no drift finding was raised. Eleven gaps + are recorded in §7 as future corpus work; none was closed by rushing content into a Spec, per + the session's standing constraint. The registry pre-repair (D1/D2 re-pointed to + `spec:model.core-model` and `spec:model.spec-sections`) is the session's only non-sweep edit, + and `check-self-hosting-gates.mjs`'s DECISIONS.md pins were verified before it was committed. + +### S6 (the adversarial review, its remediation, and the close) + +1. **A point is made honest by removing the competing gate from its world, never by asserting + around it.** _Forcing material:_ review-09's P-1 — `concreteness-refusal` stayed green with the + concreteness law deleted, because its probe world ran under a parent owning an example space, + where `resolveExampleVocabulary` withholds the step contract for the same input. Two fixes were + available: add a Then step asserting that generation reports **no** + `contracts/unmatched-vocabulary-step` finding, or change the world so the second gate cannot + fire. The first was rejected: an absence assertion pinned to a finding id goes vacuous the day + that id is renamed, which is the same class of defect one rung down. _Disposition:_ the parent + offers a second Given — a parent that declares no shared vocabulary — and the point uses it. The + vocabulary gate is then structurally absent (the resolver returns no issues when no `refines` + parent owns a space), so the withheld contract can only be the concreteness law's doing. + Re-probed both ways: deleting the unbound-slot refusal turns that point red and no other test in + the suite; disabling the vocabulary gate leaves it green, which is honest, because the point does + not name that law. No ADR test is met — the base already forced it — so it stays a plan record. +2. **The concreteness law's scope was stated, because the honest world depends on it.** The point + now runs under a parent with no vocabulary, so `spec:extraction.executable-contracts` says in its + own words that the concreteness law reads the example's own form alone and refuses with or + without a parent vocabulary, and that vocabulary resolution is a separate, later gate whose + withholding names its own finding. This is a scope clarification of a ratified law rather than a + new law — but it was written down deliberately, because a reader of the point should not have to + infer from code why the parent has no space. +3. **A tautological Then step is coverage that does not exist, so it was removed rather than + ratified.** _Forcing material:_ review-09's P-2 — the schema-version point's second Then compared + the serialized payload against the engine's own exported constant, so both sides moved together + under every mutation. _Disposition:_ the step is gone from the parent vocabulary, the point, the + binding, and the oracle; the authored `"0.4.0"` literal remains and still dies when the constant + is bumped (re-probed). Removing a vocabulary step is not weakening coverage when the step could + not fail on its own — ruling 1's "conversion must never weaken coverage" is about *assertions + that can fail*, and this one could not. +4. **Two audit rows were made true by enrichment, not by retraction.** _Forcing material:_ + review-09's D-2 and D-3 — both rows carried a deletion-authorizing `carried` verdict while + resting on a surviving concept doc plus code (`05` §3 + `readiness-floor.ts`) or on a file that + does not hold what the row claimed (`src/ids.ts` has no namespace *set*). Retracting the + verdicts would have unpicked two clean deletions over a citation defect; widening the + dissolution criterion by plan ruling — which S5 had effectively done — substitutes a weaker test + than the decision states. _Disposition:_ the missing laws were authored onto their honest + carriers, in their own words and from intended truth: the `ready` floor's three clauses and the + vacuous-anchor reading onto `spec:validation.readiness-floor`, the one-oracle-per-space clause + onto `spec:validation.verification-linkage`, and the per-binding-direction reserved namespace + set plus the `doc:` reservation onto `spec:model.stable-ids`. The two deletions now stand on the + dissolution decision's own terms. What could **not** be honestly carried was not forced: the + three lower floor rungs and the per-kind evidence table remain uncarried and stay recorded as a + narrowed gap 1. +5. **`doc:`'s status was stated as it is, not as the deleted doc described it.** `src/ids.ts` mints + no `doc:` identifier, `ref()` refuses one, and the grammar admits any lowercase namespace. The + new clause therefore says the reserved set is the *builders'* law rather than the parser's, and + that the `doc:` reservation is a named deferral rather than a landed namespace — with in-system + decisions living under the `spec:decisions.*` convention, which until now was de-facto corpus + practice stated by nothing. +6. **Ruling 7 was enforced where it binds hardest — on a Spec — and carried where it does not + bind.** _Forcing material:_ review-09's R-1 found the temporal guard blind to `plan N`, `phase + N`, and bare wave handles, with seven pre-existing non-exempt sites and **one Spec** + (`spec:decisions.concept-docs-dissolve`, whose consequence read "never part of phase 1") + carrying such tokens. _Disposition:_ the Spec is repaired in timeless language, because a Spec + carrying a session-relative handle — visibly obsolete, since the deletion it defers has since + happened — is exactly the case ruling 7 exists for. The seven source and registry sites and the + guard's token list are carried on the standing temporal-guard docket row with the choice named: + widen the guard and repair the sites, or narrow ruling 7's wording to what it enforces. Phase 3 + authored none of them, and inventing a guard rule at the close would be exactly the + under-fire legislating this plan warns against. +7. **The clean-clone proof earned its place: it caught a lint exemption the phase never + extended.** _Forcing material:_ the first close-time clone of this branch failed at the + **lint** leg with 27 `no-unsafe-*` errors across the five bound suites S1–S4 added. The cause + is order, not code: `lint` runs before `generate:self-hosting`, so in a room with no + `generated/` the contract imports resolve to nothing and every `bindExample` call reads as an + unsafe call. `eslint.config.js` already carried an exemption for exactly this — scoped by an + explicit two-file list written when only two bound suites existed. Every later wave added + suites without extending it, and no wave could see the omission, because a developed checkout + always has `generated/` present. _Disposition:_ the exemption list now names all six suites + that import `generated/contracts/` — the bound-suite half of `vitest-test.mjs`'s root + contract-dependency row — and says so, so the two lists are read as the pair they are. It is + deliberately still an explicit list rather than a `self-hosting-*` glob: the corpus oracle and + the contracts self-check derive their graphs in memory, need nothing generated, and must keep + full lint strength. This is a genuine gate hole the twelve-leg chain could not have found in + place — precisely what §9 criterion 6 asks the clean-clone leg to prove — and the phase-2 close + was not exposed to it, since both suites it had were listed. + +#### The S4 readiness-sweep ledger + +Every spec that stood at `defined` when S4 opened, with its disposition. "Stays `defined`, +honestly" is a first-class outcome; promotion is per-spec judgment, never a quota. + +| Spec | Readiness | Verifier basis | Disposition | +|---|---|---|---| +| `spec:carrier.envelope-contract` | defined → **ready** | anchored `test:protocol.envelope-contract` | enriched in place first — four clauses from ratified intent; floor clears, no gap | +| `spec:carrier.prose-ownership-rule` | defined → **ready** | anchored `test:protocol.prose-ownership` | enriched in place first — three clauses from the prose-ownership decision; floor clears | +| `spec:model.anchors` | defined → **ready** | enabled examples (both points below) | converted — Example space added; terms gain the untrusted-builder clause and its entrypoints | +| `spec:model.anchors.lookalike-refusal` | **new — ready** | anchored `test:protocol.anchors.lookalike-refusal` | new point — a consumer-local lookalike mints nothing and reports nothing | +| `spec:model.anchors.physical-identity` | **new — ready** | anchored `test:protocol.anchors.physical-identity` | new point — a deep relative import resolving to the builder modules is trusted | +| `spec:validation.two-check-families` | defined → **ready** | enabled example (the point below) | converted — Example space added; two clauses added (aggregate family, entrypoints) | +| `spec:validation.two-check-families.split-report` | **new — ready** | anchored `test:protocol.two-check-families.split-report` | new point — one report, both families, neither claimed as its own | +| `spec:protocol.self-hosting` | defined | none | epic — whole-pipeline rules, no cheap verifier; promotion would add a gap warning | +| `spec:carrier.markdown-authoring` | defined | none of its own | parent of four ready children; the executable path lives on them, not on it | +| `spec:consumers.agent-surface` | defined | none | two of its rules are measured-evidence claims, not runtime laws; reader world not cheap | +| `spec:consumers.reader` | defined | none | verifier world is a full graph fixture — outside this tranche's cheap boundary | +| `spec:consumers.design-review` | defined | none | verifier world is projection rendering — outside this tranche | +| `spec:consumers.edit-model` | defined | none, by its own rule | states in its own words that it has no entrypoint and no verifier — honest `defined` | +| `spec:consumers.projections-model` | defined | none | vocabulary; its measured-curation terms are recorded evidence, not runtime law | +| `spec:extraction.build-pipeline` | defined | none | the ordered flow's world is the CLI pipeline — a named out-of-scope giant | +| `spec:extraction.regenerability` | defined | none | its law is the clean-room rebuild; that world is the close's, not a cheap point | +| `spec:extraction.claim-taxonomy` | defined | none of its own | vocabulary; its clauses are exercised through `spec:validation.claim-separation`'s ready points | +| `spec:model.core-model` | defined | none | second-tier candidate refused on ruling 1 — `test/descriptors.test.ts` is list equality, not a law | +| `spec:model.spec-sections` | defined | none | same reading as `core-model`; the section-name list is an assertion, not an example space | +| `spec:model.relations` | defined | none of its own | vocabulary; relation grammar is exercised by referential-integrity's ready points | +| `spec:model.pack-aggregate` | defined | none of its own | vocabulary; the pack law is carried executably by `spec:validation.pack-coherence` | +| `spec:model.protocol-domain` | defined | none | four-term vocabulary; no runtime law to bind | +| 21 × `spec:decisions.*` | defined | none, by construction | a decision's truth is a ratified choice, not a runtime behavior — no verifier exists and none was invented | + +## (j) §7 Done-record (execution — appended at close) + +Executed delivery, the conversion ledger's terminal states, the readiness sweep ledger, the +per-doc audit tables and deletion dispositions, watch-item terminal states, the docket close, +and close-evidence pointers. + +### Executed delivery + +The phase ran as six sequential sessions on `feature/protocol-self-application-phase-3`, each +closing green. What it delivered, against what it planned: + +- **The executable-specs rewrite is systematic where the verify loop is honestly cheap.** Fifteen + laws were converted to bound points across three tranches — the whole validation family (S1), + the extraction / model / carrier cheap wins (S2), the three uncovered engine areas that first + needed corpus additions (S3), and two more laws the readiness sweep found worth converting on + law-shape rather than cheapness (S4). **27 new example specs**, each an immediate `gwt` point + bound through a generated contract to a `specTest` anchor; **24** of them from the conversion + ledger's thirteen rows plus **3** from S4. The corpus now holds **29** example specs — these 27 + plus the two phase-2 tracers. +- **Three specs were added and three enriched to carry laws no spec held.** + `spec:extraction.example-runner` and `spec:carrier.slot-notation` are new (with the two new + `impl:` anchors that bind them); `spec:extraction.executable-contracts`, + `spec:carrier.envelope-contract`, and `spec:carrier.prose-ownership-rule` were enriched from + one- and two-rule stubs into acceptance-grade specs **before** any promotion. The corpus went + **58 → 87 specs**. +- **Readiness matured only where a verifier landed.** `7 ready / 51 defined` at the draft became + **`51 ready / 36 defined`** at the close, in four steps that each rode their own wave's + verifiers (69: 24/45 → 75: 34/41 → 84: 44/40 → 87: 51/36). Every `ready` spec carries + `has-verifier`; the corpus reports **zero** `honesty/gaps` warnings. +- **Two concept docs dissolved, three stayed on their own audit's evidence.** `02-core-model.md` + and `03-the-one-graph.md` were deleted with full reference sweeps; `05`, `06`, and `07` stay + with their gaps recorded rather than papered over. +- **Nothing weakened.** No test file was deleted, no residual vitest suite lost a case, and the + golden oracle grew by transcription only — no assertion removed, no exact matcher loosened. + +**Ledger terminal states.** + +- **§2 conversion ledger:** all thirteen rows **done**; every delivered point count sits inside + its planned range. No row was deferred or dropped. +- **§6 S4 readiness-sweep ledger:** terminal as recorded — 40 `defined` specs swept, 4 promoted + (2 enriched in place, 2 converted) plus 3 new points at `ready`, and 36 recorded as honest + `defined` with a per-spec reason. S6 promoted nothing: its enrichments landed laws on specs + already `ready`, so the readiness distribution is unchanged from S4's close. +- **§4 / §7 audit ledger:** five audits recorded, terminal — `02` **deleted** · `03` **deleted** · + `05` **stays, gaps recorded** · `06` **stays, gaps recorded** · `07` **stays, gaps recorded**. + Four audit-row citations were corrected at S6 and one row re-verdicted (see the disposition + table in `reviews/09-self-hosting-phase-3-pre-close-review.md`). + +### Recorded gaps carried out of the phase + +The eleven gaps S5 recorded stand, with two amendments made at S6: **gap 1 narrowed** (the `ready` +rung's clauses now stand on `spec:validation.readiness-floor`; the three lower rungs and the +per-kind evidence table remain uncarried) and **gap 12 added** (the Mermaid and reference-projection +surfaces of `06` §1, conceded inside a `carried` row at S5 and re-verdicted at S6). Two further +contracts entered no gap list because S5 graded them `carried` in error — the `03` §1 edge +contract's readiness-effect and `models`-uniqueness clauses, and the `02` §5 namespace set with the +`doc:` reservation. Both were **closed at S6 by enrichment**, so they neither survive as +mis-gradings nor join the list. The full list is under "Gaps carried out of S5" below, as amended. + +### Adversarial review and remediation + +An adversarial review over the full branch diff is archived at +`reviews/09-self-hosting-phase-3-pre-close-review.md`. Its central method was **mutation testing**, +not code reading: a sandbox copy of the branch, its engine broken one law at a time across 37 +mutations, with the new bound points re-run after each. Verdict: sound to close once the four +majors were dispositioned; **26 of 27** points were provably mutation-sensitive to the law they +name. + +All fifteen findings carry an explicit disposition in that document's table. Ten were fixed, one +record was corrected, one was accepted as stated, and three are carried with stated reasons (an +immutable commit subject, the oracle-granularity docket row, and the temporal guard's token list). +The four majors: **P-1** the concreteness point made discriminating and re-probed; **D-1** the last +bare-form `02` citation repaired with the sweep re-run over both citation forms; **D-2** and **D-3** +closed by enriching the honest carrying specs so the two deletions rest on executable Specs, as the +dissolution decision requires, rather than on a plan ruling that widened the criterion. + +### The docket close + +| Docket row | Disposition at close | +|---|---| +| Markdown Pack syntax ruling | **carried** — the watch item never fired; the pack manifest absorbed 29 new members as TS with no friction, so no caller has yet needed Markdown Pack authoring | +| the gen-1 `.feature` adapter | **carried** — untouched this phase; the deferred tail was never forced by a wave | +| the no-reparse read seam | **carried** — untouched; `spec:extraction.regenerability` still carries the law and no consumer re-parses source today | +| temporal-guard token assembly | **carried; the ruling-vs-guard mismatch decided at the post-close records sweep** — review-09 R-1 measured the guard's blindness (`plan N`, `phase N`, month names, bare wave handles) and found the pre-existing non-exempt sites. The one **Spec** instance was repaired at S6. The standing call is now made: **§1 ruling 7 is narrowed to the guard's enforced token list**, and guard-widening is declined with a reason — the surviving sites are prose provenance comments in source that point into the process record (the content-routing rubric bans execution context from *specs*, never from code comments), and widening `plan N`-style tokens would trade real provenance pointers for false-positive churn. The *token-assembly* residue itself (split-string workarounds in audit scripts) still carries forward unchanged | +| the editor-association gap | **carried** — named out of scope at the draft and never forced | +| corpus-test granularity (review-08 R-28) | **carried, with a stated reason and a measurement** — review-09 R-3 is right that every wave reached this row and none dispositioned it, and that the condition worsened: `test/self-hosting-graph.test.ts` grew from 2,287 to **3,888** lines and is still a single `it()` with frozen absolute counts, so the first failure masks the rest. It is carried rather than closed because splitting the oracle is a single-purpose session over the one artifact every wave's correctness claim rested on; churning it mid-phase would have put the phase's own evidence under the knife. The row rolls forward **with this reason recorded**, which is the thing S5 failed to do | +| control-character latitude | **carried** — recorded-only at review-08 and untouched since | +| the separate example id namespace | **carried** — 27 example specs were authored this phase under the existing convention (29 now stand in the corpus) with no collision and no pressure for a separate namespace; the row survives as a watch, not a want | + +### Close evidence + +- **Graph:** `87 specs · 1 pack · 65 anchors → 153 nodes · 294 edges`, **0 errors · 0 warnings**, + regenerated from a clean tree. Node mix: 87 `Primitive`, 1 `Pack`, 35 `Anchor`, 30 `CodeNode`. +- **Readiness histogram:** `ready: 51 · defined: 36` over 87 specs (`idea: 0 · scoped: 0`). All 51 + `ready` specs carry `has-verifier`; so do all 29 example points and all 15 converted parents. +- **Kind mix:** `example: 29 · decision: 21 · rule: 14 · behavior: 12 · model: 8 · contract: 1 · + workflow: 1 · constraint: 1`. +- **Tests:** **559 passing** across 41 files under the wrapper's two legs (505 in the parallel pool, + 54 in the dedicated CLI process); zero skipped; no residual suite case was deleted this phase — + one non-discriminating bound `Then` step was removed at S6 (review-09 P-2). +- **Tip inventory:** the §10 ledger names each session's green-gate commits — a subset, never the + branch inventory. The authoritative series is `git log main..HEAD`: **21 commits** at the close + proof, with the post-close records amendment below landing on top. +- **Gate:** the full twelve-leg `npm run check` green in the working checkout, and green again in + a **clean clone** — `git clone --no-local` of this repository into a scratch root outside it, + `npm ci`, then the whole chain — at the close commit `268d1cb`. The clone reproduced the same + numbers (87 specs · 1 pack · 65 anchors → 153 nodes · 294 edges, 0 errors · 0 warnings; the + example corpus at 11 specs with its one recorded `invalid-cart` warning; 505 + 54 tests; a clean + `preflight` and an empty `git status`), which is the determinism claim proved rather than + asserted. The **first** clone run failed at the lint leg and found a real gate hole — the + clean-room contract exemption had never been extended past the two bound suites that existed + when it was written (S6 ruling 7). It was fixed, re-committed, and re-cloned; only the record of + the result follows the proven commit. +- **Review:** `reviews/09-self-hosting-phase-3-pre-close-review.md`, with its disposition table + filled and a remediation addendum. + +### The per-doc concept-dissolution audits (S5) + +Five audits, one table per candidate doc, built to the §4 template 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). The governing criterion is 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 — never a bulk purge. A `gap` is a +stated law or contract no Spec, registry, or code+test surface carries, and one gap blocks +deletion. `expository-only` is restatement, motivation, or worked narrative whose law is carried +elsewhere; where the elsewhere is a *surviving* concept doc, the row names it. + +**Terminal dispositions:** `02` **deleted** · `03` **deleted** · `05` **stays — gaps recorded** · +`06` **stays — gaps recorded** · `07` **stays — gaps recorded**. + +#### `02-core-model.md` — verdict: fully carried · **deleted** + +| Doc section | Carrying surface (spec / registry / code) | Verdict | +|---|---|---| +| §1 the `Spec` primitive — one authored truth-primitive, enrich in place, envelope minimal and stable, sections optional | `spec:model.core-model` (Spec · envelope) · `spec:decisions.one-primitive` · `src/model/spec.ts` | carried | +| §1 carrier note — logical relations optional, physical Markdown writes `relations: {}` | `spec:carrier.envelope-contract` (the explicit-`relations: {}` clause) | carried | +| §1 the two other authored things (`Pack`, `anchor`) | `spec:model.pack-aggregate` · `spec:model.anchors` | carried | +| §1 the TS `Spec` type block | `src/model/spec.ts` | expository-only (representation of the typed shape) | +| §2 the three descriptors and their value sets | `spec:model.core-model` (kind · altitude · readiness) · `CONTEXT.md` "Descriptor values locked" · `src/model/descriptors.ts` + `test/descriptors.test.ts` | carried | +| §2 "why the enums look the way they do" (altitude is a clean ladder · no `quality` alias · capability/domain are projections · NFR and Scenario are labels) | `spec:decisions.one-primitive` (named coordinates; domains and capabilities are projections or Packs) · `CONTEXT.md` "named coordinates" table | carried | +| §2 readiness vs delivery facts; the `implemented`/`has-verifier`/`observed` table and its does-**not**-mean column | `spec:model.core-model` (delivery fact) · `CONTEXT.md` "Delivery facts" table · `spec:extraction.derive-graph` (facts computed from resolving edges) · `spec:validation.authored-honesty` | carried | +| §2 the payoff queries (`ready ∧ ¬implemented`, `implemented ∧ ¬ready`) | `CONTEXT.md` "The payoff queries" | carried | +| §2 the liveness ladder (binding, never liveness; skip/quarantine is CI's) | `spec:decisions.binding-not-liveness` · `CONTEXT.md` "enabled verifier" | carried | +| §3 the section list and each section's concern | `spec:model.spec-sections` (section) · `CONTEXT.md` "Sections" · `src/model/sections.ts` (the typed shapes) | carried | +| §3 prose ownership — `narrative` on the Spec, `description` on the seven singular sections, `constraints` has no owner | `spec:carrier.prose-ownership-rule` · `spec:decisions.prose-ownership` | carried | +| §3 the `decision` section carries no `status` field | `CONTEXT.md` term ledger, Rejected (`status` — adoption arc is `readiness`, replacement is `supersedes`) · `spec:decisions.typing-law` | carried | +| §3 the typing law (floor-bearing sections are closed typed shapes; the criterion outlives the list) | `spec:decisions.typing-law` · `spec:model.spec-sections` (typing law term) | carried | +| §3 section ⟷ kind duality; sections carry content, relations carry linkage; promotion is exclusive | `spec:decisions.content-only-sections` · `spec:model.spec-sections` (content-only section · promotion) · `CONTEXT.md` "section ⟷ kind duality" | carried | +| §3 verifier semantics — direct, per-spec, never transitive; the enabled verifier is a resolving test binding | `spec:model.spec-sections` (`verifies` · enabled verifier) · `spec:extraction.derive-graph` (has-verifier only to its target) · `spec:validation.verification-linkage` + its two bound points | carried | +| §3 the worked enrich-in-place examples | — | expository-only (worked narrative; the law is the rows above) | +| §4 `Pack` — aggregate, states no truth, plain `framing`, many packs per spec, `belongsTo` derived, coherence not completeness | `spec:model.pack-aggregate` · `spec:decisions.pack-reified` · `spec:validation.pack-coherence` + its bound point | carried | +| §4 refinement vs the aggregate kept distinct | `spec:model.pack-aggregate` (refinement term) · `CONTEXT.md` "Two grouping mechanisms" | carried | +| §5 stable IDs — grammar, stability, uniqueness, string linkage, no history | `spec:model.stable-ids` (ready; `namespaced-round-trip` and `malformed-refusal` points) · `src/ids.ts` | carried | +| §5 the MVP namespace set and the `doc:` reservation | `spec:model.stable-ids` (**since S6**: the per-binding-direction reserved set — `spec:` · `pack:` · `impl:` · `api:` · `component:` · `test:` · `oracle:` — and the `doc:` reservation with its actual status) · `src/ids.ts` (`CODE_ANCHOR_NAMESPACES` and the per-builder singletons) · `CONTEXT.md` flagged ambiguities (`ref()` is spec-only; `doc:` deferral) | carried — **citation corrected at S6 (review-09 D-3)**: `src/ids.ts` holds no namespace *set* (one three-member code-anchor subset, four per-builder singletons, and a grammar admitting any lowercase namespace), and nothing carried the positive half of the `doc:` reservation or the `spec:decisions.*` convention. Both now stand on the Spec. | +| §6 the six authored relations, directions and meanings | `spec:model.relations` · `CONTEXT.md` "Relations" table (with the UML anchors) | carried | +| §6 `supersedes` only on decision specs; the kind-typed endpoints | `spec:model.relations` ("A current Decision Record points forward to the decision it replaces") · `CONTEXT.md` "Relations" table · `spec:extraction.derive-graph` narrative · `spec:validation.claim-separation` (the generic endpoint-contract rule) · `src/validate/validators.ts` | carried — **citation corrected at S6 (review-09 D-4)**: the contract was always carried, but the row named only the generic endpoint sentence and the validator source; the surfaces that actually state the kind-typed endpoints are now named first. | +| §6 the `doc:`-target named deferral (MD-16) | `spec:decisions.carried-evidence` ("external `doc:` targets remain deferred") · `CONTEXT.md` flagged ambiguities | carried | +| §6 derived edges never authored (`satisfies`, `belongsTo`); every authored relation is `declared` | `spec:extraction.derive-graph` · `spec:extraction.claim-taxonomy` (claim inheritance) · `CONTEXT.md` "Derived, never authored" | carried | + +#### `03-the-one-graph.md` — verdict: fully carried · **deleted** + +| Doc section | Carrying surface (spec / registry / code) | Verdict | +|---|---|---| +| §1 two pure steps; the extractor is the only component that reads source | `spec:extraction.derive-graph` (one derivation seam, no second graph) · `spec:extraction.build-pipeline` | carried | +| §1 what the extractor reads (declared carriers · anchors · the designed-in, MVP-empty inferred layer) | `spec:extraction.derive-graph` · `spec:model.anchors` · `spec:extraction.claim-taxonomy` (inferred advisory) · `spec:extraction.excludes` (discovery surfaces) | carried | +| §1 the graph is flat; hierarchy is edges, never nested nodes | `spec:extraction.derive-graph` (flat typed arrays) | carried | +| §1 the `graph.json` payload sample and the `nodeType`/`specKind` split | `src/graph/schema.ts` · `CONTEXT.md` "`nodeType` / `specKind`" | carried (sample itself expository) | +| §1 schema `0.4.0` prose fields and fixed serializer key order | `spec:carrier.prose-ownership-rule` (owners) · `src/graph/schema.ts` · `spec:extraction.determinism` | carried | +| §1 the edge contract — one row per edge type (source, claim, authoring surface, severity, readiness effect, delivery-fact effect) | `spec:extraction.derive-graph` (endpoints, anchor-derived edges, computed facts) · `spec:validation.claim-separation` (claims + endpoint contracts) · `spec:validation.referential-integrity` (missing target is an error) · `spec:validation.verification-linkage` (the enabled-verifier condition **and, since S6, the one-oracle-per-space clause**) · `spec:validation.pack-coherence` (`belongsTo`) · `spec:validation.readiness-floor` (**since S6**, the three `ready`-floor clauses in its own authored words — the readiness-effect column) | carried — **citation corrected at S6 (review-09 D-2)**: the row first cited `spec:validation.readiness-floor` **+ `05` §3**, and the floor Spec was then a single clause-free sentence, so the readiness-effect column rested on a surviving concept doc plus `readiness-floor.ts` rather than on a Spec. The two clauses now stand on the Spec, and the `models` uniqueness clause — previously only a validator message — stands on `spec:validation.verification-linkage`. | +| §1 delivery facts are node facts, not edges | `spec:extraction.derive-graph` ("computed node facts") · `spec:extraction.claim-taxonomy` | carried | +| §2 determinism — byte-identical output, sort order, no wall-clock or run hashes, `--check-clean` | `spec:extraction.determinism` (all three rules) · `spec:extraction.regenerability` | carried | +| §2 the static-extractability consequence and the two tiers, with the per-carrier asymmetry | `spec:extraction.determinism` (rule 3) · `spec:carrier.markdown-parser` (whole-document refusal) · `04` §1 (surviving) | carried | +| §3 the `claim` table — declared / anchored / inferred | `spec:extraction.claim-taxonomy` (all three terms verbatim in law) | carried | +| §3 inference is never authoritative; no 4th claim; computed-from-authored inherits its source's claim | `spec:extraction.claim-taxonomy` (claim inheritance) · `spec:validation.claim-separation` + its two bound points | carried | +| §3 ambiguity is loud | `spec:validation.duplicate-ids` + its bound point · `spec:validation.referential-integrity` | carried | +| §3 delivery facts derived, never declared | `spec:validation.authored-honesty` + its two bound points · `spec:extraction.claim-taxonomy` | carried | +| §3 the doc-generator contrast blockquote | — | expository-only (motivation) | +| §4 regenerability, no consumer re-parses source, single JSON, graph DB deferred | `spec:extraction.regenerability` (clause for clause, including both measured-evidence lines) | carried | +| §5 git is the event log — current state only, history is a git operation, removed means gone, `supersedes` the one kept forward pointer | `spec:extraction.derive-graph` narrative (renders in the Design Review) · `spec:model.stable-ids` (IDs carry no history) · `01` founding principle 5 (surviving) · `CONTEXT.md` "git is the event log" | carried — the phase-2 prediction that §5 lacked a carrier was already closed by the narrative enrichment that landed in phase 2 on `main` (corrected at S6, review-09 D-6) | +| §5 "a graph diff is just two projections" | `spec:extraction.determinism` + `spec:extraction.regenerability` (the diff is their consequence) | expository-only (consequence; the aspirational impact UI is named in `00`/`07`) | +| §6 minimal schema versioning | `spec:extraction.schema-versioning` (ready; `declared-version` point) · `src/graph/schema.ts` | carried | + +#### `05-validation-and-honesty.md` — verdict: **gaps** · stays + +| Doc section | Carrying surface (spec / registry / code) | Verdict | +|---|---|---| +| §1 the two check families; errors fail the build, gaps inform; checked, never workflow-gated | `spec:validation.two-check-families` + its `split-report` point | carried | +| §1 the layered-enforcement table (types · schema · graph validators; architecture and custom rules aspirational) | `spec:validation.two-check-families` (the layering rule) · `spec:decisions.typing-law` · aspirational rows carried by §6 below | carried | +| §2 the one-validation-path paragraph (no pre-graph seam; per-carrier authoring feedback) | `spec:decisions.one-validation-path` · `spec:validation.two-check-families` | 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) | `spec:validation.claim-separation` + `collapsed-edge-claim` and `unratified-descriptor` | carried | +| §2 check 4 (`verifies` linkage) | `spec:validation.verification-linkage` + `unbound-example` and `unresolved-oracle` | carried | +| §2 checks 5–6 (authoring-shape and derived-facts honesty) | `spec:validation.authored-honesty` + `section-authored-fact` and `unearned-stated-fact` | carried | +| §2 check 7 (honest readiness) | `spec:validation.readiness-floor` | carried | +| §2 checks 8–9 (orphans · readiness/delivery gaps) | `spec:validation.warn-level-signals` + `orphan-signal` and `ready-gap-signal` | carried | +| §2 partial failure stays local | `spec:carrier.markdown-parser` (excludes one malformed carrier, continues healthy siblings) · `spec:extraction.determinism` | carried | +| §3 the kind-blind clause table (`idea`/`scoped`/`defined`/`ready` rungs) | the `ready` rung: `spec:validation.readiness-floor` (**since S6** — its three clauses in authored words) · the `idea`/`scoped`/`defined` rungs: none — their clauses live in `src/validate/readiness-floor.ts`, which the registry rows MD-13 and MD-9 cite **together with `05` §3** as the mirror pair | **gap** (narrowed at S6 to the three lower rungs) | +| §3 the per-kind evidence table (7 kinds × `scoped`/`defined`) | same as above — `spec:decisions.kind-conditional-floor` states *that* one clause is kind-conditional, never *which* evidence each kind names | **gap** | +| §3 the three bounding laws (monotonic · promotion-neutral · convergence is honest) | `spec:decisions.kind-conditional-floor` (consequence line) · `spec:decisions.carried-evidence` (the promoted-evidence honesty bound) | carried | +| §3 the MD-13 representation note (floor table as its own code-level source of truth) | `docs/concept/DECISIONS.md` MD-13 · `src/validate/readiness-floor.ts` | carried (registry row still cites `05` §3 as one of two mirrors) | +| §3 `ready` is earned, not asserted, and is not a delivery fact | `spec:validation.readiness-floor` · `spec:decisions.binding-not-liveness` | carried | +| §3 `ready` is the floor plus a human's declared statement; no review fact is stored | `spec:consumers.design-review` (rule 3) · `spec:decisions.binding-not-liveness` | carried | +| §3 stated vs derived readiness — the banner fires only in the dishonest direction and names the first unmet clause | `CONTEXT.md` "derived readiness" carries *rendered beside, never overwriting*; nothing carries the one-direction rule or the first-unmet-clause rule | **gap** (shared with `06` §5 and `07` §6 ③) | +| §4 pack coherence, not member completeness; no duplicated-intent check | `spec:validation.pack-coherence` + its point · `spec:model.pack-aggregate` · `spec:validation.two-check-families` (never judges content quality) | carried | +| §5 validator self-testing (should-fail and should-pass fixtures per validator) | none — a stated practice with no Spec, registry, or pinned code surface | **gap** | +| §6 aspirational tiers (architecture enforcement · custom team rules · NFR-to-`observed` · `--lenient` · incremental builds) | `00` §4 and `07` §2/§3 (surviving) name all five; the cache bound is `spec:extraction.regenerability` | expository-only against `00`/`07` | +| §7 what CI guarantees at MVP | §2 rows above · `spec:extraction.determinism` | expository-only (summary) | + +#### `06-consumers-and-projections.md` — verdict: **gaps** · stays + +| Doc section | Carrying surface (spec / registry / code) | Verdict | +|---|---|---| +| §1 projections fan out from one graph; nothing is a second source | `spec:consumers.projections-model` (projection) | carried | +| §1 the surfaces taxonomy table | `spec:consumers.projections-model` · `CONTEXT.md` "Surfaces & projections" | **gap** (partial) — the taxonomy is carried except the Mermaid and reference-projection rows, which only `06` §8 names. **Re-verdicted at S6 (review-09 D-7)**: a row conceding an uncarried surface may not read `carried`; `06` stays either way, and the conceded surface now enters the gap list as item 12. | +| §2 curated graph vs impact graph; divergence is curation, not drift; never densify from imports | `spec:consumers.projections-model` (curated graph · impact graph · curation · measured curation) · `spec:extraction.claim-taxonomy` | carried | +| §2 the impact graph's two assist roles (propose candidates · flag unambiguous drift) | none | **gap** | +| §2 the MVP boundary blockquote (file-level impact rides the curated graph; `coverage-unknown` is explicit) | `spec:consumers.reader` (rules 3–4) | carried | +| §3 the agent surface — a visible typed graph, the schema is the contract, no verb wall, neither failure mode | `spec:consumers.agent-surface` · `spec:decisions.agent-surface-scripts-graph` | carried | +| §3 the reader — joins and claim decode once, persists nothing; entry adapters; blast radius; irreducible joins | `spec:consumers.reader` (all four rules) · `spec:consumers.agent-surface` (rules 2–3) | carried | +| §3 the second-caller bar (freeze a typed contract only when a second machine consumer appears) | `00` §4 and `07` §1/§3 (surviving) | expository-only against `00`/`07` | +| §3 `bySymbol` is frozen in shape but aspirational in build | none — `spec:consumers.reader` names `findByConcept` and `byFile` only | **gap** | +| §3 measured context efficiency | `spec:consumers.agent-surface` (rules 4–5, measured evidence) | carried | +| §4 the edit model — intent composition, no patch loop, lifecycle ops are git + edit | `spec:consumers.edit-model` (all four rules) | carried | +| §5 Design Review renders a Spec/Pack in context; pure projection with no stored `Finding`; the human's call, never a gate; deterministic generated Markdown | `spec:consumers.design-review` (rules 1–5) | carried | +| §5 the per-spec field list — the stated-vs-derived readiness divergence banner | none (see `05` §3 and `07` §6 ③) | **gap** | +| §5 pages rewritten wholesale each run so no stale page survives | none — `spec:consumers.design-review` rule 4 names the index-and-pages shape and determinism, not the wholesale rewrite | **gap** | +| §5 owned prose reaches Design Review through the graph and Reader with no source reparse | `spec:carrier.prose-ownership-rule` · `spec:consumers.design-review` · pinned by `check-prose-schema.mjs` | carried | +| §6 delivery-process nouns as projections and vocabulary, never gates | `spec:consumers.projections-model` (discipline · release · baseline · phase/iteration/milestone) · `CONTEXT.md` "Delivery-process vocabulary" | carried | +| §6 the discipline ≈ kind/section mapping (Requirements → `behavior`, Test → `example` + `verifies`, …) | none | **gap** | +| §6 the disciplines × phases × iterations distribution chart as an analytical projection | none | **gap** | +| §7 the MCP surface — designed-in, deferred, distinct from the agent surface | `spec:decisions.mcp-deferred` · `CONTEXT.md` "MCP surface" | carried | +| §8 aspirational projections (Spec Studio · contract/model exports · AI slices · per-PR hosted preview) | `00` §4 and `07` §2 carry the first three; nothing carries the per-PR hosted preview | **gap** (per-PR preview only) | +| §9 interop posture — the membrane, not a replacement | `00` §2/§5 (surviving) | expository-only against `00` | +| §10 what the MVP consumer story proves | §§1–5 rows above | expository-only (summary) | + +#### `07-mvp-roadmap-and-open-questions.md` — verdict: **gaps** · stays + +The phase-2 docket recorded `07`'s *doc-repair* row as SUPERSEDED; that disposition is about the +repair bill, not about the document, and it is not a deletion basis under the dissolution +decision. The audit therefore ran on content, and found gaps. + +| Doc section | Carrying surface (spec / registry / code) | Verdict | +|---|---|---| +| §1 the MVP vertical slice, the slice table 0–5, the package line | `AGENTS.md` "The build path" (the same slice table) · `00` §3 | expository-only against `AGENTS.md`/`00` | +| §1 what "done" looks like and the North Star | `00` §3 ("Done" for the MVP) · `spec:protocol.self-hosting` · `spec:extraction.build-pipeline` | expository-only against `00` | +| §2 the CORE vs ASPIRATIONAL map, with the carrier addendum | `00` §3/§4 · `spec:decisions.carrier-ruling` | expository-only against `00` | +| §3 the cut list, nine items with rationale | `00` §4 (the same nine, condensed, with per-item rationale) | expository-only against `00` | +| §4 derived-readiness banner timing (recorded as resolved) | `05` §3 (surviving) — but see the banner gap recorded there | expository-only against `05` | +| §4 impact-graph depth (recorded as resolved) | `06` §2 (surviving) · `spec:consumers.reader` | expository-only against `06` | +| §4 inline-vs-centralized anchor semantics (open) | none — `spec:model.anchors` states the landed binding contract and the unextracted forms, not the open question about further structural semantics | **gap** | +| §4 graph-DB timing (open, measured) | `spec:extraction.regenerability` (both measured-evidence lines) | carried | +| §4 trace-link recovery — assistive suggestion only, never a declared edge | `01` P10 (surviving) · `spec:extraction.claim-taxonomy` (inferred is advisory, never authoritative) | expository-only against `01` | +| §4 when (if ever) harnesses / evidence become CORE (open) | the Gherkin half is `spec:decisions.carrier-ruling`; the harness/evidence half has no carrier | **gap** | +| §5 measure what actually hurts (the four pain-to-next-slice mappings) | none — a prioritization heuristic with no Spec or registry home | **gap** | +| §6 ① authoring ergonomics — typed sections | `spec:decisions.typing-law` | carried | +| §6 ① the one diagnostic rendering rule (location from the finding's structured fields; first contact fails clean) | none | **gap** | +| §6 ② the golden correctness oracle kept distinct from the determinism self-checks | `spec:extraction.determinism` (the self-checks) · `test/fixtures/checkout-v1/expected-graph.json` + `expected-design-review/` (the oracle, a code+test surface) | carried | +| §6 ③ the derived-readiness banner ships in the Design Review | none (the same gap as `05` §3 and `06` §5) | **gap** | +| §6 ④ `implemented` is a UI hazard — the fact name stays, views render binding language | `spec:decisions.binding-not-liveness` carries the do-not-rename half; nothing carries the view-label rule the Design Review actually implements | **gap** | +| §6 ⑤ `coverage-unknown` is acceptance, never a design note | `spec:consumers.reader` (rule 3) | carried | +| §6 the MVP acceptance checklist and its three pinned sharpenings | the three sharpenings are pinned test surfaces (the blocking-open-question should-fail fixture · the drops-no-sections extraction test · the `api:` anchor assertion) | carried | + +#### Gaps carried out of S5 (future corpus work, not this session's) + +Recorded so they are not lost; each is a stated law or open question with no carrying Spec. +None was rushed into a Spec — this session authored no Spec edits. + +1. The readiness-floor **clause tables** (kind-blind rungs and the per-kind evidence table) have + no carrying Spec; the clauses live only in `src/validate/readiness-floor.ts` and `05` §3. + **Narrowed at S6 (review-09 D-2):** the `ready` rung's three clauses now stand on + `spec:validation.readiness-floor` in its own authored words. The `idea` / `scoped` / `defined` + rungs and the per-kind evidence table remain uncarried — that residue is the standing gap. +2. The **derived-readiness banner** rule — rendered beside stated, fires only in the dishonest + direction, names the first unmet clause — is stated in `05` §3, `06` §5, and `07` §6 ③ and in + no Spec. +3. The **`implemented` view-label rule** (the fact name stays; views render binding language) is + stated only in `07` §6 ④, while the Design Review implements it. +4. The **one diagnostic rendering rule** (location from the finding's structured fields; first + contact fails clean) is stated only in `07` §6 ①. +5. **Validator self-testing** (should-fail and should-pass fixtures per validator) is stated only + in `05` §5. +6. Design Review's **wholesale page rewrite** (no stale page survives a run) is stated only in + `06` §5. +7. The **discipline ≈ kind/section mapping** and the disciplines × phases × iterations + distribution projection are stated only in `06` §6. +8. The impact graph's two **assist roles** (propose candidates · flag unambiguous drift) and + `bySymbol`'s frozen-shape-but-aspirational status are stated only in `06` §2/§3. +9. The **per-PR hosted preview** deferral is stated only in `06` §8. +10. Two open questions have no Spec home: **inline-vs-centralized anchor semantics** and **when + harnesses / evidence become CORE** (`07` §4). +11. The **measure-what-hurts** prioritization heuristic (`07` §5) has no Spec or registry home. +12. The **Mermaid and reference-projection surfaces** of `06` §1's taxonomy table are named only + by `06` §8. **Added at S6 (review-09 D-7):** S5 conceded them inside a `carried` row instead + of recording them, so the row is re-verdicted and the surface enters the list here. + +#### Reference-sweep inventory (per deletion) + +`03` — `CONTEXT.md` (2 glossary pointers) · `AGENTS.md` (the where-to-look row) · +`docs/concept/README.md` (index row + reading path) · `01` · `05` (2) · `06` · `07` (3) · +`jtbd-stories/03-one-graph.md` (4) · `jtbd-stories/07-trace-and-impact.md` (5) · +`examples/checkout-v1/README.md` · `src/graph/schema.ts` (2) · `src/graph/delivery-facts.ts` (2) · +`src/validate/validators.ts` (2) · `src/extract/derive.ts` (2) · `src/extract/reify.ts` (2) · +`src/extract/index.ts` · `test/design-review.test.ts` · `test/validators.test.ts` · +`test/reader.test.ts` (2) · `check-carrier-truth.mjs` (3 claims + 5 classification rules retired +with the doc) · `check-prose-schema.mjs` (2 pins re-pointed). + +`02` — `CONTEXT.md` (5 section pointers + the MD-16 note + the header's doc-range sentence) · +`AGENTS.md` (the where-to-look row and the glossary row) · `docs/concept/DECISIONS.md` (D1/D2, +landed as the pre-repair commit) · `docs/concept/README.md` (index row + reading path) · `01` (3) · +`04` (4) · `05` (8) · `06` · six JTBD story files (14 reference lines) · +`examples/checkout-v1/README.md` · `src/validate/validators.ts` (3, two of them user-visible +validator diagnostics) · `src/validate/readiness-floor.ts` · `src/graph/delivery-facts.ts` (2) · +`src/reader/reader.ts` · `src/model/relations.ts` · `test/checkout-v1.test.ts` · +`test/extract.test.ts` (a test title) · one extraction fixture comment · +`check-prose-schema.mjs` · `check-carrier-rule.mjs`. + +No dangling inbound reference survives either deletion. **Amended at S6 (review-09 D-1):** the +S5 sweep ran only the *backticked* citation form (`` `02` ``, `` `03` ``, `02-core-model`, +`03-the-one-graph`), and the repository uses two forms — the repo also cites bare, unbackticked +`02 §3` inside fenced code. One such line survived in `docs/concept/04`'s TS-carrier worked +example and is repaired. The sweep is now stated as it must be run: **both** forms, over every +tracked file outside `plans/`, `reviews/`, and `explorations/` — the bare-token pattern +`(^|[^0-9A-Za-z._/-])0[23]([^0-9A-Za-z._-]|$)` plus the backticked and path spellings +(`` `02` ``, `` `03` ``, `02 §`, `03 §`, `concept/02`, `concept/03`, `02-core-model`, +`03-the-one-graph`). Re-run at S6 over the repaired tree: **zero hits**. + +### Post-close records amendment (external review feedback, 2026-07-26) + +A second, independent read of the closed phase surfaced record defects; each accepted one is +repaired in this amendment series (the product graph is untouched except where named): + +1. **The example-spec count corrected**: 27 new example specs this phase (24 from the conversion + ledger + 3 from S4); the corpus total is 29, including the two phase-2 tracers. The earlier + "29 new" sentence conflated the two. +2. **The S5 gap count aligned**: the §10 S5 row now carries the S6 amendment (11 gaps at S5, + 12 after review-09 D-7). +3. **The glossary widened on the record**: `CONTEXT.md`'s *example space* entry said the space is + owned by a parent `behavior` spec; §1 ruling 2 and the landed corpus put spaces on `rule` and + `model` parents. The entry (and the matching sentence in `docs/concept/04`) now states the + practice: typically `behavior`, any kind lawful when the vocabulary parameterizes its own + law. This is the ruling-2 consequence carried onto a ratified surface instead of living only + in a closed plan. +4. **Ruling 7 narrowed and the docket row decided**: the drafted wording banned more from + source/tests than the guard enforces or the tree honors; the ruling now states the enforced + law, and the widen-vs-narrow choice is recorded as decided on the temporal-guard docket row. +5. **The tests claim made precise**: no residual suite case was deleted; one non-discriminating + bound `Then` step was removed at S6 (review-09 P-2). +6. **The tip inventory stated**: the §10 green-gate commits are a subset; `git log main..HEAD` + is the branch's authoritative series (21 commits at the close proof, this amendment on top). + +Reviewer items declined or already standing, with reasons: the pack-manifest "29 new members" +figures are correct as written (29 new spec IDs of all kinds landed this phase); the oracle-split +and clean-room-exemption hardening items are successor-plan guidance (below), not phase-3 +records; the commit-subject correction (review-09 R-2) stands as recorded — history is not +rewritten. + +### Guidance for the successor plan (recorded, not scheduled) + +1. **Split the golden oracle first.** `test/self-hosting-graph.test.ts` (3,888 lines, one `it()`, + frozen absolute counts) should get a single-purpose split session — by family or histogram + bucket, never one mega-assert — *before* any new conversion wave thrashes it again. +2. **The conversion playbook inherits review-09 P-1's lesson**: 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 (renames make it vacuous). +3. **One source of truth for the contract-dependent suites**: the clean-room lint exemption list + and `vitest-test.mjs`'s dependency table name the same six files independently — derive both + from one shared constant, or the seventh suite will repeat the clean-clone lint surprise. +4. **Gap order for the next corpus wave**: (a) the lower readiness-floor rungs and the per-kind + evidence table (narrows gap 1 and unblocks `05` pressure); (b) the derived-readiness banner + law (one gap shared by `05`/`06`/`07`); (c) the already-implemented-but-uncarried view rules + (the `implemented` view label, the one diagnostic rendering rule, the Design Review wholesale + rewrite). That order buys the most dissolution per spec authored. + +## (k) §8 Docket ledger + +Carried in from phase 2 (all remained open through S1–S5; none was adopted by a wave): the +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 program — dispositioned by the waves it reaches) · control-character latitude · the separate +example id namespace. Rows close only with reasons in §7. + +**Closed at S6:** every row is dispositioned in §7's docket-close table. All eight are **carried** +into the next phase — none closed as done — but each now carries a stated reason rather than +rolling forward silently, which is what review-09 R-3 caught. Two rows gained substance in the +process: temporal-guard token assembly now names the decision it is waiting on, and corpus-test +granularity carries the measurement (`test/self-hosting-graph.test.ts` at 3,888 lines in one +`it()`) that will justify the session that finally splits it. + +## (l) §9 Acceptance criteria + +Graded at the close, after remediation. Review-09 graded 1 / 2 / 3 / 6 **PASS** and 4 / 5 / 7 +**PARTIAL** pre-remediation; the partials are re-graded here against what actually landed. + +1. **Executable-path facts, not claims:** every converted law's parent and child earn + `has-verifier` through the executable path (anchored `verifies` from a bound test), visible + in `generated/graph.json`; zero validation errors; contract generation deterministic under + `--check-clean`. — **PASS.** All 15 converted parents and all 29 example points carry + `has-verifier` in the regenerated graph; 0 errors; `check:self-hosting` and `check:example` + both clean under `--check-clean`. Independently mutation-tested: 27 of 27 points now go red + when the law they name is removed (26 confirmed by review-09, the 27th by S6's re-probe). +2. **Honest readiness:** every promotion clears the floor and carries a resolving verifier; the + phase introduces no `honesty/gaps` warning; the closing readiness distribution is recorded. — + **PASS.** 0 errors / 0 warnings — no `honesty/gaps` warning exists at all. All 51 `ready` specs + carry `has-verifier` and clear the `ready` floor. Closing distribution `ready: 51 / defined: 36` + over 87, recorded in §7 and re-measured off disk at the close. +3. **Coverage never weakens:** residual tests are deleted only where a bound contract honestly + carries the assertion; the suite's law coverage at close is a superset of its opening + coverage. — **PASS.** Zero test files deleted; case counts unchanged across all fourteen + residual suites (verified against `main`); the only `test/` edits outside the new bound suites + are reference-sweep comments and two frozen-fixture refreshes. The one removal at S6 — the + schema-version point's tautological Then step — removed an assertion that could not fail, and + its authored-literal twin still dies under mutation. +4. **Per-doc audits recorded for `02`, `03`, `05`, `06`, `07`;** deletions only where the audit + verdict is fully-carried; no dangling inbound reference survives a deletion + (`check-carrier-truth.mjs` and the full gate stay green). — **PASS after remediation** + (PARTIAL at review time). Five audits recorded; only the two fully-carried docs deleted; all + four standalone audit scripts and the full gate green. The two defects the review found are + closed rather than argued: the one surviving bare-form `02 §3` citation is repaired and the + sweep re-run over **both** citation forms with zero hits (D-1), and the two `carried` verdicts + that rested on a surviving doc or on a file that did not hold the claim are now carried by + enriched Specs (D-2, D-3) — so both deletions stand on the dissolution decision's own terms. +5. **Watch items:** any fired ruling is recorded; every unfired item survives with a reason. — + **PASS after remediation** (PARTIAL at review time — the §5 table then carried entry states + only). No watch item fired in six sessions; §5 now carries a terminal-state column, and each of + the five items records *why* it stayed unfired rather than merely that it did. +6. **The gate holds throughout:** `npm run check` green at every blessed commit; the close runs + the full chain plus a clean-clone proof; the wrapper dependency table and preflight targets + are current. — **PASS.** The twelve-leg chain is green at the close; the pack manifest matched + the spec files at every commit of the phase; the wrapper's per-tree dependency table and the + preflight targets are current. The clean-clone proof is recorded in §10's S6 row. +7. **Records continue:** the conversion/readiness/audit ledgers are terminal; the docket rows + are dispositioned or carried with reasons; an adversarial review over the full branch diff is + archived under `reviews/` with remediation dispositions before close. — **PASS after + remediation** (PARTIAL at review time). All three ledgers are terminal; all eight docket rows + are dispositioned in §7 with reasons; the adversarial review is archived with every one of its + fifteen findings dispositioned and a remediation addendum. The two record defects the review + itself raised are answered on the record: the overstating commit subject is corrected in prose + rather than by rewriting history (R-2), and the docket row that rolled forward five times now + carries its reason and its measurement (R-3). + +## (m) §10 Session and gate ledger + +Sessions execute sequentially; each closes with a green gate, a regenerated Design Review, and a +commit series on the effort branch. This ledger is git process evidence, never graph content. + +| Session | Delivers | Gate discipline | State | +|---|---|---|---| +| S1 | validation-family conversions (§2) + promotions that honestly clear | orchestrator-verified green gate | done — 11 points bound across 6 parents; three green-gate commits | +| S2 | extraction/model cheap wins (§2) | orchestrator-verified green gate | done — 6 points bound across 4 parents; one green-gate conversion commit | +| S3 | corpus additions (§3) + codegen/runner/notation conversions | orchestrator-verified green gate | done — 2 new specs + 1 enrichment; 7 points bound across 3 parents; 2 new `impl:` anchors; one green-gate conversion commit | +| S4 | readiness maturation sweep (ruling 5) | orchestrator-verified green gate | done — 40 `defined` specs swept; 4 promoted (2 enriched in place, 2 converted) + 3 new points at `ready`; 36 recorded as honest `defined`; one green-gate sweep commit | +| S5 | per-doc audits + first dissolutions (§4) | orchestrator-verified green gate over the regenerated Design Review | done — five audits recorded (§7); `02` and `03` deleted with their full reference sweeps; `05`, `06`, `07` stay with their gaps recorded as future corpus work (11 at S5, amended to 12 at S6); D1/D2 re-pointed; three green-gate commits | +| S6 | adversarial review, remediation, full close, done-record | full chain + clean-clone; review archived | done — review archived and all 15 findings dispositioned; four majors and seven minors remediated across two green-gate commits; §5 / §7 / §8 / §9 terminal records written; the twelve-leg `npm run check` green in the checkout and again in a clean clone at `268d1cb` after that clone caught, and the phase fixed, a clean-room lint hole (S6 ruling 7) | + +Owner ratification of every gate above happens at the phase PR review; no live owner acceptance +is claimed by this ledger. diff --git a/reviews/09-self-hosting-phase-3-pre-close-review.md b/reviews/09-self-hosting-phase-3-pre-close-review.md new file mode 100644 index 0000000..21cb62d --- /dev/null +++ b/reviews/09-self-hosting-phase-3-pre-close-review.md @@ -0,0 +1,532 @@ +# 09 - Self-hosting phase-3 pre-close adversarial review + +**Reviewed:** the full `main...feature/protocol-self-application-phase-3` diff at `0ff991c` +(15 commits, 92 files, +5019 / −773), against `plans/20-self-hosting-phase-3.md` — its §1 +engineering rulings and its §9 acceptance criteria are this review's yardstick. + +**Method:** five adversarial dimensions, run in parallel. The central method for the phase's core +risk — verifier honesty — was **mutation testing**, not code reading: a full sandbox copy of the +branch was built outside the repository, its engine deliberately broken one law at a time +(37 mutations across five batteries), and the 27 new bound points re-run after each. A point counts +as honest only if it goes **red** when the law it names is removed. Every claim marked +**CONFIRMED** below was reproduced by a probe against the built product; nothing here rests on a +ledger or a receipt. + +**Disposition:** compiled for the remediation session. No product file was changed by this review; +the working tree was clean at start and at finish. The disposition column of the table at the foot +is deliberately left open. (`reviews/` is Prettier-ignored and temporal-guard-exempt, so this file +is not format-policed and may carry dates and plan references.) + +--- + +## Verdict + +**SOUND TO CLOSE ONCE THE FOUR MAJORS ARE DISPOSITIONED. NO BLOCKER.** + +The phase's central claim survives adversarial probing better than expected. **26 of the 27 new +bound points are provably mutation-sensitive to the law they name** — delete the validator, the +derivation rule, the grammar branch, or the constant each point claims to verify, and that point +goes red. None is a params-echo tautology; every Then step reads real engine output through a +public seam, and the world factories build inputs whose outcome the engine, not the factory, +decides. The graph bookkeeping is exact (pack manifest 87/87, all 29 new anchors top-level, +proximate and resolving, the golden oracle a faithful transcription with **zero** assertions +loosened), the residual vitest suites survive whole with no net case loss in any of the fourteen +checked files, all 51 `ready` specs carry `has-verifier`, the corpus holds at 0 errors / 0 warnings, +and every number in the §2 / §6 / §10 ledgers — including all five readiness distributions, +re-measured at their own commits — checks out exactly against git. + +The majors are of two kinds, and none corrupts authored data or falsifies a derived fact today. +**One (P-1)** is a genuine verifier-honesty defect: a single point claims to execute the +concreteness law and stays green with that law entirely deleted — the precise failure mode this +phase exists to prevent, and it falsifies a recorded §6 ruling. **Three (D-1, D-2, D-3)** are +defects in the S5 dissolution record: one dangling reference that directly refutes the plan's own +sweep sentence and acceptance criterion 4, and two audit rows granted a deletion-authorizing +`carried` verdict on carriers that do not carry what the row says. Deletion is the one +irreversible-feeling act in this phase, so the rows that authorized it deserve to be exact — but +nothing left the repository, and the surviving docs the rows lean on are themselves gated by the +same rule, so the exposure is bounded and self-correcting. + +Acceptance criteria 5 and 7 are **not yet met** — but honestly so: both are S6 deliverables and S6 +is marked `planned`. Archiving this review is part of discharging criterion 7. + +--- + +## Dimension 1 — Verifier honesty (mutation-tested) + +Every one of the 27 new bound points was subjected to at least one mutation aimed at the law its +spec names. Confirmed kills: + +| Point | Mutation that killed it | Killed | +|---|---|---| +| `warn-level-signals.orphan-signal` | `checkOrphans` incident-edge guard inverted | ✓ | +| `warn-level-signals.ready-gap-signal` | `checkGaps` has-verifier guard short-circuited | ✓ | +| `referential-integrity.dangling-target` | missing-target branch disabled | ✓ | +| `referential-integrity.did-you-mean` | `describeMissingTarget` suggestion suppressed | ✓ | +| `authored-honesty.section-authored-fact` | section-interior smuggled-fact scan removed | ✓ | +| `authored-honesty.unearned-stated-fact` | stated-but-underived branch disabled | ✓ | +| `claim-separation.collapsed-edge-claim` | `satisfies` → `requireClaim("anchored")` removed | ✓ | +| `claim-separation.unratified-descriptor` | descriptor fail-closed made fail-open | ✓ | +| `verification-linkage.unbound-example` | (a) linkage finding suppressed · (b) enabled-example anchor requirement dropped | ✓✓ | +| `verification-linkage.unresolved-oracle` | (a) oracle-linkage id renamed · (b) `models` made to confer `has-verifier` | ✓✓ | +| `pack-coherence.incoherent-aggregate` | duplicate-member threshold relaxed 2→3 | ✓ | +| `two-check-families.split-report` | either family's producing check disabled | ✓ | +| `excludes.segment-boundary` | exact-prefix match degraded to substring | ✓ | +| `excludes.refused-path` | `InvalidExcludePathError` replaced by a silent skip | ✓ | +| `schema-versioning.declared-version` | `schemaVersion` literal bumped | ✓ | +| `stable-ids.namespaced-round-trip` | `formatId` drops the `#` sub-part | ✓ | +| `stable-ids.malformed-refusal` | lowercase-namespace refusal removed | ✓ | +| `markdown-parser.bounded-parity` | either carrier's finding class renamed | ✓✓ | +| `executable-contracts.multi-entry-example` | multi-entry degradation finding renamed | ✓ | +| `executable-contracts.case-colliding-path` | `files.clear()` withholding removed | ✓ | +| `example-runner.step-order` | plan executed in reverse contract order | ✓ | +| `example-runner.red-step-naming` | `at step:` prefix stripped from the failure | ✓ | +| `slot-notation.typed-declaration` | `typed` form collapsed to `bound` | ✓ | +| `slot-notation.refused-guess` | unparsable RHS guessed as `bound` instead of `malformed` | ✓ | +| `anchors.lookalike-refusal` | any relative import trusted as a builder source | ✓ | +| `anchors.physical-identity` | relative-import canonicalization disabled | ✓ | +| **`executable-contracts.concreteness-refusal`** | **concreteness law deleted — point stayed GREEN** | **✗** | + +**What this rules out.** No point asserts the contract's own params back at itself: the shared +`namesFinding` / `familyReports` helpers read `validateGraph`'s real output and compare +`validatorId`, `severity`, and `family` against values authored in the spec, and no assertion is +satisfiable by the world factory alone. Two assertions that *looked* hard-wired were probed +specifically and are not: `unresolved-oracle`'s `conferred: false` dies the moment `models` edges +are made to confer `has-verifier`, and `unbound-example`'s dies the moment the enabled-example rule +drops its anchor requirement — both are exactly the "binding, never liveness" law. + +**Nor do the bound suites weaken or drift from their residual twins.** `test/validators.test.ts`, +`test/exclude-diagnostics.test.ts`, `test/graph-schema.test.ts`, `test/ids.test.ts`, +`test/extract-parity.test.ts`, `test/codegen.test.ts`, `test/runner.test.ts`, +`test/notation.test.ts`, `test/anchor-trust.test.ts`, and `test/descriptors.test.ts` carry +identical case counts on `main` and `HEAD`; zero test files were deleted; the only edits under +`test/` outside the new suites are reference-sweep comments and two frozen-fixture refreshes. §1 +ruling 1 held. + +### P-1 (MAJOR) — the concreteness point is over-determined and passes with its own law deleted + +**Where:** `specs/extraction/executable-contracts.concreteness-refusal.sdp.md`, bound at +`test/self-hosting-extraction.test.ts:381`. **CONFIRMED** by four probes. + +The point's stated subject is unambiguous — intent: *"Execute the concreteness law where the +example leaves a declared slot unbound"*; parent rule: *"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."* Its two Then steps assert `fileCount: 1` and `emitted: false`. + +Both assertions are satisfied by **two independent engine gates**, and the point cannot tell them +apart. `generateContracts` withholds a step contract when `bindableScenario` returns undefined (the +concreteness law, `src/codegen/contracts.ts:562-568`) **and, separately,** when +`resolveExampleVocabulary` reports issues (`src/codegen/contracts.ts:903-916`). The probe world's +bare `{n}` step trips both. + +Probe results: + +- Delete the concreteness slot check + (`if (parseSlots(text).some((slot) => slot.form !== "bound")) { return undefined; }`) → + **all 8 tests in the suite still pass**; the only observable change is a new + `contracts/unmatched-vocabulary-step` warning the point never reads. +- Bypass `bindableScenario` entirely (early-return the scenario) → **point still green**. +- Disable only the vocabulary gate → **point still green**. +- Disable **both** gates → the point finally goes red. + +So the point verifies a disjunction, not the law it names, and it has no teeth on the concreteness +law at all. This directly falsifies §6 S3 ruling 5, which lists *"the concreteness refusal (space +contract emitted, step contract withheld)"* among the clauses the points exercise and reports +"no drift found" on that basis. Actual coverage of the law is not lost — `test/codegen.test.ts` +still holds it — so this is a verifier-and-record defect, not a coverage regression. + +**Fix direction:** make the point discriminate — bind a used step whose skeleton *does* match the +parent vocabulary while leaving the slot unbound (so vocabulary resolution succeeds and only +concreteness can withhold), or add a Then step asserting that generation reports **no** +`contracts/unmatched-vocabulary-step` finding. Then re-run the mutation: a point that cannot be +killed by deleting its own law must not be recorded as exercising it. + +### P-2 (MINOR) — one decorative Then step on the schema-version point + +`specs/extraction/schema-versioning.declared-version.sdp.md`'s second Then step — *"the parsed +payload agrees with the engine's declared version: {agrees: true}"* — binds to +`expect(payloadOf(world).schemaVersion === schemaVersion).toBe(true)` +(`test/self-hosting-extraction.test.ts:218-223`), comparing the serialized payload against the +engine's own exported constant. Both sides move together under every mutation, so the step can only +fail if `serializeGraph` drops the field outright — which the *first* Then step (the authored +`"0.4.0"` literal) already catches, and catches strictly harder. The point as a whole is honest; +this step reads as coverage it does not provide. **CONFIRMED.** + +--- + +## Dimension 2 — Spec-content honesty + +**What held.** The S1 spec-side repairs are, on the whole, genuinely intent-backed rather than +promoted from code, and the S4 enrichment of the two carrier stubs is **true of the engine**, which +I probed rather than assumed: + +- The fail-closed descriptor clause on `spec:validation.claim-separation` is near-verbatim ratified + intent — `docs/concept/05` §2 check 3 already says *"The floor is never evaluated over an + unratified descriptor (fail closed, never a crash or a silent skip)."* +- *"A non-resolving trace is named loudly and confers no delivery fact"* on + `spec:validation.verification-linkage` is carried by the then-live `02` §2 (*"A `verifies` edge + from a verifier that is not enabled … does not yield `has-verifier`"*) and `04` §2's oracle clause + (*"the anchor confers no delivery fact"*). +- **Every clause added to `spec:carrier.envelope-contract` is true of the parser**, probed on live + reifications: a missing `relations` / `readiness` / `altitude` key each refuses with + `extract/invalid-frontmatter`; an out-of-set key refuses with `extract/unrecognized-property`; and + all five derived names — `implemented`, `has-verifier`, `observed`, `deliveryFacts`, `claim` — + refuse under `extract/reserved-property`, i.e. *"under its own finding class"* exactly as the new + clause claims. (This also re-confirms review-08's R-6 remediation landed and held.) +- `spec:extraction.example-runner`'s most surprising clause — *"The core contributes `unspecified`, + the one outcome no Spec ever states"* — is ratified material, not module JSDoc promoted into + intent: `docs/concept/04` §4 and `CONTEXT.md`'s oracle row both carry it. +- **Readiness promotions are honest.** All 51 `ready` specs carry `has-verifier` in + `generated/graph.json`; so do all 29 example specs and all 15 converted parents. The corpus + reports zero `honesty/gaps` warnings. The closing distribution counted directly off frontmatter is + `defined: 36 / ready: 51` over 87 — exactly what §6 S4 records — and the `ready` floor + (`all-relations-resolve` · `depends-on-and-refines-targets-are-defined` · `anchors-resolve`) + clears for every one. `spec:carrier.envelope-contract` and `spec:carrier.prose-ownership-rule` + were correctly enriched **before** promotion rather than promoted as the one-line stubs they were; + both are now acceptance-grade on their own terms. + +### S-1 (MINOR) — one added rule line exists to make a test dimension readable + +`spec:validation.pack-coherence` gained *"Membership is counted on the derived belongsTo edges the +manifest re-expresses, so a repeated manifest entry is named once per repeated member."* The plan is +candid that this was added *"so its `{memberCount}` dimension is readable"* (§6 S1 ruling 4). It is a +true statement about `checkPackMembers`, but it is a **counting-mechanism** description authored to +serve a bound point's vocabulary, not a law ratified anywhere in the concept material — the nearest +intended truth (`02` §4, `spec:model.pack-aggregate`) says only that `belongsTo` is derived and that +packs are checked for coherence, not completeness. This is the mildest form of what §1 ruling 9 +forbids: the direction of authorship ran test → spec. Worth an explicit disposition (ratify as +stated, or reword to state the law rather than the count). + +### S-2 (MINOR) — the did-you-mean tie rule is a code-derived refinement recorded as pre-ratified + +`spec:validation.referential-integrity` gained *"names the unique nearest known id as a suggestion +and stays silent when two candidates tie."* `05` §2 check 1 ratifies only *"with a 'did you mean…?' +suggestion **where possible**."* The tie-breaking rule is the precise behaviour of `suggestNearestId` +(`src/validate/validators.ts:130-140`, which returns `undefined` on a tie). It is defensibly +*derivable* from `05` §2's cross-cutting L2 rule (ambiguity is loud, never a silent winner), and the +clause states that rationale — so this is a legitimate reading, not a fabrication. But §6 S1 +ruling 4 records all three promotions as *"already ratified in the concept material and merely +absent from the carrying specs,"* and for this one the specific rule was not. Record-accuracy nit. + +--- + +## Dimension 3 — Graph and bookkeeping integrity + +**Clean, and unusually so.** Verified against a from-scratch regeneration +(`87 specs · 1 pack · 65 anchors → 153 nodes · 294 edges (0 errors, 0 warnings)`): + +- **Golden oracle faithful; nothing loosened.** The `test/self-hosting-graph.test.ts` diff against + `main` is five literal-count updates plus 29 appended node ids — no assertion removed, no exact + matcher replaced by a fuzzy one. The file's only four non-exact matchers are byte-identical to + `main` and guard the anchor-proximity check, not a structural claim. Every literal + (counts `87/1/65`, `153` nodes, `294` edges, histogram `{defined: 36, ready: 51}`, the full + explicit id list, all 65 anchors, every spec's descriptors) matches the regenerated graph exactly, + cross-checked through the CLI path rather than the test's own extraction. +- **Pack manifest complete.** 87 spec ids on disk, 87 manifest members, zero missing, zero ghosts, + zero duplicates; all 29 branch-added spec files present. Corroborated by 87 `belongsTo` edges. The + manifest was in sync at **every one of the 15 commits**, with spec counts stepping + 58→60→64→69→75→84→87 exactly as the ledgers claim. +- **`vitest-test.mjs` correct.** Exactly six test files import a generated contract; all six are + listed; all seven listed paths exist; `test/self-hosting-graph.test.ts` and + `test/self-hosting-contracts.test.ts` are correctly *absent* (they derive in memory). The §6 S1 + ruling 2 restructuring is real (one row per generated tree, `testPaths` list), and the missing-tree + stderr that `test/self-hosting-duplicate-ids.test.ts` pins byte-exactly is unchanged from `main`. +- **No decorative anchors.** All 29 new anchors (2 `impl:` + 27 `specTest`) are top-level, resolve + to real spec nodes, carry `claim: "anchored"`, and sit within the enforced 20-line proximity of a + real entrypoint (measured deltas 7–9). Both `impl:` anchors sit on the entrypoints their specs + *name in their own text*: `impl:protocol.example-runner` at `src/runner/index.ts:98` above + `planExample` (:106), `impl:protocol.slot-notation` at `src/notation/slots.ts:256` above + `parseSlots` (:265). Builder trust resolves physically through `src/ids.ts` / + `src/model/code-anchor.ts`; both modules stay import-graph leaves, so the `/runner` subpath keeps + its dependency-light shape (8.5 KB built). +- **Determinism holds.** `check:self-hosting` and `check:example` both exit 0; the full suite is 505 + tests green; `preflight` clean; `git status --porcelain` empty after regeneration. + +### B-1 (MINOR) — the runner anchor orphans `planExample`'s doc comment + +`src/runner/index.ts`: the JSDoc block at lines 92-97 ("The framework-neutral core: pairs each +contract step with its bound handler, in contract order…") documented `planExample` on `main`. The +new anchor constant was inserted between them, so the block now binds to `exampleRunnerAnchor` and +`planExample` is undocumented. `src/notation/slots.ts` got the same insertion right (anchor placed +above `parseSlots`'s own comment). One-line move. **CONFIRMED.** + +--- + +## Dimension 4 — The dissolutions + +### D-1 (MAJOR) — a dangling `02` reference survives, refuting the plan's own sweep claim + +**Where:** `docs/concept/04-authoring-and-binding.md:29`. **CONFIRMED.** + +``` +// content only — never refs (02 §3): a promoted example is a child spec that refines/verifies this one +``` + +`02 §3` is the sections section of the deleted `docs/concept/02-core-model.md`; its content now +lives in `spec:model.spec-sections` (the phase-2 coverage trail names exactly that mapping). The line +sits inside the canonical TS-carrier worked example of a **surviving, active** concept doc, and the +same file already uses the repaired form at :11 and :45. + +Plan §7 states flatly: *"No dangling inbound reference survives either deletion: a repository-wide +grep for `` `02` ``, `` `03` ``, `02-core-model`, and `03-the-one-graph` outside `plans/`, +`reviews/`, and `explorations/` returns nothing."* All four of those patterns do return nothing — +the claim is *literally* true and *materially* false, because the repo uses **two** citation forms +(backticked `` `05` §3 `` and bare `05 §3` inside code fences) and only the backticked one was swept. +An exhaustive bare-token sweep — `(^|[^0-9A-Za-z._/-])0[23]([^0-9A-Za-z._-]|$)` over every tracked +non-exempt file — returns **exactly this one line** and nothing else. The miss is a single line, but +it is a miss against acceptance criterion 4. + +**Fix direction:** repair the line to `` (`spec:model.spec-sections`) ``, and amend the plan's sweep +sentence to name the unbackticked citation form so the next dissolution's sweep does not repeat the +hole. + +### D-2 (MAJOR) — the `03` §1 edge-contract row is graded `carried` on a surviving concept doc plus code + +**Where:** `plans/20-self-hosting-phase-3.md:515`. **CONFIRMED** by reading the deleted table and +every named carrier. + +The row discharges `03` §1's eight-column edge-contract table. Six of its seven substantive columns +carry cleanly. The **Readiness effect** column does not: it asserts two concrete laws (`refines` / +`dependsOn` → *"`ready` floor: target ≥ `defined`"*; `satisfies` → *"`ready` floor: present anchors +must resolve"*), and the row's own citation is *"`spec:validation.readiness-floor` **+ `05` §3**."* +`spec:validation.readiness-floor` is a one-sentence stub — *"A Spec may state a readiness only when +every clause in that readiness floor passes"* — that enumerates no clause. The clauses exist only in +`src/validate/readiness-floor.ts:465-480` and in `docs/concept/05` §3, a doc **this same session +audited as "gaps · stays."** So the column moved doc → doc, not doc → Spec. + +The dissolution decision's operative sentence is narrow +(`specs/decisions/concept-docs-dissolve.sdp.md:16`): + +> Concept documents may dissolve only after their semantic contract is carried by **executable Specs +> and lean registries**. + +A surviving concept document is neither. The session legislated the widening itself, in §6 S5 +ruling 2 (*"a row whose law is carried by a surviving surface reads `expository-only`"*) and again in +the §7 preamble (*"a `gap` is a stated law … no Spec, registry, **or code+test surface** carries"*). +Two problems: the widening substitutes a weaker test than the decision states, and the `03` §1 row +uses it under a **`carried`** verdict — the verdict that authorizes deletion — while ruling 2's +escape hatch was scoped to `expository-only` rows. Discharging an *intended-truth* contract onto +`readiness-floor.ts` clause ids is also precisely the promotion `AGENTS.md` forbids (`src/` is +evidence of realization, "never permission to silently promote code behavior into intent"). + +Also uncarried by any Spec from the same row: the `models` uniqueness clause (*"more than one oracle +models the same space"* is an error), which lives solely as a validator message at +`src/validate/validators.ts:626`. + +**In fairness:** nothing left the repository; `05` is gated by the same rule and so cannot itself be +deleted until its floor clauses reach a Spec; and the equivalent citation in the `03` §2 row +(`04` §1) is decorative — `spec:extraction.determinism` rule 3 already carries that whole contract +including the per-carrier asymmetry, so that row is genuinely carried. + +**Fix direction:** either land the two floor clauses as rule lines on +`spec:validation.readiness-floor` — the cheaper and better option, since it is the thinnest `ready` +Spec in the corpus relative to the contract it is asked to carry — or amend the dissolution decision +if code+test surfaces are to count, rather than widening it by plan ruling. + +### D-3 (MAJOR) — the `02` §5 row cites a carrier that does not carry it + +**Where:** `plans/20-self-hosting-phase-3.md:500`. **CONFIRMED.** + +The row grades *"the MVP namespace set and the `doc:` reservation"* as **carried** by *"`src/ids.ts` +(the namespace set) · `CONTEXT.md` flagged ambiguities."* `src/ids.ts` contains **no namespace +set**. It has one three-member code-anchor subset +(`CODE_ANCHOR_NAMESPACES = ["impl", "api", "component"]`), four per-builder singletons (`spec`, +`pack`, `test`, `oracle`), and an open grammar gate admitting *any* lowercase namespace. `doc` +appears once, in a comment describing what `ref()` **rejects**. + +Genuinely uncarried anywhere after the deletion: + +1. the eight-member MVP namespace list (`spec` · `pack` · `impl` · `api` · `test` · `oracle` · + `component` · `doc`) — no Spec, registry, or code enumerates it; +2. the positive half of the `doc:` reservation (*"reserved for a genuinely external document linked + from a decision spec … never for an in-system decision"*) — `CONTEXT.md`'s flagged-ambiguities + entry carries only the *deferral*, and `CONTEXT.md:171` / `src/model/relations.ts:21` carry only + the parenthetical half; +3. the convention *"in-system decisions live under the `spec:decisions.*` convention"* — now only + de-facto corpus practice, stated by nothing. + +Small in volume, but graded `carried` under a fully-carried verdict, and (2) and (3) are naming law +an authoring agent would want. **Fix direction:** add them to `spec:model.stable-ids` (already +`ready`, already owns the grammar) or to the `CONTEXT.md` term ledger, and correct the citation. + +### D-4 (MINOR) — the `02` §6 row names the wrong carriers + +The `supersedes`-only-on-decision-specs row cites `spec:validation.claim-separation` + +`src/validate/validators.ts`. The former states only a generic pointer (*"relation endpoint contracts +must use their ratified forms"*) and enumerates no endpoint. The law **is** genuinely carried — by +`spec:model.relations` (*"A current Decision Record points forward to the decision it replaces"*), +`CONTEXT.md:173`, and the `derive-graph` narrative — but the row names none of them. A reader +auditing only the two cited surfaces would find one generic sentence plus a validator error string. +Contract carried; citation wrong. + +### D-5 (MINOR) — two consistency-script comments now overstate what their checks prove + +The four audit scripts were **faithfully re-pointed, not weakened** — verified assertion by +assertion against `main` (`check-carrier-truth.mjs` 32→29 CLAIMS / 53→48 RULES, with a 1:1 +correspondence to the deleted docs' matching lines, so both retirements were *forced*, not +discretionary; `check-prose-schema.mjs` 19→19; `check-carrier-rule.mjs` 9→9; +`check-self-hosting-gates.mjs` byte-identical). Each re-pointed pin was proved non-vacuous by +corrupting its target in a scratch tree and watching the script fire. Two comment defects survive: + +- `check-carrier-rule.mjs`'s header still claims it asserts *"the SAME logical/physical relations + distinction on both surviving surfaces."* After the split it pins **two independently authored + sentences** and proves only their co-presence, never their sameness — the wording coupling + `main`'s shared literal enforced is gone. §6 S5 ruling 1 discloses the change honestly; the + script's own comment does not. +- `check-carrier-rule.mjs:54-56` still names *"the core-model carrier note"* — the deleted `02`. + +Structural note, pre-existing rather than a phase-3 regression: none of the four scripts is wired +into `npm run check` (a deliberate non-wiring decision recorded in plan 17), so every re-pointed pin +is on-demand only and any future weakening of them is invisible to CI. Related: the obsolete-sweep +in `check-carrier-truth.mjs` scans `docs/concept/` + `jtbd-stories/` + `CONTEXT.md` but **never +`specs/`** — and carrier-relevant exposition is precisely what is migrating into `specs/`. Harmless +today (zero hits), but the sweep no longer reaches the content's new home. + +### D-6 (MINOR) — a provenance sentence misreads as this phase + +§6 S5 ruling 3 and the `03` §5 audit row say the carriage "landed during **the phase's** earlier +corpus work." In a phase-3 plan that reads as phase 3. It landed in **phase 2**, on `main`, in +`8f5aabe`: this branch never touched `specs/extraction/derive-graph.sdp.md` (`git log main..HEAD` on +that file is empty; `git show main:` is byte-identical). The headline claim ("closed before this +session, not by it") is accurate; the attribution is not. The narrative itself does carry the +section — all four of `03` §5's laws map clause for clause. **CONFIRMED.** + +### D-7 (MINOR) — a `carried` verdict that concedes an uncarried surface in its own text + +The `06` §1 surfaces-taxonomy row reads verdict **carried**, then qualifies: *"except the Mermaid +and reference-projection rows, which only `06` §8 names."* That is an admitted uncarried surface, +and it appears in no `gap`-verdicted row and in none of the eleven numbered gaps carried out of S5. +`06` stays, so no deletion rests on it — a bookkeeping inconsistency in the audit table's own +grammar rather than a deletion defect. (The other seventeen gap-verdicted rows do consolidate +correctly into the eleven numbered items.) + +--- + +## Dimension 5 — Process-record fidelity + +**What held — the ledgers are honest.** All 13 §2 conversion rows are true: every claimed point +exists as a spec file with the exact claimed id and has a `bindExample` handler beside a `specTest` +anchor whose `verifies` names it, and every delivered count sits inside its planned range. The §3 +modeling policy landed exactly as specified. Every §10 row's commit count maps to real commits +(S1 = 3 feat, S2 = 1, S3 = 1, S4 = 1, S5 = 3; S6 honestly `planned`). The S4 arithmetic is exact — +4 promoted + 36 stays-defined (15 individual + the `21 × spec:decisions.*` row) = 40 swept. And all +five readiness distributions were **re-measured at their own commits**, not merely checked for +internal consistency: 51/7 over 58 at the draft, 45/24 over 69 at S1, 41/34 over 75 at S2, 40/44 over +84 at S3, 36/51 over 87 at S4 — and 36/51 over 87 on disk at HEAD. + +**Temporal-guard compliance.** `check:temporal` exits 0. Independently swept by hand: all 29 new +spec files and all four new `test/self-hosting-*.test.ts` suites carry **zero** temporal tokens of +any category — no ISO dates, no month names, no `S1`–`S6` handles, no wave handles, no plan +references. Phase 3 introduced no new ruling-7 violation. + +### R-1 (MINOR) — §1 ruling 7 is stated as current-state law, and the repo does not satisfy it + +The guard's pattern is `Session[ -][0-9]|Wave[- ][A-Z]|Fold-[A-Z]|deferredInSession|plans/[0-9]+|20[0-9]{2}-[0-9]{2}-[0-9]{2}`. +It cannot see `plan 12`, `plan-12`, `phase N`, bare `S1`-style handles, month names, or "docket". +Seven non-exempt files carry plan references that ruling 7 forbids — `src/runner/index.ts:6`, +`src/notation/slots.ts:2,15`, `src/validate/readiness-floor.ts:213`, +`src/validate/validators.ts:390`, `CONTEXT.md:139,157,210`, `check-carrier-truth.mjs:6-7` — and, most +pointedly, **a Spec does**: `specs/decisions/concept-docs-dissolve.sdp.md:18` reads +*"consequence: Deletion is later work, never part of phase 1."* That is a session-relative handle in +the very decision Spec that authorized S5's deletions, now visibly obsolete because the deletion it +defers has occurred (its golden-corpus twin at `test/self-hosting-graph.test.ts:1015` mirrors it +byte for byte). **All of these pre-date this branch** — phase 3 authored none of them, and the two +files it did touch (`src/runner/index.ts`, `src/notation/slots.ts`) gained only anchor constants. +The finding is that ruling 7 reads as a description of the repo and is not one, and that the guard +will not catch the next such token either. **CONFIRMED.** + +### R-2 (MINOR) — a commit subject claims a docket close that did not happen + +Commit `a989b14`'s subject is *"docs(plans): record the S4 readiness-sweep ledger and close its +docket row."* The §8 Docket ledger is **byte-identical to the draft commit `b536754`** — it merely +restates the eight carried-in rows as "all remain open," and not one row is dispositioned. The +commit body's narrower claim (it closes the §10 S4 row) is true; the subject line overstates. +**CONFIRMED.** + +### R-3 (MINOR) — the carried-in corpus-test-granularity docket row was reached by every wave and dispositioned by none + +That row entered the docket from review-08 R-28, described as "owned by this program — dispositioned +by the waves it reaches." Every wave reached it, none dispositioned it, and the condition it names +got substantially worse: `test/self-hosting-graph.test.ts` grew from 2,287 to 3,879 lines and is +still a **single** `it()` with frozen absolute counts, so the first failure still masks the rest and +every corpus edit still churns one monolithic block. Not a correctness defect — the oracle is exact +and faithful (Dimension 3) — but the row should close with a reason rather than roll forward +silently. **CONFIRMED.** + +### Acceptance criteria (§9) + +| # | Criterion | Verdict | Evidence | +|---|---|---|---| +| 1 | Executable-path facts, not claims | **PASS** | 15/15 converted parents and 27/27 child points carry `has-verifier` in `generated/graph.json`; 0 validation errors; `check:self-hosting` and `check:example` both clean under `--check-clean`. | +| 2 | Honest readiness | **PASS** | 0 errors / 0 warnings — no `honesty/gaps` warning exists at all; closing distribution recorded and matching disk (36 / 51 over 87). | +| 3 | Coverage never weakens | **PASS** | Zero test files deleted; case counts unchanged across all fourteen residual suites; +1,582 net lines of new bound suites. | +| 4 | Per-doc audits; clean deletions | **PARTIAL** | Five audit tables present; only the two `fully carried` docs deleted; all four audit scripts and the full gate green — **but** one dangling reference survives (D-1) and two `carried` verdicts rest on carriers that do not carry them (D-2, D-3). | +| 5 | Watch items | **PARTIAL** | §5 table is byte-identical to the draft — entry states only, no terminal column. Only 2 of 5 items have a recorded disposition anywhere (table-sugar, single-literal — both "unfired"); multi-entry constraint form, array-section prose sub-owner, and Markdown Pack syntax carry no reason. S6 is `planned`, so this is not-yet-due rather than false. | +| 6 | The gate holds throughout | **PASS** | `npm run check` green at HEAD (full 12-leg chain); pack manifest ↔ spec files in sync at **every one** of the 15 commits; wrapper dependency table and preflight targets current. The clean-clone proof remains S6's. | +| 7 | Records continue | **PARTIAL** | Conversion ledger terminal (13/13 `done`), readiness ledger complete, five audit tables recorded — but §8's docket rows are undispositioned (R-2, R-3) and no adversarial review was archived. Archiving this document discharges the last clause; the docket rows still need reasons. | + +--- + +## Suggested follow-up order + +1. **P-1** — the concreteness point, TDD: change the world so the mutation kills it, then re-run the + mutation to prove it. Correct §6 S3 ruling 5's "no drift found" sentence in the same change. +2. **D-1** — the one-line reference repair, plus the plan's sweep-sentence amendment (name the + unbackticked citation form). +3. **D-2 + D-3 + D-4** — one records-and-corpus session: land the two floor clauses on + `spec:validation.readiness-floor`, land the namespace set and the `doc:` / `spec:decisions.*` + conventions, and correct the three audit-row citations. This is the cluster that makes the two + deletions defensible on the decision's own terms rather than on a plan ruling. +4. **Criteria 5 and 7** — the S6 close: watch-item terminal states with reasons, docket-row + dispositions (including R-3), the conversion-ledger and audit terminal record, and the + clean-clone proof. +5. **P-2, S-1, S-2, B-1, D-5, D-6, D-7, R-1, R-2** — one hygiene-and-records batch. R-1 deserves a + deliberate call: either widen the guard's token list to match ruling 7 and repair the eight + sites, or narrow ruling 7's wording to what the guard actually enforces. + +--- + +## Disposition table + +Filled by the S6 remediation-and-close session (see the addendum below). Every one of the fifteen +findings carries an explicit disposition; nothing rolled forward silently. + +| # | Severity | Finding | Disposition | +|---|---|---|---| +| P-1 | major | Concreteness point is over-determined; stays green with its own law deleted | **FIXED** — the point now runs under a parent that declares no shared vocabulary, removing the second gate structurally. Re-probed both ways: deleting the unbound-slot refusal turns it red (and only it); disabling the vocabulary gate leaves it green. §6 S3 ruling 5's "no drift found" sentence corrected. | +| P-2 | minor | Decorative Then step on the schema-version point | **FIXED** — the step is removed from the parent vocabulary, the point, the binding, and the oracle. The authored `"0.4.0"` literal remains, and still dies when the constant is bumped (re-probed). | +| S-1 | minor | Pack-coherence mechanism line authored test-first | **ACCEPTED AS STATED** — the sentence is true of `checkPackMembers` and states a derivation mechanism (`belongsTo` re-expresses the declared manifest) that `spec:model.pack-aggregate` and the claim-inheritance law already ratify. The authorship direction is recorded rather than reversed; no reword invented. | +| S-2 | minor | Did-you-mean tie rule recorded as pre-ratified | **RECORD CORRECTED** — §6 S1 ruling 4 now separates the ratified suggestion (`05` §2 check 1) from the tie rule, which is a derivation from the ambiguity-is-loud principle authored in this phase. The clause itself stands. | +| B-1 | minor | Runner anchor orphans `planExample`'s JSDoc | **FIXED** — the anchor constant moved above the doc comment; the comment documents `planExample` again and the anchor stays inside the enforced proximity window. | +| D-1 | major | Dangling `02 §3` reference at `docs/concept/04-authoring-and-binding.md:29` | **FIXED** — repaired to `` `spec:model.spec-sections` ``. The wider sweep (bare-token pattern plus the backticked and path spellings, over every tracked file outside `plans/`, `reviews/`, `explorations/`) returns zero hits. The plan's sweep sentence now names both citation forms. | +| D-2 | major | `03` §1 row graded `carried` on a surviving concept doc + validator source | **FIXED BY ENRICHMENT** — `spec:validation.readiness-floor` gains the `ready` floor's three clauses and the vacuous-anchor reading; `spec:validation.verification-linkage` gains the one-oracle-per-space clause. The audit row's citation is corrected to name them. The lower rungs and the per-kind evidence table stay uncarried and remain gap 1, now narrowed. | +| D-3 | major | `02` §5 row cites `src/ids.ts` for a namespace set it does not hold | **FIXED BY ENRICHMENT** — `spec:model.stable-ids` gains the per-binding-direction reserved set (`spec:` · `pack:` · `impl:` · `api:` · `component:` · `test:` · `oracle:`), the fact that the grammar itself admits any lowercase namespace, and the `doc:` reservation with its actual status: a named deferral no builder mints and `ref()` refuses, with in-system decisions living under `spec:decisions.*`. Row corrected. | +| D-4 | minor | `02` §6 row names the wrong carriers | **FIXED** — the row now names `spec:model.relations`, `CONTEXT.md`'s relations table, and the `derive-graph` narrative first, with the generic endpoint rule and the validator behind them. | +| D-5 | minor | Two stale / overstating consistency-script comments; sweep never reaches `specs/` | **PARTLY FIXED, REST CARRIED** — both comments repaired: the relations leg now states that it proves co-presence of the law across two independently authored surfaces, not verbatim sameness, and the dissolved doc is no longer named. The structural halves are carried with reasons: the four scripts stay unwired from `npm run check` (a recorded plan-17 decision), and extending `check-carrier-truth.mjs`'s obsolete-sweep to `specs/` is corpus-scope work — both ride the docket into the next phase. | +| D-6 | minor | `03` §5 provenance attributed to the wrong phase | **FIXED** — §6 S5 ruling 3 and the `03` §5 audit row now say the carriage landed in phase 2 on `main`; the headline claim was already accurate. | +| D-7 | minor | `06` §1 row concedes an uncarried surface under a `carried` verdict | **FIXED** — the row is re-verdicted **gap (partial)** and the Mermaid / reference-projection surfaces enter the carried-gap list as item 12. `06` stays either way. | +| R-1 | minor | Ruling 7 unenforced: 7 files and one Spec carry plan/phase handles the guard cannot see | **SPLIT: SPEC FIXED, GUARD CARRIED** — the one Spec instance is repaired (`spec:decisions.concept-docs-dissolve`'s consequence is restated in timeless language, oracle updated), because a Spec carrying a session-relative handle is the case ruling 7 exists for. The seven pre-existing non-exempt sites and the guard's token list are carried on the standing **temporal-guard token assembly** docket row, which now states the choice explicitly: widen the guard and repair the sites, or narrow ruling 7's wording to what the guard enforces. Phase 3 authored none of them. | +| R-2 | minor | Commit `a989b14` subject claims a docket close that did not happen | **ACCEPTED, CARRIED** — history is not rewritten. The overstatement is corrected on the record here and in the close: no docket row was dispositioned before S6; the §8 rows are dispositioned at this close. | +| R-3 | minor | Corpus-test-granularity docket row rolled forward undispositioned; oracle grew to 3,879 lines in one `it()` | **DISPOSITIONED AT CLOSE** — carried forward with a stated reason and the measured datum, not silently: splitting the golden oracle is a single-purpose session over a file the phase deliberately kept out of scope, and doing it mid-phase would have churned the one artifact every wave's correctness claim rested on. Recorded in the plan's §7 docket close. | + +--- + +## Remediation addendum — the dispositions landed on this branch + +The remediation session ran before the phase PR rather than after it, so this review's findings do +not survive into `main` as an open docket. Every finding above carries a disposition; the +per-finding work, the re-probes, and the terminal ledgers are `plans/20-self-hosting-phase-3.md` +§6 (the S6 rulings log) and §7 (the done-record). + +- **Fixed:** P-1, P-2, B-1, D-1, D-2, D-3, D-4, D-6, D-7, D-5 (comments), R-1 (the Spec instance). +- **Record corrected, no product change:** S-2, and the two plan sentences P-1 and D-6 falsified. +- **Accepted as stated:** S-1. +- **Carried with reasons:** R-2 (immutable history), R-3 (oracle granularity), R-1's guard half and + D-5's structural half (both on named docket rows). + +Two things are worth reading before touching these surfaces again. First, **P-1's fix is +structural, not assertive**: the point was made honest by removing the competing gate from its +world (a parent with no example space) rather than by asserting the absence of the other gate's +finding — an absence assertion would have gone vacuous the day that finding id was renamed. Second, +**D-2 and D-3 were closed by enrichment, not by retraction**: the two deletions are now defensible +on the dissolution decision's own terms — carried by executable Specs — rather than on a plan +ruling that widened the criterion to include surviving docs and code. diff --git a/specs/carrier/envelope-contract.sdp.md b/specs/carrier/envelope-contract.sdp.md index 972f3b6..5fa0f3d 100644 --- a/specs/carrier/envelope-contract.sdp.md +++ b/specs/carrier/envelope-contract.sdp.md @@ -2,7 +2,7 @@ id: spec:carrier.envelope-contract kind: contract altitude: feature -readiness: defined +readiness: ready relations: refines: spec:carrier.markdown-authoring decidedBy: spec:decisions.envelope-grammar-posture @@ -14,3 +14,8 @@ relations: ## Contract - 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`. diff --git a/specs/carrier/markdown-parser.bounded-parity.sdp.md b/specs/carrier/markdown-parser.bounded-parity.sdp.md new file mode 100644 index 0000000..bd78bd2 --- /dev/null +++ b/specs/carrier/markdown-parser.bounded-parity.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:carrier.markdown-parser.bounded-parity +kind: example +altitude: story +readiness: ready +relations: + refines: spec:carrier.markdown-parser + verifies: spec:carrier.markdown-parser +--- +# One finding class is shared while the carriers' outcomes stay their own + +## Intent +- outcome: Execute one same-class row of the parity matrix, including the outcomes it never claims. + +```gwt +Given the paired carrier probes named {probe: "unrecognized-property"} +When both carriers reify their probe +Then both carriers report the finding class {findingId: "extract/unrecognized-property"} +Then the TypeScript carrier reports severity {typeScriptSeverity: "warning"} and extracts {typeScriptSpecs: 1} specs +Then the Markdown carrier reports severity {markdownSeverity: "error"} and extracts {markdownSpecs: 0} specs +``` diff --git a/specs/carrier/markdown-parser.sdp.md b/specs/carrier/markdown-parser.sdp.md index db10a09..39cd1ba 100644 --- a/specs/carrier/markdown-parser.sdp.md +++ b/specs/carrier/markdown-parser.sdp.md @@ -2,7 +2,7 @@ id: spec:carrier.markdown-parser kind: behavior altitude: feature -readiness: defined +readiness: ready relations: refines: spec:carrier.markdown-authoring dependsOn: spec:carrier.envelope-contract @@ -22,5 +22,14 @@ relations: - rule: Named non-claim — `extract/unrecognized-statement` remains distinct because Markdown owns prose and structures, not TypeScript statement recognition. - rule: Named non-claim — `extract/misplaced-authoring` remains distinct because Markdown has no executable authoring-call surface. +## Example space +```gwt-vocabulary +Given the paired carrier probes named {probe:string} +When both carriers reify their probe +Then both carriers report the finding class {findingId:string} +Then the TypeScript carrier reports severity {typeScriptSeverity:"warning"|"error"} and extracts {typeScriptSpecs:number} specs +Then the Markdown carrier reports severity {markdownSeverity:"warning"|"error"} and extracts {markdownSpecs:number} specs +``` + ## Verification — executable - `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. diff --git a/specs/carrier/prose-ownership-rule.sdp.md b/specs/carrier/prose-ownership-rule.sdp.md index 9ccc07c..4213f66 100644 --- a/specs/carrier/prose-ownership-rule.sdp.md +++ b/specs/carrier/prose-ownership-rule.sdp.md @@ -2,7 +2,7 @@ id: spec:carrier.prose-ownership-rule kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:carrier.markdown-authoring decidedBy: spec:decisions.prose-ownership @@ -13,4 +13,8 @@ relations: - outcome: Keep free prose in the graph without ambiguous attachment. ## Rule -- Narrative lives before the first H2; descriptions live only under their owning singular sections; unowned prose is refused. +- 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`. diff --git a/specs/carrier/slot-notation.refused-guess.sdp.md b/specs/carrier/slot-notation.refused-guess.sdp.md new file mode 100644 index 0000000..3ce5d22 --- /dev/null +++ b/specs/carrier/slot-notation.refused-guess.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:carrier.slot-notation.refused-guess +kind: example +altitude: story +readiness: ready +relations: + refines: spec:carrier.slot-notation + verifies: spec:carrier.slot-notation +--- +# A stray brace stays prose while an unusable group stays a named slot + +## Intent +- outcome: Execute the refuse-to-guess posture where a stray brace precedes an unparsable group. + +```gwt +Given the step text {stepText: "a stray { then {n: maybe} line items"} +When the notation parses the step text +Then the notation finds {slotCount: 1} slot groups +Then the first group has the form {form: "malformed"} and the name {slotName: "n"} +Then the step skeleton is {skeleton: "a stray { then {n} line items"} +``` diff --git a/specs/carrier/slot-notation.sdp.md b/specs/carrier/slot-notation.sdp.md new file mode 100644 index 0000000..f404487 --- /dev/null +++ b/specs/carrier/slot-notation.sdp.md @@ -0,0 +1,30 @@ +--- +id: spec:carrier.slot-notation +kind: rule +altitude: story +readiness: ready +relations: + refines: spec:carrier.markdown-authoring +--- +# Slot notation declares, binds, and refuses to guess + +## Intent +- outcome: Give step text one owned typed placeholder syntax whose normalized identity a generated contract can key on. + +## Rule +- 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`. + +## Example space +```gwt-vocabulary +Given the step text {stepText:string} +When the notation parses the step text +Then the notation finds {slotCount:number} slot groups +Then the first group has the form {form:"bare"|"typed"|"bound"|"malformed"} and the name {slotName:string} +Then the step skeleton is {skeleton:string} +``` diff --git a/specs/carrier/slot-notation.typed-declaration.sdp.md b/specs/carrier/slot-notation.typed-declaration.sdp.md new file mode 100644 index 0000000..8840789 --- /dev/null +++ b/specs/carrier/slot-notation.typed-declaration.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:carrier.slot-notation.typed-declaration +kind: example +altitude: story +readiness: ready +relations: + refines: spec:carrier.slot-notation + verifies: spec:carrier.slot-notation +--- +# A typed declaration normalizes to the skeleton its binding shares + +## Intent +- outcome: Execute the declaration form and the skeleton identity on one vocabulary step. + +```gwt +Given the step text {stepText: "a cart with {n:number} line items"} +When the notation parses the step text +Then the notation finds {slotCount: 1} slot groups +Then the first group has the form {form: "typed"} and the name {slotName: "n"} +Then the step skeleton is {skeleton: "a cart with {n} line items"} +``` diff --git a/specs/decisions/concept-docs-dissolve.sdp.md b/specs/decisions/concept-docs-dissolve.sdp.md index 2b933ed..921d740 100644 --- a/specs/decisions/concept-docs-dissolve.sdp.md +++ b/specs/decisions/concept-docs-dissolve.sdp.md @@ -15,4 +15,4 @@ relations: - 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. -- consequence: Deletion is later work, never part of phase 1. +- consequence: Deletion follows the carrying work, per document, and is never bundled into the change that lands the carrier. diff --git a/specs/extraction/example-runner.red-step-naming.sdp.md b/specs/extraction/example-runner.red-step-naming.sdp.md new file mode 100644 index 0000000..5472a02 --- /dev/null +++ b/specs/extraction/example-runner.red-step-naming.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:extraction.example-runner.red-step-naming +kind: example +altitude: story +readiness: ready +relations: + refines: spec:extraction.example-runner + verifies: spec:extraction.example-runner +--- +# A red step names itself before the assertion detail + +## Intent +- outcome: Execute the failure law where a bound handler throws inside the when step. + +```gwt +Given a contract whose given step repeats {occurrences: 2} times before one when step and one then step +Given the handler bound to the {failingPhase: "when"} step throws {thrown: "boom"} +When the bound plan runs against a fresh world +Then the run {outcome: "fails"} +Then the failure names the step in the Spec's own words as {failureLabel: "at step: When the cart is submitted"} +Then the failure preserves the original detail {detail: "boom"} +``` diff --git a/specs/extraction/example-runner.sdp.md b/specs/extraction/example-runner.sdp.md new file mode 100644 index 0000000..c07346b --- /dev/null +++ b/specs/extraction/example-runner.sdp.md @@ -0,0 +1,33 @@ +--- +id: spec:extraction.example-runner +kind: behavior +altitude: feature +readiness: ready +relations: + refines: spec:extraction.executable-contracts +--- +# A bound example runs its contract steps against a fresh world + +## 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 +- rule: 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. +- rule: Duplicate step text within one example binds one handler, and every occurrence runs that one handler with its own authored params. +- rule: 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. +- rule: 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. +- rule: 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. +- rule: The realizing entrypoints are `planExample` and `runExamplePlan` in `src/runner/index.ts`. + +## Example space +```gwt-vocabulary +Given a contract whose given step repeats {occurrences:number} times before one when step and one then step +Given the handler bound to the {failingPhase:"given"|"when"|"then"} step throws {thrown:string} +When the bound plan runs against a fresh world +Then the world records the handler trace {trace:string} +Then the run {outcome:"completes"|"fails"} +Then the failure names the step in the Spec's own words as {failureLabel:string} +Then the failure preserves the original detail {detail:string} +``` diff --git a/specs/extraction/example-runner.step-order.sdp.md b/specs/extraction/example-runner.step-order.sdp.md new file mode 100644 index 0000000..d5b6c47 --- /dev/null +++ b/specs/extraction/example-runner.step-order.sdp.md @@ -0,0 +1,20 @@ +--- +id: spec:extraction.example-runner.step-order +kind: example +altitude: story +readiness: ready +relations: + refines: spec:extraction.example-runner + verifies: spec:extraction.example-runner +--- +# A repeated step runs its one handler at each occurrence, in contract order + +## Intent +- outcome: Execute the contract-order and one-handler-per-step laws over a repeating given step. + +```gwt +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 +Then the world records the handler trace {trace: "given 2 | given 2 | when | then"} +Then the run {outcome: "completes"} +``` diff --git a/specs/extraction/excludes.refused-path.sdp.md b/specs/extraction/excludes.refused-path.sdp.md new file mode 100644 index 0000000..08d475a --- /dev/null +++ b/specs/extraction/excludes.refused-path.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:extraction.excludes.refused-path +kind: example +altitude: story +readiness: ready +relations: + refines: spec:extraction.excludes + verifies: spec:extraction.excludes +--- +# A Windows-drive absolute path is refused rather than normalized + +## Intent +- outcome: Execute the refusal rule on an exclusion that cannot name a root-relative prefix. + +```gwt +Given the extraction root carries the tree {excludedTree: "foo"} and the similar sibling {similarTree: "foobar"} +Given the consumer supplies the exclusion {exclusion: "C:/work/specs"} +When the root is discovered +Then the discovery attempt {outcome: "is refused"} +Then the refusal states {diagnostic: "normalizeExcludes: invalid exclusion path"} and names the offending path +``` diff --git a/specs/extraction/excludes.sdp.md b/specs/extraction/excludes.sdp.md index 83f35fc..58c358b 100644 --- a/specs/extraction/excludes.sdp.md +++ b/specs/extraction/excludes.sdp.md @@ -2,7 +2,7 @@ id: spec:extraction.excludes kind: rule altitude: feature -readiness: defined +readiness: ready relations: refines: spec:extraction.derive-graph decidedBy: spec:decisions.exclusion-contract @@ -16,3 +16,14 @@ relations: - 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`. + +## Example space +```gwt-vocabulary +Given the extraction root carries the tree {excludedTree:string} and the similar sibling {similarTree:string} +Given the consumer supplies the exclusion {exclusion:string} +When the root is discovered +Then the discovery attempt {outcome:"completes"|"is refused"} +Then the surviving spec carrier is {specCarrier:string} and the surviving anchor candidate is {anchorCandidate:string} +Then the refusal states {diagnostic:string} and names the offending path +``` diff --git a/specs/extraction/excludes.segment-boundary.sdp.md b/specs/extraction/excludes.segment-boundary.sdp.md new file mode 100644 index 0000000..b5d84b2 --- /dev/null +++ b/specs/extraction/excludes.segment-boundary.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:extraction.excludes.segment-boundary +kind: example +altitude: story +readiness: ready +relations: + refines: spec:extraction.excludes + verifies: spec:extraction.excludes +--- +# A prefix excludes its own tree and leaves a similar sibling standing + +## Intent +- outcome: Execute the segment-boundary rule across both discovery surfaces. + +```gwt +Given the extraction root carries the tree {excludedTree: "foo"} and the similar sibling {similarTree: "foobar"} +Given the consumer supplies the exclusion {exclusion: "foo"} +When the root is discovered +Then the discovery attempt {outcome: "completes"} +Then the surviving spec carrier is {specCarrier: "foobar/included.sdp.ts"} and the surviving anchor candidate is {anchorCandidate: "foobar/helper.ts"} +``` diff --git a/specs/extraction/executable-contracts.case-colliding-path.sdp.md b/specs/extraction/executable-contracts.case-colliding-path.sdp.md new file mode 100644 index 0000000..d621ce8 --- /dev/null +++ b/specs/extraction/executable-contracts.case-colliding-path.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:extraction.executable-contracts.case-colliding-path +kind: example +altitude: story +readiness: ready +relations: + refines: spec:extraction.executable-contracts + verifies: spec:extraction.executable-contracts +--- +# A case-only path collision withholds the whole contracts tree + +## Intent +- outcome: Execute the all-or-nothing rule where two examples claim one case-folded contract path. + +```gwt +Given a parent spec whose example space declares the slot {dimension: "n"} +Given a refining example {exampleId: "spec:probe.create-order.same-case"} whose used step {binding: "binds"} that slot +Given 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 +Then the generated tree holds {fileCount: 0} files +Then the findings name {findingId: "contracts/case-colliding-path"} +``` diff --git a/specs/extraction/executable-contracts.concreteness-refusal.sdp.md b/specs/extraction/executable-contracts.concreteness-refusal.sdp.md new file mode 100644 index 0000000..238462e --- /dev/null +++ b/specs/extraction/executable-contracts.concreteness-refusal.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:extraction.executable-contracts.concreteness-refusal +kind: example +altitude: story +readiness: ready +relations: + refines: spec:extraction.executable-contracts + verifies: spec:extraction.executable-contracts +--- +# An unbound slot in a used step earns no step contract + +## Intent +- outcome: Execute the concreteness law alone, where no shared vocabulary can withhold the contract in its place. + +```gwt +Given a parent spec that declares no shared vocabulary for the slot {dimension: "n"} +Given 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 +Then the generated tree holds {fileCount: 0} files +Then the step contract for the example is emitted: {emitted: false} +``` diff --git a/specs/extraction/executable-contracts.multi-entry-example.sdp.md b/specs/extraction/executable-contracts.multi-entry-example.sdp.md new file mode 100644 index 0000000..43ab2ef --- /dev/null +++ b/specs/extraction/executable-contracts.multi-entry-example.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:extraction.executable-contracts.multi-entry-example +kind: example +altitude: story +readiness: ready +relations: + refines: spec:extraction.executable-contracts + verifies: spec:extraction.executable-contracts +--- +# A second structured entry is named, never left silently inert + +## Intent +- outcome: Execute the one-point law where an example smuggles a second case into one document. + +```gwt +Given a parent spec whose example space declares the slot {dimension: "n"} +Given a refining example {exampleId: "spec:probe.create-order.multi"} whose used step {binding: "binds"} that slot +Given the example carries {entryCount: 2} structured entries +When the contracts are generated from the derived graph +Then the step contract for the example is emitted: {emitted: true} +Then the findings name {findingId: "contracts/multi-entry-example"} +``` diff --git a/specs/extraction/executable-contracts.sdp.md b/specs/extraction/executable-contracts.sdp.md index 3619371..369323b 100644 --- a/specs/extraction/executable-contracts.sdp.md +++ b/specs/extraction/executable-contracts.sdp.md @@ -2,7 +2,7 @@ id: spec:extraction.executable-contracts kind: behavior altitude: feature -readiness: defined +readiness: ready relations: refines: spec:extraction.build-pipeline --- @@ -14,3 +14,24 @@ relations: ## Behavior - rule: `generateContracts` derives per-example step contracts and per-parent space contracts solely from the extracted graph. - rule: A generated contract is disposable, keyed by Spec ID, and becomes unavailable when its authored example cannot bind honestly to its shared vocabulary. +- rule: 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. +- rule: 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. +- rule: 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. +- rule: 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. +- rule: A vocabulary slot group that declares no usable type is named rather than dropped in silence, and no dimension enters the space for it. +- rule: 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. +- rule: 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. +- rule: The realizing entrypoint is `generateContracts` in `src/codegen/contracts.ts`. + +## Example space +```gwt-vocabulary +Given a parent spec whose example space declares the slot {dimension:string} +Given a parent spec that declares no shared vocabulary for the slot {dimension:string} +Given a refining example {exampleId:string} whose used step {binding:"binds"|"leaves unbound"} that slot +Given the example carries {entryCount:number} structured entries +Given a case-twin example {twinId:string} whose contract path differs only by letter case +When the contracts are generated from the derived graph +Then the generated tree holds {fileCount:number} files +Then the step contract for the example is emitted: {emitted:boolean} +Then the findings name {findingId:string} +``` diff --git a/specs/extraction/schema-versioning.declared-version.sdp.md b/specs/extraction/schema-versioning.declared-version.sdp.md new file mode 100644 index 0000000..cd97fe4 --- /dev/null +++ b/specs/extraction/schema-versioning.declared-version.sdp.md @@ -0,0 +1,19 @@ +--- +id: spec:extraction.schema-versioning.declared-version +kind: example +altitude: story +readiness: ready +relations: + refines: spec:extraction.schema-versioning + verifies: spec:extraction.schema-versioning +--- +# A derived payload carries a schema version its consumer can read + +## Intent +- outcome: Execute the declared-version rule over a serialized graph payload. + +```gwt +Given a graph derived from the authored spec {specId: "spec:probe.schema-versioning"} +When the graph payload is serialized +Then the payload declares the schema version {schemaVersion: "0.4.0"} +``` diff --git a/specs/extraction/schema-versioning.sdp.md b/specs/extraction/schema-versioning.sdp.md index 2070aec..c33ffb5 100644 --- a/specs/extraction/schema-versioning.sdp.md +++ b/specs/extraction/schema-versioning.sdp.md @@ -2,7 +2,7 @@ id: spec:extraction.schema-versioning kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:extraction.derive-graph --- @@ -14,3 +14,11 @@ relations: ## Rule - 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`. + +## Example space +```gwt-vocabulary +Given a graph derived from the authored spec {specId:string} +When the graph payload is serialized +Then the payload declares the schema version {schemaVersion:string} +``` diff --git a/specs/model/anchors.lookalike-refusal.sdp.md b/specs/model/anchors.lookalike-refusal.sdp.md new file mode 100644 index 0000000..8e866df --- /dev/null +++ b/specs/model/anchors.lookalike-refusal.sdp.md @@ -0,0 +1,20 @@ +--- +id: spec:model.anchors.lookalike-refusal +kind: example +altitude: story +readiness: ready +relations: + refines: spec:model.anchors + verifies: spec:model.anchors +--- +# A consumer-local lookalike builder mints no anchor and no finding + +## Intent +- outcome: Execute the builder-trust law where a repository's own module merely resembles the Protocol builders. + +```gwt +Given a repository whose one source file builds an anchor through {builderSource: "a consumer-local lookalike module"} +When the repository is extracted +Then the extraction mints {anchorCount: 0} anchors +Then the extraction reports {findingCount: 0} findings +``` diff --git a/specs/model/anchors.physical-identity.sdp.md b/specs/model/anchors.physical-identity.sdp.md new file mode 100644 index 0000000..15af9f8 --- /dev/null +++ b/specs/model/anchors.physical-identity.sdp.md @@ -0,0 +1,20 @@ +--- +id: spec:model.anchors.physical-identity +kind: example +altitude: story +readiness: ready +relations: + refines: spec:model.anchors + verifies: spec:model.anchors +--- +# A deep relative import that resolves to the Protocol builders is trusted + +## Intent +- outcome: Execute the builder-trust law where trust turns on physical module identity rather than the import's spelling. + +```gwt +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 +Then the extraction mints {anchorCount: 1} anchors +Then the extraction reports {findingCount: 0} findings +``` diff --git a/specs/model/anchors.sdp.md b/specs/model/anchors.sdp.md index 927ece4..5cef727 100644 --- a/specs/model/anchors.sdp.md +++ b/specs/model/anchors.sdp.md @@ -2,7 +2,7 @@ id: spec:model.anchors kind: model altitude: feature -readiness: defined +readiness: ready relations: refines: spec:model.core-model decidedBy: spec:decisions.binding-not-liveness @@ -19,3 +19,12 @@ relations: - **oracle anchor** — A binding that records an oracle's models target without deriving a delivery fact. - **anchor-constant form** — The top-level const builder call that the MVP extractor reifies; decorator and JSDoc forms remain unextracted representations. - **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. +- **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`. + +## Example space +```gwt-vocabulary +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 +Then the extraction mints {anchorCount:number} anchors +Then the extraction reports {findingCount:number} findings +``` diff --git a/specs/model/stable-ids.malformed-refusal.sdp.md b/specs/model/stable-ids.malformed-refusal.sdp.md new file mode 100644 index 0000000..1d5b8a8 --- /dev/null +++ b/specs/model/stable-ids.malformed-refusal.sdp.md @@ -0,0 +1,20 @@ +--- +id: spec:model.stable-ids.malformed-refusal +kind: example +altitude: story +readiness: ready +relations: + refines: spec:model.stable-ids + verifies: spec:model.stable-ids +--- +# An uppercase namespace is refused with its reason named + +## Intent +- outcome: Execute the lowercase-namespace clause of the ID grammar. + +```gwt +Given the authored identifier {identifier: "Spec:orders.create-order"} +When the identifier is parsed +Then parsing {outcome: "is refused"} +Then the refusal names the reason {reason: "namespace must be lowercase"} +``` diff --git a/specs/model/stable-ids.namespaced-round-trip.sdp.md b/specs/model/stable-ids.namespaced-round-trip.sdp.md new file mode 100644 index 0000000..6c553a4 --- /dev/null +++ b/specs/model/stable-ids.namespaced-round-trip.sdp.md @@ -0,0 +1,20 @@ +--- +id: spec:model.stable-ids.namespaced-round-trip +kind: example +altitude: story +readiness: ready +relations: + refines: spec:model.stable-ids + verifies: spec:model.stable-ids +--- +# A namespaced dotted path with a sub-part survives parsing unchanged + +## Intent +- outcome: Execute the ID grammar on the fullest well-formed shape the model allows. + +```gwt +Given the authored identifier {identifier: "spec:orders.create-order#valid-cart"} +When the identifier is parsed +Then parsing {outcome: "resolves"} +Then reformatting the parsed parts restores {restored: "spec:orders.create-order#valid-cart"} +``` diff --git a/specs/model/stable-ids.sdp.md b/specs/model/stable-ids.sdp.md index f035291..16e22ab 100644 --- a/specs/model/stable-ids.sdp.md +++ b/specs/model/stable-ids.sdp.md @@ -2,7 +2,7 @@ id: spec:model.stable-ids kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:model.core-model --- @@ -13,5 +13,17 @@ relations: ## Rule - A Protocol ID is stable, unique, namespaced, human-readable, and the only binding between intent and code. -- An ID uses a lowercase namespace and dotted path, with an optional single `#` sub-part; referential-integrity checks reject malformed or unresolved references. +- 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`. + +## Example space +```gwt-vocabulary +Given the authored identifier {identifier:string} +When the identifier is parsed +Then parsing {outcome:"resolves"|"is refused"} +Then reformatting the parsed parts restores {restored:string} +Then the refusal names the reason {reason:string} +``` diff --git a/specs/self-hosting.pack.sdp.ts b/specs/self-hosting.pack.sdp.ts index 505ee1e..82c557a 100644 --- a/specs/self-hosting.pack.sdp.ts +++ b/specs/self-hosting.pack.sdp.ts @@ -42,6 +42,35 @@ export const selfHostingV1Pack = pack({ ref("spec:model.pack-aggregate"), ref("spec:model.anchors"), ref("spec:validation.duplicate-ids.dual-carrier"), + ref("spec:validation.warn-level-signals.orphan-signal"), + ref("spec:validation.warn-level-signals.ready-gap-signal"), + ref("spec:validation.referential-integrity.dangling-target"), + ref("spec:validation.referential-integrity.did-you-mean"), + ref("spec:validation.authored-honesty.section-authored-fact"), + ref("spec:validation.authored-honesty.unearned-stated-fact"), + ref("spec:validation.claim-separation.collapsed-edge-claim"), + ref("spec:validation.claim-separation.unratified-descriptor"), + ref("spec:validation.verification-linkage.unbound-example"), + ref("spec:validation.verification-linkage.unresolved-oracle"), + ref("spec:validation.pack-coherence.incoherent-aggregate"), + ref("spec:extraction.excludes.segment-boundary"), + ref("spec:extraction.excludes.refused-path"), + ref("spec:extraction.schema-versioning.declared-version"), + ref("spec:model.stable-ids.namespaced-round-trip"), + ref("spec:model.stable-ids.malformed-refusal"), + ref("spec:carrier.markdown-parser.bounded-parity"), + ref("spec:extraction.example-runner"), + ref("spec:extraction.example-runner.step-order"), + ref("spec:extraction.example-runner.red-step-naming"), + ref("spec:extraction.executable-contracts.concreteness-refusal"), + ref("spec:extraction.executable-contracts.multi-entry-example"), + ref("spec:extraction.executable-contracts.case-colliding-path"), + ref("spec:carrier.slot-notation"), + ref("spec:carrier.slot-notation.typed-declaration"), + ref("spec:carrier.slot-notation.refused-guess"), + ref("spec:model.anchors.lookalike-refusal"), + ref("spec:model.anchors.physical-identity"), + ref("spec:validation.two-check-families.split-report"), ref("spec:decisions.plain-language-references"), ref("spec:decisions.concept-docs-dissolve"), ref("spec:decisions.one-validation-path"), diff --git a/specs/validation/authored-honesty.sdp.md b/specs/validation/authored-honesty.sdp.md index e5fcc50..8bf785b 100644 --- a/specs/validation/authored-honesty.sdp.md +++ b/specs/validation/authored-honesty.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.authored-honesty kind: rule altitude: feature -readiness: defined +readiness: ready relations: refines: spec:validation.two-check-families --- @@ -14,3 +14,12 @@ relations: ## Rule - 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`. + +## Example space +```gwt-vocabulary +Given the graph holds a spec {specId:string} +Given 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 +Then the report names {findingId:string} at severity {severity:"warning"|"error"} +Then the finding names the fact {relatedId:string} and states {phrase:string} +``` diff --git a/specs/validation/authored-honesty.section-authored-fact.sdp.md b/specs/validation/authored-honesty.section-authored-fact.sdp.md new file mode 100644 index 0000000..1a48f49 --- /dev/null +++ b/specs/validation/authored-honesty.section-authored-fact.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.authored-honesty.section-authored-fact +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.authored-honesty + verifies: spec:validation.authored-honesty +--- +# A delivery fact smuggled into a section is refused + +## Intent +- outcome: Execute the authoring-shape refusal on a section carrier that names a derived fact. + +```gwt +Given the graph holds a spec {specId: "spec:probe.smuggled-fact"} +Given the spec hand-authors the delivery fact {factName: "implemented"} at {site: "a behavior section carrier"} +When the graph is validated +Then the report names {findingId: "honesty/authoring-shape"} at severity {severity: "error"} +Then the finding names the fact {relatedId: "implemented"} and states {phrase: "derived, never authored"} +``` diff --git a/specs/validation/authored-honesty.unearned-stated-fact.sdp.md b/specs/validation/authored-honesty.unearned-stated-fact.sdp.md new file mode 100644 index 0000000..3d52f2b --- /dev/null +++ b/specs/validation/authored-honesty.unearned-stated-fact.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.authored-honesty.unearned-stated-fact +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.authored-honesty + verifies: spec:validation.authored-honesty +--- +# A stated delivery fact no binding earns is refused + +## Intent +- outcome: Execute the delivery-fact refusal where the stated array outruns the recomputed facts. + +```gwt +Given the graph holds a spec {specId: "spec:probe.unearned-fact"} +Given the spec hand-authors the delivery fact {factName: "has-verifier"} at {site: "the node deliveryFacts array"} +When the graph is validated +Then the report names {findingId: "honesty/delivery-facts"} at severity {severity: "error"} +Then the finding names the fact {relatedId: "has-verifier"} and states {phrase: "derived, never authored"} +``` diff --git a/specs/validation/claim-separation.collapsed-edge-claim.sdp.md b/specs/validation/claim-separation.collapsed-edge-claim.sdp.md new file mode 100644 index 0000000..5f3fe81 --- /dev/null +++ b/specs/validation/claim-separation.collapsed-edge-claim.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:validation.claim-separation.collapsed-edge-claim +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.claim-separation + verifies: spec:validation.claim-separation +--- +# A binding edge cannot borrow the declared claim + +## Intent +- outcome: Execute the edge-contract law where a satisfies edge carries the authored claim. + +```gwt +Given the graph holds a spec {specId: "spec:probe.create-order"} +Given the graph carries an off-contract {element: "edge claim"} spelled {value: "declared"} +When the graph is validated +Then the report names {findingId: "conformance/claim-separation"} at severity {severity: "error"} +Then the finding message states {phrase: "never collapsed"} +Then the report holds {floorCount: 0} readiness-floor findings +``` diff --git a/specs/validation/claim-separation.sdp.md b/specs/validation/claim-separation.sdp.md index b5c7053..725cf9c 100644 --- a/specs/validation/claim-separation.sdp.md +++ b/specs/validation/claim-separation.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.claim-separation kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:validation.two-check-families --- @@ -13,4 +13,15 @@ relations: ## Rule - 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`. + +## Example space +```gwt-vocabulary +Given the graph holds a spec {specId:string} +Given the graph carries an off-contract {element:"edge claim"|"descriptor value"} spelled {value:string} +When the graph is validated +Then the report names {findingId:string} at severity {severity:"warning"|"error"} +Then the finding message states {phrase:string} +Then the report holds {floorCount:number} readiness-floor findings +``` diff --git a/specs/validation/claim-separation.unratified-descriptor.sdp.md b/specs/validation/claim-separation.unratified-descriptor.sdp.md new file mode 100644 index 0000000..67c5045 --- /dev/null +++ b/specs/validation/claim-separation.unratified-descriptor.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:validation.claim-separation.unratified-descriptor +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.claim-separation + verifies: spec:validation.claim-separation +--- +# An unratified kind fails closed instead of reaching the floor + +## Intent +- outcome: Execute the descriptor law where a foreign producer states a kind the model never ratified. + +```gwt +Given the graph holds a spec {specId: "spec:probe.create-order"} +Given the graph carries an off-contract {element: "descriptor value"} spelled {value: "saga"} +When the graph is validated +Then the report names {findingId: "conformance/claim-separation"} at severity {severity: "error"} +Then the finding message states {phrase: "outside the ratified descriptor values"} +Then the report holds {floorCount: 0} readiness-floor findings +``` diff --git a/specs/validation/pack-coherence.incoherent-aggregate.sdp.md b/specs/validation/pack-coherence.incoherent-aggregate.sdp.md new file mode 100644 index 0000000..d5126a4 --- /dev/null +++ b/specs/validation/pack-coherence.incoherent-aggregate.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.pack-coherence.incoherent-aggregate +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.pack-coherence + verifies: spec:validation.pack-coherence +--- +# A repeated member and a non-model modelRef are both named + +## Intent +- outcome: Execute both halves of the pack law against one incoherent aggregate. + +```gwt +Given a pack {packId: "pack:probe.checkout"} lists the spec {specId: "spec:probe.create-order"} {memberCount: 2} times +Given the pack also names that spec as a modelRef +When the graph is validated +Then the report names {findingId: "conformance/pack-coherence"} at severity {severity: "error"} +Then the report holds {findingCount: 2} pack-coherence findings +``` diff --git a/specs/validation/pack-coherence.sdp.md b/specs/validation/pack-coherence.sdp.md index d4d0ab3..63ea845 100644 --- a/specs/validation/pack-coherence.sdp.md +++ b/specs/validation/pack-coherence.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.pack-coherence kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:validation.two-check-families --- @@ -13,4 +13,14 @@ relations: ## Rule - 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`. + +## Example space +```gwt-vocabulary +Given a pack {packId:string} lists the spec {specId:string} {memberCount:number} times +Given the pack also names that spec as a modelRef +When the graph is validated +Then the report names {findingId:string} at severity {severity:"warning"|"error"} +Then the report holds {findingCount:number} pack-coherence findings +``` diff --git a/specs/validation/readiness-floor.sdp.md b/specs/validation/readiness-floor.sdp.md index 3f0ec6d..9ce101f 100644 --- a/specs/validation/readiness-floor.sdp.md +++ b/specs/validation/readiness-floor.sdp.md @@ -17,3 +17,6 @@ relations: ## Rule - 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. diff --git a/specs/validation/referential-integrity.dangling-target.sdp.md b/specs/validation/referential-integrity.dangling-target.sdp.md new file mode 100644 index 0000000..cbad688 --- /dev/null +++ b/specs/validation/referential-integrity.dangling-target.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.referential-integrity.dangling-target +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.referential-integrity + verifies: spec:validation.referential-integrity +--- +# An unrelated missing target is a bare conformance error + +## Intent +- outcome: Execute the unresolved-reference law where no known id is near the missing one. + +```gwt +Given the graph holds one spec {presentId: "spec:probe.create-order"} +Given the spec declares a dependsOn relation to {targetId: "spec:probe.fulfilment-policy"} +When the graph is validated +Then the report names {findingId: "conformance/referential-integrity"} at severity {severity: "error"} +Then the finding offers the nearest-id suggestion: {suggested: false} +``` diff --git a/specs/validation/referential-integrity.did-you-mean.sdp.md b/specs/validation/referential-integrity.did-you-mean.sdp.md new file mode 100644 index 0000000..713abbf --- /dev/null +++ b/specs/validation/referential-integrity.did-you-mean.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.referential-integrity.did-you-mean +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.referential-integrity + verifies: spec:validation.referential-integrity +--- +# A unique near miss earns a did-you-mean suggestion + +## Intent +- outcome: Execute the unresolved-reference law where exactly one known id is a near miss. + +```gwt +Given the graph holds one spec {presentId: "spec:probe.create-order"} +Given the spec declares a dependsOn relation to {targetId: "spec:probe.create-ordr"} +When the graph is validated +Then the report names {findingId: "conformance/referential-integrity"} at severity {severity: "error"} +Then the finding offers the nearest-id suggestion: {suggested: true} +``` diff --git a/specs/validation/referential-integrity.sdp.md b/specs/validation/referential-integrity.sdp.md index 2e3535c..e3891f5 100644 --- a/specs/validation/referential-integrity.sdp.md +++ b/specs/validation/referential-integrity.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.referential-integrity kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:validation.two-check-families --- @@ -13,4 +13,14 @@ relations: ## Rule - 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`. + +## Example space +```gwt-vocabulary +Given the graph holds one spec {presentId:string} +Given the spec declares a dependsOn relation to {targetId:string} +When the graph is validated +Then the report names {findingId:string} at severity {severity:"warning"|"error"} +Then the finding offers the nearest-id suggestion: {suggested:boolean} +``` diff --git a/specs/validation/two-check-families.sdp.md b/specs/validation/two-check-families.sdp.md index df234c3..435c625 100644 --- a/specs/validation/two-check-families.sdp.md +++ b/specs/validation/two-check-families.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.two-check-families kind: rule altitude: feature -readiness: defined +readiness: ready relations: refines: spec:protocol.self-hosting decidedBy: spec:decisions.one-validation-path @@ -17,3 +17,15 @@ relations: - 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`. + +## Example space +```gwt-vocabulary +Given the graph holds a spec {specId:string} at readiness {readiness:"idea"|"ready"} +Given the spec declares a dependsOn relation to the absent target {targetId:string} +When the graph is validated +Then the aggregate report states no family of its own +Then the conformance family reports {conformanceId:string} at severity {conformanceSeverity:"warning"|"error"} +Then the honesty family reports {honestyId:string} at severity {honestySeverity:"warning"|"error"} +``` diff --git a/specs/validation/two-check-families.split-report.sdp.md b/specs/validation/two-check-families.split-report.sdp.md new file mode 100644 index 0000000..b995617 --- /dev/null +++ b/specs/validation/two-check-families.split-report.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:validation.two-check-families.split-report +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.two-check-families + verifies: spec:validation.two-check-families +--- +# One report carries both families and claims neither as its own + +## 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. + +```gwt +Given the graph holds a spec {specId: "spec:probe.two-check-families"} at readiness {readiness: "ready"} +Given the spec declares a dependsOn relation to the absent target {targetId: "spec:probe.absent-dependency"} +When the graph is validated +Then the aggregate report states no family of its own +Then the conformance family reports {conformanceId: "conformance/referential-integrity"} at severity {conformanceSeverity: "error"} +Then the honesty family reports {honestyId: "honesty/gaps"} at severity {honestySeverity: "warning"} +``` diff --git a/specs/validation/verification-linkage.sdp.md b/specs/validation/verification-linkage.sdp.md index 048f066..890ed21 100644 --- a/specs/validation/verification-linkage.sdp.md +++ b/specs/validation/verification-linkage.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.verification-linkage kind: rule altitude: feature -readiness: defined +readiness: ready relations: refines: spec:validation.two-check-families --- @@ -13,4 +13,15 @@ relations: ## Rule - 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`. + +## Example space +```gwt-vocabulary +Given the graph holds a parent spec {parentId:string} +Given a non-resolving {verifierKind:"example spec"|"oracle anchor"} named {verifierId:string} points at it +When the graph is validated +Then the report names {findingId:string} at severity {severity:"warning"|"error"} +Then the parent earns the delivery fact has-verifier: {conferred:boolean} +``` diff --git a/specs/validation/verification-linkage.unbound-example.sdp.md b/specs/validation/verification-linkage.unbound-example.sdp.md new file mode 100644 index 0000000..4821e40 --- /dev/null +++ b/specs/validation/verification-linkage.unbound-example.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.verification-linkage.unbound-example +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.verification-linkage + verifies: spec:validation.verification-linkage +--- +# A declared verifier no test binds confers nothing + +## Intent +- outcome: Execute the verifies-linkage law where no test anchor completes the spec-to-test trace. + +```gwt +Given the graph holds a parent spec {parentId: "spec:probe.create-order"} +Given a non-resolving {verifierKind: "example spec"} named {verifierId: "spec:probe.create-order.valid-cart"} points at it +When the graph is validated +Then the report names {findingId: "conformance/verifies-linkage"} at severity {severity: "warning"} +Then the parent earns the delivery fact has-verifier: {conferred: false} +``` diff --git a/specs/validation/verification-linkage.unresolved-oracle.sdp.md b/specs/validation/verification-linkage.unresolved-oracle.sdp.md new file mode 100644 index 0000000..3da019b --- /dev/null +++ b/specs/validation/verification-linkage.unresolved-oracle.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.verification-linkage.unresolved-oracle +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.verification-linkage + verifies: spec:validation.verification-linkage +--- +# An oracle with no example space to model confers nothing + +## Intent +- outcome: Execute the oracle-linkage law where the modeled spec owns no example space. + +```gwt +Given the graph holds a parent spec {parentId: "spec:probe.order-policy"} +Given a non-resolving {verifierKind: "oracle anchor"} named {verifierId: "oracle:probe.order-policy"} points at it +When the graph is validated +Then the report names {findingId: "conformance/oracle-linkage"} at severity {severity: "error"} +Then the parent earns the delivery fact has-verifier: {conferred: false} +``` diff --git a/specs/validation/warn-level-signals.orphan-signal.sdp.md b/specs/validation/warn-level-signals.orphan-signal.sdp.md new file mode 100644 index 0000000..c1e0f16 --- /dev/null +++ b/specs/validation/warn-level-signals.orphan-signal.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.warn-level-signals.orphan-signal +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.warn-level-signals + verifies: spec:validation.warn-level-signals +--- +# A disconnected spec warns and fails nothing + +## Intent +- outcome: Execute the orphan signal on a spec no relation reaches. + +```gwt +Given the graph holds a spec {specId: "spec:probe.orphan-signal"} at readiness {readiness: "idea"} +Given the spec declares {relations: "no relation"} +When the graph is validated +Then the report names {findingId: "conformance/orphans"} at severity {severity: "warning"} +Then the report holds {errorCount: 0} errors +``` diff --git a/specs/validation/warn-level-signals.ready-gap-signal.sdp.md b/specs/validation/warn-level-signals.ready-gap-signal.sdp.md new file mode 100644 index 0000000..b60f59d --- /dev/null +++ b/specs/validation/warn-level-signals.ready-gap-signal.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:validation.warn-level-signals.ready-gap-signal +kind: example +altitude: story +readiness: ready +relations: + refines: spec:validation.warn-level-signals + verifies: spec:validation.warn-level-signals +--- +# A ready spec without a verifier warns and fails nothing + +## Intent +- outcome: Execute the gap signal on a connected ready spec no verifier resolves. + +```gwt +Given the graph holds a spec {specId: "spec:probe.gap-signal"} at readiness {readiness: "ready"} +Given the spec declares {relations: "a decidedBy decision"} +When the graph is validated +Then the report names {findingId: "honesty/gaps"} at severity {severity: "warning"} +Then the report holds {errorCount: 0} errors +``` diff --git a/specs/validation/warn-level-signals.sdp.md b/specs/validation/warn-level-signals.sdp.md index 2c24952..701016b 100644 --- a/specs/validation/warn-level-signals.sdp.md +++ b/specs/validation/warn-level-signals.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.warn-level-signals kind: rule altitude: feature -readiness: defined +readiness: ready relations: refines: spec:validation.two-check-families --- @@ -14,3 +14,12 @@ relations: ## Rule - 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`. + +## Example space +```gwt-vocabulary +Given the graph holds a spec {specId:string} at readiness {readiness:"idea"|"ready"} +Given the spec declares {relations:"no relation"|"a decidedBy decision"} +When the graph is validated +Then the report names {findingId:string} at severity {severity:"warning"|"error"} +Then the report holds {errorCount:number} errors +``` diff --git a/src/extract/derive.ts b/src/extract/derive.ts index 1b3ee7d..4a7d329 100644 --- a/src/extract/derive.ts +++ b/src/extract/derive.ts @@ -102,10 +102,10 @@ function deriveAnchorNode(entry: ReifiedAnchor): AnchorNode | CodeNode { /** * The declared + anchored layers of the one graph: one `Primitive` node per spec, one `Pack` node * per pack, one binding node per anchor (`CodeNode` for a code anchor, `Anchor` for a test anchor - * — the `03` §1 edge contract), one edge per authored relation, one derived `belongsTo` per + * — the `spec:extraction.derive-graph` edge contract), one edge per authored relation, one derived `belongsTo` per * manifest entry, and one anchored `satisfies`/`verifies` edge per anchor. `belongsTo` is a * deterministic re-expression of the declared manifest, so it inherits its source's claim — there - * is no 4th claim (`03` §3). A dangling target is emitted, not dropped: the unresolved id itself + * is no 4th claim (`spec:extraction.claim-taxonomy`). A dangling target is emitted, not dropped: the unresolved id itself * is the sentinel the referential-integrity check (`validateGraph`) flags — but resolution does * gate the delivery facts (see `computeDeliveryFacts`). Zero `inferred` claims by decision: the * consumers (the reader's entry adapters and file-level impact) resolve off the curated layers diff --git a/src/extract/index.ts b/src/extract/index.ts index 7eb7c8a..6d65f47 100644 --- a/src/extract/index.ts +++ b/src/extract/index.ts @@ -164,7 +164,8 @@ function fileParses(program: Program, source: ParsedSourceFile, findings: Findin } /** - * The extractor — the producer, the only component that reads source (`03` §1): the declared + * The extractor — the producer, the only component that reads source + * (`spec:extraction.derive-graph`): the declared * layer (spec files, pack manifests) plus the anchored layer (anchor constants in source files). * Files are reified standalone by pure AST reading (no type checker, no tsconfig dependence, no * import following — static reification without execution, MD-14); a file that does not parse is diff --git a/src/extract/reify.ts b/src/extract/reify.ts index fc96dc9..b43c6ba 100644 --- a/src/extract/reify.ts +++ b/src/extract/reify.ts @@ -29,7 +29,7 @@ import { setOwn } from "./set-own.js"; * stays out of the graph. */ /** - * The extraction finding ids, pinned. Two tiers (`03` §2), covering both authored surfaces (spec + * The extraction finding ids, pinned. Two tiers (`spec:extraction.determinism`), covering both authored surfaces (spec * files and anchor constants): envelope failures (`non-static-envelope` · `invalid-id` · * `duplicate-id` · `reserved-property`) are hard errors — the carrier is not extracted and the * build fails; content failures (`non-static-section` · `unrecognized-statement` · @@ -589,7 +589,8 @@ function containsDescription(value: unknown): boolean { /* ----- lossy reification: the section tier ----- */ interface LossyDrop { - /** The property removed — the drop unit (`03` §2: that one property, the rest survives). */ + /** The property removed — the drop unit (`spec:extraction.determinism`: that one property, the + * rest survives). */ readonly droppedPath: string; /** The deepest failing node, for the message. */ readonly failurePath: string; diff --git a/src/graph/delivery-facts.ts b/src/graph/delivery-facts.ts index 7ed8d83..71f8745 100644 --- a/src/graph/delivery-facts.ts +++ b/src/graph/delivery-facts.ts @@ -1,12 +1,13 @@ import type { DeliveryFactName, GraphEdge, GraphNode, PrimitiveNode } from "./schema.js"; /** - * Delivery facts, computed exactly per `02` §2 — derived from edges, never authored. This is the + * Delivery facts, computed exactly per `spec:model.core-model` — derived from edges, never authored. This is the * one derivation rule, shared by the extractor (which decorates `Primitive` nodes with it) and * the delivery-facts honesty check (which recomputes it over the same graph and compares) — one * derivation path, never two: * - * - `implemented` — ≥1 `satisfies` edge resolves to the spec along its `03` §1 contract row + * - `implemented` — ≥1 `satisfies` edge resolves to the spec along its + * `spec:extraction.derive-graph` contract row * (anchored, from a `CodeNode` present in the graph). A dangling or off-contract binding * confers nothing. * - `has-verifier` — an **anchored** `verifies` edge resolves to the spec along its contract row @@ -24,7 +25,8 @@ import type { DeliveryFactName, GraphEdge, GraphNode, PrimitiveNode } from "./sc * Facts are listed in ladder order (`implemented` → `has-verifier`). */ /** - * The one resolving-test-anchor rule: an anchored `verifies` edge counts only along its `03` §1 + * The one resolving-test-anchor rule: an anchored `verifies` edge counts only along its + * `spec:extraction.derive-graph` * contract row — its source resolves to an `Anchor` node present in the graph. Shared by * delivery-fact derivation, the verifies-linkage check, and the reader's enabled decode so the * three surfaces can never disagree: on a malformed or foreign graph an off-contract edge (wrong @@ -43,7 +45,7 @@ export function isResolvingTestAnchorVerify( /** * The one enabled-example rule — the declared twin of the resolving-test-anchor rule: a declared - * `verifies` edge confers the binding only along its `02` §2 contract row — its source resolves, + * `verifies` edge confers the binding only along its `spec:model.spec-sections` contract row — its source resolves, * by first carrier exactly as the graph index keys, to an `example`-kind `Primitive` that a * resolving test anchor binds. Shared by delivery-fact derivation and the reader's enabled decode * so the two conferral surfaces can never disagree — including on a duplicate-id graph, where diff --git a/src/graph/schema.ts b/src/graph/schema.ts index 8fe9980..c285f77 100644 --- a/src/graph/schema.ts +++ b/src/graph/schema.ts @@ -57,7 +57,8 @@ export interface PrimitiveNode extends GraphNodeBase { readonly specKind: SpecKind; readonly altitude: SpecAltitude; readonly readiness: SpecReadiness; - /** Degradable: a non-static title is dropped with a warning, never a hard error (`03` §2). */ + /** Degradable: a non-static title is dropped with a warning, never a hard error + * (`spec:extraction.determinism`). */ readonly title?: string; /** Owned Spec prose; Markdown carries it between the H1 title and the first H2. */ readonly narrative?: string; @@ -77,7 +78,7 @@ export interface PackNode extends GraphNodeBase { readonly title?: string; readonly framing?: string; /** - * Node data, not edges: the `03` edge contract has no `modelRefs` edge type — the + * Node data, not edges: the `spec:extraction.derive-graph` edge contract has no `modelRefs` edge type — the * pack-coherence check reads this (every entry resolves to a `model`-kind spec). */ readonly modelRefs?: readonly string[]; diff --git a/src/model/relations.ts b/src/model/relations.ts index e7cd3c9..9f27e7e 100644 --- a/src/model/relations.ts +++ b/src/model/relations.ts @@ -18,7 +18,7 @@ export interface SpecRelation readonly type: TType; /** * Authored relation targets are `spec:`-only. The base reserves `decidedBy` → `doc:` for a - * genuinely external ADR (`02` §6), but that target type is a named deferral until the need + * genuinely external ADR (`spec:model.relations`), but that target type is a named deferral until the need * arrives (MD-16) — see also the `ref()` note in `ids.ts`. */ readonly target: SpecId; diff --git a/src/notation/slots.ts b/src/notation/slots.ts index 2a8fcf0..2de5907 100644 --- a/src/notation/slots.ts +++ b/src/notation/slots.ts @@ -22,6 +22,9 @@ * dedupes ("same words, same meaning" — the ubiquitous-language bet at step level). */ +import { codeAnchorId, ref } from "../ids.js"; +import { codeAnchor } from "../model/code-anchor.js"; + export type SlotScalar = string | number | boolean; export type SlotDeclaredType = @@ -250,6 +253,14 @@ function parseGroup(group: RawGroup): SlotGroup | undefined { : { form: "bound", name, value: parsed.value, raw: group.raw }; } +const slotNotationAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.slot-notation"), + label: "parses slot groups and normalizes a step to its skeleton", + satisfies: ref("spec:carrier.slot-notation"), +}); + +void slotNotationAnchor; + /** Every slot group in one step text, in authored order. Prose braces are not returned. */ export function parseSlots(text: string): readonly SlotGroup[] { const slots: SlotGroup[] = []; diff --git a/src/reader/reader.ts b/src/reader/reader.ts index e77090a..fa63c33 100644 --- a/src/reader/reader.ts +++ b/src/reader/reader.ts @@ -338,7 +338,8 @@ function matchSections(sections: SpecSections | undefined, needle: string): read for (const [name, content] of Object.entries(sections as Record)) { // String values are content everywhere; record keys are content only in `model.terms`, where - // the keys *are* the vocabulary (`02` §3) — structural keys (`given`, `outcome`) never match. + // the keys *are* the vocabulary (`spec:model.spec-sections`) — structural keys (`given`, + // `outcome`) never match. if (containsNeedle(content, name === "model")) { matched.push(name); } diff --git a/src/runner/index.ts b/src/runner/index.ts index 527998a..3e010f4 100644 --- a/src/runner/index.ts +++ b/src/runner/index.ts @@ -1,3 +1,5 @@ +import { codeAnchorId, ref } from "../ids.js"; +import { codeAnchor } from "../model/code-anchor.js"; import { renderStepText } from "../notation/slots.js"; /** @@ -87,6 +89,14 @@ function substituteParams(skeleton: string, params: ParamShape): string { ); } +const exampleRunnerAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.example-runner"), + label: "plans and executes a bound example against the caller's world", + satisfies: ref("spec:extraction.example-runner"), +}); + +void exampleRunnerAnchor; + /** * The framework-neutral core: pairs each contract step with its bound handler, in contract order. * Duplicate step text within an example dedupes to one handler by construction — the bindings diff --git a/src/validate/readiness-floor.ts b/src/validate/readiness-floor.ts index f3f7103..1e33da6 100644 --- a/src/validate/readiness-floor.ts +++ b/src/validate/readiness-floor.ts @@ -176,7 +176,8 @@ function hasPromotedRuleOrExampleEvidence(node: PrimitiveNode, index: GraphIndex } /** - * The constrainedBy evidence slot is the promoted twin of the inline `constraints` section (`02` §3 + * The constrainedBy evidence slot is the promoted twin of the inline `constraints` section + * (`spec:model.spec-sections` * duality), so it counts only when the edge resolves in the graph to a `constraint`-kind spec that * carries its own evidence (MD-16). A dangling or wrong-kind target is not evidence. */ diff --git a/src/validate/validators.ts b/src/validate/validators.ts index 33e9568..0da3502 100644 --- a/src/validate/validators.ts +++ b/src/validate/validators.ts @@ -245,7 +245,7 @@ function checkDuplicateIds(graph: GraphSchema): readonly Finding[] { return findings; } -/* ----- conformance/claim-separation (`05` §2 check 3; the `03` §1 edge contract) ----- */ +/* ----- conformance/claim-separation (`05` §2 check 3; the `spec:extraction.derive-graph` edge contract) ----- */ const nodeTypeSet: ReadonlySet = new Set(graphNodeTypes); const claimSet: ReadonlySet = new Set(graphClaims); @@ -351,7 +351,7 @@ function checkEdgeContractRow(edge: GraphEdge, index: GraphIndex, findings: Find } }; - // The kind-typed endpoint rows (`03` §1): evaluated only where the endpoint resolves to a + // The kind-typed endpoint rows (`spec:extraction.derive-graph`): evaluated only where the endpoint resolves to a // Primitive carrying a ratified kind — a non-Primitive endpoint is the endpoint row's finding, // and an unratified kind is the descriptor check's, never a second one here. const requireSpecKind = ( @@ -419,7 +419,7 @@ function checkEdgeContractRow(edge: GraphEdge, index: GraphIndex, findings: Find toNode, "targets", ["rule", "constraint"], - "constrainedBy bounds a spec by a rule- or constraint-kind spec (a typed dependency, `02` §6)", + "constrainedBy bounds a spec by a rule- or constraint-kind spec (a typed dependency, `spec:model.relations`)", ); return; case "decidedBy": @@ -439,7 +439,7 @@ function checkEdgeContractRow(edge: GraphEdge, index: GraphIndex, findings: Find fromNode, "originates from", ["decision"], - "supersedes is permitted only on decision specs (`02` §6)", + "supersedes is permitted only on decision specs (`spec:model.relations`)", ); requireSpecKind( toNode, @@ -808,7 +808,7 @@ function checkAuthoringShape(node: PrimitiveNode, findings: Finding[]): void { /* ----- honesty/delivery-facts (`05` §2 check 6) ----- */ /** - * Delivery facts are derived, never authored (`02` §2) — and on the public graph seam a + * Delivery facts are derived, never authored (`spec:model.core-model`) — and on the public graph seam a * `Primitive` node's stated `deliveryFacts` must equal what the one derivation rule recomputes * from the graph's resolving binding edges (`computeDeliveryFacts`, shared with the extractor — * one derivation path, never two). Extractor output holds by construction; the check has teeth diff --git a/test/checkout-v1.test.ts b/test/checkout-v1.test.ts index 99f8c03..684bd9e 100644 --- a/test/checkout-v1.test.ts +++ b/test/checkout-v1.test.ts @@ -146,7 +146,8 @@ describe("checkout-v1 tracer bullet (extractor-fed)", () => { .map((node) => [node.id, node.deliveryFacts ?? []]), ); - // Two satisfies bindings + the enabled valid-cart example verifying it (`02` §2). + // Two satisfies bindings + the enabled valid-cart example verifying it + // (`spec:model.spec-sections`). expect(factsById.get("spec:orders.create-order")).toEqual(["implemented", "has-verifier"]); // The test anchor verifies the example directly — the example earns its own has-verifier. expect(factsById.get("spec:orders.create-order.valid-cart")).toEqual(["has-verifier"]); diff --git a/test/design-review.test.ts b/test/design-review.test.ts index 0e81744..a7dd7c2 100644 --- a/test/design-review.test.ts +++ b/test/design-review.test.ts @@ -426,7 +426,8 @@ describe("the Design Review — the one generated read-only view", () => { expect(validCart).toContain("the enabled verifying binding (a resolving test anchor)"); // ...and the not-enabled one over a foreign graph: a declared-claim verifies edge from an - // Anchor node is off-contract (`03` §1: a test anchor's verifies edge is `anchored`) — it + // Anchor node is off-contract (`spec:extraction.derive-graph`: a test anchor's verifies edge + // is `anchored`) — it // confers no verifier binding, and the enabled-binding line must not render beside it. const graph = deriveFixtureGraph({ specs: [ diff --git a/test/extract.test.ts b/test/extract.test.ts index f368013..ffa8829 100644 --- a/test/extract.test.ts +++ b/test/extract.test.ts @@ -620,6 +620,11 @@ describe("Markdown carrier discovery", () => { 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`.", ], }, }, @@ -663,7 +668,11 @@ describe("Markdown carrier discovery", () => { intent: { outcome: "Keep free prose in the graph without ambiguous attachment." }, behavior: { rules: [ - "Narrative lives before the first H2; descriptions live only under their owning singular sections; unowned prose is refused.", + "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`.", ], }, }, @@ -797,7 +806,7 @@ const extractContractTestAnchor = specTest({ void extractContractTestAnchor; describe("anchor extraction corpora", () => { - it("anchored-binding: the full ladder — anchored edges and delivery facts per `02` §2", () => { + it("anchored-binding: the full ladder — anchored edges and delivery facts per `spec:extraction.derive-graph`", () => { const result = extract({ root: corpusRoot("anchored-binding") }); expect(result.report.findings).toEqual([]); diff --git a/test/fixtures/extract/anchored-binding/verifying-example.sdp.ts.txt b/test/fixtures/extract/anchored-binding/verifying-example.sdp.ts.txt index e37778d..a723e2a 100644 --- a/test/fixtures/extract/anchored-binding/verifying-example.sdp.ts.txt +++ b/test/fixtures/extract/anchored-binding/verifying-example.sdp.ts.txt @@ -1,7 +1,8 @@ import { refines, spec, specId, verifies } from "@libar-dev/software-delivery-protocol"; // The enabled verifier: this example declares verifies(parent), and the test anchor in -// bindings.ts verifies *this example* — two edges, two targets, no transitive shortcut (`02` §2). +// bindings.ts verifies *this example* — two edges, two targets, no transitive shortcut +// (`spec:model.spec-sections`). export const anchoredParentExampleSpec = spec({ id: specId("spec:orders.anchored-parent.example"), title: "Example verifying the anchored parent", diff --git a/test/fixtures/extract/self-hosting-carrier/specs/carrier/envelope-contract.sdp.md.txt b/test/fixtures/extract/self-hosting-carrier/specs/carrier/envelope-contract.sdp.md.txt index 972f3b6..5fa0f3d 100644 --- a/test/fixtures/extract/self-hosting-carrier/specs/carrier/envelope-contract.sdp.md.txt +++ b/test/fixtures/extract/self-hosting-carrier/specs/carrier/envelope-contract.sdp.md.txt @@ -2,7 +2,7 @@ id: spec:carrier.envelope-contract kind: contract altitude: feature -readiness: defined +readiness: ready relations: refines: spec:carrier.markdown-authoring decidedBy: spec:decisions.envelope-grammar-posture @@ -14,3 +14,8 @@ relations: ## Contract - 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`. diff --git a/test/fixtures/extract/self-hosting-carrier/specs/carrier/prose-ownership-rule.sdp.md.txt b/test/fixtures/extract/self-hosting-carrier/specs/carrier/prose-ownership-rule.sdp.md.txt index 9ccc07c..4213f66 100644 --- a/test/fixtures/extract/self-hosting-carrier/specs/carrier/prose-ownership-rule.sdp.md.txt +++ b/test/fixtures/extract/self-hosting-carrier/specs/carrier/prose-ownership-rule.sdp.md.txt @@ -2,7 +2,7 @@ id: spec:carrier.prose-ownership-rule kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:carrier.markdown-authoring decidedBy: spec:decisions.prose-ownership @@ -13,4 +13,8 @@ relations: - outcome: Keep free prose in the graph without ambiguous attachment. ## Rule -- Narrative lives before the first H2; descriptions live only under their owning singular sections; unowned prose is refused. +- 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`. diff --git a/test/reader.test.ts b/test/reader.test.ts index f2fa57f..875995c 100644 --- a/test/reader.test.ts +++ b/test/reader.test.ts @@ -643,7 +643,8 @@ describe("the reader — the thin typed loader behind the agent surface", () => ], }); // A foreign producer points an anchored verifies edge from the CodeNode at the example — - // off-contract (`03` §1: an anchored verifies edge resolves from an Anchor node only). + // off-contract (`spec:extraction.derive-graph`: an anchored verifies edge resolves from an + // Anchor node only). const foreign: GraphSchema = { ...graph, edges: [ @@ -699,7 +700,8 @@ describe("the reader — the thin typed loader behind the agent surface", () => ], }); // A foreign producer points an inferred verifies edge from the anchor-bound example at the - // parent — off-contract (`03` §1: an example's verifies edge confers the binding only as + // parent — off-contract (`spec:extraction.derive-graph`: an example's verifies edge confers + // the binding only as // `declared`); the claim taxonomy is never collapsed into "an example, so enabled". const foreign: GraphSchema = { ...graph, diff --git a/test/self-hosting-carrier.test.ts b/test/self-hosting-carrier.test.ts new file mode 100644 index 0000000..a11bc45 --- /dev/null +++ b/test/self-hosting-carrier.test.ts @@ -0,0 +1,187 @@ +import { readFileSync } from "node:fs"; + +import { expect } from "vitest"; + +import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; + +import { boundedParityContract } from "../generated/contracts/carrier.markdown-parser.bounded-parity.contract.js"; +import { refusedGuessContract } from "../generated/contracts/carrier.slot-notation.refused-guess.contract.js"; +import { typedDeclarationContract } from "../generated/contracts/carrier.slot-notation.typed-declaration.contract.js"; +import { + parseSlots, + reifyMarkdownCarrier, + reifyTypeScriptCarrier, + stepSkeleton, +} from "../src/index.js"; +import type { CarrierReification, SlotGroup } from "../src/index.js"; + +/** + * The bound executable points of the carrier family: the ruled Markdown parser's bounded parity, + * and the slot micro-notation the carrier's fenced blocks own. The parity point reads one + * same-class row of the matrix from the paired probe carriers the matrix already owns — the claim + * under test is exactly the authored one, a shared validator ID with severity and + * extract-versus-refuse outcomes left carrier-specific, so the point asserts the agreement and the + * divergence together. The notation points parse one step text in process; no filesystem is + * involved. + * + * `test/extract-parity.test.ts` and `test/notation.test.ts` stay as regression evidence over the + * whole matrix, the four named non-claims, and every slot spelling; these points state the laws, + * never every row. + */ +const fixtureRoot = new URL("./fixtures/import/parity/", import.meta.url); + +interface ParityWorld { + probe: string; + findingId: string; + typeScript: CarrierReification | undefined; + markdown: CarrierReification | undefined; +} + +function parityWorld(): ParityWorld { + return { probe: "", findingId: "", typeScript: undefined, markdown: undefined }; +} + +function reifiedOf(world: ParityWorld, carrier: "typeScript" | "markdown"): CarrierReification { + const reification = world[carrier]; + + if (reification === undefined) { + throw new Error("The reification step must run before the carrier outcomes are asserted."); + } + + return reification; +} + +/** The severity the shared finding class carries on one carrier — the half parity never claims. */ +function severityOf(world: ParityWorld, carrier: "typeScript" | "markdown"): string | undefined { + return reifiedOf(world, carrier).findings.find( + (finding) => finding.validatorId === world.findingId, + )?.severity; +} + +const parityBindings = { + "the paired carrier probes named {probe}": ( + world: ParityWorld, + params: { readonly probe: string }, + ) => { + world.probe = params.probe; + }, + "both carriers reify their probe": (world: ParityWorld) => { + world.typeScript = reifyTypeScriptCarrier( + readFileSync(new URL(`${world.probe}.sdp.ts`, fixtureRoot), "utf8"), + `${world.probe}.sdp.ts`, + ); + world.markdown = reifyMarkdownCarrier( + readFileSync(new URL(`${world.probe}.sdp.md`, fixtureRoot), "utf8"), + `${world.probe}.sdp.md`, + ); + }, + "both carriers report the finding class {findingId}": ( + world: ParityWorld, + params: { readonly findingId: string }, + ) => { + world.findingId = params.findingId; + + for (const carrier of ["typeScript", "markdown"] as const) { + expect( + reifiedOf(world, carrier).findings.map((finding) => finding.validatorId), + carrier, + ).toContain(params.findingId); + } + }, + "the TypeScript carrier reports severity {typeScriptSeverity} and extracts {typeScriptSpecs} specs": + ( + world: ParityWorld, + params: { + readonly typeScriptSeverity: "warning" | "error"; + readonly typeScriptSpecs: number; + }, + ) => { + expect(severityOf(world, "typeScript")).toBe(params.typeScriptSeverity); + expect(reifiedOf(world, "typeScript").specs).toHaveLength(params.typeScriptSpecs); + }, + "the Markdown carrier reports severity {markdownSeverity} and extracts {markdownSpecs} specs": ( + world: ParityWorld, + params: { readonly markdownSeverity: "warning" | "error"; readonly markdownSpecs: number }, + ) => { + expect(severityOf(world, "markdown")).toBe(params.markdownSeverity); + expect(reifiedOf(world, "markdown").specs).toHaveLength(params.markdownSpecs); + }, +}; + +const boundedParityTestAnchor = specTest({ + id: testAnchorId("test:protocol.markdown-parser.bounded-parity"), + label: "the bounded-parity point verifies one shared finding class and its split outcomes", + verifies: ref("spec:carrier.markdown-parser.bounded-parity"), +}); +void boundedParityTestAnchor; + +bindExample(boundedParityContract, parityWorld, parityBindings); + +/* ----- spec:carrier.slot-notation ----- */ + +interface SlotWorld { + stepText: string; + slots: readonly SlotGroup[] | undefined; +} + +function slotWorld(): SlotWorld { + return { stepText: "", slots: undefined }; +} + +function slotsOf(world: SlotWorld): readonly SlotGroup[] { + if (world.slots === undefined) { + throw new Error("The parse step must run before the slot groups are asserted."); + } + + return world.slots; +} + +const slotNotationBindings = { + "the step text {stepText}": (world: SlotWorld, params: { readonly stepText: string }) => { + world.stepText = params.stepText; + }, + "the notation parses the step text": (world: SlotWorld) => { + world.slots = parseSlots(world.stepText); + }, + "the notation finds {slotCount} slot groups": ( + world: SlotWorld, + params: { readonly slotCount: number }, + ) => { + // Prose braces are absent by construction: the notation returns slots only. + expect(slotsOf(world)).toHaveLength(params.slotCount); + }, + "the first group has the form {form} and the name {slotName}": ( + world: SlotWorld, + params: { + readonly form: "bare" | "typed" | "bound" | "malformed"; + readonly slotName: string; + }, + ) => { + const first = slotsOf(world)[0]; + + expect(first?.form).toBe(params.form); + expect(first?.name).toBe(params.slotName); + }, + "the step skeleton is {skeleton}": (world: SlotWorld, params: { readonly skeleton: string }) => { + expect(stepSkeleton(world.stepText)).toBe(params.skeleton); + }, +}; + +const slotNotationTypedTestAnchor = specTest({ + id: testAnchorId("test:protocol.slot-notation.typed-declaration"), + label: "the declaration point verifies the typed form and its skeleton", + verifies: ref("spec:carrier.slot-notation.typed-declaration"), +}); +void slotNotationTypedTestAnchor; + +bindExample(typedDeclarationContract, slotWorld, slotNotationBindings); + +const slotNotationRefusedTestAnchor = specTest({ + id: testAnchorId("test:protocol.slot-notation.refused-guess"), + label: "the refusal point verifies prose braces and the unusable slot", + verifies: ref("spec:carrier.slot-notation.refused-guess"), +}); +void slotNotationRefusedTestAnchor; + +bindExample(refusedGuessContract, slotWorld, slotNotationBindings); diff --git a/test/self-hosting-extraction.test.ts b/test/self-hosting-extraction.test.ts new file mode 100644 index 0000000..728613b --- /dev/null +++ b/test/self-hosting-extraction.test.ts @@ -0,0 +1,562 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { 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 { refusedPathContract } from "../generated/contracts/extraction.excludes.refused-path.contract.js"; +import { segmentBoundaryContract } from "../generated/contracts/extraction.excludes.segment-boundary.contract.js"; +import { caseCollidingPathContract } from "../generated/contracts/extraction.executable-contracts.case-colliding-path.contract.js"; +import { concretenessRefusalContract } from "../generated/contracts/extraction.executable-contracts.concreteness-refusal.contract.js"; +import { multiEntryExampleContract } from "../generated/contracts/extraction.executable-contracts.multi-entry-example.contract.js"; +import { redStepNamingContract } from "../generated/contracts/extraction.example-runner.red-step-naming.contract.js"; +import { stepOrderContract } from "../generated/contracts/extraction.example-runner.step-order.contract.js"; +import { declaredVersionContract } from "../generated/contracts/extraction.schema-versioning.declared-version.contract.js"; +import { discoverFiles } from "../src/extract/discover.js"; +import type { DiscoveredFiles } from "../src/extract/discover.js"; +import { serializeGraph } from "../src/extract/serialize.js"; +import { deriveGraph, generateContracts, refines, spec, specId } from "../src/index.js"; +import type { GeneratedContracts, GraphSchema, Spec } from "../src/index.js"; +import { planExample, runExamplePlan } from "../src/runner/index.js"; +import type { ExampleContract, StepKind } from "../src/runner/index.js"; +import { deriveFixtureGraph } from "./helpers/fixture-graph.js"; + +/** + * The bound executable points of the extraction family. The exclusion law reads a real filesystem + * root, so its world is a temporary fixture tree — never the repository root, which no pooled test + * may touch. The schema-version law needs no filesystem at all: `deriveGraph` and `serializeGraph` + * are the payload seam a consumer actually reads. The contract-generation and runner laws run over + * in-memory probe graphs and in-memory contracts, so no point here touches repository-root + * `generated/`. + * + * `test/exclude-diagnostics.test.ts`, `test/graph-schema.test.ts`, `test/codegen.test.ts`, and + * `test/runner.test.ts` stay as regression evidence: these points state the laws, never every + * diagnostic spelling, degradation row, or exported contract field. + */ + +const temporaryRoots = new Set(); + +afterEach(() => { + for (const root of temporaryRoots) { + rmSync(root, { recursive: true, force: true }); + } + temporaryRoots.clear(); +}); + +/* ----- spec:extraction.excludes ----- */ + +interface ExcludeWorld { + readonly root: string; + exclusion: string; + discovered: DiscoveredFiles | undefined; + refusal: Error | undefined; +} + +function excludeWorld(): ExcludeWorld { + const root = mkdtempSync(join(tmpdir(), "sdp-self-hosting-excludes-")); + temporaryRoots.add(root); + + return { root, exclusion: "", discovered: undefined, refusal: undefined }; +} + +function discoveredOf(world: ExcludeWorld): DiscoveredFiles { + if (world.discovered === undefined) { + throw new Error("The discovery step must run before the surviving carriers are asserted."); + } + + return world.discovered; +} + +const excludeBindings = { + "the extraction root carries the tree {excludedTree} and the similar sibling {similarTree}": ( + world: ExcludeWorld, + params: { readonly excludedTree: string; readonly similarTree: string }, + ) => { + // Both trees carry both discovery surfaces — a spec carrier and an anchor candidate — so one + // exclusion is observed on each surface rather than on the declared layer alone. + for (const tree of [params.excludedTree, params.similarTree]) { + mkdirSync(join(world.root, tree), { recursive: true }); + writeFileSync( + join( + world.root, + tree, + tree === params.excludedTree ? "excluded.sdp.ts" : "included.sdp.ts", + ), + "", + "utf8", + ); + writeFileSync(join(world.root, tree, "helper.ts"), "", "utf8"); + } + }, + "the consumer supplies the exclusion {exclusion}": ( + world: ExcludeWorld, + params: { readonly exclusion: string }, + ) => { + world.exclusion = params.exclusion; + }, + "the root is discovered": (world: ExcludeWorld) => { + try { + world.discovered = discoverFiles(world.root, [world.exclusion]); + } catch (error) { + world.refusal = error instanceof Error ? error : new Error(String(error)); + } + }, + "the discovery attempt {outcome}": ( + world: ExcludeWorld, + params: { readonly outcome: "completes" | "is refused" }, + ) => { + expect(world.discovered !== undefined).toBe(params.outcome === "completes"); + expect(world.refusal !== undefined).toBe(params.outcome === "is refused"); + }, + "the surviving spec carrier is {specCarrier} and the surviving anchor candidate is {anchorCandidate}": + ( + world: ExcludeWorld, + params: { readonly specCarrier: string; readonly anchorCandidate: string }, + ) => { + const discovered = discoveredOf(world); + + expect(discovered.specFiles.map((file) => file.relativePath)).toEqual([params.specCarrier]); + expect(discovered.anchorCandidateFiles.map((file) => file.relativePath)).toEqual([ + params.anchorCandidate, + ]); + }, + "the refusal states {diagnostic} and names the offending path": ( + world: ExcludeWorld, + params: { readonly diagnostic: string }, + ) => { + const message = world.refusal?.message ?? "the exclusion refusal is missing"; + + expect(message).toContain(params.diagnostic); + expect(message).toContain(`"${world.exclusion}"`); + }, +}; + +const excludesSegmentBoundaryTestAnchor = specTest({ + id: testAnchorId("test:protocol.excludes.segment-boundary"), + label: "the segment-boundary point verifies the exact-prefix exclusion rule", + verifies: ref("spec:extraction.excludes.segment-boundary"), +}); +void excludesSegmentBoundaryTestAnchor; + +bindExample(segmentBoundaryContract, excludeWorld, excludeBindings); + +const excludesRefusedPathTestAnchor = specTest({ + id: testAnchorId("test:protocol.excludes.refused-path"), + label: "the refused-path point verifies the malformed-exclusion refusal", + verifies: ref("spec:extraction.excludes.refused-path"), +}); +void excludesRefusedPathTestAnchor; + +bindExample(refusedPathContract, excludeWorld, excludeBindings); + +/* ----- spec:extraction.schema-versioning ----- */ + +interface SchemaVersionWorld { + graph: GraphSchema | undefined; + payload: string | undefined; +} + +function schemaVersionWorld(): SchemaVersionWorld { + return { graph: undefined, payload: undefined }; +} + +function payloadOf(world: SchemaVersionWorld): Record { + if (world.payload === undefined) { + throw new Error("The serialization step must run before the payload is read."); + } + + return JSON.parse(world.payload) as Record; +} + +const schemaVersionBindings = { + "a graph derived from the authored spec {specId}": ( + world: SchemaVersionWorld, + params: { readonly specId: string }, + ) => { + world.graph = deriveGraph( + [ + { + id: params.specId, + file: "specs/probe.sdp.md", + line: 1, + data: { + id: params.specId, + title: "Probe for the declared schema version", + kind: "rule", + altitude: "story", + readiness: "idea", + }, + }, + ], + [], + [], + ); + }, + "the graph payload is serialized": (world: SchemaVersionWorld) => { + if (world.graph === undefined) { + throw new Error("The derivation step must run before the payload is serialized."); + } + + world.payload = serializeGraph(world.graph); + }, + "the payload declares the schema version {schemaVersion}": ( + world: SchemaVersionWorld, + params: { readonly schemaVersion: string }, + ) => { + expect(payloadOf(world).schemaVersion).toBe(params.schemaVersion); + }, +}; + +const schemaVersioningTestAnchor = specTest({ + id: testAnchorId("test:protocol.schema-versioning.declared-version"), + label: "the declared-version point verifies the readable payload version", + verifies: ref("spec:extraction.schema-versioning.declared-version"), +}); +void schemaVersioningTestAnchor; + +bindExample(declaredVersionContract, schemaVersionWorld, schemaVersionBindings); + +/* ----- spec:extraction.executable-contracts ----- */ + +const PROBE_PARENT_ID = "spec:probe.create-order"; + +interface ContractsWorld { + dimension: string; + parentDeclaresSpace: boolean; + exampleId: string; + binds: boolean; + entryCount: number; + twinId: string | undefined; + generated: GeneratedContracts | undefined; +} + +function contractsWorld(): ContractsWorld { + return { + dimension: "", + parentDeclaresSpace: true, + exampleId: "", + binds: true, + entryCount: 1, + twinId: undefined, + generated: undefined, + }; +} + +function generatedOf(world: ContractsWorld): GeneratedContracts { + if (world.generated === undefined) { + throw new Error("The generation step must run before the emitted tree is read."); + } + + return world.generated; +} + +/** The contract path a spec ID claims — the case-folded collision surface the law is about. */ +function contractPathOf(id: string): string { + return `${id.slice(id.indexOf(":") + 1)}.contract.ts`; +} + +/** + * The probe parent, with or without a shared vocabulary. Without one, the vocabulary gate is + * structurally absent — `resolveExampleVocabulary` reports nothing for a child whose parents own + * no example space — so a withheld step contract can only be the concreteness law's doing, and the + * point that names that law is killed by removing it and by nothing else. + */ +function probeParent(world: ContractsWorld): Spec { + if (!world.parentDeclaresSpace) { + return spec({ + id: specId(PROBE_PARENT_ID), + title: "Probe parent for contract generation", + kind: "behavior", + altitude: "feature", + readiness: "defined", + intent: { outcome: "Carry a probe example without declaring a shared vocabulary." }, + behavior: { rules: ["a submitted cart becomes an order"] }, + }); + } + + return spec({ + id: specId(PROBE_PARENT_ID), + title: "Probe parent for contract generation", + kind: "behavior", + altitude: "feature", + readiness: "defined", + intent: { outcome: "Declare one typed dimension for a probe example space." }, + behavior: { + exampleSpace: { + given: [`a customer has a cart with {${world.dimension}:number} line items`], + when: ["the customer submits the cart for order creation"], + then: ["an order is created"], + }, + }, + }); +} + +function probeExample(id: string, world: ContractsWorld): Spec { + const entries = Array.from({ length: world.entryCount }, (_, index) => ({ + given: [ + world.binds + ? `a customer has a cart with {${world.dimension}: ${String(2 + index)}} line items` + : `a customer has a cart with {${world.dimension}} line items`, + ], + when: ["the customer submits the cart for order creation"], + then: ["an order is created"], + })); + + return spec({ + id: specId(id), + title: "Probe example for contract generation", + kind: "example", + altitude: "story", + readiness: "defined", + intent: { outcome: "Bind one point against the probe example space." }, + behavior: { examples: entries }, + relations: [refines(specId(PROBE_PARENT_ID))], + }); +} + +const contractsBindings = { + "a parent spec whose example space declares the slot {dimension}": ( + world: ContractsWorld, + params: { readonly dimension: string }, + ) => { + world.dimension = params.dimension; + world.parentDeclaresSpace = true; + }, + "a parent spec that declares no shared vocabulary for the slot {dimension}": ( + world: ContractsWorld, + params: { readonly dimension: string }, + ) => { + world.dimension = params.dimension; + world.parentDeclaresSpace = false; + }, + "a refining example {exampleId} whose used step {binding} that slot": ( + world: ContractsWorld, + params: { readonly exampleId: string; readonly binding: "binds" | "leaves unbound" }, + ) => { + world.exampleId = params.exampleId; + world.binds = params.binding === "binds"; + }, + "the example carries {entryCount} structured entries": ( + world: ContractsWorld, + params: { readonly entryCount: number }, + ) => { + world.entryCount = params.entryCount; + }, + "a case-twin example {twinId} whose contract path differs only by letter case": ( + world: ContractsWorld, + params: { readonly twinId: string }, + ) => { + world.twinId = params.twinId; + }, + "the contracts are generated from the derived graph": (world: ContractsWorld) => { + const specs = [probeParent(world), probeExample(world.exampleId, world)]; + + if (world.twinId !== undefined) { + specs.push(probeExample(world.twinId, world)); + } + + world.generated = generateContracts(deriveFixtureGraph({ specs })); + }, + "the generated tree holds {fileCount} files": ( + world: ContractsWorld, + params: { readonly fileCount: number }, + ) => { + expect(generatedOf(world).files.size).toBe(params.fileCount); + }, + "the step contract for the example is emitted: {emitted}": ( + world: ContractsWorld, + params: { readonly emitted: boolean }, + ) => { + expect(generatedOf(world).files.has(contractPathOf(world.exampleId))).toBe(params.emitted); + }, + "the findings name {findingId}": ( + world: ContractsWorld, + params: { readonly findingId: string }, + ) => { + const named = generatedOf(world).findings.filter( + (finding) => finding.validatorId === params.findingId, + ); + + expect(named.length).toBeGreaterThan(0); + // Generation findings describe what did not emit; gating is validateGraph's alone. + expect(named.every((finding) => finding.severity === "warning")).toBe(true); + }, +}; + +const concretenessRefusalTestAnchor = specTest({ + id: testAnchorId("test:protocol.executable-contracts.concreteness-refusal"), + label: "the concreteness point verifies the unbound-slot refusal", + verifies: ref("spec:extraction.executable-contracts.concreteness-refusal"), +}); +void concretenessRefusalTestAnchor; + +bindExample(concretenessRefusalContract, contractsWorld, contractsBindings); + +const multiEntryExampleTestAnchor = specTest({ + id: testAnchorId("test:protocol.executable-contracts.multi-entry-example"), + label: "the multi-entry point verifies the named second entry", + verifies: ref("spec:extraction.executable-contracts.multi-entry-example"), +}); +void multiEntryExampleTestAnchor; + +bindExample(multiEntryExampleContract, contractsWorld, contractsBindings); + +const caseCollidingPathTestAnchor = specTest({ + id: testAnchorId("test:protocol.executable-contracts.case-colliding-path"), + label: "the collision point verifies the all-or-nothing withholding", + verifies: ref("spec:extraction.executable-contracts.case-colliding-path"), +}); +void caseCollidingPathTestAnchor; + +bindExample(caseCollidingPathContract, contractsWorld, contractsBindings); + +/* ----- spec:extraction.example-runner ----- */ + +type CartStep = "a cart with {n} line items" | "the cart is submitted" | "an order is created"; + +// A type literal, deliberately not an interface: only type-alias object literals carry the +// implicit index signature the `Record` constraint needs. +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type CartStepParams = { + "a cart with {n} line items": { readonly n: number }; + "the cart is submitted": Record; + "an order is created": Record; +}; + +type CartContract = ExampleContract; + +interface RunnerWorld { + occurrences: number; + failingPhase: StepKind | undefined; + thrown: Error | undefined; + readonly trace: string[]; + failure: unknown; + completed: boolean; +} + +function runnerWorld(): RunnerWorld { + return { + occurrences: 0, + failingPhase: undefined, + thrown: undefined, + trace: [], + failure: undefined, + completed: false, + }; +} + +function cartContract(occurrences: number): CartContract { + const steps: CartContract["steps"][number][] = []; + + for (let occurrence = 0; occurrence < occurrences; occurrence += 1) { + steps.push({ kind: "given", text: "a cart with {n} line items", params: { n: 2 } }); + } + + steps.push({ kind: "when", text: "the cart is submitted", params: {} }); + steps.push({ kind: "then", text: "an order is created", params: {} }); + + return { spec: "spec:probe.cart", title: "A probe cart example", steps }; +} + +function throwWhenRed(world: RunnerWorld, phase: StepKind): void { + if (world.failingPhase === phase && world.thrown !== undefined) { + throw world.thrown; + } +} + +function failureOf(world: RunnerWorld): Error { + if (!(world.failure instanceof Error)) { + throw new Error("The run step must fail before the failure message is read."); + } + + return world.failure; +} + +const runnerBindings = { + "a contract whose given step repeats {occurrences} times before one when step and one then step": + (world: RunnerWorld, params: { readonly occurrences: number }) => { + world.occurrences = params.occurrences; + }, + "the handler bound to the {failingPhase} step throws {thrown}": ( + world: RunnerWorld, + params: { readonly failingPhase: "given" | "when" | "then"; readonly thrown: string }, + ) => { + world.failingPhase = params.failingPhase; + world.thrown = new Error(params.thrown); + }, + "the bound plan runs against a fresh world": async (world: RunnerWorld) => { + const plan = planExample( + cartContract(world.occurrences), + { + "a cart with {n} line items": (running, params) => { + throwWhenRed(running, "given"); + running.trace.push(`given ${String(params.n)}`); + }, + "the cart is submitted": (running) => { + throwWhenRed(running, "when"); + running.trace.push("when"); + }, + "an order is created": (running) => { + throwWhenRed(running, "then"); + running.trace.push("then"); + }, + }, + ); + + try { + await runExamplePlan(plan, world); + world.completed = true; + } catch (error) { + world.failure = error; + } + }, + "the world records the handler trace {trace}": ( + world: RunnerWorld, + params: { readonly trace: string }, + ) => { + expect(world.trace.join(" | ")).toBe(params.trace); + }, + "the run {outcome}": ( + world: RunnerWorld, + params: { readonly outcome: "completes" | "fails" }, + ) => { + expect(world.completed).toBe(params.outcome === "completes"); + expect(world.failure !== undefined).toBe(params.outcome === "fails"); + }, + "the failure names the step in the Spec's own words as {failureLabel}": ( + world: RunnerWorld, + params: { readonly failureLabel: string }, + ) => { + expect(failureOf(world).message.startsWith(params.failureLabel)).toBe(true); + }, + "the failure preserves the original detail {detail}": ( + world: RunnerWorld, + params: { readonly detail: string }, + ) => { + // The original error survives as the thrown object, so an assertion renderer still reads its + // own fields off it; the step name is a prefix, never a replacement. + expect(failureOf(world)).toBe(world.thrown); + expect(failureOf(world).message).toContain(params.detail); + }, +}; + +const exampleRunnerStepOrderTestAnchor = specTest({ + id: testAnchorId("test:protocol.example-runner.step-order"), + label: "the step-order point verifies contract order and the one handler per step", + verifies: ref("spec:extraction.example-runner.step-order"), +}); +void exampleRunnerStepOrderTestAnchor; + +bindExample(stepOrderContract, runnerWorld, runnerBindings); + +const exampleRunnerRedStepTestAnchor = specTest({ + id: testAnchorId("test:protocol.example-runner.red-step-naming"), + label: "the red-step point verifies the self-naming failure law", + verifies: ref("spec:extraction.example-runner.red-step-naming"), +}); +void exampleRunnerRedStepTestAnchor; + +bindExample(redStepNamingContract, runnerWorld, runnerBindings); diff --git a/test/self-hosting-graph.test.ts b/test/self-hosting-graph.test.ts index f591157..67534eb 100644 --- a/test/self-hosting-graph.test.ts +++ b/test/self-hosting-graph.test.ts @@ -33,7 +33,7 @@ const expectedSpecs = [ id: "spec:carrier.envelope-contract", specKind: "contract", altitude: "feature", - readiness: "defined", + readiness: "ready", file: "specs/carrier/envelope-contract.sdp.md", title: "The Markdown envelope is explicit and bounded", narrative: null, @@ -44,6 +44,11 @@ const expectedSpecs = [ 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`.", ], }, }, @@ -53,7 +58,7 @@ const expectedSpecs = [ id: "spec:carrier.markdown-parser", specKind: "behavior", altitude: "feature", - readiness: "defined", + readiness: "ready", file: "specs/carrier/markdown-parser.sdp.md", title: "The product parser reifies the ruled Markdown subset", narrative: null, @@ -72,6 +77,15 @@ const expectedSpecs = [ "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", @@ -82,6 +96,35 @@ const expectedSpecs = [ }, 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", @@ -150,7 +193,7 @@ const expectedSpecs = [ id: "spec:carrier.prose-ownership-rule", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/carrier/prose-ownership-rule.sdp.md", title: "Every prose edge has one owner", narrative: null, @@ -158,7 +201,11 @@ const expectedSpecs = [ intent: { outcome: "Keep free prose in the graph without ambiguous attachment." }, behavior: { rules: [ - "Narrative lives before the first H2; descriptions live only under their owning singular sections; unowned prose is refused.", + "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`.", ], }, }, @@ -240,7 +287,7 @@ const expectedSpecs = [ id: "spec:extraction.excludes", specKind: "rule", altitude: "feature", - readiness: "defined", + readiness: "ready", file: "specs/extraction/excludes.sdp.md", title: "Extraction exclusions are strict consumer input", narrative: null, @@ -254,10 +301,84 @@ const expectedSpecs = [ "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"], + 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", @@ -343,7 +464,7 @@ const expectedSpecs = [ id: "spec:extraction.schema-versioning", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/extraction/schema-versioning.sdp.md", title: "The graph declares its schema version", narrative: null, @@ -356,16 +477,52 @@ const expectedSpecs = [ 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"], + 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: "defined", + readiness: "ready", file: "specs/extraction/executable-contracts.sdp.md", title: "The build derives executable contracts from graph examples", narrative: null, @@ -378,75 +535,166 @@ const expectedSpecs = [ 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"], + deliveryFacts: ["implemented", "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", + 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: "Turn authored carriers into validated derived artifacts." }, + intent: { + outcome: + "Execute the concreteness law alone, where no shared vocabulary can withhold the contract in its place.", + }, 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.", + 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: [], + deliveryFacts: ["has-verifier"], }, { - id: "spec:validation.readiness-floor", - specKind: "rule", - altitude: "feature", + id: "spec:extraction.executable-contracts.multi-entry-example", + specKind: "example", + altitude: "story", readiness: "ready", - file: "specs/validation/readiness-floor.sdp.md", - title: "Stated readiness must clear its floor", + 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: "Refuse maturity claims that their authored evidence does not support." }, + intent: { + outcome: + "Execute the one-point law where an example smuggles a second case into one document.", + }, behavior: { - rules: [ - "A Spec may state a readiness only when every clause in that readiness floor passes.", + 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: ["implemented", "has-verifier"], + deliveryFacts: ["has-verifier"], }, { - id: "spec:validation.duplicate-ids", + 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/validation/duplicate-ids.sdp.md", - title: "Duplicate carrier IDs are excluded loudly", + file: "specs/extraction/example-runner.sdp.md", + title: "A bound example runs its contract steps against a fresh world", narrative: null, sections: { - intent: { outcome: "Prevent ambiguous authored identity from entering the graph." }, + 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: [ - "If more than one carrier declares an ID, every duplicate site receives extract/duplicate-id and no ambiguous node is derived.", + "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 {firstCarrier:string} carrier declares {specId:string}", - "a {secondCarrier:string} carrier declares {specId:string}", + "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 extraction root is read"], + when: ["the bound plan runs against a fresh world"], [["t", "hen"].join("")]: [ - "both sites report {findingId:string}", - "no graph node is emitted for {specId:string}", + "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}", ], }, }, @@ -454,26 +702,28 @@ const expectedSpecs = [ deliveryFacts: ["implemented", "has-verifier"], }, { - id: "spec:validation.duplicate-ids.dual-carrier", + id: "spec:extraction.example-runner.step-order", specKind: "example", altitude: "story", readiness: "ready", - file: "specs/validation/duplicate-ids.dual-carrier.sdp.md", - title: "TypeScript and Markdown duplicates are both refused", + 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 duplicate-ID rule across both carrier surfaces." }, + intent: { + outcome: + "Execute the contract-order and one-handler-per-step laws over a repeating given step.", + }, behavior: { examples: [ { given: [ - 'a {firstCarrier: "TypeScript"} carrier declares {specId: "spec:fixture.duplicate"}', - 'a {secondCarrier: "Markdown"} carrier declares {specId: "spec:fixture.duplicate"}', + "a contract whose given step repeats {occurrences: 2} times before one when step and one then step", ], - when: ["the extraction root is read"], + when: ["the bound plan runs against a fresh world"], [["t", "hen"].join("")]: [ - 'both sites report {findingId: "extract/duplicate-id"}', - 'no graph node is emitted for {specId: "spec:fixture.duplicate"}', + 'the world records the handler trace {trace: "given 2 | given 2 | when | then"}', + 'the run {outcome: "completes"}', ], }, ], @@ -482,32 +732,257 @@ const expectedSpecs = [ 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", + 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: "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.", - }, + 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: [], + deliveryFacts: ["has-verifier"], }, { - id: "spec:decisions.plain-language-references", - specKind: "decision", - altitude: "feature", - readiness: "defined", - file: "specs/decisions/plain-language-references.sdp.md", + 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: { @@ -541,7 +1016,9 @@ const expectedSpecs = [ 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 is later work, never part of phase 1."], + consequences: [ + "Deletion follows the carrying work, per document, and is never bundled into the change that lands the carrier.", + ], }, }, deliveryFacts: [], @@ -638,7 +1115,7 @@ const expectedSpecs = [ id: "spec:model.stable-ids", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/model/stable-ids.sdp.md", title: "Stable IDs are the Protocol's durable join key", narrative: null, @@ -650,12 +1127,78 @@ const expectedSpecs = [ 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 dotted path, with an optional single `#` sub-part; referential-integrity checks reject malformed or unresolved references.", + "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"], + 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", @@ -689,7 +1232,7 @@ const expectedSpecs = [ id: "spec:model.anchors", specKind: "model", altitude: "feature", - readiness: "defined", + readiness: "ready", file: "specs/model/anchors.sdp.md", title: "Source anchors bind code without carrying intent", narrative: null, @@ -698,6 +1241,18 @@ const expectedSpecs = [ 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": @@ -712,40 +1267,148 @@ const expectedSpecs = [ "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"], + deliveryFacts: ["implemented", "has-verifier"], }, { - id: "spec:validation.two-check-families", - specKind: "rule", - altitude: "feature", - readiness: "defined", - file: "specs/validation/two-check-families.sdp.md", - title: "Validation separates well-formedness from non-pretending", + 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: - "Keep the graph trustworthy by checking conformance and honesty without judging content quality or enforcing workflow.", + "Execute the builder-trust law where a repository's own module merely resembles the Protocol builders.", }, 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.", + 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"], + 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: "defined", + readiness: "ready", file: "specs/validation/referential-integrity.sdp.md", title: "Every graph reference resolves", narrative: null, @@ -757,105 +1420,442 @@ const expectedSpecs = [ 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: [], + deliveryFacts: ["has-verifier"], }, { - id: "spec:validation.claim-separation", - specKind: "rule", + id: "spec:validation.pack-coherence.incoherent-aggregate", + specKind: "example", altitude: "story", - readiness: "defined", - file: "specs/validation/claim-separation.sdp.md", - title: "Graph claims and contracts stay distinct", + 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: - "Preserve the graph's declared, anchored, and inferred distinctions while keeping its typed contracts lawful.", + outcome: "Execute both halves of the pack law against one incoherent aggregate.", }, behavior: { - rules: [ - "Node and edge types, claims, descriptors, and relation endpoint contracts must use their ratified forms; the claim taxonomy never collapses.", - "The realizing validator entrypoint is `checkClaimSeparation` in `src/validate/validators.ts`.", + 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: [], + deliveryFacts: ["has-verifier"], }, { - id: "spec:validation.verification-linkage", + id: "spec:validation.authored-honesty", specKind: "rule", altitude: "feature", - readiness: "defined", - file: "specs/validation/verification-linkage.sdp.md", - title: "Declared verification resolves to a performing trace", + readiness: "ready", + file: "specs/validation/authored-honesty.sdp.md", + title: "Machine truth is never authored", narrative: null, sections: { intent: { outcome: - "Keep verification relationships meaningful by requiring declared test and oracle traces to resolve to their enabled bindings.", + "Keep derived graph truth trustworthy by rejecting any authored substitute for machine-derived claims or facts.", }, 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.", - "The realizing validator entrypoints are `checkVerifiesLinkage` and `checkOracleLinkage` in `src/validate/validators.ts`.", + "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: [], + deliveryFacts: ["has-verifier"], }, { - id: "spec:validation.pack-coherence", - specKind: "rule", + id: "spec:validation.authored-honesty.section-authored-fact", + specKind: "example", altitude: "story", - readiness: "defined", - file: "specs/validation/pack-coherence.sdp.md", - title: "Packs are coherent aggregates", + 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: - "Keep review aggregates coherent without treating them as truth-bearing delivery artifacts.", + "Execute the authoring-shape refusal on a section carrier that names a derived fact.", }, behavior: { - rules: [ - "Pack membership must not repeat a Spec, and every modelRef must resolve to a model-kind Spec.", - "The realizing validator entrypoint is `checkPackCoherence` in `src/validate/validators.ts`.", + 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: [], + deliveryFacts: ["has-verifier"], }, { - id: "spec:validation.authored-honesty", - specKind: "rule", - altitude: "feature", - readiness: "defined", - file: "specs/validation/authored-honesty.sdp.md", - title: "Machine truth is never authored", + 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: - "Keep derived graph truth trustworthy by rejecting any authored substitute for machine-derived claims or facts.", + "Execute the delivery-fact refusal where the stated array outruns the recomputed 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`.", + 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: [], + deliveryFacts: ["has-verifier"], }, { id: "spec:validation.warn-level-signals", specKind: "rule", altitude: "feature", - readiness: "defined", + readiness: "ready", file: "specs/validation/warn-level-signals.sdp.md", title: "Missing connective evidence warns without failing", narrative: null, @@ -869,9 +1869,78 @@ const expectedSpecs = [ "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: [], + 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", @@ -1535,6 +2604,35 @@ const expectedPackMembers = [ "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", @@ -1566,6 +2664,8 @@ const expectedDeclaredRelations = [ ["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"], @@ -1584,10 +2684,64 @@ const expectedDeclaredRelations = [ ["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"], @@ -1599,11 +2753,121 @@ const expectedDeclaredRelations = [ ["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"], @@ -1620,6 +2884,10 @@ const expectedDeclaredRelations = [ ["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"], @@ -1645,6 +2913,20 @@ const expectedDeclaredRelations = [ ["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 expectedWarnings = [] as const; @@ -1800,6 +3082,176 @@ const expectedAnchors = [ 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", @@ -2010,6 +3462,126 @@ const expectedAnchors = [ 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 lineContaining(source: string, token: string): number { @@ -2041,7 +3613,7 @@ describe("the self-hosting corpus", () => { subjectId, })), ).toEqual(expectedWarnings); - expect(result.counts).toEqual({ specs: 58, packs: 1, anchors: 36 }); + expect(result.counts).toEqual({ specs: 87, packs: 1, anchors: 65 }); expect(nodeIds).toEqual( [ "pack:self-hosting-v1", @@ -2103,10 +3675,39 @@ describe("the self-hosting corpus", () => { "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", ...expectedAnchors.map((anchor) => anchor.id), ].sort(), ); - expect(result.graph.nodes).toHaveLength(95); + expect(result.graph.nodes).toHaveLength(153); expect( primitiveNodes .map((node) => ({ @@ -2150,7 +3751,7 @@ describe("the self-hosting corpus", () => { }), {}, ), - ).toEqual({ defined: 51, ready: 7 }); + ).toEqual({ defined: 36, ready: 51 }); expect( result.graph.edges .filter((edge) => edge.type === "belongsTo") @@ -2165,7 +3766,7 @@ 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(180); + expect(result.graph.edges).toHaveLength(294); expect( result.graph.edges .filter((edge) => edge.claim === "anchored") diff --git a/test/self-hosting-model.test.ts b/test/self-hosting-model.test.ts new file mode 100644 index 0000000..9379381 --- /dev/null +++ b/test/self-hosting-model.test.ts @@ -0,0 +1,246 @@ +import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { afterAll, expect } from "vitest"; + +import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; + +import { lookalikeRefusalContract } from "../generated/contracts/model.anchors.lookalike-refusal.contract.js"; +import { physicalIdentityContract } from "../generated/contracts/model.anchors.physical-identity.contract.js"; +import { malformedRefusalContract } from "../generated/contracts/model.stable-ids.malformed-refusal.contract.js"; +import { namespacedRoundTripContract } from "../generated/contracts/model.stable-ids.namespaced-round-trip.contract.js"; +import { extract, formatId, parseId } from "../src/index.js"; +import type { ExtractionResult, IdParts } from "../src/index.js"; + +/** + * The bound executable points of the ID grammar: two representative shapes — the fullest + * well-formed identifier the model allows, and one refusal that names its reason. The `it.each` + * tables in `test/ids.test.ts` stay as regression evidence over every accepted and rejected + * spelling; a law is converted, never a table row. + * + * Beside them, the anchor-trust points: builder trust is physical module identity, so a + * consumer-local lookalike mints nothing while a deep relative import that resolves to this + * package's own builder modules is trusted. `test/anchor-trust.test.ts` stays as regression + * evidence over the package-import surface and the graph shapes each case produces. + */ +interface IdentifierWorld { + identifier: string; + parsed: IdParts | undefined; + refusal: Error | undefined; +} + +function identifierWorld(): IdentifierWorld { + return { identifier: "", parsed: undefined, refusal: undefined }; +} + +const stableIdBindings = { + "the authored identifier {identifier}": ( + world: IdentifierWorld, + params: { readonly identifier: string }, + ) => { + world.identifier = params.identifier; + }, + "the identifier is parsed": (world: IdentifierWorld) => { + try { + world.parsed = parseId(world.identifier); + } catch (error) { + world.refusal = error instanceof Error ? error : new Error(String(error)); + } + }, + "parsing {outcome}": ( + world: IdentifierWorld, + params: { readonly outcome: "resolves" | "is refused" }, + ) => { + expect(world.parsed !== undefined).toBe(params.outcome === "resolves"); + expect(world.refusal !== undefined).toBe(params.outcome === "is refused"); + }, + "reformatting the parsed parts restores {restored}": ( + world: IdentifierWorld, + params: { readonly restored: string }, + ) => { + if (world.parsed === undefined) { + throw new Error("The parse step must resolve before the parts are reformatted."); + } + + expect(formatId(world.parsed)).toBe(params.restored); + }, + "the refusal names the reason {reason}": ( + world: IdentifierWorld, + params: { readonly reason: string }, + ) => { + const message = world.refusal?.message ?? "the identifier refusal is missing"; + + // The refusal names the offending value beside its reason: the ID is the durable join key, so + // a diagnostic that hid it would leave the broken binding unfindable. + expect(message).toContain(params.reason); + expect(message).toContain(`"${world.identifier}"`); + }, +}; + +const namespacedRoundTripTestAnchor = specTest({ + id: testAnchorId("test:protocol.stable-ids.namespaced-round-trip"), + label: "the round-trip point verifies the namespaced dotted-path grammar", + verifies: ref("spec:model.stable-ids.namespaced-round-trip"), +}); +void namespacedRoundTripTestAnchor; + +bindExample(namespacedRoundTripContract, identifierWorld, stableIdBindings); + +const malformedRefusalTestAnchor = specTest({ + id: testAnchorId("test:protocol.stable-ids.malformed-refusal"), + label: "the malformed point verifies the lowercase-namespace refusal", + verifies: ref("spec:model.stable-ids.malformed-refusal"), +}); +void malformedRefusalTestAnchor; + +bindExample(malformedRefusalContract, identifierWorld, stableIdBindings); + +/* ----- spec:model.anchors ----- */ + +const packageRoot = fileURLToPath(new URL("..", import.meta.url)); +const anchorTrustRoots: string[] = []; + +afterAll(() => { + for (const root of anchorTrustRoots.splice(0)) { + rmSync(root, { force: true, recursive: true }); + } +}); + +interface AnchorTrustWorld { + root: string; + extraction: ExtractionResult | undefined; +} + +function anchorTrustWorld(): AnchorTrustWorld { + return { root: "", extraction: undefined }; +} + +function extractionOf(world: AnchorTrustWorld): ExtractionResult { + if (world.extraction === undefined) { + throw new Error("The extraction step must run before its counts are asserted."); + } + + return world.extraction; +} + +/** The one binding source under test, written into a throwaway repository root. */ +const builderSourceSetup: Record void> = { + "a consumer-local lookalike module": (root) => { + mkdirSync(join(root, "src"), { recursive: true }); + writeFileSync(join(root, "src", "code-anchor.ts"), "export const codeAnchor = () => ({});\n"); + writeFileSync( + join(root, "src", "ids.ts"), + "export const codeAnchorId = String;\nexport const ref = String;\n", + ); + writeFileSync( + join(root, "src", "consumer.ts"), + `import { codeAnchor } from "./code-anchor.js"; +import { codeAnchorId, ref } from "./ids.js"; + +const binding = codeAnchor({ + id: codeAnchorId("impl:consumer.lookalike"), + satisfies: ref("spec:consumer.lookalike"), +}); +void binding; +`, + ); + }, + "a relative import resolving to the Protocol builder modules": (root) => { + mkdirSync(join(root, "model"), { recursive: true }); + mkdirSync(join(root, "deep", "nested"), { recursive: true }); + symlinkSync(join(packageRoot, "src", "ids.ts"), join(root, "ids.ts")); + symlinkSync( + join(packageRoot, "src", "model", "code-anchor.ts"), + join(root, "model", "code-anchor.ts"), + ); + writeFileSync( + join(root, "deep", "nested", "binding.ts"), + `import { codeAnchor } from "../../model/code-anchor.js"; +import { codeAnchorId, ref } from "../../ids.js"; + +const binding = codeAnchor({ + id: codeAnchorId("impl:protocol.deep-binding"), + satisfies: ref("spec:protocol.deep-binding"), +}); +void binding; +`, + ); + }, + // Declared in the parent's {builderSource} vocabulary but bound by no example point today — + // package-import trust is regression-covered by test/anchor-trust.test.ts. + "the published Protocol package": (root) => { + writeFileSync( + join(root, "binding.ts"), + `import { codeAnchor, codeAnchorId, ref } from "@libar-dev/software-delivery-protocol"; + +const binding = codeAnchor({ + id: codeAnchorId("impl:consumer.package-binding"), + satisfies: ref("spec:consumer.package-binding"), +}); +void binding; +`, + ); + }, +}; + +const anchorTrustBindings = { + "a repository whose one source file builds an anchor through {builderSource}": ( + world: AnchorTrustWorld, + params: { + readonly builderSource: + | "a consumer-local lookalike module" + | "a relative import resolving to the Protocol builder modules" + | "the published Protocol package"; + }, + ) => { + const setup = builderSourceSetup[params.builderSource]; + + if (setup === undefined) { + throw new Error( + `No repository shape is written for the builder source "${params.builderSource}".`, + ); + } + + world.root = mkdtempSync(join(tmpdir(), "sdp-anchor-trust-")); + anchorTrustRoots.push(world.root); + setup(world.root); + }, + "the repository is extracted": (world: AnchorTrustWorld) => { + world.extraction = extract({ root: world.root }); + }, + "the extraction mints {anchorCount} anchors": ( + world: AnchorTrustWorld, + params: { readonly anchorCount: number }, + ) => { + expect(extractionOf(world).counts.anchors).toBe(params.anchorCount); + }, + "the extraction reports {findingCount} findings": ( + world: AnchorTrustWorld, + params: { readonly findingCount: number }, + ) => { + // An untrusted builder is silent, not a diagnostic: a file that never bound to the Protocol + // is not authoring drift, so the refusal must show up as an absent anchor and nothing else. + expect(extractionOf(world).report.findings).toHaveLength(params.findingCount); + }, +}; + +const lookalikeRefusalTestAnchor = specTest({ + id: testAnchorId("test:protocol.anchors.lookalike-refusal"), + label: "the lookalike point verifies that a consumer-local builder mints nothing", + verifies: ref("spec:model.anchors.lookalike-refusal"), +}); +void lookalikeRefusalTestAnchor; + +bindExample(lookalikeRefusalContract, anchorTrustWorld, anchorTrustBindings); + +const physicalIdentityTestAnchor = specTest({ + id: testAnchorId("test:protocol.anchors.physical-identity"), + label: "the physical-identity point verifies the resolved relative builder import", + verifies: ref("spec:model.anchors.physical-identity"), +}); +void physicalIdentityTestAnchor; + +bindExample(physicalIdentityContract, anchorTrustWorld, anchorTrustBindings); diff --git a/test/self-hosting-validators.test.ts b/test/self-hosting-validators.test.ts new file mode 100644 index 0000000..17208fb --- /dev/null +++ b/test/self-hosting-validators.test.ts @@ -0,0 +1,604 @@ +import { expect } from "vitest"; + +import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; + +import { orphanSignalContract } from "../generated/contracts/validation.warn-level-signals.orphan-signal.contract.js"; +import { readyGapSignalContract } from "../generated/contracts/validation.warn-level-signals.ready-gap-signal.contract.js"; +import { sectionAuthoredFactContract } from "../generated/contracts/validation.authored-honesty.section-authored-fact.contract.js"; +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 { 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"; +import { splitReportContract } from "../generated/contracts/validation.two-check-families.split-report.contract.js"; +import { unboundExampleContract } from "../generated/contracts/validation.verification-linkage.unbound-example.contract.js"; +import { unresolvedOracleContract } from "../generated/contracts/validation.verification-linkage.unresolved-oracle.contract.js"; +import { computeDeliveryFacts, schemaVersion, validateGraph } from "../src/index.js"; +import type { + Finding, + GraphClaim, + GraphEdge, + GraphNode, + PrimitiveNode, + SpecKind, + SpecReadiness, + ValidationReport, +} from "../src/index.js"; + +/** + * The bound executable points of the validation family: each `bindExample` below runs one + * authored example spec against a probe graph the Given steps assemble in memory. The graph is + * the public validation seam, so a hand-built `GraphSchema` is the honest input class here — + * several of these laws have teeth only for a producer other than this repository's extractor. + * + * The plain-vitest suite in `test/validators.test.ts` stays as regression evidence: a bound + * contract replaces an assertion only where it honestly carries it, and these points state the + * laws, never every table row. + */ +interface ValidatorWorld { + readonly nodes: GraphNode[]; + readonly edges: GraphEdge[]; + subjectId: string; + report: ValidationReport | undefined; +} + +function validatorWorld(): ValidatorWorld { + return { nodes: [], edges: [], subjectId: "", report: undefined }; +} + +interface ProbeOptions { + readonly kind?: SpecKind; + readonly readiness?: SpecReadiness; +} + +/** A probe spec carrying its kind's evidence, so only the law under test moves the findings. */ +function probeSpec(id: string, options: ProbeOptions = {}): PrimitiveNode { + return { + id, + nodeType: "Primitive", + claim: "declared", + specKind: options.kind ?? "behavior", + altitude: "feature", + readiness: options.readiness ?? "idea", + title: `Probe for ${id}`, + file: "specs/probe.sdp.md", + sections: { + intent: { outcome: `Probe one validator law at "${id}".` }, + behavior: { rules: ["The probe carries its own kind evidence."] }, + }, + }; +} + +function validate(world: ValidatorWorld): void { + world.report = validateGraph({ schemaVersion, nodes: world.nodes, edges: world.edges }); +} + +function reportOf(world: ValidatorWorld): ValidationReport { + if (world.report === undefined) { + throw new Error("The validation step must run before the outcome is asserted."); + } + + return world.report; +} + +function findingsOf(world: ValidatorWorld, validatorId: string): readonly Finding[] { + return reportOf(world).findings.filter((finding) => finding.validatorId === validatorId); +} + +/** The shared Then step of the family: the named check fires, and only at the stated severity. */ +function namesFinding( + world: ValidatorWorld, + params: { readonly findingId: string; readonly severity: "warning" | "error" }, +): void { + const findings = findingsOf(world, params.findingId); + + expect(findings.length).toBeGreaterThan(0); + expect(findings.every((finding) => finding.severity === params.severity)).toBe(true); +} + +/* ----- spec:validation.warn-level-signals ----- */ + +const warnLevelSignalBindings = { + "the graph holds a spec {specId} at readiness {readiness}": ( + world: ValidatorWorld, + params: { readonly specId: string; readonly readiness: "idea" | "ready" }, + ) => { + world.subjectId = params.specId; + world.nodes.push(probeSpec(params.specId, { readiness: params.readiness })); + }, + "the spec declares {relations}": ( + world: ValidatorWorld, + params: { readonly relations: "no relation" | "a decidedBy decision" }, + ) => { + if (params.relations === "no relation") { + return; + } + + // A decision endpoint terminates the chain honestly: the ready floor reads only dependsOn and + // refines targets, so the probe's single warning stays the one under test. + const decisionId = `${world.subjectId}-decider`; + world.nodes.push(probeSpec(decisionId, { kind: "decision" })); + world.edges.push({ + from: world.subjectId, + type: "decidedBy", + to: decisionId, + claim: "declared", + }); + }, + "the graph is validated": validate, + "the report names {findingId} at severity {severity}": ( + world: ValidatorWorld, + params: { readonly findingId: string; readonly severity: "warning" | "error" }, + ) => { + namesFinding(world, params); + expect(findingsOf(world, params.findingId).map((finding) => finding.subjectId)).toEqual([ + world.subjectId, + ]); + }, + "the report holds {errorCount} errors": ( + world: ValidatorWorld, + params: { readonly errorCount: number }, + ) => { + expect(reportOf(world).findings.filter((finding) => finding.severity === "error")).toHaveLength( + params.errorCount, + ); + }, +}; + +const warnLevelOrphanTestAnchor = specTest({ + id: testAnchorId("test:protocol.warn-level-signals.orphan-signal"), + label: "the orphan point verifies the disconnected-spec warning", + verifies: ref("spec:validation.warn-level-signals.orphan-signal"), +}); +void warnLevelOrphanTestAnchor; + +bindExample(orphanSignalContract, validatorWorld, warnLevelSignalBindings); + +const warnLevelGapTestAnchor = specTest({ + id: testAnchorId("test:protocol.warn-level-signals.ready-gap-signal"), + label: "the gap point verifies the unverified-ready warning", + verifies: ref("spec:validation.warn-level-signals.ready-gap-signal"), +}); +void warnLevelGapTestAnchor; + +bindExample(readyGapSignalContract, validatorWorld, warnLevelSignalBindings); + +/* ----- spec:validation.referential-integrity ----- */ + +const referentialIntegrityBindings = { + "the graph holds one spec {presentId}": ( + world: ValidatorWorld, + params: { readonly presentId: string }, + ) => { + world.subjectId = params.presentId; + world.nodes.push(probeSpec(params.presentId)); + }, + "the spec declares a dependsOn relation to {targetId}": ( + world: ValidatorWorld, + params: { readonly targetId: string }, + ) => { + world.edges.push({ + from: world.subjectId, + type: "dependsOn", + to: params.targetId, + claim: "declared", + }); + }, + "the graph is validated": validate, + "the report names {findingId} at severity {severity}": ( + world: ValidatorWorld, + params: { readonly findingId: string; readonly severity: "warning" | "error" }, + ) => { + namesFinding(world, params); + expect(findingsOf(world, params.findingId).map((finding) => finding.subjectId)).toEqual([ + world.subjectId, + ]); + }, + "the finding offers the nearest-id suggestion: {suggested}": ( + world: ValidatorWorld, + params: { readonly suggested: boolean }, + ) => { + const message = + findingsOf(world, "conformance/referential-integrity")[0]?.message ?? + "the referential-integrity finding is missing"; + + if (params.suggested) { + expect(message).toContain(`Did you mean "${world.subjectId}"?`); + return; + } + + expect(message).not.toContain("Did you mean"); + }, +}; + +const danglingTargetTestAnchor = specTest({ + id: testAnchorId("test:protocol.referential-integrity.dangling-target"), + label: "the dangling-target point verifies the unresolved-reference error", + verifies: ref("spec:validation.referential-integrity.dangling-target"), +}); +void danglingTargetTestAnchor; + +bindExample(danglingTargetContract, validatorWorld, referentialIntegrityBindings); + +const didYouMeanTestAnchor = specTest({ + id: testAnchorId("test:protocol.referential-integrity.did-you-mean"), + label: "the near-miss point verifies the unique did-you-mean suggestion", + verifies: ref("spec:validation.referential-integrity.did-you-mean"), +}); +void didYouMeanTestAnchor; + +bindExample(didYouMeanContract, validatorWorld, referentialIntegrityBindings); + +/* ----- spec:validation.authored-honesty ----- */ + +const authoredHonestyBindings = { + "the graph holds a spec {specId}": ( + world: ValidatorWorld, + params: { readonly specId: string }, + ) => { + world.subjectId = params.specId; + world.nodes.push(probeSpec(params.specId)); + }, + "the spec hand-authors the delivery fact {factName} at {site}": ( + world: ValidatorWorld, + params: { + readonly factName: "implemented" | "has-verifier"; + readonly site: "a behavior section carrier" | "the node deliveryFacts array"; + }, + ) => { + const node = world.nodes[world.nodes.length - 1]; + + if (node?.nodeType !== "Primitive") { + throw new Error("The spec step must run before the fact is smuggled onto it."); + } + + // Typed sections are the authoring-time guardrail; the graph is a public seam, so both + // smuggling routes are shaped here as the value data a foreign producer could hand over. + const smuggled = + params.site === "the node deliveryFacts array" + ? { ...node, deliveryFacts: [params.factName] } + : { + ...node, + sections: { + ...node.sections, + behavior: { ...node.sections?.behavior, [params.factName]: true }, + }, + }; + + world.nodes[world.nodes.length - 1] = smuggled; + }, + "the graph is validated": validate, + "the report names {findingId} at severity {severity}": ( + world: ValidatorWorld, + params: { readonly findingId: string; readonly severity: "warning" | "error" }, + ) => { + namesFinding(world, params); + expect(findingsOf(world, params.findingId).map((finding) => finding.subjectId)).toEqual([ + world.subjectId, + ]); + }, + "the finding names the fact {relatedId} and states {phrase}": ( + world: ValidatorWorld, + params: { readonly relatedId: string; readonly phrase: string }, + ) => { + const findings = reportOf(world).findings.filter( + (finding) => finding.relatedId === params.relatedId && finding.subjectId === world.subjectId, + ); + + expect(findings).toHaveLength(1); + expect(findings[0]?.message).toContain(params.phrase); + }, +}; + +const sectionAuthoredFactTestAnchor = specTest({ + id: testAnchorId("test:protocol.authored-honesty.section-authored-fact"), + label: "the section point verifies the authoring-shape refusal", + verifies: ref("spec:validation.authored-honesty.section-authored-fact"), +}); +void sectionAuthoredFactTestAnchor; + +bindExample(sectionAuthoredFactContract, validatorWorld, authoredHonestyBindings); + +const unearnedStatedFactTestAnchor = specTest({ + id: testAnchorId("test:protocol.authored-honesty.unearned-stated-fact"), + label: "the stated-fact point verifies the delivery-fact refusal", + verifies: ref("spec:validation.authored-honesty.unearned-stated-fact"), +}); +void unearnedStatedFactTestAnchor; + +bindExample(unearnedStatedFactContract, validatorWorld, authoredHonestyBindings); + +/* ----- spec:validation.claim-separation ----- */ + +const claimSeparationBindings = { + "the graph holds a spec {specId}": ( + world: ValidatorWorld, + params: { readonly specId: string }, + ) => { + world.subjectId = params.specId; + world.nodes.push(probeSpec(params.specId)); + }, + "the graph carries an off-contract {element} spelled {value}": ( + world: ValidatorWorld, + params: { + readonly element: "edge claim" | "descriptor value"; + readonly value: string; + }, + ) => { + const node = world.nodes[world.nodes.length - 1]; + + if (node?.nodeType !== "Primitive") { + throw new Error("The spec step must run before the off-contract shape is placed."); + } + + if (params.element === "descriptor value") { + world.nodes[world.nodes.length - 1] = { ...node, specKind: params.value as SpecKind }; + return; + } + + const binderId = "impl:probe.create-order-use-case"; + world.nodes.push({ + id: binderId, + nodeType: "CodeNode", + claim: "anchored", + file: "src/probe/create-order.use-case.ts", + line: 3, + }); + world.edges.push({ + from: binderId, + type: "satisfies", + to: world.subjectId, + claim: params.value as GraphClaim, + }); + }, + "the graph is validated": validate, + "the report names {findingId} at severity {severity}": namesFinding, + "the finding message states {phrase}": ( + world: ValidatorWorld, + params: { readonly phrase: string }, + ) => { + const messages = findingsOf(world, "conformance/claim-separation").map( + (finding) => finding.message, + ); + + expect(messages.filter((message) => message.includes(params.phrase))).toHaveLength(1); + }, + "the report holds {floorCount} readiness-floor findings": ( + world: ValidatorWorld, + params: { readonly floorCount: number }, + ) => { + expect(findingsOf(world, "honesty/readiness-floor")).toHaveLength(params.floorCount); + }, +}; + +const collapsedEdgeClaimTestAnchor = specTest({ + id: testAnchorId("test:protocol.claim-separation.collapsed-edge-claim"), + label: "the collapsed-claim point verifies the binding-edge contract row", + verifies: ref("spec:validation.claim-separation.collapsed-edge-claim"), +}); +void collapsedEdgeClaimTestAnchor; + +bindExample(collapsedEdgeClaimContract, validatorWorld, claimSeparationBindings); + +const unratifiedDescriptorTestAnchor = specTest({ + id: testAnchorId("test:protocol.claim-separation.unratified-descriptor"), + label: "the unratified-kind point verifies the fail-closed descriptor law", + verifies: ref("spec:validation.claim-separation.unratified-descriptor"), +}); +void unratifiedDescriptorTestAnchor; + +bindExample(unratifiedDescriptorContract, validatorWorld, claimSeparationBindings); + +/* ----- spec:validation.verification-linkage ----- */ + +const verificationLinkageBindings = { + "the graph holds a parent spec {parentId}": ( + world: ValidatorWorld, + params: { readonly parentId: string }, + ) => { + world.subjectId = params.parentId; + world.nodes.push(probeSpec(params.parentId)); + }, + "a non-resolving {verifierKind} named {verifierId} points at it": ( + world: ValidatorWorld, + params: { + readonly verifierKind: "example spec" | "oracle anchor"; + readonly verifierId: string; + }, + ) => { + if (params.verifierKind === "example spec") { + world.nodes.push(probeSpec(params.verifierId, { kind: "example" })); + world.edges.push({ + from: params.verifierId, + type: "verifies", + to: world.subjectId, + claim: "declared", + }); + return; + } + + // The oracle anchor resolves as a node and rides its contract row; what it cannot resolve + // through is the modelled spec's example space, which the parent probe never owns. + world.nodes.push({ + id: params.verifierId, + nodeType: "Anchor", + claim: "anchored", + file: "test/probe-oracle.test.ts", + line: 5, + }); + world.edges.push({ + from: params.verifierId, + type: "models", + to: world.subjectId, + claim: "anchored", + }); + }, + "the graph is validated": validate, + "the report names {findingId} at severity {severity}": namesFinding, + "the parent earns the delivery fact has-verifier: {conferred}": ( + world: ValidatorWorld, + params: { readonly conferred: boolean }, + ) => { + const derived = computeDeliveryFacts(world.nodes, world.edges).get(world.subjectId) ?? []; + + expect(derived.includes("has-verifier")).toBe(params.conferred); + }, +}; + +const unboundExampleTestAnchor = specTest({ + id: testAnchorId("test:protocol.verification-linkage.unbound-example"), + label: "the unbound-example point verifies the incomplete spec-to-test trace", + verifies: ref("spec:validation.verification-linkage.unbound-example"), +}); +void unboundExampleTestAnchor; + +bindExample(unboundExampleContract, validatorWorld, verificationLinkageBindings); + +const unresolvedOracleTestAnchor = specTest({ + id: testAnchorId("test:protocol.verification-linkage.unresolved-oracle"), + label: "the unresolved-oracle point verifies the oracle binding refusal", + verifies: ref("spec:validation.verification-linkage.unresolved-oracle"), +}); +void unresolvedOracleTestAnchor; + +bindExample(unresolvedOracleContract, validatorWorld, verificationLinkageBindings); + +/* ----- spec:validation.pack-coherence ----- */ + +const packCoherenceBindings = { + "a pack {packId} lists the spec {specId} {memberCount} times": ( + world: ValidatorWorld, + params: { + readonly packId: string; + readonly specId: string; + readonly memberCount: number; + }, + ) => { + world.subjectId = params.packId; + world.nodes.push(probeSpec(params.specId)); + world.nodes.push({ + id: params.packId, + nodeType: "Pack", + claim: "declared", + title: "Probe aggregate", + file: "specs/probe.pack.sdp.ts", + }); + + // Membership is the manifest re-expressed as belongsTo edges, one per entry — a repeated + // manifest entry is a repeated edge, which is what the coherence check counts. + for (let entry = 0; entry < params.memberCount; entry += 1) { + world.edges.push({ + from: params.specId, + type: "belongsTo", + to: params.packId, + claim: "declared", + }); + } + }, + "the pack also names that spec as a modelRef": (world: ValidatorWorld) => { + const memberId = world.edges.find((edge) => edge.type === "belongsTo")?.from; + const packIndex = world.nodes.findIndex((node) => node.id === world.subjectId); + const packNode = world.nodes[packIndex]; + + if (memberId === undefined || packNode?.nodeType !== "Pack") { + throw new Error("The pack step must run before its modelRefs are named."); + } + + world.nodes[packIndex] = { ...packNode, modelRefs: [memberId] }; + }, + "the graph is validated": validate, + "the report names {findingId} at severity {severity}": namesFinding, + "the report holds {findingCount} pack-coherence findings": ( + world: ValidatorWorld, + params: { readonly findingCount: number }, + ) => { + expect(findingsOf(world, "conformance/pack-coherence")).toHaveLength(params.findingCount); + }, +}; + +const incoherentAggregateTestAnchor = specTest({ + id: testAnchorId("test:protocol.pack-coherence.incoherent-aggregate"), + label: "the incoherent-aggregate point verifies both halves of the pack law", + verifies: ref("spec:validation.pack-coherence.incoherent-aggregate"), +}); +void incoherentAggregateTestAnchor; + +bindExample(incoherentAggregateContract, validatorWorld, packCoherenceBindings); + +/* ----- spec:validation.two-check-families ----- */ + +/** The family a finding names, read from the finding itself rather than from its id spelling. */ +function familyReports( + world: ValidatorWorld, + family: "conformance" | "honesty", + params: { readonly findingId: string; readonly severity: "warning" | "error" }, +): void { + const findings = findingsOf(world, params.findingId); + + expect(findings.length).toBeGreaterThan(0); + expect(findings.every((finding) => finding.family === family)).toBe(true); + expect(findings.every((finding) => finding.severity === params.severity)).toBe(true); +} + +const twoCheckFamilyBindings = { + "the graph holds a spec {specId} at readiness {readiness}": ( + world: ValidatorWorld, + params: { readonly specId: string; readonly readiness: "idea" | "ready" }, + ) => { + world.subjectId = params.specId; + world.nodes.push(probeSpec(params.specId, { readiness: params.readiness })); + }, + "the spec declares a dependsOn relation to the absent target {targetId}": ( + world: ValidatorWorld, + params: { readonly targetId: string }, + ) => { + world.edges.push({ + from: world.subjectId, + type: "dependsOn", + to: params.targetId, + claim: "declared", + }); + }, + "the graph is validated": validate, + "the aggregate report states no family of its own": (world: ValidatorWorld) => { + // The aggregate spans both families, so it never mislabels itself with a single one; each + // finding names its own family, and only ever one of the two ratified ones. + expect(reportOf(world).family).toBeUndefined(); + expect([...new Set(reportOf(world).findings.map((finding) => finding.family))].sort()).toEqual([ + "conformance", + "honesty", + ]); + }, + "the conformance family reports {conformanceId} at severity {conformanceSeverity}": ( + world: ValidatorWorld, + params: { + readonly conformanceId: string; + readonly conformanceSeverity: "warning" | "error"; + }, + ) => { + familyReports(world, "conformance", { + findingId: params.conformanceId, + severity: params.conformanceSeverity, + }); + }, + "the honesty family reports {honestyId} at severity {honestySeverity}": ( + world: ValidatorWorld, + params: { readonly honestyId: string; readonly honestySeverity: "warning" | "error" }, + ) => { + familyReports(world, "honesty", { + findingId: params.honestyId, + severity: params.honestySeverity, + }); + }, +}; + +const splitReportTestAnchor = specTest({ + id: testAnchorId("test:protocol.two-check-families.split-report"), + label: "the split-report point verifies both families in one aggregate report", + verifies: ref("spec:validation.two-check-families.split-report"), +}); +void splitReportTestAnchor; + +bindExample(splitReportContract, validatorWorld, twoCheckFamilyBindings); diff --git a/test/validators.test.ts b/test/validators.test.ts index 957ea23..1e7a07c 100644 --- a/test/validators.test.ts +++ b/test/validators.test.ts @@ -555,7 +555,8 @@ describe("graph validators", () => { [parent, example, impostor], [ { from: example.id, type: "verifies", to: parent.id, claim: "declared" }, - // Off-contract: an anchored verifies edge resolves from an Anchor node only (`03` §1). + // Off-contract: an anchored verifies edge resolves from an Anchor node only + // (`spec:extraction.derive-graph`). { from: impostor.id, type: "verifies", to: example.id, claim: "anchored" }, ], ), diff --git a/vitest-test.mjs b/vitest-test.mjs index 26ad18f..bf51297 100644 --- a/vitest-test.mjs +++ b/vitest-test.mjs @@ -8,16 +8,25 @@ 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", - testPath: "test/self-hosting-duplicate-ids.test.ts", + 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", - testPath: "examples/checkout-v1/test/orders/create-order.valid-cart.test.ts", + testPaths: ["examples/checkout-v1/test/orders/create-order.valid-cart.test.ts"], }, ]; @@ -46,7 +55,9 @@ const matchesPathFilter = (testPath, filter) => const selectsCliTest = pathFilters.some((filter) => matchesPathFilter(cliTestPath, filter)); const requiredContracts = hasPathFilter ? contractDependencies.filter((dependency) => - pathFilters.some((filter) => matchesPathFilter(dependency.testPath, filter)), + dependency.testPaths.some((testPath) => + pathFilters.some((filter) => matchesPathFilter(testPath, filter)), + ), ) : contractDependencies; const missingContracts = requiredContracts.filter(