diff --git a/.agents/skills/sdp-agent-surface/SKILL.md b/.agents/skills/sdp-agent-surface/SKILL.md index 35a688cb..1bee27ff 100644 --- a/.agents/skills/sdp-agent-surface/SKILL.md +++ b/.agents/skills/sdp-agent-surface/SKILL.md @@ -14,7 +14,9 @@ evaluation sink. There is no verb wall — you script the graph. ## The shape of the graph The graph is flat: one array of nodes, one array of edges, nothing nested. Hierarchy is edges, so -every question is a filter or a join, never a tree walk. Four node types exist: +every question is a filter or a join, never a tree walk. Beside them, `graph.locations` is the +location table: the source line of every keyed Design and UI entry and every open question. Nodes +carry no lines, and a location is never an identity or a binding target. Four node types exist: - `Primitive` — one authored Spec, positioned by `specKind` × `altitude` × `readiness`, carrying its title, narrative, reified section content, and the derived `deliveryFacts`. Use case, NFR, @@ -96,12 +98,22 @@ The public projection publishers are `sdp view`, `sdp census`, `sdp mermaid`, an In this source checkout, use `npm run generate:self-hosting` or `npm run check:self-hosting` when all four roots must be published or certified together. -The catalog contains twenty-eight ready-made bodies in `docs/agent-surface/recipes.md` in the +The catalog contains thirty-two ready-made bodies in `docs/agent-surface/recipes.md` in the Protocol repository and `node_modules/@libar-dev/software-delivery-protocol/docs/agent-surface/recipes.md` in an adopter. -Recipes 1-28 each open under a numbered heading that names the recipe. Every body there runs +Recipes 1-32 each open under a numbered heading that names the recipe. Every body there runs verbatim and a test proves it. Start from a recipe; adapt it in place. +A recipe that takes a subject (a Spec id, a Pack id, changed files, addresses, a term) reads it +from `params.`, and the catalog names the parameter. Pass it as JSON data with `--params`, +never by editing the body; `--params @PATH` reads the JSON from a file. The build also ships each +body as `dist/recipes/-.js`, so run it as shipped: + +```sh +pnpm --silent sdp:q "$(cat dist/recipes/03-what-does-this-spec-guarantee-and-who-verifies-it.js)" --params '{"spec":"spec:consumers.reader"}' --json +pnpm exec sdp q "$(cat node_modules/@libar-dev/software-delivery-protocol/dist/recipes/04-what-breaks-if-i-change-these-files.js)" --params @changed-files.json --json +``` + For structural questions, use component membership, uses fan-in and fan-out, structural neighborhood, census structural coverage, and the projection-coverage upper bound (recipes 12-16). For architecture questions, use the architecture map to see components and their shaping decisions @@ -124,9 +136,11 @@ keyed entry of a Spec's Design or UI section, and `spec:#question.` nam question whose marker carries that key; it belongs in prose, and `sdp validate` reports an address whose Spec or key does not exist as an error. When you hold addresses written outside the Specs, in a register, a test, or a page, address resolution (recipe 25) gives one row per -address: whether it resolves and, when it does not, why. When you hold a key or a term and need -the entry that carries it, use entry search (recipe 23): it matches whole tokens and names the -entry, where concept search stops at the section. It also gives the entry's address when the entry is a +address: whether it resolves and, when it does not, why; a resolving row carries the entry's +value, file, and line. When you hold a key or a term and need the entry that carries it, use entry +search (recipe 23): it matches whole tokens and names the entry and its line, where concept search +stops at the section. Both read lines from the location table, and a null line means not located, +never absent. Entry search also gives the entry's address when the entry is a top-level Design or UI key that starts with a lowercase ASCII letter and goes on in ASCII letters and digits, other than `description`, and when the entry is the text of an open question that carries a key. Every other entry, a lawful `"01"` key included, gets `address: null`. When a @@ -136,14 +150,27 @@ the key and the span content as authored. To see where Specs rest on each other through `dependsOn`, run dependency cycles (recipe 26): it lists each such set with one closed path through it, and reports without refusing. +For design work, run Pack design (recipe 29): per member, the stated next rung with the clauses +that hold the member below it, the design columns, open questions with addresses and lines, +decisions, and the realizing and referencing units with their components, plus the Pack's +boundary. The Design Review's Pack page renders the same assembly. Before changing a Spec, +design-change impact (recipe 30) lists every Spec that rests on it, transitively, with the units, +verifiers, and Packs to follow. The decision register (recipe 31) lists every decision with its +rung, its supersession both ways, the decisions it narrows and those that narrow it through +`refines`, the Specs it shapes, and its keyed questions. Architecture +crossings (recipe 32) lists the `uses` edges whose components differ in context or layer, tallied +by direction, and refuses nothing. + Reach for the files only when you need the authored prose itself — the exact words to edit. ## The entry adapters `g.specContext(id)` lists the units that realize the Spec under `implementations` and the units -that answer to it under `references`, each with its file and line. `g.byFile(path)` returns every -node recorded at a path and the Specs reachable from it. A comment-form anchor is recorded at its -own site, so a source file with no Protocol import still answers. A node may come back with an +that answer to it under `references`, each with its file, line, and component, and its rows of the +location table under `entryLocations`. `g.packContext(id)` carries the Pack design: each member's +stated next rung, design columns, and bindings, and the Pack's boundary. `g.byFile(path)` returns +every node recorded at a path and the Specs reachable from it. A comment-form anchor is recorded +at its own site, so a source file with no Protocol import still answers. A node may come back with an empty Spec list: an identity-only anchor, with no `satisfies` and no `references`, is lawful and binds no Spec, and the empty list is its honest answer, not a failed lookup. `g.blastRadius(files)` follows `satisfies`, `references`, `verifies`, and oracle `models` from the @@ -153,13 +180,15 @@ list implies coverage. ## The contract -`sdp q [''] [--root PATH] [--exclude PATH]... [--json]` +`sdp q [''] [--root PATH] [--exclude PATH]... [--params JSON | --params @PATH] [--json]` -Three bindings are injected: +Four bindings are injected: - `g` — the reader over the derived graph (the same `createReader` the package exports) - `graph` — the raw graph schema (nodes, edges, claims) - `report` — the validation report, so findings are queryable data, never a gate +- `params` — the JSON object `--params` supplies, or `{}` without it; a value that is not a JSON + object, or a file that cannot be read, is refused before the body runs Body rules: a plain JavaScript **async function body**. No `import`/`export`, no TypeScript-only syntax; `await` is fine. `return` is the machine output contract, but `sdp q` does not suppress diff --git a/.agents/skills/sdp-authoring/SKILL.md b/.agents/skills/sdp-authoring/SKILL.md index bab18bac..cf4f7592 100644 --- a/.agents/skills/sdp-authoring/SKILL.md +++ b/.agents/skills/sdp-authoring/SKILL.md @@ -17,10 +17,12 @@ with `sdp q 'return g.specContext("")' --root node_modules/@libar-dev/softwa or open its carrier under that directory. At this repository root, the `sdp:q` wrapper supplies the exact self-hosting exclusions. Paste each -recipe body between the quotes: +recipe body between the quotes, or run the file the build ships for it under `dist/recipes/`, and +pass a recipe's parameter as data with `--params`: ```sh pnpm --silent sdp:q '' +pnpm --silent sdp:q "$(cat dist/recipes/09-promotion-preflight.js)" --params '{"spec":"spec:"}' ``` For an adopter, select its root and exclusions explicitly: diff --git a/.agents/skills/sdp-sessions/SKILL.md b/.agents/skills/sdp-sessions/SKILL.md index e4ae3721..ea50e4c5 100644 --- a/.agents/skills/sdp-sessions/SKILL.md +++ b/.agents/skills/sdp-sessions/SKILL.md @@ -33,8 +33,10 @@ pnpm exec sdp q '' --root PATH pnpm exec sdp q '' --root PATH --exclude PATH --exclude PATH ``` -Open the catalog and substitute only the parameter line of a parameterized recipe. The recipe -result informs the session; it never grants permission or records a verdict. +Pass a parameterized recipe its subject as JSON data with `--params` (or `--params @PATH` for a +file), never by editing the body; each recipe names its parameter, and the build ships every body +as `dist/recipes/-.js`. The recipe result informs the session; it never grants +permission or records a verdict. ## Choose an advisory work shape @@ -56,7 +58,11 @@ Resolve the target's blocking open questions and review the carrying Specs. The register (recipe 20) lists every open question in the corpus when the neighbors matter too. A clear floor is evidence, not an automatic `ready` statement. Use pinned declarations (recipe 24) to list every signature, type, validator or table the target and its neighbors pin as a code span -opening a keyed Design entry, so a test author and a builder working apart read one list. +opening a keyed Design entry, so a test author and a builder working apart read one list. When the +design is a Pack, read it with Pack design (recipe 29): per member, the stated next rung and what +holds the member below it, the design columns, open questions with their lines, and the code that +answers to it. Before changing a Spec, run design-change impact (recipe 30) for every Spec that +rests on it and the units, verifiers, and Packs the change asks to follow. ### Implement @@ -79,7 +85,7 @@ existing code, the design's own `dependsOn` or `refines` says so, never a refere For a Pack, use the Pack review backbone (recipe 5) and warn-level signals (recipe 8). Without a Pack, use the target Spec context (recipe 3) with warn-level signals (recipe 8). To review a change, take the scope from changed-file blast radius (recipe 4) over the diff, then run the -mention audit (recipe 22) with that scope as its parameter. Use address resolution (recipe 25) on +mention audit (recipe 22) with that scope as `params.scope`. Use address resolution (recipe 25) on the entry addresses a register, a test, or a page cites outside the Specs. When reviewing component, architecture, or projection questions, use structural neighborhood (recipe 14), census structural coverage (recipe 15), the projection-coverage upper bound (recipe 16), the architecture @@ -87,7 +93,11 @@ map (recipe 17), and the decision map (recipe 18). For a Pack reviewed from the references into a design (recipe 27): per member, the referencing units, the realizing units, and whether a verifier is bound, with an empty column read as unbound. Use roles, layers and contexts (recipe 28) to review the architecture vocabulary the anchors state and to spot two spellings of -one value. Review findings and gaps as data; the review never becomes a workflow gate. +one value. To review a Pack's design as it stands, use Pack design (recipe 29); to review decisions, +the decision register (recipe 31), with each decision's rung, supersession, narrowing through `refines` both ways, +and the Specs it shapes; to review dependency direction, architecture crossings (recipe 32), which reports the +`uses` edges that cross a context or layer and refuses nothing. Review findings and gaps as data; +the review never becomes a workflow gate. ### Close / slim diff --git a/.omo/evidence/plan-41/mentions.md b/.omo/evidence/plan-41/mentions.md new file mode 100644 index 00000000..9b10e21c --- /dev/null +++ b/.omo/evidence/plan-41/mentions.md @@ -0,0 +1,24 @@ +# Plan 41: unbacked mentions and honesty gaps (lane c2-repairs) + +Measured on `lane/c2-repairs` from `28dcbdd`. Each line names the pair or the Spec, what the lane +did, and why. Re-run `sdp validate` and recipes 9 and 22 rather than trusting these lines. + +## Prose mentions + +1. `spec:carrier.markdown-body-grammar` → `spec:validation.authored-honesty`: declared `constrainedBy`. The grammar refuses delivery-fact and edge names as keys "under" that rule-kind Spec, so the rule bounds the grammar; recipe 9 finds no floor failure. +2. `spec:carrier.markdown-parser` → `spec:carrier.inline-code-spans`: declared `constrainedBy`. The parser keeps matched code spans literal at its raw-HTML guards under that rule, so the rule bounds the parser; the target states `defined`, which the `ready` floor accepts, and recipe 9 finds no failure. +3. `spec:consumers.adopter-on-ramp` → `spec:carrier.markdown-body-grammar`: declared `dependsOn`. The on-ramp ships a table of the body grammar that points at that Spec, so the on-ramp needs the grammar to hold; recipe 26 finds no dependency cycle after the edge. +4. `spec:consumers.delivery-session-on-ramp` → `spec:decisions.planning-truths-placement`: declared `decidedBy`. The decision places session law on the on-ramp's handoff rule, so it shapes this Spec. +5. `spec:consumers.delivery-session-on-ramp` → `spec:decisions.shipped-projections-frozen`: left as prose. The handoff rule names the freeze only as one example of a register carrier the next session re-measures; the on-ramp neither rests on the freeze nor is shaped by it. +6. `spec:decisions.architectural-significance-rides-primitives` → `spec:model.structural-patterns`: left as prose. The mentioning record is superseded by `spec:decisions.architectural-annotation`, a superseded record gains no relation, and the target already names the superseding ruling by `decidedBy`. +7. `spec:decisions.carrier-ruling` → `spec:decisions.carrier-universality`: left as prose. The earlier ruling points forward to a later one that refines it through `spec:decisions.gherkin-carrier-option`; no relation runs from an earlier decision to its later refinement, and `supersedes` would misstate a refinement as a replacement. +8. `spec:decisions.jsdoc-graph-extraction-refused` → `spec:model.spec-sections`: left as prose. The mentioning record is superseded by `spec:decisions.anchor-comment-form`, and it names the promotion law only as the place promoted rules go. +9. `spec:decisions.planning-truths-placement` → `spec:consumers.impact-graph`: left as prose. The decision names the Spec that holds the impact-graph deferral; a decision does not rest on the Spec it places a hold on. If the owner wants the pair backed, the honest edge runs the other way, `decidedBy` from `spec:consumers.impact-graph`; the lane did not add it, because its scope was the mentioning Spec. + +## Honesty gaps + +1. `spec:carrier.markdown-authoring`: bound, `test:protocol.markdown-authoring` in `test/import-round-trip.test.ts`. The suite builds a TypeScript corpus of all eight kinds with anchors, emits its Markdown twin, asserts both derive the same nodes, edges and delivery facts, and catches a corrupted twin: the Spec's one rule, that both carriers feed one derivation path. +2. `spec:extraction.claim-taxonomy`: gap left. No test takes the taxonomy as its subject. Edge claims are asserted as part of graph derivation (`test/extract.test.ts`, verifying `spec:extraction.derive-graph`), the never-collapsed rule by the claim-separation validator tests and examples (`spec:validation.claim-separation`), and claim inheritance (`belongsTo` carries `declared`) only in passing, in the Pack carrier parity example and the self-hosting oracle suite. `graphClaims` is missing from `test/graph-schema.test.ts`'s schema-contract test; a pin there plus an extraction test of inherited claims would make an honest verifier. +3. `spec:model.pack-aggregate`: bound, `test:protocol.pack-builder` in `test/builders.test.ts`. "Builds packs as plain manifests with no authored truth fields" checks the realizing unit, `src/model/pack.ts`, yields id, title, framing, specs and modelRefs and nothing truth-bearing; `test/builders.typecheck.ts` is the compile-time twin that refuses intent, readiness and constraints on a Pack. +4. `spec:model.relations`: bound, `test:protocol.relation-builders` in `test/builders.test.ts`. "Builds plain serializable specs with declared relations" checks the realizing unit, `src/model/relations.ts`, yields each of the six relations as a declared, directed edge to its target. The kind-typed endpoints are checked under the claim-separation validator in `test/validators.test.ts`. +5. `spec:model.spec-sections`: gap left. The Spec composes laws other Specs carry, and each is checked there: the typing law at compile time in `test/builders.typecheck.ts` (`spec:decisions.typing-law`), content-only sections in the extraction corpora (`spec:decisions.content-only-sections`), direct `verifies` under `spec:extraction.delivery-facts`, and question keys under `spec:model.open-question-keys`. The one test of the section model itself lists the eight section names (`test/descriptors.test.ts`), too thin to call a verifier. diff --git a/.omo/evidence/plan-41/research/platform-inventory-sol.md b/.omo/evidence/plan-41/research/platform-inventory-sol.md new file mode 100644 index 00000000..3fe49e5b --- /dev/null +++ b/.omo/evidence/plan-41/research/platform-inventory-sol.md @@ -0,0 +1,255 @@ +The Protocol already supplies much of the graph data this workflow needs. The largest remaining burden is assembling that data into a reviewable capability, joining it to decisions and compiled comparisons, and keeping the result current. + +The four production scripts inspected total **2,439 physical lines**. Their focused tests total **2,153 lines**. The decision register contains **175 rows and 296 source references**; the consensus ledger contains **278 findings**. These are substantial adopter investments, but they do not all belong in the Protocol. Owner authority, advisor routing, scheduling, and consensus remain local policy. + +In this report, **A** means `/Users/darkomijic/dev-libar/libar-platform`; **P** means `/Users/darkomijic/dev-libar/software-delivery-protocol`. Sizes are physical lines, including comments and blank lines, or exact bytes. Status is assessed against Protocol `822aea4`. “Departed” identifies a request that a ready decision Spec deliberately rules against. + +Verification was read-only. Fresh queries returned 243 adopter Specs, six Packs, 429 nodes, 1,499 edges, and 12 warnings. The history-view page’s read-only `--check` passed. I did not run `validate`, `build`, the full `check.py`, or tests that write fixtures or generated files. In particular, [`check.py:135`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:135) invokes `validate`, so this report does not claim a fresh full-check `OK`. + +## 1. The design workflow as practiced + +One iteration has two related loops: a capability design pass before implementation, and a review-and-repair loop around a built unit. + +| Step | Reads | Produces or updates | How the result advances | +|---|---|---|---| +| **1. Select the unit and its unresolved choices.** | `STATE.md`, `ROADMAP.md`, `SESSIONS.md`, register filters, current graph. | A bounded task and its file limits; an external brief when work is delegated. | The roadmap orders work; the main thread identifies what needs a ruling before design or build. [`SESSIONS.md:55`](/Users/darkomijic/dev-libar/libar-platform/design/SESSIONS.md:55), [`ROADMAP.md:13`](/Users/darkomijic/dev-libar/libar-platform/design/ROADMAP.md:13). | +| **2. Put durable design in carrying Specs.** | The owner’s doc, language, ruled decisions, evidence, existing related Specs. | Spec text, typed relations, keyed Design entries, examples, blocking or non-blocking questions; a capability Pack gathers the reading order. | Independently reviewed or realized promises receive separate Specs. Shared content retains one home. [`PLAN.md:600`](/Users/darkomijic/dev-libar/libar-platform/design/PLAN.md:600). | +| **3. Regenerate the capability’s reading page and declarations.** | Fresh graph; Pack membership; recipes 20 and 24; `pinned-pairs.json`; decision register; member carriers for line locations. | `design/pages/history-view.md`; `generated/pinned/history-view.ts`. | The page presents the proposed capability beside its supporting design and realization. The module makes its declaration shapes available to compiled comparisons. [`pack-page.mjs:412`](/Users/darkomijic/dev-libar/libar-platform/scripts/pack-page.mjs:412), [`pinned.mjs:293`](/Users/darkomijic/dev-libar/libar-platform/scripts/pinned.mjs:293). | +| **4. Read the capability by lens.** | Advisor protocol, project context, lens file, Pack page first; recipes 5, 20, 21 and 24; original Specs when the page is insufficient. | An external memo, at most 15 findings; each proposes exact Spec text, a question, a register row, or no change. | Each finding identifies its entry address or carrying Spec and states the basis of its claim. The memo also reports what the page could not answer. [`task-pass.md:11`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/task-pass.md:11), [`task-pass.md:24`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/task-pass.md:24). | +| **5. Sort decisions and write leans.** | Register sources: owner queue, Spec questions, extensions, provisional plan ambiguities, product decisions and Spec lines. | External JSON patches for OD rows. | Rows are classified `tactical`, `delegated`, or `owner`. A full lean has six fields: `do`, `restsOn`, `against`, `settles`, `changes`, `ifWrong`. [`register.md:17`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/register.md:17), [`protocol.md:51`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/protocol.md:51). | +| **6. Check the advice independently.** | The memo, lean or fork; cited sources; a separate copy carrying a planted false citation. | External check report; advisor corrections; register `checkedBy` after acceptance. | The other model family checks factual claims, not the recommendation’s desirability. This is a local evidence discipline, not a Protocol approval fact. [`advisors/README.md:81`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/README.md:81), [`protocol.md:64`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/protocol.md:64). | +| **7. Prepare owner forks where needed.** | Owner-class rows, their lean, cited evidence, affected Specs and unit dependencies. | A fork under `design/decisions/forks/`, plus an external register patch naming it. | The fork lays out options, costs, the strongest case against the lean, what only the owner decides, and the provisional reading. The owner’s answer is later recorded separately. [`task-fork.md:1`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/task-fork.md:1), [`OD-025.md:9`](/Users/darkomijic/dev-libar/libar-platform/design/decisions/forks/OD-025.md:9). | +| **8. Apply rulings through the main thread.** | Checked proposals and owner words. | Exact Spec changes; questions removed or revised; OD row status, authority, date and ruling; tactical lines in `STATE.md`; owner answer in forks. | Advisors do not edit canonical Specs or the register. `decisions.py apply` validates the entire candidate patch before atomic replacement. A question, owner item or E-number change updates its row in the same commit. [`advisors/README.md:85`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/README.md:85), [`decisions.py:190`](/Users/darkomijic/dev-libar/libar-platform/design/tools/decisions.py:190). | +| **9. Repeat the pass over the revised capability.** | Revised Specs, regenerated page and module, remaining questions. | Further exact changes, new questions or rows, or confirmation. | No authored “pass status” is put on Specs. Git carries the pass history. `ready` means approved to build and remains the owner’s statement. [`PLAN.md:605`](/Users/darkomijic/dev-libar/libar-platform/design/PLAN.md:605), [`PLAN.md:611`](/Users/darkomijic/dev-libar/libar-platform/design/PLAN.md:611). | +| **10. Build and prove the ruled design.** | The same ruled Specs, declaration module and examples. | Implementation and tests, written independently where commissioned; source anchors; CI results. | Equality comparisons detect drift for realized declarations. Ahead comparisons detect that code has caught up. Behavioral evidence must name compiled, pure test, `convex-test`, or native backend. [`PLAN.md:602`](/Users/darkomijic/dev-libar/libar-platform/design/PLAN.md:602), [`SESSIONS.md:42`](/Users/darkomijic/dev-libar/libar-platform/design/SESSIONS.md:42). | +| **11. Review a frozen unit and fold in repairs.** | Changed Specs, graph neighbors, mention audit, implementation and relevant tier evidence. | Lens findings and verdicts; settled findings in the consensus ledger; repair changes; later confirmation or `unreviewed` flags. | The five review lenses are fidelity, Convex, SDP, architecture and completeness. The main thread settles a finding before it enters the ledger. Consensus is a local rule about latest lens verdicts covering the relevant HEAD. [`SESSIONS.md:79`](/Users/darkomijic/dev-libar/libar-platform/design/SESSIONS.md:79), [`SESSIONS.md:121`](/Users/darkomijic/dev-libar/libar-platform/design/SESSIONS.md:121). | +| **12. Close the unit.** | Checks, tests, CI, settled rulings and remaining leads. | Current page; `STATE.md`; roadmap adjustments; concise report and one-line external run log; commit/PR. | Tests and CI are the proof. Work status and unresolved leads stay outside intended Spec truth. [`SESSIONS.md:114`](/Users/darkomijic/dev-libar/libar-platform/design/SESSIONS.md:114), [`ROADMAP.md:53`](/Users/darkomijic/dev-libar/libar-platform/design/ROADMAP.md:53). | + +The current Pack page is **2,725 lines, 194,238 bytes**. It assembles member readiness, next-rung obstacles, outside dependencies, questions, OD rows, source bindings, examples and declaration comparisons. It is a review index with substantial declaration detail; readers still need the original Specs for the complete behavioral contract. The pass task expressly requires that fallback. [`history-view.md:21`](/Users/darkomijic/dev-libar/libar-platform/design/pages/history-view.md:21), [`task-pass.md:16`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/task-pass.md:16). + +The pinned module is **923 lines, 33,913 bytes**. Its emitter adds TypeScript interpretation, dependency closure, Convex imports, fallback names, table/index assembly and formatting to recipe 24’s language-independent declaration list. The comparison inventory currently contains **49 bound, 17 ahead and 10 not-compared pairs**, rather than the older feedback’s 53/14/9. Three comparisons erase the context parameter; five compare table shapes. These are **compiled-tier comparison intentions**, not selected test-run results. [`pinned.mjs:36`](/Users/darkomijic/dev-libar/libar-platform/scripts/pinned.mjs:36), [`pinned.mjs:305`](/Users/darkomijic/dev-libar/libar-platform/scripts/pinned.mjs:305), [`pinned-pairs.json:1`](/Users/darkomijic/dev-libar/libar-platform/tests/types/pinned-pairs.json:1). + +## 2. Mechanism inventory + +The ruling abbreviations below refer to these exact Protocol boundaries: + +- **Freeze:** ready `spec:decisions.shipped-projections-frozen`, MD-32. Reshaping a shipped projection requires a superseding decision. The later authored-entry-order decision reopens only list rendering. [Freeze:16](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/shipped-projections-frozen.sdp.md:16), [list exception:18](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/authored-entry-order.sdp.md:18). +- **Declarations:** `spec:extraction.contract-declarations`, currently **defined**, not a ready decision. One-line opening spans; language and prefixes remain adopter-owned; emission stays local until a second adopter needs the same preamble. [Declarations:25](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/extraction/contract-declarations.sdp.md:25). +- **Planning:** ready `spec:decisions.planning-truths-placement`, MD-33. Semantic dependencies and decision gates have graph homes; scheduling phrases do not. [Planning:18](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/planning-truths-placement.sdp.md:18). +- **Front door:** ready `spec:decisions.agent-front-door`, MD-22. Exported reader plus one evaluation sink; no new query-verb vocabulary. [Front door:17](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/agent-front-door.sdp.md:17). +- **Bindings:** ready `spec:decisions.binding-not-liveness`, MD-7. Bindings and enabled verifiers do not establish execution or approval. [Bindings:16](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/binding-not-liveness.sdp.md:16). +- **Architecture:** ready `spec:decisions.architectural-annotation`, MD-38. Local negative architecture rules remain adopter checks; free roles do not become a universal registry or quota. [Architecture:36](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/architectural-annotation.sdp.md:36). + +Shared-file sizes repeat below and must not be summed. + +| Mechanism | What it does | Measured size | Graph data it reads | What it adds beyond the graph | Capability that could absorb or retire it | Standing ruling | Second adopter? | +|---|---|---:|---|---|---|---|---| +| [`scripts/pack-page.mjs:412`](/Users/darkomijic/dev-libar/libar-platform/scripts/pack-page.mjs:412) | Builds the capability review page and checks freshness. | 1,201 lines | Pack context; Spec contexts; relations, bindings, examples; recipes 20/24. | Pair and OD joins, formatted declarations, boundary explanations, source links and repository context. | Capability review renderer with external-input adapters. | Freeze for changing Design Review; Studio package/reader questions for a separate surface. | **Yes.** Complex capability review needs this assembly. | +| [`pack-page.mjs:292`](/Users/darkomijic/dev-libar/libar-platform/scripts/pack-page.mjs:292) carrier-line parser | Reopens Markdown to locate Design entries and questions. | 17-line function | Spec file path only. | Per-entry source line. | Extractor-owned source locations keyed by existing addresses. | **None** identified; needs a schema/source-location contract. | **Yes.** Any source-linked viewer otherwise repeats extraction knowledge. | +| [`pack-page.mjs:215`](/Users/darkomijic/dev-libar/libar-platform/scripts/pack-page.mjs:215) declaration presentation | Formats TypeScript fragments with wrappers and fallback. | Shared 1,201-line file; L215–265 | Declaration text. | Language-specific readable layout. | Optional language formatter in a viewer adapter. | Declarations; Freeze if added to the shipped review. | **Maybe.** Same language need, different formatter. | +| [`design/pages/history-view.md:1`](/Users/darkomijic/dev-libar/libar-platform/design/pages/history-view.md:1) | Committed, reviewable capability snapshot. | 2,725 lines; 194,238 bytes | Generator output. | A browsable assembly with local joins. | Disposable static review artifact, generated and checked by CI. | Pure Design Review excludes changing context. | **Yes.** The exact committed-page policy is local. | +| [`scripts/pinned.mjs:293`](/Users/darkomijic/dev-libar/libar-platform/scripts/pinned.mjs:293) | Emits a compilable declaration module for a Pack. | 408 lines | Pack members; recipe 24 across the corpus. | TypeScript AST handling, name closure, duplicate checks, value order, tables/indexes. | Reusable language emitter package or recipe kit. | Declarations explicitly keeps emission local pending a second adopter with the same preamble. | **Maybe.** General for typed design; Convex preamble is specific. | +| [`pinned.mjs:36`](/Users/darkomijic/dev-libar/libar-platform/scripts/pinned.mjs:36) preamble and gaps | Supplies framework names and unpinned dependencies. | Shared 408-line file; L36–80; 13 gap names | Names referenced by declarations. | Convex imports, code aliases, two placeholder types. | Explicit emitter configuration with visible qualifications. | Declarations. | **Maybe.** The configuration pattern generalizes; these names do not. | +| [`generated/pinned/history-view.ts:1`](/Users/darkomijic/dev-libar/libar-platform/generated/pinned/history-view.ts:1) | Derived module imported by comparison tests. | 923 lines; 33,913 bytes | Pinned declarations and closure. | Compilable TypeScript projection. | Standard output of the emitter adapter. | Declarations: generated, never authored or imported by Specs. | **Maybe.** Depends on language and comparison strategy. | +| [`tests/types/pinned-pairs.json:1`](/Users/darkomijic/dev-libar/libar-platform/tests/types/pinned-pairs.json:1) | Records declaration/code pair intentions and qualifications. | 538 lines; 76 pairs | Entry addresses, checked against declarations. | `bound`/`ahead`/`notCompared`, mode, symbol, reason. | External comparison-input schema consumed by review surfaces. | Bindings prevents treating intention as execution. | **Yes** for typed design ahead of code. | +| [`pinned.test-d.ts:42`](/Users/darkomijic/dev-libar/libar-platform/tests/types/pinned.test-d.ts:42) | Asserts equality, intended inequality, table shape and context-erased shape. | 383 lines | Derived pinned module, indirectly. | **Compiled-tier** proof about selected code/design pairs. | Helpers can reduce boilerplate; meaningful assertions remain adopter tests. | **None** for helpers; Bindings for interpreting results. | **Yes**, with different pairs and assertions. | +| [`pack-page.test.ts:1`](/Users/darkomijic/dev-libar/libar-platform/tests/pure/pack-page.test.ts:1), [`pinned.test.ts:1`](/Users/darkomijic/dev-libar/libar-platform/tests/pure/pinned.test.ts:1) | Exercise local rendering and emitter behavior. | 900 + 255 lines | Sample graph/recipe inputs. | Regression coverage for hand-built machinery. | Move reusable behavior and its tests with the package. | **None**; local policy tests remain. | **Yes** if each adopter writes an equivalent generator. | +| [`design/decisions/register.json:1`](/Users/darkomijic/dev-libar/libar-platform/design/decisions/register.json:1) | Operational index of unresolved and ruled choices. | 7,133 lines; 440,363 bytes; 175 rows | 159 question sources, plus other sources. | Authority class, advisor, scheduling block, lean, check, fork and ruling history. | Graph-backed decision/question view plus an external work register. | Planning permits semantic decisions, not the whole workflow schema. | **Yes** for iterative decisions; exact classes are local. | +| [`design/tools/decisions.py:59`](/Users/darkomijic/dev-libar/libar-platform/design/tools/decisions.py:59) | Queries, filters, validates and atomically applies OD patches. | 467 lines | Recipes 20/25; question existence and source association. | Local row state machine, authority rules, immutable fields, atomic writing. | Reusable external-register adapter; graph-native semantic decisions reduce duplicate content. | Planning; Front door rules out replacing recipes with many query commands. | **Maybe.** Indexing and safe patching generalize; authority policy does not. | +| [`test_decisions.py:1`](/Users/darkomijic/dev-libar/libar-platform/design/tools/test_decisions.py:1) | Tests filtering, patch refusal, atomicity, question links and check failures. | 615 lines; 40 test methods | Fixture query answers and current register/Specs. | Regression protection for the register and local checks. | Retire corresponding tests as reusable machinery moves. | **None.** | **Maybe.** Follows whether the register adapter is reused. | +| [`design/decisions/forks/OD-025.md:1`](/Users/darkomijic/dev-libar/libar-platform/design/decisions/forks/OD-025.md:1) and other forks | Present options, costs, evidence, provisional choice and owner answer. | 13 files; 792 lines | Spec identities and cited design. | A decision packet and work history. | Decision-Spec authoring template and decision-context view. | Planning gives durable tradeoffs a decision-Spec home; work history stays external. | **Yes.** The specific owner ritual is optional. | +| [`design/advisors/README.md:1`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/README.md:1), protocol, context, tasks and lenses | Define advisor inputs, bounded outputs and authority. | 12 Markdown files including lenses; 795 lines | Packs, questions, declarations, dependencies. | Lens questions, evidence labels, task limits and routing. | Configurable pass-bundle recipe and reusable templates. | Planning keeps per-session routing outside planning law. | **Maybe.** Multiple perspectives generalize; these four advisors do not. | +| [`protocol.md:64`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/protocol.md:64) independent factual check and plant | Checks citations before a lean or fork advances. | Convention in protocol/README/task files | Cited Specs and code, not a separate graph fact. | Calibration and factual review evidence. | Optional review practice supported by source links and disposable bundles. | Bindings; no approval fact should be inferred. | **Maybe.** Useful practice, not mandatory Protocol machinery. | +| [`design/reviews/consensus-ledger.json:1`](/Users/darkomijic/dev-libar/libar-platform/design/reviews/consensus-ledger.json:1) | Tracks findings, repairs, deferrals and verdict history. | 4,798 lines; 717,038 bytes; 278 findings | No automatic graph join; files and citations are stored. | Finding lifecycle and local review consensus. | External review-input adapter and graph-based affected-design views. | Design Review stores no findings canonically; Bindings excludes approval facts. | **Maybe.** Review tracking is general; consensus rule is local. | +| `design/reviews/r3-*.jsonl` | Preserves the finished round’s raw review records. | 4 files; 180 records; 266,289 bytes | Reviewers’ graph/source reads. | 95 findings, 37 confirmations, 25 checked records, 19 progress records, four verdicts. | Optional import into an external review view. | **None** for reading; these finished records retain their text. | **No** need for this exact archive format. | +| [`STATE.md:302`](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:302) | Records current work, 69 numbered tactical decisions, owner queue and leads. | 588 lines; 183,456 bytes | Manually summarized graph/check/register facts. | Temporal state, choices about work, unresolved evidence. | Derived summaries and graph-native durable decisions can thin it. | Planning; work status remains outside intended truth. | **Yes** for a thin handoff; **no** for this volume and exact format. | +| [`ROADMAP.md:3`](/Users/darkomijic/dev-libar/libar-platform/design/ROADMAP.md:3) | Orders units in rounds and lanes with needs, size and deferrals. | 175 lines; 32,358 bytes | Spec/OD references, manually. | Schedule, unit order, estimates, owner sittings. | Derive semantic prerequisites; retain a thin local schedule. | Planning refuses scheduling relations such as `precedes` and `inArc`. | **Yes** for scheduling; it should not become graph law. | +| [`SESSIONS.md:1`](/Users/darkomijic/dev-libar/libar-platform/design/SESSIONS.md:1) | Defines session kinds, authority, review, close and handoff. | 138 lines | Recipes and graph scope, by convention. | Team workflow and consensus policy. | Optional session templates and graph-generated briefs. | Planning separates generic session law from per-session routing. | **Maybe.** A second team chooses its own policy. | +| [`PLAN.md:596`](/Users/darkomijic/dev-libar/libar-platform/design/PLAN.md:596) | Documents design-ahead-of-code practice and initial design conventions. | 738 lines | Spec and recipe references. | Local policy, initial ambiguities, E-number allocation. | General authoring guidance can absorb §6.7; durable rulings move to Specs. | Planning; owner’s source doc remains authoritative. | **Yes** for guidance, **no** for this platform’s plan. | +| [`design/README.md:1`](/Users/darkomijic/dev-libar/libar-platform/design/README.md:1) | Human corpus index, counts, links and extension register. | 453 lines; 97,596 bytes | Counts and references copied from graph/checks. | Navigation and historical E aliases. | Generated census/question/decision summaries. | Freeze for reshaping existing census; a local index has no such obstacle. | **Yes** for navigation; copied counts are avoidable. | +| [`check.py:125`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:125) orchestrator | Runs Protocol validation, local policy and agreement checks. | 363 lines | Findings, counts, readiness, questions, recipes 25/26/28, structural edges. | Local policy failures and a session summary. | Read-only inspection API plus reusable diagnostics recipes; retain local gates. | Checks must not acquire team workflow or content-quality authority. | **Yes** for a small wrapper; **no** for duplicating Protocol extraction. | +| [`prose-mentions.json:1`](/Users/darkomijic/dev-libar/libar-platform/design/tools/prose-mentions.json:1) and [`check.py:156`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:156) | Accept intentional unbacked mentions; reject new, blank or stale exceptions. | 64-line JSON; 12 pairs; shared check file | Prose-mention findings. | Human reason for not adding a typed relation. | External reason-input schema and relationship-view adapter. | Checked mentions mints no edge; local strictness need not become a core gate. | **Yes.** Intentional plain pointers recur. | +| [`check.py:67`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:67), [`check.py:171`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:171) | Finds external entry citations and resolves them in a batch. | Shared 363-line file | Recipe 25. | File discovery, token locations and local refusal. | Reusable address scanner and data parameters for recipes. | Front door; no need for a new query verb. | **Yes.** Registers, tests and work docs cite entries. | +| [`check.py:183`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:183) | Turns reported `dependsOn` cycles into local failures. | Shared check file | Recipe 26. | Local prohibition of cycles. | Recipe already supplies the graph algorithm; keep the policy wrapper. | Planning supports semantic dependencies but does not require this universal gate. | **Maybe.** Reporting is general; refusal is contextual. | +| [`check.py:206`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:206) | Rejects platform components using another context, including via member units. | Shared check file | `uses`, `memberOf`, component contexts. | This platform’s isolation law. | Reusable structural predicate recipe, configured by adopter. | Architecture explicitly keeps negative constraints local. | **Maybe.** Same predicate pattern, different rule. | +| [`check.py:233`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:233) | Requires question keys and requires blocking-question Specs to state `scoped`. | Shared check file | Recipe 20. | Stricter local policy than the Protocol. | Keep a short adopter gate over existing data. | Protocol keys are optional; its floor is not this exact workflow rule. | **No** as a universal rule. | +| [`check.py:259`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:259) | Rejects any stated `ready`. | Two-line branch in shared check file | Carrier readiness. | Current blanket ban, described as owner protection. | Local owner-authorized policy must be corrected when readiness is stated. | No Protocol obstacle; it must not become core workflow policing. | **No.** It currently does not consult an owner ruling. | +| [`check.py:261`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:261), [`check.py:297`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:297) | Checks Law/F citations against `constrainedBy`, and E citations against the README register. | Shared check file | Carrier relations plus prose tokens. | Platform-specific aliases and registration rules. | Standard Spec IDs and derived decision/question indices reduce aliases; configured citation checks can remain. | Planning favors existing graph homes, not another truth registry. | **Maybe.** Citation integrity is general; Law/F/E syntax is local. | +| [`check.py:283`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:283) | Checks register questions against current Specs and closed-source text. | Shared check file; uses `decisions.py` | Recipes 20/25 and Spec files. | Cross-store referential consistency. | External-register adapter with standard address resolution. | **None.** | **Yes** while work registers remain external. | +| [`check.py:290`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:290) | Checks README summary literals and local links. | Shared check file | Graph counts and findings. | Detects stale copied prose. | Generate the summaries instead of validating copies. | **None** for local documents. | **Yes**, but preferably retired. | +| [`check.py:307`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:307) | Validates ledger status vocabulary and counts open findings by slice. | Shared check file | None beyond stored ledger. | Local record-shape sanity. | Optional external-ledger schema validation. | **None**; consensus remains local. | **Maybe.** | +| [`check.py:317`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:317) | Checks the explicit Pack-page list for missing, unknown or stale pages. | Shared check file | Page generator’s current graph. | Committed-artifact freshness gate. | Standard static review export with `--check`/CI comparison. | Freeze only if the shipped projection is changed. | **Yes.** | +| [`check.py:336`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:336) | Prints a corpus digest. | Seven-line calculation | None; hashes selected files. | Fingerprint of a selected input subset. | Explicit generation provenance with a defined input manifest. | Pure Design Review cannot absorb changing repository context. | **Maybe.** The current digest is not a full-tree identity. | + +Three limits are worth preserving in any replacement: + +1. The digest omits `STATE.md`, `ROADMAP.md`, the decision register, pages, pins and code. It must not be presented as the identity of the complete review input. +2. External-address scanning covers selected source extensions and seven work records. It does not cover every advisor memo, fork or archived review. +3. The ledger check validates status values, not the five-lens consensus rule. That rule is applied by the session workflow. [`check.py:80`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:80), [`check.py:307`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:307), [`check.py:338`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:338). + +There is also a documentation drift: [`advisors/README.md:99`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/README.md:99) says no tool checks register/question agreement, but that check now exists at [`check.py:283`](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:283). + +## 3. Open asks to the Protocol + +“Met” means the requested capability exists at `822aea4`, not that all adopter repairs or owner rulings have happened. “Partly met” distinguishes available graph data from the missing human surface or integration. + +### Original P1–P8 + +Source: [`docs/sdp-development-from-application-platform.md:250`](/Users/darkomijic/dev-libar/libar-platform/docs/sdp-development-from-application-platform.md:250). + +| Ask and source | Status | Evidence at `822aea4` | +|---|---|---| +| **P1, carrier grammar and authoring guidance**, L250. | **Met** | `spec:carrier.markdown-body-grammar` specifies accepted section forms; the packaged authoring skill documents the carrier. | +| **P2, literal inline code, including generics**, L266. | **Met** | `spec:carrier.inline-code-spans` preserves code-span content rather than treating generic syntax as prose markup. | +| **P3, typed-dependency readiness**, L282. | **Met** | The floor evaluates typed dependencies; `SpecContext.nextRungFailures` exposes unmet clauses and target details. [`reader.ts:599`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/reader/reader.ts:599). | +| **P4, authored examples before bound tests without premature warnings**, L298. | **Met** | Below-ready unbound examples are lawful; declared and enabled verification remain separate. The renderer explicitly names an unbound example as not enabled. [`design-review-context.ts:54`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/projections/design-review-context.ts:54). | +| **P5, graph-first idioms, recipes and packaged Specs**, L314. | **Met for the requested read surface** | Recipes 20–24 cover questions, dependency footing, mention audit, entry search and declarations. The package includes Specs, recipes and agent skills. [`package.json:11`](/Users/darkomijic/dev-libar/software-delivery-protocol/package.json:11). Local E-register policy remains local. | +| **P6, checked prose references and entry lookup**, L334. | **Met for referential integrity; semantic disagreement remains open** | Checked mentions resolves Spec and keyed-entry references, reports unbacked pairs, and recipes 22/23/25 inspect them. It explicitly does not detect two Specs restating one shape differently. [`checked-mentions.sdp.md:20`](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/checked-mentions.sdp.md:20), L37. | +| **P7, authored entry order and readable rendering**, L355. | **Met** | Open-section order survives serialization; the ready authored-entry-order decision permits list rendering. `renderOpenBag` iterates authored entries and renders strings as prose. [`design-review-sections.ts:83`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/projections/design-review-sections.ts:83). | +| **P8, design declarations progressing to a checked module**, L371. | **Partly met** | Recipe 24 extracts opening spans; the adopter emits and compares a module. Opaque fences and a closed declaration section are not implemented. Current contract-declarations rules choose one-line spans and local emission. [`contract-declarations.sdp.md:19`](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/extraction/contract-declarations.sdp.md:19), L25–30. | + +### The 40 requests in feedback 02 + +All 40 are accounted for below. Items explicitly assigned to platform adapters or tests are not recast as missing engine features. + +The main implementation evidence is: + +- **Current renderer:** [`design-review-sections.ts:83`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/projections/design-review-sections.ts:83), [`design-review-pages.ts:57`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/projections/design-review-pages.ts:57), [`design-review-context.ts:26`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/projections/design-review-context.ts:26). +- **Current recipes:** [20, questions](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/agent-surface/recipes.md:1067); [21, dependency footing](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/agent-surface/recipes.md:1162); [23, entry search](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/agent-surface/recipes.md:1423); [24, declarations](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/agent-surface/recipes.md:1584); [25, addresses](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/agent-surface/recipes.md:1612); [26, cycles](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/agent-surface/recipes.md:1683). +- **Studio:** captured design, not a shipped browser. Its Pack has 12 scoped Specs. Package home and unserved reader remain blocking questions. [`spec-studio.sdp.md:23`](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/consumers/spec-studio.sdp.md:23). + +| Feedback 02 request | Status | Evidence and remaining work | +|---|---|---| +| **1. Readable keyed Design entries with source links**, [L17](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:17). | **Partly met** | Authored-order lists are implemented. Entries have neither extractor-provided line locations nor individual source links. | +| **2. Formatted declarations, original copying and fallback**, [L19](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:19). | **Open in Protocol** | Implemented by the local TypeScript presentation adapter. Protocol string entries remain escaped Markdown prose. | +| **3. Working entry page links and copy-address action**, [L43](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:43). | **Open** | Canonical addresses resolve, but list rendering creates no address-specific anchors or copy action. | +| **4. Spec table of contents**, [L45](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:45). | **Open** | Sections render; no generated Spec TOC or keyed-entry navigation. | +| **5. Separate declaration from explanation**, [L47](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:47). | **Open in Protocol** | The local page does this; `renderOpenBag` renders the whole string together. | +| **6. Put review decisions before exhaustive metadata**, [L49](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:49). | **Open** | Current Spec page presents bindings before section content. Changing shipped anatomy requires a narrow Freeze reopening or a separate surface. | +| **7. Reference previews**, [L51](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:51). | **Open** | Reader context is available; no interactive preview surface ships. | +| **8. Packs as the main entrance**, [L57](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:57). | **Open** | The index starts with the Specs table and lists Packs afterward. Studio’s scoped design makes Packs the default lens. | +| **9. Pack order through extraction, serialization, reader and rendering**, [L59](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:59). | **Met** | `spec:extraction.pack-member-order`; ordered member data survives the path. The adopter deleted its carrier-order workaround. | +| **10. Authored Pack reading guide**, [L61](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:61). | **Partly met** | Pack framing is authored and rendered. Named reading stages/groups are not provided. | +| **11. Focal design versus supporting context**, [L63](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:63). | **Open** | Neither the shipped Pack table nor graph membership assigns these presentation purposes. Configuration can add them without temporal Spec truth. | +| **12. Dependencies outside a Pack**, [L65](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:65). | **Partly met** | Recipe 21 and Spec neighborhoods provide footing. The shipped Pack page has no boundary-dependency section; the local page supplies it. | +| **13. Refinement outline**, [L67](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:67). | **Partly met** | Relations and bounded Mermaid views exist; a navigable corpus refinement outline does not. | +| **14. Human search across concepts, entries, declarations and files**, [L69](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:69). | **Partly met** | `findByConcept`, `byFile`, recipes 23/24 provide entrances. No human search box or entry landing surface ships. | +| **15. Split independently decided promises, reunite in a Pack**, [L71](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:71). | **Met as a modeling capability** | Specs have independent readiness and questions; Packs restore reading order. Choosing a useful split remains author judgment. | +| **16. Separate readiness, detail, bindings and selected results**, [L89](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:89). | **Partly met** | Stated/derived readiness and binding existence are separate. Declaration detail and external selected-run results are not assembled in one view. | +| **17. Explain the next rung and its target**, [L100](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:100). | **Partly met** | `nextRungFailures` and dependency targets are implemented. Shipped review rendering still shows divergence from the stated rung, not the full prospective explanation. | +| **18. Pair inventory beside each declaration**, [L102](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:102). | **Open in Protocol; met locally** | The local page joins `pinned-pairs.json`. No standard external comparison adapter ships. | +| **19. Show comparison omissions, placeholders and skips**, [L104](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:104). | **Open as reusable integration** | Local pairs/emitter expose much of this; the Protocol has no common qualification input or view. | +| **20. Assert the intended difference for consequential ahead pairs**, [L106](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:106). | **Partly met locally; not an engine gap** | Selected compiled assertions exist, including a common-shape assertion for `HistoryEvent`. Further semantic assertions belong in adopter tests, not inferred graph facts. | +| **21. Behavioral proof beside type compatibility**, [L108](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:108). | **Partly met** | Examples and enabled verifiers are graph data; the local page extracts tier wording. A selected behavioral run is still a separate input. | +| **22. Repair absent bindings before using absence to plan**, [L110](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:110). | **Met as Protocol support** | Comment anchors allow source-local bindings. Whole-Spec honesty now makes some broad Specs intentionally referenced rather than satisfied; remaining scope repairs are adopter work. | +| **23. Binding site versus implementation site**, [L112](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:112). | **Met for the motivating source-import problem** | Source comment anchors retired the test-to-source mapping. **Symbol attachment is departed** under MD-36’s file-grain rule, discussed below. | +| **24. Reverse file view**, [L114](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:114). | **Partly met** | `byFile` supplies recorded coverage and associated Specs. No dedicated human file page assembles declarations, examples and Packs. | +| **25. Selected test results with verifier, commit, tier, composition and target**, [L116](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:116). | **Open** | No such join ships. Studio’s `#question.testRunResults` records the unresolved choice. MD-7 requires this to remain external execution evidence. | +| **26. Actionable choice list joined to OD rows**, [L120](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:120). | **Partly met** | Recipe 20 supplies keyed questions. Advisor, class, provisional reading and unit blocks remain in the local register; no reusable view joins them. | +| **27. Stable question identities**, [L122](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:122). | **Met** | Optional unique lower-camel question keys and `#question.key` addresses are implemented. Local policy requires all questions to have keys. | +| **28. Review lenses over the same capability**, [L124](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:124). | **Open as a surface** | Local lens tasks exist. Studio lenses are scoped design; its four product lenses are not the adopter’s four advisor roles. No new Spec kind is needed. | +| **29. Generate a Pack-and-lens reading bundle**, [L126](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:126). | **Open** | Recipes supply pieces; no shipped disposable pass bundle combines order, questions, footing, declarations and examples. | +| **30. Compare graph snapshots at entry granularity**, [L128](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:128). | **Open** | No shipped comparison distinguishes entry text, explanation, order, readiness, relations and examples. | +| **31. Changes since a pass**, [L130](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:130). | **Open** | Git provides history, but the Protocol has no pass-base comparison wrapper. It must not mint approval or staleness facts in canonical Specs. | +| **32. Decision rationale beside affected design**, [L132](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:132). | **Partly met** | Decision sections, `decidedBy`, and recipe 18’s decision map exist. OD/fork rationale is not joined into the capability view. | +| **33. Cycles with strongly connected sets and witness paths**, [L136](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:136). | **Partly met** | Recipe 26 implements `dependsOn`, including self-loops and witness cycles. The separately requested broader typed-relation view is absent. | +| **34. Batch address resolution returning identity, value and reason**, [L138](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:138). | **Partly met** | Recipe 25 returns address, resolution, Spec, section, key and exact reason. It does **not** return the entry value or source position. | +| **35. Accepted mention reasons in the relation view**, [L140](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:140). | **Open** | Local allowlist and new/stale checks exist. Protocol findings contain no accepted-reason input or presentation join. | +| **36. Inspection separate from publication**, [L142](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:142). | **Partly met** | `q` and exported extraction/reader/rendering APIs operate in memory. The full local check still invokes the writing `validate` path; there is no local viewer refresh shell. | +| **37. Changing repository/run context outside pure Design Review**, [L144](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:144). | **Partly met** | The boundary is already ruled. A shipped surrounding context wrapper is absent. | +| **38. Small static read-only browser**, [L146](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:146). | **Open** | Studio captures this direction but is scoped and blocked on package home/unserved reader. Existing projections are Markdown, census, Mermaid and Gherkin, not this browser. | +| **39. Rendered capability as a CI review artifact**, [L148](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:148). | **Open as supplied integration** | Renderers can be called by CI; no packaged changed-capability artifact wrapper supplies base and provenance. | +| **40. Test the five human reading tasks**, [L150](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:150). | **Partly met as evidence** | The adopter ran the page-based passes and collected their obstacles. These do not establish acceptance of a Protocol browser that has not shipped. | + +### Additional and repeated asks in feedback 01 + +| Ask and source | Status | Evidence at `822aea4` | +|---|---|---| +| **Source anchors without importing the Protocol**, [“from slice S1,” L237](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:237), repeated at L75. | **Met** | MD-36 comment form and the anchors subpath solve the motivating source-import problem. The adopter retired companion anchor sites. | +| **Detect a block whose declaration moved or disappeared**, [L77](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:77). | **Departed: `spec:decisions.anchor-comment-form`, MD-36** | Ready rule binds by file, not attached syntax or symbol. This is not an unimplemented conformance gap. [`anchor-comment-form.sdp.md:30`](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/anchor-comment-form.sdp.md:30). | +| **Every significant pattern represented in architecture**, [L114](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:114). | **Partly met, with deliberate limits** | MD-38 supersedes the earlier restrictions and admits free roles, component layer/context, membership and uses. Universal annotation quotas, a pattern namespace and role-quality checks remain refused. [`architectural-annotation.sdp.md:22`](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/architectural-annotation.sdp.md:22), L34–48. | +| **Capability page, Pack order, next rung, keyed questions and cycle/address queries**, [L79](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:79), L120–154. | **Data met; full page partly met** | Order, keys, next-rung target data and recipes 25/26 are implemented. The richer assembly remains the 1,201-line local page script. | +| **Entry source line in graph**, [“What still needs the Protocol,” L108](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:108). | **Open** | Primitive nodes carry a file and section content, not per-entry line data. The adopter re-parses carriers. [`schema.ts:94`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/graph/schema.ts:94). | +| **Recipe parameters supplied as data**, [L108](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:108). | **Open for the address recipe** | Recipe 25 still instructs the caller to replace its opening `const addresses`; `decisions.py` rewrites it. Recipe 4 already demonstrates environment input. | +| **Human entrance, focal/supporting design, pin/register joins and code/test location**, [L132](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:132). | **Partly met** | Underlying order, source anchors and queries are available. Feedback 02 items 8, 11, 18, 24 and 26 remain the surface/integration work. | +| **A distinct designed-stub maturity**, [L169](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:169). | **Open as the requested distinction** | No `designed` rung or fact exists. Declaration presence can already be derived independently of the four readiness rungs. The later feedback recommends displaying these dimensions separately before changing the model. | +| **Non-conferring stub/design references**, [L170](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:170). | **Met** | MD-37 `references` indicates code answering to design without claiming implementation. | +| **Multiline declaration carrier and compiled signature comparison**, [L171](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:171), L173–181. | **Partly met** | Extraction and local compiled comparisons exist. Current contract-declarations rules retain one-line spans and explicitly say a fence is not the declaration shape; the fuller proposal remains captured in its non-blocking question. | +| **Stable step keys independent of wording**, [L187](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:187). | **Open** | Runner bindings still use exact `step.text`; duplicate text deliberately shares a handler. [`runner/index.ts:113`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/runner/index.ts:113), L128. | +| **`after` as reliable cleanup, or documentation of its success-only behavior**, [L188](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:188). | **Open in the inspected adapter surface** | The adapter awaits the plan and then `after`, with no `finally`; its lifecycle comment does not spell out this limitation. [`vitest.ts:37`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/adapters/vitest.ts:37). | +| **A test verifying multiple examples**, [L191](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:191). | **Met** | MD-37 admits nonempty plural verifier targets. | +| **Surface bound examples with no implementation binding**, [L193](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:193). | **Open as the requested diagnostic** | The ingredients exist, but the requested shipped warning/recipe is absent. A diagnostic can report the absence without asserting work should be implemented. | +| **Backlog recipe parameterized by rung**, [L194](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:194). | **Open, with reduced local need** | Recipe 1 is still hard-coded to `ready`. OD-163 and PLAN §6.7 now adopt `ready` as approval to build, so the original workaround’s local rationale changed. | +| **Document the limit of binding evidence, including mutation evidence**, [L196](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:196). | **Partly met** | MD-7 and binding-language rendering clearly distinguish existence from execution. The specific mutation-testing guidance requested here is not supplied by those surfaces. | +| **Installable commit dependency / prerelease**, [L204](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:204). | **Open for the requested install path** | Package version remains `0.0.0`; published files expect `dist`; scripts contain no `prepare`. The adopter’s built tarball remains its workaround. [`package.json:3`](/Users/darkomijic/dev-libar/software-delivery-protocol/package.json:3), L11, L51. | +| **Engine version plus build commit**, [L206](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:206). | **Open** | Graph schema version is available, but it is not engine identity. No inspected CLI version/provenance surface supplies package version plus build commit. | +| **Recipe bodies as separate runnable files**, [L223](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:223). | **Open** | Bodies remain fenced in the Markdown catalog; all three local consumers scrape them. This needs no new query verb. | +| **Detect contradictory duplicated shape prose**, [L230](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:230). | **Open as semantic review assistance** | Checked mentions explicitly cannot detect it. Automatically policing content quality is outside the stated checking boundary; avoiding duplicate ownership and supporting human comparison are lawful improvements. | +| **Build writes test suites outside `generated/`**, [L238](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:238). | **Open as a placement concern** | Companion `*.test.generated.ts` suites remain part of the build practice recorded by the adopter. This is distinct from read-only inspection. | +| **Partial realization per entry**, [L239](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:239). | **Departed: `spec:decisions.anchor-binding-grain`, MD-37** | Ready decision requires whole-Spec realization and refuses partial delivery facts/entry-level binding targets. Split coherent promises or use non-conferring references. [`anchor-binding-grain.sdp.md:30`](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/anchor-binding-grain.sdp.md:30). | +| **Composition metadata beside evidence**, [L240](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-01.md:240). | **Open as reusable integration** | Composition and tier belong to external execution inputs. They are not supplied by graph binding facts. | + +The same-root observation and parent-without-example-space observation in feedback 01 L189–190 describe working constraints and adoption, not separate requests to add cross-root binding or a new registrar model. + +### Feedback 03, annotation report and STATE leads + +| Source and request | Status | Evidence or disposition | +|---|---|---| +| Feedback 03 **round 2’s four Protocol PRs**: readable rendering, Pack order, address/cycle recipes, question keys, [L19](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-03-selection.md:19). | **Met** | These are now the shipped list rendering, ordered Pack membership, recipes 25/26 and keyed questions. | +| Feedback 03 **“not now”**: new rung, TS fences, Protocol declaration formatting, Studio, [L28](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-03-selection.md:28). | **Still deferred or open as above** | This is an ordering recommendation, not evidence that the requests were withdrawn or implemented. Studio is captured at scoped. | +| Feedback 03 round 1, shared pair inventory, page freshness, formatting and five reading tasks, [L11](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-03-selection.md:11), L44–60. | **Met locally, not reusable Protocol provision** | Pack page, shared JSON and local checks exist; passes used the page. Owner readiness statements and remaining design choices are adopter decisions. | +| `pr-12-annotations-section.md`, source binding, references, plural targets and architecture vocabulary, [L25](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/pr-12-annotations-section.md:25). | **Met within the ruled scope** | MD-36/37/38 and recipe 27 support them. File grain and whole-Spec realization are deliberate limits. The capability “layer” rename is an adopter-language decision. | +| STATE **lead 10**, backlog at `defined`, [L445](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:445). | **Parameter open; local policy changed** | Recipe 1 still uses ready. PLAN L611 now rules ready as approval to build. | +| STATE **lead 17**, source anchors and generated suites, [L454](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:454). | **Anchors met; suite placement open** | Comment form closes the source-import problem; generated companion suites are a separate concern. | +| STATE **lead 45**, precise test links and aggregate answer spread across sections, [L552](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:552). | **Partly met / open surface work** | Binding nodes have lines and the local page links anchors. Pair-test location precision and a single dependency explanation remain incomplete. | +| STATE **lead 46**, entry locations and recipe parameters, [L558](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:558). | **Open** | Graph lacks per-entry lines; recipe 25 still requires source rewriting. | +| STATE **lead 46**, attachment identity, [L560](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:560). | **Departed: MD-36** | The now-ready file-grain rule settles this differently from the earlier open question. | +| STATE **lead 46**, root carrier check scripts neither gated nor retired, [L561](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:561). | **Open maintenance disposition** | `package.json` does not put those scripts in its check chain. I did not rerun their reported failures, so the historical failure is not a fresh test result. | +| STATE owner queue **missing Protocol terms**, [L398](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:398). | **Open ratification work** | Several terms are absent or candidate vocabulary: next/target rung, question key, cycle, declaration/address terms and Studio language. Checked mentions itself flags candidate terms pending readiness. | +| STATE owner queue **25 Studio departures, five blocking**, [L397](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:397). | **Captured, not implemented; ruled departures are not gaps** | Studio questions preserve the differences from v0. Ready decisions, including binding and projection boundaries, stand unless explicitly superseded. | + +STATE lead 46’s D-number/`decidedBy` check and prose-only history binding declarations, and lead 47’s scope splits, outcome correction and worked-example relocation are explicitly **adopter repairs**. They should not inflate the Protocol backlog. [`STATE.md:556`](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:556), [`STATE.md:564`](/Users/darkomijic/dev-libar/libar-platform/design/STATE.md:564). + +## 4. Design truths that live outside the graph here + +The placement decision supplies most needed homes without a new primitive or relation. It does not make every work record a Spec. + +| Outside record | Durable truth it may carry | Ruled graph home | What blocks or limits moving it | +|---|---|---|---| +| **OD register: sources and unresolved questions** | An unsettled choice affecting a promise. | The carrying Spec’s open question; keyed address for reference. | Most Spec-question rows already point there. The remaining duplication is provisional summaries, authority and work status. A register question that blocks a unit need not block readiness. | +| **OD register: decided tradeoffs and refusals** | Chosen option, rejected alternatives, costs and consequences. | `decision`-kind Spec, reached through `decidedBy`; later reversal through `supersedes` where applicable. | Requires classification and exact authoring. Some OD rows are headings, duplicate pointers, tactical choices or evidence, not semantic decisions. No engine change is required. | +| **OD register: `blocks`, advisor, class, checkedBy, dates and patch status** | Unit scheduling, authority and review history. | No universal canonical graph home for this work state. | MD-33 excludes scheduling authority; MD-7 excludes approval facts. Keep an external register and join it for review. | +| **Forks** | Alternatives and a durable owner ruling. | Decision Spec’s context, decision, rationale, alternatives and consequences; affected Specs link by `decidedBy`. | The fork also contains draft authorship, citation-check notes, date and sitting history. Those portions remain lineage evidence. The current tool does not perform this semantic split. | +| **Consensus ledger and r3 archives** | Findings about a guarantee, a missing dependency, a wrong design or incomplete implementation. | Correct the carrying Spec; unresolved durable choice becomes its question; a ruled tradeoff becomes a decision Spec. | Finding status, reviewer verdict and consensus are work records. Pure Design Review stores no canonical findings, and source bindings cannot prove approval. Preserve the finished r3 records. | +| **STATE tactical decisions** | Some are lasting technical policy; others are instructions about this unit. | Lasting tradeoff: decision Spec. Already-owned guarantee: ordinary revision to its carrying Spec. Generic advisory planning rule: graph-first planning Spec or recipe. | They need sorting. “npm,” a type/module rule, owner authority, lane isolation and one-off job setup are not the same truth category. Session-specific routing stays in STATE/SESSIONS. | +| **STATE leads** | Deferred behavior, re-entry condition, evidence still needed, or merely a suspected gap. | Deferred Spec’s own blocking question; `dependsOn` for a true semantic prerequisite; existing carrying Spec for a known correction. | A lead is not yet intended truth. It must first be checked and ruled. Resolved and retracted leads should not generate new Specs. | +| **ROADMAP needs-first links** | Some express actual design prerequisites; others express “do this sitting/build first.” | Semantic prerequisite: `dependsOn`; decision gate: `decidedBy` to a decision Spec; re-entry: the deferred Spec’s question. | Round, lane, size, sequence and session dependency are scheduling. MD-33 expressly refuses `precedes`/`inArc`. Moving those into `dependsOn` would change its meaning. | +| **PLAN §10 E-register** | Where the owner’s doc leaves a platform choice open. | Question on the carrying Spec; decision Spec when ruled. | Historical E aliases and doc attribution still serve local provenance. A derived index could retire their repeated summaries, but cannot decide the choices. | +| **PLAN §11 ambiguities** | Language, structural and product questions with provisional readings. | Carrying Spec question; chosen tradeoff in decision Spec; settled guarantee on its existing Spec. | Several have already been ruled but remain in the initial plan. The move is a corpus reconciliation, not a new Protocol feature. | +| **Pair inventory and emitter gaps** | A design/code comparison intention, or an unpinned design dependency. | The actual design shape belongs in the carrying Spec; absence of a necessary declaration is a design gap. | Pair states, erased parameters, implementation symbol mappings and selected compiler results remain adapter/test context. They must not become authored delivery facts. | +| **Pass history** | What a reviewer read and what changed afterward. | Durable outcomes go to Specs/questions/decisions. | The pass itself is Git/work context. A comparison wrapper can display it without adding approval primitives. | + +The exact ruling is [`planning-truths-placement.sdp.md:18`](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/planning-truths-placement.sdp.md:18). The adopter’s own application pattern agrees: delegated rulings become ordinary Spec rules with no advisor/date; owner words enter the fork and row; a reopened ruling remains the provisional reading until changed. [`advisors/README.md:87`](/Users/darkomijic/dev-libar/libar-platform/design/advisors/README.md:87). + +The register’s schema shows why wholesale migration would be wrong. Its 18 fields mix semantic references with work authority and scheduling. Its sources are 159 Spec questions, 54 owner-queue items, 51 extensions, 17 Spec lines, 12 plan ambiguities and three product decisions. It also has 46 `possiblySame` groups requiring human reconciliation. The lawful move is to remove duplicate semantic truth while retaining a smaller operational index. + +## 5. Top 12 Protocol changes, ranked + +Ranking considers reusable burden retired and breadth across adopters. The cited footprints overlap; they are affected machinery, not additive savings estimates. + +| Rank | Change | Burden and generality | Smallest lawful shape | +|---:|---|---|---| +| **1** | **Supply a capability review surface that assembles the existing graph answers.** | Targets the largest script: 1,201 lines, its 900-line test suite, and a 194,238-byte committed page. Almost every complex design adopter needs ordered members, questions, boundary dependencies, declarations and realization context together. | Start with a read-only Pack review renderer consuming reader values. Either narrowly supersede Freeze for specified changes or rule Studio’s package home and unserved reader. Do not reopen all four projections. | +| **2** | **Standardize external review inputs and their joins.** | Retires repeated pair, OD, mention-reason and eventual test-run joins. It enables the richer view without promoting work records into canonical truth. | Small adapter contracts keyed by Spec ID, existing entry address or verifier ID. Keep comparison intention, local decision authority and selected execution result visibly separate. | +| **3** | **Provide reusable declaration-emitter machinery with explicit configuration.** | Targets the 408-line emitter, its 255-line pure tests and framework-name closure. General for typed designs; framework preambles vary. | An optional language emitter package/recipe kit with configured imports, gaps and prefixes. Preserve opaque declaration text in the core. Moving it into `sdp build` must satisfy the current second-adopter/same-preamble condition or revise that Spec explicitly. | +| **4** | **Make semantic decision capture and display graph-first.** | Reduces duplicate rationale across the 440,363-byte register, 13 forks, STATE and plan. Decisions and alternatives are broadly reusable. | A decision-authoring template plus a recipe/view over `decidedBy`, alternatives and consequences; migrate only durable rulings. Retain OD authority, sort status, unit blocks and sitting history externally. | +| **5** | **Expose extractor-owned source locations for addressable entries and questions.** | Retires `specLines`, avoids each consumer understanding carrier syntax, and supports precise sources everywhere. | A regenerable source-location mapping keyed by existing addresses, with file and line/range. Define it for both carriers without making entries new primitives or permitting entry-level realization facts. | +| **6** | **Give existing addresses working review links.** | Replaces key searching and moving line pointers in reviews, briefs and decision packets. General across every keyed design corpus. | Stable rendered targets for `design`, `ui` and keyed questions; copy-address action in HTML. Preserve unaddressable sections as such rather than inventing canonical IDs. | +| **7** | **Ship recipe bodies as runnable assets and parameters as data.** | Retires Markdown fence scraping in three scripts and source rewriting in `decisions.py`; the recorded batch was 489 addresses and 28,788 characters of JSON. | Package the same bodies as files; adopt environment/data input such as `SDP_ADDRESSES_JSON`, following recipe 4. Keep the single `q` sink. Extend recipe 25 to return value/source where available. | +| **8** | **Add graph-snapshot and since-pass comparison.** | Reduces manual review-scope reconstruction and helps repeated passes focus on changed promises rather than reread everything. General across design iteration. | Pure comparison by Spec ID, section and existing key, including order, relations, questions and examples. A repository wrapper supplies the two commits or snapshots; it records no approval fact. | +| **9** | **Generate disposable Pack-and-lens reading bundles.** | Reduces hand-written advisor briefs and repeated recipe assembly across the 795-line advisor corpus. General for specialist review; lens policies remain configurable. | A shipped recipe combining Pack order, questions, declarations, dependency footing and examples, with an adopter-supplied lens selection. No new kind, mandatory stage sequence or advisor-role registry. | +| **10** | **Present prospective readiness and Pack-boundary dependency explanations.** | Retires local assembly around already-implemented floor and target data, including the fragmented aggregate answer. General for designs ahead of realization. | Render `nextRungFailures` and target details; add a Pack boundary-footing recipe first if a stable join is missing. Keep semantic dependencies distinct from register unit blocks. | +| **11** | **Offer a read-only inspection/check path and reproducible review export.** | Lets local checks, refresh and CI inspect without replacing generated trees; reduces committed-page freshness plumbing. General to all adopters and Protocol self-hosting. | Reuse exported extraction/validation/rendering in memory; publish artifacts in a separate explicit step. Export wrapper identifies inputs and comparison base; pure pages remain deterministic graph projections. | +| **12** | **Fix installability and engine provenance.** | Retires tarball preparation and dependency-string workarounds; prevents confusing an engine change with a corpus change. Universal adopter need. | Reproducibly publish/install built packages and expose package version plus build commit. Keep engine identity distinct from graph `schemaVersion`, and place generation provenance in the artifact wrapper. | + +These changes do not require a universal consensus engine, an OD state machine in the graph, or per-entry implementation facts. The strongest return comes from supplying the missing review assembly and portable adapters around the existing graph, while moving durable choices into the graph homes already ruled. diff --git a/.omo/evidence/plan-41/research/self-hosting-audit-astra.md b/.omo/evidence/plan-41/research/self-hosting-audit-astra.md new file mode 100644 index 00000000..c9eb19b9 --- /dev/null +++ b/.omo/evidence/plan-41/research/self-hosting-audit-astra.md @@ -0,0 +1,304 @@ +The Protocol self-hosts its extraction and validation well. Its weaker area is **using the corpus as the working environment for iterative design**. Much of the necessary representation already exists, but the repository uses it unevenly: one enormous review Pack, incomplete question addressing, little declaration-level design, and live guidance that still points to superseded decisions. + +The first adopter’s strongest contribution is a repeatable review practice around capability Packs. Copying that practice would deliver more immediate value than adding another primitive. + +This audit changed no files and ran neither `sdp build` nor `sdp validate`. Counts below were measured through read-only `q` calls on 2026-10-10. They are not a full-green-gate claim. + +## 1. Where the Protocol’s design lives today + +### The graph + +I extracted and executed recipes **1, 2, 7, 8, 9, 11, 20, 22 and 26 verbatim** from their fenced JavaScript blocks. Recipe 9 used its catalog sample, `spec:model.enrichment-lifecycle`. I also ran recipes 24 and 27. + +References to “R1”, “R20”, etc. below mean those executions of the [recipe catalog](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/agent-surface/recipes.md:81). + +The graph contains **228 Specs, 2 Packs, 465 nodes and 1,076 edges**. + +| Spec family | idea | scoped | defined | ready | Total | +|---|---:|---:|---:|---:|---:| +| carrier | 0 | 0 | 22 | 25 | 47 | +| consumers | 3 | 10 | 6 | 26 | 45 | +| decisions | 0 | 2 | 1 | 40 | 43 | +| extraction | 0 | 0 | 5 | 19 | 24 | +| model | 0 | 1 | 8 | 9 | 18 | +| observation | 1 | 0 | 0 | 0 | 1 | +| protocol | 0 | 0 | 2 | 0 | 2 | +| validation | 0 | 0 | 14 | 34 | 48 | +| **Total** | **4** | **13** | **58** | **153** | **228** | + +The family count was derived with this query body: + +```js +const families = {}; +for (const s of g.specs()) { + const family = s.id.slice(5).split(".")[0]; + families[family] ??= { idea: 0, scoped: 0, defined: 0, ready: 0 }; + families[family][s.statedReadiness]++; +} +return { + specs: g.specs().length, + nodes: graph.nodes.length, + edges: graph.edges.length, + families, +}; +``` + +| Measurement | Current result | +|---|---| +| R1 operational build backlog | **0**; excludes 66 ready examples and 40 ready decisions; no excluded example lacks a verifier | +| R2 implemented below ready | **16**, all stated `defined`, all derive `ready`, all have no unmet floor clause | +| R7 stated readiness above the floor | **0** | +| R8 findings | **0 errors, 14 warnings**: 5 `honesty/gaps`, 9 `conformance/prose-mentions` | +| R9 sample | Enrichment lifecycle is `scoped`; `defined` is held by a blocking open question | +| R11 lower ladder | **75 Specs**: 4 idea, 13 scoped, 58 defined | +| R20 open questions | **44 on 25 Specs**; 9 blocking across 8 Specs; no malformed questions | +| Question keys, counted from `sections.intent.openQuestions` | **27 keyed, 17 unkeyed**; 4 of the unkeyed questions block | +| R22 mention audit | 48 occurrences, 42 pairs: 25 forward-backed, 8 reverse-only, 9 unbacked, 0 unresolved | +| R26 dependency cycles | **0** cyclic sets and **0** self-dependent Specs | +| R24 pinned declarations | **9 entries on one Spec**, all custom-element names on `spec:consumers.spec-studio.components` | + +Only **7 Specs have a Design section**, and **7 have a UI section**. That is not a quality score, but it explains why self-hosting exercises the adopter’s design-heavy usage poorly. + +The two Packs are: + +| Pack | Membership and purpose | +|---|---| +| [`pack:self-hosting-v1`](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/self-hosting.pack.sdp.md:1) | 225 members: 150 ready, 58 defined, 13 scoped, 4 idea. A corpus-wide aggregate, with `protocol-domain` and `core-model` as model references. | +| [`pack:spec-studio-v1`](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/consumers/spec-studio.pack.sdp.md:1) | 12 members, all scoped, in reading order. All 12 also belong to the self-hosting Pack. R27 finds no implementation, reference or verifier bindings on them; 9 members relate to implemented Specs they build on. | + +The Studio Pack is already a useful example of design ahead of code. Its absence of bindings is honest. + +### The other homes + +| Home | What it carries today | What belongs in a ruled graph home | +|---|---|---| +| `plans/` | Commissioning context, execution evidence, historical dispositions and re-entry conditions. Plans 35, 37 and 40 are EXECUTED; 36 is DRAFTED lineage with its commissioned work closed; 38 and 39 are DRAFTED pointers. | Current dependencies, refusals, deferred intent and blocking questions. Plan 38 largely follows this split, but some deferral conditions remain only in older plan prose. | +| Surviving `docs/concept/` documents | Product ambition, principles, old implementation descriptions, deferred representations and roadmap rationale. | Durable intended behavior, constraints, decisions and open questions. Exposition may remain after those truths have carrying Specs. | +| `DECISIONS.md` | Ratified names, concise glosses, curation and canonical pointers. | Decision substance already belongs in decision Specs. The registry should remain an index, but must expose amendments and point to current carriers. | +| `CONTEXT.md` | Ratified terms, aliases, candidate terms and some model exposition. | Keep the vocabulary here. Behavioral guarantees and representation detail belong in Specs rather than accumulating in the glossary. | +| `docs/lineage/` | Historical v0 design and annotation research. | Selected forward intent belongs in sub-ready Specs; the historical text remains evidence. The Studio Pack and plan-40 decisions demonstrate this transfer. | +| `reviews/` | Review prompts, findings and closure evidence. | Accepted changes belong in carrying Specs; unresolved durable questions belong on their subjects. Review occurrences and historical verdicts remain git-backed evidence. | +| `.agents/skills/` and recipes | Operational instructions for reading, authoring and routing work. | Their product guarantees already have consumer Specs. They should cite law, not become a competing owner of it. | +| `AGENTS.md` | Repository instructions and a current-state summary. | Repository operations can remain here. Live readiness and design disposition should resolve through graph queries instead of a growing status digest. | + +The placement rule is explicit in the ready [planning-truths decision](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/planning-truths-placement.sdp.md). [Plan 38](/Users/darkomijic/dev-libar/software-delivery-protocol/plans/38-graph-first-planning-arc.md:15) correctly distinguishes decisions, existing guarantees, blocking holds and lawful non-decisions. Historical plans are not automatically misplaced intent. + +### Design truth that has not fully dissolved + +I searched these concepts through `g.findByConcept(term)` and inspected the returned owners. “No owner found” below means no carrying Spec emerged from those searches; it is not a claim that no related word occurs anywhere. + +| Remaining truth | Evidence and graph position | +|---|---| +| Interoperability with design tools, visual-regression and accessibility artifacts; tenant variants, federation and per-language extraction | [Vision, lines 35–38](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/00-vision-scope-and-mvp-boundary.md:35). Searches for `accessibility`, `visual-regression`, `design-tool`, `tenant`, `federation`, `per-language`, `multi-repo` and `polyglot` returned no Specs. | +| OpenAPI/AsyncAPI and JSON-LD/PROV-O/SHACL export intent | [Consumers, export table](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/06-consumers-and-projections.md:196). `OpenAPI` returned no match. LikeC4 is mentioned by the Studio components Spec, but that is not an export contract. | +| Token-budgeted bundles and GraphRAG retrieval | [Consumers, deferred agent capabilities](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/06-consumers-and-projections.md:100). `budget` and `GraphRAG` returned no Specs. The Studio data question mentions a context bundle without defining it. | +| “One runtime truth” and framework-composition adapter intent | [Authoring, runtime bindings](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/04-authoring-and-binding.md:132). Studio’s `runtimeComposition` question preserves the v0 problem, but searches for `one runtime truth` and `runtime composition` found no carrying law. | +| Harness as a projection of the example space plus an anchored oracle | [Authoring, harnesses](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/04-authoring-and-binding.md:157). Studio captures a harness element and questions about its subject and coverage, but not this complete contract. | +| Optional static-authoring lint, per-ID carrier configuration and generated Spec-ID union | [Authoring, lines 50 and 63](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/04-authoring-and-binding.md:50), [principles, L8](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/01-founding-principles-and-invariants.md:99). `spec-static` and `spec-ids` returned no Specs. Canonical-surface search found existing carrier rulings, not the deferred configuration. | +| Architecture enforcement, incremental extraction, caching and sharding | [Roadmap cut list](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/07-mvp-roadmap-and-open-questions.md:51). Searches found no owners for enforcement, incremental extraction or sharding; the caching hit concerned HTML generation cost. | +| Confidence on inferred edges | [Principle P10](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/01-founding-principles-and-invariants.md:48) says inferred edges carry confidence. The graph edge shape has no confidence field; the Studio responsive Spec records that mismatch as an open question. | + +These need capture or deliberate retirement, not immediate implementation. The ready [concept-dissolution decision](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/concept-docs-dissolve.sdp.md) requires semantic transfer before deletion, with deletion separate from the carrying change. + +## 2. Self-hosting gaps + +### 2.1 Review scope is missing where it matters + +There is no annotations Pack, adopter-hardening Pack or graph-first-planning Pack. A 225-member aggregate cannot provide the same review focus as a capability Pack. + +More concretely, these three Specs belong to **no Pack at all**: + +- `spec:decisions.anchor-binding-grain` +- `spec:decisions.anchor-comment-form` +- `spec:decisions.architectural-annotation` + +Evidence: + +```js +return g.specs() + .filter(s => s.packs.length === 0) + .map(s => s.id); +``` + +Pack membership is optional, so this is a review omission, not a conformance error. Existing Packs are sufficient to repair it; no `inArc` relation is needed. + +### 2.2 Plan 38’s deferral transfer is incomplete + +The graph-first-planning Spec says deferred work carries its own blocking questions, but its [re-entry rule](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/consumers/graph-first-planning.sdp.md:27) merely names three candidates. + +The actual conditions for the **reference projection** and **structural-edge Mermaid** remain in [plan 35’s H table](/Users/darkomijic/dev-libar/software-delivery-protocol/plans/35-agent-surface-adoption-and-self-binding.md:78). Concept searches find each phrase only on graph-first-planning. Neither has its own deferred Spec carrying that condition. + +Studio’s package-home and unserved-reader conditions did transfer into keyed questions. The context-bundle condition remains in plans 35/37 and concept prose without a dedicated owner. + +Smallest repair: capture each surviving deliverable at `idea`, with its actual re-entry condition as a keyed blocking question. + +### 2.3 Seventeen questions cannot be addressed stably + +R20 plus a key count finds **17 unkeyed questions**, including these four blockers: + +| Spec | Blocking question | +|---|---| +| `consumers.graph-first-planning` | How an arc boundary is represented | +| `consumers.impact-graph` | Language-neutral identity and extraction boundary | +| `model.enrichment-lifecycle` | What design detail survives implementation | +| `observation.runtime-overlay` | Observation identity and freshness boundary | + +All IDs in this table have the `spec:` prefix. + +Keys are optional by law. The defect is practical: the Protocol does not consistently use the addressing mechanism it built for the adopter. Keying these questions changes neither their meaning nor readiness. + +### 2.4 The 16-Spec drift queue needs individual dispositions + +R2 identifies: + +| Family | Defined Specs carrying `implemented` | +|---|---| +| carrier | `inline-code-spans`, `markdown-body-grammar` | +| consumers | `adopter-on-ramp`, `agent-surface.address-and-cycle-recipes`, `agent-surface.register-recipes`, `projections-model`, `shipped-protocol-corpus` | +| extraction | `open-section-order`, `pack-member-order`, `regenerability` | +| model | `core-model`, `open-question-keys` | +| validation | `next-rung-floor`, `prose-mentions`, `typed-dependency-floor`, `unbound-example-posture` | + +Every row derives `ready`; none has an unmet floor clause. That does **not** justify bulk promotion. + +Three recurring rows have substantive recorded reasons: unfinished projection/measurement work, unsupported measurement thresholds, and the enrichment-lifecycle question. Those reasons appear in [plan 37’s disposition table](/Users/darkomijic/dev-libar/software-delivery-protocol/plans/37-adoption-tranches-drift-maturation-and-bundle-measurement.md:109). In particular, `regenerability` still asserts “measured evidence” thresholds in its current Spec. + +The useful repair is to separate: + +- complete intent waiting for an owner statement; +- mixed realized and deferred scope needing enrichment or promotion; +- unsupported claims needing evidence or correction. + +Changing `satisfies` to `references` merely to clear the alarm would be equally misleading. + +### 2.5 Five ready Specs have no resolving verifier + +R8 names: + +- `spec:carrier.markdown-authoring` +- `spec:extraction.claim-taxonomy` +- `spec:model.pack-aggregate` +- `spec:model.relations` +- `spec:model.spec-sections` + +These are intentional, oracle-pinned warnings, not newly discovered regressions. See [expected warnings](/Users/darkomijic/dev-libar/software-delivery-protocol/test/self-hosting-oracle/index.ts:52). + +The omission is a direct, reviewable verification story for these broad contracts. A child’s verifier does not automatically verify its parent. Bind a verifier only where the test actually checks the parent’s promise; otherwise retain the honest gap. + +### 2.6 Nine prose pairs lack relations, but not all need one + +R22 reports the following unbacked pairs. IDs omit `spec:`. + +| Mentioning Spec | Target | +|---|---| +| `carrier.markdown-body-grammar` | `validation.authored-honesty` | +| `carrier.markdown-parser` | `carrier.inline-code-spans` | +| `consumers.adopter-on-ramp` | `carrier.markdown-body-grammar` | +| `consumers.delivery-session-on-ramp` | `decisions.planning-truths-placement` | +| `consumers.delivery-session-on-ramp` | `decisions.shipped-projections-frozen` | +| `decisions.architectural-significance-rides-primitives` | `model.structural-patterns` | +| `decisions.carrier-ruling` | `decisions.carrier-universality` | +| `decisions.jsdoc-graph-extraction-refused` | `model.spec-sections` | +| `decisions.planning-truths-placement` | `consumers.impact-graph` | + +The oracle pins the pairs, but supplies no pair-specific reason for leaving them as prose. The adopter’s “relation or recorded reason” practice would make review clearer. Do not invent a dependency merely to silence a warning, especially inside superseded decision records. + +### 2.7 Current prose still teaches superseded intent + +Concrete examples: + +- [Authoring and binding, lines 87–97](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/04-authoring-and-binding.md:87) describes constant-only extraction, one target per anchor and structural fields as aspirational. Plan 40’s ready decisions supersede those descriptions. +- [Roadmap, line 73](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/07-mvp-roadmap-and-open-questions.md:73) still treats richer anchor structure as unresolved; line 102 calls watch/scaffolding forward-looking despite plan 35. +- [Studio lenses](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/consumers/spec-studio.lenses.sdp.md:12) still declares `decidedBy` to superseded MD-34 and asks its architecture question against the old vocabulary. +- [Studio Spec page, line 23](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/consumers/spec-studio.spec-page.sdp.md:23) says component and uses are the only structural fields. +- [AGENTS.md, line 30](/Users/darkomijic/dev-libar/software-delivery-protocol/AGENTS.md:24) says one first-adopter Spec remains held by an owner question. Plan 39 records that authored-entry-order was ratified and built in round 2; the graph states it `ready`. +- AGENTS.md’s “capture-rung” summary also obscures that `structural-patterns` and `structural-self-binding` now state `defined`. + +These are repairable drift, not reasons to reverse the ready decisions. + +### 2.8 The registry does not expose its important exceptions + +[DECISIONS.md’s MD-32 row](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/concept/DECISIONS.md:43) describes the freeze without pointing to the two ready, narrowly superseding decisions: + +- `spec:decisions.authored-entry-order` +- `spec:decisions.question-key-rendering` + +Neither appears in its auxiliary pointer list. D4 still points to concept chapter 06 rather than `spec:consumers.design-review`. + +The graph contains the amendments. The human entry point should reveal them. + +### 2.9 Vocabulary debt needs classification + +A case-insensitive scan of `CONTEXT.md` found no occurrence of: + +> question key; next rung; target rung; dependency cycle; Spec Studio; panel; shell; custom element; gathered intent; hand-off; build stamp; hosted preview; harness; pinned declaration; capability Pack; Pack page; ahead; not compared; anchor site; register row. + +However: + +- **Mention and entry address already have candidate definitions**, explicitly awaiting the checked-mentions record’s `ready` statement. They are not missing. See [CONTEXT.md:232](/Users/darkomijic/dev-libar/software-delivery-protocol/CONTEXT.md:232). +- **Readiness floor and derived readiness are defined**; “stated rung” is already used. +- “Bound” occurs in other ratified senses, but the adopter’s **bound/ahead/not-compared pair** does not. +- “Lens” occurs descriptively; an advisor **pass** is not defined by occurrences of tests passing. + +Prioritize semantic distinctions such as question key, pinned declaration, pass and pair. Ordinary UI nouns can remain local to Studio Specs. The adopter’s work vocabulary should not become new Protocol states. + +## 3. Patterns the adopter uses that the Protocol’s own corpus does not + +A fresh query of the adopter found **243 Specs, 6 Packs, 60 Specs with Design sections, and 166 open questions, all keyed**. Its history-view Pack has **19 members and 53 open questions**. The decision-register JSON contains **175 rows**: 45 decided, 82 sorted, 21 waiting, 12 unsorted and 15 folded. These are local register states, not Protocol readiness. + +| Adopter practice | Evidence | Smallest useful Protocol adoption | +|---|---|---| +| Capability review across package boundaries | [PLAN §6.7](/Users/darkomijic/dev-libar/libar-platform/design/PLAN.md:596); history-view’s 19 members | Add a small ordered Pack for the current design-management work, with shared laws referenced rather than copied. | +| One page combining design and realization | [History-view page](/Users/darkomijic/dev-libar/libar-platform/design/pages/history-view.md:9) combines rungs, outside dependencies, questions, register rows, bindings and pairs | First compose a Pack-scoped recipe from existing reader data. Use it for a real review before choosing a shipped projection change. | +| Pinned declarations compared with implementation | [Emitter](/Users/darkomijic/dev-libar/libar-platform/scripts/pinned.mjs:1), [type comparisons](/Users/darkomijic/dev-libar/libar-platform/tests/types/pinned.test-d.ts:43) | Pin one important Protocol contract, derive it locally, and prove that changing the pin breaks a comparison against implementation. | +| Explicit design ahead of code | [PLAN §6.7, items 1, 3 and 7](/Users/darkomijic/dev-libar/libar-platform/design/PLAN.md:600) | Use `references` for incomplete realization; promote a coherent independently realized part to its own Spec. Keep pair comparisons separate from delivery facts. | +| Repeated review from named perspectives | [Pass task](/Users/darkomijic/dev-libar/libar-platform/design/advisors/task-pass.md:1) | Write an advisory review recipe for one Pack, with Protocol-relevant lenses and findings addressed to entries or sections. Fold accepted changes into Specs; keep pass evidence in git. | +| Questions joined to decisions and proposed rulings | [Register contract](/Users/darkomijic/dev-libar/libar-platform/design/advisors/register.md), [register data](/Users/darkomijic/dev-libar/libar-platform/design/decisions/register.json) | Key the questions and link them from a review docket. Put durable rulings in decision Specs and subject-level `decidedBy` relations. | +| Address integrity outside Specs | [check.py](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:67) sends addresses in code, tests and work records to recipe 25 | Add a repository-local check over explicitly selected files, using the existing resolver. | +| Explicit dispositions for unbacked mentions | [check.py:156](/Users/darkomijic/dev-libar/libar-platform/design/tools/check.py:156) | Record a short reason for each intentionally unbacked self-hosting pair and remove stale dispositions when the warning disappears. | +| Review feedback improves the review page | Pass task requires a sentence on what the page could not answer | Capture those reports as evidence on the relevant consumer Spec instead of designing the entire Studio in advance. | + +Two cautions matter. The adopter’s page is **generated by a hand-built script**, not manually maintained Markdown. And its script still reparses carrier text for entry lines and “other Design entries” through [`specLines`](/Users/darkomijic/dev-libar/libar-platform/scripts/pack-page.mjs:291). That is evidence of a Protocol limitation, not a pattern to copy unchanged. + +## 4. Design-management capability gaps in the engine + +| Need | What is lawful today | Missing capability or ruling boundary | +|---|---|---| +| Show a Pack’s internal shape and outside dependencies | Ordered members, typed relations and Spec contexts already support the query. | A composed review view is missing. No new primitive is needed. Reshaping a frozen shipped projection requires a narrow superseding decision; a local recipe does not. | +| Identify which entries are realized | Split independently tracked content into Specs; use whole-Spec bindings and non-conferring references. | **Departed**, explicitly ruled by ready `spec:decisions.anchor-binding-grain`, MD-37. Entry-address bindings and partial-realization facts were refused. Restoring them requires superseding that decision. | +| Compare a pinned declaration with code | Recipe 24 supplies declarations; adopter-owned generation and tests are expressly allowed. | No generic emitter or comparison model ships. This is a deliberate second-adopter threshold, stated in [contract-declarations](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/extraction/contract-declarations.sdp.md:24), not missing carrier support. | +| Record design passes and findings | Git-backed memos; accepted truth in Specs; remaining uncertainty in keyed questions. | The graph has no pass, reviewer, approval or persisted human-finding type. Add advisory practice first. A new graph model would need a ruling preserving the no-workflow boundary. | +| Join a decision to the exact question it settles | A decision can cite a question address in prose; its subject can declare `decidedBy`. | There is no typed question-resolution edge or persistent closed-question entity. Removing the question also removes its live address. Semantic resolution/history in the graph needs a new ruling; git evidence does not. | +| Link a reader directly to a Design or question entry’s source line | Stable addresses resolve; anchors expose file and line. | Primitive and Pack nodes intentionally lack lines. [Schema comments](/Users/darkomijic/dev-libar/software-delivery-protocol/src/graph/schema.ts:129) explain this, and [regenerability](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/extraction/regenerability.sdp.md:16) forbids consumers reparsing source. A producer-owned entry-location map needs authored design and compatibility decisions. | +| Compare design between review passes | Git provides prior versions; graphs can be derived for chosen revisions. | No shipped semantic design-diff view joins changed entries, decisions, questions and affected Packs. Start as a comparison recipe; do not add a mutable history store. | +| Draw architecture before code exists | Model/contract Specs, Design entries, Packs and typed Spec relations. | **Departed** from v0’s independent declared Component: ready `spec:decisions.architectural-annotation`, MD-38, places the component on realizing code. Inventing pre-code CodeNodes would violate that ruling. | +| Distinguish “next author rung” from “next unmet structural rung” | Stated and derived readiness are both present. | Recipe 9’s `nextRung` is above **derived** readiness. The adopter page describes the rung above **stated** readiness. A combined review must name these separately; they diverge when the floor is already ahead. | + +The last distinction is observable in the code: [recipe 9](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/agent-surface/recipes.md:370) indexes from `derivedReadiness`, while the adopter’s [reading guide](/Users/darkomijic/dev-libar/libar-platform/design/pages/history-view.md:15) describes the next stated rung. An empty failure list must not be presented as owner approval. + +Other v0 differences should remain classified as **departed**, rather than missing features: + +- Named coordinates on one Spec: `spec:decisions.one-primitive`. +- Content-only sections and exclusive promotion: `spec:decisions.content-only-sections`. +- Markdown default: `spec:decisions.carrier-ruling`. +- Rejection of lifecycle gating: `spec:decisions.adopt-the-nouns`. +- Bindings rather than test-result or approval facts: `spec:decisions.binding-not-liveness`. + +The edit-model departure is also explicit current intent, but its carrier is the **defined behavior Spec** `spec:consumers.edit-model`; it should not be misrepresented as a separately ready decision. + +## 5. Top 12 self-hosting improvements, ranked + +| Rank | Improvement | Smallest lawful shape | +|---:|---|---| +| **1** | Repair current guidance that contradicts settled law | Update the stale anchor descriptions, Studio architecture questions and relations, and AGENTS.md’s held-Spec summary. Preserve historical decision records. | +| **2** | Give the active work a reviewable Pack | Add one ordered design-management Pack. Include the three plan-40 decisions in the self-hosting aggregate. No arc primitive or scheduling relation. | +| **3** | Finish question addressing | Add stable keys to the 17 unkeyed questions, starting with the four blockers. Preserve wording and blocking flags. | +| **4** | Complete the plan-to-graph transfer | Capture reference projection, structural-edge diagrams and context bundles at honest lower rungs, carrying the actual re-entry conditions from plans 35/37. | +| **5** | Review the 16 drift rows individually | Prepare owner-review packets from current contexts. Correct unsupported claims and separate mixed scope where warranted; promote only on an owner statement. | +| **6** | Use one Pack review query for actual design work | Compose members, stated/derived readiness, next unmet floor, external dependencies, decisions, questions and R27 bindings. Measure what reviewers still have to assemble manually. | +| **7** | Establish repeatable design passes | Extend the session practice with advisory lenses such as model integrity, authoring/adopter ergonomics, realization/verification and consumer usability. Findings cite entries; durable outcomes enter Specs. | +| **8** | Exercise pinned declarations on the Protocol itself | Select one meaningful public contract. Author the pin, derive a local module and compare it against the implementation. Require a changed pin to fail the comparison before expanding the practice. | +| **9** | Make the decision index reveal current rulings | Add pointers to authored-entry-order and question-key-rendering, annotate MD-32’s narrow amendments, and point D4 to its carrying Spec. Use keyed-question citations in review records. | +| **10** | Disposition the five gaps and nine mention pairs | Bind only meaningful parent verifiers. For each unbacked mention, declare the relation that fits or record why none does. Keep these informative rather than introducing new Protocol gates. | +| **11** | Capture remaining concept-only intent before dissolution | Transfer one coherent topic at a time into existing owners or new idea Specs. Reconcile contradictions first; delete exposition only in a later change after checking the transfer. | +| **12** | Address the demonstrated source-location limitation | Author an entry-location contract and compare representations that keep semantic graph equality stable. Use the adopter’s carrier reparse as the concrete demand; do not broaden this into entry-level delivery facts. | diff --git a/.omo/evidence/plan-41/research/v0-00-02.md b/.omo/evidence/plan-41/research/v0-00-02.md new file mode 100644 index 00000000..324b39ce --- /dev/null +++ b/.omo/evidence/plan-41/research/v0-00-02.md @@ -0,0 +1,353 @@ +# v0 chapters 00 to 02 against the current Protocol: a gap map + +Scope: `docs/lineage/v0-design/00-vision-and-product.md`, `01-core-primitives.md` and +`02-system-architecture.md`, read claim by claim against the corpus and engine on +`feature/design-management-v0` (tree equal to `main` at `822aea4`), 2026-10-10. Every Spec state +below was read from the graph (`pnpm --silent sdp:q`), and the carrying Spec was then read. +"Uncaptured" was checked with at least two concept searches and a grep of `specs/`, `src/` and +`docs/concept/`. Counts are measured, not inherited: re-run the queries rather than quoting them. + +Status words: + +- **realized**: a Spec states it and the engine realizes it. +- **captured-ahead**: a Spec states it below `ready`, or without an implementation. +- **departed**: a decision Spec, or a ruling Spec the corpus treats as law, rules otherwise. + Where the ruling sits only in a `defined` Spec or a concept document, the home column says so, + because re-opening it then needs a new decision, not a supersession. +- **uncaptured**: no Spec states it and nothing rules it out. +- **out-of-scope**: v0 itself deferred it, or it is runtime-specific with no bearing on the + Protocol. + +The map holds 181 claims: 92 realized, 16 captured-ahead, 45 departed, 17 uncaptured, 11 +out-of-scope (chapter 00: 22, 3, 10, 2, 5; chapter 01: 42, 7, 21, 10, 2; chapter 02: 28, 6, 14, +5, 4). The weight of the gaps sits in chapter 01: the `designed` rung, open-question metadata, +relation reasons, and the design and ui details v0 typed. + +Relevance (high, medium, low) is given for uncaptured and partly captured rows only. It is +relevance to design management: holding designs ahead of code, iterating, reviewing, tracking +design against realization, decisions and open questions, and agents reading the graph. + +## Chapter 00: vision and product + +| Claim | v0 ref | Status | Home | Relevance | Proposal | +|---|---|---|---|---|---| +| Delivery truth is scattered (tickets, boards, ADR files, feature files, tests, CI dashboards) and joined by prose links that rot | §1 | realized | `spec:decisions.executable-meta-model` (MD-1), `spec:protocol.self-hosting`, `docs/concept/00` | — | — | +| "If I change this, what Specs and components break?" must have an answer | §1 | realized | `g.blastRadius` (`spec:consumers.reader`, `spec:consumers.reader.changeset-entry`), recipe 4; file-level only | — | Spec-level counterpart is a separate row below (§9) | +| AI work is shallow without a structured graph | §1 | realized | `spec:consumers.agent-surface`, `spec:decisions.agent-front-door` (MD-22) | — | — | +| Compliance evidence is gathered ad hoc each time | §1 | out-of-scope | v0 08 territory; `spec:observation.runtime-overlay` (idea) | — | — | +| The whole lifecycle is authored as typed TypeScript, one primitive from idea to evidence | §2.1 | departed | One primitive realized (`spec:decisions.one-primitive`, MD-4); the carrier is Markdown by default (`spec:decisions.carrier-ruling`, MD-18), TS a lawful per-ID option | — | — | +| Lightweight markers in product code bind intent to implementation | §2.2 | realized | `spec:model.anchors`, `spec:decisions.anchor-comment-form` (MD-36); `src/extract/` comment and constant forms | — | — | +| One extractor derives one graph from model files, markers and inferred structure | §2.3 | realized | `spec:extraction.derive-graph`, `spec:extraction.claim-taxonomy`; `inferred` is designed in and ships empty | — | — | +| Multi-tier validation fails the build on missing trace links, broken IDs, forbidden dependencies and incomplete maturity transitions | §2.4 | departed | Broken IDs and floor failures are errors (`spec:validation.referential-integrity`, `spec:validation.readiness-floor`); missing trace links are informative gaps (`spec:validation.warn-level-signals`); forbidden dependencies stay the adopter's own gate (`spec:decisions.architectural-annotation`, MD-38) | — | — | +| Every human and AI view is generated from the graph, above all an interactive HTML Spec Studio | §2.5 | captured-ahead | Four shipped projections (`spec:consumers.projections-model`, frozen by MD-32); Studio is `pack:spec-studio-v1`, all members `scoped`; `spec:decisions.studio-html-surface` `scoped` | high | Keep; the Studio's blocking questions (`#unservedReader`, `#packageHome`) gate it | +| A patch-back protocol turns Studio edits into validated JSON patches applied to the TypeScript Specs | §2.6 | departed | `spec:consumers.edit-model` (`defined`) and `docs/concept/06` §4; no `ready` decision Spec carries it | high | See "Departures worth re-opening", B | +| A living model links intent, design, code, tests, decisions and runtime evidence | §2 | realized | The graph; runtime evidence is the `observed` fact, `spec:observation.runtime-overlay` (idea) | — | — | +| Statement A: a Spec is a durable statement of desired truth, sparse or rich, one stable format, completeness tightening with maturity | §3.A | realized | `spec:model.core-model`, MD-4, `spec:validation.readiness-floor` | — | — | +| Statement A: one primitive gains facets; refinement is enrichment, not conversion | §3.A | realized | `spec:model.spec-sections` ("Facet" became "section") | — | — | +| Statement B: two orthogonal axes, abstraction and readiness, move independently | §3.B | realized | `altitude` × `readiness` (P8 in `docs/concept/01`, `spec:model.core-model`); the independence rule in `spec:validation.readiness-floor` | — | — | +| Statement B: readiness is a seven-rung ladder, `sketch` to `verified` | §3.B | departed | Four rungs, `idea` to `ready` (`spec:model.core-model`, `defined`; P8); `bound`, `executable`, `verified` became derived delivery facts (D1). No `ready` decision Spec states the rung set | high | See "Departures worth re-opening", A | +| Statement B: each readiness level has a validation profile, enforced by validators | §3.B | realized | Readiness floor and kind evidence (`spec:validation.readiness-floor`, `spec:validation.kind-evidence`, MD-12, MD-13); "profile" became "floor" | — | — | +| Statement C: the graph is the truth and everything else a projection, never authored, edited or committed by hand | §3.C | realized | `spec:extraction.regenerability` (`defined`), `spec:extraction.determinism`, `spec:consumers.wholesale-view-rewrite`; one carve-out, adopted registrars are committed (MD-31) | — | — | +| Diagrams cannot drift, because they are a function of the graph | §3.C | realized | `spec:consumers.mermaid-view`, `--check-clean` | — | — | +| The audience is TypeScript-heavy; compiler leverage is load-bearing | §4 | departed | Markdown carrier (MD-18); declarations are language-neutral (`spec:extraction.contract-declarations`); anchors are still `.ts`/`.tsx` only (MD-36) | — | — | +| Teams with real complexity, explicit modelling and AI agents | §4 | realized | The first adopter, `libar-platform`; the agent surface | — | — | +| Personas: domain engineer, architect, PM/BA, QA, AI agent | §4 | departed | Job stories with no personas (`jtbd-stories/README.md`); no decision Spec | — | — | +| A PM or BA reads and refines Specs, open questions, alternatives, examples and impact without learning TypeScript | §4, §5 | captured-ahead | `pack:spec-studio-v1` (`scoped`); the Markdown carrier already lowers the bar | high | — | +| Domain engineer: anchor once, CI fails on broken links | §5 | realized | Anchors plus `spec:validation.referential-integrity` (error) | — | — | +| Architect: generated diagrams; ADRs are graph nodes that code references | §5 | realized | Decision Specs; code `references` a decision (`spec:decisions.anchor-binding-grain`, MD-37); recipes 17, 18; Mermaid | — | — | +| QA: test tags resolve to typed IDs; orphan tests fail CI | §5 | realized | Test anchors `verifies` (`spec:validation.verification-linkage`); `.sdp.gherkin` tags (`spec:carrier.gherkin-authoring`) | — | — | +| AI agent receives `change-impact-.json` with nodes, edges, source files and unresolved questions | §5, §9 | realized | Scripted reads, not files: `sdp q`, `specContext`, recipe 19; the token-budgeted context bundle is designed in and deferred (`spec:consumers.spec-studio.data#question.aiSlices`) | — | — | +| Auditor: OTel, SLSA and CycloneDX evidence attached to the graph | §5 | out-of-scope | v0 08; `spec:observation.runtime-overlay` (idea) | — | — | +| Complements Jira and Linear; no process state | §7 | realized | `spec:decisions.adopt-the-nouns` (MD-2) | — | — | +| Not a universal requirements tool (ReqIF, SysML later) | §7 | out-of-scope | v0 deferred it | — | — | +| Intent is explicit; inferred edges never pass for declared truth | §7 | realized | `spec:extraction.claim-taxonomy`, `spec:validation.claim-separation`; v0's `confidence` field dropped | — | — | +| TypeScript is the canonical surface; visual edits round-trip through patches | §7 | departed | MD-18; `spec:consumers.edit-model` | — | — | +| Design tools (Figma, Pencil) stay canonical for visual design; the system links to them | §7 | uncaptured | `ui` and `design` are open bags (MD-11); the `doc:` namespace is a named deferral (`spec:model.stable-ids`) | medium | Top recommendation 10 | +| The membrane: ingest LikeC4, OpenAPI, ADR Markdown, Cucumber Messages, OTel, SBOM; emit back | §8 | out-of-scope | `sdp import` (TS to Markdown) realized, the `.feature` adapter deferred; rich exports aspirational (`docs/concept/07`) | — | — | +| North Star: a Pack maturity heatmap (how many bound, how many specified with no implementation) | §9 | realized | Recipe 5 (`byStatedReadiness`, `implementedCount`); Design Review Pack table with stated rung and floor reached (`spec:consumers.derived-readiness-banner`); Studio maturity map `scoped` (`spec:consumers.spec-studio.components`) | — | — | +| North Star: an impact panel, "changing this Spec affects N downstream Specs, components, routes, ADRs" | §9 | uncaptured | File-level blast radius is realized; recipe 19 gives one hop; no transitive Spec-change impact | high | Top recommendation 4 | +| North Star: open questions shown on the Spec | §9 | realized | `spec:model.open-question-keys`, recipe 20, Design Review design questions | — | — | +| North Star: a scenario harness shows the uncovered path | §9 | captured-ahead | Example spaces, oracle, coverage gaps realized (`spec:extraction.executable-contracts`, `spec:extraction.runnable-modules`); the interactive harness is `spec:consumers.spec-studio.components#question.harnessElement` | medium | — | +| North Star: a PM proposes a scenario, exports a patch, `akg patch apply` codemods the Spec and opens a PR | §9 | departed | `spec:consumers.edit-model`; the hand-off is blocking at `spec:consumers.spec-studio.intent-panel#question.handOff` | high | See "Departures worth re-opening", B | +| North Star: the agent writes `verification.tests` and `evidence.verifiedBy` | §9 | departed | Delivery facts are derived; authoring one is refused (`spec:validation.authored-honesty`) | — | — | +| North Star: CI feeds test results back and the Spec becomes `verified` | §9 | departed | `spec:decisions.binding-not-liveness` (MD-7): `has-verifier` says a binding exists | — | — | +| North Star: a status report generated from the graph | §9 | realized | Census and Design Review; the HTML form is the Studio (`scoped`) | — | — | +| Working assumptions: Fastify + Effect, Vitest + Cucumber + Playwright, pnpm, OTel, SLSA | §10 | out-of-scope | Runtime-specific; a Vitest adapter is realized (`src/adapters/`) | — | — | + +## Chapter 01: core primitives + +| Claim | v0 ref | Status | Home | Relevance | Proposal | +|---|---|---|---|---|---| +| Five primitives: Spec, SpecPack, Facet, two axes, SpecRelation | intro | realized | Renamed: Spec, Pack, section, three descriptors, relations (`CONTEXT.md` rejected-terms ledger) | — | — | +| A Spec is a persistent, identified statement of desired truth, the same shape from idea to verified | §1 | realized | `spec:model.core-model`, MD-4 | — | — | +| Outer shape: id, title, kind, abstraction, readiness, optional facets, relations | §1.1 | realized | Closed five-key envelope plus H1 title (`spec:carrier.envelope-contract`) | — | — | +| `owner` on the Spec, a soft pointer to a team or person, never RBAC | §1.1, §10 | uncaptured | Envelope closed; asked at `spec:consumers.spec-studio.spec-page#question.headerFields` | medium | Owner question; the closer need is on open questions (top recommendation 3) | +| `tags` on the Spec | §1.1 | departed | Closed envelope (`spec:carrier.envelope-contract`); `spec:consumers.spec-studio.shell#question.searchTags` | — | — | +| `notes`, free Markdown at any maturity | §1.1 | realized | Narrative and section descriptions (`spec:decisions.prose-ownership`, MD-19; `spec:carrier.prose-ownership-rule`) | — | — | +| Types give shape, validators give completeness; no conditional types | §1.1 | realized | P7 (`docs/concept/01`), `spec:decisions.typing-law` (MD-11) | — | — | +| Nine kinds, including `capability`, `quality`, `interface` | §1.2 | departed | Eight kinds (`spec:model.core-model`): capability is a projection or a Pack (MD-4 consequence), quality is a constraint flavor, interface is `contract`, `model` added | — | — | +| One kind per Spec; a fact that straddles kinds becomes two Specs and a relation | §1.2 | realized | The one-kind rule in `spec:model.core-model` | — | — | +| Nine abstraction levels, `initiative` to `contract` | §1.3 | departed | `altitude` is `epic`, `feature`, `story`; domains and capabilities are projections or Packs (MD-4) | — | — | +| Readiness measures how complete, validated, executable and observed a Spec is | §1.4 | departed | Readiness is design maturity only; realization is derived delivery facts (D1, `spec:model.core-model`) | — | — | +| The `designed` rung and its profile: components, ports and decisions identified; before it, "how" is not required | §1.4, §2.5, §8 stage 4 | uncaptured | No rung, no derived signal, no floor clause reads `design`. The code side is covered: a stub `references` its design (MD-37) | high | Top recommendations 1 and 2; "Departures worth re-opening", A | +| The `bound` rung and its profile: every binding and runtime ID resolves | §1.4, §8 stage 5 | departed | `implemented` delivery fact (`spec:extraction.delivery-facts`) plus the `ready` floor's anchor clause; partial realization refused (`spec:decisions.anchor-binding-grain`, MD-37) | — | See "Departures worth re-opening", D | +| The `executable` and `verified` rungs: mode executable with resolving tests; last result passed within a TTL | §1.4, §2.8, §8 stages 6, 7 | departed | `has-verifier` fact; `verification.mode` is posture (`spec:decisions.verification-posture-not-realization`, MD-23); no pass state (MD-7) | — | — | +| Progression is expected, not linear; a capability may stay `framed` forever without failing CI | §1.4 | realized | Independence across refinement (`spec:validation.readiness-floor`); no gates (MD-2) | — | — | +| "Readiness is a claim. Validators verify the claim." | §1.4 | realized | The floor checks the stated rung; "claim" is now reserved for the claim taxonomy (`CONTEXT.md` locked usage) | — | — | +| Facets are independent and grow by enrichment, never replacement | §2 | realized | Optional sections (`spec:model.spec-sections`); exclusive promotion (`spec:decisions.content-only-sections`, MD-10) | — | — | +| Intent: actor, problem, outcome, value, risks, assumptions | §2.1 | realized | `IntentSection` in `src/model/sections.ts` | — | — | +| Open question with a blocking flag that holds promotion | §2.1 | realized | `spec:model.open-question-keys` (`defined`, implemented); a blocking question holds a Spec below `defined` | — | — | +| Open question `owner` and `links` | §2.1 | uncaptured | Asked at `spec:consumers.spec-studio.spec-page#question.questionMetadata`; the adopter keeps 175 decision rows outside the graph | high | Top recommendation 3 | +| A sketch needs only intent: an outcome or a parent | §2.1, §8 stage 1 | realized | The `idea` floor | — | — | +| Behavior: rules, examples, flows | §2.2 | realized | `BehaviorSection` | — | — | +| A rule has an optional id (promotable) and a rationale | §2.2 | uncaptured | Promotion is realized (MD-10); an inline rule has no key or address and no rationale field (`spec:consumers.spec-studio.lenses#question.ruleCoverage`) | medium | Top recommendation 9 | +| Example: Given/When/Then, prose early, step-bound later, promoted rather than replaced | §2.2 | realized | `gwt` notation, slots and step contracts (`spec:carrier.slot-notation`, `spec:extraction.executable-contracts`); point-per-example (MD-17) | — | — | +| Flow: steps with actor, action, observable | §2.2 | realized | `flows` as prose strings; the `workflow` kind for a multi-step process | — | — | +| Constraint: kind, statement, target, measurableBy; a performance constraint with a target is an NFR | §2.3 | realized | `ConstraintSection` (flavor, statement, target, measurableBy); `spec:validation.kind-evidence` | — | — | +| An NFR target must be a parseable threshold, not "fast enough" | §2.3 | departed | Presence only (`constraintTargetsAreMachineReadable` in `src/validate/readiness-floor.ts`); parsing content would judge quality, which MD-1 keeps out | — | — | +| Model: terms | §2.4 | realized | `ModelSection.terms`; `model` kind evidence | — | — | +| Model: entities, commands, events, states as separate lists | §2.4 | uncaptured | Terms only | low | — | +| A shared model keeps a Pack coherent and slices self-contained | §2.4 | realized | Pack `modelRefs` (`spec:model.pack-aggregate`, `spec:validation.pack-coherence`) | — | — | +| Design: decisions and architecture dependencies | §2.5 | realized | As relations, `decidedBy` and `dependsOn`; sections hold no references (MD-10) | — | — | +| Design: components the Spec realizes, named from the Spec | §2.5 | departed | The component is its anchor; a component no code realizes yet is a Spec; Spec-side binding lists refused (MD-37, MD-38) | medium | Top recommendation 6 | +| Design: ports | §2.5 | uncaptured | `role` can name a unit's pattern (MD-38); no port node; `spec:consumers.spec-studio.lenses#question.architectureVocabulary` | low | Inside top recommendation 6 | +| Design: tradeoffs | §2.5 | realized | `DecisionSection` alternatives, rationale, consequences | — | — | +| Design: diagrams (LikeC4 view ids, image paths) | §2.5 | uncaptured | Mermaid is derived; no typed link to an authored diagram | medium | Top recommendation 10 | +| Design as the "how": keyed entries and signatures | §2.5 | captured-ahead | Open `design` section (MD-11), pinned declarations (recipe 24), `spec:extraction.contract-declarations` (`defined`), authored order (`spec:extraction.open-section-order`, `defined`; `spec:decisions.authored-entry-order`) | high | Top recommendation 1 decides whether a floor reads it | +| Runtime: Fastify routes, Effect layers, Awilix, external systems, event channels, resolved against extracted runtime | §2.6 | out-of-scope | Runtime-composition extraction is aspirational (`docs/concept/07`); `spec:consumers.spec-studio.spec-page#question.runtimeComposition`; the `api:` namespace survives | — | — | +| Bindings: `code` listed on the Spec | §2.7 | departed | Direction inverted: the anchor's `satisfies` (`spec:model.anchors`); a Spec-side binding list refused (MD-37) | — | — | +| Bindings: `tests` listed on the Spec | §2.7 | departed | Test anchors `verifies`; examples declare `verifies` | — | — | +| Bindings: `schemas` | §2.7 | uncaptured | `spec:consumers.spec-studio.spec-page#question.schemaBindings`; the `contract` kind and pinned declarations cover part of it | low | — | +| Bindings: `docs` | §2.7 | captured-ahead | `doc:` is a named deferral (`spec:model.stable-ids`, MD-16) | medium | Top recommendation 10 | +| A validator confirms each binding ID has an extracted location | §2.7 | realized | The `ready` floor's anchor clause; `spec:validation.referential-integrity` | — | — | +| Verification: mode manual, reviewed, contract, executable | §2.8 | realized | `VerificationSection.mode`, MD-23 | — | — | +| Verification: the examples that verify the Spec | §2.8 | realized | The example's `verifies` relation | — | — | +| Verification: runner, lastResult, lastRunAt | §2.8 | departed | MD-7 | — | — | +| Evidence: `implementedBy` filled from edges | §2.9 | realized | `implemented` (`spec:extraction.delivery-facts`) | — | — | +| Evidence: `verifiedBy`, tests that passed in the last build | §2.9 | departed | `has-verifier` says a binding exists (MD-7) | — | — | +| Evidence: `observedIn`, runtime pointers | §2.9 | captured-ahead | `spec:observation.runtime-overlay` (idea, blocking) | low | — | +| Evidence: `buildProvenance` (SLSA) | §2.9 | out-of-scope | v0 08; no bearing on design work | — | — | +| Evidence is never authored; CI fills it, validators read it | §2.9 | realized | Derived facts; `spec:validation.authored-honesty` | — | — | +| UI: Storybook stories, flows, screenshots, accessibility status, Pencil documents, Figma nodes | §2.10 | uncaptured | The `ui` section is an open bag (MD-11); `spec:consumers.spec-studio.spec-page#question.uiStories`. An accessibility status would be a status on a Spec and falls under MD-7 | medium | Top recommendation 10 | +| Pack: id, title, member list | §3 | realized | `spec:model.pack-aggregate`; Markdown manifest (`spec:decisions.pack-markdown-carrier`, MD-25) | — | — | +| Pack intent: outcome and value | §3 | departed | Framing is a plain note; a Pack states no truth (`spec:decisions.pack-reified`) | — | See "Departures worth re-opening", C | +| Pack shared model | §3 | realized | `modelRefs` | — | — | +| Pack constraints that descend to every member | §3 | departed | `spec:decisions.pack-reified`; a constraint reaches a Spec only through its own `constrainedBy` | — | See C | +| Pack abstraction | §3 | departed | MD-4 | — | — | +| Pack readiness computed from its members | §3 | realized | As a distribution, never a rung: recipe 5, the Design Review Pack table | — | — | +| Ideate at the group level: missing Specs, overlaps, inconsistent terms, shared constraints | §3 | realized | Human and agent review over the Design Review Pack page and recipes 5, 9, 27 | — | — | +| Pack coherence at the earliest stage: every term defined, no duplicated intent without `refines` | §3 | departed | `spec:validation.pack-coherence` checks membership and `modelRefs` only and rules out a duplicated-intent check | — | — | +| Relations: refines, dependsOn, decidedBy, constrainedBy, verifies, supersedes | §4 | realized | `spec:model.relations`, `src/model/relations.ts` | — | — | +| `belongsTo` authored on the Spec | §4 | departed | Derived from the Pack manifest (`spec:model.pack-aggregate`) | — | — | +| `exemplifies` | §4 | departed | Dropped: `example` kind plus `refines` plus `verifies` (`CONTEXT.md` relations) | — | — | +| `satisfiedBy` from the Spec to a route, layer or implementation | §4 | departed | Derived `satisfies` from the code anchor (MD-37) | — | — | +| Relation attributes: `refines` rationale, `dependsOn` strength, `constrainedBy` aspect, `supersedes` reason | §4 | uncaptured | A relation carries type, target and claim only; edge-text ownership is a pending deliverable of MD-19 (`CONTEXT.md`) | medium | Top recommendation 7 | +| "Implements" split into a code binding and a runtime binding | §4 | departed | One `satisfies` plus a non-conferring `references`, split by meaning; `implements` refused again (MD-37, MD-38) | — | — | +| Relation constructors keep literal IDs `as const` | §4 | realized | The TS DSL option (`ref()`), MD-18 | — | — | +| One feature holds children at different readiness | §5 | realized | The independence rule in `spec:validation.readiness-floor` | — | — | +| Enrich in place: same id, more detail, no new file | §6 | realized | MD-4, `spec:model.core-model` | — | — | +| Refine into children; the parent stays the starting point for roadmap, AI context and architecture views | §6 | realized | `refines`; recipe 19 refinement neighborhood | — | — | +| Detail survives every later level (intent still answers why at `verified`) | §2.1, §6 | captured-ahead | `spec:model.enrichment-lifecycle` (`scoped`, blocking distillation question) | medium | — | +| IDs are unique, refactor-stable, readable, lintable | §7 | realized | `spec:model.stable-ids`, `spec:validation.duplicate-ids`, `spec:validation.referential-integrity` (nearest-id suggestion) | — | — | +| Grammar `:[#sub-segment]` | §7.1 | realized | `#` now names an entry address, never an identity (`spec:model.stable-ids`) | — | — | +| Namespaces `spec`, `pack`, `component`, `impl`, `api`, `test` | §7.1 | realized | The builders' reserved set, plus `oracle` | — | — | +| Namespaces `capability`, `port`, `layer`, `adr`, `decision`, `external`, `schema`, `event` | §7.1 | departed | Decisions are Specs; capability is a projection (MD-4); layer is an attribute, not a node (MD-38); no builder mints the rest | — | — | +| Branded IDs and a regenerated `SpecId` union checked by `tsc` | §7.2 | captured-ahead | Optional and deferred (`docs/concept/01`, L8); the generated-union pattern is realized for steps | low | — | +| Stage profiles `sketch`, `framed`, `specified` | §8 stages 1–3 | realized | The `idea`, `scoped` and `defined` floors, kind-conditional (MD-12) | — | — | +| No artifact migration; each stage is enrichment | §8 | realized | MD-4 | — | — | +| The primitive set is the smallest closed set; Initiative, Epic, Story collapse into levels or relations | §9 | realized | MD-4; epic, feature and story are altitudes | — | — | +| Capability mapping by a capability-level Spec or Pack | §9 | captured-ahead | Packs realized; a Capability Map projection is named in `CONTEXT.md` and asked at `spec:consumers.spec-studio.lenses#question.capabilityGrouping` | low | — | +| Process state is not modeled | §10 | realized | MD-2 | — | — | +| Time-series data stays outside; the graph holds pointers | §10 | captured-ahead | `spec:observation.runtime-overlay` (idea) | — | — | +| A tenant variation is a child Spec refining the parent | §10 | realized | `refines` and the one-kind rule | — | — | + +## Chapter 02: system architecture + +| Claim | v0 ref | Status | Home | Relevance | Proposal | +|---|---|---|---|---|---| +| The repository is canonical; everything else is derived and regenerable | intro | realized | `spec:extraction.regenerability`, `spec:extraction.determinism` | — | — | +| Layer 1 holds Specs, source, tests, feature files, ADR Markdown cited by id, config | §1 | realized | Carriers and anchors; ADRs are decision Specs; an external ADR (`doc:`) is deferred | — | — | +| Layer 1 changes only by a human or a validated patch | §1, §7 inv. 3 | departed | An agent edits source the way a human does (`spec:consumers.edit-model`) | — | See B | +| Layer 2 is pure, deterministic and stateless | §1 | realized | `spec:extraction.determinism`; `spec:decisions.one-validation-path` (MD-14) | — | — | +| Sub-extractors for the typed model, marker constants and JSDoc tags | §1 | realized | Markdown, TS and Gherkin carriers; constant and comment forms (MD-36) | — | — | +| Decorator extraction | §1 | departed | "The decorator form stays an unextracted representation" (MD-36) | — | — | +| Fastify route, Effect layer and Awilix extractors | §1, §4.1 step 4 | out-of-scope | Runtime-specific; aspirational in `docs/concept/07` | — | — | +| Test discovery by filename and AST signature | §1 | departed | A test binds by an explicit `specTest` anchor (`spec:model.anchors`) | — | — | +| Gherkin parsed with the official parser | §1, §4.1 step 5 | realized | `spec:carrier.gherkin-authoring` (`.sdp.gherkin`, MD-27, MD-28) | — | — | +| Overlay extractors (dependency-cruiser, Nx, CodeQL) add inferred edges | §1, §4.1 step 6 | captured-ahead | `spec:consumers.impact-graph` (idea, blocking); MD-38 keeps any inferred structure advisory | low | — | +| The graph file is schema-pinned and versioned | §1 | realized | `spec:extraction.schema-versioning` | — | — | +| The graph schema is also published as JSON Schema | §1 | uncaptured | TS types only | low | — | +| An in-memory Graph with typed queries (`specs()`, `impactOf()`, `unverifiedExecutable()`) | §1 | realized | Reader with three frozen entry adapters plus 28 recipes (MD-22, `spec:decisions.agent-surface-scripts-graph`) | — | — | +| Layer 4 projections are write-only; nobody edits them | §1 | realized | `spec:consumers.projections-model`, `spec:consumers.wholesale-view-rewrite` | — | — | +| The HTML Spec Studio | §1 | captured-ahead | `pack:spec-studio-v1` (`scoped`) | high | — | +| LikeC4, OpenAPI/AsyncAPI and JSON-LD exports | §1 | uncaptured | Named aspirational in `docs/concept/07`; no Spec | low | — | +| AI slices (per Pack, per capability, change impact) | §1 | uncaptured | `context bundle` is a locked term, designed in and deferred; no Spec states a slice (`spec:consumers.spec-studio.data#question.aiSlices`) | medium | Top recommendation 4 is the scripted form | +| A traceability matrix | §1 | realized | Design Review Spec page bindings, recipe 10, recipe 27 | — | — | +| An ADR index generated from the graph | §1 | uncaptured | `docs/concept/DECISIONS.md` is hand-kept (MD numbers, curation states); recipe 18 ranks decisions but is not a register | medium | Top recommendation 8 | +| Status dashboards | §1 | realized | `spec:consumers.census-page` | — | — | +| The patch-back loop is the only write path from views to source | §2 | departed | `spec:consumers.edit-model` (`defined`), `docs/concept/06` §4 | high | See B | +| An agent writing Specs without `patch apply` is out of scope | §2 | departed | Inverted: the agent is the editor (`spec:consumers.edit-model`) | — | — | +| Without patches, either HTML edits are transcribed by hand or HTML becomes the truth | §2 | departed | The Markdown carrier is readable; the Studio's composing panel writes nothing (`spec:consumers.spec-studio.intent-panel`) | — | — | +| Eleven packages with a top-down dependency order | §3 | departed | One package (`spec:decisions.protocol-naming`, MD-5) with subpaths | — | — | +| Markers import without pulling DSL or runtime weight | §3 | realized | The zero-dependency `/anchors` subpath; the comment form needs no import (MD-36) | — | — | +| Test-side helpers (`specTest`, executable examples) | §3 | realized | `specTest`, `specOracle`, the Vitest adapter (`spec:extraction.example-runner`) | — | — | +| Build flow: discover, extract, validate (errors exit non-zero), project | §4.1 | realized | `spec:extraction.build-pipeline` | — | — | +| Marker provenance `annotation` | §4.1 step 3 | realized | The `anchored` claim | — | — | +| Evidence ingestion fills `EvidenceFacet` and `lastResult` | §4.1 step 7 | departed | MD-7 | — | — | +| Validation emits a report | §4.1 step 9 | realized | The report is data in `sdp q` and the projections | — | — | +| Transactional patch apply: speculative apply, validate, then codemod | §4.2 | departed | "No speculative apply" (`docs/concept/06` §4) | medium | See B: validate a worktree before merge | +| Incremental builds, per-file cache, project references | §5 | captured-ahead | A deferral inside `spec:extraction.regenerability` (`defined`); its evidence line ("comfortable below roughly 50 Specs") predates a 228-Spec corpus and a 243-Spec adopter and should be re-measured | low | Revise the evidence line | +| Routes, layers and ports become a typed runtime subgraph | §6 | out-of-scope | Runtime-specific | — | — | +| Validators check architecture rules (no singleton on a request-scoped layer; no direct dependency on an external system) | §6 | departed | Negative architecture constraints stay declared intent; a corpus enforces them in its own gate (MD-38) | — | — | +| The architecture is read from code that already exists, not drawn separately | §6 | departed | Not inferred from imports (MD-38); kept in spirit, since architecture is annotated at the code site with no separate file | — | — | +| Invariant 1: projections are a pure, byte-identical function of the graph, checked by `--check-clean` | §7 | realized | `spec:consumers.design-review.pure-projection`, the census, Mermaid and Gherkin check-clean rules | — | — | +| Invariant 2: the graph is a pure function of source, with stable ordering | §7 | realized | `spec:extraction.determinism`; authored order kept for Pack members and open sections (`spec:extraction.pack-member-order`, `spec:extraction.open-section-order`) | — | — | +| Invariant 3: CI rejects edits under `generated/`; the Studio never writes | §7 | realized | `generated/` is ignored and wiped on build (`spec:consumers.wholesale-view-rewrite`); adopted registrars committed and byte-checked (MD-31); the composing panel writes nothing | — | — | +| A drift between graph and source is a reproducible extractor bug | §7 | realized | The drift rule; MD-14 | — | — | +| A config file: paths, runtime anchor, overlays, evidence sources, rule severities, projection toggles | §8 | uncaptured | `--root` and `--exclude` (`spec:decisions.exclusion-contract`, MD-20); team floor and severity overrides are named deferrals in `spec:validation.readiness-floor` and `spec:validation.warn-level-signals` | low | — | +| A human approval gate for patches at `readiness >= designed` | §8 | departed | No workflow gates (MD-2); `ready` is a human statement (MD-7) | — | — | +| CI runs typecheck, tests, build, validate, `--check-clean` | §9 | realized | `npm run check`; `sdp build --check-clean` | — | — | +| The Studio is published per PR to a static host; reviewers get a link | §9 | captured-ahead | `spec:consumers.spec-studio.sharing` (`scoped`) | medium | — | +| Structured exit codes 0–3 and a fail-on-warnings switch | §9.1 | departed | Severity is fixed by the Protocol and warnings never fail (`spec:validation.warn-level-signals`) | — | — | +| A watch loop: re-extract, validate, regenerate projections, serve the Studio live | §10 | realized | `sdp validate --watch` (validate only; a lawful non-decision in plan 35) | — | — | +| An IDE language-service plugin | §10 | out-of-scope | v0 called it longer-term | — | — | +| DSL arguments are restricted to static literals; others are rejected | §11 | realized | `spec:extraction.determinism` (static envelope fields fail when not reifiable) | — | — | +| Patch races guarded by `baseGraphHash` | §11 | departed | No patches; git merges | — | — | +| Every validator has a test against a bad graph | §11 | captured-ahead | `spec:validation.validator-self-testing` (`defined`, no binding) | low | — | +| Generated artifacts committed by mistake | §11 | realized | Ignore rules, wholesale rewrite, check-clean | — | — | +| The Studio checks the graph schema version at load | §11 | captured-ahead | `spec:extraction.schema-versioning`; the Studio is `scoped` | low | — | +| No graph database until traversal hurts | §12 | realized | `spec:extraction.regenerability` | — | — | +| No bespoke compiler transformer; tier the gate | §12 | realized | MD-14; P7 | — | — | +| No IDE plugin required | §12 | realized | — | — | — | +| No migration to Effect required | §12 | out-of-scope | Runtime-specific | — | — | +| Existing test stacks are first class; others need adapters | §12 | realized | Vitest adapter; `.sdp.gherkin` | — | — | +| No migration off Jira; no process state imported | §12 | realized | MD-2 | — | — | + +## Top recommendations + +Ranked by value to design management first and closeness to v0 second. None needs a `ready` +decision Spec superseded; the ones that touch a law name it. + +1. **Let a Spec state that its design is done.** v0 had a `designed` rung (01 §1.4, §2.5, §8 + stage 4); the Protocol has no way to say it. The adopter's owner asked for exactly this: "the + Protocol should support Specs at any level of maturity, and that includes a Spec whose key + abstractions are designed as stubs" (`libar-platform/docs/feedback/sdp-feedback-01.md`, "A Spec + with designed stubs"). Its 792 Design entries, 653 of them with inline code, sit on Specs that + read the same at `defined` whether their interfaces are settled or not. The code side landed in + plan 40 (a stub `references` its design). Capture `spec:model.design-maturity` (kind `model`, + rung `idea`) with one blocking question that weighs four answers: a fifth rung between `defined` + and `ready`; a kind-conditional design clause on `ready` (MD-12 already allows one per rung); a + derived design signal that is neither a rung nor a delivery fact; or a recipe only. Two laws + shape it: the typing law (MD-11) closes the `design` section's shape the moment a floor reads + it, and P8's corollary ("significance governs detail") argues against requiring design on every + Spec. +2. **One recipe that lays v0's ladder out as independent columns.** Per Spec in a Pack or corpus: + stated rung and floor reached, Design entries and pinned declarations, shaping decisions with + their rungs, units that reference it, units that satisfy it, enabled verifiers, blocking + questions. That is v0's `designed`, `bound`, `executable` read as derived columns with no + ladder, which recipe 27 already set as the pattern. Today the answer is spread over recipes 5, + 9, 10, 24 and 27, and the adopter rebuilt it by hand in `scripts/pack-page.mjs`. It is cheap, + needs no engine change, and is the reader evidence the adopter's own selection asks for before + a rung is ruled. Home: `spec:consumers.agent-surface.design-ledger-recipe` (behavior, `scoped`). +3. **Give an open question an owner, links and a provisional reading.** v0's `OpenQuestion` had + `owner` and `links` (01 §2.1); keys landed, the rest did not. The adopter keeps 175 decision + rows in `design/decisions/register.json` beside the graph, each with who decides (class and + advisor lens), sources, cites, a provisional reading the design proceeds on, and a lean. Most of + that belongs on the question itself. Status fields (sorted, waiting, decided) must stay out under + MD-2: a decided question leaves the Spec and its ruling lands as a decision Spec. Home: + `spec:model.open-question-fields` (rule, `scoped`), refining `spec:model.open-question-keys`; + `spec:consumers.spec-studio.spec-page#question.questionMetadata` is its first consumer. +4. **Spec-level change impact.** v0's North Star impact panel and `graph.impactOf(specId)` (00 §9, + 02 §1) ask what follows when a design changes. Blast radius answers for files; recipe 19 gives + one hop. A recipe that walks inbound `refines`, `dependsOn`, `constrainedBy` and `decidedBy` + transitively, then lists each dependent's referencing and satisfying units, verifiers, Packs and + the prose that mentions it, is the "change to the design asks the code to follow" list MD-37 + implies. Home: `spec:consumers.agent-surface.design-impact-recipe` (behavior, `scoped`). +5. **A typed scoped-intent document as the hand-off.** v0's patch loop (02 §2, §4.2) made a + proposed change reviewable and checked before it touched source. The Protocol refused the patch + format, but the edit model's principle (`docs/concept/06` §4) sets its own re-entry bar: a + structured edit contract when a second machine writer appears. The adopter now has several + machine proposers: its four advisors each write a JSON patch to the decision register, and the + main thread applies each lean to the Specs after a cross-model check + (`libar-platform/design/advisors/README.md`, `register.md`). A typed intent document (scope by id or entry + address, base commit, requested change, reason), checked by running `sdp validate` on a worktree + before merge, keeps what v0 wanted without a codemod. It also answers the blocking + `spec:consumers.spec-studio.intent-panel#question.handOff`. Home: mature + `spec:consumers.intent-composition` (behavior, `idea` to `scoped`) and add a contract child + (`contract`, `idea`). +6. **Component designs ahead of code, compared with the anchored architecture.** v0 named + components and ports on the Spec (01 §2.5). MD-38 keeps a component that no code realizes yet as + a Spec. Make that a convention: a component design Spec carries Design entries for its planned + layer, context and uses, and a recipe compares them with the `component:` anchor once it + exists (no anchor yet, layer or context mismatch, a `uses` edge the design does not name). The + adopter states its component list and layer and context rules in prose (`design/PLAN.md` §2.1) + and enforces isolation in `check.py`. Home: `spec:model.component-design` (model, `idea`) and a + recipe child of `spec:consumers.agent-surface`. It stays inside MD-38, which refused a separate + architecture file; the Spec is the design. +7. **A reason on a relation.** v0's relations carried a rationale, an aspect, a strength or a + reason (01 §4). Today the why sits in narrative prose. On 2026-10-01 the adopter's mention audit + (recipe 22) found 78 Spec pairs named in prose with no relation behind them, 50 with none in + either direction (`sdp-feedback-01.md`); it now lists 12 as accepted warnings. MD-19 left + edge-text ownership as a pending deliverable. Home: `spec:model.relation-rationale` (rule, + `idea`), with the carrier change in `spec:carrier.markdown-authoring`. +8. **A decision register read from the graph.** v0 generated an ADR index (02 §1); the Protocol + keeps `docs/concept/DECISIONS.md` by hand (numbers, curation, `held`, `superseded`), and the + adopter keeps its own register. Decision Specs, `supersedes`, stated rungs and decided subjects + are already graph data. A recipe that lists every decision with its rung (`defined` reads as + held), its supersedes chain both ways, its subjects, its keyed open questions and its + alternatives would replace most of the hand-kept table. Home: + `spec:consumers.agent-surface.decision-register-recipe` (behavior, `scoped`). +9. **Keys on rules.** v0's `RuleSpec` had an optional id (01 §2.2). A rule entry has no address, so + a review, a register row or an example cannot name the rule it is about + (`spec:consumers.spec-studio.lenses#question.ruleCoverage`). The open-question key is the + pattern. Home: `spec:model.rule-keys` (rule, `idea`), refining `spec:model.spec-sections`. +10. **Typed links to external design material.** v0 linked to Figma, Pencil, Storybook, diagrams and + external ADRs (00 §7, 01 §2.5, §2.10). Today they would be untyped strings in an open bag, and + `doc:` is a named deferral (MD-16). Home: `spec:model.external-design-links` (rule, `idea`), + related to `spec:model.stable-ids`. + +## Departures worth re-opening + +Three of the departures rest on a `defined` Spec or a concept document rather than a `ready` +decision Spec: the four-rung ladder (`spec:model.core-model` and P8), the refusal of any structured +edit (`spec:consumers.edit-model` and `docs/concept/06` §4), and the dropped personas +(`jtbd-stories/README.md`). Re-opening any of them takes a new decision Spec that passes the ADR +test; nothing has to be superseded. + +**A. The `designed` rung (re-open).** The other three dropped rungs should stay departed. `bound` +became `implemented`, and `executable` and `verified` became `has-verifier`; the adopter's evidence +supports that split. Nine of twenty mutation runs left a bound test green +(`sdp-feedback-01.md`, "What the build learned"), so a passed run would not have proved +`verified`. `designed` is different: it describes intent, not realization, so it does not collide +with D1. The evidence is the owner's request and the adopter's design passes, which needed a home +for "the key abstractions are settled" (`sdp-feedback-01.md`, items 1 to 3). Two cautions: the +adopter's own selection put "a `designed` rung" under "not now" until a reader pass shows the need +(`sdp-feedback-03-selection.md`), and P8 warns that a fixed tier invites padding. So capture it as +a question now (top recommendation 1), build the ledger recipe (2) as the cheap probe, and rule the +form after that. + +**B. The patch loop, partly (re-open the hand-off, not the codemod).** v0's patch loop gave three +things: a proposed change you can review before it lands, a check before apply, and a guard against +a stale base. The edit model keeps the check, since every edit passes the same validators. It drops +the other two, because the agent edits source directly. Two pieces of evidence meet the re-entry +bar `docs/concept/06` §4 sets, a second machine writer: the Studio's composing panel needs a +hand-off format (blocking `#handOff`), and the adopter's advisors already hand the main thread +structured patches whose leans it applies to Specs. A +typed intent document with a base commit, validated on a worktree, gets back the reviewable, +pre-checked change without a patch schema or a codemod (top recommendation 5). + +**C. Pack intent and descending constraints (hold).** v0 Packs carried intent and constraints that +applied to every member (01 §3). `spec:decisions.pack-reified` refuses this, and the adopter has +shown no case it could not express with framing plus each member's own `constrainedBy`. Keep the +ruling. + +**D. Partial realization, v0's `bound` at entry grain (hold, watch).** MD-37 refused per-entry +`implemented` on 2026-10-10, unanimously. The cost is visible: at the adopter, 15 Specs moved from +`satisfies` to `references`, and `implemented` fell from 24 Specs to 9 (`pr-12-annotations-section.md`). +Recipe 27 still shows the difference: referenced but not satisfied. Keep the ruling while the +adopter tests the split path on the seven Specs it found with parts that could split. Re-open only +if those splits produce artificial child Specs. + +**E. Components named on the Spec (no re-open).** MD-38's retained limitation is sound. A component +design Spec as a convention (top recommendation 6) recovers v0's design-ahead view without a second +place for the component to drift. diff --git a/.omo/evidence/plan-41/research/v0-03-05-astra.md b/.omo/evidence/plan-41/research/v0-03-05-astra.md new file mode 100644 index 00000000..43c76ae2 --- /dev/null +++ b/.omo/evidence/plan-41/research/v0-03-05-astra.md @@ -0,0 +1,155 @@ +The largest design-management gap is **architecture that exists before its code**. The Protocol now records code structure well, but the ready architectural-annotation decision explicitly excludes independently declared Components. Runtime composition has a related gap: `uses` records a dependency, but cannot distinguish an interface requirement, its provider, a selected implementation, or a test substitution. + +Several other v0 ideas already have homes. Ordered Pack membership, entry addresses, readiness floors, claim separation, and typed graph queries are realized. Graph comparison, configurable floors, runtime observations, and much of the Studio presentation are captured ahead. Restoring v0’s larger node taxonomy would therefore duplicate some existing capabilities while leaving the more consequential semantic gaps unresolved. + +I read [chapter 03](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/lineage/v0-design/03-graph-metamodel.md) and [chapter 05](/Users/darkomijic/dev-libar/software-delivery-protocol/docs/lineage/v0-design/05-runtime-anchors.md) in full, queried the current graph, and inspected the relevant engine code. No files were edited; no build, validate, or test commands were run. + +Classification notes: + +- **Realized** means the particular claim has a carrying Spec and inspected implementation evidence. It does not mean its tests were executed in this session. +- **Captured-ahead** distinguishes a stated future contract from a question that merely records the unresolved need. A Spec’s overall implementation binding does not realize every deferred clause. +- **Departed** identifies a ready decision that rules otherwise. An absent field or an incompatible current implementation alone is insufficient. +- **Uncaptured** applies to the specific v0 claim. Each such finding was checked with at least two concept searches; representative search pairs appear in the home column. +- Proposal IDs are suggestions, not authored Specs. “Extend” means enrich the existing home; it does not recommend changing its readiness automatically. + +## Chapter 03: Graph metamodel + +| Claim | v0 ref | Status | Home | Design relevance | Proposal: Spec id, kind, rung, or decision to supersede | +|---|---|---|---|---|---| +| One flat graph supplies every consumer; hierarchy is expressed through edges. | Intro; §1; §12 | Realized | `spec:extraction.derive-graph` → [`impl:protocol.derive-graph`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/extract/derive.ts:180). | High | Retain. | +| Publish the graph’s TypeScript contract. | §1 | Realized | `spec:consumers.agent-surface`, `spec:extraction.derive-graph` → `impl:protocol.agent-surface`, `impl:protocol.graph-edge-types`; types exported through `src/index.ts`. | High | Retain. | +| Publish a version-pinned JSON Schema. | §1 | Uncaptured | No publication contract found with `JSON Schema` / `schema publication`. `GraphSchema` is presently a TypeScript interface. | Medium | New `spec:extraction.graph-json-schema`, contract, scoped. | +| Include generation time, configuration hash, source-file manifest, validation, and evidence index in the graph envelope. | §1 | Uncaptured as that envelope | `configHash` / `sourceManifest` and `file hashes` / `reproducibility` find no contract. Current graph holds version, nodes, edges; reports are separate. `spec:extraction.determinism` deliberately excludes wall-clock and run-specific variation. | Medium | New `spec:extraction.snapshot-context`, contract, scoped. Keep reproducibility metadata outside the deterministic graph unless a consumer needs it inside. | +| Specs preserve authored detail and expose stated versus derived readiness. | §2.1 | Realized | `spec:model.spec-sections` → `impl:protocol.spec-sections`; `spec:validation.readiness-floor` → [`impl:protocol.readiness-floor`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/validate/readiness-floor.ts:613). Derived readiness is returned by the reader, not stored on the raw Spec node. | High | Retain the semantics; no need to copy the old representation. | +| Store incoming/outgoing edge counts on each Spec for UI ranking. | §2.1 | Uncaptured | `incomingEdges` / `degree` found no count contract. Existing edges make the calculation possible. | Low | Recipe first; no new Spec unless a consumer requires a stable ranking contract. | +| Separate Capability, Decision, Rule, Example, and Constraint node kinds, with their own delivery descriptors. | §2 | Departed | `spec:decisions.one-primitive`, ready. These are Spec coordinates; capability is a projection or Pack. | High | Retain the decision. | +| A Pack carries its own intent, shared model, constraints, and readiness. | §2.2 | Departed | `spec:decisions.pack-reified`, ready. Packs state no system truth; shared models are referenced Specs. | High | Retain. Put truth in member Specs. | +| Preserve Pack member order alongside membership edges. | §2.2 | Realized | `spec:extraction.pack-member-order`, stated defined → [`impl:protocol.pack-member-order`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/extract/derive.ts:94). | High | Retain; this v0 capability has returned. | +| Lightweight capabilities can exist before a full design; capability aliases resolve to a Spec. | §2.3 | Departed for separate identities | `spec:decisions.one-primitive`, ready, places capabilities in Specs/projections/Packs. A separate `capability:` alias has no current contract. | High | Use idea/scoped behavior Specs and capability Packs. Do not restore duplicate identities. | +| Decisions are standalone ADR nodes with proposed/accepted/deprecated/superseded lifecycle status. | §2.4 | Departed | `spec:decisions.one-primitive` and `spec:decisions.adopt-the-nouns`, ready. Decision content is typed Spec content; lifecycle gating is rejected. | High | Retain decision Specs and git history. Review whether decision queries answer adopter needs before adding statuses. | +| Decision context, alternatives, consequences, shaping relations, and replacement are graph-visible. | §2.4; §3 | Realized | `spec:model.spec-sections` → `impl:protocol.spec-sections`; `spec:model.relations` → [`impl:protocol.spec-relations`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/model/relations.ts:63). | High | Retain; improve presentation through the existing Studio work. | +| Components carry layer and bounded context and collect implementations. | §2.5 | Realized for anchored components | `spec:model.anchors` → `impl:protocol.anchor-model`, `impl:protocol.anchor-extraction`; `CodeNode.layer/context` and `memberOf` are implemented. | High | Retain. `spec:model.structural-patterns` is defined and records the broader authoring intent. | +| Components can be independently declared before code, with package/capability association. | §2.5 | Departed for independent Components | `spec:decisions.architectural-annotation`, ready, expressly refuses a separate architecture declaration and an unrealized Component CodeNode. | High | Supersede that decision narrowly; propose `spec:model.planned-architecture`, model, scoped. | +| Referencing an unknown component creates an implicit node and warning. | §2.5 | Departed | `spec:decisions.architectural-annotation`, ready, requires structural targets to exist; dangling targets are errors. | Medium | Retain explicit identity. Planned architecture should not depend on silently invented nodes. | +| Ports preserve interface identity, signatures, and schema links independently of implementations. | §2.6 | Captured-ahead as an open need; concrete contract uncaptured | `spec:consumers.spec-studio.spec-page#question.runtimeComposition` and `spec:consumers.spec-studio.lenses#question.architectureVocabulary`, scoped. `Port node` / `interface identity` find no model contract. | High | New `spec:model.architectural-contracts`, model, scoped. Begin with contract-kind Specs; decide whether separate Port nodes earn their cost. | +| Implementation nodes bind real source locations. | §2.7 | Realized | `spec:model.anchors` → [`impl:protocol.anchor-extraction`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/extract/anchors.ts:121). `CodeNode` records file and line. | High | Retain. | +| Implementation nodes record exported symbol, language, and public/internal visibility. | §2.7 | Uncaptured at that field contract | `exportName` / `exported symbol`, `visibility` / `public internal` find no such model. `spec:consumers.impact-graph`, idea, captures broader symbol extraction only. | Medium | Extend `spec:consumers.impact-graph`; settle symbol identity before adding fields. | +| Schema bindings are graph-visible. | §2.8 | Captured-ahead, question only | `spec:consumers.spec-studio.spec-page#question.schemaBindings`, scoped, explicitly asks whether schema bindings need a home. | High | New `spec:model.schema-bindings`, model, scoped. | +| Schema nodes carry representation/source and convert Zod, TypeBox, etc. to JSON Schema. | §2.8 | Uncaptured | `schema conversion` / `zod` and `JSON Schema` / `schema publication` find no conversion contract. | Medium | New `spec:extraction.schema-adapters`, behavior, idea, after schema-binding identity is settled. | +| Event nodes carry channel/schema/AsyncAPI identity and connect emitters and handlers. | §2.9 | Uncaptured | `Event node` / `AsyncAPI`, `handles` / `channel` find no event model. | High | New `spec:model.event-topology`, model, scoped. Model intended event contracts before runtime extraction. | +| Route, Layer, and external-system information appears in design review. | §§2.10–2.11; §2 architecture list | Captured-ahead, questions only | Studio `runtimeComposition` and `architectureVocabulary`, scoped, name the missing data explicitly. | High | New `spec:model.runtime-composition`, model, scoped; separate intended composition from realized bindings. | +| Route nodes preserve HTTP method/path, schemas, plugin, and component. | §2.10 | Uncaptured as a node contract | `route` finds the Studio question; `HTTP method` / `API path` find no semantic contract. An `api:` CodeNode is currently just a code binding. | Medium | New `spec:model.transport-contracts`, model, scoped. Keep Fastify extraction in an adapter. | +| Layers record provided/required interfaces, lifetime, framework, and test replacement. | §2.11 | Captured-ahead for composition; uncaptured for substitution | Studio `runtimeComposition`, scoped, records the composition question. `testDouble` / `substitution` find no replacement contract. | High | `spec:model.runtime-composition`, model, scoped; new `spec:model.implementation-substitution`, model, scoped. | +| Effect `Context.Tag` has its own node. | §2.12 | Out-of-scope | Effect-specific identity. The general interface-identity requirement is assessed above. | Low | No core Tag kind; an adapter may map tags to the eventual interface model. | +| One test explicitly verifies multiple Specs. | §2.13 | Realized | `spec:model.anchors` → `impl:protocol.anchor-model`, `impl:protocol.anchor-extraction`; plural `verifies` derives one edge per target. | High | Retain. | +| Test nodes retain runner, title, and nested example path. | §2.13 | Captured-ahead for runner display; remaining fields uncaptured | `spec:consumers.spec-studio.verification-panels#question.runResults`, scoped. `testTitle` / `test execution` find no Test-node metadata contract. | Medium | New `spec:observation.test-evidence`, contract, scoped; distinguish test identity from execution identity. | +| TestRun is one execution with result, duration, start time, and Build link; ingest reporter output. | §2.14 | Captured-ahead as a consumer question; ingestion contract uncaptured | Studio `runResults` and `testRunResults`, scoped. `durationMs` / `startedAt` find no execution schema. | High | `spec:observation.test-evidence`, contract, scoped. Preserve `has-verifier` semantics. | +| Build and Deployment evidence links commits, pipelines, artifacts, attestations, SBOMs, environments, and times. | §2.15 | Captured-ahead at presentation level | `spec:consumers.spec-studio.verification-panels` and `.lenses`, scoped, explicitly name these views. `pipelineRunId` / `commitSha` find no underlying node schema. | Medium | New `spec:observation.delivery-evidence`, contract, scoped. | +| Observations link external runtime signals to measurable Spec targets. | §2.15 | Captured-ahead | `spec:observation.runtime-overlay`, idea, holds the unresolved identity/freshness boundary; Studio evidence panels are scoped. | Medium | Mature the existing Spec. Do not infer `observed` from implementation bindings. | +| Authored `refines`, `dependsOn`, `decidedBy`, `constrainedBy`, `verifies`, and decision replacement are typed edges. | §3; §3.1 | Realized | `spec:model.relations` → `impl:protocol.spec-relations`; `spec:validation.claim-separation` → [`impl:protocol.claim-separation`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/validate/validators.ts:508). | High | Retain. | +| Pack/capability membership and component containment share broad `belongsTo`/`contains` semantics. | §3; §3.1 | Departed | `spec:decisions.pack-reified`, `spec:decisions.one-primitive`, and `spec:decisions.architectural-annotation`, ready. Current `belongsTo` is Spec→Pack; `memberOf` is code→component. | High | Retain the distinctions. Reconsider only planned-component representation. | +| Code realization may be declared from the Spec as `satisfiedBy`; `implements` is another binding slot. | §3; §3.1 | Departed | `spec:decisions.structural-anchor-semantics`, retained by `spec:decisions.architectural-annotation`, ready: `satisfies` is the sole realization slot. | High | Retain code→Spec realization. Any interface-provider relation must have different semantics. | +| Explicit example-to-Spec verification and test-to-Spec verification remain distinguishable. | §3; §3.1 | Realized | `spec:extraction.delivery-facts` → [`impl:protocol.delivery-facts`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/graph/delivery-facts.ts:44). Declared versus anchored `verifies` preserves the distinction. | High | Retain. `refines` supplies the example’s parent relation rather than restoring `exemplifies`. | +| `implements`/`exposes`, `provides`/`requires`, and `invokes` form an interface/runtime edge table. | §3; §3.1 | Captured-ahead as unresolved architecture vocabulary | Studio `architectureVocabulary` and `runtimeComposition`, scoped; no admitted endpoint/claim contracts exist. | High | Define those contracts in `spec:model.architectural-contracts` and `spec:model.runtime-composition`, model, scoped. | +| `handles` and `emits` connect implementations to events. | §3; §3.1 | Uncaptured | `handles` / `Event node`, `emits` / `AsyncAPI` find no applicable contract. | High | `spec:model.event-topology`, model, scoped. | +| Imports and calls are inferred only; exhaustive mechanical reach remains advisory. | §3; §4.3 | Captured-ahead | `spec:consumers.impact-graph`, idea. Current derivation produces no inferred structural edges. | High | Mature that Spec independently of curated architecture. | +| `ranAs`, `builtFrom`, `deployedFrom`, and `observedIn` connect execution/delivery evidence. | §3; §3.1 | Captured-ahead in purpose; exact edge table uncaptured | Runtime-overlay idea and Studio evidence scoped; `TestRun` / `Deployment` find presentation homes, not edge contracts. | Medium | Specify edges and claims within the proposed evidence contracts, not as UI inventions. | +| `refines` rejects a child at a higher altitude than its parent. | §3 constraints | Uncaptured | `refinement altitude` / `higher altitude` find no such clause. Inspected `checkEdgeContractRow` checks Spec endpoints, not altitude ordering. | Medium | New `spec:model.refinement-semantics`, rule, scoped; decide whether ordering is a law or advisory signal. | +| Every edge has checked endpoint types and claim rules. | §3; §3.1 | Realized for the current vocabulary | `spec:validation.claim-separation` → `impl:protocol.claim-separation`. Current table is closed at twelve edge types. | High | Extend the same table when new semantics are ratified. | +| Declared intent, human bindings, and inferred structure never collapse; inferred structure is not silently promoted. | §4 | Realized for current claims; future inference captured | `spec:extraction.claim-taxonomy` → [`impl:protocol.graph-claims`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/graph/schema.ts:35); `spec:consumers.impact-graph`, idea, prohibits silent rewriting. | High | Retain. Treat `anchored` as authoritative binding, not weaker intent. | +| Edges retain file/line/marker or extractor identity, confidence, and reasoning. | §4 | Uncaptured as edge-level attribution | `edge source` / `extractor identity`, `confidence` / `reasoning` find no contract. `GraphEdge` currently has only endpoints, type, and claim. | High | New `spec:extraction.fact-attribution`, contract, scoped. Separate epistemic claim from where/how the fact was obtained. | +| Inferred suggestions have visibly different presentation and require human action to become declarations. | §4.3 | Captured-ahead in principle | `spec:consumers.impact-graph`, idea, captures assistive suggestions; `spec:consumers.intent-composition`, idea, captures the future interaction. Exact dotted/faded styling is not implemented. | Medium | Extend those homes once an inferred producer exists. | +| Each node has one claim/source owner; duplicate IDs fail loudly. | §5 | Realized | `spec:validation.duplicate-ids` → [`impl:protocol.duplicate-id-exclusion`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/extract/index.ts:92); claim taxonomy supplies one claim per node. | High | Retain. | +| Node provenance distinguishes Spec file, architecture file, marker, Gherkin, extraction, and external evidence. | §5 | Uncaptured as that source taxonomy | `node provenance` / `source-marker` find no contract. Current nodes carry claim and file; binding nodes also carry line. Carrier identity and extraction method are not equivalent to claim. | Medium | Include necessary source attribution in `spec:extraction.fact-attribution`, contract, scoped. | +| Namespaced IDs and `#` identify content inside a Spec without creating another Spec. | §6.1–§6.2 | Realized, narrower | `spec:model.stable-ids` → [`impl:protocol.stable-ids`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/ids.ts:171); prose validation resolves `#design.key`, `#ui.key`, and `#question.key`. | High | Retain and extend only for demonstrated address needs. | +| Arbitrary `#fixtures.*` subpaths, snake_case segments, and literal `api:POST:/orders/:id` IDs are admitted. | §6.1–§6.2 | Uncaptured under current grammar | `API path` / `HTTP method`; `spec:model.stable-ids` explicitly gives the narrower grammar. No ready decision specifically rules on restoring literal transport IDs. | Medium | Extend `spec:model.stable-ids` only after transport identity is designed; prefer method/path as data. | +| IDs freeze at `bound`; a rename requires a superseding identity. | §6.3 | Departed from the lifecycle model | `spec:decisions.adopt-the-nouns` and `spec:decisions.one-primitive`, ready, reject that lifecycle ladder. `spec:model.stable-ids` explicitly puts renames in ordinary git edits. | Medium | Retain git history. Do not restore a maturity-triggered rename gate. | +| Content-hash caches detect renames and suggest `supersedes`. | §6.3 | Uncaptured | `contentHash` / `rename detection` find no contract. | Low | Defer. A future graph-diff consumer can propose identity changes without a permanent cache. | +| IDs are case-sensitive and mixed-uppercase paths produce lint warnings. | §6.3 | Realized for identity; warning uncaptured | `spec:model.stable-ids` → `impl:protocol.stable-ids` preserves case. `uppercase` / `case-sensitive` find no warning policy; mixed-case paths are expressly lawful. | Low | Keep lawful case; no new lint without an adopter need. | +| Generate literal unions of all known IDs so reference typos fail at TypeScript compile time. | §6.4 | Uncaptured | `SpecId union` / `known IDs`, `generated union` / `literal union` find no ID-union contract. Current IDs are branded strings; graph referential checks detect missing targets. | Medium | New `spec:extraction.known-id-contracts`, behavior, idea. Evaluate benefit across Markdown and TS authoring first. | +| Formal readiness clauses compute a structural floor beside the author’s statement. | §7 | Realized | `spec:validation.readiness-floor` → `impl:protocol.readiness-floor`; one table evaluates stated, derived, and prospective rungs. | High | Retain. | +| Runtime binding, execution, and verification are later readiness rungs. | §7 | Departed | `spec:decisions.one-primitive`, `spec:decisions.binding-not-liveness`, and `spec:decisions.kind-conditional-floor`, ready. Design maturity and delivery facts are independent. | High | Retain the separation. | +| Teams override declarative readiness profiles without forking validators. | §7 | Captured-ahead | `spec:validation.readiness-floor`, ready, explicitly defers configurable floors; `spec:validation.warn-level-signals`, ready, similarly defers severity overrides. Neither is implemented. | Medium | Refine with `spec:validation.floor-configuration`, behavior, scoped, only after defining which honesty rules remain invariant. | +| A typed API supports lookups, relations, tests, structural queries, and impact analysis. | §8 | Realized through reader plus scripts | `spec:consumers.reader` → [`impl:protocol.reader`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/reader/reader.ts:404); agent surface and structural recipes supply composable queries. | High | Retain the existing entry adapters and recipes. | +| Every useful graph question becomes a dedicated API method. | §8 | Departed | `spec:decisions.agent-surface-scripts-graph`, ready. New joins remain recipes until the second-consumer threshold is met. | Medium | Do not reproduce the v0 method list. | +| Exhaustive impact and graph-to-graph diffs support change review. | §8 | Captured-ahead | `spec:consumers.impact-graph`, idea, explicitly states `graph(A)` versus `graph(B)` comparison. Current blast radius is curated and one hop. | High | Split out `spec:consumers.design-diff`, behavior, scoped, so authored-design comparison need not await exhaustive symbol extraction. | +| Orphans mean unreachable from every Pack/capability; unverified execution means no passing TestRun. | §8 | Departed in execution semantics; orphan variant uncaptured | `spec:decisions.binding-not-liveness`, ready, rules against reading bindings as runs. Current `spec:validation.warn-level-signals` defines relationless orphans; `ownership` / `capability` do not establish a reachability-orphan rule. | Medium | Offer separate recipes for ungrouped Specs and evidence gaps; do not rename existing signals. | +| The Protocol checks forbidden architectural dependencies. | §8 | Departed | `spec:decisions.architectural-annotation`, ready: negative architecture constraints are declared Specs, enforced by an adopter’s own gate. | High | Retain ownership initially; add reusable graph-check examples rather than silently expanding core validators. | +| JSON-LD, PROV-O, and SHACL provide interoperable graph semantics and validation. | §9 | Uncaptured | `JSON-LD` / `linked data`, `SHACL` / `PROV-O` find no contract. | Low | New `spec:consumers.linked-data-export`, behavior, idea, only for a named consumer. | +| Every graph declares its schema version. | §10 | Realized | `spec:extraction.schema-versioning` → [`impl:protocol.schema-version`](/Users/darkomijic/dev-libar/software-delivery-protocol/src/graph/schema.ts:9); inspected version is `0.8.0`. | High | Retain. | +| SemVer change classes, incompatible-major refusal, and migration are defined. | §10 | Captured-ahead, broadly | `spec:extraction.schema-versioning`, ready, explicitly defers SemVer negotiation and migration until needed. The detailed v0 policy is not yet contracted. | Medium | Refine with `spec:extraction.schema-compatibility`, contract, scoped, when Studio or another external consumer needs it. | +| Migration is a graph patch that codemods canonical source. | §10 | Captured-ahead under a different write model | `spec:consumers.edit-model`, defined, explicitly uses ordinary source edits and rejects structured view patches. No ready decision specifically ratifies the old migration mechanism. | Low | Design migration separately; do not assume the deferred migration command inherits a patch engine. | +| One order-management fragment can feed Studio, architecture diagrams, OpenAPI, and agent context. | §11 | Realized for contextual review; other outputs captured or absent | `spec:consumers.design-review` → `impl:protocol.design-review`; Studio scoped. OpenAPI generation has no home found through `schema conversion` / `JSON Schema`. | High | Use one worked design-to-realization example to prove the proposed contracts; do not treat the historical fragment as executable evidence. | +| Reject dangling references and duplicate identities; never infer verification from filenames; never author delivery evidence. | §12 | Realized | `spec:validation.referential-integrity` → `impl:protocol.referential-integrity`; duplicate exclusion; `spec:model.anchors`; `spec:validation.authored-honesty` → `impl:protocol.authored-honesty-shape`. | High | Retain. | +| Restrict `provides` to Layer→Port and reserve arbitrary metadata for namespaced extension keys. | §12 | Uncaptured | No current `provides` contract. `namespace metadata` / `ext.linear` find no extension policy; current graph nodes/edges have no generic metadata bag. | Medium | Put endpoint rules in runtime composition. Add `spec:model.graph-extensions`, contract, idea, only if extensions are needed. | + +Two v0 inconsistencies should not become requirements accidentally. Its worked fragment uses `satisfiedBy` in both directions. Its broad edge list also admits run→Spec verification while the following constraints restrict verification to Test/Example→Spec. The current explicit direction and claim table are better foundations. + +## Chapter 05: Runtime anchors + +| Claim | v0 ref | Status | Home | Design relevance | Proposal: Spec id, kind, rung, or decision to supersede | +|---|---|---|---|---|---| +| Architecture should be grounded in the application’s actual dependency composition. | Intro | Realized for authored structural bindings only | `spec:model.anchors` → `impl:protocol.anchor-extraction`; `memberOf` and `uses` expose declared code structure. They do not establish actual runtime reachability or composition. | High | Add `spec:model.runtime-composition`, model, scoped, without strengthening existing binding facts. | +| Exactly one mechanism owns runtime composition; mixed mechanisms are temporary and diagnosed. | §1 | Uncaptured | `one runtime truth` / `runtimeAnchor` find no contract. One graph and one canonical carrier do not imply one runtime composition owner. | High | New `spec:model.runtime-composition-ownership`, rule, scoped. Define the ownership boundary per application/context before considering warnings. | +| Runtime-anchor choice selects extractors, conventions, graph variants, and test wiring. | §1 | Uncaptured | `runtime composition` / `wiring` find no adapter-selection contract. | Medium | New `spec:extraction.runtime-adapters`, contract, scoped, after the neutral composition model. | +| Fastify is the recommended HTTP edge; route files contain transport work and invoke use cases. | §2 | Out-of-scope | Framework and application-architecture prescription. The Protocol should describe an adopter’s choices. | Low | No core requirement. An adopter may author its own architecture constraint. | +| One declaration both registers a route and exposes identity, bindings, component, and schemas to extraction. | §2.1 | Captured-ahead for metadata; wrapper uncaptured | Studio `runtimeComposition` and `schemaBindings`, scoped. Current anchors are statically read identity/structure bindings, not runtime registration wrappers. | High | `spec:extraction.runtime-adapters`, contract, scoped. Test whether reading existing registrations avoids another declaration source. | +| Handler inspection derives invocation edges through the selected composition mechanism. | §2.1 | Captured-ahead in purpose only | Studio `runtimeComposition`, scoped; `spec:consumers.impact-graph`, idea, captures mechanical structure. No framework resolver exists. | Medium | Adapter behavior Specs after defining `invokes` and its claim. | +| A route without a recognized invocation warns unless explicitly standalone. | §2.1 | Uncaptured | `route` / `dead registration` find no route-coverage policy. Existing Spec-orphan warnings are unrelated. | Low | Adapter-specific advisory check, not a universal Protocol rule. | +| Logical components must not be inferred from a noisy physical plugin tree. | §2.2 | Realized in the current declaration boundary | `spec:model.anchors` → `impl:protocol.anchor-extraction`; `spec:decisions.architectural-annotation`, ready, refuses inferred authoritative architecture. | High | Retain. Planned components should remain explicit too. | +| Fastify plugin registration and request decorators help resolve dependencies. | §§2.2–2.3 | Out-of-scope as extraction mechanics | Fastify-specific syntax. The reusable requirement is adapter-owned resolution with explicit attribution. | Low | Defer to a Fastify adapter. | +| Effect Tags, Layers, yielded services, and the `R` type supply interface/dependency facts. | §3.1 | Out-of-scope as implementation technique | Effect-specific compiler interpretation. Interface/provider semantics remain relevant and are assessed separately. | Low | Do not adopt these compiler shapes as the Protocol’s model. | +| Live and test implementations provide the same interface; substitution does not rewrite dependents. | §3.2; §5 | Uncaptured as a semantic guarantee | `testDouble` / `substitution`, `Port node` / `interface identity` find no contract. Studio names a test layer but does not define replacement semantics. | High | New `spec:model.implementation-substitution`, model, scoped. | +| A composition root selects providers and establishes that each required dependency is satisfied. | §3.3 | Captured-ahead as a runtime question; completeness rule uncaptured | Studio `runtimeComposition`, scoped. `runtime composition` / `manual factory` find no composition-root contract. | High | `spec:model.runtime-composition`, model, scoped, with selected-provider and unresolved-requirement semantics. | +| An Effect-backed route must have `R = never` after provisioning. | §3.3 | Out-of-scope | Effect-specific proof condition. It is not a general readiness rule. | Low | Adapter check if Effect support is commissioned. | +| Effect is preferred for typed dependencies, scopes, errors, asynchronous construction, and substitution. | §3.4 | Out-of-scope | Framework selection advice. | Low | No Protocol decision required. | +| Awilix is a pragmatic transitional choice for an existing class-DI application. | §4.1 | Out-of-scope | Adoption advice for a particular runtime. | Low | No core requirement. | +| A registration map yields provider identity, dependencies, lifetime, and Spec bindings. | §4.2 | Captured-ahead, question only | `spec:consumers.spec-studio.spec-page#question.runtimeComposition`, scoped, explicitly describes the Awilix case. | High | Express neutral registration facts in `spec:model.runtime-composition`; extract them through an adapter. | +| Validate lifetime compatibility, missing registrations, dead registrations, and cycles. | §4.3 | Uncaptured as runtime checks | `lifetime` / `registration` reach only the Studio question; `dead registration` / `dependency cycle` find no runtime policy. Curated `uses` cycles are expressly lawful under the ready architectural-annotation decision. | High | New `spec:validation.runtime-composition-checks`, behavior, scoped. Distinguish runtime constructibility from general architectural cycles. | +| Dependency edges retain request scope. | §4.4 | Uncaptured | `scope` / `lifetime` find no scope-bearing edge contract. Current `GraphEdge` has no metadata. | Medium | Include lifecycle/scope facts in the composition contract only where they affect design reasoning. | +| Framework changes preserve Spec identity and realization links. | §5 | Realized for binding identity | `spec:model.stable-ids` → `impl:protocol.stable-ids`; `spec:model.anchors` → `impl:protocol.anchor-extraction`. Bindings do not depend on a DI framework. | High | Retain; extend this continuity to interface/provider identities. | +| Awilix→Effect migration can coexist by bounded context, with warnings and a component progress heatmap. | §6 | Out-of-scope for the prescribed migration; general migration view uncaptured | `migration` finds schema/carrier work, not runtime transition semantics; `runtimeAnchor` finds none. | Medium | If demanded, new `spec:consumers.architecture-transition-view`, behavior, idea. Derive coexistence from composition facts, not authored completion percentages. | +| Manual factories are also extractable composition declarations through `defineWiring`. | §7 | Uncaptured | `manual factory` / `wiring` find no supported factory model. Generic `uses` annotations are not equivalent to selected construction wiring. | High | Make a small manual-composition example the first probe for `spec:extraction.runtime-adapters`, before framework adapters. | +| Every implementation needs a wiring entry; custom wiring has weaker validation. | §7 | Uncaptured | `manual factory` / `runtime composition` find no wiring-coverage law. Not every code unit needs runtime construction. | Medium | Scope coverage to declared composition participants; reject the blanket “every `impl:*`” rule. | +| Configuration is itself a design contract with code and schema bindings. | §8.1 | Realized for a contract-kind Spec and code binding; schema part captured-ahead | `spec:model.core-model` → `impl:protocol.spec-primitive`; `spec:model.anchors`; Studio `schemaBindings`, scoped. | High | Use a configuration contract as a schema-binding worked example. No configuration-specific node kind is needed. | +| Configuration schemas can generate external validation/OpenAPI artifacts. | §8.1 | Uncaptured | `schema conversion` / `JSON Schema` find no generator contract. | Medium | `spec:extraction.schema-adapters`, behavior, idea. Keep publication format separate from configuration intent. | +| Logger dependencies and live/test logging implementations appear in composition. | §8.2 | Uncaptured as composition semantics | `provides` / `requires`, `testDouble` / `substitution` find no applicable model beyond Studio questions. | Medium | Use logging as another example of the general composition/substitution contract, not a special feature. | +| Tracing observations link measurable Spec targets to external telemetry. | §8.3 | Captured-ahead | `spec:observation.runtime-overlay`, idea; Studio evidence panels scoped. The single Tracer layer is framework-specific. | Medium | Mature the existing overlay’s identity and freshness question. | +| External systems are explicit boundary identities with dependencies and diagram presence. | §8.4 | Captured-ahead, question only | Studio `architectureVocabulary` and `runtimeComposition`, scoped. `ExternalSystem` / `external systems` reveal no standalone model contract. | High | New `spec:model.external-boundaries`, model, scoped, usable before an adapter is implemented. | +| Queue handlers connect implementations to event contracts; schedules connect triggers to implementations. | §8.5 | Uncaptured | `handles` / `Event node`; `Schedule` / `cron` find no contract. | High | `spec:model.event-topology`, model, scoped; add scheduling only when the worked example needs it. | +| Choose Fastify+Effect, fall back to Awilix/custom, then keep one runtime owner. | §9 | Out-of-scope for choices; ownership uncaptured | Framework preference is not a Protocol concern. The general ownership rule remains the §1 gap. | Low / High | No framework mandate; pursue `spec:model.runtime-composition-ownership`. | +| Other DI/server frameworks can plug in, and workers may be in-process or separate. | §10 | Uncaptured as adapter/topology guarantees | `runtimeAnchor` / `manual factory`, `worker` / `runtime composition` find no general extension contract. | Medium | Define adapter boundaries and process-neutral composition in the proposed runtime Specs. | + +## Top recommendations + +1. **Reopen architecture before code.** Supersede the narrow part of `spec:decisions.architectural-annotation` that makes a component exist only at a realizing anchor. Start `spec:model.planned-architecture` at scoped. Preserve the distinction between intended architecture and anchored realization; do not manufacture CodeNodes to make a design visible. + +2. **Separate design comparison from exhaustive code impact.** The graph comparison promise already lives in `spec:consumers.impact-graph`, idea, but its blocking question concerns language-neutral symbol extraction. A scoped `spec:consumers.design-diff` can compare Specs, entries, questions, relations, and bindings across commits without solving that problem. + +3. **Define interface requirements and provider selection.** Start `spec:model.architectural-contracts` and `spec:model.runtime-composition`, model/scoped. Establish what `uses` cannot say before selecting new node or edge names. Contract-kind Specs may supply the intended interface identity. + +4. **Prove composition with a small manual-factory example.** Show a planned contract, two providers, one selected composition, a missing requirement, and a test substitution. This tests general semantics without committing the Protocol to Effect or Awilix. + +5. **Give external boundaries and event flow a design-time home.** Start `spec:model.external-boundaries` and `spec:model.event-topology`, model/scoped. These matter to reviewing designs ahead of code and are not adequately represented by generic dependencies alone. + +6. **Define fact attribution independently of `claim`.** Start `spec:extraction.fact-attribution`, contract/scoped. Preserve source location, extraction method, and external evidence identity where needed. Keep `declared`/`anchored`/`inferred` as epistemic categories. + +7. **Turn Studio questions into owned model contracts.** Runtime composition, schema bindings, and execution evidence currently have UI questions but no supplying contracts. The Studio should consume those contracts rather than determine them incidentally. + +8. **Add test evidence without changing `has-verifier`.** Start `spec:observation.test-evidence`, contract/scoped. A binding, a particular execution, its commit, and its result answer different questions. The adopter’s pinned-declaration comparisons provide a concrete consumer. + +9. **Use existing readiness machinery before expanding it.** Prospective floor failures, keyed questions, and typed dependencies already support iterative design. Configurable floors are captured; they should follow a demonstrated need, not a return to v0’s seven-rung ladder. + +10. **Defer interoperability formats and cache machinery.** JSON-LD, SHACL, generated ID unions, and hash-based rename suggestions are legitimate uncaptured claims, but they have less immediate design value than planned architecture, comparison, and composition. + +## Departures worth re-opening + +**The realized-only Component rule is the strongest case.** The ready [architectural-annotation decision](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/architectural-annotation.sdp.md) explicitly says an unrealized component lives in Specs and has no CodeNode until an anchor exists. That prevents fake realization, but leaves the intended architecture without an equivalent structural view. The scoped Studio architecture and runtime questions expose the consequence directly. Reopen the representation of planned architecture while preserving anchored realization as separate evidence. + +**The refusal of additional structural semantics deserves a narrower review.** The [structural-anchor decision](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/structural-anchor-semantics.sdp.md), retained in the architectural-annotation ruling, rightly refuses `implements` as a duplicate of code-to-Spec `satisfies`. But v0 also asks a different question: which implementation provides an interface, and which composition selects it? Restoring that distinction need not restore the ambiguous `implements` slot. A superseding decision should state the distinct semantics and endpoint contracts. + +**The projection freeze should reopen only for a concrete reader need.** The [shipped-projections decision](/Users/darkomijic/dev-libar/software-delivery-protocol/specs/decisions/shipped-projections-frozen.sdp.md) requires a superseding decision for reshaping the existing views. The adopter’s [feedback on iterative review](/Users/darkomijic/dev-libar/libar-platform/docs/feedback/sdp-feedback-02.md:118) supplies specific evidence: compare changes since a design pass, inspect declaration differences, and keep rationale near the affected design. A new comparison view may satisfy that need without reopening every shipped projection. + +**Core enforcement of architecture constraints is a conditional case, not an immediate recommendation.** The architectural-annotation decision leaves negative dependency checks to adopter gates and accepts multi-node `uses` cycles. V0’s runtime checks concern different facts, such as an unresolved provider or incompatible lifetime. First model those facts and prove an adapter check. Only then decide whether any conformance check belongs in the Protocol. + +I would retain the departures that preserve one Spec primitive, truth-free Packs, independent design readiness and delivery facts, explicit verification bindings, and ordinary source edits. None blocks the design-management improvements above. diff --git a/.omo/evidence/plan-41/research/v0-04-06.md b/.omo/evidence/plan-41/research/v0-04-06.md new file mode 100644 index 00000000..3ae2a224 --- /dev/null +++ b/.omo/evidence/plan-41/research/v0-04-06.md @@ -0,0 +1,264 @@ +# v0 chapters 04 and 06 against the current corpus and engine + +Read on 2026-10-10 at `822aea4` (branch `feature/design-management-v0`). Every status below was checked with +`sdp q` (concept search with two or more wordings, `specContext`, the raw graph and `report`) and, where the +graph could not answer, with a grep of `specs/` and `src/`. Adopter evidence is from +`/Users/darkomijic/dev-libar/libar-platform` at `d310eb1` (its PR 12 merge), pinned to `822aea4`. + +Status words: **realized** (a Spec states it and the engine does it), **captured-ahead** (a Spec states it below +`ready`, as an open question, or as a named deferral, without realization), **departed** (a decision Spec, or a +ready Spec carrying a ruling, says otherwise), **uncaptured** (no Spec states it and nothing rules it out), +**out-of-scope** (v0 deferred it, or it is framework-specific). Relevance is to design management: designs ahead +of code, iteration, review, design-versus-realization tracking, decisions and open questions, graph reading by +agents. New captures are proposed at `idea` or `scoped` only. + +## Chapter 04, authoring surfaces + +| # | Claim | v0 ref | Status | Home | Relevance | Proposal | +|---|---|---|---|---|---|---| +| 1 | Four surfaces ranked by canonicity; the TS DSL is the canonical source of truth | intro | departed | the carrier ruling (MD-18, `spec:decisions.carrier-ruling`): Markdown default, TS DSL a per-ID option and import source | low | none | +| 2 | Constructors are pure typed functions; branded IDs checked at call time; the extractor matches `spec(...)` calls | §1.1, §1.3 | realized | `.sdp.ts` option (MD-15), `src/ids.ts` builders, `spec:model.stable-ids` | low | none | +| 3 | Spec files are static data: no conditionals, computed IDs, async, or product imports | §1.2 | realized | `spec:extraction.determinism` (static envelope rule), `src/extract/reify.ts` non-static refusals; spreads are refused where v0 allowed them | low | none | +| 4 | Relation constructors carry metadata: `refines` rationale, `dependsOn` strength, `constrainedBy` aspect, `supersedes` reason | §1.3 | uncaptured | `spec:model.relations` holds bare targets | medium | `spec:model.relation-reasons` (model, idea); needs a decision because the envelope's relation shape is closed. Evidence: the adopter keeps mention pairs "with a one-line reason" in `design/tools/prose-mentions.json` | +| 5 | An `exemplifies` relation from an example to its parent | §1.3 | realized | as a composition: example kind + `refines` + `verifies` (`CONTEXT.md`); closed edge list in `spec:extraction.derive-graph` | low | none | +| 6 | `enrichSpec` splits one Spec across files, merged by ID | §1.3 | departed | one canonical surface per ID (MD-18); `spec:validation.duplicate-ids.dual-carrier` | low | none | +| 7 | `executableExample` and `step` constructors | §1.3 | realized | example Specs, `spec:carrier.slot-notation`, `spec:extraction.executable-contracts`, `spec:extraction.runnable-modules` | low | none | +| 8 | Intent facet: actor, outcome, value, risks, assumptions, open questions | §1.4 | realized | `src/model/sections.ts`; keyed questions `spec:model.open-question-keys` | high | none | +| 9 | Behavior rules and examples; constraints with `target` and `measurableBy`; model terms | §1.4 | realized | `spec:model.spec-sections`, `spec:validation.kind-evidence` | medium | none | +| 10 | Design facet: components, ports, decisions, tradeoffs | §1.4 | realized (reshaped) | `design` is an open bag (the typing law, MD-11); decisions by `decidedBy`; components by anchors (MD-38); pinned declarations `spec:extraction.contract-declarations` (defined) | high | see recommendation 7 and departure C | +| 11 | Runtime facet: routes, Effect layers, external systems on the Spec | §1.4 | departed | anchors name runtime units; "the runtime section is gone" (`docs/concept/04` §3) | low | none | +| 12 | Bindings facet on the Spec: code, schemas, tests | §1.4, §7 | departed | the binding-grain ruling (MD-37) refuses a Spec-side list of bindings; schema bindings asked in `spec:consumers.spec-studio.spec-page#question.schemaBindings` | low | none | +| 13 | Verification facet: mode, runner, tests | §1.4 | realized | mode is posture (MD-23); tests by test anchors | low | none | +| 14 | UI facet: stories, flows, design documents | §1.4, §5 | realized (content only) | `ui` open bag, rendered in authored order (`spec:decisions.authored-entry-order`) | medium | see rows 48 and 49 | +| 15 | `owner` and `tags` on the Spec | §1.4 | captured-ahead (open question) | the envelope is closed (`spec:carrier.envelope-contract`); `spec:consumers.spec-studio.spec-page#question.headerFields` (scoped) | medium | answer `#headerFields`; see recommendation 10 | +| 16 | Claiming `bound` without runtime and bindings fails CI | §1.4 | departed | `bound` became the derived `implemented` fact; D1 in `spec:model.core-model`; binding, never liveness (MD-7) | low | none | +| 17 | File layout: Specs by area, Pack file beside members, `/arch`, ADRs as Markdown, glossary, experiments | §1.5 | realized (reshaped) | `specs//`, `*.pack.sdp.md` (MD-25); decisions are Specs (MD-26); `/arch` refused (MD-38 alternative); `explorations/` excluded | low | none | +| 18 | Glossary terms cited from `model.terms` catch typo drift | §1.5 | departed | `spec:decisions.checked-mentions` (defined): a model term has no address | medium | owner question: a `#term.` address would let prose cite a term and fail when it is renamed | +| 19 | Specs and code linked only by ID strings; `no-cross-imports` lint both ways | §1.6 | realized (partly) | `ref()` and `@sdp*` tags carry string IDs; the TS carrier trusts only Protocol builders; no lint on product code importing a carrier | low | none | +| 20 | A marker binds a code location to a graph ID and nothing else | §2 | realized | `spec:model.anchors`; MD-36, MD-37, MD-38 | high | none | +| 21 | Decorator markers on classes | §2.1 | departed | the comment form (MD-36): "the decorator form stays an unextracted representation" | low | none | +| 22 | Method-level markers (`@arch.handles` on a method) | §2.1 | departed | MD-36: a nested block is a misplaced-tag error; the file is the binding grain (plan 40, R4) | low | none | +| 23 | JSDoc tag markers, TSDoc-compatible, multi-value lists | §2.2 | realized | comment form `@sdpAnchor`, `@sdpSatisfies`, …, comma lists (MD-36); `src/extract/anchors.ts` | high | none | +| 24 | Marker constants (`markImplementation` identity wrapper) | §2.3 | realized | constant form `codeAnchor` / `specTest` / `specOracle`, zero-dependency `/anchors` subpath (MD-36) | medium | none | +| 25 | Marker carries id and kind; `component`; optional plural `satisfies` | §2.4 | realized | kind by namespace; MD-37, MD-38 | high | none | +| 26 | `capability` on a marker | §2.4 | departed | one primitive, named coordinates (MD-4): capabilities are projections or Packs | low | none | +| 27 | `implements` a port | §2.4 | departed | MD-38 refuses `implements` again | low | none | +| 28 | `exposes` a port; ports as nodes | §2.4, §7 | captured-ahead (open question) | `spec:consumers.spec-studio.spec-page#question.runtimeComposition`, `spec:consumers.spec-studio.lenses#question.architectureVocabulary` (scoped) | medium | answer those questions; `#architectureVocabulary` still cites MD-34, which MD-38 superseded | +| 29 | `handles` and `emits` events | §2.4, §7 | uncaptured | no event identity or edge; the anchor envelope is closed (MD-36, MD-38) | medium | owner question first: the adopter is event-sourced (deciders, projections, event store). Would amend MD-38 | +| 30 | `visibility` and `tags` on a marker | §2.4 | departed | closed envelope (MD-36); `role` covers part of tags; visibility is MD-38's significance criterion, not an attribute | low | none | +| 31 | Markers never carry readiness, intent, behavior, or verification | §2.4 | realized | `spec:model.anchors`; MD-38 consequence | high | none | +| 32 | `marker-required` lint for chosen significant patterns | §2.5 | captured-ahead | MD-38 consequence "anchor-required lint stays warn-level and optional"; `docs/concept/04` §2; no engine | high | recommendation 8 | +| 33 | Gherkin is an equal-canonicity surface for behavior Specs | §3 | realized | MD-27, MD-28, MD-29; `spec:carrier.gherkin-authoring` (ready, implemented) | medium | none | +| 34 | Tags for spec id, readiness, decisions, NFRs | §3.1 | realized | id, readiness, altitude and relation tags (`@decided-by`, `@constrained-by`, …) | low | none | +| 35 | Tags for pack, capability, component, api, layer, owner | §3.1 | departed | membership is manifest-owned (MD-25); capability (MD-4); bindings run code to Spec (MD-37); no owner field | low | none | +| 36 | Readiness per scenario, the most precise wins | §3.1 | realized | each Scenario is an example Spec with its own readiness | low | none | +| 37 | Feature description sections Intent, Value, Risks, Assumptions | §3.2 | realized | keyed description bullets (`DESCRIPTION_KEYS` in `src/extract/gherkin.ts`) | low | none | +| 38 | Open questions in a Feature description | §3.2 | departed | `spec:carrier.gherkin-authoring`: "no Gherkin form is invented for open questions" | medium | departure E | +| 39 | Step definitions paired with scenarios through Cucumber Messages | §3.3 | departed | the Gherkin option (MD-27): execution through generated contracts and anchored handlers | low | none | +| 40 | Round-trip Gherkin and TS; one canonical form per ID set in config | §3.4 | realized | `sdp gherkin` (`spec:consumers.gherkin-view`), `sdp import` (`spec:carrier.sdp-import`); canonical form chosen per ID by suffix, no config | low | none | +| 41 | Gherkin lint with "did you mean" | §3.5 | realized | `spec:validation.referential-integrity.did-you-mean`, `spec:carrier.gherkin-authoring.unknown-tag-refused` | low | none | +| 42 | Lint reads a generated `spec-ids.d.ts` union | §3.5 | uncaptured | deferred in `docs/concept/01` only; no Spec | low | none | +| 43 | A harness declares controls and an `expected` function per Spec | §4 | realized (oracle half) | oracle = expected: `specOracle`, `spec:validation.oracle-target-eligibility`, the three-way comparator in `spec:extraction.runnable-modules`; controls = example-space slots | high | none | +| 44 | Harness coverage: which control combinations an example covers, and which are missing | §4 | captured-ahead | `spec:consumers.spec-studio.verification-panels` `ui.coverageGrid` (scoped, held behind the Studio's blocking questions); `witness` and `coverage gap` are ratified terms; no Spec or recipe computes them | high | recommendation 1 | +| 45 | The Studio renders a harness as an interactive panel | §4 | captured-ahead (open question) | `spec:consumers.spec-studio.components#question.harnessElement`, `spec:consumers.spec-studio.shell#question.railHarnesses`, `verification-panels#question.coverageSource` (scoped) | medium | none beyond recommendation 1 | +| 46 | "Propose scenarios" writes patches for uncovered combinations | §4 | departed | edit model (`spec:consumers.edit-model`, defined); the coverage grid's action composes intent instead | medium | none | +| 47 | Harness uses: pre-implementation exploration, stakeholder review, agent enumeration of permutations | §4.1 | uncaptured | no Spec | high | recommendation 1 | +| 48 | A harness is not a test runner and not authoritative | §4.2 | realized | the oracle is never extracted (`CONTEXT.md` oracle row) | medium | none | +| 49 | UI references by ID or URL: Storybook, Pencil, Figma | §5.1, §5.2 | captured-ahead (named deferral) | free text in the `ui` bag; external `doc:` targets deferred by carried evidence (MD-16) and `spec:model.stable-ids`; `spec-page#question.uiStories` | medium | none now; the adopter authors no UI section | +| 50 | Playwright run status, visual baselines and accessibility status ingested | §5.3, §5.4 | departed | MD-7: the graph records no run | low | see recommendation 12 | +| 51 | Each role authors on its native surface | §6 | realized (partly) | Markdown default, Gherkin for BDD authors (MD-27), TS option, anchors in code, `sdp-authoring` skill for agents | medium | none | +| 52 | An AI agent generates patches against the DSL | §6 | departed | edit model: the agent edits source and git records it (`spec:consumers.edit-model`) | low | none | +| 53 | One reading surface serves every reader, each from its own concern | §6 | uncaptured | the Studio is scoped (`spec:consumers.spec-studio`); its lenses are graph views (Packs, architecture, tests, evidence), not reader perspectives; `CONTEXT.md` names "discipline (a lens/filter over the graph)" with no Spec | high | recommendation 2 | +| 54 | What stays in code: kind, component, satisfied Specs, ports, events | §7 | realized (partly) | kind, component, `satisfies`, `references` (MD-37, MD-38); ports and events as rows 28 and 29 | medium | none | +| 55 | `/arch` holds capabilities, components, ports, external systems, views, forbidden-dependency rules | §7 | departed | separate architecture file refused (MD-38); capabilities (MD-4); views fixed (MD-32); forbidden dependencies stay out of Protocol validators (MD-38) | high | departures A and D | +| 56 | Triangulation `missing-implementation` | §7 | departed | MD-37 refuses Spec-side bindings | low | none | +| 57 | Triangulation `duplicate-node-id` | §7 | realized | `spec:validation.duplicate-ids`; one id in two anchor forms is a duplicate (MD-36) | low | none | +| 58 | Triangulation `unknown-component` | §7 | realized | every `component` and `uses` target must exist (MD-38); `spec:validation.referential-integrity` | medium | none | + +## Chapter 06, extraction and validation + +| # | Claim | v0 ref | Status | Home | Relevance | Proposal | +|---|---|---|---|---|---|---| +| 1 | The pipeline is pure: same inputs, same outputs | intro | realized | `spec:extraction.determinism`, `spec:extraction.regenerability` | medium | none | +| 2 | Independent extractors emit fragments; one builder merges them | §1 | realized | carrier reification feeds `deriveGraph` once (`spec:extraction.derive-graph`) | low | none | +| 3 | Fastify, Effect and Awilix extractors | §1, §5.4 | out-of-scope | framework adapters are aspirational (`docs/concept/04` §3, one runtime truth) | low | none | +| 4 | ADR scanner over Markdown decision files | §1 | departed | decisions are decision-kind Specs (MD-26) | low | none | +| 5 | DSL extractor recognizes spec, pack, enrich, components, ports, decisions, external systems, harness, views | §2.1 | realized (partly) | Spec and Pack (TS option, MD-25); components by anchors; decisions as Specs; ports, external systems, harness and views have no constructor | low | none | +| 6 | A narrow static evaluator; a non-static property drops with a warning and the rest extracts | §2.2 | realized | `spec:extraction.determinism` ("optional TypeScript section detail may warn and drop"); a Markdown document refuses whole | low | none | +| 7 | Relations kept verbatim; resolution is cross-file | §2.3 | realized | `spec:extraction.derive-graph`, `spec:validation.referential-integrity` | low | none | +| 8 | Extractor warnings with severity, code, file, line | §2.4 | realized | one finding currency (`spec:validation.diagnostic-rendering`) | medium | none | +| 9 | Fragments cached per file by content hash | §2.4 | uncaptured | MD-22 derives on every invocation; measured 0.8 s on the adopter's 243 Specs | low | none | +| 10 | Three marker forms feed one extractor | §3.1-§3.3 | realized | constant and comment forms (MD-36); decorators departed (row 04/21) | high | none | +| 11 | Marker precedence and merge; disagreement is `marker-conflict` | §3.4 | departed | MD-36: the same id in two forms is a duplicate, never merged | low | none | +| 12 | Strict and additive facets merge across candidates | §4.1 | departed | one carrier per ID (MD-18) | low | none | +| 13 | An unresolved reference is reported at its source location; the graph still serializes | §4.2 | realized (file grain) | `spec:validation.referential-integrity`; findings carry a file and a section path, never a line | high | recommendation 4 | +| 14 | Provenance kept per node and edge | §4.3 | realized (simplified) | three claims (`spec:extraction.claim-taxonomy`); no per-edge extractor or location payload | medium | none | +| 15 | A regenerated ID union for `tsc` | §4.4 | uncaptured | deferred in `docs/concept/01` only | low | none | +| 16 | dependency-cruiser import edges as an inferred overlay | §5.1 | captured-ahead | `spec:consumers.impact-graph` (idea, blocking question); MD-38 admits an advisory inferred layer | high | recommendation 9 | +| 17 | dependency-cruiser rules become `forbidden-dependency` errors | §5.1 | departed | MD-38: negative architecture constraints are never a Protocol finding | high | departure A | +| 18 | Nx project graph ingest | §5.2 | out-of-scope | tool-specific | low | none | +| 19 | CodeQL and Semgrep findings as observation nodes | §5.3 | out-of-scope | v0 §14 leaves security policy to those tools | low | none | +| 20 | Deep TS checker: Effect `R` completeness | §5.4 | out-of-scope | framework-specific | low | none | +| 21 | Five validation tiers, each one kind of correctness | §6 | realized (three tiers) | `spec:validation.two-check-families`: types, schema, graph validators, none substitutes | medium | none | +| 22 | Tier 1: branded IDs, discriminated relations, enum membership | §6.1 | realized | `src/ids.ts`; unratified descriptors fail closed (`spec:validation.claim-separation`); generated contracts fail `tsc` on drift | medium | none | +| 23 | Tier 2: graph JSON Schema, patch schema, config schema | §6.2 | realized (partly) | `schemaVersion` (`spec:extraction.schema-versioning`); no published JSON Schema; no patch or config exists | low | none | +| 24 | `duplicate-node-id`, `unresolved-reference`, `id-grammar` | §6.3.1 | realized | `spec:validation.duplicate-ids`, `spec:validation.referential-integrity`, `spec:model.stable-ids` | medium | none | +| 25 | `forbidden-cross-namespace-ref` | §6.3.1 | realized | edge contract rows in `checkClaimSeparation` (e.g. `decidedBy` must target a decision) | medium | none | +| 26 | Seven readiness profiles, sketch to verified | §6.3.2 | departed | four rungs beside derived delivery facts: D1 (`spec:model.core-model`), the kind-conditional floor (MD-12), MD-7 | high | departure C | +| 27 | `specified` requires no blocking open question | §6.3.2 | realized | the `defined` floor clause (`spec:validation.readiness-floor`) | high | none | +| 28 | `designed` requires design components and decisions | §6.3.2 | departed | no design rung; the design section is open (MD-11) | high | recommendation 7, departure C | +| 29 | `bound`, `executable`, `verified` (with a TTL on the last run) as rungs | §6.3.2 | departed | delivery facts `implemented`, `has-verifier`, `observed` (aspirational); MD-7 | low | none | +| 30 | Teams override profiles | §6.3.2 | captured-ahead (named deferral) | `spec:validation.readiness-floor`: "a team-overridable floor configuration is a designed-for deferral" | low | none | +| 31 | `executable-needs-tests` | §6.3.3 | realized (reshaped) | mode is posture (MD-23); a `ready` unbound example warns (`spec:validation.unbound-example-posture`) | medium | none | +| 32 | `bound-route-without-impl`, `orphan-route` | §6.3.3 | out-of-scope | Fastify-specific | low | none | +| 33 | `orphan-implementation` | §6.3.3 | departed | an identity-only anchor is lawful (MD-37); `blastRadius` lists unlinked units; the orphan signal reads Specs (`spec:validation.warn-level-signals`) | low | none | +| 34 | `orphan-test` | §6.3.3 | realized (reshaped) | a test anchor's `verifies` is non-empty (MD-37); a dangling target is a finding | low | none | +| 35 | `invalid-readiness-promotion` | §6.3.4 | realized (state, not transition) | the stated rung is checked against its floor; no transition rule (adopt the nouns, reject the gates, MD-2) | medium | none | +| 36 | `superseded-spec-still-referenced`, `deprecated-decision-applied` | §6.3.4 | uncaptured | no check. Live in this corpus: `spec:consumers.spec-studio.lenses` is `decidedBy` MD-34, which MD-38 supersedes; five consumer Specs are `decidedBy` the projections freeze, which `spec:decisions.question-key-rendering` supersedes in part | high | recommendation 3 | +| 37 | `nfr-needs-target` | §6.3.5 | realized | constraint evidence complete only when every entry has a target (`spec:validation.kind-evidence.untargeted-constraint`) | medium | none | +| 38 | `nfr-needs-measurable`, `nfr-target-format` | §6.3.5 | uncaptured | `measurableBy` optional; a target is any non-empty string | low | none | +| 39 | `blocking-open-question` | §6.3.6 | realized | `spec:validation.readiness-floor.blocking-open-question` | high | none | +| 40 | `stale-open-question` after a TTL | §6.3.6 | uncaptured | a question carries no date; `spec-page#question.questionMetadata` asks; determinism bars a wall-clock check | medium | recommendation 10 | +| 41 | `inferred-only-edge-promotion-needed` | §6.3.7 | captured-ahead | `spec:consumers.impact-graph` rule on assistive relationship suggestions (idea) | low | none | +| 42 | `marker-conflict` | §6.3.7 | realized | as a duplicate id (MD-36) | low | none | +| 43 | Tier 4 architecture rules (dependency-cruiser, ts-arch, ESLint) folded into the report | §6.4 | departed | MD-38 consequence; the two check families | high | departure A, recommendation 6 | +| 44 | Typical rules: no external from application, domain purity, layer direction, route has component | §6.4 | departed | MD-38 put `layer`, `context` and `uses` on anchors but keeps the check in the corpus's gate; the adopter checks context isolation in `design/tools/check.py` (plan 40, R6) | high | recommendation 6 | +| 45 | Tier 5 custom team rules with the full graph API, run beside the built-ins | §6.5 | departed | two families only (`spec:validation.two-check-families`); MD-38 and plan 40 R3 send corpus rules to the corpus's own gate; `sdp q` gives the graph API | high | departure B, recommendation 5 | +| 46 | Per-rule severity, `failOn`, overrides | §7 | captured-ahead (named deferral) | `spec:validation.warn-level-signals`: per-team severity override is a designed-for deferral | low | none | +| 47 | Exit codes 0, 1, and 2 for an infrastructure error | §7 | realized (partly) | 0 and 1 only | low | none | +| 48 | CI writes `validation.json`; the Studio reads it | §7 | realized (reshaped) | the report is data through `sdp q` (`report`, MD-22); the Design Review renders findings | medium | none | +| 49 | Per-Spec readiness banners naming why | §8 | realized | `spec:consumers.derived-readiness-banner` (ready, implemented) | high | none | +| 50 | Coverage heatmaps per Pack or capability | §8 | captured-ahead | Pack member table (`spec:consumers.binding-language-views.pack-member-table`) and recipe 27 realized; tests lens and coverage grid in the Studio (scoped) | medium | recommendation 1 | +| 51 | Click-through from a finding to `file:line` in the IDE | §8 | realized (file grain) | anchors carry file and line; graph findings carry file and section path only (0 of the 14 current findings has a line); entries carry no line | high | recommendation 4 | +| 52 | Suggested patches for some findings | §8 | departed | edit model; Studio actions compose intent (`verification-panels` `ui.unsetTarget`, scoped; `spec:consumers.intent-composition`, idea) | medium | none | +| 53 | Patch schema, base graph hash, transactional apply, codemod | §9 | departed | `spec:consumers.edit-model` (defined) and `CONTEXT.md` ("patch-back / codemod-from-view" rejected). No decision Spec rules it | low | name the ruling in a decision when the edit model states `ready` | +| 54 | Cold and warm build targets; watch updates within 1 s of a save | §10 | uncaptured (engine only) | `sdp validate --watch` exists on carrier edits only; no Spec carries it (`spec:consumers.graph-first-planning` calls its placement a lawful non-decision) and `src/cli/validate-watch.ts` carries no anchor | medium | `spec:consumers.validate-watch` (behavior, scoped) with a question on source edits | +| 55 | Test discovery by file pattern and AST signature | §11.1 | departed | explicit test anchors only (`spec:model.anchors`); a comment-form test anchor works in any `.ts` test, Playwright included | low | none | +| 56 | A filename heuristic infers `verifies` at low confidence | §11.1 | uncaptured | the claim taxonomy would admit it as advisory `inferred`; nothing derives it | low | none | +| 57 | Gherkin tags and Playwright annotations as `verifies` sources | §11.1 | realized (partly) | `.feature` is non-canonical (MD-28); Playwright through the comment form | low | none | +| 58 | Test runs become `TestRun` nodes with results and last-verified times | §11.2 | departed | MD-7; asked for views in `verification-panels#question.runResults` and `lenses#question.testRunResults` | medium | recommendation 12 | +| 59 | OTel queries mapped to Specs; observations checked against NFR targets | §11.2 | captured-ahead | `spec:observation.runtime-overlay` (idea, blocking question) | low | none | +| 60 | `validate` output with stable finding ids | §12 | realized (partly) | summary line and findings; identity is validator, subject and path, no stable finding id | low | none | +| 61 | Each validator has should-fail and should-pass fixtures; an inventory snapshot | §13 | captured-ahead | `spec:validation.validator-self-testing` (defined, unbound by design); practiced through each validator's examples | low | none | +| 62 | Out of scope: semantic equivalence, prose style, cross-repo meaning, runtime budgets, security | §14 | realized | `spec:validation.two-check-families`; checked-mentions consequence "no reference check detects two Specs that restate one shape" | medium | none | + +## Top recommendations + +Ranked by value to design management, then closeness to v0. + +1. **Compute example-space coverage now, without the Studio.** v0 04 §4 and 06 §8. `witness` and `coverage gap` + are ratified terms, yet nothing computes them; the only home is the Studio coverage grid, held behind two + blocking questions. A reader join or recipe that enumerates a parent's literal-union slots, marks each + combination an example's bound point witnesses, and lists the missing ones gives a design its missing + examples before code exists, and gives an agent the permutations to propose. 44 parents here own an example + space (52 literal-union slots); the adopter has 18 (89 slots). Home: `spec:consumers.agent-surface.example-space-coverage` + (behavior, idea), refining `spec:consumers.agent-surface`. No ruling conflicts: the harness path (plan 27, O4) + was never refused. +2. **Review perspectives over a Pack.** v0 04 §6. The adopter iterates designs through four advisor lenses + (`design/advisors/lenses/{convex,domain,operator,product}.md`, `task-pass.md`) by hand, and its feedback asks + for "a review lens as a filter over the same capability" (feedback 02, items 28 and 29). `CONTEXT.md` already + names a discipline as "a lens/filter over the graph". A corpus-owned perspective would order and filter a + Pack's questions, entries, constraints and examples into a disposable reading brief, with no new kind and no + stage. Home: `spec:consumers.discipline-view` (behavior, idea), refining `spec:consumers.projections-model`. + Flag the term: "lens" already names the Studio's graph views. No conflict: a new projection does not reopen + MD-32. +3. **Warn on a Spec shaped by a superseded decision.** v0 06 §6.3.4. Decision lineage is design management, and + the self-hosted corpus already holds six such edges: `spec:consumers.spec-studio.lenses` is `decidedBy` MD-34, + which MD-38 replaced (its `#architectureVocabulary` question is now answered), and five consumer Specs are + `decidedBy` the projections freeze, which `spec:decisions.question-key-rendering` "supersedes" while saying + the freeze "stands for everything else". That second case shows `supersedes` used for an amendment, which + `spec:model.relations` does not describe; the rule needs an owner ruling on partial supersession, or a repair + that uses `refines` for amendments. Home: `spec:validation.superseded-decision-signal` (rule, idea), refining + `spec:validation.warn-level-signals`. Warn-level, so no ruling conflicts. +4. **Record source lines for entries and findings.** v0 06 §2.4, §4.2, §8. Graph findings carry a file and a + section path, never a line, and the graph records no line for a Design entry, an open question or a rule. The + adopter's Pack page re-reads carriers to link each entry (feedback 01, "An entry's line"), which is a second + parser. Home: `spec:extraction.entry-locations` (behavior, idea), refining `spec:extraction.derive-graph`. + A schema bump; no ruling conflicts. +5. **Run corpus rules on the one validation path.** v0 06 §6.5 and §7. The adopter's `design/tools/check.py` + (363 lines) holds about ten corpus rules: mention pairs listed with reasons, external addresses resolve, no + `dependsOn` cycle, platform isolation, every question keyed, cited laws backed by `constrainedBy`, Pack pages + current. It reads kind, readiness and relations by regex from `.sdp.md` files, the second read model the + agent-surface skill forbids. Corpus-authored graph bodies whose rows join the one report as findings of a + named corpus family would retire that. Home: a decision `spec:decisions.corpus-rules` (decision, idea) and + `spec:validation.corpus-rules` (behavior, idea). Conflicts: `spec:validation.two-check-families` (two families + only) and the MD-38 consequence that sends such checks to the corpus's own gate. +6. **Check declared architecture constraints against anchored structure.** v0 06 §6.4, 04 §7. Since MD-38, `layer`, + `context` and `uses` sit on anchors, so v0's layer-direction and context-isolation rules are checkable from + anchored data with no inference. The adopter enforces one (R6) in `check.py`. A constraint Spec stating, for + example, "no `uses` from `domain` to `adapter`" would yield findings that name the offending edge. Home: + `spec:validation.architecture-constraints` (rule, idea). Needs a decision that supersedes MD-38's consequence + "negative architecture constraints … never a finding of the Protocol's validators". +7. **A design-ahead backlog recipe.** v0 06 §6.3.2 (`designed`), 04 §1.4 (design facet). The adopter: "A Spec at + `defined` with no design and a Spec at `defined` whose interfaces are settled look the same in the graph, so + the build backlog cannot tell which Specs an implementer can start on" (feedback 01, S0). Join stated rung, + floor reached, keyed Design entries and pinned declarations (recipe 24), and the absence of `implemented`. + Home: `spec:consumers.agent-surface.design-ahead-backlog` (behavior, idea). No conflict as a recipe; a rung + would supersede D1, MD-11 and MD-12 (departure C). +8. **Realize the anchor-coverage signal MD-38 already rules.** v0 04 §2.5. The decision says the lint stays + warn-level and optional; nothing implements it. The adopter's `rebuild.ts` realized a Spec with no anchor and + misled readers; its first slice built code for a day with no code anchor. Here `src/cli/validate-watch.ts`, + a whole CLI behavior, has no anchor and no Spec (25 of 79 engine files carry no anchor, most of them helpers). + Home: `spec:validation.anchor-coverage-signal` (rule, idea), with corpus-declared file patterns. Owner question: + the Protocol has no corpus settings file, so where do the patterns live? +9. **Report drift between declared `uses` and real imports.** v0 06 §5.1. The adopter checked by hand that + "component `uses` match the relative imports between the components". An advisory inferred import overlay, + compared with anchored `uses`, tracks architecture design against realization. Home: + `spec:consumers.impact-graph.structural-drift` (behavior, idea), refining `spec:consumers.impact-graph`, whose + rules already name "unambiguous-drift flags". MD-38 admits an advisory inferred layer; file-level TypeScript + imports may sidestep the parent's blocking question on language-neutral symbol identity. +10. **Question age and owner from git.** v0 06 §6.3.6, 04 §1.4. The adopter's register + (`design/decisions/register.json`) carries class, advisor and status by hand; `spec-page#question.questionMetadata` + asks for owner and date. The commit that introduced a question's line is deterministic per commit, where a + wall-clock TTL is not. Home: `spec:consumers.agent-surface.question-age` (behavior, idea). Owner question: + derivation would read git, a new input beside the files; check against `spec:extraction.determinism`. +11. **Optional reasons on relations.** v0 04 §1.3. Why a design rests on another is review content, the adopter + keeps mention pairs with reasons by hand, and a reason on `supersedes` would carry the amendment nuance of + recommendation 3. Home: `spec:model.relation-reasons` (model, idea). Needs a decision: the envelope's relation + shape is closed (`spec:carrier.envelope-contract`). +12. **Run evidence as a labelled input to views, outside the graph.** v0 06 §11.2, 04 §5.3. The adopter asks to + "attach test results as a separate, clearly identified input", joined by verifier identity, with commit and + tier (feedback 02, item 25); two Studio open questions wait on the same answer. Keeping it a view-time input + leaves MD-7 intact. Home: `spec:observation.run-evidence` (behavior, idea), refining + `spec:observation.runtime-overlay`. + +## Departures worth re-opening + +- **A. Architecture constraints as Protocol findings (MD-38 consequence; v0 06 §6.4).** MD-38 brought back the + data v0 checked (layer, context, uses) and kept the check out. The adopter needed it on day one: R6 of plan 40 + asks `check.py` to fail when a `platform` component, or a member of one, uses a unit in another context. A + check over declared `uses` and declared layers infers nothing and judges no content; it is conformance of + anchored structure to declared intent. +- **B. Two check families only, corpus rules in the corpus's gate (`spec:validation.two-check-families`, MD-38, + plan 40 R3; v0 06 §6.5).** The adopter's gate grew to about ten graph-shaped rules, and parses carriers by regex + to do it. Hosting corpus rules on the one validation path keeps them graph-only and puts their findings in the + same report and Design Review. The guardrail survives if the Protocol never authors those rules. +- **C. No design rung (D1 in `spec:model.core-model`, the typing law MD-11, the kind-conditional floor MD-12; + v0 06 §6.3.2 `designed`).** The adopter's owner asked for Specs "at any level of maturity, including a Spec whose + key abstractions are designed as stubs"; the adopter holds 977 Design entries in 59 Specs, and all 15 members of + `pack:history-view` state `defined`. Try recommendation 7 first. If a derived signal cannot separate designed + from undesigned Specs, `spec:extraction.contract-declarations` (defined) is the first typed step toward a floor + clause that reads design evidence. +- **D. No component declared ahead of code (MD-38 retained limitation; v0 03 §2.5, 04 §7 `/arch`).** A design + ahead of code cannot name the components it will add except in Spec prose, and the Studio's architecture lens + can draw only anchored components. The evidence is weaker than A to C: the adopter's designs so far name new + components in Design entries. Worth re-opening alongside A, because an architecture constraint about a planned + component needs that component to exist in the graph. +- **E. No open questions in the Gherkin carrier (`spec:carrier.gherkin-authoring`; v0 04 §3.2).** Open questions + carry design iteration (the adopter keys 166 of them), so a Gherkin-carried behavior must move to Markdown to + hold one. Low priority: no adopter authors Gherkin yet. + +Held, and not worth re-opening on this evidence: the patch loop (the adopter's feedback asks to keep generated +status out of authored Specs and to route change through agents), decorators, Spec-side binding lists, and test +runs as graph facts. + +## Self-hosting observations from this read + +- Six `decidedBy` edges point at superseded decisions (recommendation 3); `spec:consumers.spec-studio.lenses` + `#architectureVocabulary` still reads MD-34's refusal after MD-38 admitted `layer` and `context`. +- `sdp validate --watch` has no carrying Spec and no anchor; `byFile("src/cli/validate-watch.ts")` returns + nothing. +- The edit-model departure from v0's patch loop is carried by a `defined` behavior Spec and a glossary row, + with no decision Spec. +- `spec:extraction.regenerability` still cites measured comfort "below roughly 50 Specs"; the adopter derives + 243 Specs in 0.8 s. diff --git a/.omo/evidence/plan-41/research/v0-07-08.md b/.omo/evidence/plan-41/research/v0-07-08.md new file mode 100644 index 00000000..b7b537b2 --- /dev/null +++ b/.omo/evidence/plan-41/research/v0-07-08.md @@ -0,0 +1,331 @@ +# v0 chapters 07 and 08 against the corpus: a claim-by-claim gap map + +Read-only research, 2026-10-10, on `feature/design-management-v0` at `822aea4` (tree equal to `main`). +Graph read with `pnpm --silent sdp:q` at derivation time: 228 Specs, 2 Packs, 43 decision Specs +(40 `ready`, 1 `defined`, 2 `scoped`). Re-run the queries rather than inherit these numbers. + +Sources: `docs/lineage/v0-design/07-spec-studio-and-projections.md`, `08-delivery-evidence-and-tooling.md`, +`docs/lineage/annotation-scout-2026-10-04.md`; the corpus under `specs/`; the surviving concept docs +`docs/concept/00`, `04`, `06`, `07`; plan 35's deferral table; PRs #28 and #29; the adopter +`libar-platform` at `d310eb1` (PR #12 merged), its `scripts/pack-page.mjs`, `scripts/pinned.mjs`, +`design/advisors/`, `design/decisions/register.json`, `design/tools/check.py`, `evidence/runs/`, and +`docs/feedback/*.md`. + +## Scope and method + +Chapter 07 sections 1–3, 11 and 12 are already captured as `pack:spec-studio-v1` (12 Specs, all +`scoped`, 27 keyed open questions). This map covers the rest of chapter 07 (§4 interaction patterns, +§5 projections, §6 patch loop, §7 harnesses, §8–10 lenses in detail, §13 AI integrations, §14 the loop) +and all of chapter 08. Where a Studio Pack member already holds a claim, the row names it. + +Each claim was checked with at least two concept searches in different words (`g.findByConcept`), a +grep of `specs/` and `docs/concept/`, and `g.byFile` on the code that might realize it. + +Status values: + +- **realized**: a Spec states it and the engine realizes it. +- **captured-ahead**: a Spec states it below `ready` or without implementation. "Question only" means + the claim is held whole by a keyed open question and nothing else. +- **departed**: a ruling refuses it. Most rows cite a `ready` decision Spec. Rows marked + **(no decision record)** rest on a non-decision Spec or a concept-doc principle instead. That + difference matters for the owner's direction (see "Departures worth re-opening"). +- **uncaptured**: no Spec states it and no ruling refuses it. "Concept-only" means a surviving concept + doc names it as aspirational but no Spec does. +- **out-of-scope**: v0 itself deferred it, or it is runtime plumbing with no bearing on the Protocol. + +Relevance is to the owner's goal: designs held ahead of code, iteration from several perspectives, +review, design-versus-realization tracking, decisions and open questions, AI-native graph reading. + +## Chapter 07, the rest (Spec Studio and projections) + +| # | Claim | v0 ref | Status | Home | Relevance | Proposal | +|---|---|---|---|---|---|---| +| 7-01 | Every Studio edit round-trips through validated JSON patches before touching canonical source ("interactions produce patches") | intro, §14 | departed (no decision record) | `spec:consumers.edit-model` (behavior, `defined`, no anchor); concept 06 §4 | high | Record the departure as a decision Spec and rule what returns (rec 5) | +| 7-02 | Validators decide what is allowed | intro, §14 | realized | `spec:validation.two-check-families` (ready); one validation path (MD-14); `src/validate/` | — | — | +| 7-03 | Inline edit stages a change locally, with discard, export and open-as-PR; nothing on disk changes | §4 P1, §2.4 | captured-ahead | `spec:consumers.spec-studio.intent-panel` (scoped; `#handOff` blocking) | high | — | +| 7-04 | "Propose via AI" emits a structured Markdown prompt with a graph slice and an instruction template, pasted into an agent | §4 P2 | captured-ahead | `spec:consumers.intent-composition` (idea); intent-panel `composingActions` (scoped). No Spec states the payload | high | Context bundle (rec 3) | +| 7-05 | The browser holds no API keys, makes no external calls, and never runs model output without approval | §4 P2 | uncaptured | `spec:consumers.spec-studio` states static and write-careful, not "no network" | medium | Add one rule to `spec:consumers.spec-studio` (scoped) | +| 7-06 | A harness renders controls and the oracle's `expected()` live; it models, CI verifies | §4 P3, §7 | captured-ahead | Oracle realized (`spec:validation.oracle-target-eligibility` ready; `specOracle` in `spec:model.anchors`); space contract realized (`spec:extraction.executable-contracts`); the UI only in concept 04 §4 and `spec:consumers.spec-studio.components#question.harnessElement` | medium | Capture `spec:consumers.harness-panel` (behavior, idea) over example space plus oracle, no `harness:` namespace | +| 7-07 | Coverage of slot combinations: covered by a test, or missing, with "propose tests" | §4 P3, §7, §2.3 H | captured-ahead | `spec:consumers.spec-studio.verification-panels` `coverageGrid` (scoped; `#coverageSource`) | medium | A recipe first: literal-union slots × bound points | +| 7-08 | Proposed tests land as examples below the executable rung, with an action item | §7 | realized | `spec:validation.unbound-example-posture` (defined, implemented); recipe 10 | — | — | +| 7-09 | The Studio is one projection among consumer-specific views | §5 | realized | `spec:consumers.projections-model` (defined, implemented); the four shipped projections | — | — | +| 7-10 | The graph emits a LikeC4 model: persons, systems, containers, components, relations, views | §5.1 | uncaptured (concept-only) | concept 06 §8; `spec:consumers.spec-studio.components#question.embeddedDiagrams`. The data now exists: component anchors with `layer`, `context` and `uses` (MD-38) | medium | `spec:consumers.architecture-view` (rec 9) | +| 7-11 | A LikeC4 static architecture portal; the Studio embeds its views | §5.1 | captured-ahead | components `diagramView` entry `sdp-likec4-view` (scoped) | low | — | +| 7-12 | OpenAPI and AsyncAPI generated from Route nodes with schemas, with `x-akg-spec` links | §5.2 | uncaptured (concept-only) | No Route or Schema node. Nearest: `spec:extraction.contract-declarations` (defined) and recipe 24; the adopter's `scripts/pinned.mjs` emits a TypeScript module from them | low | None. Contract-declarations already names its trigger: a second adopter freezes the emitter into `sdp build` | +| 7-13 | Gherkin export of behavior Specs for stakeholder review, tagged, regenerable | §5.3 | realized | `spec:consumers.gherkin-view` (ready). Departed form: every Spec, `.feature.md`, visibly lossy, no tags, never a carrier (MD-29) | low | — | +| 7-14 | JSON-LD export with optional SHACL shapes | §5.4 | uncaptured (concept-only) | concept 06 §8; concept 00 "membrane" | low | — | +| 7-15 | Pre-built AI slices, ≤32k tokens each, with nodes, edges, inlined source, findings, open questions and a summary | §5.5 | captured-ahead (question only) | `spec:consumers.spec-studio.data#question.aiSlices`; concept 06 §3. Plan 35's re-entry trigger lives in plan prose, not the graph. The agent front door (MD-22) refuses a committed artifact answering for the graph, so a bundle must be composed per invocation | high | Rec 3 | +| 7-16 | A per-Pack slice | §5.5 | realized | recipes 5 and 27 | — | — | +| 7-17 | A per-capability slice | §5.5 | departed | Capability is a Pack or a high-altitude behavior; grouping is derived from id families and the component graph, not new Packs (`spec:decisions.architectural-annotation`) | low | — | +| 7-18 | A change-impact slice around a modified Spec | §5.5 | realized | `g.blastRadius` (`spec:consumers.reader`, ready); recipes 4 and 19 | — | — | +| 7-19 | A slice per readiness level, with what is missing for the next | §5.5 | realized | recipes 9 and 11; `nextRungFailures` (`spec:validation.next-rung-floor`, defined, implemented) | — | — | +| 7-20 | A failing-tests slice | §5.5 | departed | `spec:decisions.binding-not-liveness` (MD-7) | low | — | +| 7-21 | Slices served through an MCP server | §5.5 | departed | `spec:decisions.mcp-deferred` (D6) | low | — | +| 7-22 | Traceability matrix: Spec, maturity, implementation, tests | §5.6 | realized | Design Review index and Pack table (`spec:consumers.design-review`, `spec:consumers.binding-language-views`, ready); recipe 27 | — | — | +| 7-23 | The matrix's decisions column and per-row status | §5.6 | uncaptured | No projection shows `decidedBy` or a per-row finding. The adopter's Pack page adds next rung, open questions, pinned pairs and outside-Pack footing | high | Rec 6 | +| 7-24 | A readiness dashboard's distribution and validation summary | §5.6 | realized | `spec:consumers.census-page` (ready): stated and derived rows, findings | — | — | +| 7-25 | A maturity heatmap by Pack or capability | §5.6 | captured-ahead | components `maturityMap` (`sdp-maturity-map`, scoped) | medium | Rec 11 | +| 7-26 | "Stalled" Specs: no change in a while | §5.6 | uncaptured | Specs carry no time; git does | low | Falls out of a design diff (rec 2) | +| 7-27 | Decisions with their subjects and how much they shape | §5.7 | realized | recipe 18, decision map (agent recipe only) | — | — | +| 7-28 | A generated, human-readable decision index with status, supersession and date | §5.7 | uncaptured | `docs/concept/DECISIONS.md` is kept by hand: MD numbers, ratified names, curation, superseded-by. `spec:decisions.decision-readiness-posture` makes that file the ratification evidence | high | Rec 4 | +| 7-29 | A proposed decision linked to the open question it answers ("Promote to ADR", "referenced by 1 open question") | §5.7, §2.3 B | captured-ahead | intent-panel `composingActions` (scoped). No graph link from a question to the decision that rules it | high | Rec 4 | +| 7-30 | In-browser staging of patch operations, lost on refresh unless kept in local storage | §6.1 | departed (no decision record) | `spec:consumers.edit-model`; persistence of gathered intents is captured in intent-panel (scoped) | low | — | +| 7-31 | A patch envelope: schema version, id, target, base graph hash, rationale, operations, proposer, time | §6.2 | departed (no decision record) | `spec:consumers.edit-model`; concept 06 §4: a structured edit contract only when a second machine writer appears; `spec:consumers.spec-studio.intent-panel#question.patchLoop` | high | Departures §1; rec 5 | +| 7-32 | CLI apply: schema check, base-hash conflict, speculative apply on a graph clone, validators, codemod, re-extract, commit or PR | §6.3 | departed (no decision record) | `spec:consumers.edit-model`; concept 06 §4 | medium | — | +| 7-33 | Codemod operations add, replace, remove, promote-readiness, split-spec, merge-spec | §6.4 | departed (no decision record) | edit-model rule: lifecycle changes are ordinary source and git edits. Scaffolding is realized by `sdp new spec` (`spec:consumers.authoring-on-ramp`) | low | — | +| 7-34 | Not patchable: id (needs `supersedes`), extractor-populated evidence, source-derived bindings | §6.5 | realized | `spec:validation.authored-honesty`, `spec:model.stable-ids` (ready); `supersedes` in `spec:model.relations` | — | — | +| 7-35 | The architecture lens is its own page family | §8 | captured-ahead | `spec:consumers.spec-studio.lenses` `architectureLens` (scoped) | medium | — | +| 7-36 | Capability map: capabilities with readiness and Spec counts | §8.1 | captured-ahead (question only) | `spec:consumers.spec-studio.lenses#question.capabilityGrouping`; concept 06 §6 names a Capability Map | medium | Rec 11 | +| 7-37 | Container diagram; hovering a container shows its Specs | §8.2 | captured-ahead | lens level only (lenses, scoped). The data is realized: recipe 17, architecture map; census role, layer and context taxonomy | medium | Rec 9 | +| 7-38 | Component map: components and their dependencies | §8.3 | realized | component membership and `uses` (MD-38; recipes 12–14, 17) | — | — | +| 7-39 | Ports and external systems on the component map | §8.3 | departed | `spec:decisions.architectural-annotation`: no independently declared component; what no code realizes is a Spec | low | — | +| 7-40 | A forbidden-dependency report with offending edges drawn | §8.4 | departed | `spec:decisions.architectural-annotation`: negative constraints stay declared intent, a corpus checks them in its own gate. The adopter does, in `design/tools/check.py` | medium | Rec 10, a recipe not a validator | +| 7-41 | A dependency-cruiser graph of inferred import edges | §8.5 | departed | MD-38: architecture is never inferred from imports as anchored or declared; inferred structure belongs to `spec:consumers.impact-graph` (idea) | low | — | +| 7-42 | Verifier presence per Spec and per Pack | §9 | realized | `has-verifier` (`spec:extraction.delivery-facts`); recipes 5 and 10 | — | — | +| 7-43 | Share of Specs verified in the latest build | §9 | departed | MD-7: bindings state existence, run verdicts stay in CI | low | Rec 7 shows results beside bindings without a fact | +| 7-44 | Flaky and slow tests | §9 | departed | MD-7; concept 07 cut 1 | low | — | +| 7-45 | Orphan tests and orphan implementations | §9 | realized | `spec:validation.referential-integrity` for unresolved anchor targets; `spec:validation.warn-level-signals` for orphan Specs; `unlinked` in blast radius for identity-only units (MD-37) | — | — | +| 7-46 | "Run subset": a test filter expression for the Specs in view | §9 | uncaptured | blast radius names verifying test anchors and files (recipe 4); nothing emits a filter | low | — | +| 7-47 | Evidence lens: builds with SLSA and SBOMs, deployments, OTel per Spec beside its targets | §10 | captured-ahead | lenses `evidenceLens`, verification-panels `evidencePanel` (scoped); `spec:observation.runtime-overlay` (idea, blocking) | medium | — | +| 7-48 | Incident references from postmortem records | §10 | uncaptured | — | low | — | +| 7-49 | An MCP server is the recommended agent integration | §13.1 | departed | `spec:decisions.mcp-deferred` (D6); concept 06 §3 and §7: agents script the graph, MCP serves user-facing apps | low | — | +| 7-50 | `getSpec` and `searchSpecs` | §13.1 | realized | `g.specContext`, `g.findByConcept`, entry search (`spec:consumers.reader`; the agent front door, MD-22; recipes 3, 6, 23) | — | — | +| 7-51 | Agent access is read-only; writes go another way | §13.1, ch08 §9.8 | realized | MD-22: the sink reads only and writes no artifact | — | — | +| 7-52 | `proposeChange` drafts a patch on the server | §13.1 | departed (no decision record) | `spec:consumers.edit-model` | medium | Rec 5 | +| 7-53 | `ai bundle `: the Spec, its impact slice, source files, findings, and a footer asking for a change | §13.2 | captured-ahead | plan 35 "context bundle" deferral (trigger in plan prose); `spec:consumers.intent-composition` (idea). The adopter builds it by hand: `design/advisors/task-pass.md` | high | Rec 3 | +| 7-54 | GraphRAG retrieval for very large graphs | §13.3 | out-of-scope | v0 deferred it to v2; concept 06 §3 | low | — | +| 7-55 | Generate, explore, validate, regenerate | §14 | realized | projections-model, design-review, agent surface, two-check-families, `spec:extraction.regenerability` | — | — | +| 7-56 | Refine, export and apply through patches | §14 | departed (no decision record) | `spec:consumers.edit-model`: intent, agent, git, checks | high | Rec 5 | + +## Chapter 08 (delivery evidence and tooling) + +CLI commands that restate a claim mapped elsewhere (`lint`, `arch check`, `patch`, `evidence`, `ai`, +`export gherkin`) take that row's status and are not counted again. + +| # | Claim | v0 ref | Status | Home | Relevance | Proposal | +|---|---|---|---|---|---|---| +| 8-01 | The graph answers runtime questions: is the NFR met, which build is live, which incident touched this Spec | intro | captured-ahead | `spec:observation.runtime-overlay` (idea); concept 00 trajectory | medium | — | +| 8-02 | Opt-in evidence ingestors add Build, Deployment and Observation nodes; the schema has room for them | §1 | captured-ahead | runtime-overlay (idea). `observed` is a fact name in `src/graph/schema.ts`, never computed (`spec:extraction.delivery-facts`, ready). Node types stay closed at four | medium | — | +| 8-03 | TestRun nodes from test reporters (Vitest, Cucumber Messages, Playwright) | §1, §2, §3.2 | departed | MD-7; concept 00 and 07: test verdicts are CI's, never ingested | medium | Rec 7: a view-side join, no fact | +| 8-04 | The graph stores pointers to telemetry, not payloads | §2 | captured-ahead | runtime-overlay outcome: "without turning operational payloads into authored model truth" | medium | — | +| 8-05 | OpenTelemetry semantic conventions, CI/CD spans included, as the runtime standard | §2, §4.3 | uncaptured | runtime-overlay names no standard | low | — | +| 8-06 | SLSA and GitHub attestations, referenced by URI | §2, §5.1 | uncaptured | UI text only, in verification-panels | low | — | +| 8-07 | One CycloneDX SBOM per build, by path and hash, with a summary | §2, §5.2 | uncaptured | UI text only | low | — | +| 8-08 | PROV-O alignment for the linked-data export | §2 | uncaptured | — | low | — | +| 8-09 | JSON Schema nodes derived from Zod or TypeBox | §2 | uncaptured | `spec:consumers.spec-studio.spec-page#question.schemaBindings`; contract declarations pin types as code spans | low | — | +| 8-10 | Ingested items carry an `evidence` provenance layer | §2 | uncaptured | Three claims only (`spec:extraction.claim-taxonomy`); MD-38 says derivation adds no fourth claim. Which claim an observation carries is unstated | low | A non-blocking question on runtime-overlay | +| 8-11 | One CI pipeline: typecheck, lint, tests, SBOM, attestations, build, validate, projections `--check-clean` | §3.1 | realized | `npm run check`, `.github/workflows/ci.yml`; `--check-clean` on build and each projection (`spec:extraction.determinism` and the projection Specs, ready) | — | — | +| 8-12 | `validate` is the merge gate | §3.1 | realized | validate's exit code; it polices conformance and honesty only (MD-2, `spec:validation.two-check-families`) | — | — | +| 8-13 | Gherkin tag lint | §3.1, §6.2 | realized | `spec:carrier.gherkin-authoring` (ready) and its refusal examples | — | — | +| 8-14 | A Studio preview per pull request on a static host | §3.1 step 8 | captured-ahead | `spec:consumers.spec-studio.sharing` (scoped; `#buildStamp` blocking) | high | Rec 8 | +| 8-15 | Per build, a Build node with commit, run id, attestations and SBOM | §3.2 | captured-ahead | runtime-overlay (idea) | low | — | +| 8-16 | `observedIn` pointers on Specs whose constraint targets have queries | §3.2, §4.1 | captured-ahead | runtime-overlay (idea; blocking identity and freshness question). Constraint targets are already machine-readable (`spec:validation.kind-evidence.untargeted-constraint`, ready) | medium | — | +| 8-17 | A Deployment node per deployment | §3.2 | captured-ahead | runtime-overlay (idea) | low | — | +| 8-18 | An automatic `verified` readiness after post-deploy checks, with a TTL | §3.2 | departed | Readiness is an authored statement above a structural floor (MD-7 consequence; four rungs in `spec:model.core-model`; D1) | low | — | +| 8-19 | Pull model: a file maps Spec targets to backend queries; a scheduled pull | §4.1 | captured-ahead | runtime-overlay (idea) | medium | — | +| 8-20 | An `nfr-violated` finding when an observation misses its target | §4.1 | uncaptured (concept-only) | concept 00 and 07 §2 | medium | Decide with runtime-overlay whether this is an honesty check (stated target against observed value) | +| 8-21 | Push model: an OpenTelemetry collector plugin | §4.2 | out-of-scope | collector plumbing; nothing to say until runtime-overlay's identity question is answered | low | — | +| 8-22 | A licence-compliance validator with per-Spec tag overrides | §5.3 | uncaptured | Concept 00 lists "licence answerability" as aspirational. Tags are refused by the closed envelope (`spec:carrier.envelope-contract`); a policy gate sits outside "conformance and honesty" | low | — | +| 8-23 | `ids regenerate`: a generated id union for the type checker | §6.1 | departed | the carrier ruling (MD-18); ids are checked by `spec:validation.referential-integrity` with did-you-mean | — | — | +| 8-24 | `build`, full | §6.1 | realized | `spec:extraction.build-pipeline` (ready) | — | — | +| 8-25 | `build --incremental` with a cache | §6.1, §9.7 | uncaptured (concept-only) | concept 07 cut 9. `spec:extraction.regenerability` cites a measurement "below roughly 50 Specs"; the corpus has 228 | low | Re-measure and update the rule's evidence line | +| 8-26 | `build --check-clean` | §6.1 | realized | `spec:extraction.determinism` (ready) | — | — | +| 8-27 | `explain` and `search` verbs | §6.1 | departed | no verb wall (`spec:decisions.agent-surface-scripts-graph`, MD-22); the answers are realized through `sdp q` (recipes 3, 6, 23) | medium | Humans get search in the Studio shell (scoped), not a CLI verb | +| 8-28 | `graph stats` | §6.1 | realized | census (`spec:consumers.census-page`); the build summary line | — | — | +| 8-29 | `graph diff ` | §6.1 | captured-ahead | one rule inside `spec:consumers.impact-graph` (idea), held by a blocking symbol-identity question that does not apply to the curated graph | high | Rec 2 | +| 8-30 | `validate --tier` and `--rule` | §6.2 | realized | findings are data in `sdp q` (`report`, recipe 8); no flags | low | — | +| 8-31 | `projections --only …` | §6.3 | realized | four public publishers plus the private suite driver (MD-32) | — | — | +| 8-32 | `watch` with live reload, `studio open` | §6.3 | uncaptured | `sdp validate --watch` exists in `src/cli/validate-watch.ts`, but no Spec states it and no anchor binds it (`g.byFile` returns nothing). Studio live reload is not stated | medium | Rec 12 | +| 8-33 | `export ts `: a `.feature` file to a TypeScript Spec | §6.3 | departed | the `.sdp.gherkin` extension (MD-28): bare `.feature` stays non-canonical import-source territory; `sdp import` converts TypeScript to Markdown only | low | — | +| 8-34 | `migrate` and `migrate plan` | §6.7 | captured-ahead | `spec:extraction.schema-versioning` (ready): a migration command stays deferred until a consumer needs one | low | — | +| 8-35 | A config file declaring globs, evidence paths and canonical format | §7 | uncaptured | none; exclusions are explicit arguments (MD-20) | low | — | +| 8-36 | A monorepo of thirteen packages | §7 | departed | one package (`spec:decisions.protocol-naming`, MD-5) with subpaths; the Studio's home is open (`spec:consumers.spec-studio#question.packageHome`) | medium | Rec 1 | +| 8-37 | Registered custom doc tags (`tsdoc.json`) | §7 | realized | the comment form's TSDoc-compatible `@sdp*` tags, parsed by the Protocol's own grammar (MD-36) | — | — | +| 8-38 | `specs/` in TypeScript, with a glossary terms file and harness modules | §7 | departed | `.sdp.md` by family (MD-18), Pack manifests (MD-25), glossaries as model Specs; the harness module became an oracle beside the tests (concept 04 §4) | — | — | +| 8-39 | An `arch/` folder: capabilities, components, ports, external systems, rules, views, ADR files | §7 | departed | a separate architecture file is refused (`spec:decisions.architectural-annotation`); decisions are decision Specs (MD-4) | — | — | +| 8-40 | `features/` with step files | §7 | realized | `.sdp.gherkin` (MD-27, MD-28) with generated contracts and registrars (`spec:extraction.runnable-modules`, ready) | — | — | +| 8-41 | `generated/` is ignored by git and regenerable | §7 | realized | `spec:extraction.regenerability`, `spec:consumers.wholesale-view-rewrite`. Adopted registrars are committed (MD-31) | — | — | +| 8-42 | Each roadmap phase is complete on its own; a team can stop at any phase | §8 | realized | concept 07 §5 orders work by measured pain; the plan series | — | — | +| 8-43 | Phase 0, foundations | §8.1 | realized | plans 01–02, slice 0 | — | — | +| 8-44 | Phase 1: DSL, markers, core validators, CI gate, one bounded context | §8.2 | realized | concept 07 slices 1–5; `examples/checkout-v1` | — | — | +| 8-45 | Phase 1: Studio v0, read-only Spec pages | §8.2 | departed (no decision record) | the MVP view is generated Markdown (`spec:consumers.design-review`, ready; concept 06 §5); the Studio is captured ahead in `pack:spec-studio-v1` | high | Rec 1 | +| 8-46 | Phase 3: dependency-cruiser, ts-arch tests, ESLint marker rules | §8.4 | departed | MD-38: negative constraints are a corpus's own gate; anchor-required lint stays warn-level and optional | low | — | +| 8-47 | Phase 3: an Effect Layer extractor with `R` analysis | §8.4 | out-of-scope | framework-specific; concept 07 cut 2 | — | — | +| 8-48 | Multi-language extractors sharing one schema | §8.6, §9.5 | uncaptured | the comment form reads `.ts` and `.tsx` only (MD-36); `spec:consumers.impact-graph` blocks on a language-neutral identity | low | — | +| 8-49 | A graph database mirror once the graph is large | §8.6, §9.2 | realized | `spec:extraction.regenerability` (defined, implemented): deferred until about 10k nodes or measured pain | — | — | +| 8-50 | SysML v2 and CALM interop | §8.6 | uncaptured (concept-only) | concept 00 "membrane" | low | — | +| 8-51 | A VS Code extension with inline validation | §8.6 | uncaptured | concept 04 names only the editor association for `.sdp.gherkin` | low | — | +| 8-52 | Trace-link recovery as suggestions, never authoritative | §8.6, §9.3 | captured-ahead | `spec:consumers.impact-graph` (idea) rule; MD-38: an inferred structural layer stays advisory | low | — | +| 8-53 | Tenant variants as child Specs, with a diff view | §8.6 | uncaptured (concept-only) | concept 00, far horizon | low | — | +| 8-54 | Cross-repository federation | §8.6 | uncaptured | an adopter can read the shipped Protocol corpus with `--root` (`spec:consumers.shipped-protocol-corpus`), but no relation crosses corpora | medium | Owner question: should an adopter Spec relate to a Protocol Spec? | +| 8-55 | Intent stays central; inline markers are selective | §9.1 | realized | MD-36, MD-37, MD-38: intent in Specs; anchors carry identity, bindings, role, layer and context | — | — | +| 8-56 | Decorators of either flavour, read statically | §9.4 | departed | the decorator form stays unextracted (`spec:model.anchors`; MD-36) | — | — | +| 8-57 | Authoring stays TypeScript in polyglot repositories | §9.5 | departed | the carrier ruling (MD-18): Markdown, which is language-neutral | — | — | +| 8-58 | Governance in git and CI; no RBAC | §9.6 | realized | concept 00 non-goal; `spec:consumers.design-review`: validators never record or require approval; `spec:consumers.projections-model`: a signed tag is the approval artifact | — | — | +| 8-59 | At scale: shard the Specs and limit the scanned globs | §9.7 | realized | explicit exclusions (MD-20, `spec:extraction.excludes`) | — | — | +| 8-60 | AI mitigations: every proposal is validated before it applies, reviewed as a diff, and agents get read-only access | §9.8 | realized | agent edits pass the same checks (edit-model, two-check-families); git diff is the review; MD-22; a handoff never inherits a verdict (`spec:consumers.delivery-session-on-ramp`) | — | — | +| 8-61 | `proposedBy` recorded on each proposal for audit | §9.8 | uncaptured | git authorship and co-author trailers stand in | low | Part of rec 5's envelope | +| 8-62 | Split a large Spec across files and merge them by id | §9.9 | departed | one carrier per id (`spec:validation.duplicate-ids`, ready) | — | — | +| 8-63 | Promote examples to their own Specs; put long prose in decision records | §9.9 | realized | example kind, one point per example (MD-17), carried evidence (MD-16), one primitive (MD-4) | — | — | +| 8-64 | Start with one bounded context; pair adoption with an existing pain | §9.10 | realized | concept 07 §1 and §5; `examples/checkout-v1` | — | — | +| 8-65 | Make the Studio the first visible win | §9.10 | departed (no decision record) | sink priority: agents first, Design Review second (concept 06 intro); `spec:decisions.studio-html-surface#question.primarySurface` blocking | high | Departures §2 | +| 8-66 | A `--lenient` mode that warns instead of failing, so strictness can ratchet up | §9.10 | captured-ahead | `spec:validation.warn-level-signals` (ready): a per-team severity override is a designed-for deferral; concept 07 §2 | low | — | +| 8-67 | TypeScript is the canonical authoring language | §10 | departed | MD-18 | — | — | +| 8-68 | The graph is the truth | §10 | departed | The repository is the only canonical source and the graph is the sole read model (concept 01 P1; `spec:consumers.projections-model`) | — | — | + +## Counts + +Counted over the rows above, one primary status per row: chapter 07 has 56 rows and chapter 08 has 68. + +| Status | Ch 07 | Ch 08 | Total | +|---|---|---|---| +| realized | 17 | 21 | 38 | +| captured-ahead | 13 | 12 | 25 | +| departed | 16 | 16 | 32 | +| uncaptured | 9 | 17 | 26 | +| out-of-scope | 1 | 2 | 3 | +| total | 56 | 68 | 124 | + +9 of the 32 departures have no decision record behind them: 7-01, 7-30, 7-31, 7-32, 7-33, 7-52 and 7-56 +rest on `spec:consumers.edit-model`, a `defined` behavior Spec, and 8-45 and 8-65 rest on concept 06. + +Of the 26 uncaptured claims, two are high relevance: 7-23, the matrix's decisions column and status, +and 7-28, a generated decision index. Five are medium: 7-05, 7-10, 8-20, 8-32 and 8-54. The other 19 +are runtime and supply-chain standards (SLSA, SBOM, PROV-O, licences, OpenTelemetry conventions), +far-horizon interop, or small conveniences. They bear on delivery evidence, not on design work. + +## What the adopter built by hand that these chapters describe + +| Adopter artifact | What it does | v0 claim it realizes | Protocol home today | +|---|---|---|---| +| `scripts/pack-page.mjs` → `design/pages/history-view.md` (1,201-line script, 2,725-line page) | One page per capability Pack. Members in authored order, each with its stated rung, the floor and the next rung's first unmet clause. Code by `references` and `satisfies` with role, layer and context. Examples and verifiers by tier, pinned pairs, open questions joined to register rows, and footing outside the Pack in both directions. The engine pin is stamped, and `--check` fails when the page is stale | 07 §5.6 traceability and dashboard, §5.5 per-Pack slice, §2.3 Spec page A–J, §12 stamp | Studio Pack (`scoped`), Design Review Pack page (frozen, MD-32), recipes 5, 9, 20, 21, 24, 27 | +| `scripts/pinned.mjs` + `tests/types/pinned-pairs.json` + `pinned.test-d.ts` | Emits each Pack's pinned declarations as a module the type tests compile. A pair is bound, ahead or not compared, so a design that leads its code is tracked at declaration grain | 07 §5.2 contract export linked to its Spec; §9 "rules not exercised"; designs held ahead of code | `spec:extraction.contract-declarations` (`defined`; the adopter owns the emitter until a second adopter needs it); recipe 24 | +| `design/advisors/` (four lenses: convex, domain, operator, product; `task-pass.md`) | A pass reads one Pack from one lens: the Pack page, then recipes 5, 20, 21 and 24. It writes up to 15 findings. Each proposes exact Spec text at an entry address, an open question, or a register row. A model from another family checks every citation against a copy that carries one planted false citation | 07 §4 P2 "propose via AI", §13.2 prompt bundle, §5.5 slices; 08 §9.8 hallucination mitigations | `spec:consumers.delivery-session-on-ramp` (advisory routing), `spec:consumers.intent-composition` (`idea`). No Spec states the pass brief or the proposal shape | +| `design/decisions/register.json` (175 rows: 82 sorted, 45 decided, 21 waiting, 15 folded, 12 unsorted) + `decisions.py` + `forks/` | Open decisions with sources, class, advisor, lean, check, fork, ruling and date; the script applies JSON patches to the register | 07 §5.7 decision index with proposed decisions linked to open questions; §2.3 B "Mark resolved / Promote to ADR" | recipe 20 and question keys (`spec:model.open-question-keys`, `defined`); decision Specs. No projection, no link from a question to the decision that rules it | +| `design/tools/check.py` | Platform isolation over declared `uses`, `layer` and `context`. Checks entry addresses outside the Specs (recipe 25), `dependsOn` cycles (recipe 26), listed prose-mention exceptions, and that the pages are current | 07 §8.4 forbidden-dependency report; 08 §6.2 validation tiers | MD-38 gives such rules to the corpus's own gate; recipes 25, 26, 28 | +| `evidence/runs/*.json` + `scripts/acceptance.mjs` | Per-run records: commit, clean tree, tier, target, each test's result and duration, backends, measurements. Acceptance checks the rows that must have passed | 08 §2 and §3.2 TestRun and Build; §4 observation against targets; 07 §2.3 H and I panels | departed in the graph (MD-7); `spec:consumers.spec-studio.verification-panels#question.runResults` (non-blocking) | + +## Top recommendations + +Ranked by value to design management first, closeness to v0 second. + +1. **Name the Studio's reader and package home, and start the Studio with the read-only Pack page.** + v0 07 §2, §5.6, §12; 08 §8.2, §9.10. Plan 35 gives two conditions for the Studio to re-enter: a ruling + that names its package home, and a ruling that names the reader the Markdown Design Review does + not serve. Both are now blocking questions on `spec:consumers.spec-studio`. The adopter has met the + evidence half. Its probe page exists and passes read it (`task-pass.md`). Feedback 02 names the + reader: a person inside the repository who inspects and iterates on a capability. It also proposes + `@libar-dev/` as the home. Rule `#unservedReader` and `#packageHome`, then `#primarySurface` on + `spec:decisions.studio-html-surface`. No supersession is needed: the Studio is a new root beside + the four frozen projections. +2. **Compare designs between two commits, down to Spec, section and entry.** v0 08 §6.1 `graph diff`. + Iteration needs to answer "what changed since the operator pass" (feedback 02 #30, #31). Today one + rule inside `spec:consumers.impact-graph` holds this, and that Spec is blocked by a symbol-identity + question that does not apply to the curated graph. Capture `spec:consumers.design-diff` + (behavior, `idea`). Derive both graphs and persist neither, as the impact-graph rule already says. +3. **A context bundle: the pass brief as a composed, token-budgeted slice.** v0 07 §5.5, §13.2, + §4 P2. Plan 35's trigger was "agent sessions still hand-assemble the same token-budgeted slice after + the recipes". The adopter's `task-pass.md` does exactly that for every pass, and feedback 02 #29 asks + for it. Capture `spec:consumers.context-bundle` (behavior, `scoped`). Ship it first as a recipe that + takes a Pack, a Spec neighborhood or an open question. Compose it per invocation, so MD-22's refusal + of committed artifacts holds. This also moves the trigger out of plan prose and into the graph. +4. **A decision index, and a link from an open question to the decision that rules it.** v0 07 §5.7, + §2.3 B. `docs/concept/DECISIONS.md` is a hand-kept registry: MD numbers, ratified names, curation, + superseded-by. It repeats facts the graph holds (`supersedes`, readiness), and + `spec:decisions.decision-readiness-posture` makes it the ratification evidence. The adopter keeps a + 175-row register for open decisions. Capture `spec:consumers.decision-index` (behavior, `idea`) and + start with a recipe. One owner question stays open: where the ratified name lives on a decision + Spec. Retiring the hand-kept registry would need a decision that supersedes the decision readiness + posture. +5. **Record the patch-loop departure as a decision, and rule what of it returns.** v0 07 §6, §2.4, + §4 P1; 08 §6.4. The largest departure in chapter 07 rests only on `spec:consumers.edit-model`, a + `defined` behavior Spec, and on concept 06 §4. Write a decision Spec (decision, `scoped`). Then use + `spec:consumers.intent-composition` to settle the hand-off shape: an addressed proposal with the + base commit, the rationale, the proposer, and changes keyed by entry address. The adopter already + writes proposals that way. See "Departures worth re-opening" §1. +6. **Bring the adopter's Pack-page joins into the Protocol.** v0 07 §5.6, §2.3 J. The joins are the + next rung with its first unmet clause, a `decidedBy` column, open questions per member, and footing + outside the Pack. Either supersede `spec:decisions.shipped-projections-frozen` narrowly for the + Design Review Pack page, the way the authored-entry-order decision did for entry rendering, or give + these joins to the Studio's Pack page (rec 1). Choose one. The adopter would then keep only its + platform-specific joins, the pinned pairs and the register rows. +7. **Join CI run records to verifiers in a view, never in the graph.** v0 07 §9, §10; 08 §2, §3.2. + The adopter keeps run records and an acceptance check, and feedback 02 #25 asks for the join. + Capture `spec:observation.run-evidence` (behavior, `idea`) refining + `spec:observation.runtime-overlay`. Join by verifier identity, label the result as external input, + and show its commit and freshness. Answering the Studio's `#testRunResults` and `#runResults` would + follow. MD-7 stands because no delivery fact is derived. The Design Review's rule against run + identity keeps the join out of the frozen pages. +8. **Answer `#buildStamp`: put provenance in a wrapper beside a deterministic page set.** v0 07 §12; + 08 §3.1 step 8. Determinism keeps the commit and time out of the pages. A PR review artifact needs + them (feedback 02 #37, #39), and the adopter stamps its engine pin. Rule the question on + `spec:consumers.spec-studio.sharing`. The stamp lives in a manifest beside the pages, never inside + them. +9. **An architecture view over component anchors.** v0 07 §5.1, §8.2, §8.3. MD-38 now gives the + graph components with a layer, a context and `uses`, enough for a container and component view. + Capture `spec:consumers.architecture-view` (behavior, `idea`). It emits a C4-shaped or LikeC4 + model as a new root, so Mermaid stays frozen. Fix the stale lens Spec at the same time (see the + drift list). +10. **A shipped recipe for `uses` edges that cross a context or a layer.** v0 07 §8.4. MD-38 leaves + the rule to each corpus's own gate, and the adopter wrote it by hand in `check.py`. A recipe + reports the crossings and refuses nothing. Home: `spec:consumers.agent-surface.register-recipes` + (behavior, `scoped`). +11. **A Pack map as the entrance.** v0 07 §8.1, §5.6 heatmap. List every Pack with its readiness + distribution, blocking questions, headroom between floor and stated rung, and members held by a + dependency (feedback 02 #8). Ship a recipe first, then answer + `spec:consumers.spec-studio.lenses#question.capabilityGrouping` with "Packs". +12. **Bind `sdp validate --watch` to a Spec, and keep inspection apart from publication.** v0 08 §6.3, + §9.7. The watcher is built but no Spec states it and no anchor binds it, a self-hosting gap. The + adopter also asks for refresh without the build path that wipes `generated/` (feedback 02 #36). + Add a rule and an anchor to `spec:consumers.authoring-on-ramp`, or capture + `spec:consumers.validate-watch` (behavior, `scoped`). + +## Departures worth re-opening + +1. **The patch loop, as an addressed proposal and not as a codemod.** The edit model keeps views from + writing source. Concept 06 §4 lets a structured edit contract return only when a second machine + writer appears. The adopter now has two: + - advisor agents, whose memos propose "exact text for a Spec, with the entry address it replaces + or follows" (`task-pass.md`); + - writer agents, which apply the rulings, while `decisions.py apply ` applies JSON patches to + the register. + + What looks better now is v0's envelope (§6.2) without its apply machinery: a proposal that names + its base commit, its rationale and its proposer, keyed by entry address and checked by address + resolution (recipe 25) before an agent applies it. The conformance and honesty checks stay the + gate, with no speculative apply and no ts-morph codemod. First give the departure a decision + record. Today it has none. +2. **HTML first for human review.** Concept 06 §5 settles generated Markdown for the MVP, and + `#primarySurface` is still open. The adopter's one Pack page is 2,725 lines of Markdown with folded + lists. Feedback 02 asks for a table of contents, reference previews, entry links, search and + keyboard navigation. This is v0 §1's "Markdown caps out" argument, measured on the first adopter. + Agents stay the first sink. The human view would move from Markdown pages to the Studio. +3. **"Never ingested" for test results.** Keep MD-7: no run result becomes a delivery fact. Concept + 00 and 07 go further, though, and say test results are "never ingested", which also rules out + showing them. The adopter's run records, its acceptance check, and feedback 02's acceptance task + ("whether a paused-rebuild example has native-backend evidence") argue for a labelled join in a + view (rec 7). The concept text is the place to change. +4. **A hand-kept decision registry instead of a generated index.** v0 generated its index of + decision records, and gen 1 generated `DECISIONS` pages from the same graph (annotation scout §2). + The Protocol keeps `DECISIONS.md` by hand and makes it the ratification evidence for decision + readiness. If a decision Spec's own `ready`, plus an authored ratified name, became the evidence, + the index would become a projection and one hand-kept store would go (rec 4). + +Not worth re-opening on this evidence: seven readiness rungs (the adopter itself declined a +`designed` rung, feedback 03); MCP for agents (agents script the graph and no app caller exists); +a forbidden-dependency validator (the adopter's own gate works under MD-38); and test results as +graph facts. + +## Drift found while mapping + +- `spec:consumers.spec-studio.lenses` is `decidedBy` `spec:decisions.architectural-significance-rides-primitives`, + which MD-38 superseded. Its `#architectureVocabulary` question still says the ruling "admits no + structural vocabulary beyond component membership and `uses`". It is the only Spec decided by a + superseded decision. +- `spec:consumers.spec-studio.spec-page#question.runtimeComposition` says "`component` and `uses` are its + only structural fields". That is stale since MD-38 added `role`, `layer` and `context`. +- `sdp validate --watch` (`src/cli/validate-watch.ts`) has no carrying Spec and no anchor. +- `spec:extraction.regenerability` cites a measurement "below roughly 50 Specs". The corpus has 228. +- Plan 35's context-bundle and reference-projection re-entry triggers live only in plan prose. + `spec:consumers.graph-first-planning` says a trigger is the deferred Spec's own blocking question, + and it names three triggers where plan 35's table has four rows. diff --git a/AGENTS.md b/AGENTS.md index 406935d2..829e2302 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,18 @@ authoritative for what the Protocol claims; **`src/` and tests** are authoritati current realization. A disagreement is **drift to resolve**, never permission to silently promote code behavior into intent. +> **plan 42 is DRAFTED**. The Spec Studio's phase 0, a read-only static view (`sdp studio`), +> re-entered on the owner's rulings; intent in `pack:spec-studio-v1`. A design pass splits the +> Studio's Specs for honest binding and prototypes the Pack page before any code. +> **plan 41 is EXECUTED**. The design-management arc, intent in `pack:design-management-v1`: the +> Design Review's Pack page shows a Pack's design (the Pack design page, MD-39), the graph +> records entry lines, recipes take `--params` and ship under `dist/recipes/`, recipes 29 to 32 +> serve design work, `sdp --version` names the build, and the original design's unbuilt promises +> are captured as Specs. The owner ruled every open item and the fold landed in the +> same branch: the arc's Specs and the drift queue state `ready`; a decision that narrows another +> `refines` it, `supersedes` replaces whole, and a replaced record that explains nothing is +> deleted (MD-30, MD-34 and MD-35 were); the captures' questions are answered; and the Studio's +> phase 0 is ruled a read-only static root. The plan's last section lists the rulings. > **plan 40 is EXECUTED**. The annotations arc: the comment form (MD-36), optional plural > bindings and design references (MD-37), and architecture attributes (MD-38), all three > `ready`, with their open questions closed by the plan's rulings; the engine, self-binding, @@ -24,10 +36,11 @@ code behavior into intent. > **plan 39 is DRAFTED**. The first-adopter arc, a thin lineage pointer in the plan-38 shape. Six > of its Specs (inline code spans, the unbound-example posture, the typed-dependency floor, the > register recipes, the adopter on-ramp, the shipped corpus) are implemented with bound evidence -> and, like the verified Markdown body grammar, wait for the owner to state `ready`. The -> entry-address engine is built: the prose-mentions validator and the open-section order carry -> `implemented` and wait for the same statement, the checked-mentions decision and contract -> declarations need no engine binding, and one Spec stays held by an owner question. The backlog +> and, like the verified Markdown body grammar, state `ready` since plan 41's fold. The +> entry-address engine is built: the prose-mentions validator and the open-section order state +> `ready` as well, the checked-mentions decision is ratified (MD-40), contract +> declarations need no engine binding, and the list rendering an owner question held +> (`spec:decisions.authored-entry-order`) was ratified and built in round 2. The backlog > and readiness are read from the graph (recipes 1, 2, 9, 11). > **plan 38 is DRAFTED** — the graph-first planning arc, a thin lineage pointer, not a briefs > index: the arc's forward intent is authored as capture-rung Specs @@ -89,7 +102,7 @@ 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 the Specs under `specs/` and in the surviving concept docs | **first, always** | -| `.agents/skills/` + `docs/agent-surface/recipes.md` | **the agent on-ramps** — three repository-owned skills: `sdp-agent-surface` (reading the graph), `sdp-authoring` (authoring intent), `sdp-sessions` (advisory delivery-session routing), also exposed to Claude through the `.claude/skills` symlink; plus the twenty-eight runnable `sdp q` bodies | **mandatory** — after `CONTEXT.md`, load the matching skill before any corpus question, Spec authoring, or delivery-session routing; see "Query the graph first" | +| `.agents/skills/` + `docs/agent-surface/recipes.md` | **the agent on-ramps** — three repository-owned skills: `sdp-agent-surface` (reading the graph), `sdp-authoring` (authoring intent), `sdp-sessions` (advisory delivery-session routing), also exposed to Claude through the `.claude/skills` symlink; plus the thirty-two runnable `sdp q` bodies | **mandatory** — after `CONTEXT.md`, load the matching skill before any corpus question, Spec authoring, or delivery-session routing; see "Query the graph first" | | `specs/` | **the self-hosted corpus** — the Protocol's own Specs in its own carrier (families: `model` · `extraction` · `validation` · `carrier` · `consumers` · `protocol` · `observation` · `decisions`, plus the self-hosting Pack); the primary carrier of intended truth | when design truth is in question — but query it through `sdp q` first, then read the carrying Spec | | `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/` (+ README) | **the technical design** — the surviving principle-led docs: vision & MVP boundary, founding principles (P1–P10), authoring & binding, consumers, roadmap; the core model, the one graph, and validation & honesty dissolved into the `model.*`, `extraction.*`, and `validation.*` Spec families — locate any of them with concept search (recipe 6) | to know *how* it is designed | @@ -118,9 +131,10 @@ load `.agents/skills/sdp-authoring/SKILL.md`. Before routing delivery work, load questions. `sdp q` is the agent front door (MD-22): it derives the graph in process and evaluates a plain -JavaScript async-function body you supply, with three bindings injected — `g` (the reader), -`graph` (the raw schema), `report` (the validation report). `return` is the output contract; add -`--json` for machine-readable output. For any corpus question — what a Spec guarantees, what is +JavaScript async-function body you supply, with four bindings injected — `g` (the reader), +`graph` (the raw schema), `report` (the validation report), and `params` (the JSON a caller passes +with `--params`, `{}` without it). `return` is the output contract; add `--json` for +machine-readable output. For any corpus question — what a Spec guarantees, what is ready but unimplemented, what a change touches, where a concept lives — script the graph instead of reading `.sdp.md` files by hand: @@ -137,7 +151,7 @@ The full CLI surface is `sdp build · validate · view · census · mermaid · g and stays alive after findings; operator stop exits 0. `--watch` is validate-only and cannot combine with `--check-clean`. The four projection publishers remain independent public verbs; repository generation/check scripts certify all four roots through the private projection-suite driver. The -twenty-eight runnable recipe bodies live in `docs/agent-surface/recipes.md` (each executed as written by +thirty-two runnable recipe bodies live in `docs/agent-surface/recipes.md` (each executed as written by `test/recipes.test.ts`), and the repository-owned skills (`sdp-agent-surface` for reading, `sdp-authoring` for writing intent, `sdp-sessions` for advisory work-shape routing) are the on-ramps. In this checkout, always go through diff --git a/CONTEXT.md b/CONTEXT.md index bff56752..4451d8c6 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -58,8 +58,9 @@ The familiar delivery nouns are **named coordinates on the one primitive, never |---|---|---| | **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) | — | +| **question key** | the optional lower-camel key an open question carries in its marker, as in `[blocking #arcBoundary]`, unique in its Spec, which gives the question the entry address `spec:#question.` (`spec:model.open-question-keys`) | "question id" · "anchor" | -## The other authored things (no system truth — → `spec:model.pack-aggregate`, `04` §2) +## The other authored things (no system truth — → `spec:model.pack-aggregate`, `spec:model.anchors`) | Term | Definition | Aliases to avoid | |---|---|---| @@ -71,7 +72,7 @@ The familiar delivery nouns are **named coordinates on the one primitive, never | **role** | the optional one-token value on a code anchor, lowercase kebab and never normalized, naming the architectural pattern its unit plays (`service` · `decider` · `projection` · …); a corpus-owned vocabulary whose taxonomy the census renders with counts, checked against no list (the architectural-annotation ruling, MD-38) | "pattern" · "kind" (reserved for the Spec descriptor) · "tag" | | **layer** | the optional closed-set attribute of a `component:` anchor — `edge` · `application` · `domain` · `adapter` · `infrastructure`; outside the set, or on a non-component anchor, an envelope error | "tier" | | **context** | the optional one-token value on a `component:` anchor, in the token grammar of `role`, naming its bounded context; on a non-component anchor, an envelope error | "bounded_context" (the v0 field) · "domain" | -| **architecturally significant unit** | a code unit with exported public surface or cross-component reach that warrants graph-visible structural binding — component membership, uses declarations for its architectural dependencies, a role, and references to the designs it answers to; annotated on the anchor of the code that realizes it; the accepted set is an owner-reviewed declaration, never derived from imports (carried by `spec:model.structural-patterns`) | "pattern" (refused by the architectural-annotation ruling, MD-38, which supersedes MD-34) | +| **architecturally significant unit** | a code unit with exported public surface or cross-component reach that warrants graph-visible structural binding — component membership, uses declarations for its architectural dependencies, a role, and references to the designs it answers to; annotated on the anchor of the code that realizes it; the accepted set is an owner-reviewed declaration, never derived from imports (carried by `spec:model.structural-patterns`) | "pattern" (refused by the architectural-annotation ruling, MD-38, which replaced MD-34) | **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). @@ -113,6 +114,7 @@ facts literal while the canonical recipe reports both audited exclusions. | **`extractor`** | the producer — the *only* component that reads source; derives nodes, edges, `claim`s, and the validation report | — | | **`nodeType` / `specKind`** | structural class (`Primitive`/`Pack`/`Anchor`/`CodeNode`/…) vs the truth-category on `Primitive` nodes — kept split so they never collide | a single `kind` field | | **git is the event log** | history and prior states live in git; the graph carries only current state | audit tables / lifecycle bookkeeping in the graph | +| **location table** | the graph's table, beside its nodes and edges, of the source line of every keyed Design and UI entry and every open question; a location is never an identity or a binding target, and Primitive and Pack nodes carry no lines (`spec:extraction.entry-locations`) | "source map" · "entry anchor" | ## Validation & honesty (→ `spec:validation.two-check-families`, `spec:validation.readiness-floor`) @@ -125,6 +127,10 @@ facts literal while the canonical recipe reports both audited exclusions. | **`gap`** | a surfaced absence (e.g. `ready` with no verifier) — informative, never a gate | — | | **`orphan`** | a `Spec` with no relations and nothing pointing at it — informative | — | | **derived readiness** | the highest rung whose floor clauses pass, computed from the graph ("structural floor reached"); rendered **beside** the stated rung, never overwriting the author's statement | — | +| **stated next rung** | the rung above a Spec's stated rung, with the floor clauses that keep the floor from reaching it; with no failing clause the rung waits only for its author's statement, which the Pack page writes as `nothing blocks`, never a promotion. It differs from the floor's next rung, the rung above the floor reached, which promotion preflight (recipe 9) reports (`spec:consumers.pack-design`) | "next rung" alone · "target rung" | +| **mention** | a `spec:` id written in a Spec's narrative or section text, outside the `gwt` and `gwt-vocabulary` fences; checked for resolution and for a backing declared relation in either direction, and it mints no edge (the checked-mentions decision, `spec:decisions.checked-mentions`) | "reference" · "citation" · "link" | +| **entry address** | a Spec id followed by `#`, a section name (`design`, `ui`, or `question`), a dot, and the entry's lower-camel ASCII key, naming one keyed entry of that Spec's open section, or one of its open questions that carries that key; admitted in prose and by `parseId` and refused in every id slot, so a Spec's own id never carries `#`; checked in prose by `spec:validation.prose-mentions` | "anchor" (reserved for the in-code binding) · "fragment" · "sub-id" | +| **dependency cycle** | a set of two or more Specs in which each reaches every other through declared `dependsOn` edges, or a Spec that declares `dependsOn` on itself; recipe 26 reports each and nothing refuses it (`spec:consumers.agent-surface.address-and-cycle-recipes`) | "circular dependency" · "loop" | `ready` = the floor cleared **plus a human's `declared` statement**; that a review occurred is never a graph fact or a checked property (that would be workflow-gating). The honesty bound: claim **"deterministically @@ -139,9 +145,14 @@ validated," never "provably correct."** | **agent surface** | a **visible typed graph the agent *scripts*** via the typed CLI — no verb wall; the schema *is* the contract (under-typing hides a capability) | a 30-verb API · raw-JSON-you-rejoin | | **`reader`** | the thin typed loader: joins + `claim` decode done once, returns composable data; authors/persists nothing — a front door, not a store | "handle" | | **agent front door** | the CLI-facing realization of the agent surface (MD-22): **two entrances over one seam** — the package exports the `reader` constructor, and the CLI carries the evaluation sink; a composite naming both entrances, distinct from the `reader`'s own "front door, not a store" gloss above, which names the reader's role over the graph | a query API · a verb wall | -| **evaluation sink** | the front door's CLI entrance, the verb `sdp q`: derives the graph in process on every invocation, injects `g` / `graph` / `report`, evaluates the operator-supplied async body, prints its `return`; writes nothing and mints no query vocabulary | a REPL · a second read model | +| **evaluation sink** | the front door's CLI entrance, the verb `sdp q`: derives the graph in process on every invocation, injects `g` / `graph` / `report` / `params`, evaluates the operator-supplied async body, prints its `return`; writes nothing and mints no query vocabulary | a REPL · a second read model | | **demand map** | the ruled statement of how agents actually arrive — holding a **string**, a **file**, or a **changeset**, never the Spec id they are looking for — carried as clauses on `spec:consumers.agent-surface`; the reason the frozen entry adapters are `findByConcept` · `byFile` · `blastRadius` | — | | **recipe** | a runnable, documented `sdp q` body composing the frozen adapters — the growth valve past them: when a question is not answered, script it, and a join freezes into the `reader` only at the second-caller bar; catalog at `docs/agent-surface/recipes.md`, every body executed as written by the recipe check | a new query verb | +| **recipe parameter** | the input a caller passes a recipe as data through `sdp q --params`, read by the body from `params` with the catalog's sample as its fallback (`spec:consumers.agent-surface.recipe-parameters`) | "argument" · "recipe flag" | +| **Pack design** | the reader's per-Pack assembly of each member's stated next rung, design columns and code bindings, with the Pack's boundary; the Design Review's Pack page renders it (`spec:consumers.pack-design`) | "Pack page" (names the rendering) · "capability page" | +| **design columns** | the counts and lists derived for a Spec beside its readiness: its keyed Design and UI entries, pinned declarations, open and blocking questions, and the decisions it names; never a score or a rung, and design maturity is read from them, never stated (`spec:model.design-maturity`) | "design score" · "designed" (the original design's stage) | +| **boundary** | the Specs outside a Pack joined to a member by one of the four typed relations `refines`, `dependsOn`, `constrainedBy` and `decidedBy`: the Pack **rests on** a Spec a member names by one of them and is **rested on by** a Spec that names a member by one of them; outside Specs that `verifies` or `supersedes` a member list apart as `verifiedBy` and `supersededBy` (`spec:consumers.pack-design`) | "outside dependencies" · "perimeter" | +| **perspective** | a reader's concern a Pack is read from, such as the domain model, operations or the runtime platform: corpus data a recipe takes as a parameter to filter and order a Pack's members and their content without changing them, adding no kind, stage or sequence; the corpus owns its perspectives and the Protocol ships none (`spec:consumers.review-perspectives`) | "lens" (names the Studio's graph views) · "discipline" (the kind-or-section filter) | | **blast radius** | the reader's file-level impact query: a changeset → the directly impacted `Spec`s/`Pack`s (authored at, or bound to, the changed files) + the explicit one-hop neighborhood; a changed identity-only CodeNode, bound to no Spec, is reported in its own bucket, neither dropped nor folded into coverage-unknown; never claims exhaustive reach | — | | **coverage-unknown** | the honest blind spot of file-level impact: a changed file the graph records nothing at, **named in the result** — never silently dropped | — | | **at-risk** | a node one explicit hop from an impacted node (the connecting edge + its `claim` carried), itself neither impacted nor at a changed file | — | @@ -160,6 +171,7 @@ validated," never "provably correct."** | **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 | — | +| **pinned declaration** | a keyed Design entry whose value opens with a code span, stating one signature, type, validator or table a derived module can hold code to; recipe 24 lists them (`spec:extraction.contract-declarations`) | "pin" alone · "stub" | | **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 | — | @@ -182,12 +194,12 @@ a sibling set back as a table — the graph never holds a multi-point example. | Relation | Direction | Means | Industry anchor | |---|---|---|---| -| **refines** | child → parent | a more precise child; how altitude descends | UML «refine» | +| **refines** | child → parent | a more precise child; how altitude descends; a decision that narrows one part of another decision refines it | UML «refine» | | **dependsOn** | dependent → dependency | needs another `Spec` to hold | UML *Dependency* | | **constrainedBy** | bounded → constraint | bounded by a rule / NFR / policy `Spec` (a *typed* dependency) | — | | **decidedBy** | shaped → decision | shaped by a Decision Record (`doc:` only for a genuinely external ADR) | UML «trace» | | **verifies** | verifier → target | an example/scenario verifies a target — also emitted derived from a test anchor (same type, different `claim`, never collapsed) | UML «verify» | -| **supersedes** | new → old | current forward-pointer between two Decision Records that both still exist | — | +| **supersedes** | new → old | the newer Decision Record replaces the older one whole; the older record and the edge stay only while its text explains something a reader still needs, otherwise the newer record carries what survives and the older one is deleted, with git as its history | — | **Derived, never authored:** `satisfies` (code → `Spec`, from an anchor; one edge per target) · `references` (code → `Spec`, from an anchor; confers nothing) · `models` (oracle → `Spec`) · `belongsTo` (→ grouping, from the @@ -229,18 +241,6 @@ delivery fact, workflow state, or graph state. 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 checked-mentions record** (`spec:decisions.checked-mentions`, ruled - by the owner and stated `defined`; the two terms move into the **Validation & honesty** table and the - locked ledger when the record states `ready`). **mention**: a `spec:` id written in a Spec's - narrative or section text, outside the `gwt` and `gwt-vocabulary` fences; checked for resolution - and for a backing declared relation in either direction, and it mints no edge (aliases to avoid: - "reference" · "citation" · "link"). **entry address**: a Spec id followed by `#`, a section name - (`design`, `ui`, or `question`), a dot, and the entry's lower-camel ASCII key, naming one keyed - entry of that Spec's open section, or one of its open questions that carries that key; admitted - in prose and by `parseId` and refused in every id slot, so a Spec's own id never carries `#`, - and checked in prose by `spec:validation.prose-mentions`, which is implemented - and still states `defined` (aliases to avoid: "anchor", reserved for the in-code binding · - "fragment" · "sub-id"). - ~~Candidate vocabulary from the executable-spec exploration: *notation* · *carrier*~~ — **ratified by the carrier ruling (MD-18)**; see **The authoring carrier** above. The rest of the exploration's candidates ratified with their referents at the plan-12 session — see **The @@ -260,7 +260,11 @@ delivery fact, workflow state, or graph state. sense) — the last four ratified at the phase-5 PR review (the front-door ruling, MD-22); the **Surfaces & projections** rows above give the referents · `constant form` · `comment form` · `references` · `role` · `layer` · `context` — the six of the annotations arc (MD-36 to - MD-38; all three rulings state `ready`). + MD-38; all three rulings state `ready`) · `question key` · `stated next rung` · `design + columns` · `Pack design` · `boundary` (rests on, rested on by) · `location table` · `recipe + parameter` · `pinned declaration` · `dependency cycle` · `perspective` · `mention` · `entry + address` — ratified at the owner's PR 30 rulings, the last two with the + checked-mentions decision. - **Descriptor values locked:** `kind` ∈ {`behavior`,`workflow`,`example`,`rule`,`constraint`,`model`, `decision`,`contract`} · `altitude` ∈ {`epic`,`feature`,`story`} · `readiness` ∈ {`idea`,`scoped`,`defined`,`ready`}. @@ -271,7 +275,7 @@ delivery fact, workflow state, or graph state. `decision` section — FSM imprint; adoption arc = `readiness`, replacement = `supersedes`, rejected paths = `alternatives`/`consequences` — MD-11) · `pattern` (as an architectural primitive, kind, or `pattern:` namespace — ordinary English remains; the architectural-annotation ruling, MD-38, - superseding MD-34) · `annotation` (→ `anchor`; the verb "annotate" survives as "write an anchor"). + which replaced MD-34) · `annotation` (→ `anchor`; the verb "annotate" survives as "write an anchor"). - **Locked usage:** readiness is **"stated/asserted," never "claimed"** ("claim" is reserved for the `claim` taxonomy) · the meta-model defines the **contract**, **instances conform**; "govern"/"police" retired · checks are **conformance checks + honesty checks** · **pre-graph** = upstream of graph derivation in the diff --git a/README.md b/README.md index 7bf6d0c8..90a446b7 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,9 @@ pnpm --silent sdp:q 'return g.specs().length' ``` The `sdp:q` script supplies this repository's required fixture exclusions. Use the -[twenty-eight graph-first recipes](docs/agent-surface/recipes.md) for backlog, drift, verifier, impact, -Pack, readiness, promotion, open-question, dependency, mention, and entry-search queries. +[thirty-two graph-first recipes](docs/agent-surface/recipes.md) for backlog, drift, verifier, impact, +Pack, readiness, promotion, open-question, dependency, mention, entry-search, Pack design, +design-change, decision, and architecture queries. The [architecture walkthrough](docs/agent-surface/architecture.md) follows shared policy, runner and adapter responsibilities, and the one read model through the graph. @@ -147,4 +148,4 @@ for carrier support. The package installs three agent on-ramps as `SKILL.md` files under `node_modules/@libar-dev/software-delivery-protocol/.agents/skills/`. Use `sdp-agent-surface` to read the graph, `sdp-authoring` to author intent, and `sdp-sessions` for advisory delivery-session -routing. The same package includes the twenty-eight recipe bodies at `docs/agent-surface/recipes.md`. +routing. The same package includes the thirty-two recipe bodies at `docs/agent-surface/recipes.md`. diff --git a/check-prose-schema.mjs b/check-prose-schema.mjs index 56b5822d..fbe2e191 100644 --- a/check-prose-schema.mjs +++ b/check-prose-schema.mjs @@ -23,7 +23,7 @@ for (const section of [ ]) { expectContains("src/model/sections.ts", section, "missing section description owner"); } -expectContains("src/graph/schema.ts", 'schemaVersion = "0.8.0"', "missing 0.8.0 schema literal"); +expectContains("src/graph/schema.ts", 'schemaVersion = "0.9.0"', "missing 0.9.0 schema literal"); expectContains("src/graph/schema.ts", "readonly narrative?: string;", "missing graph narrative"); expectContains("src/reader/reader.ts", "readonly narrative?: string;", "missing Reader narrative"); expectContains( @@ -47,7 +47,7 @@ expectContains( ); expectContains( "specs/extraction/schema-versioning.declared-version.sdp.md", - '{schemaVersion: "0.8.0"}', + '{schemaVersion: "0.9.0"}', "missing declared-version example literal", ); expectContains( @@ -62,7 +62,7 @@ expectContains( ); expectContains( "docs/concept/06-consumers-and-projections.md", - "Design Review now renders `Spec.narrative` and the seven approved section descriptions from those Reader/graph values (schema `0.8.0`), with no source reparse and stable omission when prose is absent.", + "Design Review now renders `Spec.narrative` and the seven approved section descriptions from those Reader/graph values (schema `0.9.0`), with no source reparse and stable omission when prose is absent.", "missing landed prose projection claim", ); diff --git a/docs/agent-surface/architecture.md b/docs/agent-surface/architecture.md index aa7dd383..75554f88 100644 --- a/docs/agent-surface/architecture.md +++ b/docs/agent-surface/architecture.md @@ -24,7 +24,7 @@ lineage evidence. The Protocol's intended truth lives in its Specs. | Design rationale | A declared `decidedBy` edge to a decision Spec | | Delivery state | Stated readiness beside independently derived delivery facts | -The architectural significance ruling, `spec:decisions.architectural-significance-rides-primitives`, +The architectural annotation ruling, `spec:decisions.architectural-annotation`, keeps the existing kinds. A runtime responsibility can be a behavior, an interface a contract, and a quality bound a constraint. A decision records a qualifying trade-off. Familiar software design pattern names can appear in ordinary Design prose without creating a tag registry, diff --git a/docs/agent-surface/recipes.md b/docs/agent-surface/recipes.md index 603c5a47..acc842d9 100644 --- a/docs/agent-surface/recipes.md +++ b/docs/agent-surface/recipes.md @@ -32,25 +32,47 @@ pnpm exec sdp q '' --root PATH --exclude PATH --exclude PATH `--root PATH` picks the extraction root (default: the working directory) and `--exclude` is repeatable for root-relative path prefixes. `PATH` is a placeholder, not a literal directory. -**Some recipes open with a parameter.** Recipes 3, 6, 9, 14, 19, 21, 22, 23, 25, and 27 take -their subject on the opening `const` line(s): a Spec id, a search term, a component id, a list of -Spec ids, a list of entry addresses, or a Pack id. Those lines name *this* repository's corpus so -every body runs as written here (the recipe check executes each one verbatim); in your own corpus, -substitute your subject on that line before running. A Spec id, component id, or Pack id absent -from the graph returns `found: false` rather than failing. +**Some recipes take a parameter.** Recipes 3, 4, 5, 6, 9, 14, 19, 21, 22, 23, 25, 27, 29, and 30 +read their subject from the `params` binding on their opening line: a Spec id, a list of changed +files, a Pack id, a search term, a component id, a list of Spec ids, or a list of entry addresses. +Each recipe names its parameter. Pass it as JSON data with `--params`, never by editing the body: -**Recipe 4 is different.** Recipe 4 filenames travel via `SDP_CHANGED_FILES_JSON`; callers never -substitute filenames into the JavaScript fence. Keep its query body static and pass changed paths -as JSON data through that environment variable, as shown in the recipe-4 instructions below. +```sh +pnpm --silent sdp:q '' --params '{"spec":"spec:model.anchors"}' --json +pnpm --silent sdp:q '' --params @params.json --json +``` + +`--params @PATH` reads the JSON from a file, which suits a long list. Without `--params`, `params` +is `{}` and the body falls back to the sample on its opening line. The samples name *this* +repository's corpus, so every body runs as written here (the recipe check executes each one +unchanged). A Spec id, component id, or Pack id absent from the graph returns `found: false` +rather than failing. A value that is not JSON, JSON that is not an object (null, an array, a +string, a number or a boolean), or a file that cannot be read is refused before the body runs, +with exit 1 and one line on stderr. + +**Every body ships as a file.** The build writes each body below to +`dist/recipes/-.js`, where `NN` is the two-digit recipe number and the slug is the +heading in lower kebab case, so recipe 25 is `dist/recipes/25-address-resolution.js`. The package +ships that directory, at +`node_modules/@libar-dev/software-delivery-protocol/dist/recipes/` in an adopter. Run a file as +shipped instead of copying its body out of this page: + +```sh +pnpm --silent sdp:q "$(cat dist/recipes/25-address-resolution.js)" --params @addresses.json --json +``` + +This catalog stays the one owner of the bodies. The files are derived from it, and the recipe +check holds each one byte for byte to its body here. **The contract, in one place.** The front door derives the graph in process and evaluates the body -you supply; `return` is the output contract. Three bindings are injected: +you supply; `return` is the output contract. Four bindings are injected: | Binding | What it is | |---|---| | `g` | the reader over the derived graph — the same `createReader` the package exports | | `graph` | the raw graph schema object (nodes, edges, claims) | | `report` | the validation report, so honesty findings are queryable data and never a gate | +| `params` | the JSON object `--params` supplies, or `{}` without it; input to the body, never a verb | **Body rules.** A body is a plain JavaScript async function body — no `import`/`export`, no TypeScript-only syntax. It may `await`. Default output is bounded `util.inspect`; `--json` prints @@ -102,11 +124,19 @@ const byFamily = Object.create(null); for (const spec of backlog) { const family = spec.id.slice("spec:".length).split(".")[0]; byFamily[family] = byFamily[family] ?? []; + const { designColumns } = g.specContext(spec.id); byFamily[family].push({ id: spec.id, kind: spec.specKind, altitude: spec.altitude, hasVerifier: spec.deliveryFacts.includes("has-verifier"), + designColumns: { + entries: designColumns.entries, + declarations: designColumns.declarations, + openQuestions: designColumns.openQuestions.length, + blockingQuestions: designColumns.blockingQuestions, + decisions: designColumns.decisions.length, + }, }); } @@ -127,7 +157,9 @@ verification evidence rather than implementation work (MD-24); ready decision re registry-ratification evidence rather than implementation or verifier work (MD-26). The raw `ready ∧ ¬implemented` expression remains literally true while this operational recipe excludes both kinds, audits example verifier bindings, and reports both excluded counts. The reverse pairing -is recipe 2. +is recipe 2. Each backlog row also reports `designColumns` from its Spec context: counts of keyed +Design and UI entries, pinned declarations, open and blocking questions, and shaping decisions. +These columns are derived, never a score, rung or approval. ## 2. The drift alarm @@ -165,8 +197,11 @@ rung. A hit *with* an unmet clause is the expensive one. *When you need this: you are about to implement or review one Spec and want its sections, relations, and bindings in one shot.* +The parameter is `params.spec`, the Spec id to read; an unknown id returns `{ id, found: false }` +rather than failing. + ```js -const id = "spec:consumers.reader"; +const id = params.spec ?? "spec:consumers.reader"; const context = g.specContext(id); if (context === undefined) { @@ -222,17 +257,24 @@ that it passed. Pass/fail is CI's, exactly as skip and quarantine are. *When you need this: you have a diff (or are about to make one) and want the Specs it reaches.* -The caller acquires the changed paths and passes them as data, never as query source. From the repository root, this exact pipeline preserves every valid Git filename byte except NUL (which Git filenames cannot contain), including newlines, while JSON-encoding the NUL-delimited list before it reaches the environment: +The parameter is `params.files`, the list of changed paths, relative to the root. The caller +acquires the paths and passes them as data, never as query source. From the repository root, this +pipeline keeps every valid Git filename byte except NUL (which Git filenames cannot contain), +newlines included, by JSON-encoding the NUL-delimited list before it reaches the sink: ```sh -SDP_CHANGED_FILES_JSON="$(git diff --name-only -z | node -e 'const fs = require("node:fs"); const names = fs.readFileSync(0).toString("utf8").split("\0"); process.stdout.write(JSON.stringify(names.slice(0, -1)));')" \ -pnpm --silent sdp:q 'const changed = JSON.parse(process.env.SDP_CHANGED_FILES_JSON ?? "[]"); const radius = g.blastRadius(changed); const impactReasons = (item) => ({ id: item.id, reasons: item.reasons.map((reason) => reason.throughBinding === undefined ? { file: reason.file, via: null } : { file: reason.file, via: reason.throughBinding.id, edgeType: reason.throughBinding.edgeType, claim: reason.throughBinding.claim }) }); const atRiskReasons = (item) => ({ id: item.id, nodeType: item.nodeType, reasons: item.reasons.map((reason) => ({ from: reason.from, edgeType: reason.edgeType, to: reason.to, claim: reason.claim })) }); return { changedFiles: radius.changedFiles, impactedSpecs: radius.impactedSpecs.map(impactReasons), atRiskSpecs: radius.atRisk.filter((item) => item.nodeType === "Primitive").map(atRiskReasons), atRiskOther: radius.atRisk.filter((item) => item.nodeType !== "Primitive").map(atRiskReasons), coverageUnknownFiles: radius.coverageUnknown, unlinkedUnits: radius.unlinked.map((unit) => ({ id: unit.id, file: unit.file })) };' --json +changed="$(mktemp)" +git diff --name-only -z | node -e 'const fs = require("node:fs"); const names = fs.readFileSync(0).toString("utf8").split("\0"); process.stdout.write(JSON.stringify({ files: names.slice(0, -1) }));' > "$changed" +pnpm --silent sdp:q "$(cat dist/recipes/04-what-breaks-if-i-change-these-files.js)" --params "@$changed" --json +rm -f "$changed" ``` -The query body is static and reads only the JSON environment value; neither the shell nor the reader reevaluates filenames. `JSON.parse` receives data, so quotes, shell metacharacters, spaces, Unicode, and embedded newlines remain filenames rather than JavaScript or shell syntax. The reader never shells to git. +The body is static and reads only the JSON value; neither the shell nor the reader reevaluates +filenames. `--params` parses data, so quotes, shell metacharacters, spaces, Unicode, and embedded +newlines remain filenames rather than JavaScript or shell syntax. The reader never shells to git. ```js -const changed = JSON.parse(process.env.SDP_CHANGED_FILES_JSON ?? "[]"); +const changed = params.files ?? []; const radius = g.blastRadius(changed); const impactReasons = (item) => ({ id: item.id, reasons: item.reasons.map((reason) => reason.throughBinding === undefined ? { file: reason.file, via: null } : { file: reason.file, via: reason.throughBinding.id, edgeType: reason.throughBinding.edgeType, claim: reason.throughBinding.claim }) }); const atRiskReasons = (item) => ({ id: item.id, nodeType: item.nodeType, reasons: item.reasons.map((reason) => ({ from: reason.from, edgeType: reason.edgeType, to: reason.to, claim: reason.claim })) }); @@ -246,14 +288,22 @@ Every result class is returned. **Impacted Specs** are authored-at or bound-to a *When you need this: you are reviewing a Pack as a unit and want its members' readiness, delivery facts, and verifier gaps.* +The parameter is `params.pack`, the Pack to review; without it the body reads the first Pack in +the graph. An unknown Pack returns `{ id, found: false }`. + ```js -const packs = g.packs(); +const id = params.pack ?? g.packs()[0]?.id; -if (packs.length === 0) { +if (id === undefined) { return { packs: 0 }; } -const context = g.packContext(packs[0].id); +const context = g.packContext(id); + +if (context === undefined) { + return { id, found: false }; +} + const byStatedReadiness = {}; for (const member of context.members) { @@ -286,8 +336,10 @@ aggregate over Specs that do. *When you need this: you have a phrase and no id — the grep-to-graph bridge.* +The parameter is `params.term`, the phrase to look for. + ```js -const term = "blast radius"; +const term = params.term ?? "blast radius"; const matches = g.findByConcept(term); return { @@ -373,7 +425,7 @@ refusals; a green corpus has no errors, but may carry intentional warnings. evidence before touching the carrier.* ```js -const id = "spec:model.enrichment-lifecycle"; +const id = params.spec ?? "spec:model.enrichment-lifecycle"; const context = g.specContext(id); if (context === undefined) { @@ -403,7 +455,7 @@ return { }; ``` -The `id` line is the recipe's parameter — substitute the Spec whose promotion you are weighing. +The parameter is `params.spec`, the Spec whose promotion you are weighing. An empty `currentFloorFailures` list says the stated rung is honest. It does not confer the next rung, and `floorReached` above the stated rung is information rather than an automatic edit. `nextRungFailures` are the unmet clauses of `nextRung`, and a typed-dependency failure lists its @@ -551,11 +603,11 @@ component dependencies, not a validation finding. *When you need this: you want the members, neighboring components, and Specs satisfied by one component.* -The opening `const subject` is the parameter. Replace it with the component you are reviewing; an -unknown subject returns the exact not-found shape and does not throw. +The parameter is `params.component`, the component you are reviewing; an unknown component +returns the exact not-found shape and does not throw. ```js -const subject = "component:protocol.reader"; +const subject = params.component ?? "component:protocol.reader"; const component = graph.nodes.find((node) => node.nodeType === "CodeNode" && node.id === subject); if (component === undefined) { @@ -905,11 +957,11 @@ is the absence of an edge; `decidedBy` subjects stay grouped by family. *When you need this: you want one Spec's refinement and dependency neighborhood, shaping decisions, bound components, verifiers, and file-level entry points.* -The opening `const id` is the parameter. Replace it with the Spec you are planning around; an -unknown id returns `{ found: false }` rather than failing. +The parameter is `params.spec`, the Spec you are planning around; an unknown id returns +`{ id, found: false }` rather than failing. ```js -const id = "spec:consumers.agent-surface"; +const id = params.spec ?? "spec:consumers.agent-surface"; const context = g.specContext(id); if (context === undefined) { @@ -1165,11 +1217,11 @@ but is not a lower-camel string. Those entries do not count as questions. the rung each one states and the floor each one reaches. For the floor's verdict on the Spec itself, run promotion preflight (recipe 9).* -The opening `const id` is the parameter. Replace it with the Spec you are weighing; an unknown id -returns `{ id, found: false }` rather than failing. +The parameter is `params.spec`, the Spec you are weighing; an unknown id returns +`{ id, found: false }` rather than failing. ```js -const id = "spec:extraction.derive-graph"; +const id = params.spec ?? "spec:extraction.derive-graph"; const context = g.specContext(id); if (context === undefined) { @@ -1236,11 +1288,11 @@ Spec can state `ready` is the floor clause carried by `spec:validation.typed-dep resolve, or that no declared relation backs in either direction. The `reverseOnly` list stays a separate audit list, for mentions backed only by a relation from the target.* -The opening `const scope` is the parameter. Replace it with a list of Spec ids to audit -mentions from those Specs only. An empty list audits the whole corpus. +The parameter is `params.scope`, a list of Spec ids to audit mentions from those Specs only. An +empty or absent list audits the whole corpus. ```js -const scope = []; +const scope = params.scope ?? []; const backingTypes = [ "refines", "dependsOn", @@ -1425,11 +1477,10 @@ every location of every pair. *When you need this: you hold a word or a key and want the entries that carry it, where concept search (recipe 6) stops at the section.* -The opening `const term` is the parameter. Replace it with your word or phrase; an empty term -returns no matches. +The parameter is `params.term`, your word or phrase; an empty term returns no matches. ```js -const term = "suffix"; +const term = params.term ?? "suffix"; const tokensOf = (text) => text .replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, "$1 $2") @@ -1472,38 +1523,46 @@ for (const spec of g.specs()) { ? `${spec.id}#${section}.${entry}` : null; }; - const visit = (section, entry, key, text) => { + const lines = new Map(context.entryLocations.map((location) => [location.entry, location.line])); + // A Design or UI line is looked up by the top-level key the walk carries, never by the joined + // path: a literal key `a.b` and a nested `a: { b }` join to one path, and only the top-level + // entry has a row in the location table. + const lineOf = (section, entry, topKey) => { + const index = /^openQuestions\[(\d+)\](?:\.question)?$/u.exec(entry ?? "")?.[1]; + const located = + section === "intent" && index !== undefined + ? `question[${index}]` + : (section === "design" || section === "ui") && topKey !== null + ? `${section}.${topKey}` + : null; + return lines.get(located) ?? null; + }; + const row = (section, entry, matchedIn, text, topKey) => ({ + id: spec.id, + section, + entry, + address: addressOf(section, entry), + line: lineOf(section, entry, topKey), + matchedIn, + text, + }); + const visit = (section, entry, key, text, topKey) => { const matchedIn = [ ...(key !== null && hits(key) ? ["key"] : []), ...(hits(text) ? ["text"] : []), ]; - if (matchedIn.length > 0) - matches.push({ - id: spec.id, - section, - entry, - address: addressOf(section, entry), - matchedIn, - text, - }); + if (matchedIn.length > 0) matches.push(row(section, entry, matchedIn, text, topKey)); }; - const walk = (section, value, entry, key) => { + const walk = (section, value, entry, key, topKey) => { if (typeof value === "string") { - visit(section, entry, key, value); + visit(section, entry, key, value, topKey); return; } if (key !== null && hits(key)) { - matches.push({ - id: spec.id, - section, - entry, - address: addressOf(section, entry), - matchedIn: ["key"], - text: JSON.stringify(value), - }); + matches.push(row(section, entry, ["key"], JSON.stringify(value), topKey)); } if (Array.isArray(value)) { - value.forEach((item, index) => walk(section, item, `${entry ?? ""}[${index}]`, null)); + value.forEach((item, index) => walk(section, item, `${entry ?? ""}[${index}]`, null, null)); } else if (typeof value === "object" && value !== null) { // A key the author coins is content: any key of the open sections (`design`, `ui`) except // `description`, a `model.terms` term, and the key of an open question, which travels with @@ -1523,14 +1582,17 @@ for (const spec of g.specs()) { : section === "intent" ? questionKeyOf(path) : null, + entry === null ? name : null, ); } } }; - if (typeof context.narrative === "string") visit("narrative", null, null, context.narrative); + if (typeof context.narrative === "string") { + visit("narrative", null, null, context.narrative, null); + } for (const [section, content] of Object.entries(context.sections ?? {})) { - walk(section, content, null, null); + walk(section, content, null, null, null); } } @@ -1566,6 +1628,14 @@ Each row carries `address: string | null`. A Design or UI entry with a key match marker carries a key, at `openQuestions[].question`, has address `spec:#question.`, and that key matches as the row's key; the key is not a row of its own. `description` in Design or UI, off-grammar keys, other nested paths, and all other sections have `null`. +Each row also carries `line`, the line in the Spec's carrier where the entry starts, from the +location table the Spec context carries. The table locates a top-level Design or UI key other than +`description` and the text of an open question, at `openQuestions[].question` or, for a question +written as a plain string, at `openQuestions[]`, so a row for any other entry has `line: null`, as +does an entry the carrier did not locate; null reads as not located, never as absent +(`spec:extraction.entry-locations`). The line comes from the top-level key the walk reaches, never +from the joined path, so a nested `shape: { detail }` reports `line: null` even beside a literal +top-level key `shape.detail`, whose path it spells. Each row names the Spec, the `section`, the `entry` inside it, and the entry's `text`. `entry` is the key for a keyed entry (`envelopeSketch`, `terms.claim inheritance`), the field and zero-based index for a list entry (`rules[2]`), and `null` for the Spec's narrative, which is reported under @@ -1614,11 +1684,11 @@ return { totals: { entries: rows.length, specs: new Set(rows.map((row) => row.sp *When you need this: you hold entry addresses written outside the Specs, in a register, a test, or a page, and want to know which still resolve.* -The opening `const addresses` is the parameter. Replace it with the addresses you hold; an empty -list returns no rows. +The parameter is `params.addresses`, the list of addresses you hold; an empty list returns no +rows. A long list belongs in a file passed as `--params @PATH`. ```js -const addresses = [ +const addresses = params.addresses ?? [ "spec:consumers.design-review#ui.packPage", "spec:consumers.design-review#ui.memberTable", "spec:consumers.absent#design.anyKey", @@ -1627,31 +1697,55 @@ const addresses = [ const pattern = /^(spec:[A-Za-z0-9][A-Za-z0-9-]*(?:\.[A-Za-z0-9][A-Za-z0-9-]*)*)#(design|ui|question)\.([a-z][A-Za-z0-9]*)$/u; const specIds = new Set(g.specs().map((spec) => spec.id)); -const holds = (id, section, key) => { - const sections = g.specContext(id)?.sections ?? {}; +const entryOf = (sections, section, key) => { if (section === "question") { const questions = sections.intent?.openQuestions; - return ( - Array.isArray(questions) && - questions.some((entry) => typeof entry === "object" && entry !== null && entry.key === key) - ); + const index = Array.isArray(questions) + ? questions.findIndex( + (entry) => typeof entry === "object" && entry !== null && entry.key === key, + ) + : -1; + return index < 0 + ? undefined + : { value: questions[index].question, entry: `question[${index}]` }; } const entries = sections[section]; - return ( - key !== "description" && - typeof entries === "object" && - entries !== null && - Object.hasOwn(entries, key) - ); + if (key === "description" || typeof entries !== "object" || entries === null) return undefined; + return Object.hasOwn(entries, key) + ? { value: entries[key], entry: `${section}.${key}` } + : undefined; }; const rows = addresses.map((address) => { - const row = (resolves, id, section, key, reason) => ({ address, resolves, id, section, key, reason }); + const miss = (reason) => ({ + address, + resolves: false, + id: null, + section: null, + key: null, + value: null, + file: null, + line: null, + reason, + }); const parsed = typeof address === "string" ? pattern.exec(address) : null; - if (parsed === null) return row(false, null, null, null, "malformed"); + if (parsed === null) return miss("malformed"); const [, id, section, key] = parsed; - if (!specIds.has(id)) return row(false, null, null, null, "spec"); - if (!holds(id, section, key)) return row(false, null, null, null, "entry"); - return row(true, id, section, key, null); + if (!specIds.has(id)) return miss("spec"); + const context = g.specContext(id); + const found = entryOf(context.sections ?? {}, section, key); + if (found === undefined) return miss("entry"); + const location = context.entryLocations.find((row) => row.entry === found.entry); + return { + address, + resolves: true, + id, + section, + key, + value: found.value, + file: location?.file ?? null, + line: location?.line ?? null, + reason: null, + }; }); const count = (reason) => rows.filter((row) => row.reason === reason).length; @@ -1668,8 +1762,12 @@ return { ``` Each row answers one input, in input order, repeats included. A row that resolves carries the -Spec `id`, the `section` (`design`, `ui`, or `question`), the `key`, and `reason: null`. A row that -does not resolve carries `id`, `section`, and `key` as null and one `reason`: `"malformed"` for an +Spec `id`, the `section` (`design`, `ui`, or `question`), the `key`, the entry's `value`, and +`reason: null`. The value is the Design or UI entry's value as the graph holds it, or the +question's text. `file` and `line` say where the entry is written, from the location table the +Spec context carries; both are null when the carrier did not locate the entry, which reads as not +located, never as absent. A row that does not resolve carries `id`, `section`, `key`, `value`, +`file`, and `line` as null and one `reason`: `"malformed"` for an input that is not an entry address, a bare Spec id, a `pack:` id, and a value that is not a string among them; `"spec"` for an address whose Spec is absent; and `"entry"` for an address whose Spec holds no such entry. A Design or UI key resolves as an own key of that section other than @@ -1677,8 +1775,9 @@ holds no such entry. A Design or UI key resolves as an own key of that section o marker, `[blocking #aggregateReach]`. These are the grammar and the rule `sdp validate` applies to an address written in a Spec's prose, under `spec:decisions.checked-mentions`; the mention audit (recipe 22) reads addresses inside the Specs, and this recipe reads the ones written anywhere -else. `totals` counts the inputs, the resolving rows, and each reason. The law is -`spec:consumers.agent-surface.address-and-cycle-recipes`. +else. `totals` counts the inputs, the resolving rows, and each reason. The laws are +`spec:consumers.agent-surface.address-and-cycle-recipes` and, for the value, file and line, +`spec:extraction.entry-locations`. ## 26. Dependency cycles @@ -1799,11 +1898,11 @@ never refuses: a cycle is data about the authored dependencies, and the readines answers to it, which code realizes it, whether a verifier is bound, and which built Specs it builds on, with no fact read as another.* -The opening `const id` is the parameter. Replace it with the Pack you are reviewing; an unknown -Pack returns `{ id, found: false }`. +The parameter is `params.pack`, the Pack you are reviewing; an unknown Pack returns +`{ id, found: false }`. ```js -const id = "pack:spec-studio-v1"; +const id = params.pack ?? "pack:spec-studio-v1"; const pack = g.packContext(id); if (pack === undefined) { @@ -1917,3 +2016,402 @@ counts are edges, kept apart from units: one unit that references three Specs is one referencing unit. None of these attributes or edges confers a delivery fact or moves a readiness floor. The census renders the same taxonomy for a human reader. The law is `spec:decisions.architectural-annotation`. + +## 29. Pack design + +*When you need this: you are designing or reviewing a Pack and want where its design stands, +member by member: what holds each one below its next rung, what design it carries, which code +answers to it, and what the Pack rests on, as data a page or a script renders.* + +The parameter is `params.pack`, the Pack you are designing; an unknown Pack returns +`{ id, found: false }`. + +```js +const id = params.pack ?? "pack:spec-studio-v1"; +const pack = g.packContext(id); + +if (pack === undefined) { + return { id, found: false }; +} + +const unitOf = (binding) => ({ + id: binding.codeId, + file: binding.file ?? null, + line: binding.line ?? null, + role: binding.role ?? null, + component: + binding.component === undefined + ? null + : { + id: binding.component.id, + layer: binding.component.layer ?? null, + context: binding.component.context ?? null, + }, +}); +const members = pack.members.map((member) => { + if (!member.resolved) return { id: member.id, resolved: false }; + const { design } = member; + + return { + id: member.id, + resolved: true, + title: member.title ?? null, + specKind: member.specKind, + statedReadiness: member.statedReadiness, + floorReached: member.derivedReadiness ?? "none", + statedNextRung: member.statedNextRung ?? null, + statedNextRungFailures: member.statedNextRungFailures.map((failure) => ({ + clauseId: failure.clauseId, + description: failure.description, + ...(failure.targets === undefined ? {} : { targets: failure.targets }), + })), + design: { + entries: design.entries, + declarations: design.declarations, + openQuestions: design.openQuestions.length, + blockingQuestions: design.blockingQuestions, + }, + questions: design.openQuestions.map((entry) => ({ + key: entry.key ?? null, + blocking: entry.blocking, + address: entry.key === undefined ? null : `${member.id}#question.${entry.key}`, + line: entry.line ?? null, + question: entry.question, + })), + decisions: design.decisions.map((decision) => ({ + id: decision.id, + statedReadiness: decision.statedReadiness ?? null, + resolved: decision.resolved, + })), + implementedBy: member.implementations.map(unitOf), + referencedBy: member.references.map(unitOf), + verifiers: member.verifiers, + examples: member.examples, + }; +}); +const boundaryRow = (row) => ({ + id: row.id, + title: row.title ?? null, + statedReadiness: row.statedReadiness, + implemented: row.implemented, + via: row.via, +}); +const resolved = members.filter((member) => member.resolved); +const count = (test) => resolved.filter(test).length; +const sum = (field) => resolved.reduce((total, member) => total + member.design[field], 0); + +return { + found: true, + id, + title: pack.title ?? null, + totals: { + members: members.length, + unresolved: members.length - resolved.length, + heldBelowNextRung: count((member) => member.statedNextRungFailures.length > 0), + waitingForAuthor: count( + (member) => member.statedNextRung !== null && member.statedNextRungFailures.length === 0, + ), + entries: sum("entries"), + declarations: sum("declarations"), + openQuestions: sum("openQuestions"), + blockingQuestions: sum("blockingQuestions"), + withImplementations: count((member) => member.implementedBy.length > 0), + withReferences: count((member) => member.referencedBy.length > 0), + restsOn: pack.boundary.restsOn.length, + restedOnBy: pack.boundary.restedOnBy.length, + verifiedBy: pack.boundary.verifiedBy.length, + supersededBy: pack.boundary.supersededBy.length, + }, + members, + boundary: { + restsOn: pack.boundary.restsOn.map(boundaryRow), + restedOnBy: pack.boundary.restedOnBy.map(boundaryRow), + verifiedBy: pack.boundary.verifiedBy.map(boundaryRow), + supersededBy: pack.boundary.supersededBy.map(boundaryRow), + }, +}; +``` + +The body pre-shapes the Pack design the reader assembles on the Pack context and computes nothing +of its own. Members keep the manifest's order. Each resolved member carries its stated rung, the +floor reached (`"none"` when no rung derives), and its stated next rung, the rung above the stated +one, `null` at `ready`. `statedNextRungFailures` lists the floor clauses that hold the member below +that rung, each with its `targets` when it names dependencies. An empty list beside a next rung +means the floor already holds that rung: the rung waits for its author, and +`totals.waitingForAuthor` counts those members. It is never a promotion. Promotion preflight +(recipe 9) reads the rung above the floor reached instead, under its own names. + +`design` counts the member's keyed Design and UI entries other than `description`, its pinned +declarations by recipe 24's rule, its open questions, and the blocking ones. `questions` lists each +open question with its key, its blocking flag, its entry address when it carries a key, the line +the location table records (`null` when not located), and its text. `decisions` lists the +decisions the member names through `decidedBy`, each with its stated rung, and `resolved: false` +when the target is no Spec. `implementedBy` and `referencedBy` list the code units that satisfy and +that reference the member, each with its file, line, role, and component, with the layer and +context its component anchor states; a reference confers nothing. `verifiers` and `examples` count +the member's verifiers and verifying examples beside how many are enabled. An unresolved member +keeps only its `id` and `resolved: false`. + +`boundary.restsOn` lists the Specs outside the Pack that a member relates to by `refines`, +`dependsOn`, `constrainedBy` or `decidedBy`, and `boundary.restedOnBy` the Specs outside it that +relate to a member by the same four relations. `boundary.verifiedBy` lists outside Specs that +`verifies` a member and `boundary.supersededBy` those that `supersedes` one, separately. Each row +carries the outside Spec's stated rung, +whether it carries `implemented`, and under `via` the members it joins per relation type. The +columns are counts and lists, never a score or a rung. The Design Review's Pack page renders the +same assembly. The laws are `spec:consumers.pack-design` and +`spec:consumers.agent-surface.design-recipes`. + +## 30. Design-change impact + +*When you need this: you are about to change a Spec's design and want every Spec that rests on it, +with the code, verifiers and Packs the change asks to follow.* + +The parameter is `params.specs`, the Spec ids you are changing; an id the graph does not hold comes +back in `changed` with `resolved: false` and reaches nothing. + +```js +const changed = params.specs ?? ["spec:validation.readiness-floor"]; +const followed = new Set(["refines", "dependsOn", "constrainedBy", "decidedBy"]); +const specIds = new Set(g.specs().map((spec) => spec.id)); +const byCodeUnit = (left, right) => (left < right ? -1 : left > right ? 1 : 0); +const inbound = new Map(); +for (const edge of graph.edges) { + if (!followed.has(edge.type) || edge.claim !== "declared") continue; + if (!specIds.has(edge.from) || !specIds.has(edge.to)) continue; + if (!inbound.has(edge.to)) inbound.set(edge.to, []); + inbound.get(edge.to).push(edge); +} + +const reached = new Map(); +const queue = []; +for (const id of changed) { + if (!specIds.has(id) || reached.has(id)) continue; + reached.set(id, { distance: 0, path: [] }); + queue.push(id); +} +for (let at = 0; at < queue.length; at += 1) { + const id = queue[at]; + const here = reached.get(id); + const edges = [...(inbound.get(id) ?? [])].sort( + (left, right) => byCodeUnit(left.from, right.from) || byCodeUnit(left.type, right.type), + ); + for (const edge of edges) { + if (reached.has(edge.from)) continue; + reached.set(edge.from, { + distance: here.distance + 1, + path: [...here.path, { from: edge.from, type: edge.type, to: id }], + }); + queue.push(edge.from); + } +} + +const unitsOf = (bindings) => + bindings.map((unit) => ({ id: unit.codeId, file: unit.file ?? null, line: unit.line ?? null })); +const rows = [...reached.entries()] + .map(([id, reach]) => { + const context = g.specContext(id); + return { + id, + distance: reach.distance, + path: reach.path, + statedReadiness: context.statedReadiness, + implementedBy: unitsOf(context.implementations), + referencedBy: unitsOf(context.references), + enabledVerifiers: context.verifiers + .filter((verifier) => verifier.enabled) + .map((verifier) => verifier.verifierId), + packs: context.packs, + }; + }) + .sort((left, right) => left.distance - right.distance || byCodeUnit(left.id, right.id)); + +return { + changed: changed.map((id) => ({ id, resolved: specIds.has(id) })), + totals: { + specs: rows.length, + dependents: rows.filter((row) => row.distance > 0).length, + units: new Set( + rows.flatMap((row) => [...row.implementedBy, ...row.referencedBy].map((unit) => unit.id)), + ).size, + }, + rows, +}; +``` + +The walk follows `refines`, `dependsOn`, `constrainedBy` and `decidedBy` edges backwards, from the +Spec they point at to the Spec that declares them, so it reaches every Spec that refines, depends +on, is constrained by, or is decided by a changed Spec, and then every Spec that rests on those, to +any depth. It never follows a relation outward: what a changed Spec rests on does not move with it, +and dependency footing (recipe 21) reads that side. Each Spec is reached once, by a shortest path. +`distance` counts its hops, 0 for a changed Spec. `path` lists the hops from the changed Spec out, +each naming the dependent Spec (`from`), the relation, and the Spec it rests on (`to`). The walk is +breadth-first and takes each Spec's dependents in id order, then relation type, so of two shortest +paths it keeps the one it finds first. Rows sort by distance, then id. + +For each row, `implementedBy` lists the units that satisfy the Spec and `referencedBy` the units +that reference it, each with its file and line. Both are code a design change asks to follow, and a +reference confers nothing. `enabledVerifiers` lists the ids of the verifiers whose binding resolves, +a test anchor or an enabled example; a binding says nothing about whether a test passes. `packs` +lists the Packs the Spec belongs to. `totals.dependents` counts the Specs reached beyond the changed +ones, and `totals.units` the distinct units across all rows. The law is +`spec:consumers.agent-surface.design-recipes`. + +## 31. Decision register + +*When you need this: you want every decision with its rung, what it supersedes and what supersedes +it, what it narrows and what narrows it, what it shapes, and what it leaves open, read from the graph instead of kept by hand.* + +```js +const byCodeUnit = (left, right) => (left < right ? -1 : left > right ? 1 : 0); +const ends = (relations, type) => + relations + .filter((relation) => relation.type === type) + .map((relation) => relation.otherId) + .sort(byCodeUnit); +const decisions = g.specs().filter((spec) => spec.specKind === "decision"); +const decisionIds = new Set(decisions.map((spec) => spec.id)); +const narrowing = (relations) => ends(relations, "refines").filter((id) => decisionIds.has(id)); +const rows = decisions + .map((spec) => spec.id) + .sort(byCodeUnit) + .map((id) => { + const context = g.specContext(id); + const authored = context.sections?.intent?.openQuestions; + const questions = Array.isArray(authored) ? authored : []; + return { + id, + title: context.title ?? null, + statedReadiness: context.statedReadiness, + supersedes: ends(context.relationsOut, "supersedes"), + supersededBy: ends(context.relationsIn, "supersedes"), + narrows: narrowing(context.relationsOut), + narrowedBy: narrowing(context.relationsIn), + shapes: ends(context.relationsIn, "decidedBy"), + questions: questions + .filter((entry) => typeof entry === "object" && entry !== null) + .filter((entry) => typeof entry.key === "string") + .map((entry) => ({ + key: entry.key, + blocking: entry.blocking === true, + address: `${id}#question.${entry.key}`, + })), + packs: context.packs, + }; + }); +const byRung = {}; +for (const row of rows) byRung[row.statedReadiness] = (byRung[row.statedReadiness] ?? 0) + 1; + +return { + totals: { + decisions: rows.length, + byRung, + superseded: rows.filter((row) => row.supersededBy.length > 0).length, + shapingNothing: rows.filter((row) => row.shapes.length === 0).length, + }, + rows, +}; +``` + +The body takes no parameter. It returns one row per decision Spec, sorted by id in code-unit order. +`supersedes` lists the decisions this one supersedes and `supersededBy` the ones that supersede it; +follow either to walk a chain. `narrows` lists decision-kind Specs this decision `refines`, and +`narrowedBy` lists decision-kind Specs that `refines` it. Refinement narrows part of a decision; +supersession replaces the whole decision. Other kinds that refine a decision stay out of these +two lists. `shapes` lists the Specs that name the decision through +`decidedBy`. `questions` lists the decision's open questions that carry a key, each with its +blocking flag and entry address; the open-question register (recipe 20) lists the unkeyed ones +too. `totals.byRung` counts the decisions by stated rung, `totals.superseded` the ones another +decision supersedes, and `totals.shapingNothing` the ones no Spec names through `decidedBy`. A +superseded decision stays a row: supersession is a relation, never a removal. The ratified names +stay in the decision registry, `docs/concept/DECISIONS.md`, and the decision map (recipe 18) ranks +decisions by shaping fan-in. The law is `spec:consumers.agent-surface.design-recipes`. + +## 32. Architecture crossings + +*When you need this: you want every component dependency that crosses a bounded context or a +layer, and which way it runs, before you write or review an architecture rule.* + +```js +const byCodeUnit = (left, right) => (left < right ? -1 : left > right ? 1 : 0); +const nodes = new Map(graph.nodes.map((node) => [node.id, node])); +const componentOf = new Map(); +for (const edge of graph.edges) if (edge.type === "memberOf") componentOf.set(edge.from, edge.to); +const componentFor = (id) => { + const node = nodes.get(id); + if (node?.nodeType !== "CodeNode") return undefined; + if (id.startsWith("component:")) return node; + const component = nodes.get(componentOf.get(id)); + return component?.nodeType === "CodeNode" ? component : undefined; +}; +const describe = (component) => ({ + id: component.id, + layer: component.layer ?? null, + context: component.context ?? null, +}); + +const uses = graph.edges.filter((edge) => edge.type === "uses"); +let resolved = 0; +const rows = []; +for (const edge of uses) { + const from = componentFor(edge.from); + const to = componentFor(edge.to); + if (from === undefined || to === undefined) continue; + resolved += 1; + const differs = ["context", "layer"].filter( + (field) => + typeof from[field] === "string" && + typeof to[field] === "string" && + from[field] !== to[field], + ); + if (differs.length > 0) { + rows.push({ + from: edge.from, + to: edge.to, + fromComponent: describe(from), + toComponent: describe(to), + differs, + }); + } +} +rows.sort((left, right) => byCodeUnit(left.from, right.from) || byCodeUnit(left.to, right.to)); + +const tally = (field) => { + const counts = new Map(); + for (const row of rows) { + if (!row.differs.includes(field)) continue; + const pair = `${row.fromComponent[field]} -> ${row.toComponent[field]}`; + counts.set(pair, (counts.get(pair) ?? 0) + 1); + } + return Object.fromEntries( + [...counts.entries()].sort(([left], [right]) => byCodeUnit(left, right)), + ); +}; + +return { + totals: { + usesEdges: uses.length, + resolvedToComponents: resolved, + crossings: rows.length, + contextCrossings: rows.filter((row) => row.differs.includes("context")).length, + layerCrossings: rows.filter((row) => row.differs.includes("layer")).length, + byContextPair: tally("context"), + byLayerPair: tally("layer"), + }, + rows, +}; +``` + +The body takes no parameter. It resolves both ends of each `uses` edge to a component: a +`component:` unit is its own, and any other unit belongs to the target of its `memberOf` edge. +`totals.usesEdges` counts every `uses` edge and `totals.resolvedToComponents` the ones whose two +ends both resolve; an edge with an end that belongs to no component is left out of the rows. An +edge crosses when both components state a context and the contexts differ, or both state a layer +and the layers differ. Two units in one component never cross, and an attribute either component +leaves unstated never counts as a difference. Each row names both units (`from`, `to`), both components with their +layer and context, and `differs`, the attributes that differ. Rows sort by `from`, then `to`. +`totals.byContextPair` and `totals.byLayerPair` tally the crossings as `from -> to`, so the +direction of a corpus's dependencies reads at a glance. The recipe reports and never refuses: which +crossings are lawful is the corpus's own rule, as the architectural annotation decision leaves it. +Roles, layers and contexts (recipe 28) lists the values the anchors state. The law is +`spec:consumers.agent-surface.design-recipes`. diff --git a/docs/concept/04-authoring-and-binding.md b/docs/concept/04-authoring-and-binding.md index 96aefa0d..9cd9f859 100644 --- a/docs/concept/04-authoring-and-binding.md +++ b/docs/concept/04-authoring-and-binding.md @@ -64,68 +64,19 @@ For any given Spec or Pack ID, exactly one surface is canonical, with no mixing --- -## 2. Generic source anchors — binding code to intent (CORE) +## 2. Source anchors — binding code to intent (CORE) -An **anchor** binds a code location to a spec ID and minimal structural facts. It is the anchored layer of the graph. Anchors are **framework-neutral**: they work on any class, function, route, or module, regardless of how the runtime is wired. +The anchor law has moved into Specs. This section keeps only pointers to them, under the concept-documents dissolution decision (`spec:decisions.concept-docs-dissolve`). -```ts -// Decorator form (one Representation) -@arch.node({ id: "impl:orders.create-order-use-case", satisfies: ["spec:orders.create-order"], component: "component:orders.domain" }) -export class CreateOrderUseCase { /* ... */ } - -// JSDoc form (equivalent) -/** @arch.node id=impl:orders.create-order-use-case satisfies=spec:orders.create-order */ -export function createOrder() { /* ... */ } - -// Anchor-constant form (equivalent, decorator-free) — the one surface the MVP extractor reads -export const _anchor = codeAnchor({ - id: codeAnchorId("impl:orders.create-order-use-case"), - satisfies: ref("spec:orders.create-order"), -}); -``` - -The three syntaxes are interchangeable Representations; the *binding* is the thing. A team picks one style; -the MVP extracts the **anchor-constant form** (a top-level `const` initialized with the builder call) — the -decorator and JSDoc forms remain unextracted Representations. The builder is the generic **`codeAnchor`** -over the implementation-flavored code namespaces (`impl` / `api` / `component`) — the generic `codeAnchor` -decision (MD-8, folded into the builder's doc-comment in `src/model/anchors.ts`). One binding target per -anchor (two bindings are two anchors); the decorator sketch above shows an array form and a `component` -field that are possible later Representations, not the landed signature. - -### Anchors assert a binding — never intent (P9/P10) - -An anchor says exactly one thing: *"this code location is the implementation/test **binding** for this Spec ID"* — a binding assertion only, never system-truth content (DECISIONS R1). The landed contract is exactly that minimal: `id` · an optional display `label` · **one** binding target (`satisfies` on a code anchor, `verifies` on a test anchor, `models` on an oracle anchor). Any other field is an extraction **error** — the anchored-surface twin of authoring-shape honesty. Richer structural bindings (`component`, `implements`, `handles`/`emits`) are **ASPIRATIONAL** — possible later extensions (see the inline-vs-centralized open question, `07` §4), never the MVP contract. An anchor is **forbidden** from carrying anything spec-level: behavior, rationale, readiness, acceptance criteria, or delivery facts. This asymmetry is load-bearing: - -- **Intent stays centralized** in the spec files, never scattered through code comments. -- Anchors produce **anchored**-`claim` edges, distinct from **declared** relations (P9). -- The direction is one-way: code points *to* spec, never the reverse. - -### Test binding — the `verifies` trace (CORE) - -A test declares which spec it verifies via a **binding-only test anchor** — identity plus the `verifies` -target, never an executing callback (DECISIONS R3: a binding that carried a `run` body would couple the -graph binding to execution, contradicting "the graph records that an enabled verifier *exists*, never that -it ran"). The test body itself stays an ordinary runner test beside the anchor: - -```ts -import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; - -export const createOrderValidCartTest = specTest({ - id: testAnchorId("test:orders.create-order.valid-cart"), - label: "valid cart verifies the create-order happy path", - verifies: ref("spec:orders.create-order.valid-cart"), -}); - -// ... 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 `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. - -### An anchor-required lint (optional, CORE-adjacent) +- **The anchor model** — `spec:model.anchors`: identity, an optional label, zero or more Spec targets and optional structure, written in the constant form or the comment form, both feeding one closed envelope; code, test and oracle anchors; which builder imports the extractor trusts. +- **The comment form** (MD-36) — `spec:decisions.anchor-comment-form`: a top-level `/** … */` block with reserved `@sdp*` tags binds code that cannot import the package. The decorator form stays an unextracted representation. +- **Bindings are optional, plural, and may reference a design** (MD-37) — `spec:decisions.anchor-binding-grain`: `satisfies` is optional and plural, `verifies` is plural, `references` names a design the code answers to without realizing it, and every target names a whole Spec. +- **Architectural significance is annotated where it is realized** (MD-38) — `spec:decisions.architectural-annotation`: `component`, `uses` and `role`, plus `layer` and `context` on a component anchor. None of them confers a delivery fact, and an anchor-required lint stays optional and warn-level. +- **Binding, never liveness** (MD-7) — `spec:decisions.binding-not-liveness`: a test anchor records that a verifier exists, never that it ran; pass and fail stay in CI. +- **Oracle targets** — `spec:validation.oracle-target-eligibility`: an example space has zero or one resolving oracle, and consumers fail closed. +- **How to write one** — the `sdp-authoring` skill, "Bind code, tests, and oracles". -A lint rule can flag designated patterns (e.g. exported use-case classes, route handlers) that lack an anchor, so significant code does not silently fall out of the graph. Useful, not load-bearing. +One point no Spec states, kept here: anchors are framework-neutral. An anchor binds any class, function, route or module, however the runtime is wired, so binding code never needs the framework's composition (§3). --- diff --git a/docs/concept/06-consumers-and-projections.md b/docs/concept/06-consumers-and-projections.md index 96c17430..016eadae 100644 --- a/docs/concept/06-consumers-and-projections.md +++ b/docs/concept/06-consumers-and-projections.md @@ -145,7 +145,7 @@ Why patching dissolves: The flagship human view is the Design Review's relationship slice: a derived, regenerable human-readable projection — **fully derived** and reproducible (delete and rebuild identically). Per spec it shows: header (title, `kind`, `altitude`, `readiness`, and any stated-vs-derived divergence); intent and behaviour (rules/examples); relations; bindings (implementing code, tests, with source links, derived from anchors); verification status (does a linked, enabled verifier *exist* — the `has-verifier` delivery fact, not run results); an impact list; and `claim` cues (declared vs anchored vs inferred shown distinguishably, P9). The census, Mermaid, and Gherkin-shaped read projections are co-equal derived views with narrower jobs; none re-specifies the Design Review. -Owned prose is already available through the graph and Reader: `narrative` appears in a spec summary/context and concept search, while section descriptions remain in `SpecContext.sections` and are searchable. Design Review now renders `Spec.narrative` and the seven approved section descriptions from those Reader/graph values (schema `0.8.0`), with no source reparse and stable omission when prose is absent. +Owned prose is already available through the graph and Reader: `narrative` appears in a spec summary/context and concept search, while section descriptions remain in `SpecContext.sections` and are searchable. Design Review now renders `Spec.narrative` and the seven approved section descriptions from those Reader/graph values (schema `0.9.0`), with no source reparse and stable omission when prose is absent. Schema `0.6.0` changes accepted ids by reserving the `#` sub-part for an entry address and changes ordering semantics by preserving authored key order in `design`, `ui`, and `model.terms`, with a `design` or `ui` section's `description` first. Field shapes are unchanged. diff --git a/docs/concept/07-mvp-roadmap-and-open-questions.md b/docs/concept/07-mvp-roadmap-and-open-questions.md index 2f2d2885..8418ece1 100644 --- a/docs/concept/07-mvp-roadmap-and-open-questions.md +++ b/docs/concept/07-mvp-roadmap-and-open-questions.md @@ -70,7 +70,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. *(`spec:consumers.derived-readiness-banner`, `spec:validation.readiness-floor`.)* - **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.)* +- **Inline-vs-centralized anchor semantics (resolved, recorded here for traceability).** Anchors still carry no intent, and the structure they may carry is ruled. `satisfies` is optional and plural, `verifies` is plural, and `references` names a design the code answers to (the binding-grain ruling, MD-37). `component` and `uses` stay, and a code anchor may add a `role`, a component anchor a `layer` and a `context` (the architectural-annotation ruling, MD-38, which supersedes MD-30 and MD-34). No `implements` field is admitted. *(`spec:model.anchors`.)* - **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 — MD-27 admits graph-aware Gherkin for behavior and example Specs, MD-28 settles `.sdp.gherkin`; harnesses remain open). Driven by measured pain after the MVP loop holds, not by the roadmap. @@ -98,10 +98,11 @@ the standing invariant plus where its protection lives. - **① Authoring ergonomics — the headline forward risk.** If authoring feels heavy, authors (human **and** agent) avoid the system or overfit specs to satisfy tooling. Two levers ship in the MVP: **typed sections** (autocomplete + shape guardrails — the typing law, MD-11) and the one diagnostic rendering rule (location - rendered from the finding's structured fields; first contact fails clean — §1, Slice 5). The remaining - levers are genuinely forward-looking: `sdp validate --watch`; later `sdp new spec` / `sdp explain` (below - the second-caller bar, `06` §3). Threads back to the anti-padding rule: make *dishonesty* fail without - rewarding low-signal filler (a floor to clear, never a quota to fill). + rendered from the finding's structured fields; first contact fails clean — §1, Slice 5). Two more levers + shipped with plan 35: `sdp new spec` writes the idea-rung carrier (`spec:consumers.authoring-on-ramp`), + and `sdp validate --watch` re-runs validation on every carrier edit (`spec:consumers.validate-watch`). + `sdp explain` stays below the second-caller bar (`06` §3). Threads back to the anti-padding rule: make + *dishonesty* fail without rewarding low-signal filler (a floor to clear, never a quota to fill). - **② A golden correctness oracle, kept distinct from the determinism self-checks.** Both exist, labeled distinctly, never conflated. The **correctness oracle** — "did the extractor produce the *right* graph" — is the committed golden fixture: `test/fixtures/checkout-v1/expected-graph.json` plus the diff --git a/docs/concept/DECISIONS.md b/docs/concept/DECISIONS.md index a59380da..b7bc0eaa 100644 --- a/docs/concept/DECISIONS.md +++ b/docs/concept/DECISIONS.md @@ -4,7 +4,7 @@ ## The ratified-name registry -In prose, lead with meaning and use a code only as a parenthetical lookup key. Curation records the ADR three-part test: hard to reverse, surprising without context, and a real trade-off. `held` marks a decision Spec that states `defined` and awaits the owner's `ready`; `superseded` marks a ruling a later entry replaces, and the row points forward. +In prose, lead with meaning and use a code only as a parenthetical lookup key. Curation records the ADR three-part test: hard to reverse, surprising without context, and a real trade-off. `held` marks a decision Spec that states `defined` and awaits the owner's `ready`; `superseded` marks a ruling a later entry replaces whole, and the row points forward. When the replaced record holds nothing a reader still needs, the replacing decision carries what survives and the record is deleted instead; its row stays, marked `deleted`, with the commit that last held its text. A decision that narrows one part of another refines it, and the narrowed row names it. Numbering is append-only: absent identifiers such as MD-3 and MD-6 are retired or reserved historical positions and are never reused. @@ -38,15 +38,17 @@ positions and are never reused. | MD-27 | the Gherkin carrier option | durable | Behavior and example Spec IDs may select a graph-aware Gherkin carrier; Markdown stays default, execution stays behind generated contracts and anchored handlers, and no lifecycle taxonomy is imported. MD-28 settles the canonical suffix as `.sdp.gherkin`. | [Spec](../../specs/decisions/gherkin-carrier-option.sdp.md) (`spec:decisions.gherkin-carrier-option`) | | MD-28 | the `.sdp.gherkin` extension | durable | Canonical Gherkin carriers use `.sdp.gherkin`; bare `.feature` stays non-canonical import-source territory so Protocol files stay outside default Cucumber globs. | [Spec](../../specs/decisions/sdp-gherkin-extension.sdp.md) (`spec:decisions.sdp-gherkin-extension`) | | MD-29 | the carrier universality bound | durable | Gherkin remains honest for behavior and example Specs; universality is per-ID carriers plus a generated read projection, with Markdown still the default. | [Spec](../../specs/decisions/carrier-universality.sdp.md) (`spec:decisions.carrier-universality`) | -| MD-30 | structural anchors confer nothing | superseded by MD-38 | `component` and `uses` author narrow CodeNode structure while leaving realization, readiness, and delivery facts untouched; the semantics survive inside MD-38. | [Spec](../../specs/decisions/structural-anchor-semantics.sdp.md) (`spec:decisions.structural-anchor-semantics`) | +| MD-30 | structural anchors confer nothing | deleted | `component` and `uses` author narrow CodeNode structure while leaving realization, readiness, and delivery facts untouched; replaced by MD-38, which restates what survives. | deleted; its text reads in git at `90c75dd` (`specs/decisions/structural-anchor-semantics.sdp.md`) | | MD-31 | adopted registrars are committed | durable | A registrar imported by tracked authored code is committed and byte-checked; unadopted registrar siblings remain ignored and regenerable. | [Spec](../../specs/decisions/adopted-registrars-committed.sdp.md) (`spec:decisions.adopted-registrars-committed`) | -| MD-32 | the shipped projections stay frozen | durable | The shipped Design Review, census, Mermaid, and Gherkin projections are settled read models; re-specifying them is refused, and reopening needs a superseding decision that passes the ADR test. | [Spec](../../specs/decisions/shipped-projections-frozen.sdp.md) (`spec:decisions.shipped-projections-frozen`) | -| MD-33 | planning truths live in ruled graph homes | durable | Each planning-truth type has one ruled home in the graph, the briefs index shape is retired as a carrier of law, and reopening needs a superseding decision that passes the ADR test. | [Spec](../../specs/decisions/planning-truths-placement.sdp.md) (`spec:decisions.planning-truths-placement`) | -| MD-34 | architectural significance rides existing primitives | superseded by MD-38 | Specs carrying architectural significance are linked by the existing relations and no pattern vocabulary is admitted; the CodeNode-grain role it gave up returns under MD-38. | [Spec](../../specs/decisions/architectural-significance-rides-primitives.sdp.md) (`spec:decisions.architectural-significance-rides-primitives`) | -| MD-35 | source commentary never enters the graph | superseded by MD-36 | Comment prose authors no graph content and law-carrying comments promote into Specs; the identity half, refusing reserved tags in a doc comment, is reversed by MD-36. | [Spec](../../specs/decisions/jsdoc-graph-extraction-refused.sdp.md) (`spec:decisions.jsdoc-graph-extraction-refused`) | +| MD-32 | the shipped projections stay frozen | durable | The shipped Design Review, census, Mermaid, and Gherkin projections are settled read models; re-specifying them is refused; a decision that supersedes it replaces it whole, and a decision that refines it narrows it for one projection or rendering, each passing the ADR test. Three records refine it, each for one part of the Design Review: open-section entries render as a list, an open question shows its key, and the Pack page presents the Pack's design (MD-39). | [Spec](../../specs/decisions/shipped-projections-frozen.sdp.md) (`spec:decisions.shipped-projections-frozen`); narrowed by [Spec](../../specs/decisions/authored-entry-order.sdp.md) (`spec:decisions.authored-entry-order`), [Spec](../../specs/decisions/question-key-rendering.sdp.md) (`spec:decisions.question-key-rendering`), and [Spec](../../specs/decisions/pack-design-page.sdp.md) (`spec:decisions.pack-design-page`) | +| MD-33 | planning truths live in ruled graph homes | durable | Each planning-truth type has one ruled home in the graph, the briefs index shape is retired as a carrier of law, and a refusal is replaced whole by a decision that supersedes it or narrowed by one that refines it, either passing the ADR test. | [Spec](../../specs/decisions/planning-truths-placement.sdp.md) (`spec:decisions.planning-truths-placement`) | +| MD-34 | architectural significance rides existing primitives | deleted | Specs carrying architectural significance are linked by the existing relations and no pattern vocabulary is admitted; replaced by MD-38, which restates what survives and returns the CodeNode-grain role. | deleted; its text reads in git at `90c75dd` (`specs/decisions/architectural-significance-rides-primitives.sdp.md`) | +| MD-35 | source commentary never enters the graph | deleted | Comment prose authors no graph content and law-carrying comments promote into Specs; replaced by MD-36, which restates that half and reverses the identity half. | deleted; its text reads in git at `90c75dd` (`specs/decisions/jsdoc-graph-extraction-refused.sdp.md`) | | MD-36 | the comment form | durable | A `/** … */` comment carrying reserved camelCase `@sdp*` tags is a second extracted representation of the anchor, feeding the same closed envelope as the constant form; prose still authors nothing, no import is required, and the package ships a zero-dependency `/anchors` subpath. | [Spec](../../specs/decisions/anchor-comment-form.sdp.md) (`spec:decisions.anchor-comment-form`) | | MD-37 | bindings are optional, plural, and may reference a design | durable | `satisfies` is optional and plural, `verifies` plural, targets name whole Specs, an identity-only code anchor is lawful, and the anchored `references` edge says code answers to a design it does not claim to realize, conferring no delivery fact; a design that builds on existing code says so by its own relation, never by a reference from the code. | [Spec](../../specs/decisions/anchor-binding-grain.sdp.md) (`spec:decisions.anchor-binding-grain`) | | MD-38 | architectural significance is annotated where it is realized | durable | A code anchor may carry a `role`, a `component:` anchor its closed-set `layer` and its `context`, and the anchor is the declared component; the `component`/`uses` semantics, structural non-conferral, no inference, no status on code, and no pattern vocabulary survive from MD-30 and MD-34. | [Spec](../../specs/decisions/architectural-annotation.sdp.md) (`spec:decisions.architectural-annotation`) | +| MD-39 | the Pack design page | durable | The Design Review's Pack page presents the Pack design the reader assembles: each member's next rung and what holds it there, its design columns, the Pack's boundary, open questions with their addresses and lines, and the code that realizes or references each member; it refines the projections freeze for that page only. | [Spec](../../specs/decisions/pack-design-page.sdp.md) (`spec:decisions.pack-design-page`) | +| MD-40 | checked mentions | durable | A Spec id written in prose is a mention that must resolve and is warned once per pair when no declared relation backs it; an entry address names one keyed Design or UI entry or keyed open question. | [Spec](../../specs/decisions/checked-mentions.sdp.md) (`spec:decisions.checked-mentions`) | ### Current executable decision-spec pointers @@ -63,6 +65,6 @@ positions and are never reused. | **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 | +| **D4** | Design Review is the flagship curated projection | [Design Review spec](../../specs/consumers/design-review.sdp.md) (`spec:consumers.design-review`) | | **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`) | | **D6** | MCP integration is designed-in and deferred | [MCP-deferred decision](../../specs/decisions/mcp-deferred.sdp.md) (`spec:decisions.mcp-deferred`) | diff --git a/plans/41-design-management-closer-to-v0.md b/plans/41-design-management-closer-to-v0.md new file mode 100644 index 00000000..970ccd88 --- /dev/null +++ b/plans/41-design-management-closer-to-v0.md @@ -0,0 +1,465 @@ +# Plan 41. Design management, closer to the original design + +> **Status:** ✅ EXECUTED on `feature/design-management-v0`, from `main` at `822aea4`. The engine, +> corpus, recipes and skills changed together; the full gate passes. The arc's intent lives in the +> graph: `pack:design-management-v1` holds the built and captured Specs in reading order, and +> readiness, backlog and findings are read from the graph (recipes 1, 2, 9, 11, 20), never from +> this file. The owner ruled every open item on 2026-10-12, and the fold landed on the same branch: +> MD-39 and MD-40 ratified, the arc's Specs and the drift queue `ready`, the captures' questions +> answered, the candidate terms ratified, and plan 42 drafted for the Studio. The rulings, the +> fold's lanes, its review and its measurements are the last section. If this file and the graph +> disagree, the graph wins and this file is stale. + +## Why this arc + +The owner's direction, restated on 2026-10-10: improve the Protocol as far as it goes toward +complex, iterative design work, move closer to the original design (`docs/lineage/v0-design/`), +and improve self-hosting. Three inputs, read on 2026-10-10: + +- **The first adopter's design machinery.** `libar-dev/libar-platform` designs ahead of its code + with capability Packs, design passes by perspective, a decision register and pinned + declarations. To do it, it hand-built about 2,400 lines of tooling: a 1,201-line Pack page + generator (`scripts/pack-page.mjs`), a 408-line declarations emitter, a 467-line register tool + and a 363-line check. The page re-parses carrier files for entry lines, rewrites recipe 25's + source to pass its addresses, and scrapes recipe bodies out of the Markdown catalog. Its open + asks to the Protocol are in `docs/feedback/sdp-feedback-01.md` and `-02.md` there. +- **The original design.** A claim-by-claim map of all nine v0 chapters against the corpus + (research run 2026-10-10, kept in `.omo/evidence/plan-41/research/`). The three lanes that + counted, chapters 00 to 02, 04, 06, 07 and 08, found 186 claims realized, 54 captured ahead of + code, 111 departed by a ruling, 55 with no home and 19 out of scope; the fourth lane mapped + chapters 03 and 05 by table. The uncaptured claims that matter most for design work: a `designed` stage (01 §1.4), components declared before + code (03 §2.5), source lines for entries (06 §2.4), design comparison between commits (08 §6.1), + context bundles (07 §5.5), example-space coverage (04 §4), custom corpus rules (06 §6.5), and a + decision index (02 §1). +- **The Protocol's own corpus.** It self-hosts extraction and validation well but uses its own + design features thinly: 2 Packs (one a 225-member aggregate), 7 Specs with a Design section, + 17 open questions without keys, guidance in `docs/concept/` and two Studio Specs that still + teaches rulings plan 40 superseded, and plan 35's deferral triggers still living in plan prose. + +## The owner's rulings of 2026-10-10 + +Asked in session; each answer is the recommended option. + +1. **The Pack page.** Ship the Pack's design as data (an exported reader value and a recipe) and + render it in the Design Review's Pack page, through a decision that supersedes the projections + freeze for that page only. The Studio later renders the same data. +2. **`designed`.** No new rung. Derive design columns beside readiness (keyed entries, pinned + declarations, decisions, open and blocking questions) and capture the rung question as an + `idea` Spec that names the forms it weighs. +3. **Planned architecture.** Capture `spec:model.planned-architecture` at `idea`: component designs + as Specs, a comparison with anchored components once code exists, and the architectural + annotation ruling's realized-only component held as a keyed question. +4. **The drift queue.** A per-Spec review packet for the sixteen Specs the drift alarm names; the + owner states `ready` on the ones accepted. The packet is the last section of this plan. + +## What this arc builds + +Each item is a Spec authored first, then built. Each states `defined` until the owner states +`ready`; the decision states `defined` until the owner ratifies its text. + +### B1. The Pack design page (`spec:decisions.pack-design-page`) + +A decision that supersedes `spec:decisions.shipped-projections-frozen` for the Design Review's Pack +page only, as `spec:decisions.authored-entry-order` did for open-section rendering. The page +presents the Pack's design as it stands: what each member is, where its readiness stands against +the next rung, what design it carries, what it rests on outside the Pack, its open questions with +their addresses and lines, and the code that realizes or references it. The Spec page, the index, +census, Mermaid and Gherkin stay frozen. + +### B2. The Pack design (`spec:consumers.pack-design`) + +`packContext` gains the design columns, computed once by the reader, so the page, a recipe and an +adopter's script read one answer: + +- per member: the rung above the **stated** rung and its unmet clauses (empty when the floor + holds it, the drift-queue case), beside the existing floor; the design columns (keyed Design + and UI entries, pinned declarations by recipe 24's rule, open and blocking questions, the + decisions it is `decidedBy` with their rungs); the realizing and referencing units, each with + its component's layer and context; enabled verifiers and verifying examples. +- per Pack: the boundary, the Specs outside the Pack that members relate to (rests on) and that + relate to members (rested on by), each with relation type, stated rung and `implemented`. + +A code unit binding gains its component (`memberOf` target, with layer and context), additive on +`SpecContext` too. No new reader method: this extends the existing Pack context, and it clears the +second-caller bar (the Design Review and the adopter's page, whose hand-rolled walk read the rung +above the stated one where preflight reads the rung above the floor). + +### B3. Entry locations (`spec:extraction.entry-locations`) + +The extractor records the source line of every keyed Design and UI entry and every open question, +in a location table beside the nodes. Primitive and Pack nodes stay line-free, so node-level +goldens survive Spec edits and carrier parity compares nodes, not lines. The reader's Spec context +carries the Spec's locations. Recipe 25 returns each resolved entry's value, file and line; recipe +23 returns the line. Graph schema `0.9.0`. + +### B4. Recipe parameters and recipe files (`spec:consumers.agent-surface.recipe-parameters`) + +`sdp q --params JSON` (or `--params @path`) injects a fourth binding, `params`, defaulting to an +empty object. Every parameterized recipe reads its parameter from `params` and falls back to the +catalog's sample: `spec` (3, 9, 19, 21), `files` (4, replacing `SDP_CHANGED_FILES_JSON`), `pack` +(5, 27, 29), `term` (6, 23), `component` (14), `scope` (22), `addresses` (25) and `specs` (30). The build writes each catalog body to +`dist/recipes/NN-slug.js`, shipped in the package, so an adopter runs a body as shipped. The catalog +stays the one owner of the bodies; the files are derived and checked. + +### B5. Design recipes (`spec:consumers.agent-surface.design-recipes`) + +Four recipes, each run by the recipe test: + +- 29 **Pack design**: the B2 assembly as data, the page's input for any renderer. +- 30 **Design-change impact**: from Specs being changed, the transitive inbound dependents + (`refines` children, `dependsOn`, `constrainedBy`, `decidedBy`), and for each the units that + satisfy or reference it, its verifiers and its Packs. What a design change asks to follow. +- 31 **Decision register**: every decision with its stated rung, its `supersedes` chain both ways, + the Specs it shapes, its keyed open questions and its Packs. `DECISIONS.md` keeps the ratified + names; the register's substance is read from the graph. +- 32 **Architecture crossings**: `uses` edges whose two ends, resolved to their components, sit in + different contexts or layers. Reports, refuses nothing; a corpus gates its own rule (MD-38). + +### B6. Engine provenance (`spec:consumers.engine-provenance`) + +`sdp --version` prints the package version and the commit the build came from, so an adopter can +tell the engine moving from its corpus moving. The build records the commit; the graph stays free +of it. + +## What this arc captures (v0, ahead of code) + +Each enters at `idea` or `scoped` with keyed open questions, and a `dependsOn` or `refines` to the +realized Spec it builds on: + +| Spec | v0 source | What it holds | +| --- | --- | --- | +| `spec:model.design-maturity` | 01 §1.4, §8 | The `designed` question: a fifth rung, a kind-conditional clause, the derived columns (B2), or none. | +| `spec:model.planned-architecture` | 03 §2.5, 05 | Component designs before code, compared with anchored components; MD-38's realized-only rule as a question; provider selection. | +| `spec:model.open-question-fields` | 01 §2.1 | Who decides a question, links, a provisional reading; no status. | +| `spec:model.rule-keys` | 01 §2.2 | Keys on behavior rules, addressable as `#rule.`. | +| `spec:model.relation-reasons` | 01 §4 | An optional reason on an authored relation. | +| `spec:consumers.design-diff` | 08 §6.1 | Designs compared between two commits at entry grain. | +| `spec:consumers.context-bundle` | 07 §5.5 | The pass brief composed per invocation; plan 35's trigger is met by the adopter's pass task. | +| `spec:consumers.review-perspectives` | 04 §6 | Perspectives over a Pack (v0's reader types, the adopter's advisor lenses). | +| `spec:consumers.intent-composition.proposal` | 02 §2, 07 §6 | The reviewable half of the patch loop: an addressed change proposal. | +| `spec:consumers.reference-projection` | plan 35 H | Deferred, with its re-entry trigger as a keyed question. | +| `spec:consumers.structural-mermaid` | plan 35 H | Deferred, with its re-entry trigger as a keyed question. | +| `spec:validation.superseded-decision-signal` | 06 §6.3.4 | A warning for a Spec shaped by a superseded decision; partial supersession as a question. | +| `spec:validation.architecture-constraints` | 06 §6.4 | Declared architecture rules checked against anchored structure; MD-38's refusal as a question. | +| `spec:validation.corpus-rules` | 06 §6.5 | Corpus-authored rules on the one validation path. | +| `spec:observation.run-evidence` | 06 §11.2, 08 | CI run records joined to verifiers in a view, never in the graph. | +| `spec:consumers.agent-surface.example-space-coverage` | 04 §4 | Witnesses and coverage gaps per literal slot; a recipe would need a second notation parser, so the reader may decode spaces and points instead. | + +## Self-hosting repairs + +- **Stale guidance.** `docs/concept/04-authoring-and-binding.md` §2 dissolves into pointers to + `spec:model.anchors` and the three plan-40 decisions; `docs/concept/07` rows that plan 35 and 40 + settled are corrected; `spec:consumers.spec-studio.lenses` moves its `decidedBy` from the + superseded MD-34 to MD-38; `spec:consumers.spec-studio.spec-page` stops saying component and + uses are the only structural fields; `AGENTS.md`'s status lines are re-pointed. +- **Question keys.** The 17 unkeyed questions gain keys, wording and flags unchanged. +- **Packs.** `pack:design-management-v1` for this arc; the three plan-40 decisions join the + self-hosting Pack. +- **Graph-first planning.** Its blocking arc-boundary question gains a key and the evidence of two + arcs authored as Packs (Studio, this one); the owner rules it. +- **The decision registry.** MD-32's row names its two narrow supersessions; D4 points at its + carrying Spec; MD-39 enters for B1. +- **Mentions and gaps.** Each of the nine unbacked mentions gets the relation that fits or stays + prose; a ready Spec without a verifier gets a test anchor only where a test checks the Spec's own + promise. Four mentions gained a relation and five stay prose; three of the five gaps gained a + verifier and two stay open. Each disposition and its reason is in + `.omo/evidence/plan-41/mentions.md`. +- **Vocabulary.** Candidate terms (question key, stated next rung, Pack design, boundary, pinned + declaration, location table, recipe parameter) enter `CONTEXT.md`'s flagged ambiguities for the + owner to ratify. +- **`sdp validate --watch`** gets the Spec and anchor it lacks. + +## Lanes and order + +1. **Contract.** This plan and the B1 to B6 Specs, authored first and validated. +2. **Parallel lanes**, each in its own worktree, on disjoint files: E1 entry locations + (`src/extract`, `src/graph`), E2 Pack design and page (`src/reader`, `src/projections`), E3 + CLI (`src/cli`, build), C1 captures (`specs/`), C2 self-hosting repairs (`specs/`, `docs/`, + `CONTEXT.md`, `AGENTS.md`), D the drift packet (read-only). +3. **Integration.** Merge lanes; recipes 29 to 32 on B2 to B4; recipe catalog, skills and docs; + self-hosting oracle pins re-derived and labelled re-measured; the engine's own anchors. +4. **Gate.** `npm run check` green. +5. **Review.** A read-only adversarial review of the branch (gpt-6-astra) and a line read + (gpt-6.1-sol); findings ruled and folded. +6. **Adopter acceptance.** The Pack design page and recipes run read-only over the adopter's + corpus (`pack:history-view`) from this branch's build, measured against its hand-built page. + +Commit per coherent unit after its focused tests pass; the full gate before the close commit. +Never push without the owner's word. + +## Acceptance criteria + +- The Design Review page for `pack:spec-studio-v1`, and for the adopter's `pack:history-view`, shows + each member's stated next rung with its unmet clauses, its design columns, its boundary, its + open questions with addresses and lines, and its realizing and referencing units with layer and + context, from the graph alone. +- Recipe 25 over the adopter's external addresses returns each value and line, passed with + `--params @file` and run from `dist/recipes/` as shipped. +- Recipes 29 to 32 run as written in the recipe test. +- `sdp --version` names the package version and build commit. +- No v0 claim this arc captures lacks a carrying Spec; `npm run check` passes. + +## Lanes, as run + +Each lane worked in its own git worktree on its own branch, merged without conflict. + +| Lane | Model | What it built | +| --- | --- | --- | +| Research | Opus 5.5 (three v0 lanes), gpt-6-astra (v0 03 and 05; self-hosting audit), gpt-6.1-sol (adopter inventory) | The six maps in `.omo/evidence/plan-41/research/`. | +| E1 entry locations | gpt-6.1-sol | The location table in the Markdown and TypeScript extractors, schema `0.9.0`, parity and goldens on nodes and edges only. The Gherkin carrier carries no open questions, so it records no rows. | +| E2 Pack design | Opus 5.5 | The Pack context's design columns and boundary, the code unit's component, the Spec context's entry locations, and the Pack page. | +| E3 CLI | Opus 5.5 | `sdp q --params`, the parameterized recipes, `dist/recipes/`, `sdp --version`, the watch loop's anchor. | +| C1 captures | Opus 5.5 | Fourteen captured Specs, each with keyed questions and a relation to what it builds on. | +| C2 repairs | Opus 5.5 | The stale guidance, 17 question keys, Pack membership, the registry rows, four backed mentions, three verifier bindings, candidate terms. | +| D drift packet | gpt-6.1-sol | The packet in the last section. | +| I1 recipes | Opus 5.5 | Recipes 29 to 32, lines in recipes 23 and 25, counts and skills. | +| I2 fallout | gpt-6.1-sol | The schema-version examples and goldens, the gates test, the warning pins. | +| I3 oracle | Opus 5.5 | The self-hosting oracle, transcribed from carriers and anchors. | + +## Review + +Two read-only reviews ran on the merged branch at `c1cce4c`: an adversarial review of the whole arc +(gpt-6-astra, xhigh) and a line read of the engine and tests (gpt-6.1-sol, high). Neither found a +blocker. Both confirmed the honesty laws hold: the stated next rung is never presented as approval, +references stay apart from realization, and a line-only edit moves locations without changing a +node or edge. Every finding was folded with a regression test. + +| # | Finding | Fold | +| --- | --- | --- | +| 1 | A CRLF catalog wrote no recipe files, then deleted the shipped ones (sol, major). | Lines split on `\r?\n`; an empty catalog is refused before anything is written. | +| 2 | Stale cleanup deleted another build's temporary files (sol). | Cleanup removes only finished `NN-slug.js` names. | +| 3 | The Pack boundary was quadratic in members sharing one outside Spec (sol). | An insertion-ordered set per Spec and relation; 40,000 members in about 0.1 s. | +| 4 | Line links carried raw paths and unescaped brackets (astra, sol). | Each path segment is percent-encoded; labels escape brackets. | +| 5 | A multiline title or question escaped its heading or list item (sol). | Line breaks collapse to a space in every heading and list item on the page. | +| 6 | "No code unit realizes or references this member" claimed more than the graph knows (astra). | "No implementation or design-reference binding is recorded for this member." | +| 7 | Recipe 23 lost the line of a string-form question (astra). | It maps `openQuestions[n]` to `question[n]` as the object form does. | +| 8 | `--params null` crashed every parameterized recipe (sol). | `--params` takes a JSON object; anything else is refused before the body runs. The recipe-parameters Spec says so. | +| 9 | The provenance tests did not prove the build publishes the commit it reads (sol). | A test drives the build step with a known hash and reads it back. | +| 10 | The Pack design anchor misstated its unit and carried the evaluator dependency (sol). | Its label names the helpers and the assembly they serve; the `uses` moved to `impl:protocol.reader`, which calls the evaluator. | +| 11 | The proposal contract sent bare Spec ids to recipe 25, which calls them malformed (astra). | A Spec id resolves through the Spec context, an entry address through recipe 25. | +| 12 | Two Specs cited recipe 33 for architecture crossings (astra, oracle lane). | Both say recipe 32; a corpus check now holds every Spec's recipe citation to the catalog. | + +A second astra read of the fold confirmed eleven folds and found one gap: the CRLF regression +test itself failed on a CRLF checkout. The suite now reads the catalog LF-normalized, as the build +step does, and the test passes against a CRLF catalog. + +A third adversarial review of the pull request (gpt-6-astra, xhigh, with three subagents) found two +more, both medium, each folded with a regression test that fails without its fix: + +| # | Finding | Fold | +| --- | --- | --- | +| 13 | Recipe 23 gave a nested entry the line of a literal top-level key its joined path spells: `shape: { detail }` beside a key `shape.detail` reported the literal key's line. | The walk carries the top-level key, and a Design or UI line is looked up by it alone; a nested entry reads `line: null`. | +| 14 | The Pack design counted every Spec that declares `verifies` as an example, because the verifier decoding labels each one `via: "example"`; a behavior Spec verifying a member read as one example. | The count reads the source's kind: only an example-kind Spec is an example. The verifier count is unchanged. | + +## Acceptance, as measured at close + +Measured on `bd1f1f8` after the fold; re-run the recipes rather than inheriting these. + +- **The gate.** `npm run check` exits 0: 1,527 pooled tests in 77 files and 93 CLI tests pass, + preflight included. +- **The graph.** 252 Specs, 3 Packs, 257 anchors, 512 nodes, 1,224 edges; validate reports 0 errors + and 7 warnings, down from 14 (two honest gaps, five prose mentions with recorded reasons). Every + Spec belongs to a Pack. All 97 open questions carry a key, 35 of them blocking. The location table + holds 155 rows. +- **The backlog and the alarm.** Recipe 1 is empty. Recipe 2 lists 23: the sixteen in the packet + below and the arc's seven implemented Specs, each waiting for the owner's statement. +- **The Pack page.** `pack:design-management-v1` and `pack:spec-studio-v1` render with each member's + stated next rung, design columns, boundary, keyed questions with line links, and code units with + role, component, layer and context, from the graph alone. +- **The first adopter.** This branch's build ran read-only over an export of libar-platform at + `d310eb1`; the platform repository was not touched. Its graph is unchanged (243 Specs, 429 nodes, + 1,499 edges). The `pack:history-view` page renders 19 members, 53 open questions with addresses + and lines, and 56 Specs the Pack rests on, the same 56 outside dependencies its hand-built page + counts. Recipe 25, run from `dist/recipes/25-address-resolution.js` with `--params @file` over the + 586 entry addresses written outside its Specs, resolves 576 and gives each its value and line; the + 10 that do not resolve come from illustrative addresses in its feedback documents. Recipe 32's + context pairs show no `uses` edge leaving the `platform` context, the result its own isolation + check reports. +- **The engine's own architecture.** Recipe 32 over this repository: 40 `uses` edges, no context + crossing, and layer pairs that run only inward (edge to application and domain, application to + domain, adapter to application). +- `sdp --version` names the build commit, and no projection or graph carries it. + +## Refused, and staying refused + +- A `designed` rung this arc, or any readiness change: ruled to derived columns. +- A component node without code: MD-38 stands; the capture holds the question. +- Lines on Primitive or Pack nodes, or an entry as an anchor target (MD-37). +- A new reader method or query verb: B2 extends the Pack context; recipes grow the surface. +- Reshaping the Spec page, index, census, Mermaid or Gherkin. +- Pass, approval, reviewer or finding records in the graph; run results as delivery facts. + +## The drift queue, for the owner + +Prepared by a read-only pass (gpt-6.1-sol) on the branch base `28dcbdd`, over the sixteen Specs +recipe 2 listed before this arc: each states `defined`, its floor holds `ready`, and a code anchor +satisfies it. Each was checked through its context, its carrier and the code its anchors bind. The +Specs this arc implements join the alarm too and are not in this packet; they wait for the owner's +reading of the arc as a whole. Re-run recipe 2 rather than trusting this list. + +**14 to state `ready` as they are · 1 to revise first · 1 to split or reference.** + +| Spec | Recommendation | Why | +| --- | --- | --- | +| `spec:carrier.inline-code-spans` | state ready | Code spans keep literal text past the HTML checks; four enabled examples bind it. | +| `spec:carrier.markdown-body-grammar` | state ready | Every section's closed grammar is realized; fourteen enabled refusal examples bind it. | +| `spec:consumers.adopter-on-ramp` | state ready | The shipped skills teach the graph homes; a document-realization unit and an enabled verifier bind it. | +| `spec:consumers.agent-surface.address-and-cycle-recipes` | state ready | Recipes 25 and 26 match their rules; probe tests cover repeats, missing entries and cycles. | +| `spec:consumers.agent-surface.register-recipes` | state ready | Recipes 20 to 24 match their rules; an enabled verifier binds them. | +| `spec:consumers.projections-model` | split or reference | Its units realize the shipped projections, but the Spec also promises views no unit builds and cites a measurement no artifact supports. | +| `spec:consumers.shipped-protocol-corpus` | state ready | The package ships the carriers and glossary; a `specs/`-only extraction yields intent alone. | +| `spec:extraction.open-section-order` | state ready | Authored order survives extraction, serialization, the reader and the Design Review. | +| `spec:extraction.pack-member-order` | state ready | The manifest's order survives to the reader and the page; an enabled example binds it. | +| `spec:extraction.regenerability` | revise first | Deterministic regeneration is realized; the two "measured evidence" thresholds have no measurement behind them. | +| `spec:model.core-model` | state ready | One Spec, independent coordinates and directly derived facts are realized by three units; plan 37's hold moved to `spec:model.enrichment-lifecycle`. | +| `spec:model.open-question-keys` | state ready | Keys, addresses and refusals match in both carriers; three enabled examples bind it. | +| `spec:validation.next-rung-floor` | state ready | The evaluator takes a target rung and failures name their targets; two enabled examples bind it. | +| `spec:validation.prose-mentions` | state ready | Mentions and addresses must resolve and unbacked pairs warn once; an enabled verifier binds it. | +| `spec:validation.typed-dependency-floor` | state ready | The four typed relations' targets must state `defined`; four enabled examples bind it. | +| `spec:validation.unbound-example-posture` | state ready | Unbound examples below `ready` stay quiet data; three enabled examples bind it. | + +### `spec:consumers.projections-model`: split or reference + +It promises disposable views derived from one graph, a reader that persists nothing, and +publication that keeps validation's exit code. Its two satisfying units, in +`src/projections/design-review.ts`, realize that. The same Spec also describes an exhaustive impact +graph, git-tag release and baseline views and roadmap views no unit builds, and a "single-digit to +about one quarter" curation figure with no comparison artifact. It has no verifier of its own. + +The change: keep the realized projection, publication and reader contract here; leave impact +behavior to `spec:consumers.impact-graph`; capture release, baseline and roadmap views as deferred +children; drop the curation figure until a reproducible comparison supports it. If the broader text +stays, the units should reference this Spec rather than satisfy it. + +### `spec:extraction.regenerability`: revise first + +It promises that the graph and every projection rebuild to identical bytes and that consumers +read the graph rather than keep another model. The satisfying unit in `src/cli/build-command.ts` +re-derives and compares; the read-only pass reproduced identical graph, contract, registrar and +projection outputs. Two rules claim measured evidence (comfortable rebuilds below about 50 Specs, a +graph database deferred until about 10k nodes) with no workload, timing or method behind them, and +the corpus is now past 250 Specs. + +The change: delete the two measured-evidence rules and keep the qualitative deferral, or replace +them with a cited, reproducible measurement kept apart from an owner-chosen re-entry threshold. +Then it can state `ready`; the regeneration contract needs no split. + +## The owner's rulings of 2026-10-12, and their fold + +The owner ruled every open item of PR 30 on a ruling sheet of 49 cards, each with the recommended +option, and added one direction: when a newer decision replaces an older one whose text holds +nothing a reader still needs, delete the older record instead of keeping it beside a `supersedes` +edge. Git keeps it. The rulings land in their graph homes; this list is lineage. + +- **Decisions.** `supersedes` replaces a whole decision; a decision that narrows one part of + another `refines` it, so the three projection-freeze carve-outs re-point and the superseded-decision + signal's rule stands. Ratify `spec:decisions.pack-design-page` (MD-39) and + `spec:decisions.checked-mentions`. Apply the delete-noise direction to MD-30, MD-34 and MD-35, + moving any part still in force into the decision that replaced it first. +- **Statements.** `ready` on the arc's seven built Specs (the Pack design pair after the two page + fixes) and on the fourteen drift-queue Specs. `spec:extraction.regenerability` loses its two + unmeasured rules, then states `ready`. `spec:consumers.projections-model` keeps its realized + contract, drops the curation figure, leaves the impact graph to its own Spec, captures roadmap + views as an `idea` child, then states `ready`. +- **Pack page.** `holds` reads `nothing blocks`. The boundary's "rested on by" counts the four + relations recipe 30 follows; inbound `verifies` and `supersedes` list apart. +- **Captures.** design maturity is derived, never stated (no rung, floor clause or design fact), + and recipe 1 shows the design columns; a component designed before code is a Spec its + `component:` anchor later satisfies, with Design keys named as the anchor's attributes; an arc + is a Pack named for its capability; example spaces decode once in the engine onto the Spec + context; a question's decider is a free corpus token and its fields ride labelled sub-items; a + rule key is a `[#key]` marker on any entry of the primary behavior owner, addressed `#rule.`; + a relation reason is an object per target; perspective is the term, declared as corpus data a + recipe takes; a context bundle is a recipe with a character budget and a fixed cut order; a + design diff is a recipe given the base graph as a parameter, comparing keyed entries only by + key; a change proposal is one JSON schema that holds only the changes whose scope moved; + architecture rules stay the corpus's, declared in a constraint Spec and walked by a recipe; + corpus rules report outside the two families and run only from a file the operator names; + run evidence joins by test file and reads CTRF. The reference projection and structural + Mermaid triggers are not met. +- **Studio.** Phase 0 is a read-only static root. It serves a person designing a capability in + the repository and a PR reviewer with a link, measured by the adopter's five reader tasks; the + read-only Studio ships in the open-source package; it stands beside the Markdown Design Review; + the build stamp lives in a file the publishing step writes; composed intent is copied or saved. +- **Gaps and vocabulary.** The claim taxonomy gains a verifier; spec sections keeps its gap; the + five prose mentions stay; raw paths in the frozen pages' link targets are a defect under the + Design Review's encoding rule; the Gherkin question form waits. The nine candidate terms, + `perspective`, `mention` and `entry address` are ratified. +- **Adopter.** Fold the edits into this PR, merge, then re-pin libar-platform once. + +### Lanes + +Each lane works in its own worktree on its own branch, on disjoint files, from the commit that +adds this section. + +| Lane | Model | Owns | +| --- | --- | --- | +| F1 captures | Opus 5.5 | The captures' and Studio Specs' answered questions and rungs. | +| F2 decisions and statements | Opus 5.5 | Decision records, the supersession re-pointing and deletions, ready statements, regenerability, the projections model split, both Pack manifests, `CONTEXT.md`, `DECISIONS.md`. | +| F3 engine | gpt-6.1-sol | The Pack page fixes, link encoding, design columns on the Spec context and recipe 1, recipe 31's narrowing decisions, honest verifiers, and the Specs that state them. | +| F4 integration | Opus 5.5 | Merges, the self-hosting oracle, test and doc fallout, the gate. | +| F5 review | gpt-6-astra | A read-only adversarial review of the fold. | + +Commit per coherent unit after its focused tests pass; the full gate before the close commit. +Never push without the owner's word. + +### The fold, as run + +| Lane | Model | Commits | What it did | +| --- | --- | --- | --- | +| F1 captures | Opus 5.5 | `2086781`, `ad98768`, `dda3432` | Answered 30 blocking questions on 18 carriers; 16 moved to `defined`. | +| F2 decisions | Opus 5.5 | `7fbd194`, `c1318fa`, `35539a0`, `074ae7f` | Narrowing by `refines`; 20 `ready` statements and two ratifications; regenerability revised; the projections model split, with `spec:consumers.projections-model.roadmap-views` captured; twelve terms ratified; MD-30, MD-34 and MD-35 deleted after two consequences moved into MD-38. | +| Corpus review | gpt-6.1-sol | `5b2d855` | Eight findings folded. Design maturity keeps readiness's ratified meaning; the planned component's own id is keyed `id`; perspective selectors, corpus-rule severity and the bundle's budget floor became blocking, so those three Specs state `scoped`. | +| F3 engine | gpt-6.1-sol | `2cad2d9` to `176c4e9` | `nothing blocks`; the boundary on four relations with `verifiedBy` and `supersededBy` apart; every Design Review link target encoded; `designColumns` on the Spec context and in recipe 1; recipe 31's `narrows` and `narrowedBy`; verifiers for the claim taxonomy, core model, regenerability and projections model. | +| F4 integration | gpt-6.1-sol | `134c184`, `4abeacb` | The oracle and test expectations re-pinned from carriers. | +| Main thread | Opus 5.5 | `8d6a6bb`, `6a74e30` | Calendar dates out of Spec prose; status lines; plan 42 drafted. | + +### Review of the fold + +gpt-6-astra (xhigh) reviewed `90c75dd..4abeacb` read-only. No blocker. Its independent probes +found no engine finding: the boundary split, shared design columns, recipe 1's counts, recipe 31's +narrowing, and link encoding of spaces, parentheses, `#`, `%` and non-ASCII paths all held. It +found no widened or graph-computed oracle pin, and nothing still in force lost from the three +deleted records. + +| # | Finding | Fold | +| --- | --- | --- | +| 1 | Plan 42's acceptance needs two associations the adopter's graph does not hold: the register id OD-157, and the test comparing a declaration's type (major). | The design pass recommends their lawful homes in the adopter, and acceptance reads the page set with the files it links to. | +| 2 | `spec:decisions.planning-truths-placement` (MD-33) still let a refusal reopen only through `supersedes` (minor). | Its decision, consequence and registry row now distinguish whole replacement from narrowing. | +| 3 | The core-model verifier never compared coordinates with the supplied envelope, and its non-propagation case bound the parent (minor). | Both outputs are asserted against the envelope, and a child-only binding leaves the parent unimplemented; four mutations go red (gpt-6.1-sol). | + +### The owner's second round + +Before the push, the owner forwarded an outside review of `92e70d7` and asked for its points to be +resolved. Fable 5.1 (high) advised on the exact wording of the three rulings against standing law. + +- **Perspective selectors.** A perspective selects by Spec id or id prefix, kind, section presence, + Design key with an optional value, question key and authored relation type with a direction, and + by layer or context only through a binding it names. Each row says whether it matched on declared + intent or anchored structure, and selected members keep their blocking questions in view. +- **Corpus-rule severity.** Under `sdp validate --rules`, an `error` corpus finding exits nonzero, + a `warning` stays informative, and a broken rule exits nonzero as a rule execution failure. The + two-family rule now names the Protocol's own validators, and architecture constraints may be + gated by the corpus's `--rules` invocation. +- **The bundle's budget floor.** The budget is a hard limit in UTF-16 code units of the compact + JSON the recipe returns. When protected content alone exceeds it, the recipe returns a + budget-exceeded result as data, with exit 0, and no partial bundle. +- **The Studio's measure.** `spec:consumers.spec-studio` states plan 42's five tasks and access + boundary; the native-backend task passes on a bound verifier with no run result supplied. +- **Run evidence.** A result joined by file is evidence from the verifier's file, never a verdict + for one verifier. +- **The HTML decision.** `spec:decisions.studio-html-surface` drops its unsupported claims, rests + on search, filters, navigation and progressive disclosure, defers richer interactions, and + states `ready`. + +### Acceptance of the fold, as measured + +Measured after the review's fold; re-run the recipes rather than inheriting these. + +- **The gate.** `npm run check` exits 0: 1,540 pooled tests in 77 files and 93 CLI tests, preflight + clean. +- **The graph.** 250 Specs, 3 Packs, 263 anchors, 516 nodes, 1,222 edges; validate reports 0 errors + and 4 warnings (the spec-sections gap the owner kept, three prose mentions). +- **The alarm and the backlog.** Recipe 2 is empty, down from 23. Recipe 1 is empty. +- **Open questions.** 58, 6 of them blocking, down from 97 and 35: three re-entry triggers + (reference projection, structural Mermaid, roadmap views) and three that predate this arc. diff --git a/plans/42-spec-studio-phase-zero.md b/plans/42-spec-studio-phase-zero.md new file mode 100644 index 00000000..5c01326b --- /dev/null +++ b/plans/42-spec-studio-phase-zero.md @@ -0,0 +1,118 @@ +# Plan 42. The Spec Studio, phase 0: a read-only static view + +> **Status:** DRAFTED on `feature/design-management-v0`, after plan 41's fold. A thin lineage +> pointer: the Studio's intent lives in `pack:spec-studio-v1`, and its readiness, open questions and +> backlog are read from the graph (recipes 1, 9, 11, 20, 29), never from this file. If this file and +> the graph disagree, the graph wins and this file is stale. + +## Why + +The owner ruled the Studio's re-entry on 2026-10-12, on the PR 30 ruling sheet: + +- **The reader.** A person in the repository who designs a capability ahead of its code, and a + pull request reviewer who has only a link. The Markdown Design Review serves neither well: its + Pack page is a 13-column table, and its pages cannot be searched or filtered. The adopter's + reviewers read carrier files instead (`libar-platform/docs/feedback/sdp-feedback-01.md` and + `-02.md`, items 36 to 40). +- **The measure.** The adopter's five reader tasks, completed from the Studio page set and the + files it links to, as in the adopter's own trial: find the + history contract, locate OD-157, explain the aggregate dependency, find one declaration's type + comparison, and say whether a paused-rebuild example has native-backend evidence. Phase 0 + reads no run evidence, so the last task passes when the reader finds that the example requires + the native backend tier and has a bound verifier, and reports that no run result is supplied. + `spec:consumers.spec-studio` states the same tasks and the same access boundary. +- **The home.** The read-only Studio ships in `@libar-dev/software-delivery-protocol`; hosted + collaboration may later live under `@libar-ai/`. +- **Its place.** It stands beside the Markdown Design Review, which stays frozen and stays the + surface agents, diffs and GitHub read. +- **Its stamp.** The page set carries no commit or time. A publishing step writes one stamp file + beside it, and the shell shows it when present. + +Plan 41 built what phase 0 renders: the Pack design on the reader's Pack context, design columns +on the Spec context, entry lines in the location table, and recipes 29 to 32. + +## What phase 0 is + +A new projection root, `sdp studio [--root] [--exclude] [--out DIR]`, writing +`generated/spec-studio/` from reader values: + +- **Index.** Every Spec and Pack, filtered by kind, altitude, stated rung, floor reached, delivery + facts and Pack, with search. +- **Pack page.** The Pack design (recipe 29's value) as member cards in authored order, each with + its stated rung, floor, what holds it below the next rung, design columns, open questions with + their addresses and lines, and bindings; then the boundary and the code that answers to the Pack. + Collapsible sections replace the Markdown page's wide table. +- **Spec page.** One Spec's sections in authored order, with an anchor per entry address + (`#design.`, `#ui.`, `#question.`), relations both ways, bindings in binding + language, and the stated rung beside the floor. +- **Search.** The build writes an index from the reader's concept fields, entry keys, pinned + declarations and file paths (adopter item 14). The page matches with the reader's own matching + code, bundled for the browser, so page search and `findByConcept` share one implementation. + +Constraints: opens from `file://` with no network request; two builds of one graph are +byte-identical; the page computes no join, claim, fact or floor; keyboard navigation, text beside +every color, readable line lengths and a phone layout (`spec:consumers.spec-studio.responsive`). +Custom elements without a framework (`spec:decisions.studio-web-components`). + +Later phases, each its own arc: the `--watch` live view and a one-file Pack export; PR previews +with the stamp file; run evidence once `spec:observation.run-evidence` is built; diagrams once +`spec:consumers.spec-studio.components#question.embeddedDiagrams` is ruled; the composing panel. + +## The design pass first + +The Studio's Specs describe the whole Studio, so phase 0's code could not satisfy them whole. An +Opus 5.5 design pass, before any code: + +1. **Split for honest binding.** Give phase 0's pages their own child Specs (the Studio's Pack page, + index and search at least), so the code satisfies whole Specs and references the parents. + Deferred panels and lenses stay in their current Specs. +2. **Settle phase 0's open questions as UI and rules.** At least `shell#searchTags`, + `shell#recentEdits`, `shell#railHarnesses`, `spec-page#headerFields`, `spec-page#rungColors`, + `spec-page#questionMetadata`, `lenses#capabilityGrouping` and `data#aiSlices`. Each answer goes to + the owner as a recommendation, as on the ruling sheet. +3. **Prototype the Pack page on real data.** One static page over recipe 29's output for + `pack:design-management-v1` and the adopter's `pack:history-view`, for the owner to react to + before the templates are fixed. +4. **The data and element contracts.** The data file shapes (`spec:consumers.spec-studio.data`), + the element names and attributes for phase 0, and the CLI verb's flags, stated in their Specs. +5. **Make the five tasks answerable from the graph.** Two associations the tasks need live outside + the adopter's graph today (gpt-6-astra, reviewing plan 41's fold). The register id OD-157 maps + to `spec:application.history-rebuild#question.verifyPassUnderPause` only in + `design/decisions/register.json`. The type-comparison suite `tests/types/pinned.test-d.ts` + checks `spec:application.history-projection#design.typeHistoryEvent` with no test anchor, so + the Spec shows no verifier. A Studio that renders graph values cannot supply either. Recommend + lawful homes the graph already reads, both adopter authoring: the register id in the + question's text, and a `specTest` anchor on the suite verifying the Spec. The adopter applies + them when it re-pins. + +The owner states `ready` on the phase 0 Specs; building starts from those statements. + +## Build lanes (after the statements) + +| Lane | Model | Builds | +| --- | --- | --- | +| S1 data and verb | gpt-6.1-sol | `sdp studio`, the data writer, determinism and `file://` tests. | +| S2 pages | gpt-6.1-sol | Page templates against the UI entries the design pass wrote. | +| S3 elements | gpt-6.1-sol | Filter, search and collapse elements; the bundled matcher. | +| Review | gpt-6-astra | An adversarial review of the branch. | +| Acceptance | gpt-6-astra | The five reader tasks over a Studio build of libar-platform, read-only. | + +Opus 5.5 reviews the pages' copy and the element APIs against the design pass during the build. + +## Acceptance criteria + +- An Astra job given only the Studio page set built from libar-platform, and the files it links to, + completes the five reader tasks once the two associations above are in the adopter's graph, and + its notes name where it got stuck. +- Two renders of one graph are byte-identical; the pages open from `file://` with no network + request; validation findings show as data on the pages that carry them. +- The Markdown Design Review, census, Mermaid and Gherkin projections are unchanged. +- `npm run check` passes. + +## Refused + +- A second graph in the page, or any join, claim, fact or floor computed in the browser. +- Writing to canonical source from the page; a pull request opened from the page. +- A commit, time or run identity inside the page set. +- A framework; a CDN or any network fetch. +- HTML replacing the Markdown Design Review, or reshaping a frozen projection. diff --git a/specs/carrier/inline-code-spans.sdp.md b/specs/carrier/inline-code-spans.sdp.md index 43d79ae9..b296e8b3 100644 --- a/specs/carrier/inline-code-spans.sdp.md +++ b/specs/carrier/inline-code-spans.sdp.md @@ -2,7 +2,7 @@ id: spec:carrier.inline-code-spans kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:carrier.markdown-body-grammar --- @@ -13,8 +13,8 @@ relations: - outcome: Let a code span carry any literal text, angle brackets included, while raw HTML outside code spans stays refused. ### Open questions -- [non-blocking] CommonMark lets a code span cross a line ending inside a paragraph, so a renderer and the Protocol can disagree about which text is inside a span. Should the owner keep the line-scoped rule or match spans across a paragraph? -- [non-blocking] A backslash-escaped backtick still opens a span, and a tag that starts before a backtick pair loses to the span. CommonMark decides both cases the other way. Should the owner keep these readings or follow CommonMark in these two cases? +- [non-blocking #spanAcrossLines] CommonMark lets a code span cross a line ending inside a paragraph, so a renderer and the Protocol can disagree about which text is inside a span. Should the owner keep the line-scoped rule or match spans across a paragraph? +- [non-blocking #commonMarkCases] A backslash-escaped backtick still opens a span, and a tag that starts before a backtick pair loses to the span. CommonMark decides both cases the other way. Should the owner keep these readings or follow CommonMark in these two cases? ## Rule - A code span is literal content. The raw-HTML refusal never reads inside one, whether the span sits in a Spec's narrative, a section description, a list entry, or a Pack's framing prose. diff --git a/specs/carrier/markdown-body-grammar.sdp.md b/specs/carrier/markdown-body-grammar.sdp.md index 0f2d9e47..008edc45 100644 --- a/specs/carrier/markdown-body-grammar.sdp.md +++ b/specs/carrier/markdown-body-grammar.sdp.md @@ -2,10 +2,11 @@ id: spec:carrier.markdown-body-grammar kind: behavior altitude: feature -readiness: defined +readiness: ready relations: refines: spec:carrier.markdown-authoring dependsOn: spec:carrier.slot-notation + constrainedBy: spec:validation.authored-honesty decidedBy: - spec:decisions.carrier-ruling - spec:decisions.prose-ownership @@ -17,7 +18,7 @@ relations: - outcome: State what each section of a Markdown Spec body accepts and refuses, so an author can write a lawful carrier from this Spec alone. ### Open questions -- [non-blocking] The Markdown carrier admits one constraint entry per Spec; the TypeScript model and carrier admit several, and the kind-evidence law reads every entry. The owner's ruling makes one entry the law across carriers. Two reviewers contest it: a `constrainedBy` edge points at one Spec and says nothing about how many entries that Spec holds, and the narrowing would force a Spec with two bounds to promote or bundle them. The revision stays unlanded until that cost is weighed. +- [non-blocking #constraintEntryCount] The Markdown carrier admits one constraint entry per Spec; the TypeScript model and carrier admit several, and the kind-evidence law reads every entry. The owner's ruling makes one entry the law across carriers. Two reviewers contest it: a `constrainedBy` edge points at one Spec and says nothing about how many entries that Spec holds, and the narrowing would force a Spec with two bounds to promote or bundle them. The revision stays unlanded until that cost is weighed. ## Behavior - rule: The body opens with one H1 title. Text between the title and the first H2 is the Spec's narrative, and narrative accepts plain paragraphs only. diff --git a/specs/carrier/markdown-parser.sdp.md b/specs/carrier/markdown-parser.sdp.md index 28ae26ca..3d617513 100644 --- a/specs/carrier/markdown-parser.sdp.md +++ b/specs/carrier/markdown-parser.sdp.md @@ -6,6 +6,7 @@ readiness: ready relations: refines: spec:carrier.markdown-authoring dependsOn: spec:carrier.envelope-contract + constrainedBy: spec:carrier.inline-code-spans --- # The product parser reifies the ruled Markdown subset diff --git a/specs/consumers/adopter-on-ramp.sdp.md b/specs/consumers/adopter-on-ramp.sdp.md index 21d1e03b..57c3429d 100644 --- a/specs/consumers/adopter-on-ramp.sdp.md +++ b/specs/consumers/adopter-on-ramp.sdp.md @@ -2,13 +2,14 @@ id: spec:consumers.adopter-on-ramp kind: behavior altitude: story -readiness: defined +readiness: ready relations: refines: spec:consumers.authoring-on-ramp dependsOn: - spec:validation.typed-dependency-floor - spec:consumers.agent-surface.register-recipes - spec:consumers.shipped-protocol-corpus + - spec:carrier.markdown-body-grammar decidedBy: - spec:decisions.planning-truths-placement - spec:decisions.decision-readiness-posture diff --git a/specs/consumers/agent-surface.address-and-cycle-recipes.sdp.md b/specs/consumers/agent-surface.address-and-cycle-recipes.sdp.md index 6a79f9b1..5a1181de 100644 --- a/specs/consumers/agent-surface.address-and-cycle-recipes.sdp.md +++ b/specs/consumers/agent-surface.address-and-cycle-recipes.sdp.md @@ -2,7 +2,7 @@ id: spec:consumers.agent-surface.address-and-cycle-recipes kind: behavior altitude: story -readiness: defined +readiness: ready relations: refines: spec:consumers.agent-surface.register-recipes dependsOn: spec:decisions.checked-mentions diff --git a/specs/consumers/agent-surface.design-recipes.sdp.md b/specs/consumers/agent-surface.design-recipes.sdp.md new file mode 100644 index 00000000..787646d5 --- /dev/null +++ b/specs/consumers/agent-surface.design-recipes.sdp.md @@ -0,0 +1,27 @@ +--- +id: spec:consumers.agent-surface.design-recipes +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.agent-surface.register-recipes + dependsOn: + - spec:consumers.pack-design + - spec:consumers.agent-surface.recipe-parameters +--- +# Design work reads its answers from catalog recipes + +## Intent +- problem: Iterating on a design asks questions no recipe answers in one body: where a Pack's design stands, what a change to a Spec asks to follow, which decisions shape what, and which component dependencies cross a context or a layer. The first adopter answers each with its own script. +- outcome: Answer each of those questions with a catalog recipe that runs as written, so design work reads them from the graph. + +### Open questions +- [non-blocking #exampleSpaceCoverage] Example-space coverage, the witnesses and coverage gaps of each literal slot (v0 04 §4), needs the slot notation parsed, and a recipe body that parses it would be a second parser; `spec:consumers.agent-surface.example-space-coverage` holds whether the reader decodes example spaces and bound points instead. + +## Behavior +- rule: The build backlog (recipe 1) keeps its existing fields and adds `designColumns` for each listed Spec, read from its Spec context: counts of keyed Design and UI entries, pinned declarations, open and blocking questions, and the decisions it names. These columns are derived, never a score, rung or approval. +- rule: Pack design (recipe 29) takes a Pack id in `params.pack` and returns the Pack context's design assembly as data: the members in authored order with their stated next rung and its unmet clauses, their design columns, bindings and verifiers, and the Pack's boundary. It returns `found: false` for an id that is no Pack. +- rule: Design-change impact (recipe 30) takes Spec ids in `params.specs` and returns every Spec that rests on them, transitively, through inbound `refines`, `dependsOn`, `constrainedBy` or `decidedBy` edges, each with the relation path that reaches it and its distance. For the changed Specs and every dependent it lists the units that satisfy it, the units that reference it, its enabled verifiers and its Packs. A `references` edge counts as a unit to notify and confers nothing. +- rule: Decision register (recipe 31) takes no parameter and returns one row per decision Spec, sorted by id: its title, stated rung, the decisions it supersedes and the decisions that supersede it, the decision-kind Specs it `refines` as `narrows` and those that `refines` it as `narrowedBy`, the Specs that name it through `decidedBy`, its keyed open questions, and its Packs. It reads the graph alone; the ratified names stay in the decision registry document. +- rule: Architecture crossings (recipe 32) takes no parameter and returns every `uses` edge whose two ends, each resolved to its own component directly or through `memberOf`, have components that both state a context and differ in it, or both state a layer and differ in it. Each row names both units, both components and what differs, and the totals tally the crossings by context pair and by layer pair, `from -> to`, so a corpus reads the direction of its dependencies at a glance. It reports and never refuses: a corpus gates its own architecture rule, as the architectural annotation decision leaves it. +- rule: Each recipe body lives in the catalog and runs as written in the recipe test, and each reads its parameter as the recipe-parameters Spec states. diff --git a/specs/consumers/agent-surface.example-space-coverage.sdp.md b/specs/consumers/agent-surface.example-space-coverage.sdp.md new file mode 100644 index 00000000..7d7c9ea9 --- /dev/null +++ b/specs/consumers/agent-surface.example-space-coverage.sdp.md @@ -0,0 +1,25 @@ +--- +id: spec:consumers.agent-surface.example-space-coverage +kind: behavior +altitude: story +readiness: defined +relations: + refines: spec:consumers.agent-surface.design-recipes + dependsOn: spec:carrier.slot-notation +--- +# Example spaces report which values their examples cover + +The original design rendered a coverage heatmap from each example space: which combinations of its typed slots an example witnesses and which none does (`docs/lineage/v0-design/04-authoring-surfaces.md` §4 and §4.1, `06-extraction-and-validation.md` §8). The Protocol ratified the terms witness and coverage gap and computes neither, so a designer cannot see, before code exists, which examples a space still lacks. + +## Intent +- problem: A parent's example space declares literal-union slots and its example children bind one point each, but no recipe or reader value says which literal values the children witness and which no child does. +- outcome: Report, for every example space, each literal slot value with the children that witness it and the values no child witnesses, so a design pass sees its missing examples before code exists. + +### Open questions +- [non-blocking #combinationCoverage] Is coverage per slot value enough, or does a design pass need the combinations of two or more slots, as the original heatmap drew them? + +## Behavior +- rule: The engine decodes each Spec's example space into its typed slots, and each example child's steps into its bound point, once, through the notation module that writes the space contracts, and the reader carries both on the Spec context; no recipe or projection parses slot notation a second time. +- rule: Coverage is a catalog recipe over the decoded values: for each slot typed as a union of literals, each value with the example children whose bound point witnesses it, and the values no child witnesses. +- rule: A slot that is not a union of literals has no finite set of values, and coverage reports it as not enumerable rather than as covered or as a coverage gap. +- rule: Coverage reports and confers nothing: it moves no readiness, records no verdict, and a coverage gap is what a design pass reads, never a validation finding. diff --git a/specs/consumers/agent-surface.recipe-parameters.sdp.md b/specs/consumers/agent-surface.recipe-parameters.sdp.md new file mode 100644 index 00000000..fcf6ff8d --- /dev/null +++ b/specs/consumers/agent-surface.recipe-parameters.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:consumers.agent-surface.recipe-parameters +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.agent-surface + decidedBy: spec:decisions.agent-front-door +--- +# Recipes take parameters as data and ship as runnable files + +## Intent +- problem: A parameterized recipe takes its input on its opening line, so a caller rewrites the body's source to pass 489 addresses, recipe 4 alone reads an environment variable, and every script that runs a recipe cuts its body out of the Markdown catalog first. +- outcome: Pass a recipe's parameter as data through the one evaluation sink, and ship every catalog body as a file a caller runs as written. + +## Behavior +- rule: `sdp q` takes `--params` with a JSON object, or `--params @PATH` with a file holding one, the path resolved from the working directory, and injects it as a fourth binding, `params`, beside `g`, `graph` and `report`. Without the flag `params` is an empty object. A value that is not valid JSON, JSON that is not an object (null, an array, a string, a number or a boolean), or a file that cannot be read, is refused before the body runs, with exit 1, one line on stderr and no output on stdout. +- rule: The three existing bindings keep their names and meaning. The sink still evaluates one operator-supplied body and adds no query vocabulary: a parameter is input to a body, never a verb. +- rule: Every recipe that takes a parameter reads it from `params` under the name the catalog states and falls back to the catalog's sample when the name is absent, so each body still runs as written. Recipe 4 reads its changed files from `params.files`, and the environment variable it used before is retired. +- rule: The build writes each catalog body to `dist/recipes/-.js`, where `NN` is the two-digit recipe number and the slug is the recipe's heading in lower kebab case, and the package ships that directory. The catalog stays the one owner of the bodies; the files are derived, and the recipe test checks that each file equals its catalog body byte for byte. +- rule: The realizing sites are `src/cli/q-command.ts` for the flag and binding, and the build step that writes `dist/recipes/`. diff --git a/specs/consumers/agent-surface.register-recipes.sdp.md b/specs/consumers/agent-surface.register-recipes.sdp.md index 4a37ba0e..fc75df8d 100644 --- a/specs/consumers/agent-surface.register-recipes.sdp.md +++ b/specs/consumers/agent-surface.register-recipes.sdp.md @@ -2,7 +2,7 @@ id: spec:consumers.agent-surface.register-recipes kind: behavior altitude: story -readiness: defined +readiness: ready relations: refines: spec:consumers.agent-surface --- diff --git a/specs/consumers/context-bundle.sdp.md b/specs/consumers/context-bundle.sdp.md new file mode 100644 index 00000000..0a775eea --- /dev/null +++ b/specs/consumers/context-bundle.sdp.md @@ -0,0 +1,35 @@ +--- +id: spec:consumers.context-bundle +kind: behavior +altitude: feature +readiness: defined +relations: + refines: spec:consumers.agent-surface + dependsOn: spec:consumers.agent-surface.design-recipes + decidedBy: spec:decisions.agent-front-door +--- +# A context bundle is composed per invocation within a token budget + +The original design cut the graph into slices for language models ahead of time. It wrote one slice per Pack, per capability, around a changed Spec, per readiness level and for failing tests, each under 32k tokens with its nodes, edges, source files, findings, open questions and a summary, and a prompt bundle for one Spec (`docs/lineage/v0-design/07-spec-studio-and-projections.md` §5.5, §13.2). The ratified vocabulary keeps the term and defines a context bundle as a token-budgeted curated slice pushed to an agent. + +Plan 35 deferred the bundle until evidence showed agent sessions still hand-assembling the same token-budgeted slice after the catalog recipes (plan 35, "H leftover projections"). The first adopter's evidence meets that trigger. Every design pass there starts from the same hand-written brief, which tells the agent to read the Pack's page, run recipes 5, 20, 21 and 24 over the Pack and its members, read the member Specs where the page falls short, and end the memo with what the page did not give (`libar-platform/design/advisors/task-pass.md`). Four advisor lenses run that brief over each Pack, and the adopter asks for the reading material to be generated from a Pack and a lens (`libar-platform/docs/feedback/sdp-feedback-02.md`, item 29). The agent front door refuses a committed artifact that answers in the graph's name, so a bundle is composed on each invocation and never saved as slices. It builds on the Pack design recipe, recipe 29. + +## Intent +- actor: An agent starting one design pass over a Pack, a Spec's neighborhood or a set of open questions. +- problem: Each pass assembles the same reading material by running the same recipes and joining their answers by hand, and nothing bounds what it hands the model. +- outcome: Give an agent the reading material for one design pass in one composed, token-budgeted slice of the graph, so no session hand-assembles it. + +### Open questions +- [non-blocking #bundleScopes] Which scopes does a bundle take beyond a Pack: a Spec with its neighborhood, the Specs a changeset touches, the open questions of a Pack? +- [non-blocking #perspectiveFilter] Does a bundle take a review perspective as a filter, so one call yields the brief for one lens? + +## Behavior +- rule: A bundle is a catalog recipe that takes its scope and its budget as recipe parameters; it adds no entrance and no query verb. +- rule: A bundle is composed from the graph on each invocation by the reader every recipe uses; nothing is precomputed, saved or committed, and two invocations over the same graph with the same parameters compose the same bundle. +- rule: A bundle for a Pack carries the Pack design assembly, its members in authored order with their design columns and its boundary, then each member's open questions with their addresses and lines, its dependency footing, its pinned declarations and its examples. +- rule: The budget is a hard limit on a successful bundle, counted in UTF-16 code units, the `length` a recipe body reads, of the compact `JSON.stringify` of the value the recipe returns, its scope, budget and left-out statement included; `sdp q --json` prints a longer indented form, and the budget never measures printed text. The bundle also reports a token estimate labelled as an estimate, so no model's tokenizer binds the Protocol. +- rule: When the scope exceeds the budget, the bundle cuts in a fixed order: boundary rows down to their ids first, then member narrative, then example step text, then non-blocking questions. It never cuts member ids, stated and next rungs, blocking questions, pinned declarations or entry addresses. +- rule: When the protected content alone exceeds the budget after every permitted cut, the recipe returns a budget-exceeded result as data and no partial bundle: the requested scope, the budget, and the minimum count the protected content needs in the same unit; `sdp q` exits 0, because the result is the body's return and not a refusal. +- rule: A bundle gives the file and line of an entry or a binding in place of a source excerpt, because the reader never reads source. +- rule: A bundle states its scope, its budget and what it left out to stay within the budget, so a reader knows what to read in the Specs themselves. +- rule: A bundle carries facts from the graph and no instructions of its own; the brief that tells an agent what to look for stays the corpus's own text. diff --git a/specs/consumers/delivery-session-on-ramp.sdp.md b/specs/consumers/delivery-session-on-ramp.sdp.md index 71321520..51d7f286 100644 --- a/specs/consumers/delivery-session-on-ramp.sdp.md +++ b/specs/consumers/delivery-session-on-ramp.sdp.md @@ -5,6 +5,7 @@ altitude: story readiness: ready relations: refines: spec:consumers.authoring-on-ramp + decidedBy: spec:decisions.planning-truths-placement --- # Delivery sessions route work from current graph state diff --git a/specs/consumers/design-diff.sdp.md b/specs/consumers/design-diff.sdp.md new file mode 100644 index 00000000..62ee4d99 --- /dev/null +++ b/specs/consumers/design-diff.sdp.md @@ -0,0 +1,32 @@ +--- +id: spec:consumers.design-diff +kind: behavior +altitude: feature +readiness: defined +relations: + refines: spec:consumers.projections-model + constrainedBy: spec:extraction.determinism +--- +# Designs are compared between two commits at entry grain + +The original design's CLI compared two graph snapshots with `graph diff ` (`docs/lineage/v0-design/08-delivery-evidence-and-tooling.md` §6.1). Today one rule of the impact graph's Spec states that comparison. It derives the graph at each commit and reports the nodes and edges added, removed or changed. That Spec is held by a blocking question on language-neutral symbol identity, which concerns exhaustive code structure and not the curated graph a design lives in, so the comparison a design review needs waits on a question it never asks. + +Iterating on a design asks what changed since the last reading. The first adopter asks for a comparison by Spec id, section and entry key that tells changed declaration text from changed prose, moved entries, readiness edits, relation edits and new or removed examples, and for what changed since a design pass, read against the commit that pass read (`libar-platform/docs/feedback/sdp-feedback-02.md`, items 30 and 31). Git shows the changed lines; this comparison shows the changed design. + +## Intent +- actor: A reviewer or an agent starting the next design pass over a Pack, holding the commit the last pass read. +- problem: A line diff of the carriers shows edits but not which Specs, entries, questions and relations changed, so each pass re-reads the whole Pack to find what moved. +- outcome: Show what changed in a design between two commits, down to the Spec, the section and the keyed entry, so a reviewer reads what moved since the last pass. + +### Open questions +- [non-blocking #comparingRuleHome] Should the comparing rule leave the impact graph's Spec once this Spec is defined, so the impact graph keeps exhaustive code structure only? +- [non-blocking #passBase] A design pass is review context, not a graph fact. Is the base commit always one the caller names, or may a corpus keep the commit each pass read in its own records for the comparison to read? + +## Behavior +- rule: A comparison is a catalog recipe that takes the base graph as a recipe parameter, the graph `sdp build` writes at the base commit, and compares it with the graph the evaluation derives at the head; it adds no verb and no binding. +- rule: The base graph is input the caller holds, derived from the repository alone at the base commit; it never answers in the graph's name, and the comparison writes nothing. +- rule: It reports the Specs added and removed, and for a Spec present at both commits the changes to its title, kind, altitude, stated readiness, relations, narrative and each section. +- rule: Inside the Design and UI sections, the open questions and any other section whose entries carry keys, it compares keyed entries by key and reports each as added, removed, changed in value or moved in order. +- rule: An unkeyed entry has no identity across two commits, neither its position nor its text, so it is compared as part of its whole section. +- rule: It reports changes to Pack membership and order, and to the code units, verifiers and delivery facts bound to each Spec, kept apart from the authored changes because they derive from anchors. +- rule: A comparison states facts about two commits and confers nothing. It grants no approval, raises no staleness flag, moves no readiness and never records that a pass happened. diff --git a/specs/consumers/design-review.pack-design-page.sdp.md b/specs/consumers/design-review.pack-design-page.sdp.md new file mode 100644 index 00000000..9d63ab0b --- /dev/null +++ b/specs/consumers/design-review.pack-design-page.sdp.md @@ -0,0 +1,26 @@ +--- +id: spec:consumers.design-review.pack-design-page +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-review + dependsOn: + - spec:consumers.pack-design + - spec:consumers.binding-language-views + decidedBy: spec:decisions.pack-design-page +--- +# A Pack page shows where the Pack's design stands + +## Intent +- outcome: Render a Pack's design assembly as one deterministic Markdown page a reviewer reads before a design pass, between iterations and before stating readiness. + +## Behavior +- rule: The page keeps its title line, framing and vocabulary references, then renders, in this order: the member table, what holds each member below its next rung, the Pack's boundary, the open questions, the code, the verifier coverage gaps, and the findings. +- rule: The member table has one row per member in authored order: the linked Spec with its position from 1 and its title, kind, altitude, stated rung, floor reached, the stated next rung with `nothing blocks` when its unmet clauses are empty and the first unmet clause otherwise, the design columns as counts (entries, declarations, questions with the blocking count), the decisions it names, and the implementation and verifier binding columns with the present and none values the binding-language rule fixes for every aggregate table, beside a design-reference column that reads present when a code unit references the member and none otherwise. Unit counts stay off the table, which offers existence and never a degree. An unresolved member keeps a row that says so, and the later sections skip it, because the graph holds none of its data. +- rule: The next-rung section lists each member whose stated next rung has unmet clauses, every clause with the targets it names and their stated rungs. A member whose floor holds its next rung is listed apart as waiting for its author's statement, which the page never presents as approval. +- rule: The boundary section lists, in order, Rests on, Rested on by, Verified by and Superseded by, from `restsOn`, `restedOnBy`, `verifiedBy` and `supersededBy`. Each empty list reads `None.`; otherwise a table gives each row with the linked Spec, its stated rung, whether it carries `implemented`, and the members it joins under each relation type. +- rule: The open-questions section groups questions by member in authored order. Each question shows its blocking flag, its entry address when it carries a key, its text, and a link to its line in the carrier file when the location table records one. +- rule: The code section lists, per member, each realizing and each referencing unit with its id, a link to its file and line, its role, and its component with that component's layer and context, under the binding-language headings the Design Review already uses. A member with no unit says so in one line. +- rule: A line link is a relative link from the page to the carrier or source file with a `#L` fragment. Every value is escaped by the Design Review's encoding rules, and the page carries no timestamp, commit or run identity, so two renders of one graph are byte-identical. +- rule: The realizing site is the Pack page renderer under `src/projections/`. diff --git a/specs/consumers/design-review.sdp.md b/specs/consumers/design-review.sdp.md index 5ad98426..632e572b 100644 --- a/specs/consumers/design-review.sdp.md +++ b/specs/consumers/design-review.sdp.md @@ -18,14 +18,14 @@ relations: - rule: A human may use the review context when stating readiness, while validators check only the structural readiness floor and never record or require review approval. - rule: The MVP view is deterministic generated Markdown with an index and pages for Specs and Packs; richer visual representations remain outside this behavior. - rule: The page set is a function of the graph alone — it carries no timestamp, no commit, and no run identity — so two renders of the same corpus are byte-identical. -- rule: Rendering encodes by Markdown syntax context: prose and table fields escape structural characters, each entry of a `design` or `ui` section renders as a list item in authored order with its key as inline code, as ruled by `spec:decisions.authored-entry-order`, fenced JSON preserves a structured value inside one entry through JSON encoding, and inline code uses a delimiter that preserves literal backticks. +- rule: Rendering encodes by Markdown syntax context: link targets percent-encode each path segment while preserving directory separators and generated line fragments, prose and table fields escape structural characters, each entry of a `design` or `ui` section renders as a list item in authored order with its key as inline code, as ruled by `spec:decisions.authored-entry-order`, fenced JSON preserves a structured value inside one entry through JSON encoding, and inline code uses a delimiter that preserves literal backticks. - rule: The realizing entrypoint is `renderDesignReview` in `src/projections/design-review.ts`, which reads the reader and returns pages; writing them is the caller's job. ## UI The generated Design Review exposes three page anatomies from the same graph. - specPage: A Spec page presents descriptors, readiness, relations, bindings, authored sections, and findings in one context. -- packPage: A Pack page presents framing, model references, and a member table in the manifest's authored order with each member's kind, altitude, readiness, and implementation and verifier bindings. +- packPage: A Pack page presents the Pack's design as its child `spec:consumers.design-review.pack-design-page` states. - indexPage: The index presents one sortable-style Markdown table for Specs and a linked bullet list for Packs, with stable links into their detail pages. ## Example space diff --git a/specs/consumers/engine-provenance.sdp.md b/specs/consumers/engine-provenance.sdp.md new file mode 100644 index 00000000..9de6b5d1 --- /dev/null +++ b/specs/consumers/engine-provenance.sdp.md @@ -0,0 +1,20 @@ +--- +id: spec:consumers.engine-provenance +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.adopter-on-ramp + constrainedBy: spec:extraction.determinism +--- +# The CLI names the engine that produced a result + +## Intent +- problem: An adopter pinned to a build of the Protocol cannot ask the CLI which build it is: `sdp --version` prints the usage text and the package version is `0.0.0` at every commit, so when a check changes with no Spec changed, nothing tells the engine moving from the corpus moving. +- outcome: Let `sdp --version` name the package version and the commit the build came from, without putting either into the graph. + +## Behavior +- rule: `sdp --version` prints one line, `sdp ()`, and exits 0. The version is the package's own version; the commit is the full hash the build was made from, or `unknown` when the build could not read one. +- rule: The build records the commit in a file beside the compiled CLI, and the CLI reads that file; it never runs git at run time. +- rule: Neither the version nor the commit enters the graph, a projection or a generated contract, so a derived artifact stays a function of the corpus alone. +- rule: The realizing sites are `src/cli/sdp.ts` and the build step that records the commit. diff --git a/specs/consumers/graph-first-planning.sdp.md b/specs/consumers/graph-first-planning.sdp.md index d480f403..3b37dfd7 100644 --- a/specs/consumers/graph-first-planning.sdp.md +++ b/specs/consumers/graph-first-planning.sdp.md @@ -2,7 +2,7 @@ id: spec:consumers.graph-first-planning kind: behavior altitude: feature -readiness: idea +readiness: scoped relations: refines: spec:consumers.delivery-session-on-ramp decidedBy: spec:decisions.planning-truths-placement @@ -13,18 +13,16 @@ relations: - outcome: Forward arc intent is captured as sub-ready Specs so planning sessions derive the backlog and readiness from the graph, with plans reduced to thin lineage pointers. -### Open questions - -- [blocking] How does an arc boundary stay legible in the graph (a Pack, a relation cluster, a naming convention) without minting a workflow gate or an authored delivery fact? Evidence note: the briefs-index register's rows landed in four homes: tradeoff refusals on `decision`-kind Specs, existing behavior guarantees on their carrying Specs, holds as blocking open questions, and a lawful non-decision in the plan record; that split is observed evidence for this question, not a ruling on arc-boundary representation. - ## Behavior - rule: `spec:consumers.delivery-session-on-ramp` owns per-session routing from graph state; this Spec owns only arc-scale commissioning — how the next arc's intent enters the corpus and is read back. - rule: Planning remains advisory reading of the graph; no recipe, preflight, or plan document authorizes, blocks, or sequences delivery work. - rule: A work-item dependency is authored only as a `dependsOn` edge between Specs whose truth genuinely needs the other to hold; independence is the absence of the edge, and no scheduling or sequencing phrase is ever authored. - rule: A decision record is a `decision`-kind Spec joined to its subject by `decidedBy`; a lawful non-decision lives as decision content or a plan record, never as a decision record, and it never mints an authored delivery fact. -- rule: A do-not-reopen row's home follows its shape: a tradeoff refusal lives on a `decision`-kind Spec, never a `constraint`, and reopens only through a later decision that `supersedes` it and passes the ADR three-part test; an existing behavior guarantee stays on its carrying Spec and reopens by revising that Spec; a hold stays a blocking open question on its Spec; a lawful non-decision stays in the plan record and mints no Spec. -- rule: A re-entry trigger is the deferred Spec's own blocking open questions, plus a `dependsOn` edge when a true precondition exists; the plan 35 deferrals name three re-entry triggers for this arc: Spec Studio, the reference projection, and the structural-edge Mermaid; no plan document re-arms deferred work. +- rule: A do-not-reopen row's home follows its shape: a tradeoff refusal lives on a `decision`-kind Spec, never a `constraint`, and reopens only through a later decision that passes the ADR three-part test and either `supersedes` it, replacing it whole, or `refines` it, narrowing it for one part; an existing behavior guarantee stays on its carrying Spec and reopens by revising that Spec; a hold stays a blocking open question on its Spec; a lawful non-decision stays in the plan record and mints no Spec. +- rule: A decision that replaces another whole keeps the older record and its `supersedes` edge only while the older text explains something a reader still needs; otherwise it carries what survives as its own consequences and the older record is deleted, with git as its history. +- rule: An arc is a Pack named for the capability it designs, never for a plan number, listing the arc's Specs in reading order; membership confers no readiness, delivery fact or gate, and the Pack outlives the arc as a review aggregate while the plan file names it as a lineage pointer. +- rule: A re-entry trigger is the deferred Spec's own blocking open questions, plus a `dependsOn` edge when a true precondition exists; the plan 35 deferrals named four re-entry triggers: Spec Studio, the reference projection, the context bundle and the structural-edge Mermaid, and each deferred Spec now carries its trigger as its own blocking question, except the context bundle, whose trigger the first adopter's design passes have met; no plan document re-arms deferred work. - rule: A deliverable with exclusive ownership across consumers has exactly one Spec identity; every consumer `dependsOn` that identity instead of restating the deliverable. - rule: Selection-pressure heuristics stay advisory, carried as behavior rules here or as recipes; they authorize, block, and sequence nothing. - rule: Session law re-measures from the graph first; a session never inherits readiness, backlog, or placement state from a prior plan, register, or summary. diff --git a/specs/consumers/impact-graph.sdp.md b/specs/consumers/impact-graph.sdp.md index fd44e070..bbbad671 100644 --- a/specs/consumers/impact-graph.sdp.md +++ b/specs/consumers/impact-graph.sdp.md @@ -13,7 +13,7 @@ relations: - outcome: Give symbol and import impact questions an exhaustive derived substrate without promoting mechanical structure into curated intent. ### Open questions -- [blocking] Which language-neutral identity and extraction boundary can support exhaustive symbol reach without freezing a single compiler's representation into the Protocol? +- [blocking #symbolIdentity] Which language-neutral identity and extraction boundary can support exhaustive symbol reach without freezing a single compiler's representation into the Protocol? ## Behavior - rule: Mechanical import and symbol structure is inferred and remains distinct from the sparse curated graph. diff --git a/specs/consumers/intent-composition.proposal.sdp.md b/specs/consumers/intent-composition.proposal.sdp.md new file mode 100644 index 00000000..32b6b061 --- /dev/null +++ b/specs/consumers/intent-composition.proposal.sdp.md @@ -0,0 +1,35 @@ +--- +id: spec:consumers.intent-composition.proposal +kind: contract +altitude: story +readiness: defined +relations: + refines: spec:consumers.intent-composition + dependsOn: + - spec:consumers.edit-model + - spec:consumers.agent-surface.address-and-cycle-recipes + - spec:consumers.design-diff +--- +# A change proposal is addressed, based and checked before it lands + +The original design's patch loop made every change from a view reviewable and checked before it touched source. A patch named its target, the graph hash it was made against, a rationale, its operations and who proposed it; a CLI checked its schema and its base, applied it to a copy of the graph, ran the validators and only then rewrote the Spec files (`docs/lineage/v0-design/02-system-architecture.md` §2; `07-spec-studio-and-projections.md` §6). The Protocol refused the patch and the codemod. A view composes scoped intent, an agent edits source as a person would, git records the edit and the same checks evaluate it. That edit model drops two things the patch loop gave: a proposed change a person can review before an agent applies it, and a guard against a proposal made against an older design. + +The first adopter's agents already write proposals in that shape. A design pass returns findings that each name an entry address or a Spec section, state the gap, and propose exact text for a Spec with the entry it replaces or follows (`libar-platform/design/advisors/task-pass.md`), and its register tool applies a JSON patch only after checking all of it (`libar-platform/design/advisors/register.md`). This Spec states the reviewable half of the patch loop without the codemod. A proposal is a request that an agent applies as an ordinary edit, checked by `sdp validate` on a worktree before it merges. It also gives the Spec Studio's composing panel a shape to hand off. + +## Intent +- problem: A proposed design change travels as free prose, so a reviewer cannot tell what it touches, which version of the design it read, or whether it still applies once the design has moved. +- outcome: Give a proposed design change a reviewable shape: what it touches by Spec id or entry address, the commit it was read at, the change requested and why, checked on a worktree before merge. + +### Open questions +- [non-blocking #proposer] Does a proposal name who proposed it, as the original's `proposedBy` did, or do git authorship and co-author trailers stand in once it is applied? + +## Contract +- A proposal is one JSON document on one schema, which a recipe takes as a parameter; a Markdown memo of the proposal is a rendering of that document and never a second carrier. +- A proposal names its scope as one or more Spec ids or entry addresses, each resolved before anything is applied: a Spec id through the reader's Spec context, an entry address through address resolution. +- A proposal names the commit whose graph it was read against. +- A proposal states the requested change in words an agent can apply, such as the exact text for an entry with the address it replaces or follows, a new open question with its flag, or a relation to declare or remove. +- A proposal states its reason in one line. +- Before a proposal is applied, the design diff between its base commit and the head is read entry by entry. A change whose scope moved since the base goes to a person instead of being applied, every other change applies, and one moved entry never holds back the whole proposal. +- Applying a proposal is an ordinary source edit by an agent or a person; no tool rewrites a carrier from it, and nothing is applied to a copy of the graph first. +- An applied proposal lands only from a worktree on which `sdp validate` reports no error, and the conformance and honesty checks stay the only gate. +- A proposal carries no status, approval or verdict, and the graph records none; whether it was accepted is git history and the adopter's own record. diff --git a/specs/consumers/pack-design.sdp.md b/specs/consumers/pack-design.sdp.md new file mode 100644 index 00000000..aa22201e --- /dev/null +++ b/specs/consumers/pack-design.sdp.md @@ -0,0 +1,31 @@ +--- +id: spec:consumers.pack-design +kind: behavior +altitude: feature +readiness: ready +relations: + refines: spec:consumers.reader + dependsOn: + - spec:validation.next-rung-floor + - spec:extraction.pack-member-order + - spec:extraction.entry-locations + - spec:extraction.contract-declarations +--- +# The Pack context carries the Pack's design + +A capability designed ahead of its code is read as a Pack: its members in reading order, how far each has matured, what design each carries, what it rests on, and which code answers to it. The reader already holds every one of those facts. This Spec assembles them once on the Pack context so a page, a recipe and an adopter's script read one answer. + +## Intent +- problem: Reading a Pack's design means joining member readiness, the floor one rung above the stated rung, design entries, decisions, open questions, code bindings with their components, and the Specs outside the Pack, and every consumer that joins them by hand reads the floor or the boundary a different way. +- outcome: Give each Pack member its design columns and give the Pack its boundary on the existing Pack context, so the Design Review, a recipe and an adopter's page read the design from one assembly. + +## Behavior +- rule: The Spec context keeps every field it has and adds `designColumns`, derived counts and lists computed by one helper. The Pack context reads the same columns as each resolved member's `design` and adds the boundary to the Pack. It is an extension of the existing per-Pack context, not a new reader method. +- rule: A member's stated next rung is the rung above its stated rung, none when it states `ready`. Its unmet clauses come from the one floor evaluator with that rung as the target, each failure with its targets when it has them. The list is empty when the floor already holds the stated next rung, which the page reads as `nothing blocks`: a rung that waits for its author, never a promotion. The floor reached and the context's existing next-rung failures, which read the rung above the floor, stay as they are, so the two readings never share a name. +- rule: A member's design columns count its keyed `design` and `ui` entries, other than `description`; its pinned declarations, the `design` entries whose value opens with a code span under the rule of the contract-declarations Spec; its open questions and how many of them block, listing each question with its text, its blocking flag, its key when it carries one, and its source line from the location table; and they list the decisions it names through `decidedBy`, each with its stated rung, or unresolved when the target is no Spec. The columns are counts and lists read from the graph, never a score or a rung. +- rule: A member lists the code units that satisfy it and the code units that reference it, each as a code unit binding. A code unit binding carries its component when the unit has a `memberOf` edge: the component's id with its layer and context when the component anchor states them. The component rides the binding on the Spec context as well, as an addition. +- rule: A member counts its verifiers and its enabled verifiers, and the examples that declare `verifies` on it with how many of them are enabled, by the reader's existing verifier decoding. +- rule: The Pack's boundary lists the Specs outside the Pack joined to a member by an authored relation. A Spec a member relates to by `refines`, `dependsOn`, `constrainedBy` or `decidedBy` is one the Pack rests on; a Spec that relates to a member by the same four relations is one that rests on the Pack. The boundary lists inbound `verifies` separately as `verifiedBy` and inbound `supersedes` as `supersededBy`, with the same row shape. Each row carries the outside Spec's id, title, stated rung and whether it carries `implemented`, and for each relation type the members it joins, the types in the closed relation list's order and the members in authored order. A relation whose target is not a Spec in the graph is left to referential integrity and does not appear. +- rule: Members keep the manifest's authored order, an unresolved member keeps its existing row with no design columns, and boundary rows sort by id in code-unit order. +- rule: The assembly is a pure function of the graph: it reads no carrier file, records nothing, and confers no delivery fact or readiness. +- rule: The realizing site is `packContext` in `src/reader/reader.ts`. diff --git a/specs/consumers/projections-model.roadmap-views.sdp.md b/specs/consumers/projections-model.roadmap-views.sdp.md new file mode 100644 index 00000000..e92ed70e --- /dev/null +++ b/specs/consumers/projections-model.roadmap-views.sdp.md @@ -0,0 +1,25 @@ +--- +id: spec:consumers.projections-model.roadmap-views +kind: model +altitude: story +readiness: idea +relations: + refines: spec:consumers.projections-model +--- +# Roadmap views read releases, baselines and phases from git and the graph + +The projections model once defined release, baseline and roadmap vocabulary beside the projections the Protocol builds, though no unit builds a view for them. The owner ruled that the realized contract stays in the projections model and this vocabulary waits here, deferred, until a reader needs it. + +## Intent + +- outcome: Name the release, baseline and roadmap views a delivery reader may want, as optional projections that never gate or sequence work. + +### Open questions + +- [blocking #namedRoadmapReader] Re-entry trigger: a named reader who needs release, baseline or roadmap views that the shipped projections, the recipes and git do not serve, with a way to measure that need inside the arc that builds them. + +## Model + +- **release** — A tagged set surfaced as a git-tag projection. +- **baseline** — A named approved snapshot whose signed git tag is the approval artifact, with approval remaining outside the authored model. +- **phase / iteration / milestone** — Descriptive vocabulary for optional roadmap projections, never gates or enforced sequences. diff --git a/specs/consumers/projections-model.sdp.md b/specs/consumers/projections-model.sdp.md index a4eb6472..2c8edfab 100644 --- a/specs/consumers/projections-model.sdp.md +++ b/specs/consumers/projections-model.sdp.md @@ -2,7 +2,7 @@ id: spec:consumers.projections-model kind: model altitude: feature -readiness: defined +readiness: ready relations: refines: spec:protocol.self-hosting decidedBy: spec:decisions.mcp-deferred @@ -13,17 +13,12 @@ relations: - outcome: Give agents and humans consumer-specific views while preserving the repository as the only canonical source. ### Open questions -- [non-blocking] Does the pure-projection binding-language law stated in src/projections/design-review.ts commentary promote here or to a story-altitude child under comment promotion? +- [non-blocking #bindingLanguagePromotion] Does the pure-projection binding-language law stated in src/projections/design-review.ts commentary promote here or to a story-altitude child under comment promotion? ## Model - **projection** — A pure, disposable, regenerable function of the graph that produces a consumer artifact without becoming a second source of truth. - **diagnostic publication posture** — After extraction succeeds, a projection publishes its honestly labelled graph view even when validation reports errors, and returns the validation exit code so findings remain both visible and nonzero. - **curated graph** — The authored architectural read model of declared intent and anchored bindings, valued for editorial sparsity. -- **impact graph** — A separately derived code-structure surface for exhaustive usage and blast-radius questions, valued for exhaustiveness and never promoted into architecture. - **reader** — The thin typed front door that decodes graph joins and taxonomy once, returns composable data, and persists nothing. -- **curation** — The deliberate difference between the sparse curated graph and the code-structure surface; it is not drift. -- **measured curation** — In a measured comparison, the curated graph selected from single-digit to about one quarter of the mechanical impact-graph surface. +- **curation** — The deliberate difference between the sparse curated graph and the exhaustive code-structure surface `spec:consumers.impact-graph` designs; it is not drift. - **discipline** — A lens or projection that filters or groups Specs by kind or section; it is not a phase to pass through. -- **release** — A tagged set surfaced as a git-tag projection. -- **baseline** — A named approved snapshot whose signed git tag is the approval artifact, with approval remaining outside the authored model. -- **phase / iteration / milestone** — Descriptive vocabulary for optional roadmap projections, never gates or enforced sequences. diff --git a/specs/consumers/reference-projection.sdp.md b/specs/consumers/reference-projection.sdp.md new file mode 100644 index 00000000..bf2bdb5b --- /dev/null +++ b/specs/consumers/reference-projection.sdp.md @@ -0,0 +1,20 @@ +--- +id: spec:consumers.reference-projection +kind: behavior +altitude: feature +readiness: idea +relations: + refines: spec:consumers.projections-model +--- +# A reference projection serves a reader outside the repository + +The surfaces taxonomy names a reference projection, an interface and API reference kept current from the graph (`docs/concept/06-consumers-and-projections.md`, "The surfaces & projections taxonomy"). The original design generated its nearest relative, OpenAPI and AsyncAPI documents built from route and schema nodes and linked back to their Specs (`docs/lineage/v0-design/07-spec-studio-and-projections.md` §5.2). Plan 35 deferred it, because no candidate named a reader that the four shipped roots, the Design Review, the census, Mermaid and the Gherkin-shaped view, fail to serve (plan 35, "H leftover projections"). The trigger lived in plan prose; this Spec carries it as its own blocking question, the home the planning-truths placement decision gives a re-entry trigger. + +## Intent +- outcome: Publish a current interface and API reference from the graph for a reader outside the repository, once such a reader is named. + +### Open questions +- [blocking #namedOutsideReader] Re-entry trigger from plan 35: a named human consumer outside the repository whom the four shipped roots do not serve, with a way to measure that demand inside the arc that builds it. The taxonomy row is not that consumer. +- [non-blocking #referenceSource] Would the reference read pinned declarations, contract-kind Specs or `api:` code anchors, and does any of them hold enough to document an interface? + +## Behavior diff --git a/specs/consumers/review-perspectives.sdp.md b/specs/consumers/review-perspectives.sdp.md new file mode 100644 index 00000000..379ba7fc --- /dev/null +++ b/specs/consumers/review-perspectives.sdp.md @@ -0,0 +1,29 @@ +--- +id: spec:consumers.review-perspectives +kind: behavior +altitude: feature +readiness: defined +relations: + refines: spec:consumers.projections-model + dependsOn: spec:consumers.pack-design +--- +# A Pack is read from a named perspective + +The original design gave every reader one reading surface, each reading from their own concern: the analyst, the domain engineer, the architect, the tester, the designer and the agent (`docs/lineage/v0-design/04-authoring-surfaces.md` §6). The first adopter iterates each capability's design through four advisor lenses, its runtime platform, the domain, operations and the product, each a written list of the questions its field asks, run over the same Pack by the same pass brief (`libar-platform/design/advisors/lenses/`, `libar-platform/design/advisors/task-pass.md`). It asks for a lens to be a filter over the same capability that reorders and emphasizes questions, entries, examples and constraints and opens the same Specs, with no new Spec kind and no compulsory sequence of stages (`libar-platform/docs/feedback/sdp-feedback-02.md`, item 28). + +The owner ruled the word: perspective. The Spec Studio keeps lens for its graph views, its Packs, architecture, tests and evidence views, and discipline stays the filter by kind or section. The adopter's advisor lens is a reader's concern, not a view of the graph, so in the Protocol's terms it is a perspective. + +## Intent +- actor: A reviewer or an agent who reads a Pack for one concern, such as the domain model, operations or the runtime platform. +- problem: Every reading of a Pack shows every member in the same order with the same emphasis, so a reader with one concern sorts it by hand, and an adopter keeps its perspectives as prose briefs the graph cannot apply. +- outcome: Let a reviewer read the same Pack from one perspective at a time, filtered and ordered for that perspective, without a new kind or a stage. + +## Behavior +- rule: A perspective filters and orders a Pack's members and their content and never changes them; every row opens the same Spec, and the whole Pack stays one step away. +- rule: A perspective adds no kind, readiness, stage or sequence; reading a Pack from one perspective records nothing, and no perspective is required before another or before `ready` is stated. +- rule: The corpus owns its perspectives and their questions; the Protocol supplies the filter and ships no list of perspectives. +- rule: A perspective is corpus data: a JSON object the corpus keeps beside its briefs and passes to the perspective filter, a catalog recipe, through `--params`. The reader loads no perspective file, and no validator lists or checks perspectives, so no registry forms. +- rule: A perspective selects members by Spec id or id prefix, kind, section presence, Design key with an optional value, question key, and authored relation type with a stated direction; it selects component layer or context only through the member's code unit bindings, satisfying or referencing, that the perspective names; it infers nothing from prose, paths or imports. +- rule: A member selected by a Design key, including the `layer` and `context` keys a component design states, is selected on declared intent; a member selected through a binding's component is selected on anchored structure; each row names which, and the two are never merged into one architecture answer. +- rule: Values within one selector are alternatives and different selectors combine as requirements; an unsupported selector is a diagnostic in the result, and a valid selector that matches nothing is an explicit empty result. +- rule: Members keep the manifest's authored order unless the perspective gives an explicit priority order, authored order breaking ties; every selected member's blocking questions stay visible whatever else the perspective filters out. diff --git a/specs/consumers/shipped-protocol-corpus.sdp.md b/specs/consumers/shipped-protocol-corpus.sdp.md index 3c0dcfb2..2e2345e1 100644 --- a/specs/consumers/shipped-protocol-corpus.sdp.md +++ b/specs/consumers/shipped-protocol-corpus.sdp.md @@ -2,7 +2,7 @@ id: spec:consumers.shipped-protocol-corpus kind: behavior altitude: story -readiness: defined +readiness: ready relations: refines: spec:consumers.authoring-on-ramp --- diff --git a/specs/consumers/spec-studio.intent-panel.sdp.md b/specs/consumers/spec-studio.intent-panel.sdp.md index 7ed19376..332653e9 100644 --- a/specs/consumers/spec-studio.intent-panel.sdp.md +++ b/specs/consumers/spec-studio.intent-panel.sdp.md @@ -2,31 +2,30 @@ id: spec:consumers.spec-studio.intent-panel kind: behavior altitude: story -readiness: scoped +readiness: defined relations: refines: spec:consumers.intent-composition dependsOn: - spec:consumers.edit-model - spec:model.open-question-keys + - spec:consumers.intent-composition.proposal --- # A composing panel on every page gathers scoped intent for an agent ## Intent - outcome: Let a reader gather the changes they want while they read, each bounded by a Spec, its neighbors, a Pack or one open question, and hand them to an agent that edits source. -### Open questions -- [blocking #handOff] How does a static page hand composed intent to an agent: as a file the reader saves, as text the reader copies, or as a pull request? The original exports patch JSON or opens a pull request, and `spec:consumers.intent-composition` names no entrypoint for the composing surface. -- [non-blocking #patchLoop] The original stages each change as a JSON patch, checks its schema in the browser and its effect on the graph through the CLI or an agent, and applies it to canonical source; `spec:consumers.edit-model` has a view compose scoped intent that an agent turns into an ordinary source edit, with no patch loop. Does any part of the patch loop return, or is scoped intent the panel's only output? - ## Behavior - rule: The composing panel is visible on every page. - rule: The panel writes nothing: source changes only when an agent edits it and git records the edit, and the conformance and honesty checks judge that edit as they judge every edit. - rule: A reader gathers intents across pages before handing them off. - rule: Gathered intents persist in the browser's local storage, so refreshing a tab on a phone loses none. - rule: A composed intent names its scope by id or entry address, so the agent that receives it reads the same Spec, Pack, entry or question through the graph. +- rule: The panel hands its gathered intents off as one change proposal, on the change-proposal contract, which the reader copies as text or saves as a document. The page opens no pull request, because a static page holds no credentials; any pull request comes from the agent's ordinary source edit. +- rule: No part of the original's patch loop returns to the page: it stages no patch, applies nothing to source or to a copy of the graph, and the proposal is the reviewable half of that loop, applied as an ordinary edit. ## UI - gatheredIntents: the intents gathered so far, headed with their count, each on one line naming its scope and the change it asks for, such as adding an example or changing a constraint's target. -- panelActions: discard, hand off, and open as a pull request. +- panelActions: discard, copy the proposal as text, and save the proposal as a document. - composingActions: the actions on other panels that compose intent: resolve an open question or promote it to a decision record, propose or edit an example, propose or adjust a constraint target, and propose examples for missing combinations. ## Example space diff --git a/specs/consumers/spec-studio.lenses.sdp.md b/specs/consumers/spec-studio.lenses.sdp.md index 49db5488..faeaa6b7 100644 --- a/specs/consumers/spec-studio.lenses.sdp.md +++ b/specs/consumers/spec-studio.lenses.sdp.md @@ -11,7 +11,7 @@ relations: - spec:observation.runtime-overlay decidedBy: - spec:decisions.binding-not-liveness - - spec:decisions.architectural-significance-rides-primitives + - spec:decisions.architectural-annotation --- # Four lenses read the graph as Packs, architecture, tests and evidence @@ -19,7 +19,7 @@ relations: - outcome: Let a reader see the graph four ways, by Pack, by architecture, by verification and by evidence, from one top navigation. ### Open questions - [non-blocking #capabilityGrouping] The original's Packs lens groups Specs by Pack or by capability; a capability is a projection over high-altitude behavior Specs or a Pack grouping, no Capability Map projection exists, and grouping by architectural significance is derived from id families and the component graph. Which grouping does the capability tree read? -- [non-blocking #architectureVocabulary] The original's architecture lens draws runtime layers, ports and external systems beside components and their dependencies; `spec:decisions.architectural-significance-rides-primitives` admits no structural vocabulary beyond component membership and `uses`. Does the lens draw only what those edges hold? +- [non-blocking #architectureVocabulary] The original's architecture lens draws runtime layers, ports and external systems beside components and their dependencies; `spec:decisions.architectural-annotation` gives a component anchor a layer and a bounded context and any code anchor a role, beside component membership and `uses`, and admits no port or external-system node. Does the lens group components by layer and context and draw only what those anchors hold? - [non-blocking #testRunResults] The original's tests lens shows each test's last run result; a verifier binding states that a verifier exists, and pass and fail stay in CI, as `spec:decisions.binding-not-liveness` rules. Does the lens show run results read from CI output, or bindings only? - [non-blocking #ruleCoverage] The original's tests lens names the rules of a Spec that no test exercises; an example verifies a whole Spec and a rule entry has no address, so the graph cannot tell which rule an example exercises. Does a rule gain an address, or does the lens name the uncovered Specs only? diff --git a/specs/consumers/spec-studio.sdp.md b/specs/consumers/spec-studio.sdp.md index e77adb1f..f9ad5220 100644 --- a/specs/consumers/spec-studio.sdp.md +++ b/specs/consumers/spec-studio.sdp.md @@ -2,7 +2,7 @@ id: spec:consumers.spec-studio kind: behavior altitude: feature -readiness: scoped +readiness: defined relations: refines: spec:consumers.projections-model dependsOn: @@ -19,11 +19,11 @@ relations: ## Intent - actor: A stakeholder who reviews a design, on a desktop or on a phone, and reads far more than they change. - outcome: Give a person reviewing a design one generated HTML workbench that explores the whole graph through lenses, shows each Spec and Pack in context, and turns a wanted change into scoped intent rather than an edit. -### Open questions -- [blocking #packageHome] Which package carries the Studio: the open-source `@libar-dev/software-delivery-protocol`, or a commercial `@libar-ai/` package? The Studio's deferral re-enters on a recorded ruling that names this home; earlier planning placed the Studio under `@libar-ai/`, and no Spec rules it. -- [blocking #unservedReader] Which reader does the Studio serve that the generated Markdown Design Review does not? The first adopter generates its own Pack page beside the Design Review, with the members in reading order, the clause that holds each below its next rung, the Specs outside the Pack they rest on, and their open questions with register rows, while the Design Review's Pack page lists the members and the verifier gaps. ## Behavior +- rule: The Studio serves a person working in the repository who designs a capability ahead of its code and iterates on it in passes, and a reviewer of a pull request who holds only a link. The Markdown Design Review serves neither well: its pages have no search, no filters and no navigation beyond links, and its readers in the first adopter open the carrier files instead. +- rule: The Studio's first build is accepted when a reader completes, from the Studio's page set and the files it links to, the five reader tasks the first adopter set over its history-view Pack (`libar-platform/docs/feedback/sdp-feedback-01.md`): find the history contract, locate OD-157, explain the aggregate dependency, find one declaration's type comparison, and say whether a paused-rebuild example has native-backend evidence. The first build reads no run evidence, so the last task passes when the reader finds that the example requires the native backend tier and has a bound verifier, and reports that no run result is supplied. +- rule: The read-only Studio ships in the open-source package `@libar-dev/software-delivery-protocol`, beside the four shipped projections; hosted review, shared comments and hosted previews may later ship under `@libar-ai/`. - rule: The Studio is a projection of the one graph: its page set under `generated/spec-studio/` is regenerated from the graph, states no truth, and confers nothing back into the graph. - rule: The Studio is a mostly static single-page application, buildable as an `index.html` page and its assets, and it needs no server. - rule: The Studio works offline: the same files open from the file system through `file://` for local use and from any static host for sharing. diff --git a/specs/consumers/spec-studio.sharing.sdp.md b/specs/consumers/spec-studio.sharing.sdp.md index 66e4cb8f..ee156c49 100644 --- a/specs/consumers/spec-studio.sharing.sdp.md +++ b/specs/consumers/spec-studio.sharing.sdp.md @@ -2,24 +2,24 @@ id: spec:consumers.spec-studio.sharing kind: behavior altitude: story -readiness: scoped +readiness: defined relations: refines: spec:consumers.spec-studio - dependsOn: spec:extraction.schema-versioning + dependsOn: + - spec:extraction.schema-versioning + - spec:consumers.engine-provenance constrainedBy: spec:extraction.determinism --- # A Studio build is stamped and shared from a static host ## Intent - outcome: Let a stakeholder open the Studio of a pull request from a link, and let its author know which graph and which build the stakeholder read. -### Open questions -- [blocking #buildStamp] The original stamps the Studio's data with the git commit and the build time; the Studio renders byte-identical files from one graph, and `spec:extraction.determinism` keeps wall-clock timestamps and run-specific values out of generated output. Where does the stamp live, if anywhere? - ## Behavior - rule: A build is shared by uploading its page set to a static host, such as S3, Vercel or GitHub Pages. - rule: Each pull request can publish its Studio as a preview. - rule: A stakeholder opens the link, navigates the Studio and composes intent, and the pull request's author hands that intent to an agent that edits the source. +- rule: The Studio's build writes no stamp, so every page and data file it writes stays a function of the graph alone. The publishing step that uploads a build writes one stamp file beside the page set, and the shell shows it as build context when it is present. ## Design -- dataStamp: each data file carries the graph's `schemaVersion`, the git commit SHA it was built from or `unknown` for a local build, and the build timestamp. +- buildStamp: `stamp.json` beside the page set, written by the publishing step and never by the Studio's build, holding the graph's `schemaVersion`, the git commit the build came from or `unknown` for a local build, the engine `sdp --version` names, and the build time. - previewPath: a pull request's preview is served at `/preview//`. diff --git a/specs/consumers/spec-studio.spec-page.sdp.md b/specs/consumers/spec-studio.spec-page.sdp.md index 74837e6a..c8321b3c 100644 --- a/specs/consumers/spec-studio.spec-page.sdp.md +++ b/specs/consumers/spec-studio.spec-page.sdp.md @@ -20,7 +20,7 @@ relations: - [non-blocking #headerFields] The original header shows an owner, a capability, tags and a last-verified time; the envelope is closed to `id`, `kind`, `altitude`, `readiness` and `relations` with the H1 as title, a capability is a projection or a Pack, and a verifier binding never records when a test last ran. Which of the four does the header show, and from which graph value? - [non-blocking #rungColors] The original pins one color for each of seven rungs: light grey for `sketch`, blue for `framed`, cyan for `specified`, green for `designed`, emerald for `bound`, indigo for `executable` and violet for `verified`; readiness has four rungs, idea through ready, and the floor reached renders beside the stated rung. Does the palette color the stated rung, the floor reached, or both? - [non-blocking #questionMetadata] The original shows each open question's owner and the date it was added; an open question carries its text, whether it blocks and an optional key, and git records when its line was added. Does the panel show an owner or a date, and from where? -- [non-blocking #runtimeComposition] The original's runtime panel binds a Fastify route to the Effect program it invokes, as `api:POST:/orders` invokes an Effect program with `R = CreateOrderUseCase`; shows the Effect layer `layer:CreateOrderUseCaseLive`, which provides `port:CreateOrderUseCase`, requires `port:OrderRepository` and `port:EventBus`, has the lifetime `scoped` and the test layer `layer:CreateOrderUseCaseTest`; names the external system `external:postgres`; and, for Awilix, binds the registration `createOrderUseCase` to `impl:CreateOrderUseCase` as `asClass CreateOrderUseCase` with the lifetime `SCOPED`, depending on `impl:OrderRepository` and `impl:EventBus`. An anchor carries identity only, and `component` and `uses` are its only structural fields. Does the panel show more than the structural neighborhood of the Spec's implementation anchors? +- [non-blocking #runtimeComposition] The original's runtime panel binds a Fastify route to the Effect program it invokes, as `api:POST:/orders` invokes an Effect program with `R = CreateOrderUseCase`; shows the Effect layer `layer:CreateOrderUseCaseLive`, which provides `port:CreateOrderUseCase`, requires `port:OrderRepository` and `port:EventBus`, has the lifetime `scoped` and the test layer `layer:CreateOrderUseCaseTest`; names the external system `external:postgres`; and, for Awilix, binds the registration `createOrderUseCase` to `impl:CreateOrderUseCase` as `asClass CreateOrderUseCase` with the lifetime `SCOPED`, depending on `impl:OrderRepository` and `impl:EventBus`. An anchor carries identity, its Spec targets and optional structure: `component`, `uses` and `role`, with an architectural `layer` and a `context` on a component anchor; none of them records what a program provides or requires, a lifetime or an external system. Does the panel show more than the structural neighborhood of the Spec's implementation anchors? - [non-blocking #schemaBindings] The original's bindings panel lists schema bindings beside code and test bindings; anchors bind implementation code, tests and oracles, and no anchor binds a schema. Does a schema binding need a home? - [non-blocking #uiStories] The original's page outline names a section `UI / Stories`, whose UI half the UI panel carries; the original never says what a story is. What is a story, and does the page show one? diff --git a/specs/consumers/structural-mermaid.sdp.md b/specs/consumers/structural-mermaid.sdp.md new file mode 100644 index 00000000..b292108e --- /dev/null +++ b/specs/consumers/structural-mermaid.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:consumers.structural-mermaid +kind: behavior +altitude: feature +readiness: idea +relations: + refines: spec:consumers.projections-model + decidedBy: spec:decisions.shipped-projections-frozen +--- +# Mermaid diagrams can be rooted at code units + +The shipped Mermaid projection draws bounded one-hop diagrams rooted at a Spec or a Pack and never the whole graph. Code units, their component membership and their `uses` edges appear in the census tables and in recipes 12 to 14 and 17, never in a diagram. The original design drew container and component maps from its architecture model (`docs/lineage/v0-design/07-spec-studio-and-projections.md` §8.2, §8.3). Plan 35 deferred a Mermaid rendering of structural edges, because no reader needed diagrams rooted at code units that the census and the Spec-rooted diagrams fail to serve (plan 35, "H leftover projections"). Components have since gained a layer and a context, so such a diagram would have more to draw. The shipped projections are frozen, so this view would be a new root or would need a decision that narrows the freeze. If the Spec Studio proceeds, its architecture lens is the first reader that wants code units drawn, and this Spec is expected to fold into that lens rather than become a Mermaid root. + +## Intent +- outcome: Draw component, membership and uses diagrams rooted at code units, once a reader needs them and the existing views do not serve it. + +### Open questions +- [blocking #namedStructuralReader] Re-entry trigger from plan 35: a named reader who needs diagrams rooted at code units, which census tables and Spec-rooted one-hop Mermaid do not serve, plus a recorded decision to extend or abandon the one-generated-view posture. +- [non-blocking #layerAndContext] Would a component diagram group its nodes by context and order them by layer, and would that make it the architecture view the Studio's architecture lens draws? + +## Behavior diff --git a/specs/consumers/validate-watch.sdp.md b/specs/consumers/validate-watch.sdp.md new file mode 100644 index 00000000..a003e4a7 --- /dev/null +++ b/specs/consumers/validate-watch.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:consumers.validate-watch +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.authoring-on-ramp + dependsOn: spec:decisions.one-validation-path +--- +# Validation re-runs while an author edits carriers + +## Intent +- problem: An author editing a Spec re-runs `sdp validate` by hand after every save, and the watch loop that removes that step shipped in plan 35 with no Spec stating what it promises. +- outcome: Let `sdp validate --watch` re-run the one validation path on every carrier change and stay alive after findings, as a loop for authoring and never a second validation path. + +## Behavior +- rule: `sdp validate --watch [root]` installs its watcher first, then runs the same validate path `sdp validate` runs, from scratch, once at start and again after every carrier create, change, delete or rename under the root. +- rule: A carrier is a file ending in `.sdp.md`, `.sdp.gherkin` or `.sdp.ts`. The watcher ignores `generated`, `dist`, `node_modules`, `coverage`, dot-directories, the configured `--exclude` prefixes and every non-carrier path; a removed watched directory and an event with no filename each schedule a run, because the carriers they stand for cannot be named. +- rule: Events that arrive while a run is in progress coalesce into one pending run. +- rule: Findings print as `sdp validate` prints them and the process stays alive; an operator stop exits 0. +- rule: `--watch` is a validate option only and cannot combine with `--check-clean`. It re-runs on carrier edits and never on source edits, so an author re-runs validation after editing an anchor. +- rule: The realizing site is `runValidateWatch` in `src/cli/validate-watch.ts`. diff --git a/specs/decisions/anchor-comment-form.sdp.md b/specs/decisions/anchor-comment-form.sdp.md index 322c1755..b83ebbb8 100644 --- a/specs/decisions/anchor-comment-form.sdp.md +++ b/specs/decisions/anchor-comment-form.sdp.md @@ -5,7 +5,6 @@ altitude: feature readiness: ready relations: refines: spec:model.anchors - supersedes: spec:decisions.jsdoc-graph-extraction-refused dependsOn: spec:decisions.one-validation-path --- # The comment form is a second anchor representation @@ -14,10 +13,10 @@ relations: - outcome: Admit a doc comment carrying reserved `@sdp*` tags as a second extracted representation of the anchor, feeding the same closed envelope as the constant form, so code that cannot import the Protocol binds at its real site and comment prose still authors nothing. ## Decision -- context: The source-commentary ruling it supersedes (`spec:decisions.jsdoc-graph-extraction-refused`, MD-35) refused two things in one record: comment prose authoring graph content, and identity tags in doc comments. Its rationale said the refusal "is about prose, not identity", while its operative alternative also refused structured identity tags; the annotation scout under `docs/lineage/` names that mismatch and finds that gen 1's drift was duplicate identities, scan scope, and syntax handling, never an extracted identity tag as such. The first adopter showed the cost of the single form: every one of its code anchors sits in a test or a harness because its runtime code cannot import this package, `byFile` on a source file returns nothing, and a hand-kept join stands in for the binding. The original design (`docs/lineage/v0-design/04-authoring-surfaces.md` §2) supported three marker styles feeding one extractor, and its package shape (`docs/lineage/v0-design/02-system-architecture.md` §3) kept the marker library free of runtime dependencies so product code could import it. The owner's direction opening plan 40 is to move closer to that design. +- context: The source-commentary ruling this record replaced (MD-35, deleted because nothing in it still needed its own text; it reads in git at `90c75dd`) refused two things in one record: comment prose authoring graph content, and identity tags in doc comments. Its rationale said the refusal "is about prose, not identity", while its operative alternative also refused structured identity tags; the annotation scout under `docs/lineage/` names that mismatch and finds that gen 1's drift was duplicate identities, scan scope, and syntax handling, never an extracted identity tag as such. The first adopter showed the cost of the single form: every one of its code anchors sits in a test or a harness because its runtime code cannot import this package, `byFile` on a source file returns nothing, and a hand-kept join stands in for the binding. The original design (`docs/lineage/v0-design/04-authoring-surfaces.md` §2) supported three marker styles feeding one extractor, and its package shape (`docs/lineage/v0-design/02-system-architecture.md` §3) kept the marker library free of runtime dependencies so product code could import it. The owner's direction opening plan 40 is to move closer to that design. - decision: An anchor has two extracted representations that feed one closed envelope, the constant form as ruled and a comment form. The comment form is a top-level `/** … */` block in a `.ts` or `.tsx` file carrying reserved camelCase, TSDoc-compatible tags; `@sdpAnchor ` opens it, and the id's namespace selects the flavor exactly as the constant builders do. Comment prose still authors nothing: only the reserved tags are read, and a comment that does not open with `/**` is never read. Trust is by reserved grammar, not by builder import, so no import is required. The same id in two forms is a duplicate id. The package also ships a zero-dependency subpath `@libar-dev/software-delivery-protocol/anchors` exporting the id builders and the three anchor builders and nothing else, for runtimes that may import but must not load `node:*` modules or ts-morph; the extractor trusts that specifier as a Protocol builder module. -- rationale: Hard to reverse: a second write path from source becomes a contract for the extractor, the validators, and every adopter's code. Surprising without context: the superseded ruling refused the obvious parity move, and this one admits exactly the half its own rationale left open while keeping the half it was about. Real trade-off: a tag grammar in a comment gets no compiler check and the extractor gains a second parser; in exchange, code that cannot import the package binds at its real site and `byFile` answers for it. -- rationale: What survives from the superseded ruling: comment prose authors nothing; a comment's wording beyond the reserved tags produces no graph finding; a comment that states law other surfaces depend on still promotes into a Spec under the promotion law, and restating promoted law in a comment violates exclusive promotion (MD-10), so the comment demotes to local commentary plus a Spec pointer; one graph language stays, because both forms feed one envelope and one extraction path (one validation path, MD-14). +- rationale: Hard to reverse: a second write path from source becomes a contract for the extractor, the validators, and every adopter's code. Surprising without context: the replaced ruling refused the obvious parity move, and this one admits exactly the half its own rationale left open while keeping the half it was about. Real trade-off: a tag grammar in a comment gets no compiler check and the extractor gains a second parser; in exchange, code that cannot import the package binds at its real site and `byFile` answers for it. +- rationale: What survives from the replaced ruling: comment prose authors nothing; a comment's wording beyond the reserved tags produces no graph finding; a comment that states law other surfaces depend on still promotes into a Spec under the promotion law, and restating promoted law in a comment violates exclusive promotion (MD-10), so the comment demotes to local commentary plus a Spec pointer; one graph language stays, because both forms feed one envelope and one extraction path (one validation path, MD-14). - consequence: The reserved tags are `@sdpAnchor`, `@sdpLabel`, `@sdpSatisfies`, `@sdpVerifies`, `@sdpModels`, `@sdpReferences`, `@sdpComponent`, `@sdpUses`, `@sdpRole`, `@sdpLayer`, and `@sdpContext`; the spelling is camelCase so TSDoc tooling reads each as one tag. - consequence: `@sdpAnchor ` opens the anchor; `impl:`, `api:`, and `component:` ids are code anchors, `test:` is a test anchor, `oracle:` is an oracle anchor. - consequence: Target tags by flavor are `@sdpSatisfies` on a code anchor, `@sdpVerifies` on a test anchor, and `@sdpModels` on an oracle anchor; lists are comma-separated. diff --git a/specs/decisions/architectural-annotation.sdp.md b/specs/decisions/architectural-annotation.sdp.md index b4b14a3e..3c47039f 100644 --- a/specs/decisions/architectural-annotation.sdp.md +++ b/specs/decisions/architectural-annotation.sdp.md @@ -5,9 +5,6 @@ altitude: feature readiness: ready relations: refines: spec:model.anchors - supersedes: - - spec:decisions.structural-anchor-semantics - - spec:decisions.architectural-significance-rides-primitives dependsOn: - spec:decisions.binding-not-liveness - spec:decisions.anchor-binding-grain @@ -18,10 +15,10 @@ relations: - outcome: Let a code anchor state the architectural role its unit plays, and let a component anchor state its layer and bounded context, so the graph answers architecture questions from the code that realizes the architecture, without a status, a second vocabulary registry, or a second architecture file. ## Decision -- context: Two rulings carried the structural half of the anchor: structural anchors confer nothing (`spec:decisions.structural-anchor-semantics`, MD-30) admitted `component` and `uses` as the only structural fields, and architectural significance rides existing primitives (`spec:decisions.architectural-significance-rides-primitives`, MD-34) refused a vocabulary beyond them and gave up CodeNode-grain roles. Both cited gen 1's tag-registry drift, about 50 tags to about 26, as the failure of an open structural vocabulary. The annotation scout under `docs/lineage/` corrects that account: the 50-to-26 collapse was a cleanup of gen 1's formal vocabulary that never shipped as a runtime registry, and what drifted there was duplicate identities, scan scope, and syntax handling. The original design declared a Component with `layer` and `bounded_context` (`docs/lineage/v0-design/03-graph-metamodel.md` §2.5), and gen 1 carried a role on each annotated unit. The owner's direction opening plan 40 is to bring back the value of annotating architecturally significant units and their relationships in code. +- context: Two rulings carried the structural half of the anchor: structural anchors confer nothing (MD-30) admitted `component` and `uses` as the only structural fields, and architectural significance rides existing primitives (MD-34) refused a vocabulary beyond them and gave up CodeNode-grain roles. Both cited gen 1's tag-registry drift, about 50 tags to about 26, as the failure of an open structural vocabulary. The annotation scout under `docs/lineage/` corrects that account: the 50-to-26 collapse was a cleanup of gen 1's formal vocabulary that never shipped as a runtime registry, and what drifted there was duplicate identities, scan scope, and syntax handling. The original design declared a Component with `layer` and `bounded_context` (`docs/lineage/v0-design/03-graph-metamodel.md` §2.5), and gen 1 carried a role on each annotated unit. The owner's direction opening plan 40 is to bring back the value of annotating architecturally significant units and their relationships in code. - decision: Architectural significance is annotated on the anchor of the unit that realizes it. A code anchor may carry `role`, the architectural pattern the unit plays; a `component:` anchor may additionally carry `layer`, one of `edge`, `application`, `domain`, `adapter`, or `infrastructure`, and `context`, its bounded context. The `component:` anchor is anchored architecture: it is the declared component, and there is no separate architecture file, because the anchor sits where the component is realized. It is not the original design's declared Component that could exist before code; an unrealized component lives in Specs and never as an invented CodeNode. A role is a free, corpus-owned vocabulary whose taxonomy the census renders with counts from the graph; `service`, `decider`, `projection`, `read-model`, `codec`, `contract`, `barrel`, and `utility` are the lineage set, not a closed list. `layer` is a closed set, and a value outside it is an envelope error; `layer` or `context` on a non-component anchor is an envelope error. All three attributes are optional, and omission is lawful. -- rationale: Hard to reverse: the anchor envelope and the CodeNode fields are contracts for the extractor, the census, the Design Review, and every adopter's annotations. Surprising without context: the superseded rulings refused exactly these attributes on the strength of a drift account the scout found inaccurate, so the reversal reads as a repeat of gen 1 until the correction is on the table. Real trade-off: a free role vocabulary can drift across a corpus and no validator will say so; in exchange, the role, layer, and context live on the one anchor at the one site, and the census taxonomy makes drift visible without a registry. -- rationale: What survives from both superseded rulings is restated here as consequences: the `component` and `uses` semantics and their validation rules, structural non-conferral, no inference from imports, no status or readiness on code, significance never selects a Spec kind, no `pattern:` namespace, and the engine significance criterion. +- rationale: Hard to reverse: the anchor envelope and the CodeNode fields are contracts for the extractor, the census, the Design Review, and every adopter's annotations. Surprising without context: the replaced rulings refused exactly these attributes on the strength of a drift account the scout found inaccurate, so the reversal reads as a repeat of gen 1 until the correction is on the table. Real trade-off: a free role vocabulary can drift across a corpus and no validator will say so; in exchange, the role, layer, and context live on the one anchor at the one site, and the census taxonomy makes drift visible without a registry. +- rationale: This record replaced both rulings, and both records were deleted because nothing in them still needed its own text; they read in git at `90c75dd`. What survives from them is restated here as consequences: the `component` and `uses` semantics and their validation rules, structural non-conferral, no inference from imports, no status or readiness on code, significance never selects a Spec kind, no `pattern:` namespace, and the engine significance criterion. - consequence: `component?: ComponentAnchorId` and `uses?: readonly CodeAnchorId[]` stay closed graph-ID references deriving only anchored `memberOf` and `uses` edges; neither accepts a free string or an enum detached from graph identity, and there are no per-namespace sibling builders. - consequence: Every `component` and `uses` target exists as a CodeNode, and a dangling graph id is an error. - consequence: Membership is one level: a `memberOf` edge runs only from an `impl:` or `api:` CodeNode to a `component:` CodeNode, each source has at most one component, and a `component:` node is never itself a member. @@ -37,6 +34,8 @@ relations: - consequence: No `pattern` term, kind, or `pattern:` namespace is admitted; a role is an attribute of a CodeNode, not a node; grouping is derived from id families and the component graph, not new Packs. - consequence: The engine significance criterion stays exported public surface or cross-component reach; code never satisfies a decision Spec directly (MD-26), and an anchor is never pointed at an unfinished Spec to manufacture coverage. - consequence: `role` and `context` values are one lowercase kebab token matching `^[a-z][a-z0-9-]*$`, with no normalization, so `read-model`, `readModel`, and `Read Model` cannot coexist as three categories; a value outside the token grammar is an envelope error. +- consequence: The census derives its structural rows and taxonomy from graph edges and runtime constants on every run; it re-derives no validation finding, owns no hand-kept taxonomy, and may group `uses` cycles as data. +- consequence: Code reaches the decisions that shape it through the `satisfies` → `decidedBy` join: a unit satisfies the Spec it realizes, and that Spec names its shaping decisions by `decidedBy`. - consequence: The census links each role, layer, and context value to the units that carry it, so an owner can reconcile two spellings of one category by editing the anchors; nothing forces a label, and an unlabelled unit does not populate the taxonomy. - consequence: The five layers, one line each: `edge` is the transport and presentation boundary; `application` is use cases and orchestration; `domain` is the model and its rules; `adapter` is the implementations of ports toward external systems; `infrastructure` is runtime, persistence, and platform plumbing. - consequence: A package may hold several layers or several contexts; the package-to-component mapping is reviewed, and one component per package is a choice, never a rule. diff --git a/specs/decisions/architectural-significance-rides-primitives.sdp.md b/specs/decisions/architectural-significance-rides-primitives.sdp.md deleted file mode 100644 index 896aa19f..00000000 --- a/specs/decisions/architectural-significance-rides-primitives.sdp.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: spec:decisions.architectural-significance-rides-primitives -kind: decision -altitude: feature -readiness: ready -relations: - refines: spec:model.anchors - dependsOn: - - spec:decisions.structural-anchor-semantics - - spec:decisions.binding-not-liveness ---- -# Architectural significance rides existing primitives - -## Intent -- outcome: Rule that Specs carrying architectural significance, and relationships among those Specs, are authored on the existing Spec primitive, relation vocabulary, and structural anchors, so the graph answers architecture questions without a second architecture vocabulary. - -## Decision -- context: `spec:model.structural-patterns` asked whether a vocabulary beyond `component` and `uses` passes the ADR three-part test, and "pattern" is not a ratified term; gen-1's tag-registry drift (~50→~26 tags) is the recorded failure of an open structural vocabulary (MD-30 cites it). -- decision: Architectural significance is authored on existing primitives, in order. First, architectural significance does not select a Spec kind. Choose the kind of truth being stated: behavior for runtime responsibilities, model for concepts, contract for an interface, constraint for a quality bound, and decision for a trade-off that passes the ADR test. The other existing kinds retain their meaning; no "pattern" term or kind is ratified. Second, relationships among those Specs are the existing relations — `dependsOn`, `supersedes`, `refines`, `decidedBy` — with `dependsOn` reserved for genuine semantic need and `supersedes` for actual replacement under the ADR test; scheduling-flavored edges stay refused (MD-33). Third, code linkage rides the `satisfies` → `decidedBy` join: a component or member anchor satisfies the Spec it realizes, and that Spec names its shaping decisions by `decidedBy`; code never satisfies a decision Spec directly (MD-26). Fourth, grouping is derived from id families and the component graph, not new Packs. Fifth, the significance criterion for engine self-binding is exported public surface or cross-component reach; under it `component:protocol.import` and `component:protocol.testing` enter the accepted component set. -- rationale: Hard to reverse — it permanently closes the vocabulary question and sets the grain at which architecture is authored; surprising without context — the concept "pattern" dissolves into named coordinates rather than gaining a word; real trade-off — CodeNode-grain roles for Specs carrying architectural significance and machine-checked forbidden dependencies are given up to preserve one graph language. -- consequence: The two blocking open questions on `spec:model.structural-patterns` resolve: no vocabulary beyond `component`/`uses` passes the ADR test, and no "pattern" term is ratified. -- consequence: Two documented grain limits stand as named limits, not holes: membership of Specs carrying architectural significance is Spec-grain (one `codeAnchor` carries one `satisfies`), and negative constraints remain declared intent, not machine-enforced graph findings. -- consequence: New `codeAnchor`s minted under this ruling satisfy only Specs the unit already realizes; anchors are never pointed at decision Specs or unfinished Specs to manufacture coverage. -- consequence: `component:protocol.import` and `component:protocol.testing` join the accepted component set; the structural-edges oracle's import exception comment is retired by this ruling. -- alternative: A dedicated pattern layer (`participatesIn` fields, `pattern:` ids, an architecture validator) was refused — it recreates the MD-30/MD-33-refused engine surface and the gen-1 taxonomy drift. -- alternative: Deriving architecture from imports stays refused (MD-30): structural edges are authored declarations, never inference. diff --git a/specs/decisions/authored-entry-order.sdp.md b/specs/decisions/authored-entry-order.sdp.md index 5edad9be..5ea863ce 100644 --- a/specs/decisions/authored-entry-order.sdp.md +++ b/specs/decisions/authored-entry-order.sdp.md @@ -4,9 +4,10 @@ kind: decision altitude: feature readiness: ready relations: - refines: spec:consumers.design-review + refines: + - spec:consumers.design-review + - spec:decisions.shipped-projections-frozen dependsOn: spec:extraction.open-section-order - supersedes: spec:decisions.shipped-projections-frozen --- # The Design Review renders open-section entries as a list @@ -14,7 +15,7 @@ relations: - outcome: Render the entries of an open section as a readable list in authored order, so a signature or a step reads as text and not as an escaped JSON string. ## Decision -- context: The Design Review prints an open section as one fenced JSON object, so each entry's value is an escaped string. Authored order is restored by `spec:extraction.open-section-order` as an ordinary revision, because no Spec states the sort. Rendering is different: `spec:consumers.design-review` states that fenced JSON preserves authored keys and values, and the shipped projections are frozen, so a list rendering needs a superseding record. The first adopter corpus carries 762 keyed entries, and its reviewers read the carrier files instead of the review. +- context: The Design Review prints an open section as one fenced JSON object, so each entry's value is an escaped string. Authored order is restored by `spec:extraction.open-section-order` as an ordinary revision, because no Spec states the sort. Rendering is different: `spec:consumers.design-review` states that fenced JSON preserves authored keys and values, and the shipped projections are frozen, so a list rendering needs a decision that narrows the freeze for this one rendering. The first adopter corpus carries 762 keyed entries, and its reviewers read the carrier files instead of the review. - decision: The Design Review renders each entry of a `design` or `ui` section as a list item in authored order, its key as inline code. A string value renders as Markdown text through the prose escaping rule; a number or boolean renders as inline code; an array or object renders as a fenced JSON block nested under its item, so a structured value the TypeScript carrier admits survives without loss. The fenced-JSON rule of the Design Review stops covering an open section as a whole and keeps covering a structured value inside one entry. The freeze on the four shipped projections stands for everything else. - rationale: A list reads as the author wrote it and escapes through the prose rules the Design Review already has for every rendered field, where fenced JSON turns a one-line signature into an escaped string nobody can paste. - alternative: Keeping fenced JSON in authored order makes a sequence readable and leaves every value escaped. diff --git a/specs/decisions/checked-mentions.sdp.md b/specs/decisions/checked-mentions.sdp.md index cf98f892..17a53995 100644 --- a/specs/decisions/checked-mentions.sdp.md +++ b/specs/decisions/checked-mentions.sdp.md @@ -2,7 +2,7 @@ id: spec:decisions.checked-mentions kind: decision altitude: feature -readiness: defined +readiness: ready relations: refines: spec:model.stable-ids dependsOn: spec:decisions.content-only-sections @@ -12,9 +12,6 @@ relations: ## Intent - outcome: Make a Spec id written in prose resolve, and make one keyed entry of an open section or one keyed open question addressable, so a reference below the Spec level can be checked. -### Open questions -- [non-blocking] `mention` and `entry address` are candidate terms, defined in the glossary's flagged list. They move into the ratified tables when this record states `ready`. - ## Decision - context: Specs name other Specs, and single entries of them, in prose. Referential integrity reads edges and Pack references only. A prose id that does not resolve passes validation, a mention that no relation backs hides a dependent from blast radius, and finding every Spec that repeats an entry is a text search. The first adopter corpus carried its design as keyed entries, and most of its prose mentions had no declared relation. - decision: A Spec id in narrative or section text, outside `gwt` and `gwt-vocabulary` fences, is a mention. A mention that does not resolve is a conformance error. A mention of another Spec that shares no declared relation with the mentioning Spec, in either direction, is one warning per mentioning Spec and target Spec pair, whatever the number of locations; an address on the mentioning Spec's own id is checked for its entry and never warned. An entry address is a Spec id followed by `#`, the section name `design`, `ui`, or `question`, a dot, and the entry's lower-camel ASCII key, as in `spec:#design.` or `spec:#question.`. A `design` or `ui` address resolves when the Spec exists and that section holds the key as its own key, other than `description`; a `question` address resolves when the Spec exists and one of its open questions carries that key. The `#` sub-part is reserved for the entry address in every namespace: a Spec's own id, a relation target, a Pack member or model reference, an anchor id or target, and the `specId` and `ref` builders all refuse it. A mention mints no edge, and the reader gains no join. diff --git a/specs/decisions/jsdoc-graph-extraction-refused.sdp.md b/specs/decisions/jsdoc-graph-extraction-refused.sdp.md deleted file mode 100644 index 65e6cd85..00000000 --- a/specs/decisions/jsdoc-graph-extraction-refused.sdp.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -id: spec:decisions.jsdoc-graph-extraction-refused -kind: decision -altitude: feature -readiness: ready -relations: - refines: spec:model.anchors - dependsOn: - - spec:decisions.one-validation-path - - spec:decisions.structural-anchor-semantics ---- -# Source commentary never enters the graph - -## Intent -- outcome: Rule that JSDoc, doc comments, and other source commentary never author graph nodes, relations, membership, or Spec intent, so the statically reified anchor constant remains the only write path from code into the graph. - -## Decision -- context: The v0 lineage supported three marker styles — decorators, JSDoc tags, and marker constants — all extracted (docs/lineage/v0-design/04-authoring-surfaces.md §2, 06-extraction-and-validation.md). The landed corpus keeps only the statically reified anchor constant; decorator and JSDoc forms remain unextracted representations (`spec:model.anchors`). Law-grade prose now sits in some engine file headers (for example `src/graph/delivery-facts.ts`), which raises whether the extractor should parse that prose into the graph. The ratified base rules which binding syntaxes extract (`spec:model.anchors`, MD-30); whether comment prose may author graph content is unruled until this decision, and the PR #25 review session genuinely deliberated that fork — this rules an open question, it does not repair drift. -- decision: Ordinary JSDoc and local commentary never author graph content — no nodes, no relations, no component membership, no delivery facts, no Spec intent. The only write path from source into the graph is the statically reified anchor constant under `spec:model.anchors`, and it is identity only. When a comment states rules other surfaces depend on, those rules promote into a Spec under the promotion law (`spec:model.spec-sections`) and the comment demotes to local commentary plus a Spec pointer. -- rationale: Hard to reverse — a comment-parsing write path would become a contract for source, extractor, and validators. Surprising without context — the lineage tool extracted identity tags from JSDoc, so the obvious parity move looks like extracting prose too; this refusal is about prose, not identity. Real trade-off — comment-prose extraction would make implementation files authoritative for intent (lineage 04 §2.4: markers are read-only pointers from code to spec, never the reverse) and would create a second, silently divergent read model beside the one extraction path (MD-14); giving that up keeps one graph language. -- consequence: The extractor never reads comment content; a comment's presence, absence, or wording produces no graph finding. -- consequence: A comment may explain the implementation and point at a Spec id; it confers nothing, and restating promoted law in the comment violates exclusive promotion (MD-10). -- alternative: Extracting identity tags from JSDoc (the v0 JSDoc marker style) stays refused with the decorator form — both are unextracted representations, and the anchor constant is the single binding syntax. -- alternative: Parsing law-grade prose comments into Spec sections was refused — it inverts the separation of intent from binding and recreates a shadow intent carrier beside the Specs. diff --git a/specs/decisions/pack-design-page.sdp.md b/specs/decisions/pack-design-page.sdp.md new file mode 100644 index 00000000..792125b4 --- /dev/null +++ b/specs/decisions/pack-design-page.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:decisions.pack-design-page +kind: decision +altitude: feature +readiness: ready +relations: + refines: + - spec:consumers.design-review + - spec:decisions.shipped-projections-frozen + dependsOn: spec:consumers.pack-design +--- +# The Design Review's Pack page presents the Pack's design + +## Intent +- outcome: Let a person working in the repository read where a Pack's design stands, what holds each member below its next rung, what the Pack rests on, and which code answers to it, on the page the Protocol already generates. + +## Decision +- context: The Design Review's Pack page lists the members with their readiness and bindings, and the verifier gaps. The first adopter designs whole capabilities ahead of their code as Packs and generates its own page beside the Design Review, 1,201 lines of script that walks the readiness floor one rung above the stated rung, reads what the members rest on outside the Pack, joins the open questions, and re-parses carrier files for the line of each entry. Its design passes read that page first. The shipped projections are frozen, so the Pack page cannot change until a decision narrows the freeze for it. +- decision: The Design Review's Pack page renders the Pack design that `spec:consumers.pack-design` assembles: a member table in authored order with each member's stated rung, floor reached, the rung above the stated rung with the clause that holds it there, its design columns and its bindings; a section naming what holds each member below its next rung; the Specs outside the Pack that members rest on and that rest on members; each member's open questions with their entry addresses and source lines; and each member's realizing and referencing code units with their roles and their components' layers and contexts. The verifier gaps and findings stay. The freeze on the four shipped projections stands for everything else, the Spec page and the index included. +- rationale: The assembly already exists as graph data and reader values, and an adopter that rebuilds it by hand reads the floor a second way, which the next-rung record showed it gets wrong. One assembly in the reader, rendered on the page the Protocol ships, gives a reviewer the design as it stands and leaves the adopter's script nothing to recompute. +- alternative: Starting the Spec Studio with an HTML Pack page is closer to the original design, but the Studio's package home and its reader are blocking questions, and the Pack design data serves the Studio later unchanged. +- alternative: Shipping the assembly as data only keeps every projection frozen and leaves rendering to each adopter, which is the burden this record removes. +- consequence: The Pack page grows from a member table to a design page, and the golden Design Review Pack pages regenerate. +- consequence: Line links on the page point into carrier files through the graph's location table, so the page stays a function of the graph alone. diff --git a/specs/decisions/planning-truths-placement.sdp.md b/specs/decisions/planning-truths-placement.sdp.md index 284da7e6..638905c0 100644 --- a/specs/decisions/planning-truths-placement.sdp.md +++ b/specs/decisions/planning-truths-placement.sdp.md @@ -15,6 +15,6 @@ relations: ## Decision - context: The prose briefs index carried dependency maps, decision gates, do-not-reopen rows, re-entry triggers, ownership rulings, selection-pressure heuristics, and session law, but none of those truth types had a ruled home in the graph, so plan documents drifted into carrying law the graph could not check. -- decision: Each planning-truth type lives in its ruled home. Work-item dependency truth rides `dependsOn` edges, with independence as absence of the edge and scheduling phrases never authored. Decision gates live on `decision`-kind Specs, linked by `decidedBy`. Do-not-reopen rows split by what they carry: a tradeoff refusal becomes a `decision`-kind Spec and reopens only through a later decision that `supersedes` it under the ADR three-part test; a row that restates a behavior guarantee already homed on a carrying Spec, including the runnable-modules extraction rows, stays on that Spec and changes by ordinary Spec revision; the bySymbol impact-graph row remains a blocking hold on `spec:consumers.impact-graph` and is never minted as a decision. A lawful non-decision stays in the plan record as evidence. Re-entry triggers are the deferred Spec's own blocking open questions plus `dependsOn` for a true precondition. Exclusive ownership is one Spec identity per deliverable with consumers depending on it. Selection-pressure heuristics are advisory only, carried as behavior rules on the graph-first planning Spec or in recipes. Session law splits separately, across behavior rules on the graph-first planning Spec, the on-ramp handoff rule, and the thin plan file. The briefs index shape is retired as a carrier of law. +- decision: Each planning-truth type lives in its ruled home. Work-item dependency truth rides `dependsOn` edges, with independence as absence of the edge and scheduling phrases never authored. Decision gates live on `decision`-kind Specs, linked by `decidedBy`. Do-not-reopen rows split by what they carry: a tradeoff refusal becomes a `decision`-kind Spec, which a later decision that `supersedes` it replaces whole and a later decision that `refines` it narrows for one case, either under the ADR three-part test; a row that restates a behavior guarantee already homed on a carrying Spec, including the runnable-modules extraction rows, stays on that Spec and changes by ordinary Spec revision; the bySymbol impact-graph row remains a blocking hold on `spec:consumers.impact-graph` and is never minted as a decision. A lawful non-decision stays in the plan record as evidence. Re-entry triggers are the deferred Spec's own blocking open questions plus `dependsOn` for a true precondition. Exclusive ownership is one Spec identity per deliverable with consumers depending on it. Selection-pressure heuristics are advisory only, carried as behavior rules on the graph-first planning Spec or in recipes. Session law splits separately, across behavior rules on the graph-first planning Spec, the on-ramp handoff rule, and the thin plan file. The briefs index shape is retired as a carrier of law. - rationale: The closed six-relation vocabulary already expresses every ruled home, so the ruling costs no engine work and keeps planning prose free of sequencing authority. New relation types such as `precedes`, `inArc`, or `forbids` were refused: each one is engine surface across the model, parser, extraction, validators, oracle rosters, and agent-surface docs, buying machine-checkable planning relations the corpus does not need. A `constraint`-kind home for refusals was refused because refusals are decisions. A single session-law home was refused because the planning Spec must not own per-session routing. -- consequence: Plans stay thin lineage pointers and the do-not-reopen register lives across its ruled homes: tradeoff refusals in decision Specs, already-homed guarantees on their carrying Specs, and the impact-graph row as a blocking hold. This ruling reopens only through a later `decision`-kind Spec that supersedes this one and itself passes the ADR three-part test. +- consequence: Plans stay thin lineage pointers and the do-not-reopen register lives across its ruled homes: tradeoff refusals in decision Specs, already-homed guarantees on their carrying Specs, and the impact-graph row as a blocking hold. This ruling is replaced only by a later `decision`-kind Spec that supersedes it, or narrowed by one that refines it, and either passes the ADR three-part test. diff --git a/specs/decisions/question-key-rendering.sdp.md b/specs/decisions/question-key-rendering.sdp.md index 72f18bfd..0529443c 100644 --- a/specs/decisions/question-key-rendering.sdp.md +++ b/specs/decisions/question-key-rendering.sdp.md @@ -4,9 +4,10 @@ kind: decision altitude: feature readiness: ready relations: - refines: spec:consumers.design-review + refines: + - spec:consumers.design-review + - spec:decisions.shipped-projections-frozen dependsOn: spec:model.open-question-keys - supersedes: spec:decisions.shipped-projections-frozen --- # The Design Review shows an open question's key @@ -14,7 +15,7 @@ relations: - outcome: Show an open question's key on its Spec page, so a reader can cite the question's entry address from the page. ## Decision -- context: `spec:model.open-question-keys` gives an open question an optional key in its marker and an entry address `spec:#question.`. The Design Review renders an open question as its escaped text, with the word blocking after a blocking one, and the shipped projections are frozen, so a reader of the page cannot see the key the address needs. The first adopter cites its open questions from a decision register and from generated pages. +- context: `spec:model.open-question-keys` gives an open question an optional key in its marker and an entry address `spec:#question.`. The Design Review renders an open question as its escaped text, with the word blocking after a blocking one, and the shipped projections are frozen, so a reader of the page cannot see the key the address needs until a decision narrows the freeze for it. The first adopter cites its open questions from a decision register and from generated pages. - decision: The Design Review renders an open question that carries a key with the key as inline code, `#`, at the start of its item, before the question's text; a question without a key renders as before. The freeze on the four shipped projections stands for everything else. - rationale: The key is the one part of the address the page did not show, and the `#` that opens it in the marker opens it on the page. - alternative: Rendering the whole address beside each question repeats the Spec id on every question of its own page. diff --git a/specs/decisions/shipped-projections-frozen.sdp.md b/specs/decisions/shipped-projections-frozen.sdp.md index ceaa63af..0ccd6f22 100644 --- a/specs/decisions/shipped-projections-frozen.sdp.md +++ b/specs/decisions/shipped-projections-frozen.sdp.md @@ -13,6 +13,6 @@ relations: ## Decision - context: An earlier projection-settling record refused re-specifying the four shipped projections, and the adoption register carried that refusal as a standing do-not-reopen row, but the refusal lived only as plan prose the graph could not check. -- decision: Re-specifying the shipped Design Review, census, Mermaid, or Gherkin projection is refused. These four projections are the ruled read surfaces over the one graph, and the ruling reopens only through a later decision Spec that supersedes this one and passes the ADR three-part test. +- decision: Re-specifying the shipped Design Review, census, Mermaid, or Gherkin projection is refused. These four projections are the ruled read surfaces over the one graph. A later decision Spec that supersedes this one replaces the ruling whole; a decision Spec that refines this one narrows it for one projection or one rendering and leaves the rest frozen. Either passes the ADR three-part test. - rationale: The four projections are shipped, curated read surfaces over the one graph. Re-specifying them re-litigates settled law and risks a second truth store, while a graph-checkable decision record carries the refusal where agents and validators can read it. -- consequence: Plans treat the four projections as settled read models. Any proposal to reshape one of them arrives as a superseding decision record, never as a follow-up work item. +- consequence: Plans treat the four projections as settled read models. Any proposal to reshape one of them arrives as a decision record that refines this one for that projection, or supersedes it whole, never as a follow-up work item. diff --git a/specs/decisions/structural-anchor-semantics.sdp.md b/specs/decisions/structural-anchor-semantics.sdp.md deleted file mode 100644 index ee521ce5..00000000 --- a/specs/decisions/structural-anchor-semantics.sdp.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -id: spec:decisions.structural-anchor-semantics -kind: decision -altitude: feature -readiness: ready -relations: - refines: spec:model.anchors - dependsOn: spec:decisions.binding-not-liveness ---- -# Structural anchor semantics - -## Intent -- outcome: Resolve which intent-free structural relationships code anchors may author without creating a second architecture truth. - -## Decision -- context: The generic `codeAnchor` already binds realization to a Spec through `satisfies`, while the reserved structural-semantics question leaves component membership and dependencies between anchored code units unresolved. Gen-1 also used `@architect-implements` to join separate test-pattern identities, not code to Spec, and exposed authored `uses` relationships through a broad tag registry. -- decision: In order, first, `satisfies` remains the only code-to-Spec realization slot: code that realizes a contract-kind Spec satisfies that Spec by authoring convention, so no `implements` field is admitted. Second, `component` and `uses` enter the `codeAnchor` contract as the only new structural fields. Third, their value forms are closed graph-ID references: `component?: ComponentAnchorId` is one statically reifiable reference, and `uses?: readonly CodeAnchorId[]` is a statically reifiable array of references; neither field accepts free strings or an enum detached from graph identity. Fourth, the structural references derive only the anchored `memberOf` and `uses` edges under the validation rules below. Fifth, the census always derives their taxonomy and rows from the graph and runtime constants; it is generated, disposable, and never hand-maintained. -- rationale: This is hard to reverse because the anchor authoring API and persisted edge vocabulary become contracts for source, validators, queries, and projections. It is surprising without context that contract realization remains `satisfies`, that a dependency cycle is accepted, and that structural edges confer no realization state. It is a real trade-off: local authored structure and referential checks justify two narrow fields, while rejecting `implements`, inference, lifecycle metadata, and an open tag system gives up looser notation to preserve one graph language. The caution is gen-1's taxonomy drift: a claimed 50 tags was corrected to about 26 while inconsistent counts remained (`reviews/14-executable-verification-design-review.md`, which cites the gen-1 formal-spec findings review); generated census output is therefore evidence, never another registry. -- consequence: Every `component` and `uses` target must exist as a `CodeNode`; a dangling graph ID is an error. A `memberOf` edge runs only from an `impl:` or `api:` `CodeNode` to a `component:` `CodeNode`, and each source has at most one component, enforcing one-level membership: a `component:` node cannot itself be a member. A `uses` edge runs between `CodeNode` endpoints whose IDs use the `impl:`, `api:`, or `component:` namespace. -- consequence: A present `uses` array must be non-empty and contain unique targets. Structural edges must be unique, and any structural self-reference is an error. Multi-node `uses` cycles remain authored data for census projection, not validator findings; validators do not infer transitive edges or reject cycles. -- consequence: The census renders the admitted structural bindings from graph edges, including component membership and uses relationships, and reports an explicit empty state when none exist. It may group cycles as data, but it neither re-derives validation findings nor owns a manually curated taxonomy. -- consequence: The new fields extend `codeAnchor` only; there are no per-namespace sibling anchor builders. -- consequence: The new edges are anchored claims only, never inferred claims; deriving architecture from imports is refused, because these edges are authored declarations in code. Derivation remains a mechanism and does not add a fourth claim. -- consequence: Anchors carry no intent, readiness, status, or delivery fact. `memberOf` and structural `uses` mint no delivery facts, add no delivery status, and do not change readiness floors. -- consequence: No free-form tag vocabulary, authored lifecycle, or parallel registry is admitted. -- consequence: Foreign fields remain extraction errors; after admitting `component` and `uses`, the anchor envelope remains closed and a malformed structural field refuses the whole anchor rather than yielding a partial declaration. -- consequence: Anchor-required lint remains warn-level and optional; absence of an anchor is useful evidence but never a load-bearing workflow gate. -- alternative: Adding `implements` would mirror gen-1 terminology, but would duplicate `satisfies` for contract-kind targets and make code-to-Spec realization ambiguous. -- alternative: Deriving dependencies from imports would increase coverage without annotations, but would turn incidental runtime wiring into authoritative architecture and misclassify an inferred observation as an anchored claim. -- alternative: Closed enums or free-form tags would be easy to author, but would not resolve to graph nodes and would recreate the taxonomy-governance failure this decision is intended to avoid. diff --git a/specs/decisions/studio-html-surface.sdp.md b/specs/decisions/studio-html-surface.sdp.md index 1f7363cb..3603e92d 100644 --- a/specs/decisions/studio-html-surface.sdp.md +++ b/specs/decisions/studio-html-surface.sdp.md @@ -2,7 +2,7 @@ id: spec:decisions.studio-html-surface kind: decision altitude: feature -readiness: scoped +readiness: ready relations: refines: spec:consumers.spec-studio dependsOn: spec:extraction.regenerability @@ -11,19 +11,16 @@ relations: ## Intent - outcome: Record why the Studio's surface is generated HTML rather than Markdown, and what keeps the HTML out of the diffs a reviewer reads. -### Open questions -- [blocking #primarySurface] Is the Studio's HTML the primary surface for a person reviewing a design, with Markdown as the fallback, as the original states, or one human projection beside the generated Markdown Design Review, which the consumers concept keeps as the MVP view after agents, the first sink? -- [non-blocking #generationCost] The original accepts HTML at about two to four times Markdown's generation time, with a negligible token cost under prompt caching and million-token windows, which prices a page a model writes; a projection is rendered by code from the graph, so does the argument become a bound on page weight or build time, or fall away? -- [non-blocking #canonicalCarrier] The original keeps git diffs over canonical TypeScript Specs; the carrier ruling makes Markdown the default carrier and the TypeScript DSL a lawful per-ID option. Does any part of the case for derived HTML depend on the carrier being TypeScript? ## Decision -- context: When a model writes the Specs, a person's main act is review, and review reads tables, diagrams, code, comparisons and examples across many Specs. Markdown caps out at about a hundred lines, past which nobody reads it, its authors included; large Specs are common, and Markdown rots into a wall of text quickly. +- context: When a model writes the Specs, a person's main act is review, and review reads tables, diagrams, code, comparisons and examples across many Specs. The first adopter's reviewers need search, filters, navigation and progressive disclosure across a Pack, which the generated Markdown pages do not give: the Pack page's member table runs thirteen columns wide, and those readers open the carrier files instead. - decision: The Studio renders generated HTML as its surface. The HTML is derived and never committed in raw form: git diffs stay over the canonical Spec source, and the Studio's files are regenerated from the graph. -- rationale: HTML conveys more per screen: tables, SVG diagrams, code blocks with syntax highlighting, embedded harnesses, interactive sliders, side-by-side comparisons and tabbed views are all native to it. -- rationale: HTML is shareable: a build uploaded to S3 or any static host is a link, with no rendering-engine mismatch between viewers. -- rationale: HTML is interactive: a reader can move an NFR target on a slider, toggle a scenario harness, or reorder a Pack's members by drag and drop, none of which Markdown can do. -- rationale: When a model writes the Specs, review is the person's main act, and HTML beats Markdown for review by a wide margin. -- rationale: The download is acceptable: HTML takes about two to four times Markdown's generation time, and with prompt caching and million-token windows the token cost is negligible for the value gained. -- alternative: Markdown as the primary surface diffs cleanly in git, but caps out at about a hundred lines and cannot be interactive. +- rationale: HTML conveys more per screen: collapsible sections, wide tables that scroll in place, SVG diagrams, highlighted code and side-by-side comparisons are native to it. +- rationale: HTML is shareable: a page set uploaded to any static host is a link a reviewer opens without installing anything. +- rationale: HTML is interactive: search, filters and navigation run in the page over values the build wrote, which Markdown cannot do. +- rationale: The Studio's code renders its HTML from the graph, so the original's argument from generation time and token cost, which priced a page a model writes, does not apply. +- alternative: Markdown as the only human surface diffs cleanly in git and renders on GitHub, but it cannot search, filter or collapse, which is why the Markdown Design Review stays beside the Studio rather than in its place. - consequence: The principal trade-off of HTML, diffs that are noisier in git, is sidestepped because the HTML is derived and never committed. -- consequence: Markdown remains a fallback surface. +- consequence: The Studio stands beside the Markdown Design Review rather than above it. The Studio is the workbench a person reads, and the frozen Markdown Design Review stays the surface git diffs, GitHub renders and agents read; neither ranks above the other. +- consequence: The case for derived HTML holds for any canonical carrier, because it rests on the HTML being derived and never committed, not on the Specs being written in TypeScript. +- consequence: The richer interactions the original design lists, such as moving a target on a slider, running a scenario harness or reordering members by drag and drop, wait for later phases and their own Specs; the Studio's first build is read-only. diff --git a/specs/design-management.pack.sdp.md b/specs/design-management.pack.sdp.md new file mode 100644 index 00000000..68b02372 --- /dev/null +++ b/specs/design-management.pack.sdp.md @@ -0,0 +1,33 @@ +--- +id: pack:design-management-v1 +specs: + - spec:decisions.pack-design-page + - spec:consumers.pack-design + - spec:consumers.design-review.pack-design-page + - spec:extraction.entry-locations + - spec:consumers.agent-surface.recipe-parameters + - spec:consumers.agent-surface.design-recipes + - spec:consumers.agent-surface.example-space-coverage + - spec:consumers.engine-provenance + - spec:consumers.validate-watch + - spec:model.design-maturity + - spec:model.planned-architecture + - spec:model.open-question-fields + - spec:model.rule-keys + - spec:model.relation-reasons + - spec:consumers.review-perspectives + - spec:consumers.context-bundle + - spec:consumers.design-diff + - spec:consumers.intent-composition.proposal + - spec:consumers.reference-projection + - spec:consumers.structural-mermaid + - spec:validation.superseded-decision-signal + - spec:validation.architecture-constraints + - spec:validation.corpus-rules + - spec:observation.run-evidence +modelRefs: + - spec:model.design-maturity +--- +# Design management + +The arc of plan 41, in the order a reviewer reads it: what lets a person working in the repository design a capability ahead of its code and iterate on it. First the Pack page that shows where a Pack's design stands, and the reader assembly it renders; then the graph's record of where each entry is written, the recipe parameters and files, and the recipes design work reads; then the engine's provenance and the authoring watch loop that had no Spec. The captured Specs that follow hold what the original design (`docs/lineage/v0-design/`) promised and the Protocol has not built, each at the rung its structure clears, with the question that holds it there. They run from the model, through the consumers a design pass reads with, to validation and observation. diff --git a/specs/extraction/build-pipeline.sdp.md b/specs/extraction/build-pipeline.sdp.md index 771ad8f8..43fc9107 100644 --- a/specs/extraction/build-pipeline.sdp.md +++ b/specs/extraction/build-pipeline.sdp.md @@ -13,7 +13,7 @@ relations: - outcome: Turn authored carriers into validated derived artifacts. ### Open questions -- [non-blocking] Does the derive-in-process freshness law stated in src/cli/q-command.ts commentary promote here or to a story-altitude child under comment promotion? +- [non-blocking #freshnessPromotion] Does the derive-in-process freshness law stated in src/cli/q-command.ts commentary promote here or to a story-altitude child under comment promotion? ## Workflow - Discover carriers. diff --git a/specs/extraction/claim-taxonomy.sdp.md b/specs/extraction/claim-taxonomy.sdp.md index 2901f32f..473889a1 100644 --- a/specs/extraction/claim-taxonomy.sdp.md +++ b/specs/extraction/claim-taxonomy.sdp.md @@ -13,7 +13,7 @@ relations: ## Model - **declared** — Human intent explicitly authored in a Spec or Pack; it is authoritative intent. -- **anchored** — A human binding from a code, test, or oracle location to one Spec ID; it is authoritative binding and carries no intent. +- **anchored** — A human binding from a code, test, or oracle location to zero or more Spec IDs, or between code units for structure; it is authoritative binding and carries no intent. - **inferred** — Machine-derived structural information; it is advisory and never authoritative. - **claim inheritance** — An edge computed from an authored source retains that source's declared claim; derivation is a mechanism, not a fourth claim. - **delivery fact** — A realization signal computed from resolving edges, never an authored claim or edge. diff --git a/specs/extraction/contract-declarations.sdp.md b/specs/extraction/contract-declarations.sdp.md index dc72eb4f..acc0555d 100644 --- a/specs/extraction/contract-declarations.sdp.md +++ b/specs/extraction/contract-declarations.sdp.md @@ -17,9 +17,9 @@ The re-entry trigger held with the first adopter's rebuild package. At the adopt - outcome: Give a declaration one ruled shape inside the Design section, so that a module derived from the graph can carry every pinned signature of a corpus and a Spec that disagrees with its code fails the adopter's typecheck the way a step contract does. ### Open questions -- [non-blocking] The contract kind's evidence row in the kind-evidence table is unchanged by this Spec. A closed contract section is not planned; if an adopter asks for one, it enters as a Spec of its own. -- [non-blocking] A compiled module finds a name used and never declared and a name declared twice. It does not find a bullet whose prose uses a declared name with another type, which is the drift the first adopter met at its merge. Checking a span used inside a step bullet as an expression is deferred until an adopter asks for it. -- [non-blocking] The source report's full shape is kept here so the capture does not narrow it in silence: an opaque language-tagged fence as the value of one keyed entry, then the closed section whose declarations derive one module per corpus, then implementation code importing the derived types so that a Spec which disagrees with its code fails the build. Compiling the derived module alone finds only names used and never declared, and names declared twice. +- [non-blocking #contractSection] The contract kind's evidence row in the kind-evidence table is unchanged by this Spec. A closed contract section is not planned; if an adopter asks for one, it enters as a Spec of its own. +- [non-blocking #bulletTypeDrift] A compiled module finds a name used and never declared and a name declared twice. It does not find a bullet whose prose uses a declared name with another type, which is the drift the first adopter met at its merge. Checking a span used inside a step bullet as an expression is deferred until an adopter asks for it. +- [non-blocking #reportShape] The source report's full shape is kept here so the capture does not narrow it in silence: an opaque language-tagged fence as the value of one keyed entry, then the closed section whose declarations derive one module per corpus, then implementation code importing the derived types so that a Spec which disagrees with its code fails the build. Compiling the derived module alone finds only names used and never declared, and names declared twice. ## Behavior - rule: A declaration is the code span that opens the value of one keyed Design entry, written on one line in the language the adopter's code is written in. The Protocol parses no language inside the span and stores it as authored. diff --git a/specs/extraction/entry-locations.sdp.md b/specs/extraction/entry-locations.sdp.md new file mode 100644 index 00000000..ab07dd94 --- /dev/null +++ b/specs/extraction/entry-locations.sdp.md @@ -0,0 +1,26 @@ +--- +id: spec:extraction.entry-locations +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:extraction.derive-graph + dependsOn: spec:model.open-question-keys + constrainedBy: spec:extraction.determinism +--- +# The graph records where each addressable entry is written + +An entry address names one keyed Design or UI entry, or one keyed open question. A reader that follows an address to the text needs the line it sits on, and today only the carrier file knows it, so the first adopter's page parses every member's carrier a second time to find them. + +## Intent +- problem: The graph records the file of a Spec and the line of every anchor, but not the line of an entry inside a Spec, so a page that links an address to its text, or a review that cites one, re-reads carrier syntax the extractor already parsed. +- outcome: Record the source line of every keyed Design and UI entry and every open question in the graph, apart from the nodes, so any consumer links an entry to its text without parsing a carrier. + +## Behavior +- rule: The graph carries a location table beside its nodes and edges. Each row names a Spec id, the carrier file, the entry, and the 1-based line the entry starts on in that file. An entry is a `design` or `ui` key other than `description`, written as `design.` or `ui.`, or an open question, written as `question[]` by its position in the Spec's open questions counted from 0, with its key beside it when the question carries one. +- rule: Primitive and Pack nodes stay free of lines, so a comparison of nodes and edges is unchanged by an edit that only moves an entry, and carrier parity compares nodes and edges and never the location table, whose lines differ by carrier by nature. +- rule: Every carrier the extractor reads records the lines it can locate: the Markdown carrier records each entry it reifies; the TypeScript carrier records the line of each entry's property; the Gherkin carrier records the open questions it carries. An entry with no row has no recorded line, which a consumer reads as not located and never as absent. +- rule: Rows sort by Spec id, then by entry, in code-unit order. The table is a deterministic function of the carriers at the extracted commit, as every other part of the graph is. +- rule: A location names where an entry is written and nothing else: it is not an identity, never an anchor target or a binding, and an edit above an entry moves its line. +- rule: The reader's Spec context carries the Spec's rows as its entry locations. Address resolution (recipe 25) adds the entry's value, file and line to each resolving row, and entry search (recipe 23) adds the line to each entry it matches. +- rule: The graph schema version moves to `0.9.0`. The realizing sites are the Markdown, TypeScript and Gherkin extractors in `src/extract/`, the graph schema in `src/graph/schema.ts`, and `specContext` in `src/reader/reader.ts`. diff --git a/specs/extraction/example-runner.sdp.md b/specs/extraction/example-runner.sdp.md index f7497828..e354e6d2 100644 --- a/specs/extraction/example-runner.sdp.md +++ b/specs/extraction/example-runner.sdp.md @@ -14,7 +14,7 @@ relations: - value: A failing example reads as the Spec that failed rather than as an anonymous assertion. ### Open questions -- [non-blocking] Do the every-step-and-only-the-steps and fresh-world-per-example laws stated in the runner and vitest-adapter commentary promote here or to story-altitude children under comment promotion? +- [non-blocking #runnerLawPromotion] Do the every-step-and-only-the-steps and fresh-world-per-example laws stated in the runner and vitest-adapter commentary promote here or to story-altitude children under comment promotion? ## 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. diff --git a/specs/extraction/executable-contracts.sdp.md b/specs/extraction/executable-contracts.sdp.md index 60efa0b3..92d55b6d 100644 --- a/specs/extraction/executable-contracts.sdp.md +++ b/specs/extraction/executable-contracts.sdp.md @@ -13,7 +13,7 @@ relations: - outcome: Give bound tests typed step and example-space contracts without reading authored Specs directly. ### Open questions -- [non-blocking] Do the concreteness-refusal and no-guessing outcome-identity laws stated in src/codegen/contracts.ts commentary promote here or to a story-altitude child under comment promotion? +- [non-blocking #contractLawPromotion] Do the concreteness-refusal and no-guessing outcome-identity laws stated in src/codegen/contracts.ts commentary promote here or to a story-altitude child under comment promotion? ## Behavior - rule: `generateContracts` derives per-example step contracts and per-parent space contracts solely from the extracted graph. diff --git a/specs/extraction/open-section-order.sdp.md b/specs/extraction/open-section-order.sdp.md index aa43c344..a71b0e6c 100644 --- a/specs/extraction/open-section-order.sdp.md +++ b/specs/extraction/open-section-order.sdp.md @@ -2,7 +2,7 @@ id: spec:extraction.open-section-order kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:extraction.derive-graph --- diff --git a/specs/extraction/pack-member-order.manifest-order-kept.sdp.md b/specs/extraction/pack-member-order.manifest-order-kept.sdp.md index e9b6f548..ad93087e 100644 --- a/specs/extraction/pack-member-order.manifest-order-kept.sdp.md +++ b/specs/extraction/pack-member-order.manifest-order-kept.sdp.md @@ -19,5 +19,5 @@ When the graph is derived and serialized Then the serialized Pack node lists the members {serializedMembers: "spec:probe.zeta, spec:probe.alpha, spec:probe.mid"} Then the serialized belongsTo edges run from {edgeSources: "spec:probe.alpha, spec:probe.mid, spec:probe.zeta"} Then the reader's Pack context lists the members {readerMembers: "spec:probe.zeta, spec:probe.alpha, spec:probe.mid"} -Then the payload declares the schema version {schemaVersion: "0.8.0"} +Then the payload declares the schema version {schemaVersion: "0.9.0"} ``` diff --git a/specs/extraction/pack-member-order.sdp.md b/specs/extraction/pack-member-order.sdp.md index b8b57437..8062fdd0 100644 --- a/specs/extraction/pack-member-order.sdp.md +++ b/specs/extraction/pack-member-order.sdp.md @@ -2,7 +2,7 @@ id: spec:extraction.pack-member-order kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:extraction.derive-graph dependsOn: spec:carrier.markdown-pack-authoring diff --git a/specs/extraction/regenerability.sdp.md b/specs/extraction/regenerability.sdp.md index fd7c6692..43a073da 100644 --- a/specs/extraction/regenerability.sdp.md +++ b/specs/extraction/regenerability.sdp.md @@ -2,7 +2,7 @@ id: spec:extraction.regenerability kind: rule altitude: feature -readiness: defined +readiness: ready relations: refines: spec:extraction.determinism --- @@ -15,5 +15,3 @@ relations: - Generated artifacts are disposable: deleting them and rebuilding from the same committed repository produces the same bytes. - Consumers read the graph or link to source locations recorded in it; they never re-parse source or keep a parallel model. - The graph is a single JSON projection with in-memory query support; a graph database remains deferred until measured traversal pain establishes a real need. -- Measured evidence from the self-hosting corpus keeps full rebuilds comfortable below roughly 50 Specs. -- Measured evidence defers a graph database until the graph reaches roughly 10k+ nodes or traversal pain establishes a real need. diff --git a/specs/extraction/schema-versioning.declared-version.sdp.md b/specs/extraction/schema-versioning.declared-version.sdp.md index 2db7b711..c3753d3c 100644 --- a/specs/extraction/schema-versioning.declared-version.sdp.md +++ b/specs/extraction/schema-versioning.declared-version.sdp.md @@ -15,5 +15,5 @@ relations: ```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.8.0"} +Then the payload declares the schema version {schemaVersion: "0.9.0"} ``` diff --git a/specs/model/anchors.sdp.md b/specs/model/anchors.sdp.md index 62bc91f3..a1ec21cb 100644 --- a/specs/model/anchors.sdp.md +++ b/specs/model/anchors.sdp.md @@ -25,9 +25,9 @@ relations: - **plural targets** — `satisfies` on a code anchor and `verifies` on a test anchor accept one `ref(…)` or a fresh array literal of them; `verifies` is non-empty. Each resolving target confers its fact as a single target does, and `implemented` stays a whole-Spec fact. - **identity-only anchor** — A code anchor with no `satisfies` and no `references`; it mints a CodeNode for structure and for `byFile`, derives no realization edge, and confers nothing. - **references** — The anchored CodeNode-to-Spec edge derived from `references?: readonly SpecId[]` on a code anchor: this code answers to that design without claiming to realize it. The unit follows the design or realizes part of it, so a change to the design asks the code to follow. The direction is one way: a design that builds on existing code states that on its own side, by a `dependsOn` or `refines` relation to the Spec the code satisfies, never by a reference from the code. It confers no delivery fact, moves no readiness floor, and the drift alarm ignores it; a target also named in `satisfies` is an error. The reader reports the referencing units on the Spec's context, and blast radius traverses the edge as a binding. -- **role** — An optional free string on a code anchor naming the architectural pattern the unit plays; a corpus-owned vocabulary whose taxonomy the census renders with counts, checked against no list. +- **role** — An optional one-token value on a code anchor, lowercase kebab and never normalized, naming the architectural pattern the unit plays; a corpus-owned vocabulary whose taxonomy the census renders with counts, checked against no list. - **layer** — An optional attribute of a `component:` anchor, one of `edge`, `application`, `domain`, `adapter`, or `infrastructure`; a value outside the set, or the attribute on a non-component anchor, is an envelope error. -- **context** — An optional free string on a `component:` anchor naming its bounded context; on a non-component anchor it is an envelope error. +- **context** — An optional one-token value on a `component:` anchor, in the token grammar of a role, naming its bounded context; on a non-component anchor it is an envelope error. - **declared component** — The `component:` anchor is the component's declaration; there is no separate architecture file, because the anchor sits where the component is realized. - **component realization convention** — A `component:` anchor that satisfies a Spec satisfies its seam's most-specific design Spec; that `satisfies` edge is the component's own realization claim. As guidance, checked by no validator: a component either satisfies its seam's design Spec or binds no Spec its members satisfy, and it never references one. Structural edges confer nothing under the architectural-annotation ruling (MD-38), which supersedes structural anchors confer nothing (MD-30) without changing this. - **structural anchor validity** — A `memberOf` source is an `impl:` or `api:` CodeNode and its target is a `component:` CodeNode; every structural target exists, every edge is unique, each source has at most one component, and structural self-reference is refused. A malformed or non-static structural field, an unknown `layer`, or `layer` or `context` on a non-component anchor refuses the whole anchor at reification; a graph-validly reified edge that later fails referential or structural validation remains visible with its anchor and independent `satisfies` binding. Multi-node `uses` cycles remain data and produce no finding. diff --git a/specs/model/core-model.sdp.md b/specs/model/core-model.sdp.md index b0b4eec6..050d8e63 100644 --- a/specs/model/core-model.sdp.md +++ b/specs/model/core-model.sdp.md @@ -2,7 +2,7 @@ id: spec:model.core-model kind: model altitude: feature -readiness: defined +readiness: ready relations: refines: spec:protocol.self-hosting decidedBy: diff --git a/specs/model/design-maturity.sdp.md b/specs/model/design-maturity.sdp.md new file mode 100644 index 00000000..d701cd84 --- /dev/null +++ b/specs/model/design-maturity.sdp.md @@ -0,0 +1,21 @@ +--- +id: spec:model.design-maturity +kind: model +altitude: feature +readiness: defined +relations: + refines: spec:model.core-model + dependsOn: spec:consumers.pack-design +--- +# What a Spec's design carries is derived, beside its readiness + +The original design had seven readiness stages, and its `designed` stage meant the Spec's key abstractions were settled before code (`docs/lineage/v0-design/01-core-primitives.md` §1.4 and §8). The Protocol rules four rungs, and they describe the intent, so readiness alone does not tell a `defined` Spec with no design from a `defined` Spec whose interfaces are settled. When the design-management arc was planned, the owner ruled that it derives design columns beside readiness and adds no rung. Later the owner ruled the rest: what design a Spec carries is read from the derived columns and never stated, so no rung, floor clause or design fact carries it. Readiness stays the author-stated design maturity of the Spec's intent. + +## Intent +- problem: A reviewer or an implementer cannot tell from readiness alone which Specs carry a settled design and which carry only intent, and the first adopter's owner asked for Specs whose key abstractions are designed as stubs before code. +- outcome: Show what design a Spec carries in values the graph derives, so the backlog and a review can tell a designed Spec from one that states only its intent. +- assumption: The design columns tell a Spec with pinned declarations and keyed entries from one that states only its intent. + +## Model +- **design columns** — the counts and lists the reader derives for a Spec beside its readiness: keyed Design and UI entries, pinned declarations, open and blocking questions, and the decisions it names. The Pack design carries them for each member, and the build backlog recipe shows them for each ready Spec no code realizes yet. They are derived on every read and never stated: no readiness rung, floor clause or delivery fact carries the design they show, because a rung would break the four locked readiness values, a floor clause that read Design would close that section's shape, and a design fact would mix design into the facts that say what code and tests bind. +- **designed stage** — the original design's readiness stage for a Spec whose key abstractions are settled; not a rung of the Protocol. The question reopens if the first adopter's design passes show that the design columns cannot tell a designed Spec from one that states only its intent. diff --git a/specs/model/enrichment-lifecycle.sdp.md b/specs/model/enrichment-lifecycle.sdp.md index a94717e2..b886a0eb 100644 --- a/specs/model/enrichment-lifecycle.sdp.md +++ b/specs/model/enrichment-lifecycle.sdp.md @@ -12,7 +12,7 @@ relations: - outcome: Keep a Spec useful after implementation without recreating value-transfer duplication. ### Open questions -- [blocking] After implementation, which design-time detail stays in the Spec and which detail may be removed while preserving one durable home for each explanation? +- [blocking #detailAfterImplementation] After implementation, which design-time detail stays in the Spec and which detail may be removed while preserving one durable home for each explanation? ## Model - **enrichment lifecycle** — The same Spec gains and may later slim typed detail without changing identity or moving truth into another artifact type. diff --git a/specs/model/open-question-fields.sdp.md b/specs/model/open-question-fields.sdp.md new file mode 100644 index 00000000..abc615ee --- /dev/null +++ b/specs/model/open-question-fields.sdp.md @@ -0,0 +1,31 @@ +--- +id: spec:model.open-question-fields +kind: rule +altitude: story +readiness: defined +relations: + refines: spec:model.open-question-keys + decidedBy: spec:decisions.adopt-the-nouns +--- +# An open question may name who decides it, its links and a provisional reading + +The original design gave an open question an owner and a list of links to decision records, discussions and tickets, beside its text and its blocking flag (`docs/lineage/v0-design/01-core-primitives.md` §2.1). The Protocol gave a question an optional key and an address and stopped there. The first adopter keeps the rest beside the graph. Its decision register (`libar-platform/design/decisions/register.json`, 175 rows, described in `design/advisors/register.md`) records for each open decision its sources, who decides it as a class (`tactical`, `delegated` or `owner`) and an advisor lens, the provisional reading the design proceeds on, which 96 rows carry, and a lean. It also records a status, the work unit it blocks, dates and who checked the lean. This Spec captures the part that states something about the question and leaves the work record outside. + +## Intent +- problem: A question's text and blocking flag do not say who settles it, what it rests on, or what the design assumes until it is settled, so an adopter keeps a register beside the graph that repeats each question. +- outcome: Let an open question carry who decides it, the sources it rests on and the reading the design proceeds on, without a status. +- risk: A field for who decides invites fields for when and for what state the question is in, which would rebuild a workflow tracker inside the Spec. + +### Open questions +- [non-blocking #linkTargets] May a link name only a Spec id or an entry address, which validation checks, or also an external document, which the `doc:` deferral leaves without an identity? +- [non-blocking #leanHome] The adopter's lean has six parts: what to do, what it rests on, the case against it, what it settles, what it changes and what follows if it is wrong. Does any part belong on the question, or is a lean always a draft of a decision Spec? + +## Rule +- An open question may carry three optional fields beside its text, flag and key, namely who decides it, the links it rests on and its provisional reading; a question without them stays lawful. +- Who decides a question names the person, role or body whose ruling settles it. It authorizes nothing, gates no edit, and no validator compares it with the author of a change. +- Who decides a question is one lowercase kebab token the corpus owns, on the grammar a role uses, such as `owner` or `delegated`. The graph holds no person or role to point at, so the value references nothing, and the Protocol ships no list of values. +- In the Markdown carrier the three fields ride labelled sub-items indented under the question's own line, `decider:`, `links:` and `provisional:`, each at most once and each one line, so the question line stays as authored. They are the one indented line the body grammar admits, and the TypeScript carrier carries the same three fields on the question object. +- A link names a Spec id or an entry address the question rests on and resolves as a mention does. A link mints no relation. +- The provisional reading is one line stating what the design assumes while the question is open. It is intent, never a ruling. +- An open question carries no status, date, blocked work unit or record of who checked it. A settled question leaves the Spec, a ruling that passes the decision test lands as a decision Spec the shaped Specs name through `decidedBy`, and the rest of the history stays in git and in the adopter's own records. +- Whether a question holds the readiness floor stays its `[blocking]` flag alone; none of the three fields moves a floor. diff --git a/specs/model/open-question-keys.sdp.md b/specs/model/open-question-keys.sdp.md index a86f53c1..827776b1 100644 --- a/specs/model/open-question-keys.sdp.md +++ b/specs/model/open-question-keys.sdp.md @@ -2,7 +2,7 @@ id: spec:model.open-question-keys kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:model.spec-sections dependsOn: spec:carrier.markdown-body-grammar diff --git a/specs/model/planned-architecture.sdp.md b/specs/model/planned-architecture.sdp.md new file mode 100644 index 00000000..74feecf2 --- /dev/null +++ b/specs/model/planned-architecture.sdp.md @@ -0,0 +1,29 @@ +--- +id: spec:model.planned-architecture +kind: model +altitude: feature +readiness: defined +relations: + dependsOn: spec:model.anchors + decidedBy: spec:decisions.architectural-annotation +--- +# Components are designed ahead of their code + +The original design declared a component before its code, as a Component node with a layer, a bounded context and a capability, kept in an architecture folder and reconciled with the code markers and the Specs (`docs/lineage/v0-design/03-graph-metamodel.md` §2.5; `04-authoring-surfaces.md` §7). Its runtime chapter separated what a unit requires from the provider that supplies it, the provider a composition root selects and the one a test puts in its place, and gave one mechanism the ownership of runtime composition (`05-runtime-anchors.md` §1, §3.3, §7). + +The Protocol records the architecture the code realizes. A `component:` anchor states its layer and context, and anchored `memberOf` and `uses` edges join the code units. Under the architectural annotation decision, a component no code realizes is a Spec, and the graph holds no node for it until an anchor exists. The first adopter states its component list and its context rule in prose (`libar-platform/design/PLAN.md` §2.1) and checks context isolation over anchored `uses` in its own script (`libar-platform/design/tools/check.py`). The owner ruled that the architectural annotation decision stands: a component designed before its code is a component design Spec, and the `component:` anchor that later realizes it satisfies that Spec, which is the join a comparison reads. + +## Intent +- problem: A design ahead of its code cannot state the components it adds, their layer and context and what they use, in a form the graph compares with the components the code later anchors. +- outcome: Let a design name the components it will add, with their layer, context and uses, and compare that design with the anchored components once code exists. +- risk: A planned component kept apart from its realization is a second place for the component to drift, which is why the architectural annotation decision refused a separate architecture file and why the design lives in a Spec the realizing anchor satisfies. + +### Open questions +- [non-blocking #providerSelection] The original kept apart the interface a unit requires, the providers that supply it, the provider a composition root selects and the one a test substitutes, for Fastify, Effect, Awilix and plain factories alike. Does a component design state these without naming a framework, and does any of them earn graph structure beyond `uses`? +- [non-blocking #oneRuntimeOwner] Is the original's one-runtime-truth rule, one composition mechanism per application, a Protocol rule or a corpus's own architecture rule? + +## Model +- **component design** — A Spec that states a component the code does not yet realize and what it is for. Its Design entries are keyed as the anchor's attributes are named: `id` for the planned `component:` anchor's own id, `layer`, `context`, `uses` for the components it will use, and optionally `role`. The keys are a convention a recipe reads; the Protocol closes no Design shape, and the Spec confers nothing and mints no code unit. +- **planned component** — The component a component design names. It stays intent until a `component:` anchor realizes it, and that anchor satisfies the component design; the graph holds no component node before the anchor exists. +- **realization comparison** — A catalog recipe that reads each component design beside the `component:` anchor that satisfies it, through the component data recipe 32 joins, and lists a design no anchor satisfies yet, a layer or context that differs, and a `uses` named on one side only. It reports and refuses nothing. +- **provider** — A code unit that supplies an interface another unit requires; a composition selects one provider for each requirement, and a test may select another. diff --git a/specs/model/relation-reasons.sdp.md b/specs/model/relation-reasons.sdp.md new file mode 100644 index 00000000..5ff138bd --- /dev/null +++ b/specs/model/relation-reasons.sdp.md @@ -0,0 +1,29 @@ +--- +id: spec:model.relation-reasons +kind: rule +altitude: story +readiness: defined +relations: + refines: spec:model.relations + decidedBy: spec:decisions.prose-ownership +--- +# An authored relation may carry its reason + +The original design let a relation say why it holds: a rationale on `refines`, a strength on `dependsOn`, an aspect on `constrainedBy` and a reason on `supersedes` (`docs/lineage/v0-design/01-core-primitives.md` §4; `04-authoring-surfaces.md` §1.3). The Protocol's relations carry a type, a target and a claim, and the envelope that holds them is closed, so the reason a Spec rests on another lives, if anywhere, in narrative prose a reader has to find. The prose-ownership decision gives free prose a home on a typed owner and leaves the text on an edge for later; a relation's reason is that text. + +The first adopter meets the reverse case. When its mention audit reported Spec pairs named in prose with no relation behind them, it declared the relations that were missing dependencies and kept twelve pairs as prose, each listed with a one-line reason no typed relation fits (`libar-platform/docs/feedback/sdp-feedback-01.md`, "The mention warnings were mostly missing edges"; `libar-platform/design/tools/prose-mentions.json`). A reason on a relation says why an edge holds; a listed mention reason says why none does. + +## Intent +- problem: A reviewer reading a relation cannot see why it holds, and a reviewer reading a mention left as prose cannot tell an intended pointer from a forgotten dependency without a list kept outside the graph. +- outcome: Let an author say in one line why a Spec refines, depends on, is constrained by, is decided by or supersedes another, beside the relation itself. + +### Open questions +- [non-blocking #typedAttributes] Should the original's typed attributes return, a strength on `dependsOn` and an aspect on `constrainedBy`, or does one free reason serve every relation type? +- [non-blocking #mentionReasons] Should a mention left as prose carry its reason in the mentioning Spec, so the mention audit can tell an intended pointer from a missing relation, or does that list stay the adopter's own? + +## Rule +- An authored relation may carry one optional reason, a single line of prose owned by that relation from that Spec to that target. +- A reason is intent, as its relation is; it confers no delivery fact, moves no readiness floor and changes no edge, and a relation without one stays lawful. +- In the envelope a relation target that carries a reason is an object with `target` and `reason` in place of the plain id, and plain ids and objects mix in one relation, so a relation without a reason is written as before and no target is written twice. The TypeScript carrier passes the same reason beside the target. +- A reason on `supersedes` says what the newer decision replaces, and a reason on a `refines` from one decision to another says which part of the older decision the newer one narrows. +- A reason reaches readers with its relation through the graph, so a projection or a recipe reads it from the edge and never from the carrier. diff --git a/specs/model/rule-keys.sdp.md b/specs/model/rule-keys.sdp.md new file mode 100644 index 00000000..5b92e8ea --- /dev/null +++ b/specs/model/rule-keys.sdp.md @@ -0,0 +1,27 @@ +--- +id: spec:model.rule-keys +kind: rule +altitude: story +readiness: defined +relations: + refines: spec:model.spec-sections + dependsOn: spec:model.open-question-keys +--- +# A rule or flow entry may carry a key that addresses it + +The original design gave a behavior rule an optional id, so the rule could later be promoted to a Spec of its own, and an optional rationale (`docs/lineage/v0-design/01-core-primitives.md` §2.2). The Protocol kept promotion and dropped the id. An inline rule has no key and no address, so a review finding, a register row or an example cannot name the rule it is about, and the Spec Studio's tests lens cannot name the rules no example exercises until a rule gains an address. Open questions gained optional keys on the Design key grammar; this Spec follows that pattern for rules. + +## Intent +- problem: An inline rule has no identity but its position and its text, so anything outside the Spec that cites one rule breaks silently when the rules above it move or the rule is reworded. +- outcome: Let an author key a rule or flow entry, so a review, a register row or an example can name the entry it is about. + +### Open questions +- [non-blocking #exampleNamesRule] Should an example name the rule it exercises, and where: in its prose, or in a field a rule-coverage view could read? +- [non-blocking #ruleRationale] Does a rule keep the original's rationale, or is the rationale prose the Spec's narrative already owns? + +## Rule +- Every entry of a Spec's primary behavior owner may carry an optional key on the open-question key grammar: the `rule:` and `flow:` entries of Behavior, the entries of Rule and Contract, and the entries and `rule:` entries of Workflow. One Spec keeps one key namespace across them, unique among its keyed entries, and an unkeyed entry stays lawful and has no address. +- A key sits in a marker at the start of the entry's text, after the `rule:` or `flow:` label where the entry has one, as in `- rule: [#graphOnly] The Studio is a projection.` and `- [#oneFamily] Every validator belongs to one family.`, the shape an open question's marker uses, so one form serves labelled and plain entries. +- Every keyed entry is addressed as `spec:#rule.` whatever its section, because a Spec uses one primary behavior owner. The address resolves while the Spec carries an entry with that key and fails validation in every Spec that names it once the key is renamed or removed. +- A rule address is refused in every id slot, as every entry address is, so no anchor targets a rule and no delivery fact attaches to one. +- A keyed rule promoted to a Spec of its own leaves its address behind, because promotion moves the content out, and every mention of that address fails until it names the new Spec. diff --git a/specs/observation/run-evidence.sdp.md b/specs/observation/run-evidence.sdp.md new file mode 100644 index 00000000..b9262100 --- /dev/null +++ b/specs/observation/run-evidence.sdp.md @@ -0,0 +1,29 @@ +--- +id: spec:observation.run-evidence +kind: behavior +altitude: feature +readiness: defined +relations: + dependsOn: spec:extraction.delivery-facts + decidedBy: spec:decisions.binding-not-liveness +--- +# CI run records are joined to verifiers in a view + +The original design ingested test reports into the graph. Each run became a node with its result and duration, and a passing run marked its Spec verified (`docs/lineage/v0-design/06-extraction-and-validation.md` §11.2; `08-delivery-evidence-and-tooling.md` §2, §3.2). The binding-not-liveness decision rules otherwise. `has-verifier` says a resolving verifier binding exists, and pass and fail stay in CI. The vision document says test run verdicts are never ingested (`docs/concept/00-vision-scope-and-mvp-boundary.md`). Read literally, that keeps them out of every view; this Spec reads it as keeping them out of the graph. The runtime overlay holds the other half of the original's evidence layer, runtime observations against measurable targets, which aims at the `observed` fact; run evidence derives no fact at all. + +The first adopter keeps run records anyway, one JSON file per run with its tier, target, commit, whether the tree was clean, and each test's file, name, result and duration (`libar-platform/evidence/runs/`). It asks to see them beside the bindings as a separately identified input, joined by verifier, with commit, tier, composition and target, and a note when the run predates the source being read (`libar-platform/docs/feedback/sdp-feedback-02.md`, item 25). The Spec Studio's verification panels and its tests lens each hold a question on whether to show run results read from CI output; this Spec is the answer they wait on. + +## Intent +- problem: A reader who wants to know whether a bound verifier passed has to leave the Protocol's views for CI, and an adopter that keeps run records has no view that shows them beside the binding without mistaking them for a delivery fact. +- outcome: Show a verifier's recorded runs beside its binding in a view, labelled as external input with its commit, while the graph keeps recording only that the binding exists. + +### Open questions +- [non-blocking #freshness] How does a view say a run is stale: by its commit against the commit being read, or by whether the bound files changed between the two? + +## Behavior +- rule: Run records are an input a view reads beside the graph, named by the caller; the graph never holds them, and no node, edge or delivery fact derives from them. +- rule: A run record joins every verifier whose test anchor sits in the test file the record names, because the file is the binding grain of both anchor forms and every reporter prints it; each test name in the record shows as a row under that verifier. The join is at file grain: a result is evidence from the verifier's file, never the result of that verifier, because a file may hold several suites and a filtered run may exercise only one. +- rule: The Protocol reads run records in the Common Test Report Format, CTRF, an open JSON schema with reporters for the common test runners, passed to a view as a recipe parameter. A record joins only when its reporter fills CTRF's optional `filePath` field, and an adopter with records of another shape maps them into CTRF itself. +- rule: A view shows each run beside the verifiers anchored in its file, with its result, its commit and the input it came from, labelled as external run evidence from that file and never as `has-verifier`, `implemented` or a pass verdict for one verifier. +- rule: A run whose commit differs from the commit being read shows both commits, so a reader sees how old it is. +- rule: The Design Review stays a function of the graph alone and shows no run; run evidence appears only in a view that admits external input, such as a recipe that takes the records as a parameter, or the Studio. diff --git a/specs/observation/runtime-overlay.sdp.md b/specs/observation/runtime-overlay.sdp.md index 4da34469..df5ac1c8 100644 --- a/specs/observation/runtime-overlay.sdp.md +++ b/specs/observation/runtime-overlay.sdp.md @@ -13,4 +13,4 @@ relations: - outcome: Associate runtime evidence with measurable Spec targets without turning operational payloads into authored model truth. ### Open questions -- [blocking] Which external observation identity and freshness boundary is small enough for the graph while still supporting an honest `observed` delivery fact? +- [blocking #observationIdentity] Which external observation identity and freshness boundary is small enough for the graph while still supporting an honest `observed` delivery fact? diff --git a/specs/self-hosting.pack.sdp.md b/specs/self-hosting.pack.sdp.md index fdf632c9..e187e5cb 100644 --- a/specs/self-hosting.pack.sdp.md +++ b/specs/self-hosting.pack.sdp.md @@ -76,6 +76,7 @@ specs: - spec:validation.authored-honesty - spec:validation.warn-level-signals - spec:consumers.projections-model + - spec:consumers.projections-model.roadmap-views - spec:consumers.impact-graph - spec:consumers.agent-surface - spec:consumers.design-review @@ -188,12 +189,9 @@ specs: - spec:decisions.gherkin-carrier-option - spec:decisions.sdp-gherkin-extension - spec:decisions.carrier-universality - - spec:decisions.structural-anchor-semantics - spec:decisions.adopted-registrars-committed - spec:decisions.shipped-projections-frozen - spec:decisions.planning-truths-placement - - spec:decisions.architectural-significance-rides-primitives - - spec:decisions.jsdoc-graph-extraction-refused - spec:decisions.checked-mentions - spec:decisions.authored-entry-order - spec:validation.unbound-example-posture.lower-rungs @@ -226,6 +224,33 @@ specs: - spec:decisions.studio-web-components - spec:consumers.spec-studio.responsive - spec:consumers.spec-studio.sharing + - spec:decisions.anchor-comment-form + - spec:decisions.anchor-binding-grain + - spec:decisions.architectural-annotation + - spec:decisions.pack-design-page + - spec:consumers.pack-design + - spec:consumers.design-review.pack-design-page + - spec:extraction.entry-locations + - spec:consumers.agent-surface.recipe-parameters + - spec:consumers.agent-surface.design-recipes + - spec:consumers.engine-provenance + - spec:model.design-maturity + - spec:consumers.validate-watch + - spec:consumers.agent-surface.example-space-coverage + - spec:model.planned-architecture + - spec:model.open-question-fields + - spec:model.rule-keys + - spec:model.relation-reasons + - spec:consumers.review-perspectives + - spec:consumers.context-bundle + - spec:consumers.design-diff + - spec:consumers.intent-composition.proposal + - spec:consumers.reference-projection + - spec:consumers.structural-mermaid + - spec:validation.superseded-decision-signal + - spec:validation.architecture-constraints + - spec:validation.corpus-rules + - spec:observation.run-evidence modelRefs: - spec:model.protocol-domain - spec:model.core-model diff --git a/specs/validation/architecture-constraints.sdp.md b/specs/validation/architecture-constraints.sdp.md new file mode 100644 index 00000000..6c19a7e4 --- /dev/null +++ b/specs/validation/architecture-constraints.sdp.md @@ -0,0 +1,31 @@ +--- +id: spec:validation.architecture-constraints +kind: rule +altitude: feature +readiness: defined +relations: + dependsOn: + - spec:model.anchors + - spec:validation.corpus-rules + decidedBy: spec:decisions.architectural-annotation +--- +# Declared architecture rules are checked against anchored structure + +The original design gave architecture a validation tier of its own. Its rules said that only adapter components depend on external systems, that domain code imports no infrastructure and that layers depend in one direction; dependency-cruiser, architecture tests and lint rules ran them, and their findings joined the one report (`docs/lineage/v0-design/06-extraction-and-validation.md` §6.4). Since the architectural annotation decision, the data those rules read is in the graph with no inference: a component anchor states its layer and its context, and `uses` and `memberOf` edges are anchored. The same decision keeps the check out of the Protocol. A negative architecture constraint stays declared intent in a Spec, and a corpus that wants it enforced checks its declared `uses`, `layer` and `context` in its own gate. + +The first adopter needed one such rule as soon as the attributes landed. No component in its `platform` context may have a `uses` edge to a component in another context, with each end resolved to its component through `memberOf` first. It checks the rule in its own script (`libar-platform/design/tools/check.py`, the context rule of `libar-platform/design/PLAN.md` §2.1). Recipe 32, architecture crossings, reports every `uses` edge whose ends sit in different contexts or layers and refuses nothing. The owner ruled that the architectural annotation decision stands: an architecture rule stays the corpus's own and runs as one of its corpus rules, and the Protocol supplies the walk the adopter writes by hand today. + +## Intent +- problem: A corpus that states an architecture rule in a Spec cannot have it checked by the Protocol, so each adopter writes the same walk over `uses`, `memberOf`, `layer` and `context` in its own gate. +- outcome: Check a corpus's declared architecture rules, such as a forbidden uses direction or context isolation, against the layers, contexts and uses its anchors state. + +### Open questions +- [non-blocking #ruleForms] Which rule forms cover the need: a forbidden `uses` from one layer to another, an isolated context, a component that must state a layer? The original's list also held rules on ports and routes, which have no anchors here. + +## Rule +- The Protocol's validators never judge a corpus's architecture; an architecture rule runs as one of the corpus's own rules, the corpus's gate decides what fails, and that gate may be the corpus's own invocation of `sdp validate --rules` with the severity the rule states. +- A corpus states each architecture rule as intent in a `constraint` Spec that says what is forbidden and why, and keeps the rule's machine form, its forbidden layer pairs and isolated contexts, as data its corpus rule passes to the walk. +- The Protocol supplies the walk as a catalog recipe that takes forbidden layer pairs and isolated contexts as recipe parameters, resolves each end of every anchored `uses` edge to its component, directly or through `memberOf`, and returns each edge a rule forbids with both units, both components and the rule. +- The walk reads the given rules and anchored structure only; it infers no edge from imports, reads no file path, judges no role and refuses nothing. +- A component that states no layer or context falls outside a rule that names one, and the walk reports it as unclassified rather than as a breach. +- The corpus binds its rule file to the constraint Spec as that Spec's verifier through a test anchor, so `has-verifier` says a check for the constraint exists; pass and fail stay in the corpus's own run. diff --git a/specs/validation/corpus-rules.sdp.md b/specs/validation/corpus-rules.sdp.md new file mode 100644 index 00000000..52f2869c --- /dev/null +++ b/specs/validation/corpus-rules.sdp.md @@ -0,0 +1,29 @@ +--- +id: spec:validation.corpus-rules +kind: behavior +altitude: feature +readiness: defined +relations: + dependsOn: spec:consumers.agent-surface + constrainedBy: spec:validation.two-check-families +--- +# Corpus-authored rules run on the one validation path + +The original design let a team add its own rules beside the built-in validators, each with an id, a severity and the whole graph query API, run in the same pass and reported in the same output (`docs/lineage/v0-design/06-extraction-and-validation.md` §6.5). The Protocol has two check families, conformance and honesty, and every validator belongs to one of them. The architectural annotation decision sends a corpus's own rules to the corpus's own gate. + +The first adopter's gate shows what that costs. Its 363-line check script runs `sdp validate` and parses the summary line, then holds about ten rules of its own: context isolation over declared `uses`, entry addresses written outside the Specs resolved through recipe 25, no `dependsOn` cycle through recipe 26, every open question keyed, a prose mention allowed only with a listed reason, cited laws backed by `constrainedBy`, and the Pack pages current. To read kind, readiness and relations it parses the carriers' frontmatter with regular expressions, the second read model the agent surface warns against (`libar-platform/design/tools/check.py`). + +## Intent +- problem: A corpus with rules of its own checks them off the one validation path, reading the carriers a second way and reporting in a form no Protocol surface reads. +- outcome: Let a corpus author its own graph rules and have their findings join the one validation report, so no corpus re-parses its carriers to check itself. + +## Behavior +- rule: A corpus rule is a graph body the corpus authors, given the bindings a recipe body gets, the reader, the raw graph and the report, and returning findings that each carry a rule id, a subject, a message and a severity. +- rule: A corpus rule reads the one derived graph and nothing else, so no rule reads a carrier, a source file or the network. +- rule: Each corpus finding names its rule and its corpus as its source and stands apart from the Protocol's own findings; the Protocol states none of these rules and ships none. +- rule: Corpus findings stand outside the conformance and honesty families, in a section of the report labelled as the corpus's own, so the two families keep their meaning and the guardrail that checks never judge content quality or workflow keeps binding the Protocol's own validators. +- rule: Validation runs corpus rules only from a file the operator names with `sdp validate --rules PATH`, which may repeat, and never from a file it finds in the corpus or in a configuration; it asks the trust `sdp q` already asks, to run code the operator chose. +- rule: A corpus finding carries severity `error` or `warning`. When the operator names `--rules`, an `error` corpus finding makes `sdp validate` exit nonzero and a `warning` stays informative; without `--rules` no corpus rule runs and only the Protocol's validators set the exit code. +- rule: Corpus findings keep their own source and report section, are tallied apart from the Protocol's summary line, and neither add to, suppress nor change a Protocol finding or the exit reason it sets. +- rule: A rules file that cannot load, a rule that throws, or a rule that returns a finding without a rule id, subject, message and admitted severity is a rule execution failure; `sdp validate` reports it in the corpus section and exits nonzero, so a broken rule never passes as a clean run. +- rule: A corpus rule changes no node, edge or delivery fact; it reports on the graph it is given. diff --git a/specs/validation/next-rung-floor.sdp.md b/specs/validation/next-rung-floor.sdp.md index b105b03e..b65282e9 100644 --- a/specs/validation/next-rung-floor.sdp.md +++ b/specs/validation/next-rung-floor.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.next-rung-floor kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:validation.readiness-floor dependsOn: spec:validation.typed-dependency-floor diff --git a/specs/validation/prose-mentions.sdp.md b/specs/validation/prose-mentions.sdp.md index a4cc9cd5..84330eb9 100644 --- a/specs/validation/prose-mentions.sdp.md +++ b/specs/validation/prose-mentions.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.prose-mentions kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:validation.referential-integrity decidedBy: spec:decisions.checked-mentions diff --git a/specs/validation/readiness-floor.sdp.md b/specs/validation/readiness-floor.sdp.md index 8c15c3e9..49854095 100644 --- a/specs/validation/readiness-floor.sdp.md +++ b/specs/validation/readiness-floor.sdp.md @@ -16,7 +16,7 @@ relations: - outcome: Refuse maturity claims that their authored evidence does not support. ### Open questions -- [non-blocking] Does any remaining law in the src/validate/readiness-floor.ts file header promote here under comment promotion? +- [non-blocking #headerLawPromotion] Does any remaining law in the src/validate/readiness-floor.ts file header promote here under comment promotion? ## Rule - A Spec may state a readiness only when every clause in that readiness floor passes. diff --git a/specs/validation/superseded-decision-signal.sdp.md b/specs/validation/superseded-decision-signal.sdp.md new file mode 100644 index 00000000..b88a9fe9 --- /dev/null +++ b/specs/validation/superseded-decision-signal.sdp.md @@ -0,0 +1,30 @@ +--- +id: spec:validation.superseded-decision-signal +kind: rule +altitude: story +readiness: defined +relations: + refines: spec:validation.warn-level-signals + dependsOn: spec:model.relations +--- +# A Spec shaped by a superseded decision warns + +The original design checked lifecycle integrity. It flagged a superseded Spec that live Specs still depend on, and a deprecated decision still named by `decidedBy` (`docs/lineage/v0-design/06-extraction-and-validation.md` §6.3.4). The Protocol keeps decision lineage in the graph, as `supersedes` edges between decision Specs and `decidedBy` edges from the Specs they shape, but no check reads the two together. When this arc began, the corpus showed the cost. The Spec Studio's lenses Spec still named a decision the architectural annotation decision had superseded, and its architecture question still read the old ruling after the new one admitted layer and context. + +The rule needed one ruling first. Three decisions once declared `supersedes` on the projections freeze while each replaced only one part of it, so a signal reading every `supersedes` as a whole replacement would have warned on the consumer Specs the freeze still correctly shapes. The owner ruled that `supersedes` replaces a whole decision and that a decision narrowing one part of another `refines` it; the three records now refine the freeze. + +## Intent +- problem: A Spec can stay shaped by a decision that a later decision replaced, and nothing tells its author that the ruling it rests on has moved. +- outcome: Surface a live Spec that is still shaped by a decision another decision has superseded, as an informative warning. + +### Open questions +- [non-blocking #signalReach] Does the signal read only `decidedBy`, or also a `refines` or `dependsOn` that names a superseded decision? +- [non-blocking #supersededChain] When a superseding decision is itself superseded, does the warning name the decision at the end of the chain? + +## Rule +- A `supersedes` edge replaces the decision it targets whole; a decision that narrows part of another `refines` it instead, and the signal never reads a `refines` between decisions. +- A replaced decision whose text no reader still needs is deleted rather than kept beside a `supersedes` edge, and a Spec still naming it then fails as an unresolved target, so the signal covers only replaced decisions kept as history. +- A Spec that names a decision through `decidedBy`, where another decision `supersedes` that decision, carries one warning for each such pair, naming the superseded decision and every decision that supersedes it. +- The warning is informative and never fails validation, as the orphan and gap signals never do, and its severity is fixed by the Protocol. +- The `supersedes` edge itself is never warned, and a superseded decision is never warned for being superseded. +- The signal reads the authored edges of the one graph and nothing else; it judges no content and never asks whether the newer ruling fits the Spec. diff --git a/specs/validation/two-check-families.sdp.md b/specs/validation/two-check-families.sdp.md index 2c446235..b839eb5d 100644 --- a/specs/validation/two-check-families.sdp.md +++ b/specs/validation/two-check-families.sdp.md @@ -13,14 +13,14 @@ relations: - outcome: Keep the graph trustworthy by checking conformance and honesty without judging content quality or enforcing workflow. ### Open questions -- [non-blocking] Does the one-validation-path registry law stated in the src/validate/validators.ts file header promote here or to a story-altitude child under comment promotion? +- [non-blocking #registryLawPromotion] Does the one-validation-path registry law stated in the src/validate/validators.ts file header promote here or to a story-altitude child under comment promotion? ## Rule -- 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. +- Every validator the Protocol ships belongs to either the conformance family, which checks meta-model well-formedness, or the honesty family, which rejects authored or overstated derived truth; a corpus rule run through `sdp validate --rules` belongs to neither and reports in the corpus's own section. +- Errors from the Protocol's validators 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 two families are load-bearing, so an aggregate report spanning both states no family of its own while every finding of the Protocol's own validators names the family it came from. - The realizing entrypoints are `graphValidatorIds` and `validateGraph` in `src/validate/validators.ts`. ## Example space diff --git a/specs/validation/typed-dependency-floor.sdp.md b/specs/validation/typed-dependency-floor.sdp.md index 359c9e0c..efd989d9 100644 --- a/specs/validation/typed-dependency-floor.sdp.md +++ b/specs/validation/typed-dependency-floor.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.typed-dependency-floor kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:validation.readiness-floor dependsOn: spec:model.relations diff --git a/specs/validation/unbound-example-posture.sdp.md b/specs/validation/unbound-example-posture.sdp.md index cba3732c..5dc439a9 100644 --- a/specs/validation/unbound-example-posture.sdp.md +++ b/specs/validation/unbound-example-posture.sdp.md @@ -2,7 +2,7 @@ id: spec:validation.unbound-example-posture kind: rule altitude: story -readiness: defined +readiness: ready relations: refines: spec:validation.verification-linkage --- diff --git a/src/cli/build-artifacts.ts b/src/cli/build-artifacts.ts new file mode 100644 index 00000000..1e275c37 --- /dev/null +++ b/src/cli/build-artifacts.ts @@ -0,0 +1,193 @@ +import { execFileSync } from "node:child_process"; +import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +import { BUILD_INFO_FILE, UNKNOWN_COMMIT, isFullCommit, serializeBuildInfo } from "./build-info.js"; + +// The build step that runs after the bundle succeeds (`tsup.config.ts` calls it). It is build +// tooling, never part of the shipped CLI: no entry imports it. + +/** The catalog the recipe files derive from, relative to the package root. */ +export const RECIPE_CATALOG = "docs/agent-surface/recipes.md"; + +/** The recipe files' directory, relative to the build output directory. */ +export const RECIPES_DIRECTORY = "recipes"; + +export interface RecipeFile { + readonly ordinal: number; + readonly fileName: string; + /** The bytes between the body's two fence lines: each body line ended by a line feed. */ + readonly content: string; +} + +const headingPattern = /^## (?\d+)\. (?.+)$/u; + +/** The heading text after the number, in lower kebab case. */ +export function recipeSlug(title: string): string { + return title + .toLowerCase() + .replace(/[^a-z0-9]+/gu, "-") + .replace(/^-+|-+$/gu, ""); +} + +export function recipeFileName(ordinal: number, title: string): string { + return `${String(ordinal).padStart(2, "0")}-${recipeSlug(title)}.js`; +} + +function refuse(message: string): never { + throw new Error(`${RECIPE_CATALOG}: ${message}`); +} + +/** + * Pairs each numbered `## N. Title` heading with the one fenced `js` body under it. A body with + * no heading, a heading with no body, a second body under one heading, a repeated number, or an + * empty slug refuses the build: a file name would otherwise be guessed. A catalog with no recipe + * refuses too, before anything is published, because publishing none would remove every shipped + * file. Lines may end in CRLF; each body is written with LF endings, as the catalog check reads it. + */ +export function recipeFiles(catalog: string): readonly RecipeFile[] { + const lines = catalog.split(/\r?\n/u); + const files: RecipeFile[] = []; + const seen = new Set<number>(); + let pending: { readonly ordinal: number; readonly title: string } | undefined; + let openedAt: number | undefined; + + for (const [index, line] of lines.entries()) { + if (openedAt !== undefined) { + if (line !== "```") continue; + + if (pending === undefined) { + refuse(`the js body opened at line ${String(openedAt)} has no numbered heading`); + } + + files.push({ + ordinal: pending.ordinal, + fileName: recipeFileName(pending.ordinal, pending.title), + content: lines + .slice(openedAt, index) + .map((bodyLine) => `${bodyLine}\n`) + .join(""), + }); + openedAt = undefined; + pending = undefined; + continue; + } + + const heading = headingPattern.exec(line)?.groups; + + if (heading?.ordinal !== undefined && heading.title !== undefined) { + if (pending !== undefined) { + refuse(`recipe ${String(pending.ordinal)} has no js body`); + } + + const ordinal = Number(heading.ordinal); + + if (seen.has(ordinal)) { + refuse(`recipe ${String(ordinal)} is numbered twice`); + } + + if (recipeSlug(heading.title) === "") { + refuse(`recipe ${String(ordinal)} has no heading text to name its file`); + } + + seen.add(ordinal); + pending = { ordinal, title: heading.title }; + continue; + } + + if (line === "```js") { + if (pending === undefined) { + refuse(`the js body at line ${String(index + 1)} has no numbered heading`); + } + + openedAt = index + 1; + } + } + + if (openedAt !== undefined) { + refuse(`the js body opened at line ${String(openedAt)} is never closed`); + } + + if (pending !== undefined) { + refuse(`recipe ${String(pending.ordinal)} has no js body`); + } + + if (files.length === 0) { + refuse("the catalog has no numbered recipe with a js body"); + } + + return files; +} + +/** Replaces a file in one rename, so a concurrent reader sees the old bytes or the new ones. */ +function replaceFile(path: string, content: string): void { + const temporary = `${path}.${String(process.pid)}.tmp`; + + writeFileSync(temporary, content); + renameSync(temporary, path); +} + +/** The name `recipeFileName` gives a published recipe; a temporary file never matches it. */ +const completedRecipeFile = /^\d{2,}-[a-z0-9]+(?:-[a-z0-9]+)*\.js$/u; + +/** + * Writes every recipe file, then removes each completed recipe file the catalog no longer owes. + * Any other entry stays, a concurrent build's temporary file among them. + */ +export function writeRecipeFiles(outDir: string, files: readonly RecipeFile[]): void { + const directory = join(outDir, RECIPES_DIRECTORY); + const owed = new Set(files.map((file) => file.fileName)); + + mkdirSync(directory, { recursive: true }); + + for (const file of files) { + replaceFile(join(directory, file.fileName), file.content); + } + + for (const entry of readdirSync(directory)) { + if (completedRecipeFile.test(entry) && !owed.has(entry)) { + rmSync(join(directory, entry), { recursive: true, force: true }); + } + } +} + +export type GitRunner = (cwd: string, args: readonly string[]) => string; + +const runGit: GitRunner = (cwd, args) => + execFileSync("git", [...args], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); + +/** The full hash of the commit checked out at `root`, or `unknown` when git cannot name one. */ +export function readBuildCommit(root: string, git: GitRunner = runGit): string { + try { + const commit = git(root, ["rev-parse", "HEAD"]).trim(); + + return isFullCommit(commit) ? commit : UNKNOWN_COMMIT; + } catch { + return UNKNOWN_COMMIT; + } +} + +export function writeBuildInfo(outDir: string, commit: string): void { + const directory = join(outDir, "cli"); + + mkdirSync(directory, { recursive: true }); + replaceFile(join(directory, BUILD_INFO_FILE), serializeBuildInfo({ commit })); +} + +/** + * @sdpAnchor impl:protocol.build-artifacts + * @sdpLabel the build step that writes each catalog recipe to dist/recipes and records the build commit beside the CLI + * @sdpSatisfies spec:consumers.agent-surface.recipe-parameters, spec:consumers.engine-provenance + * @sdpComponent component:protocol.cli + * @sdpRole publisher + */ +export function writeBuildArtifacts(input: { + readonly root: string; + readonly outDir: string; + readonly git?: GitRunner; +}): void { + const catalog = readFileSync(join(input.root, RECIPE_CATALOG), "utf8"); + + writeRecipeFiles(input.outDir, recipeFiles(catalog)); + writeBuildInfo(input.outDir, readBuildCommit(input.root, input.git)); +} diff --git a/src/cli/build-info.ts b/src/cli/build-info.ts new file mode 100644 index 00000000..ce191544 --- /dev/null +++ b/src/cli/build-info.ts @@ -0,0 +1,38 @@ +// The record the build leaves beside the compiled CLI, shared by the build step that writes it +// and `sdp --version`, which reads it. The CLI never runs git: the commit is whatever the build +// recorded, or `unknown`. + +/** The file name, resolved beside the compiled CLI module (`dist/cli/` in the package). */ +export const BUILD_INFO_FILE = "build-info.json"; + +export const UNKNOWN_COMMIT = "unknown"; + +/** A full Git object name: SHA-1 (40 hex digits) or SHA-256 (64). */ +const fullCommitPattern = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u; + +export function isFullCommit(value: unknown): value is string { + return typeof value === "string" && fullCommitPattern.test(value); +} + +export interface BuildInfo { + readonly commit: string; +} + +export function serializeBuildInfo(info: BuildInfo): string { + return `${JSON.stringify({ commit: info.commit })}\n`; +} + +/** Any unreadable, malformed, or off-grammar record reads as `unknown`, never as a guess. */ +export function parseBuildCommit(text: string): string { + try { + const parsed = JSON.parse(text) as unknown; + const commit = + typeof parsed === "object" && parsed !== null + ? (parsed as Record<string, unknown>).commit + : undefined; + + return isFullCommit(commit) ? commit : UNKNOWN_COMMIT; + } catch { + return UNKNOWN_COMMIT; + } +} diff --git a/src/cli/q-command.ts b/src/cli/q-command.ts index d02bb690..3fba0f43 100644 --- a/src/cli/q-command.ts +++ b/src/cli/q-command.ts @@ -50,6 +50,8 @@ export interface QueryArgs { /** `undefined` means "not supplied on argv" — stdin decides whether that is a body or a refusal. */ readonly body: string | undefined; readonly json: boolean; + /** The JSON object `--params` supplied, already parsed, or `{}` without the flag. */ + readonly params: Readonly<Record<string, unknown>>; } export interface QueryHooks { @@ -58,7 +60,12 @@ export interface QueryHooks { readonly readStdin?: () => string; } -type CompiledBody = (g: Reader, graph: GraphSchema, report: ValidationReport) => Promise<unknown>; +type CompiledBody = ( + g: Reader, + graph: GraphSchema, + report: ValidationReport, + params: unknown, +) => Promise<unknown>; type AsyncBodyConstructor = new (...parameterNamesThenBody: readonly string[]) => CompiledBody; @@ -70,14 +77,15 @@ async function asyncBodyShape(): Promise<void> { /** * The body is a plain JavaScript async function body — no `import`/`export`, no TypeScript-only * syntax — so the runner never has to resolve a module on the operator's behalf and no staleness - * switch exists to forget. `return` is the output contract. + * switch exists to forget. `return` is the output contract. The binding names, in order, are the + * scripted contract every recipe and skill depends on. */ export function compileBody(source: string): CompiledBody { const { constructor: AsyncBody } = Object.getPrototypeOf(asyncBodyShape) as { readonly constructor: AsyncBodyConstructor; }; - return new AsyncBody("g", "graph", "report", source); + return new AsyncBody("g", "graph", "report", "params", source); } function defaultIsStdinTty(): boolean { @@ -88,10 +96,82 @@ function defaultReadStdin(): string { return readFileSync(0, "utf8"); } +/** Collapses an error message onto one line, so a refusal stays the one-line diagnostic. */ +function oneLine(message: string): string { + return message.replace(/\s+/gu, " ").trim(); +} + +const queryParamsAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.query-params"), + label: "reads --params as JSON data and injects it as the sink's fourth binding, params", + satisfies: ref("spec:consumers.agent-surface.recipe-parameters"), + component: componentAnchorId("component:protocol.cli"), + role: "service", +}); +void queryParamsAnchor; + +/** How a refusal names a parsed JSON value that is not an object. */ +function jsonKind(value: unknown): string { + return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`; +} + +/** + * `--params` carries a recipe's parameter as data, never as body source: a JSON object inline, or + * `@PATH` naming a file, resolved from the working directory, that holds one. It is read and parsed + * here, before the graph derives, so a value that is not JSON, JSON that is not an object, or a + * file that cannot be read refuses the invocation before the body could run. + */ +function readParams( + value: string, + output: CliOutput, +): { readonly params: Readonly<Record<string, unknown>> } | undefined { + let text = value; + let source = "--params"; + + if (value.startsWith("@")) { + const path = value.slice(1); + + if (path === "") { + writeStderr(output, "sdp q: --params @PATH requires a path after @.\n"); + return undefined; + } + + source = `--params file ${JSON.stringify(path)}`; + + try { + // A byte-order mark is not JSON text, though an editor may save one; it is dropped. + text = readFileSync(path, "utf8").replace(/^\uFEFF/u, ""); + } catch (error) { + writeStderr( + output, + `sdp q: ${source} could not be read (${oneLine(errorMessage(error))}).\n`, + ); + return undefined; + } + } + + let parsed: unknown; + + try { + parsed = JSON.parse(text) as unknown; + } catch (error) { + writeStderr(output, `sdp q: ${source} is not valid JSON (${oneLine(errorMessage(error))}).\n`); + return undefined; + } + + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + writeStderr(output, `sdp q: ${source} must be a JSON object, got ${jsonKind(parsed)}.\n`); + return undefined; + } + + return { params: parsed as Readonly<Record<string, unknown>> }; +} + export function parseQueryArgs(args: readonly string[], output: CliOutput): QueryArgs | undefined { let root: string | undefined; let body: string | undefined; let json = false; + let rawParams: string | undefined; const rawExcludes: string[] = []; for (let index = 0; index < args.length; index += 1) { @@ -134,6 +214,29 @@ export function parseQueryArgs(args: readonly string[], output: CliOutput): Quer continue; } + if (argument === "--params") { + const value = args[index + 1]; + + if (value === undefined) { + writeStderr(output, "sdp q: --params requires a JSON object or @PATH.\n"); + return undefined; + } + + if (value.startsWith("--")) { + writeStderr(output, `sdp q: --params expects a JSON object or @PATH, got ${value}\n`); + return undefined; + } + + if (rawParams !== undefined) { + writeStderr(output, "sdp q takes at most one --params.\n"); + return undefined; + } + + rawParams = value; + index += 1; + continue; + } + if (argument.startsWith("--")) { writeStderr(output, `sdp q: unknown option ${argument}\n`); return undefined; @@ -162,7 +265,15 @@ export function parseQueryArgs(args: readonly string[], output: CliOutput): Quer const resolvedRoot = resolveExtractionRoot(root, output, "q"); - return resolvedRoot === undefined ? undefined : { root: resolvedRoot, exclude, body, json }; + if (resolvedRoot === undefined) { + return undefined; + } + + const supplied = rawParams === undefined ? { params: {} } : readParams(rawParams, output); + + return supplied === undefined + ? undefined + : { root: resolvedRoot, exclude, body, json, params: supplied.params }; } /** @@ -243,7 +354,7 @@ export async function runQuery( try { const report = validateGraph(graph); - value = await compileBody(body)(createReader(graph), graph, report); + value = await compileBody(body)(createReader(graph), graph, report, parsed.params); } catch (error) { writeStderr(output, `sdp q: ${errorMessage(error)}\n`); return 1; diff --git a/src/cli/sdp.ts b/src/cli/sdp.ts index bb77c900..ffe4ecf2 100644 --- a/src/cli/sdp.ts +++ b/src/cli/sdp.ts @@ -1,9 +1,10 @@ #!/usr/bin/env node -import { realpathSync } from "node:fs"; +import { readFileSync, realpathSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { parseBuildArgs } from "./build-args.js"; +import { BUILD_INFO_FILE, UNKNOWN_COMMIT, parseBuildCommit } from "./build-info.js"; import { runBuild } from "./build-command.js"; import { runCensus } from "./census-command.js"; import type { CensusHooks } from "./census-command.js"; @@ -25,6 +26,7 @@ import { runValidate, runView } from "./validate-view-command.js"; export const SDP_HELP_TEXT = `sdp — Libar Software Delivery Protocol Usage: sdp --help + sdp --version sdp build [root] [--exclude PATH]... [--check-clean] sdp validate [root] [--exclude PATH]... [--check-clean | --watch] sdp view [root] [--exclude PATH]... [--check-clean] @@ -33,7 +35,12 @@ Usage: sdp gherkin [root] [--exclude PATH]... [--check-clean] sdp import <path...> [--dry-run] sdp new spec PATH --id ID --kind KIND --altitude ALT --title TITLE --outcome OUTCOME - sdp q ['<body>'] [--root PATH] [--exclude PATH]... [--json] + sdp q ['<body>'] [--root PATH] [--exclude PATH]... [--params JSON | --params @PATH] [--json] + +Options: + --version Print one line, sdp <package version> (<commit>), and exit 0. The commit is the full + hash the build recorded beside the compiled CLI, or unknown; the CLI never runs git, + and neither value enters the graph or any projection. Commands: build Extract every *.sdp.ts, *.sdp.md, and *.sdp.gherkin under root (default: cwd), plus @@ -81,10 +88,14 @@ Commands: positional argument, or stdin when stdin is not a terminal; with neither, q refuses with a usage note and exits 1 rather than waiting. It is a plain JavaScript async function body — no import/export, no TypeScript-only syntax — and \`return\` is the output - contract. Three bindings are injected: \`g\`, the reader over the derived graph (the - same createReader the package exports); \`graph\`, the raw graph schema object; and + contract. Four bindings are injected: \`g\`, the reader over the derived graph (the + same createReader the package exports); \`graph\`, the raw graph schema object; \`report\`, the validation report, so honesty findings are queryable data rather than a - gate — checks never gate the read path. The graph is derived on every invocation, so a + gate — checks never gate the read path; and \`params\`, the JSON object --params + supplies, or {} without it. --params takes the JSON inline or, as @PATH, a file + holding it; a value that is not a JSON object or a file that cannot be read refuses + before the body runs. A recipe reads its parameter from \`params\` and falls back to the + catalog's sample. The graph is derived on every invocation, so a just-authored Spec is queryable immediately and no committed artifact answers in the graph's name; nothing is written anywhere. Output is bounded util.inspect (depth 4); --json prints JSON.stringify instead, unbounded. A body that throws exits 1, as does a @@ -98,6 +109,8 @@ Agent skills and reference: .agents/skills/sdp-authoring/SKILL.md the skill for authoring Specs, Packs, and anchors .agents/skills/sdp-sessions/SKILL.md the skill for routing a delivery session docs/agent-surface/recipes.md the recipe catalog: runnable q bodies + dist/recipes/NN-slug.js each catalog body as a file, run as shipped with + sdp q "$(cat PKG/dist/recipes/NN-slug.js)" CONTEXT.md the Protocol's glossary, which the skills and recipes use specs/ the Protocol's own Specs and Pack, which the skills @@ -106,9 +119,54 @@ Agent skills and reference: That graph holds intent only. The package ships no source anchors, so its delivery facts are empty and its gap warnings are not evidence about what the Protocol has realized.`; +/** Reads one file the package ships, by URL; `sdp --version` reads through it. */ +export type EngineFileReader = (url: URL) => string; + interface CliHooks extends CensusHooks, MermaidHooks, GherkinViewHooks, ValidateWatchHooks { readonly import?: ImportHooks; readonly query?: QueryHooks; + readonly readEngineFile?: EngineFileReader; +} + +export interface EngineProvenance { + readonly version: string; + readonly commit: string; +} + +const readShippedFile: EngineFileReader = (url) => readFileSync(url, "utf8"); + +/** + * The engine's identity, read from two files and never from git: the version from the + * `package.json` two directories above this module, which is the package root from `dist/cli/` and + * from `src/cli/` alike, and the commit from the record the build wrote beside the compiled CLI. + * A file that is absent or does not answer reads as `unknown`. + */ +export function readEngineProvenance(read: EngineFileReader = readShippedFile): EngineProvenance { + let version = "unknown"; + + try { + const manifest = JSON.parse(read(new URL("../../package.json", import.meta.url))) as unknown; + const value = + typeof manifest === "object" && manifest !== null + ? (manifest as Record<string, unknown>).version + : undefined; + + if (typeof value === "string" && /^\S+$/u.test(value)) { + version = value; + } + } catch { + // An unreadable manifest names no version. + } + + let commit = UNKNOWN_COMMIT; + + try { + commit = parseBuildCommit(read(new URL(`./${BUILD_INFO_FILE}`, import.meta.url))); + } catch { + // A build that recorded nothing names no commit. + } + + return { version, commit }; } /** @@ -119,6 +177,14 @@ interface CliHooks extends CensusHooks, MermaidHooks, GherkinViewHooks, Validate * @sdpRole service */ +/** + * @sdpAnchor impl:protocol.engine-provenance-cli + * @sdpLabel sdp --version names the package version and the commit the build recorded + * @sdpSatisfies spec:consumers.engine-provenance + * @sdpComponent component:protocol.cli + * @sdpRole service + */ + /** * Every verb but `q` and `validate --watch` completes synchronously. `q` awaits an * operator-supplied async body; `validate --watch` stays open until abort. The dispatcher's return @@ -137,6 +203,13 @@ export function runSdpCli( return 0; } + if (command === "--version") { + const { version, commit } = readEngineProvenance(hooks.readEngineFile); + + writeStdout(output, `sdp ${version} (${commit})\n`); + return 0; + } + if ( command !== "build" && command !== "validate" && diff --git a/src/cli/validate-watch.ts b/src/cli/validate-watch.ts index 026578d1..427921df 100644 --- a/src/cli/validate-watch.ts +++ b/src/cli/validate-watch.ts @@ -471,6 +471,13 @@ function createFilesystemWatchSource(input: { }; } +/** + * @sdpAnchor impl:protocol.validate-watch + * @sdpLabel re-runs the one validate path on every carrier change and stays alive after findings + * @sdpSatisfies spec:consumers.validate-watch + * @sdpComponent component:protocol.cli + * @sdpRole service + */ export async function runValidateWatch( parsed: BuildArgs, output: CliOutput, diff --git a/src/extract/derive.ts b/src/extract/derive.ts index 00b9a11b..1bbc471d 100644 --- a/src/extract/derive.ts +++ b/src/extract/derive.ts @@ -1,6 +1,7 @@ import { computeDeliveryFacts } from "../graph/delivery-facts.js"; import { schemaVersion } from "../graph/schema.js"; import type { + EntryLocation, AnchorNode, CodeNode, GraphEdge, @@ -72,6 +73,46 @@ function pickSections(data: Record<string, unknown>): SpecSections | undefined { return present ? sections : undefined; } +const entryLocationsAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.entry-locations"), + label: "builds the carrier entry location table", + satisfies: ref("spec:extraction.entry-locations"), + component: componentAnchorId("component:protocol.extract"), + role: "extractor", +}); +void entryLocationsAnchor; + +function deriveEntryLocations(specs: readonly ReifiedSpec[]): readonly EntryLocation[] { + return specs + .flatMap((spec) => + (spec.entryLines ?? []).map(({ entry, line }) => { + const index = /^question\[(\d+)\]$/u.exec(entry)?.[1]; + const intent = spec.data.intent; + const questions = + typeof intent === "object" && intent !== null && "openQuestions" in intent + ? intent.openQuestions + : undefined; + const question: unknown = + index !== undefined && Array.isArray(questions) ? questions[Number(index)] : undefined; + const key = + typeof question === "object" && question !== null && "key" in question + ? question.key + : undefined; + return { + spec: spec.id, + file: spec.file, + entry, + ...(typeof key === "string" ? { key } : {}), + line, + }; + }), + ) + .sort((left, right) => { + if (left.spec !== right.spec) return left.spec < right.spec ? -1 : 1; + return left.entry < right.entry ? -1 : left.entry > right.entry ? 1 : 0; + }); +} + function derivePrimitiveNode(entry: ReifiedSpec): PrimitiveNode { const title = entry.data.title; const narrative = entry.data.narrative; @@ -293,5 +334,5 @@ export function deriveGraph( return facts === undefined ? node : { ...node, deliveryFacts: facts }; }); - return { schemaVersion, nodes: decoratedNodes, edges }; + return { schemaVersion, nodes: decoratedNodes, edges, locations: deriveEntryLocations(specs) }; } diff --git a/src/extract/markdown-body-owner-behavior.ts b/src/extract/markdown-body-owner-behavior.ts index 2e58b201..ac591157 100644 --- a/src/extract/markdown-body-owner-behavior.ts +++ b/src/extract/markdown-body-owner-behavior.ts @@ -1,5 +1,6 @@ import { codeAnchorId, componentAnchorId, ref } from "../ids.js"; import { codeAnchor } from "../model/code-anchor.js"; +import type { ReifiedEntryLine } from "./reify.js"; import type { Finding } from "../validate/contracts.js"; import { parseSectionContent } from "./markdown-body-content.js"; import type { MarkdownLine } from "./markdown-body-content.js"; @@ -68,6 +69,7 @@ export function mapIntent( file: string, kind: string, findings: Finding[], + entryLines: ReifiedEntryLine[], ): void { const parsed = parseSectionContent(lines, file, findings); addDescription(section, parsed.description, file, lines[0]?.line ?? 1, findings); @@ -137,6 +139,7 @@ export function mapIntent( blocking: question[1] === "blocking", ...(key === undefined ? {} : { key }), }; + entryLines.push({ entry: `question[${String(earlier.length)}]`, line: item.line }); section.openQuestions = [...earlier, entry]; } continue; diff --git a/src/extract/markdown-body-owner-sections.ts b/src/extract/markdown-body-owner-sections.ts index 28138d08..eb43322b 100644 --- a/src/extract/markdown-body-owner-sections.ts +++ b/src/extract/markdown-body-owner-sections.ts @@ -1,3 +1,4 @@ +import type { ReifiedEntryLine } from "./reify.js"; import type { Finding } from "../validate/contracts.js"; import { parseSectionContent } from "./markdown-body-content.js"; import type { MarkdownLine } from "./markdown-body-content.js"; @@ -113,6 +114,8 @@ export function mapOpen( lines: readonly MarkdownLine[], file: string, findings: Finding[], + sectionName: string, + entryLines: ReifiedEntryLine[], ): void { const parsed = parseSectionContent(lines, file, findings); addDescription(section, parsed.description, file, lines[0]?.line ?? 1, findings); @@ -139,7 +142,11 @@ export function mapOpen( ); else if (reservedMarkdownProperties.has(entry.key)) addMarkdownFinding(findings, propertyFinding(file, item.line, entry.key)); - else section[entry.key] = entry.value; + else { + section[entry.key] = entry.value; + if (entry.key !== "description") + entryLines.push({ entry: `${sectionName}.${entry.key}`, line: item.line }); + } } } diff --git a/src/extract/markdown-body-owners.ts b/src/extract/markdown-body-owners.ts index 312423e9..957c09ca 100644 --- a/src/extract/markdown-body-owners.ts +++ b/src/extract/markdown-body-owners.ts @@ -1,3 +1,4 @@ +import type { ReifiedEntryLine } from "./reify.js"; import type { Finding } from "../validate/contracts.js"; import type { MarkdownLine } from "./markdown-body-content.js"; import { mapBehavior, mapExampleSpace, mapIntent } from "./markdown-body-owner-behavior.js"; @@ -17,10 +18,11 @@ export function mapOwner( file: string, kind: string, findings: Finding[], + entryLines: ReifiedEntryLine[], ): void { if (owner === "Intent") { const intent: Record<string, unknown> = {}; - mapIntent(intent, target, lines, file, kind, findings); + mapIntent(intent, target, lines, file, kind, findings, entryLines); target.intent = intent; return; } @@ -48,7 +50,7 @@ export function mapOwner( } if (owner === "Design" || owner === "UI") { const section: Record<string, unknown> = {}; - mapOpen(section, lines, file, findings); + mapOpen(section, lines, file, findings, owner.toLowerCase(), entryLines); target[owner.toLowerCase()] = section; return; } diff --git a/src/extract/markdown-body.ts b/src/extract/markdown-body.ts index f3da121f..16140b9b 100644 --- a/src/extract/markdown-body.ts +++ b/src/extract/markdown-body.ts @@ -6,6 +6,7 @@ import { addMarkdownFinding, capMarkdownFindings, markdownFinding } from "./mark import { mapOwner } from "./markdown-body-owners.js"; import { isUnsupportedCommonMarkBlock, normalizeProse } from "./markdown-body-content.js"; import type { MarkdownLine } from "./markdown-body-content.js"; +import type { ReifiedEntryLine } from "./reify.js"; import type { MarkdownBodyResult } from "./markdown-types.js"; const primaryOwners = new Set(["Behavior", "Rule", "Workflow", "Contract"]); @@ -199,6 +200,7 @@ export function parseMarkdownBody( else current.content.push(line); } const data: Record<string, unknown> = { title }; + const entryLines: ReifiedEntryLine[] = []; const prose = narrative(before, file, findings); if (prose.length > 0) data.narrative = prose; const ownerNames = new Set<string>(); @@ -213,11 +215,11 @@ export function parseMarkdownBody( else { ownerNames.add(name); if (primaryOwners.has(owner.name)) primarySeen = true; - mapOwner(data, owner.name, owner.content, file, kind, findings); + mapOwner(data, owner.name, owner.content, file, kind, findings, entryLines); } } const capped = capMarkdownFindings(findings, file, "extract/invalid-markdown-structure"); return capped.length === 0 - ? { ok: true, body: { data }, findings: capped } + ? { ok: true, body: { data, entryLines }, findings: capped } : { ok: false, findings: capped }; } diff --git a/src/extract/markdown-types.ts b/src/extract/markdown-types.ts index 194b6020..2dd186d4 100644 --- a/src/extract/markdown-types.ts +++ b/src/extract/markdown-types.ts @@ -1,3 +1,4 @@ +import type { ReifiedEntryLine } from "./reify.js"; import type { Finding } from "../validate/contracts.js"; export interface MarkdownFrontmatter { @@ -16,6 +17,7 @@ export type MarkdownFrontmatterResult = export interface MarkdownBody { readonly data: Record<string, unknown>; + readonly entryLines: readonly ReifiedEntryLine[]; } export type MarkdownBodyResult = diff --git a/src/extract/markdown.ts b/src/extract/markdown.ts index 207cd784..78b0bbe2 100644 --- a/src/extract/markdown.ts +++ b/src/extract/markdown.ts @@ -87,6 +87,7 @@ export function reifyMarkdownCarrier(sourceText: string, relativePath: string): id: frontmatter.frontmatter.id, file: relativePath, line: frontmatter.frontmatter.line, + entryLines: body.body.entryLines, }, ], packs: [], diff --git a/src/extract/reify.ts b/src/extract/reify.ts index 037bb46c..7e0448ee 100644 --- a/src/extract/reify.ts +++ b/src/extract/reify.ts @@ -116,12 +116,18 @@ const RESERVED_DERIVED_PROPERTIES = new Set<string>([ "models", ]); +export interface ReifiedEntryLine { + readonly entry: string; + readonly line: number; +} + export interface ReifiedSpec { /** Plain `Spec`-shaped data in authored property order — built from the AST, never evaluated. */ readonly data: Record<string, unknown>; readonly id: string; readonly file: string; readonly line: number; + readonly entryLines?: readonly ReifiedEntryLine[]; } export interface ReifiedPack { @@ -1051,6 +1057,48 @@ function firstPropertyNamed(node: Node, name: string): PropertyAssignment | unde ); } +function typeScriptEntryLines( + objectLiteral: Node, + data: Record<string, unknown>, +): readonly ReifiedEntryLine[] { + const lines: ReifiedEntryLine[] = []; + for (const sectionName of ["design", "ui"]) { + const section = data[sectionName]; + const initializer = firstPropertyNamed(objectLiteral, sectionName)?.getInitializer(); + if (!isUnknownRecord(section) || initializer === undefined) continue; + for (const key of Object.keys(section)) { + if (key === "description") continue; + const property = firstPropertyNamed(initializer, key); + if (property !== undefined) { + lines.push({ entry: `${sectionName}.${key}`, line: property.getStartLineNumber() }); + } + } + } + const intent = data.intent; + const intentNode = firstPropertyNamed(objectLiteral, "intent")?.getInitializer(); + const questionsNode = + intentNode === undefined + ? undefined + : firstPropertyNamed(intentNode, "openQuestions")?.getInitializer(); + const array = questionsNode === undefined ? undefined : unwrapTransparent(questionsNode); + if ( + isUnknownRecord(intent) && + Array.isArray(intent.openQuestions) && + array !== undefined && + Node.isArrayLiteralExpression(array) + ) { + for (const [index, element] of array.getElements().entries()) { + if (index >= intent.openQuestions.length) break; + const question = firstPropertyNamed(element, "question"); + lines.push({ + entry: `question[${String(index)}]`, + line: (question ?? element).getStartLineNumber(), + }); + } + } + return lines; +} + /** The line of `openQuestions[<index>].key` in an authored Intent, or the Intent's own line. */ function openQuestionKeyLine(intentNode: Node, index: number): number { const fallback = intentNode.getStartLineNumber(); @@ -1682,6 +1730,7 @@ function reifySpecCall( id: data.id as string, file, line: call.getStartLineNumber(), + entryLines: typeScriptEntryLines(objectLiteral, data), }, findings, }; diff --git a/src/extract/serialize.ts b/src/extract/serialize.ts index 5b16acd5..3589ae62 100644 --- a/src/extract/serialize.ts +++ b/src/extract/serialize.ts @@ -259,6 +259,18 @@ export function serializeGraph(graph: GraphSchema): string { schemaVersion: graph.schemaVersion, nodes: [...graph.nodes].sort(compareNodes).map(canonicalNode), edges: [...graph.edges].sort(compareEdges).map(canonicalEdge), + locations: [...(graph.locations ?? [])] + .sort( + (left, right) => + compareCodeUnits(left.spec, right.spec) || compareCodeUnits(left.entry, right.entry), + ) + .map((location) => ({ + spec: location.spec, + file: location.file, + entry: location.entry, + ...(location.key === undefined ? {} : { key: location.key }), + line: location.line, + })), }; return `${JSON.stringify(canonical, null, 2)}\n`; diff --git a/src/graph/schema.ts b/src/graph/schema.ts index 68237cf5..e7001793 100644 --- a/src/graph/schema.ts +++ b/src/graph/schema.ts @@ -4,7 +4,7 @@ import type { CodeAnchorLayer } from "../model/anchors.js"; import type { SpecAltitude, SpecKind, SpecReadiness } from "../model/descriptors.js"; import type { SpecSections } from "../model/sections.js"; -export const schemaVersion = "0.8.0" as const; +export const schemaVersion = "0.9.0" as const; const schemaVersionAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.schema-version"), @@ -166,8 +166,26 @@ export interface GraphEdge { readonly claim: GraphClaim; } +/** + * One row of the location table (`spec:extraction.entry-locations`): where an addressable entry + * of a Spec is written. `entry` is `design.<key>`, `ui.<key>`, or `question[<index>]` by the + * question's position in the Spec's open questions, with `key` beside it when the question carries + * one. The table rides beside the nodes, so Primitive and Pack nodes stay line-free. + */ +export interface EntryLocation { + readonly spec: string; + /** Extraction-root-relative, POSIX separators, no leading `./` — never absolute (JS-C3). */ + readonly file: string; + readonly entry: string; + readonly key?: string; + /** 1-based. */ + readonly line: number; +} + export interface GraphSchema { readonly schemaVersion: typeof schemaVersion; readonly nodes: readonly GraphNode[]; readonly edges: readonly GraphEdge[]; + /** Absent reads as empty: no entry is located. */ + readonly locations?: readonly EntryLocation[]; } diff --git a/src/projections/design-review-markdown.ts b/src/projections/design-review-markdown.ts index f71cc38b..436bde81 100644 --- a/src/projections/design-review-markdown.ts +++ b/src/projections/design-review-markdown.ts @@ -11,6 +11,14 @@ function directoryOf(pagePath: string): readonly string[] { return pagePath.split("/").slice(0, -1); } +/** Percent-encode path segments for a Markdown link target, keeping directory separators. */ +function encodeLinkTarget(path: string): string { + return path + .split("/") + .map((segment) => encodeURIComponent(segment).replaceAll("(", "%28").replaceAll(")", "%29")) + .join("/"); +} + /** Relative link between two view pages. */ export function pageHref(fromPage: string, toPage: string): string { const fromDirectory = directoryOf(fromPage); @@ -21,13 +29,18 @@ export function pageHref(fromPage: string, toPage: string): string { shared += 1; } - return `${"../".repeat(fromDirectory.length - shared)}${toParts.slice(shared).join("/")}`; + return `${"../".repeat(fromDirectory.length - shared)}${encodeLinkTarget(toParts.slice(shared).join("/"))}`; } /** Relative link from a view page to a repo file recorded in the graph (root-relative, JS-C3). */ export function sourceHref(fromPage: string, file: string): string { // Up out of the page's directories, then out of `design-review/` and `generated/`. - return `${"../".repeat(directoryOf(fromPage).length + 2)}${file}`; + return `${"../".repeat(directoryOf(fromPage).length + 2)}${encodeLinkTarget(file)}`; +} + +/** A value on one line: each whitespace run that holds a line break becomes one space. */ +export function singleLine(text: string): string { + return text.replaceAll(/\s+/gu, (run) => (/[\r\n]/u.test(run) ? " " : run)); } /** One-line table cell: field escaping plus collapsed newlines keeps content inside the table. */ diff --git a/src/projections/design-review-pack-sections.ts b/src/projections/design-review-pack-sections.ts new file mode 100644 index 00000000..9e06ada9 --- /dev/null +++ b/src/projections/design-review-pack-sections.ts @@ -0,0 +1,320 @@ +import type { + CodeUnitBinding, + PackBoundaryRow, + PackContext, + PackMemberSummary, +} from "../reader/reader.js"; +import type { ReadinessFloorTarget } from "../validate/readiness-floor.js"; +import { escapeRenderedField } from "./owned-prose.js"; +import { + pageHref, + pagePathOf, + renderInlineCode, + renderTableInlineCode, + singleLine, + sourceHref, + tableCell, +} from "./design-review-markdown.js"; + +/** + * The Pack page's design sections, rendered from the Pack design the reader assembles: what + * holds each member below its next rung, the boundary, the open questions, and the code. + * + * @sdpAnchor impl:protocol.pack-design-sections + * @sdpLabel renders the Pack page's next-rung, boundary, question and code sections + * @sdpReferences spec:consumers.design-review.pack-design-page + * @sdpComponent component:protocol.projections + * @sdpRole renderer + */ + +/** A relative link from a view page to one line of a repository file. */ +export function lineHref(page: string, file: string, line: number): string { + return `${sourceHref(page, file)}#L${String(line)}`; +} + +/** An authored value inside a heading or one list item: escaped, its line breaks collapsed. */ +function inlineText(text: string): string { + return escapeRenderedField(singleLine(text)); +} + +/** A link label: inline text whose brackets are escaped, so a `]` cannot close the label. */ +function linkLabel(text: string): string { + return inlineText(text).replaceAll("[", "\\[").replaceAll("]", "\\]"); +} + +/** The linked Spec id, then its title. */ +function specLink(page: string, id: string, title: string | undefined): string { + const display = title === undefined ? "" : ` ${inlineText(title)}`; + + return `[\`${id}\`](${pageHref(page, pagePathOf(id))})${display}`; +} + +/** Resolved members once each, in authored order: the sections group by member. */ +function resolvedMembers(context: PackContext): readonly PackMemberSummary[] { + const seen = new Set<string>(); + + return context.members.filter((member) => { + if (!member.resolved || seen.has(member.id)) { + return false; + } + + seen.add(member.id); + return true; + }); +} + +function renderTarget(page: string, target: ReadinessFloorTarget): string { + return target.statedReadiness === undefined + ? `${target.type} \`${target.id}\`, not a Spec` + : `${target.type} ${specLink(page, target.id, undefined)}, stated \`${target.statedReadiness}\``; +} + +export function renderNextRungSection(context: PackContext, page: string): readonly string[] { + const lines = [ + "## Next rung", + "", + "The floor clauses that fail at the rung above each member's stated rung, with the targets a failing dependency clause names. This is a report, never a promotion: only an author's edit states a rung.", + "", + ]; + const withNext = resolvedMembers(context).filter((member) => member.statedNextRung !== undefined); + const held = withNext.filter((member) => (member.statedNextRungFailures ?? []).length > 0); + const waiting = withNext.filter((member) => (member.statedNextRungFailures ?? []).length === 0); + + if (held.length === 0) { + lines.push("No member is held below its next rung."); + } + + // One group per held member, as the question and code sections group; a list line never opens + // with a member link, which the verifier gap list owns. + for (const [position, member] of held.entries()) { + lines.push( + ...(position === 0 ? [] : [""]), + `### ${specLink(page, member.id, member.title)}`, + "", + `Stated \`${member.statedReadiness ?? ""}\`; the floor does not hold \`${member.statedNextRung ?? ""}\`:`, + "", + ); + + for (const failure of member.statedNextRungFailures ?? []) { + lines.push(`- ${renderInlineCode(failure.clauseId)} — ${inlineText(failure.description)}`); + + for (const target of failure.targets ?? []) { + lines.push(` - ${renderTarget(page, target)}`); + } + } + } + + if (waiting.length > 0) { + lines.push( + "", + "### Waiting for the author's statement", + "", + "The floor already holds these members at their next rung. That is not approval: the page records no review, and the rung stays unstated until its author states it.", + "", + ); + + for (const member of waiting) { + lines.push( + `- stated \`${member.statedReadiness ?? ""}\`, next \`${member.statedNextRung ?? ""}\`: ${specLink(page, member.id, member.title)}`, + ); + } + } + + return lines; +} + +function renderBoundaryTable(rows: readonly PackBoundaryRow[], page: string): readonly string[] { + if (rows.length === 0) { + return ["None."]; + } + + const lines = [ + "| Spec | Stated | Implementation binding | Members joined |", + "|---|---|---|---|", + ]; + + for (const row of rows) { + const joined = row.via + .map( + (via) => + `${via.type}: ${via.members.map((member) => `[\`${member}\`](${pageHref(page, pagePathOf(member))})`).join(", ")}`, + ) + .join(" · "); + lines.push( + `| [\`${row.id}\`](${pageHref(page, pagePathOf(row.id))}) ${tableCell(row.title ?? "")} | ${row.statedReadiness} | ${row.implemented ? "present" : "none"} | ${joined} |`, + ); + } + + return lines; +} + +export function renderBoundarySection(context: PackContext, page: string): readonly string[] { + return [ + "## Boundary", + "", + "### Rests on", + "", + "The Specs outside the Pack that a member refines, depends on, is constrained by or is decided by.", + "", + ...renderBoundaryTable(context.boundary.restsOn, page), + "", + "### Rested on by", + "", + "The Specs outside the Pack that refine, depend on, are constrained by or are decided by a member.", + "", + ...renderBoundaryTable(context.boundary.restedOnBy, page), + "", + "### Verified by", + "", + "The Specs outside the Pack that verify a member.", + "", + ...renderBoundaryTable(context.boundary.verifiedBy, page), + "", + "### Superseded by", + "", + "The Specs outside the Pack that supersede a member.", + "", + ...renderBoundaryTable(context.boundary.supersededBy, page), + ]; +} + +export function renderOpenQuestionsSection(context: PackContext, page: string): readonly string[] { + const lines = ["## Open questions", ""]; + const asking = resolvedMembers(context).filter( + (member) => (member.design?.openQuestions ?? []).length > 0, + ); + + if (asking.length === 0) { + lines.push("No member records an open question."); + return lines; + } + + for (const [position, member] of asking.entries()) { + lines.push( + ...(position === 0 ? [] : [""]), + `### ${specLink(page, member.id, member.title)}`, + "", + ); + + for (const question of member.design?.openQuestions ?? []) { + const flag = question.blocking ? "**blocking**" : "non-blocking"; + const address = + question.key === undefined + ? "" + : ` · ${renderInlineCode(`${member.id}#question.${question.key}`)}`; + const location = + question.line === undefined || member.file === undefined + ? "" + : ` ([${linkLabel(member.file)}:${String(question.line)}](${lineHref(page, member.file, question.line)}))`; + lines.push(`- ${flag}${address} — ${inlineText(question.question)}${location}`); + } + } + + return lines; +} + +/** One unit: id, label, a line link, its role and own structure, and its component's. */ +function renderUnit(binding: CodeUnitBinding, page: string): string { + const label = binding.label === undefined ? "" : ` — ${inlineText(binding.label)}`; + const location = + binding.file === undefined + ? "" + : binding.line === undefined + ? ` ([${linkLabel(binding.file)}](${sourceHref(page, binding.file)}))` + : ` ([${linkLabel(binding.file)}:${String(binding.line)}](${lineHref(page, binding.file, binding.line)}))`; + const own = ( + [ + ["role", binding.role], + ["layer", binding.layer], + ["context", binding.context], + ] as const + ).flatMap(([name, value]) => (value === undefined ? [] : [`${name} ${renderInlineCode(value)}`])); + const component = binding.component; + const componentStructure = + component === undefined + ? [] + : ( + [ + ["layer", component.layer], + ["context", component.context], + ] as const + ).flatMap(([name, value]) => + value === undefined ? [] : [`${name} ${renderInlineCode(value)}`], + ); + const attributes = [ + ...own, + ...(component === undefined + ? [] + : [ + `component \`${component.id}\`${componentStructure.length === 0 ? "" : ` (${componentStructure.join(", ")})`}`, + ]), + ].join(" · "); + + return `- \`${binding.codeId}\`${label}${location}${attributes === "" ? "" : ` · ${attributes}`} \`[${binding.claim}]\``; +} + +export function renderCodeSection(context: PackContext, page: string): readonly string[] { + const lines = [ + "## Code", + "", + "The code units that realize each member, and those that answer to its design without claiming to realize it, each with its role and its component's layer and context. A reference confers no implementation binding.", + ]; + + for (const member of resolvedMembers(context)) { + const implementations = member.implementations ?? []; + const references = member.references ?? []; + lines.push("", `### ${specLink(page, member.id, member.title)}`, ""); + + if (implementations.length === 0 && references.length === 0) { + lines.push("No implementation or design-reference binding is recorded for this member."); + continue; + } + + if (implementations.length > 0) { + lines.push( + "#### Implementations", + "", + ...implementations.map((unit) => renderUnit(unit, page)), + ); + } + + if (references.length > 0) { + lines.push( + ...(implementations.length > 0 ? [""] : []), + "#### Referenced by", + "", + ...references.map((unit) => renderUnit(unit, page)), + ); + } + } + + return lines; +} + +/** The member table's stated-next-rung cell: `nothing blocks`, the first unmet clause, or none. */ +export function renderStatedNextRungCell(member: PackMemberSummary): string { + if (member.statedNextRung === undefined) { + return "—"; + } + + const first = member.statedNextRungFailures?.[0]; + + return `${member.statedNextRung} · ${first === undefined ? "nothing blocks" : renderTableInlineCode(first.clauseId)}`; +} + +/** The member table's decisions cell: each decision linked with its stated rung. */ +export function renderDecisionsCell(member: PackMemberSummary, page: string): string { + const decisions = member.design?.decisions ?? []; + + if (decisions.length === 0) { + return "—"; + } + + return decisions + .map((decision) => + decision.resolved + ? `[\`${decision.id}\`](${pageHref(page, pagePathOf(decision.id))}) (${decision.statedReadiness ?? ""})` + : `\`${decision.id}\` (unresolved)`, + ) + .join(", "); +} diff --git a/src/projections/design-review-pages.ts b/src/projections/design-review-pages.ts index 619f0ba4..eed940dc 100644 --- a/src/projections/design-review-pages.ts +++ b/src/projections/design-review-pages.ts @@ -1,5 +1,19 @@ -import type { PackContext, Reader, SpecContext, SpecSummary } from "../reader/reader.js"; +import type { + PackContext, + PackMemberSummary, + Reader, + SpecContext, + SpecSummary, +} from "../reader/reader.js"; import { escapeRenderedField, renderNarrative } from "./owned-prose.js"; +import { + renderBoundarySection, + renderCodeSection, + renderDecisionsCell, + renderNextRungSection, + renderOpenQuestionsSection, + renderStatedNextRungCell, +} from "./design-review-pack-sections.js"; import type { DesignReviewPage } from "./design-review.js"; import { renderBindings, @@ -12,6 +26,7 @@ import { PAGE_FOOTER, pageHref, pagePathOf, + singleLine, sourceHref, tableCell, } from "./design-review-markdown.js"; @@ -46,6 +61,61 @@ export function renderSpecPage(context: SpecContext): DesignReviewPage { return { path: page, content: `${lines.join("\n")}\n` }; } +const MEMBER_TABLE_HEADER = [ + "Spec", + "Kind", + "Altitude", + "Stated", + "Floor reached", + "Stated next rung", + "Design entries", + "Declarations", + "Open questions", + "Decisions", + "Design reference", + "Implementation binding", + "Verifier binding", +] as const; + +/** + * One member row, numbered from 1 in authored order. The row opens with the linked Spec and + * closes with the two binding columns, which read present or none, beside a design-reference + * column with the same values; unit counts stay off the table. + */ +function renderMemberRow(member: PackMemberSummary, number: number, page: string): string { + const ordinal = `(${String(number)})`; + + if (!member.resolved) { + return `| \`${member.id}\` ${ordinal} — **unresolved** (see findings) |${" — |".repeat(MEMBER_TABLE_HEADER.length - 1)}`; + } + + const link = `[\`${member.id}\`](${pageHref(page, pagePathOf(member.id))})`; + const title = member.title === undefined ? "" : ` ${tableCell(member.title)}`; + const present = (fact: "implemented" | "has-verifier"): string => + member.deliveryFacts.includes(fact) ? "present" : "none"; + const design = member.design; + const questions = design?.openQuestions.length ?? 0; + const cells = [ + `${link} ${ordinal}${title}`, + member.specKind ?? "—", + member.altitude ?? "—", + member.statedReadiness ?? "—", + member.derivedReadiness ?? "none", + renderStatedNextRungCell(member), + String(design?.entries ?? 0), + String(design?.declarations ?? 0), + questions === 0 + ? "0" + : `${String(questions)} (${String(design?.blockingQuestions ?? 0)} blocking)`, + renderDecisionsCell(member, page), + (member.references ?? []).length > 0 ? "present" : "none", + present("implemented"), + present("has-verifier"), + ]; + + return `| ${cells.join(" | ")} |`; +} + /** * @sdpAnchor impl:protocol.binding-language-pack-table * @sdpLabel renders Pack member implementation and verifier bindings as present or none @@ -54,13 +124,27 @@ export function renderSpecPage(context: SpecContext): DesignReviewPage { * @sdpRole renderer */ +/** + * The Pack page presents the Pack's design in a fixed order: title line, framing and vocabulary, + * then the member table, the next rung, the boundary, the open questions, the code, the verifier + * coverage gaps and the findings. Every value comes from the Pack context; no timestamp, commit + * or run identity enters, so two renders of one graph are byte-identical. + * + * @sdpAnchor impl:protocol.pack-design-page + * @sdpLabel renders the Pack page from the Pack design + * @sdpSatisfies spec:consumers.design-review.pack-design-page + * @sdpReferences spec:decisions.pack-design-page + * @sdpComponent component:protocol.projections + * @sdpRole renderer + */ + export function renderPackPage( context: PackContext, specLabel: (id: string) => string, ): DesignReviewPage { const page = pagePathOf(context.id); const lines = [ - heading(context.title, context.id), + heading(context.title === undefined ? undefined : singleLine(context.title), context.id), "", `\`${context.id}\` · Pack (the grouping / review aggregate — states no truth of its own) · authored in [${escapeRenderedField(context.file)}](${sourceHref(page, context.file)}) \`[declared]\``, ]; @@ -69,32 +153,35 @@ export function renderPackPage( lines.push("", `> ${escapeRenderedField(context.framing)}`); } + if (context.modelRefs.length > 0) { + const refs = context.modelRefs.map((ref) => specLabel(ref)).join(" · "); + lines.push("", `**Vocabulary (\`modelRefs\`):** ${refs}`); + } + lines.push( "", "## Members", "", - "| Spec | Kind | Altitude | Stated | Floor reached | Implementation binding | Verifier binding |", - "|---|---|---|---|---|---|---|", + "One row per member in the manifest's order. The stated next rung is the rung above the stated one, with `nothing blocks` when the floor already holds it and its first unmet clause otherwise. A binding or design reference reads present when a resolving anchor exists, and none otherwise.", + "", + `| ${MEMBER_TABLE_HEADER.join(" | ")} |`, + `|${"---|".repeat(MEMBER_TABLE_HEADER.length)}`, ); - for (const member of context.members) { - if (!member.resolved) { - lines.push(`| \`${member.id}\` — **unresolved** (see findings) | — | — | — | — | — | — |`); - continue; - } - - const link = `[\`${member.id}\`](${pageHref(page, pagePathOf(member.id))})`; - const present = (fact: "implemented" | "has-verifier"): string => - member.deliveryFacts.includes(fact) ? "present" : "none"; - lines.push( - `| ${link} ${tableCell(member.title ?? "")} | ${member.specKind ?? "—"} | ${member.altitude ?? "—"} | ${member.statedReadiness ?? "—"} | ${member.derivedReadiness ?? "none"} | ${present("implemented")} | ${present("has-verifier")} |`, - ); + for (const [position, member] of context.members.entries()) { + lines.push(renderMemberRow(member, position + 1, page)); } - if (context.modelRefs.length > 0) { - const refs = context.modelRefs.map((ref) => specLabel(ref)).join(" · "); - lines.push("", `**Vocabulary (\`modelRefs\`):** ${refs}`); - } + lines.push( + "", + ...renderNextRungSection(context, page), + "", + ...renderBoundarySection(context, page), + "", + ...renderOpenQuestionsSection(context, page), + "", + ...renderCodeSection(context, page), + ); const gaps = context.verifierGaps; diff --git a/src/projections/design-review.ts b/src/projections/design-review.ts index 7ec8de74..181a982e 100644 --- a/src/projections/design-review.ts +++ b/src/projections/design-review.ts @@ -1,6 +1,6 @@ import type { Reader } from "../reader/reader.js"; import { escapeRenderedField } from "./owned-prose.js"; -import { pageHref, pagePathOf } from "./design-review-markdown.js"; +import { pageHref, pagePathOf, singleLine } from "./design-review-markdown.js"; import { renderIndexPage, renderPackPage, renderSpecPage } from "./design-review-pages.js"; /** @@ -60,7 +60,7 @@ export function renderDesignReview(reader: Reader): readonly DesignReviewPage[] return known === undefined ? `\`${id}\`` - : `[\`${id}\`](${pageHref(page, pagePathOf(id))})${known.title === undefined ? "" : ` — ${escapeRenderedField(known.title)}`}`; + : `[\`${id}\`](${pageHref(page, pagePathOf(id))})${known.title === undefined ? "" : ` — ${escapeRenderedField(singleLine(known.title))}`}`; }; }; diff --git a/src/reader/reader.ts b/src/reader/reader.ts index 26be3271..c51e6496 100644 --- a/src/reader/reader.ts +++ b/src/reader/reader.ts @@ -6,7 +6,9 @@ import { import { isResolvingOracleModel } from "../graph/oracle-bindings.js"; import { authoredEdgeTypes } from "../graph/schema.js"; import type { + AuthoredEdgeType, DeliveryFactName, + EntryLocation, GraphClaim, GraphEdge, GraphEdgeType, @@ -68,11 +70,22 @@ export interface RelationEnd { readonly otherTitle?: string; } +/** + * The component a code unit belongs to: the target of its `memberOf` edge, with the layer and + * context that component's anchor states. An unresolved target keeps its id and carries neither. + */ +export interface CodeUnitComponent { + readonly id: string; + readonly layer?: CodeAnchorLayer; + readonly context?: string; +} + /** * A code unit at the source end of a binding edge, decoded to its source location and its * structural attributes (`spec:decisions.architectural-annotation`): `role` on any code unit, - * `layer` and `context` on a `component:` unit. The attributes are carried as recorded; none of - * them confers anything. + * `layer` and `context` on a `component:` unit, and the component the unit belongs to + * (`spec:consumers.pack-design`). The attributes are carried as recorded; none of them confers + * anything. */ export interface CodeUnitBinding { readonly codeId: string; @@ -83,6 +96,7 @@ export interface CodeUnitBinding { readonly role?: string; readonly layer?: CodeAnchorLayer; readonly context?: string; + readonly component?: CodeUnitComponent; } /** A code binding (`satisfies` from a `CodeNode`) decoded to its source location. */ @@ -125,6 +139,8 @@ export interface OracleBinding { /** The irreducible per-spec join — what a Design Review renders and an agent starts from. */ export interface SpecContext extends SpecSummary { readonly sections?: SpecSections; + /** Derived design counts and lists, shared with the Pack member's design. */ + readonly designColumns: PackMemberDesign; readonly floorFailures: readonly ReadinessFloorFailure[]; /** * The unmet clauses of the rung above derived readiness (`idea` when no rung derives), by the @@ -141,10 +157,59 @@ export interface SpecContext extends SpecSummary { readonly references: readonly ReferenceBinding[]; readonly verifiers: readonly VerifierBinding[]; readonly oracle?: OracleBinding; + /** + * The location-table rows of this Spec (`spec:extraction.entry-locations`), sorted by entry in + * code-unit order: where each keyed entry and open question is written. Empty when the graph + * locates none; a missing row reads as not located, never as absent. + */ + readonly entryLocations: readonly EntryLocation[]; /** The graph findings naming this spec (as subject or related) — the holes beside the assertions. */ readonly findings: readonly Finding[]; } +/** One open question of a Pack member, with its position, flag, key and recorded line. */ +export interface PackMemberQuestion { + /** The question's position among the Spec's authored open questions, from 0. */ + readonly index: number; + readonly question: string; + readonly blocking: boolean; + readonly key?: string; + /** The line the location table records for `question[<index>]`; absent when not located. */ + readonly line?: number; +} + +/** A decision a Pack member names through `decidedBy`; unresolved when the target is no Spec. */ +export interface PackMemberDecision { + readonly id: string; + readonly title?: string; + readonly statedReadiness?: SpecReadiness; + readonly resolved: boolean; +} + +/** + * A Pack member's design columns (`spec:consumers.pack-design`): counts and lists read from the + * graph, never a score or a rung. `entries` counts the keyed `design` and `ui` entries other than + * `description`; `declarations` counts the `design` entries whose value opens with a code span + * (the pinned-declaration rule of `spec:extraction.contract-declarations`). + */ +export interface PackMemberDesign { + readonly entries: number; + readonly declarations: number; + readonly blockingQuestions: number; + readonly openQuestions: readonly PackMemberQuestion[]; + readonly decisions: readonly PackMemberDecision[]; +} + +/** A count beside how many of the counted bindings are enabled. */ +export interface EnabledCount { + readonly total: number; + readonly enabled: number; +} + +/** + * A Pack member row. A resolved member carries its design (`spec:consumers.pack-design`); an + * unresolved member keeps `id`, `resolved: false` and empty `deliveryFacts`, and nothing else. + */ export interface PackMemberSummary { readonly id: string; /** False when the manifest names a member absent from the graph. */ @@ -155,6 +220,53 @@ export interface PackMemberSummary { readonly statedReadiness?: SpecReadiness; readonly derivedReadiness?: SpecReadiness; readonly deliveryFacts: readonly DeliveryFactName[]; + readonly file?: string; + /** The rung above the stated rung; absent when the member states `ready`. */ + readonly statedNextRung?: SpecReadiness; + /** + * The floor's unmet clauses with the stated next rung as target, by the one evaluator. Empty + * when the floor already holds that rung, a rung that waits for its author and never a + * promotion, or when there is no next rung. `SpecContext.nextRungFailures` reads the rung above + * the floor reached instead; the two readings never share a name. + */ + readonly statedNextRungFailures?: readonly ReadinessFloorFailure[]; + readonly design?: PackMemberDesign; + readonly implementations?: readonly ImplementationBinding[]; + readonly references?: readonly ReferenceBinding[]; + /** Every decoded verifier, and how many are enabled. */ + readonly verifiers?: EnabledCount; + /** The example-kind Specs that declare `verifies` on the member, and how many are enabled. */ + readonly examples?: EnabledCount; +} + +/** The members an outside Spec joins under one authored relation type, in authored member order. */ +export interface PackBoundaryVia { + readonly type: AuthoredEdgeType; + readonly members: readonly string[]; +} + +/** A Spec outside the Pack joined to members by authored relations. */ +export interface PackBoundaryRow { + readonly id: string; + readonly title?: string; + readonly statedReadiness: SpecReadiness; + /** Whether the outside Spec carries the `implemented` delivery fact. */ + readonly implemented: boolean; + /** One entry per relation type, in the closed list's authored order. */ + readonly via: readonly PackBoundaryVia[]; +} + +/** + * The Specs outside the Pack joined to a member (`spec:consumers.pack-design`): `restsOn` lists + * those a member `refines`, `dependsOn`, is `constrainedBy` or `decidedBy`; `restedOnBy` lists + * those relating to a member by the same four types. `verifiedBy` and `supersededBy` list + * inbound `verifies` and `supersedes` separately. Rows sort by id in code-unit order. + */ +export interface PackBoundary { + readonly restsOn: readonly PackBoundaryRow[]; + readonly restedOnBy: readonly PackBoundaryRow[]; + readonly verifiedBy: readonly PackBoundaryRow[]; + readonly supersededBy: readonly PackBoundaryRow[]; } /** A member with no verifier binding — `ready` ones are the priority slice (JS-G4). */ @@ -164,10 +276,11 @@ export interface PackVerifierGap { readonly priority: boolean; } -/** The pack reviewed as a unit: members, vocabulary refs, and the verifier gaps. */ +/** The pack reviewed as a unit: members, vocabulary refs, the verifier gaps, and its boundary. */ export interface PackContext extends PackSummary { readonly members: readonly PackMemberSummary[]; readonly verifierGaps: readonly PackVerifierGap[]; + readonly boundary: PackBoundary; readonly findings: readonly Finding[]; } @@ -385,6 +498,205 @@ function matchSections(sections: SpecSections | undefined, needle: string): read return matched.sort(compareCodeUnits); } +/* ----- the Pack design (`spec:consumers.pack-design`) ----- */ + +/** A value that opens with a code span, by recipe 24's rule: a declaration as authored. */ +const pinnedDeclaration = /^(`+)(?!`)([^\r\n]+?)(?<!`)\1(?!`)/u; + +/** The relation types by which a member rests on a Spec outside its Pack. */ +const restsOnTypes: ReadonlySet<string> = new Set([ + "refines", + "dependsOn", + "constrainedBy", + "decidedBy", +]); + +function asPlainRecord(value: unknown): Readonly<Record<string, unknown>> | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Readonly<Record<string, unknown>>) + : undefined; +} + +/** The keyed entries of an open section (`design`, `ui`), `description` excluded. */ +function keyedEntries(content: unknown): readonly (readonly [string, unknown])[] { + return Object.entries(asPlainRecord(content) ?? {}).filter(([key]) => key !== "description"); +} + +/** The rung above a stated rung; none above `ready`, and none for an unratified statement. */ +function statedNextRungOf(stated: SpecReadiness): SpecReadiness | undefined { + const position = SPEC_READINESS.indexOf(stated); + + return position < 0 ? undefined : SPEC_READINESS[position + 1]; +} + +/** + * The file's Pack design code: `specDesignColumns` reads a member's design columns and + * `packBoundary` the Pack's boundary, and `packContext` in `createReader` joins them with each + * member's stated next rung, bindings and verifier counts. The evaluator call sits in + * `createReader`, whose anchor declares it. + * + * @sdpAnchor impl:protocol.pack-design + * @sdpLabel the Pack design helpers and the Pack context assembly they serve + * @sdpSatisfies spec:consumers.pack-design + * @sdpReferences spec:extraction.entry-locations, spec:extraction.contract-declarations + * @sdpComponent component:protocol.reader + * @sdpRole reader + */ +function specDesignColumns( + node: PrimitiveNode, + index: GraphIndex, + locations: readonly EntryLocation[], +): PackMemberDesign { + const designEntries = keyedEntries(node.sections?.design); + const lineOf = new Map(locations.map((row) => [row.entry, row.line])); + const authored: unknown = node.sections?.intent?.openQuestions; + const openQuestions: PackMemberQuestion[] = []; + + // Malformed entries are the validators' finding; they keep their position and are skipped. + (Array.isArray(authored) ? (authored as readonly unknown[]) : []).forEach((entry, position) => { + const record = typeof entry === "string" ? undefined : asPlainRecord(entry); + const question = typeof entry === "string" ? entry : record?.question; + + if (typeof question !== "string" || question.trim().length === 0) { + return; + } + + const key = record?.key; + const line = lineOf.get(`question[${String(position)}]`); + openQuestions.push({ + index: position, + question, + blocking: record?.blocking === true, + ...(typeof key === "string" ? { key } : {}), + ...(line === undefined ? {} : { line }), + }); + }); + + const decisionIds = new Set( + (index.edgesByFrom.get(node.id) ?? []) + .filter((edge) => edge.type === "decidedBy" && edge.claim === "declared") + .map((edge) => edge.to), + ); + const decisions = [...decisionIds].sort(compareCodeUnits).map((id): PackMemberDecision => { + const target = index.primitivesById.get(id); + + return target === undefined + ? { id, resolved: false } + : { + id, + ...(target.title === undefined ? {} : { title: target.title }), + statedReadiness: target.readiness, + resolved: true, + }; + }); + + return { + entries: designEntries.length + keyedEntries(node.sections?.ui).length, + declarations: designEntries.filter( + ([, value]) => typeof value === "string" && pinnedDeclaration.test(value), + ).length, + blockingQuestions: openQuestions.filter((entry) => entry.blocking).length, + openQuestions, + decisions, + }; +} + +/** + * The Specs outside the Pack joined to a resolved member by a declared authored relation whose + * other end is a Spec in the graph. Members in each `via` keep authored order; `via` follows the + * closed relation list's order; rows sort by id. + */ +function packBoundary( + node: PackNode, + index: GraphIndex, + implementedOf: (id: string) => boolean, +): PackBoundary { + const inside = new Set(node.members); + // A Set keeps insertion order and admits each member once in constant time, so a Spec many + // members share costs one step per member, not one scan of the members joined so far. + const restsOn = new Map<string, Map<AuthoredEdgeType, Set<string>>>(); + const restedOnBy = new Map<string, Map<AuthoredEdgeType, Set<string>>>(); + const verifiedBy = new Map<string, Map<AuthoredEdgeType, Set<string>>>(); + const supersededBy = new Map<string, Map<AuthoredEdgeType, Set<string>>>(); + + const join = ( + rows: Map<string, Map<AuthoredEdgeType, Set<string>>>, + outsideId: string, + type: AuthoredEdgeType, + memberId: string, + ): void => { + const byType = rows.get(outsideId) ?? new Map<AuthoredEdgeType, Set<string>>(); + const members = byType.get(type) ?? new Set<string>(); + + members.add(memberId); + byType.set(type, members); + rows.set(outsideId, byType); + }; + + const isAuthored = (edge: GraphEdge): edge is GraphEdge & { type: AuthoredEdgeType } => + edge.claim === "declared" && authoredEdgeTypeSet.has(edge.type); + + for (const memberId of new Set(node.members)) { + if (!index.primitivesById.has(memberId)) { + continue; + } + + for (const edge of index.edgesByFrom.get(memberId) ?? []) { + if ( + isAuthored(edge) && + restsOnTypes.has(edge.type) && + !inside.has(edge.to) && + index.primitivesById.has(edge.to) + ) { + join(restsOn, edge.to, edge.type, memberId); + } + } + + for (const edge of index.edgesByTo.get(memberId) ?? []) { + if (isAuthored(edge) && !inside.has(edge.from) && index.primitivesById.has(edge.from)) { + const rows = restsOnTypes.has(edge.type) + ? restedOnBy + : edge.type === "verifies" + ? verifiedBy + : supersededBy; + join(rows, edge.from, edge.type, memberId); + } + } + } + + const rowsOf = (rows: Map<string, Map<AuthoredEdgeType, Set<string>>>): PackBoundaryRow[] => + [...rows.entries()] + .sort(([left], [right]) => compareCodeUnits(left, right)) + .flatMap(([id, byType]): PackBoundaryRow[] => { + const outside = index.primitivesById.get(id); + + if (outside === undefined) { + return []; + } + + return [ + { + id, + ...(outside.title === undefined ? {} : { title: outside.title }), + statedReadiness: outside.readiness, + implemented: implementedOf(id), + via: authoredEdgeTypes.flatMap((type) => { + const members = byType.get(type); + + return members === undefined ? [] : [{ type, members: [...members] }]; + }), + }, + ]; + }); + + return { + restsOn: rowsOf(restsOn), + restedOnBy: rowsOf(restedOnBy), + verifiedBy: rowsOf(verifiedBy), + supersededBy: rowsOf(supersededBy), + }; +} + /** * @sdpAnchor component:protocol.reader * @sdpLabel Protocol reader seam @@ -404,9 +716,9 @@ function matchSections(sections: SpecSections | undefined, needle: string): read /** * @sdpAnchor impl:protocol.reader * @sdpLabel thin typed graph reader construction - * @sdpSatisfies spec:consumers.reader + * @sdpSatisfies spec:consumers.reader, spec:extraction.entry-locations * @sdpComponent component:protocol.reader - * @sdpUses impl:protocol.delivery-facts + * @sdpUses impl:protocol.delivery-facts, impl:protocol.next-rung-floor * @sdpRole reader */ @@ -437,6 +749,120 @@ export function createReader(graph: GraphSchema): Reader { const factsOf = (id: string): readonly DeliveryFactName[] => recomputedFacts.get(id) ?? []; + // The location table, grouped by Spec once; each Spec's rows sort by entry in code-unit order. + const locationsBySpec = new Map<string, EntryLocation[]>(); + + for (const row of graph.locations ?? []) { + const rows = locationsBySpec.get(row.spec) ?? []; + rows.push(row); + locationsBySpec.set(row.spec, rows); + } + + for (const rows of locationsBySpec.values()) { + rows.sort((left, right) => compareCodeUnits(left.entry, right.entry) || left.line - right.line); + } + + const locationsOf = (id: string): readonly EntryLocation[] => locationsBySpec.get(id) ?? []; + + /** The unit's component: its `memberOf` target (the first by id, should a foreign graph hold more). */ + const componentOf = (codeId: string): CodeUnitComponent | undefined => { + const [componentId] = (index.edgesByFrom.get(codeId) ?? []) + .filter((edge) => edge.type === "memberOf") + .map((edge) => edge.to) + .sort(compareCodeUnits); + + if (componentId === undefined) { + return undefined; + } + + const component = index.nodesById.get(componentId); + + return component?.nodeType === "CodeNode" + ? { + id: componentId, + ...(component.layer === undefined ? {} : { layer: component.layer }), + ...(component.context === undefined ? {} : { context: component.context }), + } + : { id: componentId }; + }; + + const codeUnitBindingsTo = ( + id: string, + edgeType: "satisfies" | "references", + ): readonly CodeUnitBinding[] => + (index.edgesByTo.get(id) ?? []) + .filter((edge) => edge.type === edgeType) + .map((edge): CodeUnitBinding => { + const source = index.nodesById.get(edge.from); + const location = + source?.nodeType === "CodeNode" + ? { + ...(source.label === undefined ? {} : { label: source.label }), + file: source.file, + ...(source.line === undefined ? {} : { line: source.line }), + ...(source.role === undefined ? {} : { role: source.role }), + ...(source.layer === undefined ? {} : { layer: source.layer }), + ...(source.context === undefined ? {} : { context: source.context }), + } + : {}; + const component = componentOf(edge.from); + + return { + codeId: edge.from, + claim: edge.claim, + ...location, + ...(component === undefined ? {} : { component }), + }; + }) + .sort((left, right) => compareCodeUnits(left.codeId, right.codeId)); + + const anchorVerified = (verifierId: string): boolean => + (index.edgesByTo.get(verifierId) ?? []).some((edge) => + isResolvingTestAnchorVerify(edge, index.nodesById), + ); + + const verifierBindingsTo = (id: string): readonly VerifierBinding[] => + (index.edgesByTo.get(id) ?? []) + .filter((edge) => edge.type === "verifies") + .map((edge): VerifierBinding => { + const source = index.nodesById.get(edge.from); + + if (source?.nodeType === "Anchor") { + return { + verifierId: edge.from, + via: "test-anchor", + claim: edge.claim, + // A resolving test anchor *is* the enabled binding (MD-7) — but only along its + // contract row: an off-contract claim confers nothing, exactly as in the derived + // facts (the shared resolving-test-anchor rule; fail closed). + enabled: isResolvingTestAnchorVerify(edge, index.nodesById), + ...(source.label === undefined ? {} : { label: source.label }), + file: source.file, + line: source.line, + }; + } + + const example = index.primitivesById.get(edge.from); + // The shared enabled-example rule (delivery-facts): one predicate decides conferral for + // the decode and the derived facts, so the two surfaces can never disagree (fail closed). + const enabled = isEnabledExampleVerify(edge, index.nodesById, anchorVerified); + + return { + verifierId: edge.from, + via: "example", + claim: edge.claim, + enabled, + ...(example?.title === undefined ? {} : { label: example.title }), + ...(example === undefined ? {} : { file: example.file }), + }; + }) + .sort((left, right) => compareCodeUnits(left.verifierId, right.verifierId)); + + const enabledCount = (verifiers: readonly VerifierBinding[]): EnabledCount => ({ + total: verifiers.length, + enabled: verifiers.filter((verifier) => verifier.enabled).length, + }); + const packsOf = (specId: string): readonly string[] => (index.edgesByFrom.get(specId) ?? []) .filter((edge) => edge.type === "belongsTo") @@ -498,70 +924,9 @@ export function createReader(graph: GraphSchema): Reader { .map((edge) => relationEnd(index, edge, edge.from)) .sort(compareRelationEnds); - const codeUnitBindings = (edgeType: "satisfies" | "references"): CodeUnitBinding[] => - (index.edgesByTo.get(id) ?? []) - .filter((edge) => edge.type === edgeType) - .map((edge): CodeUnitBinding => { - const source = index.nodesById.get(edge.from); - const location = - source?.nodeType === "CodeNode" - ? { - ...(source.label === undefined ? {} : { label: source.label }), - file: source.file, - ...(source.line === undefined ? {} : { line: source.line }), - ...(source.role === undefined ? {} : { role: source.role }), - ...(source.layer === undefined ? {} : { layer: source.layer }), - ...(source.context === undefined ? {} : { context: source.context }), - } - : {}; - - return { codeId: edge.from, claim: edge.claim, ...location }; - }) - .sort((left, right) => compareCodeUnits(left.codeId, right.codeId)); - - const implementations: readonly ImplementationBinding[] = codeUnitBindings("satisfies"); - const references: readonly ReferenceBinding[] = codeUnitBindings("references"); - - const anchorVerified = (verifierId: string): boolean => - (index.edgesByTo.get(verifierId) ?? []).some((edge) => - isResolvingTestAnchorVerify(edge, index.nodesById), - ); - - const verifiers = (index.edgesByTo.get(id) ?? []) - .filter((edge) => edge.type === "verifies") - .map((edge): VerifierBinding => { - const source = index.nodesById.get(edge.from); - - if (source?.nodeType === "Anchor") { - return { - verifierId: edge.from, - via: "test-anchor", - claim: edge.claim, - // A resolving test anchor *is* the enabled binding (MD-7) — but only along its - // contract row: an off-contract claim confers nothing, exactly as in the derived - // facts (the shared resolving-test-anchor rule; fail closed). - enabled: isResolvingTestAnchorVerify(edge, index.nodesById), - ...(source.label === undefined ? {} : { label: source.label }), - file: source.file, - line: source.line, - }; - } - - const example = index.primitivesById.get(edge.from); - // The shared enabled-example rule (delivery-facts): one predicate decides conferral for - // the decode and the derived facts, so the two surfaces can never disagree (fail closed). - const enabled = isEnabledExampleVerify(edge, index.nodesById, anchorVerified); - - return { - verifierId: edge.from, - via: "example", - claim: edge.claim, - enabled, - ...(example?.title === undefined ? {} : { label: example.title }), - ...(example === undefined ? {} : { file: example.file }), - }; - }) - .sort((left, right) => compareCodeUnits(left.verifierId, right.verifierId)); + const implementations: readonly ImplementationBinding[] = codeUnitBindingsTo(id, "satisfies"); + const references: readonly ReferenceBinding[] = codeUnitBindingsTo(id, "references"); + const verifiers = verifierBindingsTo(id); // Exactly one complete oracle contract decodes as presence. Off-contract or competing edges // stay visible through findings, but never become an expected-outcome authority by accident. @@ -595,6 +960,7 @@ export function createReader(graph: GraphSchema): Reader { return { ...summary, ...(node.sections === undefined ? {} : { sections: node.sections }), + designColumns: specDesignColumns(node, index, locationsOf(id)), floorFailures: evaluateReadinessFloor(node, index), nextRungFailures: nextRung === undefined ? [] : evaluateReadinessFloor(node, index, nextRung), relationsOut, @@ -603,6 +969,7 @@ export function createReader(graph: GraphSchema): Reader { references, verifiers, ...(oracle === undefined ? {} : { oracle }), + entryLocations: locationsOf(id), findings: findingsNaming(id), }; }; @@ -622,6 +989,8 @@ export function createReader(graph: GraphSchema): Reader { } const summary = summarize(member); + const statedNextRung = statedNextRungOf(member.readiness); + const verifiers = verifierBindingsTo(member.id); return { id: summary.id, @@ -634,6 +1003,23 @@ export function createReader(graph: GraphSchema): Reader { ? {} : { derivedReadiness: summary.derivedReadiness }), deliveryFacts: summary.deliveryFacts, + file: summary.file, + ...(statedNextRung === undefined ? {} : { statedNextRung }), + statedNextRungFailures: + statedNextRung === undefined ? [] : evaluateReadinessFloor(member, index, statedNextRung), + design: specDesignColumns(member, index, locationsOf(member.id)), + implementations: codeUnitBindingsTo(member.id, "satisfies"), + references: codeUnitBindingsTo(member.id, "references"), + verifiers: enabledCount(verifiers), + // A declared `verifies` decodes as `via: "example"` whatever its source's kind, so the + // count reads the source's kind: only an example-kind Spec is an example. + examples: enabledCount( + verifiers.filter( + (verifier) => + verifier.via === "example" && + index.primitivesById.get(verifier.verifierId)?.specKind === "example", + ), + ), }; }); @@ -653,6 +1039,7 @@ export function createReader(graph: GraphSchema): Reader { ...summarizePack(node), members, verifierGaps, + boundary: packBoundary(node, index, (specId) => factsOf(specId).includes("implemented")), findings: findingsNaming(id), }; }; diff --git a/test/anchor-corpora.test.ts b/test/anchor-corpora.test.ts index 014f2bb8..32464f45 100644 --- a/test/anchor-corpora.test.ts +++ b/test/anchor-corpora.test.ts @@ -3,16 +3,12 @@ import { fileURLToPath } from "node:url"; import { afterAll, describe, expect, it } from "vitest"; -import { - extract, - extractFindingIds, - graphValidatorIds, - serializeGraph, - validateGraph, -} from "../src/index.js"; +import { extract, extractFindingIds, graphValidatorIds, validateGraph } from "../src/index.js"; import type { Finding } from "../src/index.js"; import { materializeExtractCorpus, removeMaterializedCorpus } from "./helpers/extract-corpus.js"; +import { serializeGraphStructure } from "./helpers/graph-structure.js"; + const materializedRoots: string[] = []; function corpusRoot(name: string): string { @@ -48,7 +44,7 @@ describe("the annotation corpora", () => { const goldenPath = fileURLToPath( new URL("./fixtures/extract/annotation-forms/expected-graph.json", import.meta.url), ); - expect(serializeGraph(result.graph)).toBe(readFileSync(goldenPath, "utf8")); + expect(serializeGraphStructure(result.graph)).toBe(readFileSync(goldenPath, "utf8")); // Plural targets: one satisfies edge per target; the decision reached by references alone. expect( diff --git a/test/bootstrap.test.ts b/test/bootstrap.test.ts index 1305a50f..071a1ef5 100644 --- a/test/bootstrap.test.ts +++ b/test/bootstrap.test.ts @@ -108,7 +108,13 @@ describe("bootstrap package surface", () => { const graph = deriveGraph(specs, packs, anchors); expect(reification.findings).toEqual([]); - expect(Object.keys(reification.specs[0] ?? {}).sort()).toEqual(["data", "file", "id", "line"]); + expect(Object.keys(reification.specs[0] ?? {}).sort()).toEqual([ + "data", + "entryLines", + "file", + "id", + "line", + ]); expect(graph.nodes.map((node) => node.id)).toEqual(["spec:carrier.public-seam"]); }); diff --git a/test/builders.test.ts b/test/builders.test.ts index 818805fe..d5831290 100644 --- a/test/builders.test.ts +++ b/test/builders.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; import { + SPEC_KINDS, + SPEC_ALTITUDES, + SPEC_READINESS, codeAnchor, codeAnchorId, constrainedBy, @@ -17,8 +20,123 @@ import { testAnchorId, verifies, } from "../src/index.js"; +import { deriveFixtureGraph } from "./helpers/fixture-graph.js"; +// The package builders bind this suite's anchors; the builders above are the subject under test, +// imported from source, which the extractor does not read as authoring. +import { + ref as anchorRef, + specTest as anchorSpecTest, + testAnchorId as anchorTestAnchorId, +} from "@libar-dev/software-delivery-protocol"; + +const relationBuildersTestAnchor = anchorSpecTest({ + id: anchorTestAnchorId("test:protocol.relation-builders"), + label: "relation builder tests verify the six declared, directed Spec relations", + verifies: anchorRef("spec:model.relations"), +}); +const packBuilderTestAnchor = anchorSpecTest({ + id: anchorTestAnchorId("test:protocol.pack-builder"), + label: "Pack builder tests verify the truth-free review aggregate", + verifies: anchorRef("spec:model.pack-aggregate"), +}); +void [relationBuildersTestAnchor, packBuilderTestAnchor]; + +const coreModelTestAnchor = anchorSpecTest({ + id: anchorTestAnchorId("test:protocol.core-model"), + label: "Spec builders and derivation verify independent coordinates and direct realization", + verifies: anchorRef("spec:model.core-model"), +}); +void coreModelTestAnchor; describe("builders", () => { + it("enriches one Spec envelope while keeping all three coordinates independent", () => { + for (const kind of SPEC_KINDS) { + for (const altitude of SPEC_ALTITUDES) { + for (const readiness of SPEC_READINESS) { + const base = spec({ + id: specId("spec:probe.enrich"), + title: "Enrich", + kind, + altitude, + readiness, + }); + const enriched = spec({ + ...base, + intent: { outcome: "Keep the same identity." }, + design: { shape: "One envelope." }, + }); + expect(base).toMatchObject({ kind, altitude, readiness }); + expect(enriched).toMatchObject({ kind, altitude, readiness }); + expect(enriched).toMatchObject(base); + expect(Object.keys(base).sort()).toEqual([ + "altitude", + "id", + "kind", + "readiness", + "relations", + "title", + ]); + expect(enriched.intent?.outcome).toBe("Keep the same identity."); + expect(enriched).not.toHaveProperty("deliveryFacts"); + } + } + } + }); + + it("derives delivery facts directly without propagating realization through refinement", () => { + const parent = spec({ + id: specId("spec:probe.parent"), + title: "Parent", + kind: "behavior", + altitude: "feature", + readiness: "defined", + intent: { outcome: "Create an order." }, + behavior: { rules: ["Create once."] }, + }); + const child = spec({ + id: specId("spec:probe.child"), + title: "Child", + kind: "example", + altitude: "story", + readiness: "idea", + relations: [refines(parent.id), verifies(parent.id)], + }); + const graph = deriveFixtureGraph({ + specs: [parent, child], + anchors: [ + codeAnchor({ id: codeAnchorId("impl:probe.parent"), satisfies: parent.id }), + specTest({ id: testAnchorId("test:probe.child"), verifies: child.id }), + ], + }); + const facts = (id: string) => + graph.nodes.find((node) => node.nodeType === "Primitive" && node.id === id); + expect(facts(parent.id)).toMatchObject({ + readiness: "defined", + deliveryFacts: ["implemented", "has-verifier"], + }); + expect(facts(child.id)).toMatchObject({ readiness: "idea", deliveryFacts: ["has-verifier"] }); + + const childOnlyGraph = deriveFixtureGraph({ + specs: [parent, child], + anchors: [ + codeAnchor({ id: codeAnchorId("impl:probe.child"), satisfies: child.id }), + specTest({ id: testAnchorId("test:probe.child"), verifies: child.id }), + ], + }); + const childOnlyFacts = (id: string) => + childOnlyGraph.nodes.find((node) => node.nodeType === "Primitive" && node.id === id); + expect(childOnlyFacts(parent.id)).toMatchObject({ + readiness: "defined", + deliveryFacts: ["has-verifier"], + }); + expect(childOnlyFacts(child.id)).toMatchObject({ + readiness: "idea", + deliveryFacts: ["implemented", "has-verifier"], + }); + expect(parent).not.toHaveProperty("deliveryFacts"); + expect(child).not.toHaveProperty("deliveryFacts"); + }); + it("builds plain serializable specs with declared relations", () => { const parent = ref("spec:orders.order-management"); const dependency = ref("spec:payments.authorize-payment"); diff --git a/test/check-self-hosting-gates.test.ts b/test/check-self-hosting-gates.test.ts index 3cf8fb16..22614ea1 100644 --- a/test/check-self-hosting-gates.test.ts +++ b/test/check-self-hosting-gates.test.ts @@ -76,11 +76,11 @@ afterEach(() => { }); describe("the self-hosting records gate", () => { - it("discovers plan 40 as the current primary plan", () => { - // re-measured under plan 40 + it("discovers plan 42 as the current primary plan", () => { + // re-measured after plan 41's fold expect(currentPlan).toMatchObject({ - number: 40, - name: "40-annotations-closer-to-v0.md", + number: 42, + name: "42-spec-studio-phase-zero.md", }); }); diff --git a/test/check-temporal.test.ts b/test/check-temporal.test.ts index 1541394d..fd9fa419 100644 --- a/test/check-temporal.test.ts +++ b/test/check-temporal.test.ts @@ -67,6 +67,7 @@ function runGuard(root: string) { return spawnSync(process.execPath, [join(root, guardName)], { cwd: root, encoding: "utf8", + env: { ...process.env, GIT_CEILING_DIRECTORIES: tmpdir() }, }); } diff --git a/test/checkout-v1.test.ts b/test/checkout-v1.test.ts index d04fc0af..94ad01da 100644 --- a/test/checkout-v1.test.ts +++ b/test/checkout-v1.test.ts @@ -8,10 +8,11 @@ import { extract, extractFindingIds, createReader, - serializeGraph, validateGraph, } from "../src/index.js"; +import { serializeGraphStructure } from "./helpers/graph-structure.js"; + const exampleRoot = fileURLToPath(new URL("../examples/checkout-v1", import.meta.url)); const goldenPath = fileURLToPath( new URL("./fixtures/checkout-v1/expected-graph.json", import.meta.url), @@ -56,7 +57,7 @@ describe("checkout-v1 tracer bullet (extractor-fed)", () => { const expected = readFileSync(goldenPath, "utf8"); expect( - serializeGraph(extraction.graph), + serializeGraphStructure(extraction.graph), "Golden graph mismatch. Review the diff against test/fixtures/checkout-v1/expected-graph.json; if the change is intended, regenerate the golden and commit the reviewed diff — the diff is the review.", ).toBe(expected); }); diff --git a/test/cli-q.test.ts b/test/cli-q.test.ts index 85aba31e..22f6177e 100644 --- a/test/cli-q.test.ts +++ b/test/cli-q.test.ts @@ -1,11 +1,14 @@ import { createHash } from "node:crypto"; -import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { isatty } from "node:tty"; import { fileURLToPath } from "node:url"; import { afterAll, describe, expect, it } from "vitest"; +import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; + import { runSdpCli } from "../src/cli/sdp.js"; import { createCaptureOutput } from "./helpers/cli-capture.js"; import { materializeExtractCorpus, removeMaterializedCorpus } from "./helpers/extract-corpus.js"; @@ -495,3 +498,215 @@ describe("sdp q — the agent front door", () => { expect(fingerprintTree(rootGenerated)).toBe(before); }); }); + +const queryParamsTestAnchor = specTest({ + id: testAnchorId("test:protocol.query-params"), + label: "front-door checks verify --params as the fourth binding and its refusals", + verifies: ref("spec:consumers.agent-surface.recipe-parameters"), +}); +void queryParamsTestAnchor; + +describe("sdp q --params — a recipe parameter as data", () => { + const paramsRoot = materializeExtractCorpus("anchored-binding"); + const scratch = mkdtempSync(join(tmpdir(), "sdp-q-params-")); + + afterAll(() => { + removeMaterializedCorpus(paramsRoot); + rmSync(scratch, { recursive: true, force: true }); + }); + + /** An extraction that records whether it ran: a refusal must land before the graph derives. */ + function watchedExtraction() { + const seen = { extracted: false }; + return { + seen, + hooks: { + query: { + ...terminalStdin.query, + extract: () => { + seen.extracted = true; + throw new Error("the graph must not derive after a --params refusal"); + }, + }, + }, + }; + } + + it("injects params as a fourth binding beside g, graph and report", async () => { + const capture = createCaptureOutput(); + + const exitCode = await runQ( + [ + "return { params, g: typeof g.specs, graph: Array.isArray(graph.nodes), report: report.validatorId }", + "--root", + paramsRoot, + "--params", + '{"spec":"spec:orders.anchored-parent","files":["a b.ts"],"depth":2}', + "--json", + ], + capture, + terminalStdin, + ); + + expect(exitCode).toBe(0); + expect(capture.readStderr()).toBe(""); + expect(JSON.parse(capture.readStdout())).toEqual({ + params: { spec: parentId, files: ["a b.ts"], depth: 2 }, + g: "function", + graph: true, + report: "graph", + }); + }); + + it("binds params to an empty object without the flag", async () => { + const capture = createCaptureOutput(); + + const exitCode = await runQ( + [ + "return { params, isObject: Object.getPrototypeOf(params) === Object.prototype }", + "--root", + paramsRoot, + "--json", + ], + capture, + terminalStdin, + ); + + expect(exitCode).toBe(0); + expect(JSON.parse(capture.readStdout())).toEqual({ params: {}, isObject: true }); + }); + + it("passes any JSON object through unchanged", async () => { + for (const value of ["{}", '{"nested":{"list":[1,null,"x"]},"flag":false}']) { + const capture = createCaptureOutput(); + + const exitCode = await runQ( + ["return { params }", "--root", paramsRoot, "--params", value, "--json"], + capture, + terminalStdin, + ); + + expect(exitCode).toBe(0); + expect(JSON.parse(capture.readStdout())).toEqual({ params: JSON.parse(value) as unknown }); + } + }); + + it("reads params from a file named with @PATH, a byte-order mark dropped", async () => { + const path = join(scratch, "params.json"); + writeFileSync(path, `\uFEFF${JSON.stringify({ spec: parentId })}\n`); + const capture = createCaptureOutput(); + + const exitCode = await runQ( + [ + "return g.specContext(params.spec)?.id ?? null", + "--root", + paramsRoot, + "--params", + `@${path}`, + "--json", + ], + capture, + terminalStdin, + ); + + expect(exitCode).toBe(0); + expect(capture.readStderr()).toBe(""); + expect(JSON.parse(capture.readStdout())).toBe(parentId); + }); + + it.each([ + [ + "inline text that is not JSON", + ["--params", "{spec: 1}"], + "sdp q: --params is not valid JSON (", + ], + ["an empty value", ["--params", ""], "sdp q: --params is not valid JSON ("], + ["null", ["--params", "null"], "sdp q: --params must be a JSON object, got null."], + ["an array", ["--params", "[1,2]"], "sdp q: --params must be a JSON object, got an array."], + [ + "a string", + ["--params", '"spec:orders.anchored-parent"'], + "sdp q: --params must be a JSON object, got a string.", + ], + ["a number", ["--params", "42"], "sdp q: --params must be a JSON object, got a number."], + ["a boolean", ["--params", "true"], "sdp q: --params must be a JSON object, got a boolean."], + [ + "a file that cannot be read", + ["--params", "@/no/such/params.json"], + 'sdp q: --params file "/no/such/params.json" could not be read (', + ], + ["an @ with no path", ["--params", "@"], "sdp q: --params @PATH requires a path after @."], + ["a missing value", ["--params"], "sdp q: --params requires a JSON object or @PATH."], + [ + "a flag in place of the value", + ["--params", "--json"], + "sdp q: --params expects a JSON object or @PATH, got --json", + ], + [ + "a second --params", + ["--params", "{}", "--params", "{}"], + "sdp q takes at most one --params.", + ], + ])( + "refuses %s before the body runs: exit 1, one line, nothing on stdout", + async (_case, args, message) => { + const { seen, hooks } = watchedExtraction(); + const capture = createCaptureOutput(); + + const exitCode = await runQ( + ["return 'the body must not run'", "--root", paramsRoot, ...args], + capture, + hooks, + ); + + expect(exitCode).toBe(1); + expect(capture.readStdout()).toBe(""); + expect(capture.readStderr().startsWith(message)).toBe(true); + expect(capture.readStderr().split("\n")).toHaveLength(2); + expect(seen.extracted).toBe(false); + }, + ); + + it("refuses a file whose JSON is not an object, in one line naming the file", async () => { + const path = join(scratch, "null.json"); + writeFileSync(path, "null\n"); + const { seen, hooks } = watchedExtraction(); + const capture = createCaptureOutput(); + + const exitCode = await runQ( + ["return 'the body must not run'", "--root", paramsRoot, "--params", `@${path}`], + capture, + hooks, + ); + + expect(exitCode).toBe(1); + expect(capture.readStdout()).toBe(""); + expect(capture.readStderr()).toBe( + `sdp q: --params file ${JSON.stringify(path)} must be a JSON object, got null.\n`, + ); + expect(seen.extracted).toBe(false); + }); + + it("refuses a file whose text is not JSON, in one line naming the file", async () => { + const path = join(scratch, "broken.json"); + writeFileSync(path, '{\n "spec": \n}\n'); + const { seen, hooks } = watchedExtraction(); + const capture = createCaptureOutput(); + + const exitCode = await runQ( + ["return 'the body must not run'", "--root", paramsRoot, "--params", `@${path}`], + capture, + hooks, + ); + + expect(exitCode).toBe(1); + expect(capture.readStdout()).toBe(""); + const stderr = capture.readStderr(); + expect( + stderr.startsWith(`sdp q: --params file ${JSON.stringify(path)} is not valid JSON (`), + ).toBe(true); + expect(stderr.endsWith(").\n")).toBe(true); + expect(stderr.split("\n")).toHaveLength(2); + expect(seen.extracted).toBe(false); + }); +}); diff --git a/test/cli.test.ts b/test/cli.test.ts index 0ef54bb8..177d054f 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import { cpSync, existsSync, @@ -17,7 +18,19 @@ import { afterAll, describe, expect, it, vi } from "vitest"; import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; -import { SDP_HELP_TEXT, isCliEntrypoint, onStdoutError, runSdpCli } from "../src/cli/sdp.js"; +import { + readBuildCommit, + writeBuildArtifacts, + writeBuildInfo, +} from "../src/cli/build-artifacts.js"; +import { BUILD_INFO_FILE, parseBuildCommit } from "../src/cli/build-info.js"; +import { + SDP_HELP_TEXT, + isCliEntrypoint, + onStdoutError, + readEngineProvenance, + runSdpCli, +} from "../src/cli/sdp.js"; import { isWatchedCarrierPath, type NativeWatchHandle, @@ -28,6 +41,7 @@ import { import { generateContracts } from "../src/codegen/contracts.js"; import { extract } from "../src/extract/index.js"; import { renderDesignReview } from "../src/projections/design-review.js"; +import { createReader } from "../src/reader/reader.js"; import { createCaptureOutput } from "./helpers/cli-capture.js"; import { materializeExtractCorpus, @@ -35,6 +49,18 @@ import { removeMaterializedCorpus, } from "./helpers/extract-corpus.js"; +const regenerationTestAnchor = specTest({ + id: testAnchorId("test:protocol.regenerability"), + label: "delete and rebuild tests verify byte-identical generated artifacts", + verifies: ref("spec:extraction.regenerability"), +}); +const projectionPublicationTestAnchor = specTest({ + id: testAnchorId("test:protocol.projections-model.publication"), + label: "projection publication tests verify regeneration and diagnostic exit codes", + verifies: ref("spec:consumers.projections-model"), +}); +void [regenerationTestAnchor, projectionPublicationTestAnchor]; + const repoRoot = fileURLToPath(new URL("..", import.meta.url)); const exampleRoot = join(repoRoot, "examples", "checkout-v1"); @@ -256,35 +282,11 @@ describe("sdp cli", () => { expect(exitCode).toBe(0); const stderr = capture.readStderr(); for (const [file, specId] of [ - ["specs/carrier/markdown-authoring.sdp.md", "spec:carrier.markdown-authoring"], - ["specs/extraction/claim-taxonomy.sdp.md", "spec:extraction.claim-taxonomy"], - ["specs/model/pack-aggregate.sdp.md", "spec:model.pack-aggregate"], - ["specs/model/relations.sdp.md", "spec:model.relations"], ["specs/model/spec-sections.sdp.md", "spec:model.spec-sections"], ] as const) { expect(stderr).toContain(`${file} — [warning] honesty/gaps — Spec "${specId}"`); } for (const [file, target, specId] of [ - [ - "specs/carrier/markdown-body-grammar.sdp.md", - "spec:validation.authored-honesty", - "spec:carrier.markdown-body-grammar", - ], - [ - "specs/carrier/markdown-parser.sdp.md", - "spec:carrier.inline-code-spans", - "spec:carrier.markdown-parser", - ], - [ - "specs/consumers/adopter-on-ramp.sdp.md", - "spec:carrier.markdown-body-grammar", - "spec:consumers.adopter-on-ramp", - ], - [ - "specs/consumers/delivery-session-on-ramp.sdp.md", - "spec:decisions.planning-truths-placement", - "spec:consumers.delivery-session-on-ramp", - ], [ "specs/consumers/delivery-session-on-ramp.sdp.md", "spec:decisions.shipped-projections-frozen", @@ -295,11 +297,6 @@ describe("sdp cli", () => { "spec:decisions.carrier-universality", "spec:decisions.carrier-ruling", ], - [ - "specs/decisions/jsdoc-graph-extraction-refused.sdp.md", - "spec:model.spec-sections", - "spec:decisions.jsdoc-graph-extraction-refused", - ], [ "specs/decisions/planning-truths-placement.sdp.md", "spec:consumers.impact-graph", @@ -310,8 +307,8 @@ describe("sdp cli", () => { `${file} — [warning] conformance/prose-mentions — Mention of "${target}" in "${specId}"`, ); } - // re-measured under plan 40 - expect(capture.readStdout()).toContain("validate: 0 errors · 14 warnings"); + // re-measured under plan 41's fold + expect(capture.readStdout()).toContain("validate: 0 errors · 4 warnings"); expect(readFileSync(join(root, "generated", "graph.json"), "utf8")).toContain( '"id": "pack:self-hosting-v1"', ); @@ -458,14 +455,17 @@ export const example${idSegment.replace(/[^A-Za-z0-9]/gu, "")} = spec({ const root = materializeExampleCopy(); try { - expect(runSdpCli(["build", root], createCaptureOutput().output)).toBe(0); + for (const verb of ["build", "view", "census", "mermaid", "gherkin"]) { + expect(runSdpCli([verb, root], createCaptureOutput().output)).toBe(0); + } const firstBuild = readGeneratedTree(root); rmSync(join(root, "generated"), { recursive: true, force: true }); - expect(runSdpCli(["build", root], createCaptureOutput().output)).toBe(0); + for (const verb of ["build", "view", "census", "mermaid", "gherkin"]) { + expect(runSdpCli([verb, root], createCaptureOutput().output)).toBe(0); + } - // The whole generated tree — graph.json and every contract module — regenerates - // byte-identically. + // The graph, contracts and all four projection trees regenerate byte-identically. expect(readGeneratedTree(root)).toEqual(firstBuild); } finally { rmSync(root, { recursive: true, force: true }); @@ -1374,11 +1374,13 @@ export const probe = spec({ const pagePath = join(root, "generated", "design-review", "spec", "orders.create-order.md"); expect(runSdpCli(["view", root], createCaptureOutput().output)).toBe(0); const firstRender = readFileSync(pagePath, "utf8"); + const firstTree = readGeneratedTree(root); rmSync(join(root, "generated"), { recursive: true, force: true }); expect(runSdpCli(["view", root], createCaptureOutput().output)).toBe(0); expect(readFileSync(pagePath, "utf8")).toBe(firstRender); + expect(readGeneratedTree(root)).toEqual(firstTree); } finally { rmSync(root, { recursive: true, force: true }); } @@ -1592,6 +1594,13 @@ function createWatchHarness(): { }; } +const validateWatchTestAnchor = specTest({ + id: testAnchorId("test:protocol.validate-watch"), + label: "watch-loop checks verify the validate --watch authoring loop", + verifies: ref("spec:consumers.validate-watch"), +}); +void validateWatchTestAnchor; + describe("sdp validate --watch", () => { it("documents --watch as a validate-only authoring loop", () => { expect(SDP_HELP_TEXT).toContain( @@ -2199,3 +2208,194 @@ it("clean-repo determinism: the full pipeline at a different absolute path is by rmSync(secondRoot, { recursive: true, force: true }); } }); + +const engineProvenanceTestAnchor = specTest({ + id: testAnchorId("test:protocol.engine-provenance"), + label: "version checks verify the engine provenance line and the build's commit record", + verifies: ref("spec:consumers.engine-provenance"), +}); +void engineProvenanceTestAnchor; + +describe("sdp --version", () => { + const packageVersion = ( + JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as { version: string } + ).version; + const fullCommit = "0123456789abcdef0123456789abcdef01234567"; + + /** A reader over two in-memory files, keyed by the last path segment of the URL. */ + function shippedFiles(files: Readonly<Record<string, string>>) { + return (url: URL): string => { + const name = url.pathname.slice(url.pathname.lastIndexOf("/") + 1); + const text = files[name]; + + if (text === undefined) { + throw Object.assign(new Error(`ENOENT: ${name}`), { code: "ENOENT" }); + } + + return text; + }; + } + + it("prints one line naming the package version and the recorded commit, and exits 0", () => { + const capture = createCaptureOutput(); + + const exitCode = runSdpCli(["--version"], capture.output, { + readEngineFile: shippedFiles({ + "package.json": JSON.stringify({ name: "x", version: "1.2.3" }), + [BUILD_INFO_FILE]: JSON.stringify({ commit: fullCommit }), + }), + }); + + expect(exitCode).toBe(0); + expect(capture.readStdout()).toBe(`sdp 1.2.3 (${fullCommit})\n`); + expect(capture.readStderr()).toBe(""); + }); + + it("names the commit unknown when the build recorded none or recorded something else", () => { + for (const record of [ + undefined, + "", + "not json", + "null", + JSON.stringify({ commit: "28dcbdd" }), + JSON.stringify({ commit: `${fullCommit}\nsdp 9.9.9` }), + JSON.stringify({ commit: fullCommit.toUpperCase() }), + ]) { + const capture = createCaptureOutput(); + const files: Record<string, string> = { "package.json": '{"version":"1.2.3"}' }; + if (record !== undefined) files[BUILD_INFO_FILE] = record; + + expect( + runSdpCli(["--version"], capture.output, { readEngineFile: shippedFiles(files) }), + ).toBe(0); + expect(capture.readStdout()).toBe("sdp 1.2.3 (unknown)\n"); + } + }); + + it("reads the version from the package manifest two directories above the CLI module", () => { + const urls: string[] = []; + readEngineProvenance((url) => { + urls.push(url.href); + throw new Error("absent"); + }); + + expect(urls).toEqual([ + new URL("../package.json", import.meta.url).href, + new URL(`../src/cli/${BUILD_INFO_FILE}`, import.meta.url).href, + ]); + expect(readEngineProvenance()).toEqual({ version: packageVersion, commit: "unknown" }); + }); + + it("documents --version and --params in the usage text", () => { + expect(SDP_HELP_TEXT).toContain(" sdp --version\n"); + expect(SDP_HELP_TEXT).toContain("sdp <package version> (<commit>)"); + expect(SDP_HELP_TEXT).toContain("[--params JSON | --params @PATH]"); + expect(SDP_HELP_TEXT).toContain("Four bindings are injected"); + expect(SDP_HELP_TEXT).toContain("dist/recipes/NN-slug.js"); + }); + + it("records the checked-out commit at build time, or unknown when git cannot name one", () => { + const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf8" }); + + expect(readBuildCommit(repoRoot)).toBe(head.trim()); + expect( + readBuildCommit(repoRoot, () => { + throw new Error("not a git checkout"); + }), + ).toBe("unknown"); + expect(readBuildCommit(repoRoot, () => "fatal: ambiguous argument\n")).toBe("unknown"); + + const outDir = mkdtempSync(join(tmpdir(), "sdp-build-info-")); + try { + writeBuildInfo(outDir, fullCommit); + expect(parseBuildCommit(readFileSync(join(outDir, "cli", BUILD_INFO_FILE), "utf8"))).toBe( + fullCommit, + ); + } finally { + rmSync(outDir, { recursive: true, force: true }); + } + }); + + it("publishes the commit the build step reads from git beside the CLI", () => { + const root = mkdtempSync(join(tmpdir(), "sdp-build-step-")); + const outDir = join(root, "dist"); + const asked: { readonly cwd: string; readonly args: readonly string[] }[] = []; + + try { + mkdirSync(join(root, "docs", "agent-surface"), { recursive: true }); + writeFileSync( + join(root, "docs", "agent-surface", "recipes.md"), + "## 1. One\n\n```js\nreturn 1;\n```\n", + ); + writeBuildArtifacts({ + root, + outDir, + git: (cwd, args) => { + asked.push({ cwd, args }); + return `${fullCommit}\n`; + }, + }); + + expect(asked).toEqual([{ cwd: root, args: ["rev-parse", "HEAD"] }]); + expect(readFileSync(join(outDir, "cli", BUILD_INFO_FILE), "utf8")).toBe( + `${JSON.stringify({ commit: fullCommit })}\n`, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("answers from the compiled CLI with the commit its build recorded beside it", () => { + const compiledCli = join(repoRoot, "dist", "cli", "sdp.js"); + const record = join(repoRoot, "dist", "cli", BUILD_INFO_FILE); + expect(existsSync(compiledCli), "run npm run build before the CLI check").toBe(true); + expect(existsSync(record), "the build records its commit beside the CLI").toBe(true); + + const commit = parseBuildCommit(readFileSync(record, "utf8")); + expect(commit).toMatch(/^(?:[0-9a-f]{40}|[0-9a-f]{64}|unknown)$/u); + expect(execFileSync(process.execPath, [compiledCli, "--version"], { encoding: "utf8" })).toBe( + `sdp ${packageVersion} (${commit})\n`, + ); + + // Neither value reaches a derived artifact: the graph stays a function of the corpus alone. + const graphText = JSON.stringify(extract({ root: exampleRoot }).graph); + expect(graphText).not.toContain(BUILD_INFO_FILE); + expect(commit === "unknown" || !graphText.includes(commit)).toBe(true); + }); +}); + +describe("a shipped recipe file through the compiled CLI", () => { + it("runs recipe 4 as shipped, its changed files passed with --params @PATH", () => { + const compiledCli = join(repoRoot, "dist", "cli", "sdp.js"); + const shipped = join(repoRoot, "dist", "recipes", "04-what-breaks-if-i-change-these-files.js"); + const scratch = mkdtempSync(join(tmpdir(), "sdp-shipped-recipe-")); + const graph = extract({ root: exampleRoot }).graph; + const boundFile = graph.nodes.find((node) => node.nodeType === "CodeNode"); + if (boundFile?.nodeType !== "CodeNode") { + throw new Error("the example corpus binds no code unit"); + } + const files = [boundFile.file, "absent/file.ts"]; + + try { + const params = join(scratch, "changed.json"); + writeFileSync(params, JSON.stringify({ files })); + const body = readFileSync(shipped, "utf8").replace(/\n+$/u, ""); + const stdout = execFileSync( + process.execPath, + [compiledCli, "q", body, "--root", exampleRoot, "--params", `@${params}`, "--json"], + { encoding: "utf8" }, + ); + const result = JSON.parse(stdout) as Record<string, unknown>; + const radius = createReader(graph).blastRadius(files); + + expect(result.changedFiles).toEqual(radius.changedFiles); + expect(result.coverageUnknownFiles).toEqual(["absent/file.ts"]); + expect( + (result.impactedSpecs as readonly { readonly id: string }[]).map((row) => row.id), + ).toEqual(radius.impactedSpecs.map((item) => item.id)); + expect(radius.impactedSpecs.length).toBeGreaterThan(0); + } finally { + rmSync(scratch, { recursive: true, force: true }); + } + }); +}); diff --git a/test/design-review-pack-page.test.ts b/test/design-review-pack-page.test.ts new file mode 100644 index 00000000..be0268fa --- /dev/null +++ b/test/design-review-pack-page.test.ts @@ -0,0 +1,365 @@ +import { describe, expect, it } from "vitest"; + +import { createReader, pack, packId, renderDesignReview, spec, specId } from "../src/index.js"; +import type { GraphSchema } from "../src/index.js"; +import { deriveFixtureGraph } from "./helpers/fixture-graph.js"; +import { PACK_ID, packDesignProbeGraph } from "./helpers/pack-design-probe.js"; + +/** + * The Design Review's Pack page over the Pack design probe: the fixed section order, the member + * table, the next rung, the boundary tables, the questions with addresses and line links, the + * code, and a byte-identical second render. + * + * @sdpAnchor test:protocol.pack-design-page + * @sdpLabel the Pack page renders the Pack design in its fixed order + * @sdpVerifies spec:consumers.design-review.pack-design-page + */ + +const PAGE = `pack/${PACK_ID.slice("pack:".length)}.md`; + +function renderPackPage(graph: GraphSchema = packDesignProbeGraph(), path = PAGE): string { + const page = renderDesignReview(createReader(graph)).find((entry) => entry.path === path); + + if (page === undefined) { + throw new Error(`The rendered view is missing the page "${path}".`); + } + + return page.content; +} + +/** The text under one `## ` heading, up to the next one. */ +function section(page: string, heading: string): string { + const start = page.indexOf(`\n${heading}\n`); + + if (start < 0) { + throw new Error(`The page has no "${heading}" section.`); + } + + const end = page.indexOf("\n## ", start + 1); + + return page.slice(start + 1, end < 0 ? undefined : end); +} + +describe("the Design Review's Pack page presents the Pack design", () => { + it("keeps the title line, framing and vocabulary, then renders the sections in order", () => { + const page = renderPackPage(); + const lines = page.split("\n"); + + expect(lines[0]).toBe("# Probe design"); + expect(lines[2]?.startsWith("`pack:probe.design` · Pack")).toBe(true); + expect(page.indexOf("> The probe's design as it stands.")).toBeLessThan( + page.indexOf("**Vocabulary (`modelRefs`):**"), + ); + expect(page.indexOf("**Vocabulary (`modelRefs`):**")).toBeLessThan(page.indexOf("## Members")); + expect(lines.filter((line) => line.startsWith("## "))).toEqual([ + "## Members", + "## Next rung", + "## Boundary", + "## Open questions", + "## Code", + "## Verifier coverage gaps", + "## Findings", + ]); + }); + + it("numbers one member row from 1 in authored order, with design columns and binding columns", () => { + const members = section(renderPackPage(), "## Members").split("\n"); + + expect(members).toContain( + "| Spec | Kind | Altitude | Stated | Floor reached | Stated next rung | Design entries | Declarations | Open questions | Decisions | Design reference | Implementation binding | Verifier binding |", + ); + expect( + members.filter((line) => line.startsWith("| [`spec:") || line.startsWith("| `spec:")), + ).toEqual([ + "| [`spec:probe.held`](../spec/probe.held.md) (1) Probe held | rule | story | defined | defined | ready · `typed-dependency-targets-are-defined` | 6 | 2 | 2 (0 blocking) | [`spec:probe.choice`](../spec/probe.choice.md) (defined) | none | none | none |", + "| [`spec:probe.waiting`](../spec/probe.waiting.md) (2) Probe waiting | rule | story | defined | ready | ready · nothing blocks | 0 | 0 | 0 | — | none | present | present |", + "| [`spec:probe.blocked`](../spec/probe.blocked.md) (3) Probe blocked | rule | story | scoped | scoped | defined · `no-blocking-open-questions` | 0 | 0 | 1 (1 blocking) | `spec:probe.absent` (unresolved) | present | none | none |", + "| `spec:probe.missing` (4) — **unresolved** (see findings) | — | — | — | — | — | — | — | — | — | — | — | — |", + "| [`spec:probe.modest`](../spec/probe.modest.md) (5) Probe modest | rule | story | idea | defined | scoped · nothing blocks | 0 | 0 | 0 | — | none | none | none |", + "| [`spec:probe.top`](../spec/probe.top.md) (6) Probe top | rule | story | ready | ready | — | 0 | 0 | 0 | — | none | none | none |", + "| [`spec:probe.check`](../spec/probe.check.md) (7) Probe check | example | story | idea | idea | scoped · `kind-evidence-present` | 0 | 0 | 0 | — | none | none | none |", + ]); + }); + + it("lists what holds each member below its next rung, and apart, the members that wait for their author", () => { + const nextRung = section(renderPackPage(), "## Next rung"); + const [held, waiting = ""] = nextRung.split("### Waiting for the author's statement"); + + expect(held).toContain( + [ + "### [`spec:probe.held`](../spec/probe.held.md) Probe held", + "", + "Stated `defined`; the floor does not hold `ready`:", + "", + "- `typed-dependency-targets-are-defined` — Every refines, dependsOn, constrainedBy, and decidedBy target states at least defined.", + " - dependsOn [`spec:probe.outside-basis`](../spec/probe.outside-basis.md), stated `scoped`", + "", + "### [`spec:probe.blocked`](../spec/probe.blocked.md) Probe blocked", + "", + "Stated `scoped`; the floor does not hold `defined`:", + "", + "- `no-blocking-open-questions` — Spec has no blocking open question in intent.openQuestions.", + ].join("\n"), + ); + expect(held).toContain("### [`spec:probe.check`](../spec/probe.check.md) Probe check"); + expect(waiting).toContain("That is not approval"); + expect(waiting.split("\n").filter((line) => line.startsWith("- "))).toEqual([ + "- stated `defined`, next `ready`: [`spec:probe.waiting`](../spec/probe.waiting.md) Probe waiting", + "- stated `idea`, next `scoped`: [`spec:probe.modest`](../spec/probe.modest.md) Probe modest", + ]); + // The verifier gap list owns list lines that open with a member link. + expect(nextRung).not.toMatch(/^- \[`spec:/mu); + // A member stating `ready` has no next rung, and an unresolved member has no floor. + expect(nextRung).not.toContain("spec:probe.top"); + expect(nextRung).not.toContain("spec:probe.missing"); + }); + + it("renders the boundary as four lists, each row with its rung, its binding and the members it joins", () => { + const boundary = section(renderPackPage(), "## Boundary"); + const [restsOn, inbound = ""] = boundary.split("### Rested on by"); + const [restedOnBy = "", verifiedBy = ""] = inbound.split("### Verified by"); + + expect(restsOn).toContain( + [ + "| Spec | Stated | Implementation binding | Members joined |", + "|---|---|---|---|", + "| [`spec:probe.choice`](../spec/probe.choice.md) Probe choice | defined | none | decidedBy: [`spec:probe.held`](../spec/probe.held.md) |", + "| [`spec:probe.outside-basis`](../spec/probe.outside-basis.md) Probe outside-basis | scoped | none | dependsOn: [`spec:probe.held`](../spec/probe.held.md), [`spec:probe.blocked`](../spec/probe.blocked.md), [`spec:probe.modest`](../spec/probe.modest.md) |", + ].join("\n"), + ); + expect(restedOnBy.split("\n").filter((line) => line.startsWith("| [`"))).toEqual([ + "| [`spec:probe.outside-basis`](../spec/probe.outside-basis.md) Probe outside-basis | scoped | none | refines: [`spec:probe.held`](../spec/probe.held.md) |", + "| [`spec:probe.waiting.case`](../spec/probe.waiting.case.md) Probe waiting.case | idea | none | refines: [`spec:probe.waiting`](../spec/probe.waiting.md) |", + "| [`spec:probe.waiting.unbound`](../spec/probe.waiting.unbound.md) Probe waiting.unbound | idea | none | refines: [`spec:probe.waiting`](../spec/probe.waiting.md) |", + ]); + expect(verifiedBy).toContain("verifies: [`spec:probe.waiting`](../spec/probe.waiting.md)"); + expect(verifiedBy).toContain( + "### Superseded by\n\nThe Specs outside the Pack that supersede a member.\n\nNone.", + ); + }); + + it("renders outside superseding decisions apart from Specs that rest on members", () => { + const graph = packDesignProbeGraph(); + const choice = graph.nodes.find((node) => node.id === "spec:probe.choice"); + if (choice === undefined) throw new Error("Missing probe decision."); + const boundary = section( + renderPackPage({ + ...graph, + nodes: [...graph.nodes, { ...choice, id: "spec:probe.replacement" }], + edges: [ + ...graph.edges, + { + from: "spec:probe.replacement", + to: "spec:probe.top", + type: "supersedes", + claim: "declared", + }, + ], + }), + "## Boundary", + ); + const [before = "", supersededBy = ""] = boundary.split("### Superseded by"); + expect(before).not.toContain("spec:probe.replacement"); + expect(supersededBy).toContain( + "| [`spec:probe.replacement`](../spec/probe.replacement.md) Probe choice | defined | none | supersedes: [`spec:probe.top`](../spec/probe.top.md) |", + ); + }); + + it("groups questions by member with their flag, address, escaped text and a line link", () => { + expect(section(renderPackPage(), "## Open questions")).toBe( + [ + "## Open questions", + "", + "### [`spec:probe.held`](../spec/probe.held.md) Probe held", + "", + "- non-blocking · `spec:probe.held#question.basisOwner` — Who owns the basis? ([specs/fixture.sdp.ts:21](../../../specs/fixture.sdp.ts#L21))", + "- non-blocking — Is the <name> \\| \\# final?", + "", + "### [`spec:probe.blocked`](../spec/probe.blocked.md) Probe blocked", + "", + "- **blocking** · `spec:probe.blocked#question.scopeCut` — Where is the cut? ([specs/fixture.sdp.ts:14](../../../specs/fixture.sdp.ts#L14))", + "", + ].join("\n"), + ); + }); + + it("lists each member's realizing and referencing units with a line link, role and component", () => { + const code = section(renderPackPage(), "## Code"); + + expect(code).toContain( + [ + "### [`spec:probe.waiting`](../spec/probe.waiting.md) Probe waiting", + "", + "#### Implementations", + "", + "- `impl:probe.waiting` — realizes the waiting rule ([src/fixture.ts:2](../../../src/fixture.ts#L2)) · role `service` · component `component:probe.core` (layer `domain`, context `probe`) `[anchored]`", + ].join("\n"), + ); + expect(code).toContain( + [ + "### [`spec:probe.blocked`](../spec/probe.blocked.md) Probe blocked", + "", + "#### Referenced by", + "", + "- `impl:probe.helper` ([src/fixture.ts:3](../../../src/fixture.ts#L3)) `[anchored]`", + ].join("\n"), + ); + expect(code).toContain( + "### [`spec:probe.held`](../spec/probe.held.md) Probe held\n\nNo implementation or design-reference binding is recorded for this member.\n", + ); + }); + + it("percent-encodes each path segment of a line link and escapes brackets in its label", () => { + const carrier = "specs/odd (dir)/a#b].sdp.ts"; + const source = "src/odd dir/x#y].ts"; + const probe = packDesignProbeGraph(); + const page = renderPackPage({ + ...probe, + nodes: probe.nodes.map((node) => + node.id === "spec:probe.held" + ? { ...node, file: carrier } + : node.id === "impl:probe.waiting" + ? { ...node, file: source } + : node, + ), + }); + + expect(section(page, "## Open questions")).toContain( + "- non-blocking · `spec:probe.held#question.basisOwner` — Who owns the basis? ([specs/odd (dir)/a\\#b\\].sdp.ts:21](../../../specs/odd%20%28dir%29/a%23b%5D.sdp.ts#L21))", + ); + expect(section(page, "## Code")).toContain( + "- `impl:probe.waiting` — realizes the waiting rule ([src/odd dir/x\\#y\\].ts:2](../../../src/odd%20dir/x%23y%5D.ts#L2)) · role `service`", + ); + }); + + it("keeps a multiline title, question or label inside its heading or list item", () => { + const probe = packDesignProbeGraph(); + const page = renderPackPage({ + ...probe, + nodes: probe.nodes.map((node) => { + if (node.id === PACK_ID) { + return { ...node, title: "Probe\ndesign" }; + } + + if (node.id === "impl:probe.waiting") { + return { ...node, label: "realizes the\r\nwaiting rule" }; + } + + if (node.nodeType !== "Primitive" || node.id !== "spec:probe.held") { + return node; + } + + return { + ...node, + title: "Probe held\nacross lines", + sections: { + ...node.sections, + intent: { + ...node.sections?.intent, + openQuestions: [ + { question: "Who owns\n the basis?", blocking: false, key: "basisOwner" }, + ], + }, + }, + }; + }), + }); + const lines = page.split("\n"); + + expect(lines[0]).toBe("# Probe design"); + expect(lines).toContain( + "### [`spec:probe.held`](../spec/probe.held.md) Probe held across lines", + ); + expect(lines).toContain( + "- non-blocking · `spec:probe.held#question.basisOwner` — Who owns the basis? ([specs/fixture.sdp.ts:21](../../../specs/fixture.sdp.ts#L21))", + ); + expect(lines).toContain( + "- `impl:probe.waiting` — realizes the waiting rule ([src/fixture.ts:2](../../../src/fixture.ts#L2)) · role `service` · component `component:probe.core` (layer `domain`, context `probe`) `[anchored]`", + ); + expect(lines).toContain( + "- [`spec:probe.held`](../spec/probe.held.md) — Probe held across lines (stated `defined`)", + ); + for (const continuation of ["design", "across lines", "the basis?", "waiting rule"]) { + expect(lines.filter((line) => line.trimStart().startsWith(continuation))).toEqual([]); + } + }); + + it("speaks binding language: no delivery-fact name renders as text", () => { + const page = renderPackPage(); + + expect(page.includes("implemented")).toBe(false); + expect(page.includes("has-verifier")).toBe(false); + }); + + it("renders byte-identical pages from two freshly derived graphs, with no time or commit", () => { + const first = renderPackPage(); + + expect(renderPackPage()).toBe(first); + expect(first).not.toMatch(/\b\d{4}-\d{2}-\d{2}\b/u); + expect(first).not.toMatch(/\b[0-9a-f]{40}\b/u); + }); + + it("says so in one line where a section has nothing to show", () => { + const graph = deriveFixtureGraph({ + specs: [ + spec({ + id: specId("spec:probe.alone"), + title: "Probe alone", + kind: "rule", + altitude: "story", + readiness: "ready", + intent: { outcome: "Stand alone." }, + behavior: { rules: ["The probe states one rule."] }, + }), + ], + packs: [ + pack({ + id: packId("pack:probe.alone"), + title: "Alone", + specs: [specId("spec:probe.alone")], + }), + ], + }); + const page = renderPackPage(graph, "pack/probe.alone.md"); + + expect(section(page, "## Next rung")).toContain("No member is held below its next rung."); + expect(section(page, "## Next rung")).not.toContain("### Waiting"); + expect(section(page, "## Boundary")).toBe( + [ + "## Boundary", + "", + "### Rests on", + "", + "The Specs outside the Pack that a member refines, depends on, is constrained by or is decided by.", + "", + "None.", + "", + "### Rested on by", + "", + "The Specs outside the Pack that refine, depend on, are constrained by or are decided by a member.", + "", + "None.", + "", + "### Verified by", + "", + "The Specs outside the Pack that verify a member.", + "", + "None.", + "", + "### Superseded by", + "", + "The Specs outside the Pack that supersede a member.", + "", + "None.", + "", + ].join("\n"), + ); + expect(section(page, "## Open questions")).toBe( + "## Open questions\n\nNo member records an open question.\n", + ); + expect(section(page, "## Code")).toContain( + "No implementation or design-reference binding is recorded for this member.", + ); + expect(page).not.toContain("No code unit realizes"); + }); +}); diff --git a/test/design-review.test.ts b/test/design-review.test.ts index bb9bb6c1..696d664d 100644 --- a/test/design-review.test.ts +++ b/test/design-review.test.ts @@ -24,6 +24,19 @@ import type { DesignReviewPage, GraphSchema } from "../src/index.js"; import { materializeExtractCorpus, removeMaterializedCorpus } from "./helpers/extract-corpus.js"; import { deriveFixtureGraph } from "./helpers/fixture-graph.js"; +import { + ref as projectionRef, + specTest as projectionTest, + testAnchorId as projectionTestId, +} from "@libar-dev/software-delivery-protocol"; + +const projectionsModelTestAnchor = projectionTest({ + id: projectionTestId("test:protocol.projections-model.rendering"), + label: "graph-only rendering tests verify pure disposable projection pages", + verifies: projectionRef("spec:consumers.projections-model"), +}); +void projectionsModelTestAnchor; + const exampleRoot = fileURLToPath(new URL("../examples/checkout-v1", import.meta.url)); const goldenRoot = fileURLToPath( new URL("./fixtures/checkout-v1/expected-design-review", import.meta.url), @@ -98,10 +111,16 @@ describe("the Design Review — the one generated read-only view", () => { "## Narrative\n\nThe Protocol's own delivery model exercises the same carrier, graph, checks, and projections offered to consumers.\n\n**Readiness:", ); expect(page).toContain("## Intent\n\n- **outcome:"); - expect(index).toContain("schema `0.8.0`"); - expect(pack).toContain( - "| [`spec:protocol.self-hosting`](../spec/protocol.self-hosting.md) The Protocol authors and validates itself | behavior | epic | defined | ready | none | none |", + expect(index).toContain("schema `0.9.0`"); + const row = pack + .split("\n") + .find((line) => + line.startsWith("| [`spec:protocol.self-hosting`](../spec/protocol.self-hosting.md) ("), + ); + expect(row).toContain( + ") The Protocol authors and validates itself | behavior | epic | defined | ready | ready · nothing blocks | 0 | 0 | 0 |", ); + expect(row?.endsWith("| none | none | none |")).toBe(true); }); it("renders every owned description as escaped prose and omits absent prose artifacts", () => { @@ -257,6 +276,56 @@ describe("the Design Review — the one generated read-only view", () => { }).toThrow(); }); + it("percent-encodes source link targets on Spec and Pack headers and every binding", () => { + const graph = extract({ root: exampleRoot }).graph; + const oddPath = "odd (dir)/file name#x.ts"; + const pages = renderDesignReview( + createReader({ + ...graph, + nodes: graph.nodes.map((node) => ({ ...node, file: oddPath })), + }), + ); + for (const path of ["spec/orders.create-order.md", "pack/checkout-v1.md"]) { + const page = pageByPath(pages, path); + const targets = [...page.matchAll(/\]\(([^\n]+?)\)/gu)].map((match) => match[1] ?? ""); + const sourceTargets = targets.filter((target) => target.startsWith("../../../")); + expect(sourceTargets.length).toBeGreaterThan(0); + expect( + sourceTargets.every((target) => + target.startsWith("../../../odd%20%28dir%29/file%20name%23x.ts"), + ), + ).toBe(true); + } + }); + + it("percent-encodes page link targets on the index and detail pages", () => { + // Off-contract ids stay visible in diagnostic pages and still need safe link targets. + const graph = extract({ root: exampleRoot }).graph; + const rename = (id: string) => id.replace("orders.create-order", "orders.create (order)"); + const pages = renderDesignReview( + createReader({ + ...graph, + nodes: graph.nodes.map((node) => + node.nodeType === "Pack" + ? { ...node, id: rename(node.id), members: node.members.map(rename) } + : { ...node, id: rename(node.id) }, + ), + edges: graph.edges.map((edge) => ({ + ...edge, + from: rename(edge.from), + to: rename(edge.to), + })), + }), + ); + expect(pageByPath(pages, "index.md")).toContain("](" + "spec/orders.create%20%28order%29.md)"); + expect(pageByPath(pages, "pack/checkout-v1.md")).toContain( + "../spec/orders.create%20%28order%29.md)", + ); + expect(pageByPath(pages, "spec/orders.create (order).md")).toContain( + "orders.create%20%28order%29.valid-cart.md)", + ); + }); + it("golden correctness oracle: the renderer produces the right view, page set and bytes", () => { const golden = readGoldenPages(goldenRoot); diff --git a/test/entry-locations.test.ts b/test/entry-locations.test.ts new file mode 100644 index 00000000..439429cd --- /dev/null +++ b/test/entry-locations.test.ts @@ -0,0 +1,250 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; +import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; + +import { extract, reifyMarkdownCarrier, serializeGraph } from "../src/index.js"; +import type { GraphSchema } from "../src/graph/schema.js"; +import { deriveGraph } from "../src/extract/derive.js"; +import { serializeGraphStructure } from "./helpers/graph-structure.js"; + +const entryLocationsTestAnchor = specTest({ + id: testAnchorId("test:protocol.entry-locations"), + verifies: ref("spec:extraction.entry-locations"), +}); +void entryLocationsTestAnchor; + +const roots: string[] = []; +function corpus(file: string, source: string): string { + const root = mkdtempSync(join(tmpdir(), "sdp-entry-locations-")); + roots.push(root); + writeFileSync(join(root, file), source, "utf8"); + return root; +} + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +const markdown = `--- +id: spec:fixture.locations +kind: behavior +altitude: story +readiness: idea +relations: {} +--- +# Entry locations + +## Intent +- outcome: Locate authored entries. + +### Open questions +- [non-blocking #choice] Which choice? +- [blocking] Who decides? + +## Design +Design prose has no row. + +- zeta: Last entry. More on the last entry. + +- alpha: First entry. + +## UI +UI prose has no row. + +- screen: The screen. +`; + +const typeScript = `import { spec, specId } from "@libar-dev/software-delivery-protocol"; +export const subject = spec({ + id: specId("spec:fixture.locations"), + title: "Entry locations", + kind: "behavior", altitude: "story", readiness: "idea", + intent: { + outcome: "Locate authored entries.", + openQuestions: [ + { + key: "choice", + question: "Which choice?", + blocking: false, + }, + { question: "Who decides?", blocking: true }, + ], + }, + design: ({ + description: "Design prose has no row.", + zeta: "Last entry. More on the last entry.", + alpha: "First entry.", + } as const), + ui: { description: "UI prose has no row.", screen: "The screen." }, +}); +`; + +const gherkin = `@spec.fixture.gherkin-locations @altitude.story @readiness.idea +Feature: Gherkin locations + - outcome: Use the closed Gherkin description grammar. + + @spec.fixture.gherkin-locations.child @altitude.story @readiness.idea + Scenario: A child + Given a carrier + When extracted + Then the graph is derived + + Rule: The carrier has no open-question field. +`; + +describe("entry locations", () => { + it("records Markdown Design, UI, keyed and unkeyed questions at their first item lines", () => { + const { graph, report } = extract({ root: corpus("subject.sdp.md", markdown) }); + expect(report.findings).toEqual([]); + expect(graph.locations).toEqual([ + { spec: "spec:fixture.locations", file: "subject.sdp.md", entry: "design.alpha", line: 22 }, + { spec: "spec:fixture.locations", file: "subject.sdp.md", entry: "design.zeta", line: 20 }, + { + spec: "spec:fixture.locations", + file: "subject.sdp.md", + entry: "question[0]", + key: "choice", + line: 14, + }, + { spec: "spec:fixture.locations", file: "subject.sdp.md", entry: "question[1]", line: 15 }, + { spec: "spec:fixture.locations", file: "subject.sdp.md", entry: "ui.screen", line: 27 }, + ]); + }); + + it("records TypeScript property lines through transparent wrappers", () => { + const { graph, report } = extract({ root: corpus("subject.sdp.ts", typeScript) }); + expect(report.findings).toEqual([]); + expect(graph.locations).toEqual([ + { spec: "spec:fixture.locations", file: "subject.sdp.ts", entry: "design.alpha", line: 20 }, + { spec: "spec:fixture.locations", file: "subject.sdp.ts", entry: "design.zeta", line: 19 }, + { + spec: "spec:fixture.locations", + file: "subject.sdp.ts", + entry: "question[0]", + key: "choice", + line: 11, + }, + { spec: "spec:fixture.locations", file: "subject.sdp.ts", entry: "question[1]", line: 14 }, + { spec: "spec:fixture.locations", file: "subject.sdp.ts", entry: "ui.screen", line: 22 }, + ]); + }); + + it("locates TypeScript string questions and omits properties dropped as non-static", () => { + const source = typeScript + .replace("openQuestions: [", 'openQuestions: [\n "A prose question",') + .replace('alpha: "First entry."', "alpha: dynamicValue"); + const { graph, report } = extract({ root: corpus("subject.sdp.ts", source) }); + expect(report.findings.map((finding) => finding.validatorId)).toEqual([ + "extract/non-static-section", + ]); + expect(graph.locations).toContainEqual({ + spec: "spec:fixture.locations", + file: "subject.sdp.ts", + entry: "question[0]", + line: 9, + }); + expect(graph.locations).not.toContainEqual(expect.objectContaining({ entry: "design.alpha" })); + }); + + it("emits no Gherkin rows because its closed grammar carries no open questions", () => { + const root = corpus("subject.sdp.gherkin", gherkin); + const result = extract({ root }); + expect(result.report.findings).toEqual([]); + expect(result.graph.nodes).toHaveLength(2); + expect(result.graph.locations).toEqual([]); + writeFileSync( + join(root, "subject.sdp.gherkin"), + gherkin.replace(" - outcome:", " - openQuestions: Which choice?\n - outcome:"), + ); + const refused = extract({ root }); + expect(refused.graph.locations).toEqual([]); + const grammarFinding = refused.report.findings.find( + (finding) => finding.validatorId === "extract/gherkin-grammar", + ); + expect(grammarFinding?.message).toContain( + 'description key "openQuestions" is outside the closed set', + ); + }); + + it("sorts Specs and entries in code-unit order and serializes reversed rows identically", () => { + const root = corpus("z.sdp.md", markdown.replace("spec:fixture.locations", "spec:fixture.a")); + writeFileSync( + join(root, "a.sdp.md"), + markdown.replace("spec:fixture.locations", "spec:fixture.Z"), + ); + const graph = extract({ root }).graph; + expect(graph.locations?.map(({ spec, entry }) => `${spec}#${entry}`)).toEqual([ + "spec:fixture.Z#design.alpha", + "spec:fixture.Z#design.zeta", + "spec:fixture.Z#question[0]", + "spec:fixture.Z#question[1]", + "spec:fixture.Z#ui.screen", + "spec:fixture.a#design.alpha", + "spec:fixture.a#design.zeta", + "spec:fixture.a#question[0]", + "spec:fixture.a#question[1]", + "spec:fixture.a#ui.screen", + ]); + expect(serializeGraph({ ...graph, locations: [...(graph.locations ?? [])].reverse() })).toBe( + serializeGraph(graph), + ); + }); + + it("keeps Primitive and Pack nodes line-free when an edit above entries moves their lines", () => { + const root = corpus("subject.sdp.md", markdown); + writeFileSync( + join(root, "group.pack.sdp.md"), + `---\nid: pack:fixture.locations\nspecs:\n - spec:fixture.locations\n---\n# Location group\n`, + ); + const first = extract({ root }).graph; + writeFileSync(join(root, "subject.sdp.md"), markdown.replace("## Intent", "\n\n## Intent")); + const second = extract({ root }).graph; + expect(serializeGraphStructure(second)).toBe(serializeGraphStructure(first)); + expect(second.locations).toEqual( + first.locations?.map((row) => ({ ...row, line: row.line + 2 })), + ); + for (const node of second.nodes) { + expect(node).not.toHaveProperty("line"); + expect(node).not.toHaveProperty("entryLines"); + expect(node).not.toHaveProperty("locations"); + } + expect(second.nodes.map((node) => node.nodeType).sort()).toEqual(["Pack", "Primitive"]); + }); + + it("compares carrier parity on nodes and edges while locations differ", () => { + const markdownGraph = extract({ root: corpus("subject.sdp.md", markdown) }).graph; + const typeScriptGraph = extract({ root: corpus("subject.sdp.ts", typeScript) }).graph; + expect(markdownGraph.locations).not.toEqual(typeScriptGraph.locations); + expect( + serializeGraphStructure(markdownGraph).replaceAll("subject.sdp.md", "subject.carrier"), + ).toBe( + serializeGraphStructure(typeScriptGraph).replaceAll("subject.sdp.ts", "subject.carrier"), + ); + }); + + it("serializes an empty table for entries without recorded carrier lines", () => { + const reified = reifyMarkdownCarrier(markdown, "subject.sdp.md"); + const specs = reified.specs.map(({ data, id, file, line }) => ({ data, id, file, line })); + const graph = deriveGraph(specs, [], []); + expect(graph.locations).toEqual([]); + expect( + ( + JSON.parse( + serializeGraph({ + schemaVersion: graph.schemaVersion, + nodes: graph.nodes, + edges: graph.edges, + }), + ) as GraphSchema + ).locations, + ).toEqual([]); + }); + + it("extracts the location table byte-identically twice", () => { + const root = corpus("subject.sdp.md", markdown); + expect(serializeGraph(extract({ root }).graph)).toBe(serializeGraph(extract({ root }).graph)); + }); +}); diff --git a/test/extract-parity.test.ts b/test/extract-parity.test.ts index ab545ed6..4201294a 100644 --- a/test/extract-parity.test.ts +++ b/test/extract-parity.test.ts @@ -1,3 +1,4 @@ +import { deriveGraph } from "../src/extract/derive.js"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -14,6 +15,8 @@ import { } from "../src/index.js"; import type { Finding } from "../src/validate/contracts.js"; +import { serializeGraphStructure } from "./helpers/graph-structure.js"; + const fixtureRoot = new URL("./fixtures/import/parity/", import.meta.url); type FindingId = (typeof extractFindingIds)[keyof typeof extractFindingIds]; @@ -212,3 +215,38 @@ describe("the bounded TypeScript and Markdown refusal-parity matrix", () => { expect(markdown.specs).toEqual([]); }); }); + +describe("carrier parity excludes entry locations", () => { + it("compares the nodes and edges of accepted Design entries", () => { + const markdown = reifyMarkdownCarrier( + `--- +id: spec:fixture.location-parity +kind: model +altitude: story +readiness: idea +relations: {} +--- +# Location parity + +## Design +- shape: A shared design. +`, + "twin.sdp", + ); + const typeScript = reifyTypeScriptCarrier( + `import { spec, specId } from "@libar-dev/software-delivery-protocol"; +export const subject = spec({ + id: specId("spec:fixture.location-parity"), title: "Location parity", + kind: "model", altitude: "story", readiness: "idea", + design: { shape: "A shared design." }, +});`, + "twin.sdp", + ); + expect(markdown.findings).toEqual([]); + expect(typeScript.findings).toEqual([]); + const left = deriveGraph(markdown.specs, [], []); + const right = deriveGraph(typeScript.specs, [], []); + expect(left.locations).not.toEqual(right.locations); + expect(serializeGraphStructure(left)).toBe(serializeGraphStructure(right)); + }); +}); diff --git a/test/extract.test.ts b/test/extract.test.ts index 1706c5a9..e09168d5 100644 --- a/test/extract.test.ts +++ b/test/extract.test.ts @@ -25,6 +25,8 @@ import { removeMaterializedCorpus, } from "./helpers/extract-corpus.js"; +import { serializeGraphStructure } from "./helpers/graph-structure.js"; + const exampleRoot = fileURLToPath(new URL("../examples/checkout-v1", import.meta.url)); const materializedRoots: string[] = []; @@ -387,11 +389,11 @@ The Model prose. ]); } expect( - serializeGraph(typeScript.graph).replace( + serializeGraphStructure(typeScript.graph).replace( '"file": "specs/terms.sdp.ts"', '"file": "specs/terms.sdp.md"', ), - ).toBe(serializeGraph(markdown.graph)); + ).toBe(serializeGraphStructure(markdown.graph)); }); it("integer-like keys: refuses numeric and string integer-like names in design and keeps the Spec with its other keys", () => { @@ -1319,8 +1321,8 @@ describe("Markdown carrier discovery", () => { const markdown = extract({ root: markdownRoot }); const typeScript = extract({ root: typeScriptRoot }); - const markdownSerialized = serializeGraph(markdown.graph); - const typeScriptSerialized = serializeGraph(typeScript.graph); + const markdownSerialized = serializeGraphStructure(markdown.graph); + const typeScriptSerialized = serializeGraphStructure(typeScript.graph); // This is accepted-subset equivalence only; full Markdown/TS parity waits for multi-entry // constraint syntax and the deferred hardening work. diff --git a/test/fixtures/checkout-v1/expected-census/index.md b/test/fixtures/checkout-v1/expected-census/index.md index 0cd8f361..8e1b04b6 100644 --- a/test/fixtures/checkout-v1/expected-census/index.md +++ b/test/fixtures/checkout-v1/expected-census/index.md @@ -1,6 +1,6 @@ # Census -A disposable projection of the one graph (schema `0.8.0`): 17 nodes · 32 edges · 11 Specs. +A disposable projection of the one graph (schema `0.9.0`): 17 nodes · 32 edges · 11 Specs. ## Spec kinds diff --git a/test/fixtures/checkout-v1/expected-design-review/index.md b/test/fixtures/checkout-v1/expected-design-review/index.md index 9e11881a..c6e1d3cd 100644 --- a/test/fixtures/checkout-v1/expected-design-review/index.md +++ b/test/fixtures/checkout-v1/expected-design-review/index.md @@ -1,6 +1,6 @@ # Design Review -The one generated read-only view — a pure projection of the one graph (`graph.json`, schema `0.8.0`): 17 nodes · 32 edges. +The one generated read-only view — a pure projection of the one graph (`graph.json`, schema `0.9.0`): 17 nodes · 32 edges. ## Specs diff --git a/test/fixtures/checkout-v1/expected-design-review/pack/checkout-v1.md b/test/fixtures/checkout-v1/expected-design-review/pack/checkout-v1.md index 0cddc672..a21553a6 100644 --- a/test/fixtures/checkout-v1/expected-design-review/pack/checkout-v1.md +++ b/test/fixtures/checkout-v1/expected-design-review/pack/checkout-v1.md @@ -4,23 +4,139 @@ > Let customers create orders from valid carts with honest authored traceability. +**Vocabulary (`modelRefs`):** [`spec:orders.order-model`](../spec/orders.order-model.md) — Order-management domain vocabulary + ## Members -| Spec | Kind | Altitude | Stated | Floor reached | Implementation binding | Verifier binding | -|---|---|---|---|---|---|---| -| [`spec:orders.order-management`](../spec/orders.order-management.md) Order management | behavior | epic | defined | ready | none | none | -| [`spec:orders.order-placement-flow`](../spec/orders.order-placement-flow.md) Order placement flow | workflow | feature | scoped | scoped | none | none | -| [`spec:orders.create-order`](../spec/orders.create-order.md) Customer creates an order | behavior | feature | defined | ready | present | present | -| [`spec:orders.create-order.valid-cart`](../spec/orders.create-order.valid-cart.md) Valid cart creates an order | example | story | ready | ready | none | present | -| [`spec:orders.create-order.invalid-cart`](../spec/orders.create-order.invalid-cart.md) Invalid cart is rejected | example | story | defined | ready | none | none | -| [`spec:orders.create-order.api-contract`](../spec/orders.create-order.api-contract.md) Create-order API contract | contract | story | idea | idea | none | none | -| [`spec:orders.order-total-rule`](../spec/orders.order-total-rule.md) Order total matches cart math | rule | story | defined | ready | present | none | -| [`spec:orders.order-inventory-rule`](../spec/orders.order-inventory-rule.md) Order creation requires available inventory | rule | story | defined | ready | none | none | -| [`spec:orders.order-latency-constraint`](../spec/orders.order-latency-constraint.md) Create-order latency stays within checkout budget | constraint | story | defined | ready | none | none | -| [`spec:orders.order-model`](../spec/orders.order-model.md) Order-management domain vocabulary | model | story | defined | ready | none | none | -| [`spec:decisions.order-lifecycle`](../spec/decisions.order-lifecycle.md) Order lifecycle keeps validation before creation | decision | feature | defined | ready | none | none | +One row per member in the manifest's order. The stated next rung is the rung above the stated one, with `nothing blocks` when the floor already holds it and its first unmet clause otherwise. A binding or design reference reads present when a resolving anchor exists, and none otherwise. -**Vocabulary (`modelRefs`):** [`spec:orders.order-model`](../spec/orders.order-model.md) — Order-management domain vocabulary +| Spec | Kind | Altitude | Stated | Floor reached | Stated next rung | Design entries | Declarations | Open questions | Decisions | Design reference | Implementation binding | Verifier binding | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| [`spec:orders.order-management`](../spec/orders.order-management.md) (1) Order management | behavior | epic | defined | ready | ready · nothing blocks | 0 | 0 | 0 | [`spec:decisions.order-lifecycle`](../spec/decisions.order-lifecycle.md) (defined) | none | none | none | +| [`spec:orders.order-placement-flow`](../spec/orders.order-placement-flow.md) (2) Order placement flow | workflow | feature | scoped | scoped | defined · `kind-evidence-complete` | 0 | 0 | 0 | — | none | none | none | +| [`spec:orders.create-order`](../spec/orders.create-order.md) (3) Customer creates an order | behavior | feature | defined | ready | ready · nothing blocks | 0 | 0 | 0 | [`spec:decisions.order-lifecycle`](../spec/decisions.order-lifecycle.md) (defined) | none | present | present | +| [`spec:orders.create-order.valid-cart`](../spec/orders.create-order.valid-cart.md) (4) Valid cart creates an order | example | story | ready | ready | — | 0 | 0 | 0 | — | none | none | present | +| [`spec:orders.create-order.invalid-cart`](../spec/orders.create-order.invalid-cart.md) (5) Invalid cart is rejected | example | story | defined | ready | ready · nothing blocks | 0 | 0 | 0 | — | none | none | none | +| [`spec:orders.create-order.api-contract`](../spec/orders.create-order.api-contract.md) (6) Create-order API contract | contract | story | idea | idea | scoped · `kind-evidence-present` | 0 | 0 | 1 (1 blocking) | — | none | none | none | +| [`spec:orders.order-total-rule`](../spec/orders.order-total-rule.md) (7) Order total matches cart math | rule | story | defined | ready | ready · nothing blocks | 0 | 0 | 0 | — | none | present | none | +| [`spec:orders.order-inventory-rule`](../spec/orders.order-inventory-rule.md) (8) Order creation requires available inventory | rule | story | defined | ready | ready · nothing blocks | 0 | 0 | 0 | — | none | none | none | +| [`spec:orders.order-latency-constraint`](../spec/orders.order-latency-constraint.md) (9) Create-order latency stays within checkout budget | constraint | story | defined | ready | ready · nothing blocks | 0 | 0 | 0 | — | none | none | none | +| [`spec:orders.order-model`](../spec/orders.order-model.md) (10) Order-management domain vocabulary | model | story | defined | ready | ready · nothing blocks | 0 | 0 | 0 | — | none | none | none | +| [`spec:decisions.order-lifecycle`](../spec/decisions.order-lifecycle.md) (11) Order lifecycle keeps validation before creation | decision | feature | defined | ready | ready · nothing blocks | 0 | 0 | 0 | — | none | none | none | + +## Next rung + +The floor clauses that fail at the rung above each member's stated rung, with the targets a failing dependency clause names. This is a report, never a promotion: only an author's edit states a rung. + +### [`spec:orders.order-placement-flow`](../spec/orders.order-placement-flow.md) Order placement flow + +Stated `scoped`; the floor does not hold `defined`: + +- `kind-evidence-complete` — The kind's natural evidence is complete (per-kind evidence table). + +### [`spec:orders.create-order.api-contract`](../spec/orders.create-order.api-contract.md) Create-order API contract + +Stated `idea`; the floor does not hold `scoped`: + +- `kind-evidence-present` — The kind's natural evidence is present (per-kind evidence table). + +### Waiting for the author's statement + +The floor already holds these members at their next rung. That is not approval: the page records no review, and the rung stays unstated until its author states it. + +- stated `defined`, next `ready`: [`spec:orders.order-management`](../spec/orders.order-management.md) Order management +- stated `defined`, next `ready`: [`spec:orders.create-order`](../spec/orders.create-order.md) Customer creates an order +- stated `defined`, next `ready`: [`spec:orders.create-order.invalid-cart`](../spec/orders.create-order.invalid-cart.md) Invalid cart is rejected +- stated `defined`, next `ready`: [`spec:orders.order-total-rule`](../spec/orders.order-total-rule.md) Order total matches cart math +- stated `defined`, next `ready`: [`spec:orders.order-inventory-rule`](../spec/orders.order-inventory-rule.md) Order creation requires available inventory +- stated `defined`, next `ready`: [`spec:orders.order-latency-constraint`](../spec/orders.order-latency-constraint.md) Create-order latency stays within checkout budget +- stated `defined`, next `ready`: [`spec:orders.order-model`](../spec/orders.order-model.md) Order-management domain vocabulary +- stated `defined`, next `ready`: [`spec:decisions.order-lifecycle`](../spec/decisions.order-lifecycle.md) Order lifecycle keeps validation before creation + +## Boundary + +### Rests on + +The Specs outside the Pack that a member refines, depends on, is constrained by or is decided by. + +None. + +### Rested on by + +The Specs outside the Pack that refine, depend on, are constrained by or are decided by a member. + +None. + +### Verified by + +The Specs outside the Pack that verify a member. + +None. + +### Superseded by + +The Specs outside the Pack that supersede a member. + +None. + +## Open questions + +### [`spec:orders.create-order.api-contract`](../spec/orders.create-order.api-contract.md) Create-order API contract + +- **blocking** — Does the response carry the inventory-snapshot version the order was validated against? ([specs/orders/create-order-api-contract.sdp.md:17](../../../specs/orders/create-order-api-contract.sdp.md#L17)) + +## Code + +The code units that realize each member, and those that answer to its design without claiming to realize it, each with its role and its component's layer and context. A reference confers no implementation binding. + +### [`spec:orders.order-management`](../spec/orders.order-management.md) Order management + +No implementation or design-reference binding is recorded for this member. + +### [`spec:orders.order-placement-flow`](../spec/orders.order-placement-flow.md) Order placement flow + +No implementation or design-reference binding is recorded for this member. + +### [`spec:orders.create-order`](../spec/orders.create-order.md) Customer creates an order + +#### Implementations + +- `api:orders.post` — POST /orders ([src/orders/create-order.route.ts:6](../../../src/orders/create-order.route.ts#L6)) `[anchored]` +- `impl:orders.create-order-use-case` — createOrderFromCart ([src/orders/create-order.use-case.ts:23](../../../src/orders/create-order.use-case.ts#L23)) `[anchored]` + +### [`spec:orders.create-order.valid-cart`](../spec/orders.create-order.valid-cart.md) Valid cart creates an order + +No implementation or design-reference binding is recorded for this member. + +### [`spec:orders.create-order.invalid-cart`](../spec/orders.create-order.invalid-cart.md) Invalid cart is rejected + +No implementation or design-reference binding is recorded for this member. + +### [`spec:orders.create-order.api-contract`](../spec/orders.create-order.api-contract.md) Create-order API contract + +No implementation or design-reference binding is recorded for this member. + +### [`spec:orders.order-total-rule`](../spec/orders.order-total-rule.md) Order total matches cart math + +#### Implementations + +- `impl:orders.order-total` — cart-math order total ([src/orders/create-order.use-case.ts:31](../../../src/orders/create-order.use-case.ts#L31)) `[anchored]` + +### [`spec:orders.order-inventory-rule`](../spec/orders.order-inventory-rule.md) Order creation requires available inventory + +No implementation or design-reference binding is recorded for this member. + +### [`spec:orders.order-latency-constraint`](../spec/orders.order-latency-constraint.md) Create-order latency stays within checkout budget + +No implementation or design-reference binding is recorded for this member. + +### [`spec:orders.order-model`](../spec/orders.order-model.md) Order-management domain vocabulary + +No implementation or design-reference binding is recorded for this member. + +### [`spec:decisions.order-lifecycle`](../spec/decisions.order-lifecycle.md) Order lifecycle keeps validation before creation + +No implementation or design-reference binding is recorded for this member. ## Verifier coverage gaps diff --git a/test/fixtures/checkout-v1/expected-graph.json b/test/fixtures/checkout-v1/expected-graph.json index 9c1e554c..3f198d27 100644 --- a/test/fixtures/checkout-v1/expected-graph.json +++ b/test/fixtures/checkout-v1/expected-graph.json @@ -1,5 +1,5 @@ { - "schemaVersion": "0.8.0", + "schemaVersion": "0.9.0", "nodes": [ { "id": "api:orders.post", diff --git a/test/fixtures/extract/annotation-forms/expected-graph.json b/test/fixtures/extract/annotation-forms/expected-graph.json index ad2284d6..e76b9722 100644 --- a/test/fixtures/extract/annotation-forms/expected-graph.json +++ b/test/fixtures/extract/annotation-forms/expected-graph.json @@ -1,5 +1,5 @@ { - "schemaVersion": "0.8.0", + "schemaVersion": "0.9.0", "nodes": [ { "id": "component:platform.gateway", diff --git a/test/gherkin-parity.test.ts b/test/gherkin-parity.test.ts index 263a5f8a..42d27b69 100644 --- a/test/gherkin-parity.test.ts +++ b/test/gherkin-parity.test.ts @@ -6,6 +6,8 @@ import { afterEach, describe, expect, it } from "vitest"; import { extract, generateContracts, serializeGraph } from "../src/index.js"; import { materializeGherkinCorpus, removeMaterializedCorpus } from "./helpers/extract-corpus.js"; +import { serializeGraphStructure } from "./helpers/graph-structure.js"; + const temporaryRoots: string[] = []; function materialize(name: string): string { @@ -27,8 +29,11 @@ describe("Gherkin carrier graph parity", () => { const markdown = extract({ root: markdownRoot }); const gherkin = extract({ root: gherkinRoot }); - const markdownGraph = serializeGraph(markdown.graph).replaceAll("twin.sdp.md", "twin.carrier"); - const gherkinGraph = serializeGraph(gherkin.graph).replaceAll( + const markdownGraph = serializeGraphStructure(markdown.graph).replaceAll( + "twin.sdp.md", + "twin.carrier", + ); + const gherkinGraph = serializeGraphStructure(gherkin.graph).replaceAll( "twin.sdp.gherkin", "twin.carrier", ); diff --git a/test/gherkin-view.test.ts b/test/gherkin-view.test.ts index 6a0bd23f..bcf18b2c 100644 --- a/test/gherkin-view.test.ts +++ b/test/gherkin-view.test.ts @@ -54,6 +54,13 @@ function specContext(overrides: Partial<SpecContext> = {}): SpecContext { const summary = specSummary(overrides); return { ...summary, + designColumns: { + entries: 0, + declarations: 0, + blockingQuestions: 0, + openQuestions: [], + decisions: [], + }, floorFailures: [], nextRungFailures: [], relationsOut: [], @@ -61,6 +68,7 @@ function specContext(overrides: Partial<SpecContext> = {}): SpecContext { implementations: [], references: [], verifiers: [], + entryLocations: [], findings: [], ...overrides, }; diff --git a/test/graph-schema.test.ts b/test/graph-schema.test.ts index b1dc9891..6840667c 100644 --- a/test/graph-schema.test.ts +++ b/test/graph-schema.test.ts @@ -1,6 +1,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; @@ -9,6 +10,7 @@ import { deliveryFactNames, derivedEdgeTypes, extract, + graphClaims, graphEdgeTypes, graphNodeTypes, renderDesignReview, @@ -29,8 +31,15 @@ const openSectionOrderTestAnchor = specTest({ }); void openSectionOrderTestAnchor; +const claimTaxonomyTestAnchor = specTest({ + id: testAnchorId("test:protocol.claim-taxonomy"), + label: "schema and extraction tests verify claim categories and inheritance", + verifies: ref("spec:extraction.claim-taxonomy"), +}); +void claimTaxonomyTestAnchor; + describe("graph schema", () => { - it("serializes authored entry order with description first and schema 0.8.0", () => { + it("serializes authored entry order with description first and schema 0.9.0", () => { const graph = deriveFixtureGraph({ specs: [ spec({ @@ -53,7 +62,7 @@ describe("graph schema", () => { }); const serialized = JSON.parse(serializeGraph(graph)) as GraphSchema; const node = serialized.nodes.find((entry) => entry.nodeType === "Primitive"); - expect(serialized.schemaVersion).toBe("0.8.0"); + expect(serialized.schemaVersion).toBe("0.9.0"); expect(Object.keys(node?.sections?.design ?? {})).toEqual([ "description", "zeta", @@ -196,8 +205,43 @@ The Model prose. ]); }); + it("inherits the Pack's declared claim for membership and keeps binding claims apart", () => { + const root = fileURLToPath(new URL("../examples/checkout-v1", import.meta.url)); + const { graph, report } = extract({ root }); + expect(report.findings).toEqual([]); + const pack = graph.nodes.find((node) => node.nodeType === "Pack"); + if (pack?.nodeType !== "Pack") throw new Error("Missing example Pack."); + expect(pack.claim).toBe("declared"); + const membership = graph.edges.filter((edge) => edge.type === "belongsTo"); + expect(membership.map((edge) => edge.from).sort()).toEqual([...pack.members].sort()); + expect(membership.every((edge) => edge.to === pack.id && edge.claim === pack.claim)).toBe(true); + const verifies = graph.edges.filter((edge) => edge.type === "verifies"); + expect(new Set(verifies.map((edge) => edge.claim))).toEqual(new Set(["declared", "anchored"])); + expect( + verifies + .filter((edge) => edge.claim === "declared") + .every((edge) => edge.from.startsWith("spec:")), + ).toBe(true); + expect( + verifies + .filter((edge) => edge.claim === "anchored") + .every((edge) => edge.from.startsWith("test:")), + ).toBe(true); + expect(graph.edges.every((edge) => graphClaims.includes(edge.claim))).toBe(true); + expect(graph.nodes.every((node) => graphClaims.includes(node.claim))).toBe(true); + expect(graph.edges.some((edge) => edge.type === "satisfies" && edge.claim === "anchored")).toBe( + true, + ); + expect( + graph.nodes.some( + (node) => node.nodeType === "Primitive" && node.deliveryFacts?.includes("implemented"), + ), + ).toBe(true); + }); + it("exports the graph schema contracts", () => { - expect(schemaVersion).toBe("0.8.0"); + expect(schemaVersion).toBe("0.9.0"); + expect(graphClaims).toEqual(["declared", "anchored", "inferred"]); expect(graphNodeTypes).toEqual(["Primitive", "Pack", "Anchor", "CodeNode"]); expect(deliveryFactNames).toEqual(["implemented", "has-verifier", "observed"]); expect(derivedEdgeTypes).toEqual([ diff --git a/test/helpers/graph-structure.ts b/test/helpers/graph-structure.ts new file mode 100644 index 00000000..866788c5 --- /dev/null +++ b/test/helpers/graph-structure.ts @@ -0,0 +1,8 @@ +import type { GraphSchema } from "../../src/graph/schema.js"; +import { serializeGraph } from "../../src/extract/serialize.js"; + +/** Carrier parity and node goldens exclude the carrier-specific location table. */ +export function serializeGraphStructure(graph: GraphSchema): string { + const { schemaVersion, nodes, edges } = JSON.parse(serializeGraph(graph)) as GraphSchema; + return `${JSON.stringify({ schemaVersion, nodes, edges }, null, 2)}\n`; +} diff --git a/test/helpers/pack-design-probe.ts b/test/helpers/pack-design-probe.ts new file mode 100644 index 00000000..c12d90c5 --- /dev/null +++ b/test/helpers/pack-design-probe.ts @@ -0,0 +1,188 @@ +import { + codeAnchor, + codeAnchorId, + componentAnchorId, + decidedBy, + dependsOn, + pack, + packId, + ref, + refines, + spec, + specId, + specTest, + testAnchorId, + verifies, +} from "../../src/index.js"; +import type { EntryLocation, GraphSchema } from "../../src/index.js"; +import { deriveFixtureGraph } from "./fixture-graph.js"; + +/** + * The probe the Pack design tests share: a Pack whose members hold, wait at, or stay below their + * stated next rung; carry design entries, pinned declarations, questions and decisions; are + * realized, referenced and verified; rest on Specs outside the Pack and are rested on by others; + * and include one member the graph does not hold. + */ + +export const PACK_ID = "pack:probe.design"; +export const CARRIER = "specs/fixture.sdp.ts"; + +/** A rule Spec with its statement in place, so the kind's evidence never holds a probe back. */ +function rule( + id: string, + readiness: "idea" | "scoped" | "defined" | "ready", + extra: Partial<Parameters<typeof spec>[0]> = {}, +): ReturnType<typeof spec> { + return spec({ + id: specId(id), + title: `Probe ${id.slice("spec:probe.".length)}`, + kind: "rule", + altitude: "story", + readiness, + intent: { outcome: `Probe ${id}.` }, + behavior: { rules: ["The probe states one rule."] }, + ...extra, + }); +} + +function example(id: string): ReturnType<typeof spec> { + return spec({ + id: specId(id), + title: `Probe ${id.slice("spec:probe.".length)}`, + kind: "example", + altitude: "story", + readiness: "idea", + intent: { outcome: `Probe ${id}.` }, + relations: [refines(specId("spec:probe.waiting")), verifies(specId("spec:probe.waiting"))], + }); +} + +/** The location table rows, written out of order: the reader sorts each Spec's rows by entry. */ +export const PROBE_LOCATIONS: readonly EntryLocation[] = [ + { spec: "spec:probe.blocked", file: CARRIER, entry: "question[0]", key: "scopeCut", line: 14 }, + { spec: "spec:probe.held", file: CARRIER, entry: "question[0]", key: "basisOwner", line: 21 }, + { spec: "spec:probe.held", file: CARRIER, entry: "design.tableOrders", line: 31 }, + { spec: "spec:probe.held", file: CARRIER, entry: "design.fnCreate", line: 30 }, +]; + +export function packDesignProbeGraph(): GraphSchema { + const graph = deriveFixtureGraph({ + specs: [ + rule("spec:probe.held", "defined", { + intent: { + outcome: "Hold the design.", + openQuestions: [ + { question: "Who owns the basis?", blocking: false, key: "basisOwner" }, + "Is the <name> | # final?", + ], + }, + design: { + description: "The held design.", + fnCreate: "`create(input: Input): Order` builds the order", + tableOrders: "``orders: Map<Id, `Order`>`` keeps them", + note: "Plain prose is not a declaration.", + lateSpan: "Prose before `code` is not a declaration.", + }, + ui: { description: "UI prose.", layout: "Two columns.", signature: "`x(): y`" }, + relations: [ + dependsOn(specId("spec:probe.outside-basis")), + decidedBy(specId("spec:probe.choice")), + ], + }), + rule("spec:probe.waiting", "defined", { + relations: [refines(specId("spec:probe.held"))], + }), + rule("spec:probe.blocked", "scoped", { + intent: { + outcome: "Stay below defined.", + openQuestions: [{ question: "Where is the cut?", blocking: true, key: "scopeCut" }], + }, + relations: [ + dependsOn(specId("spec:probe.outside-basis")), + decidedBy(specId("spec:probe.absent")), + ], + }), + rule("spec:probe.modest", "idea", { + relations: [dependsOn(specId("spec:probe.outside-basis"))], + }), + rule("spec:probe.top", "ready", { relations: [refines(specId("spec:probe.held"))] }), + rule("spec:probe.outside-basis", "scoped", { + relations: [refines(specId("spec:probe.held"))], + }), + spec({ + id: specId("spec:probe.choice"), + title: "Probe choice", + kind: "decision", + altitude: "story", + readiness: "defined", + intent: { outcome: "Choose." }, + decision: { context: "A choice is due.", decision: "Choose the probe." }, + relations: [dependsOn(specId("spec:probe.outside-basis"))], + }), + spec({ + id: specId("spec:probe.vocabulary"), + title: "Probe vocabulary", + kind: "model", + altitude: "story", + readiness: "idea", + intent: { outcome: "Name the probe's terms." }, + model: { terms: { probe: "a synthetic Spec" } }, + }), + example("spec:probe.waiting.case"), + example("spec:probe.waiting.unbound"), + // A member that verifies a Spec outside the Pack: a relation, never something it rests on. + spec({ + id: specId("spec:probe.check"), + title: "Probe check", + kind: "example", + altitude: "story", + readiness: "idea", + intent: { outcome: "Check the basis." }, + relations: [ + refines(specId("spec:probe.held")), + verifies(specId("spec:probe.outside-basis")), + ], + }), + ], + packs: [ + pack({ + id: packId(PACK_ID), + title: "Probe design", + framing: "The probe's design as it stands.", + specs: [ + specId("spec:probe.held"), + specId("spec:probe.waiting"), + specId("spec:probe.blocked"), + specId("spec:probe.missing"), + specId("spec:probe.modest"), + specId("spec:probe.top"), + specId("spec:probe.check"), + ], + modelRefs: [specId("spec:probe.vocabulary")], + }), + ], + anchors: [ + codeAnchor({ + id: codeAnchorId("component:probe.core"), + label: "the probe core", + layer: "domain", + context: "probe", + }), + codeAnchor({ + id: codeAnchorId("impl:probe.waiting"), + label: "realizes the waiting rule", + satisfies: ref("spec:probe.waiting"), + component: componentAnchorId("component:probe.core"), + role: "service", + }), + codeAnchor({ + id: codeAnchorId("impl:probe.helper"), + references: [ref("spec:probe.blocked")], + }), + specTest({ id: testAnchorId("test:probe.waiting"), verifies: ref("spec:probe.waiting") }), + specTest({ id: testAnchorId("test:probe.case"), verifies: ref("spec:probe.waiting.case") }), + ], + }); + + return { ...graph, locations: PROBE_LOCATIONS }; +} diff --git a/test/import-round-trip.test.ts b/test/import-round-trip.test.ts index f8b27b7b..c13302ed 100644 --- a/test/import-round-trip.test.ts +++ b/test/import-round-trip.test.ts @@ -13,9 +13,18 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; +import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; + import { emitMarkdownSpec, reifyTypeScriptCarrier } from "../src/index.js"; import { assertAuthoredRoundTrip, assertGraphRoundTrip } from "./import-round-trip.helpers.js"; +const markdownAuthoringTestAnchor = specTest({ + id: testAnchorId("test:protocol.markdown-authoring"), + label: "TypeScript and Markdown twins derive one graph, verifying Markdown authoring", + verifies: ref("spec:carrier.markdown-authoring"), +}); +void markdownAuthoringTestAnchor; + const fixtureRoot = fileURLToPath(new URL("./fixtures/import/round-trip", import.meta.url)); function materializeMarkdownCorpus(): { readonly tsRoot: string; readonly mdRoot: string } { diff --git a/test/markdown-reifier.test.ts b/test/markdown-reifier.test.ts index cc5482be..8262e3a5 100644 --- a/test/markdown-reifier.test.ts +++ b/test/markdown-reifier.test.ts @@ -11,6 +11,8 @@ import { serializeGraph, } from "../src/index.js"; +import { serializeGraphStructure } from "./helpers/graph-structure.js"; + const fixtureRoot = new URL("./fixtures/extract/self-hosting-carrier/", import.meta.url); const fixturePaths = [ @@ -784,8 +786,8 @@ export const prose = spec({ const typeScriptResult = reifyTypeScriptCarrier(typeScript, "carrier.sdp"); const markdownGraph = deriveGraph(markdownResult.specs, markdownResult.packs, []); const typeScriptGraph = deriveGraph(typeScriptResult.specs, typeScriptResult.packs, []); - const markdownSerialized = serializeGraph(markdownGraph); - const typeScriptSerialized = serializeGraph(typeScriptGraph); + const markdownSerialized = serializeGraphStructure(markdownGraph); + const typeScriptSerialized = serializeGraphStructure(typeScriptGraph); expect(markdownResult.findings).toEqual([]); expect(typeScriptResult.findings).toEqual([]); diff --git a/test/pack-design.test.ts b/test/pack-design.test.ts new file mode 100644 index 00000000..b7f3243a --- /dev/null +++ b/test/pack-design.test.ts @@ -0,0 +1,482 @@ +import { describe, expect, it } from "vitest"; + +import { createReader, pack, packId, schemaVersion, spec, specId, verifies } from "../src/index.js"; +import type { + GraphEdge, + GraphNode, + GraphSchema, + PackContext, + PackMemberSummary, +} from "../src/index.js"; +import { deriveFixtureGraph } from "./helpers/fixture-graph.js"; +import { + CARRIER, + PACK_ID, + packDesignProbeGraph as probeGraph, +} from "./helpers/pack-design-probe.js"; + +/** + * The Pack design on the Pack context, read over a hand-built probe graph: the stated next rung + * and its unmet clauses, the design columns, the bindings with their components, the verifier + * counts, the boundary both ways, and the unresolved member. + * + * @sdpAnchor test:protocol.pack-design + * @sdpLabel the Pack context carries each member's design and the Pack's boundary + * @sdpVerifies spec:consumers.pack-design + */ + +const TYPED_TARGETS = "typed-dependency-targets-are-defined"; + +function packDesign(): PackContext { + const context = createReader(probeGraph()).packContext(PACK_ID); + + if (context === undefined) { + throw new Error(`The probe graph lost "${PACK_ID}".`); + } + + return context; +} + +function member(id: string): PackMemberSummary { + const found = packDesign().members.find((entry) => entry.id === id); + + if (found === undefined) { + throw new Error(`The Pack context lists no member "${id}".`); + } + + return found; +} + +describe("the Pack design on the Pack context", () => { + it("keeps the manifest's order and every existing field", () => { + const context = packDesign(); + + expect(context.members.map((entry) => entry.id)).toEqual([ + "spec:probe.held", + "spec:probe.waiting", + "spec:probe.blocked", + "spec:probe.missing", + "spec:probe.modest", + "spec:probe.top", + "spec:probe.check", + ]); + expect(member("spec:probe.held")).toMatchObject({ + resolved: true, + title: "Probe held", + specKind: "rule", + altitude: "story", + statedReadiness: "defined", + derivedReadiness: "defined", + deliveryFacts: [], + file: CARRIER, + }); + expect(context.verifierGaps.map((gap) => gap.id)).toEqual([ + "spec:probe.held", + "spec:probe.blocked", + "spec:probe.modest", + "spec:probe.top", + "spec:probe.check", + ]); + }); + + it("names the stated next rung's unmet clauses, with the targets a dependency clause names", () => { + const held = member("spec:probe.held"); + + expect(held.statedNextRung).toBe("ready"); + expect(held.statedNextRungFailures?.map((failure) => failure.clauseId)).toEqual([ + TYPED_TARGETS, + ]); + expect(held.statedNextRungFailures?.[0]?.targets).toEqual([ + { type: "dependsOn", id: "spec:probe.outside-basis", statedReadiness: "scoped" }, + ]); + + const blocked = member("spec:probe.blocked"); + + expect(blocked.statedNextRung).toBe("defined"); + expect(blocked.statedNextRungFailures?.map((failure) => failure.clauseId)).toEqual([ + "no-blocking-open-questions", + ]); + expect(blocked.statedNextRungFailures?.[0]?.targets).toBeUndefined(); + }); + + it("reads an empty list where the floor already holds the stated next rung", () => { + expect(member("spec:probe.waiting")).toMatchObject({ + statedReadiness: "defined", + derivedReadiness: "ready", + statedNextRung: "ready", + statedNextRungFailures: [], + }); + }); + + it("keeps the stated reading apart from the floor-reached reading", () => { + const reader = createReader(probeGraph()); + const modest = packDesign().members.find((entry) => entry.id === "spec:probe.modest"); + + // Stated `idea`, floor reached `defined`: the rung above the stated one holds, while the + // rung above the floor reached fails on the dependency still stated `scoped`. + expect(modest).toMatchObject({ + statedReadiness: "idea", + derivedReadiness: "defined", + statedNextRung: "scoped", + statedNextRungFailures: [], + }); + expect( + reader.specContext("spec:probe.modest")?.nextRungFailures.map((failure) => failure.clauseId), + ).toEqual([TYPED_TARGETS]); + }); + + it("names no next rung above ready", () => { + const top = member("spec:probe.top"); + + expect(top.statedReadiness).toBe("ready"); + expect(top).not.toHaveProperty("statedNextRung"); + expect(top.statedNextRungFailures).toEqual([]); + }); + + it("counts the design columns: keyed entries, pinned declarations and questions", () => { + const design = member("spec:probe.held").design; + + // Four Design keys and two UI keys, `description` excluded from both; two Design values open + // with a code span, and a UI value never counts as a declaration. + expect(design?.entries).toBe(6); + expect(design?.declarations).toBe(2); + expect(design?.blockingQuestions).toBe(0); + expect(design?.openQuestions).toEqual([ + { + index: 0, + question: "Who owns the basis?", + blocking: false, + key: "basisOwner", + line: 21, + }, + { index: 1, question: "Is the <name> | # final?", blocking: false }, + ]); + expect(member("spec:probe.blocked").design).toMatchObject({ + entries: 0, + declarations: 0, + blockingQuestions: 1, + openQuestions: [ + { index: 0, question: "Where is the cut?", blocking: true, key: "scopeCut", line: 14 }, + ], + }); + }); + + it("exposes the same design columns on Spec context and Pack members", () => { + const reader = createReader(probeGraph()); + for (const member of reader.packContext(PACK_ID)?.members ?? []) { + if (!member.resolved) continue; + expect(reader.specContext(member.id)?.designColumns).toEqual(member.design); + } + expect(reader.specContext("spec:probe.held")?.designColumns).toMatchObject({ + entries: 6, + declarations: 2, + blockingQuestions: 0, + openQuestions: [{ key: "basisOwner", line: 21 }, { question: "Is the <name> | # final?" }], + decisions: [{ id: "spec:probe.choice", statedReadiness: "defined" }], + }); + expect(reader.specContext("spec:probe.top")?.designColumns).toEqual({ + entries: 0, + declarations: 0, + blockingQuestions: 0, + openQuestions: [], + decisions: [], + }); + }); + + it("lists the decisions a member names, unresolved when the target is no Spec", () => { + expect(member("spec:probe.held").design?.decisions).toEqual([ + { + id: "spec:probe.choice", + title: "Probe choice", + statedReadiness: "defined", + resolved: true, + }, + ]); + expect(member("spec:probe.blocked").design?.decisions).toEqual([ + { id: "spec:probe.absent", resolved: false }, + ]); + }); + + it("carries each realizing and referencing unit with its component on the member and on the Spec context", () => { + const realizing = { + codeId: "impl:probe.waiting", + claim: "anchored", + label: "realizes the waiting rule", + file: "src/fixture.ts", + line: 2, + role: "service", + component: { id: "component:probe.core", layer: "domain", context: "probe" }, + }; + + expect(member("spec:probe.waiting").implementations).toEqual([realizing]); + expect(member("spec:probe.waiting").references).toEqual([]); + expect(createReader(probeGraph()).specContext("spec:probe.waiting")?.implementations).toEqual([ + realizing, + ]); + expect(member("spec:probe.blocked").references).toEqual([ + { codeId: "impl:probe.helper", claim: "anchored", file: "src/fixture.ts", line: 3 }, + ]); + }); + + it("counts verifiers and verifying examples with how many are enabled", () => { + expect(member("spec:probe.waiting")).toMatchObject({ + verifiers: { total: 3, enabled: 2 }, + examples: { total: 2, enabled: 1 }, + }); + expect(member("spec:probe.held")).toMatchObject({ + verifiers: { total: 0, enabled: 0 }, + examples: { total: 0, enabled: 0 }, + }); + }); + + // A declared `verifies` decodes as `via: "example"` whatever its source's kind; the count + // reads the kind, so a behavior Spec that verifies a member is a verifier and no example. + it("counts a verifying Spec of another kind as a verifier, never as an example", () => { + const behavior = (id: string, extra: Partial<Parameters<typeof spec>[0]> = {}) => + spec({ + id: specId(id), + title: `Probe ${id}`, + kind: "behavior", + altitude: "story", + readiness: "idea", + intent: { outcome: `Probe ${id}.` }, + behavior: { rules: ["The probe states one rule."] }, + ...extra, + }); + const graph = deriveFixtureGraph({ + specs: [ + behavior("spec:probe.target"), + behavior("spec:probe.checker", { relations: [verifies(specId("spec:probe.target"))] }), + ], + packs: [ + pack({ + id: packId("pack:probe.kinds"), + title: "Probe kinds", + specs: [specId("spec:probe.target")], + }), + ], + }); + const target = createReader(graph) + .packContext("pack:probe.kinds") + ?.members.find((entry) => entry.id === "spec:probe.target"); + + expect(target).toMatchObject({ + verifiers: { total: 1, enabled: 0 }, + examples: { total: 0, enabled: 0 }, + }); + }); + + // `spec:probe.check` verifies the outside basis: a relation that rests on nothing, so the + // basis row lists only the members that depend on it. + it("lists the boundary both ways, rows by id and members in authored order", () => { + expect(packDesign().boundary).toEqual({ + restsOn: [ + { + id: "spec:probe.choice", + title: "Probe choice", + statedReadiness: "defined", + implemented: false, + via: [{ type: "decidedBy", members: ["spec:probe.held"] }], + }, + { + id: "spec:probe.outside-basis", + title: "Probe outside-basis", + statedReadiness: "scoped", + implemented: false, + via: [ + { + type: "dependsOn", + members: ["spec:probe.held", "spec:probe.blocked", "spec:probe.modest"], + }, + ], + }, + ], + restedOnBy: [ + { + id: "spec:probe.outside-basis", + title: "Probe outside-basis", + statedReadiness: "scoped", + implemented: false, + via: [{ type: "refines", members: ["spec:probe.held"] }], + }, + { + id: "spec:probe.waiting.case", + title: "Probe waiting.case", + statedReadiness: "idea", + implemented: false, + via: [{ type: "refines", members: ["spec:probe.waiting"] }], + }, + { + id: "spec:probe.waiting.unbound", + title: "Probe waiting.unbound", + statedReadiness: "idea", + implemented: false, + via: [{ type: "refines", members: ["spec:probe.waiting"] }], + }, + ], + verifiedBy: ["spec:probe.waiting.case", "spec:probe.waiting.unbound"].map((id) => ({ + id, + title: `Probe ${id.slice("spec:probe.".length)}`, + statedReadiness: "idea", + implemented: false, + via: [{ type: "verifies", members: ["spec:probe.waiting"] }], + })), + supersededBy: [], + }); + }); + + it("keeps verification and supersession outside restedOnBy, ignoring anchored verifiers", () => { + const graph = probeGraph(); + const source = graph.nodes.find((node) => node.id === "spec:probe.choice"); + if (source === undefined) throw new Error("Missing probe decision."); + const reader = createReader({ + ...graph, + nodes: [...graph.nodes, { ...source, id: "spec:probe.replacement" }], + edges: [ + ...graph.edges, + { + from: "spec:probe.replacement", + to: "spec:probe.top", + type: "supersedes", + claim: "declared", + }, + { + from: "spec:probe.replacement", + to: "spec:probe.top", + type: "verifies", + claim: "declared", + }, + ], + }); + const boundary = reader.packContext(PACK_ID)?.boundary; + expect(boundary?.restedOnBy.map((row) => row.id)).not.toContain("spec:probe.replacement"); + expect(boundary?.verifiedBy.map((row) => row.id)).toEqual([ + "spec:probe.replacement", + "spec:probe.waiting.case", + "spec:probe.waiting.unbound", + ]); + expect(boundary?.supersededBy).toEqual([ + { + id: "spec:probe.replacement", + title: "Probe choice", + statedReadiness: "defined", + implemented: false, + via: [{ type: "supersedes", members: ["spec:probe.top"] }], + }, + ]); + }); + + it("marks an outside Spec that carries the implementation fact", () => { + const graph = probeGraph(); + const bound: GraphSchema = { + ...graph, + nodes: [ + ...graph.nodes, + { + id: "impl:probe.basis", + nodeType: "CodeNode", + claim: "anchored", + file: "src/basis.ts", + line: 1, + }, + ], + edges: [ + ...graph.edges, + { + from: "impl:probe.basis", + type: "satisfies", + to: "spec:probe.outside-basis", + claim: "anchored", + }, + ], + }; + const restsOn = createReader(bound).packContext(PACK_ID)?.boundary.restsOn; + + expect(restsOn?.find((row) => row.id === "spec:probe.outside-basis")?.implemented).toBe(true); + expect(restsOn?.find((row) => row.id === "spec:probe.choice")?.implemented).toBe(false); + }); + + it("keeps an unresolved member's existing row and nothing more", () => { + expect(member("spec:probe.missing")).toEqual({ + id: "spec:probe.missing", + resolved: false, + deliveryFacts: [], + }); + }); + + it("carries each Spec's location rows on its Spec context, sorted by entry", () => { + const reader = createReader(probeGraph()); + + expect(reader.specContext("spec:probe.held")?.entryLocations).toEqual([ + { spec: "spec:probe.held", file: CARRIER, entry: "design.fnCreate", line: 30 }, + { spec: "spec:probe.held", file: CARRIER, entry: "design.tableOrders", line: 31 }, + { spec: "spec:probe.held", file: CARRIER, entry: "question[0]", key: "basisOwner", line: 21 }, + ]); + expect(reader.specContext("spec:probe.top")?.entryLocations).toEqual([]); + + const { locations: _dropped, ...withoutTable } = probeGraph(); + void _dropped; + const unlocated = createReader(withoutTable); + + expect(unlocated.specContext("spec:probe.held")?.entryLocations).toEqual([]); + expect( + unlocated.packContext(PACK_ID)?.members[0]?.design?.openQuestions.map((entry) => entry.line), + ).toEqual([undefined, undefined]); + }); + + it("is a pure function of the graph: two reads of one graph are equal", () => { + expect(JSON.stringify(packDesign())).toBe(JSON.stringify(packDesign())); + }); + + it("joins many members to one outside Spec in one step each, members in authored order", () => { + // Every member rests on one outside Spec, which rests on every member in turn. A join that + // scans the members already joined costs the square of the count: seconds here, where one + // step per member costs a fraction of one. + const count = 40_000; + const primitive = (id: string): GraphNode => ({ + id, + nodeType: "Primitive", + claim: "declared", + specKind: "rule", + altitude: "story", + readiness: "idea", + title: id, + file: "specs/wide.sdp.md", + sections: { intent: { outcome: "Probe." } }, + }); + const outside = "spec:wide.outside"; + const members = Array.from( + { length: count }, + (_, position) => `spec:wide.m${String(position)}`, + ); + const graph: GraphSchema = { + schemaVersion, + nodes: [ + primitive(outside), + ...members.map(primitive), + { + id: "pack:wide", + nodeType: "Pack", + claim: "declared", + title: "Wide", + members, + file: "specs/wide.pack.sdp.md", + modelRefs: [], + }, + ], + edges: members.flatMap((id): GraphEdge[] => [ + { from: id, type: "dependsOn", to: outside, claim: "declared" }, + { from: outside, type: "dependsOn", to: id, claim: "declared" }, + ]), + }; + const reader = createReader(graph); + + const started = performance.now(); + const boundary = reader.packContext("pack:wide")?.boundary; + const elapsed = performance.now() - started; + + expect(boundary?.restsOn.map((row) => row.via)).toEqual([[{ type: "dependsOn", members }]]); + expect(boundary?.restedOnBy.map((row) => row.via)).toEqual([[{ type: "dependsOn", members }]]); + expect(elapsed).toBeLessThan(2_000); + }, 30_000); +}); diff --git a/test/reader.test.ts b/test/reader.test.ts index 7fb35748..5420aa2f 100644 --- a/test/reader.test.ts +++ b/test/reader.test.ts @@ -26,6 +26,19 @@ import type { GraphSchema, Reader } from "../src/index.js"; import { materializeExtractCorpus, removeMaterializedCorpus } from "./helpers/extract-corpus.js"; import { deriveFixtureGraph } from "./helpers/fixture-graph.js"; +import { + ref as bindingRef, + specTest as bindingTest, + testAnchorId as bindingTestId, +} from "@libar-dev/software-delivery-protocol"; + +const projectionReaderTestAnchor = bindingTest({ + id: bindingTestId("test:protocol.projections-model.reader"), + label: "reader tests verify composable graph joins without persistence", + verifies: bindingRef("spec:consumers.projections-model"), +}); +void projectionReaderTestAnchor; + const exampleRoot = join(fileURLToPath(new URL("..", import.meta.url)), "examples", "checkout-v1"); /** The example graph, extracted once — the canonical consumer input (one graph, many readers). */ @@ -1176,6 +1189,11 @@ describe("the reader — the thin typed loader behind the agent surface", () => file: "src/comment-form.ts", line: 13, role: "service", + component: { + id: "component:platform.read-model", + layer: "application", + context: "platform", + }, }, ]); expect(context?.references).toEqual([ @@ -1186,6 +1204,7 @@ describe("the reader — the thin typed loader behind the agent surface", () => file: "src/constant-form.ts", line: 17, role: "decider", + component: { id: "component:platform.gateway", layer: "edge", context: "platform" }, }, ]); }); diff --git a/test/recipes.test.ts b/test/recipes.test.ts index c0a19ed1..af8bcc9c 100644 --- a/test/recipes.test.ts +++ b/test/recipes.test.ts @@ -1,10 +1,19 @@ -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; +import { recipeFiles, writeBuildArtifacts, writeRecipeFiles } from "../src/cli/build-artifacts.js"; import { compileBody } from "../src/cli/q-command.js"; import { runSdpCli } from "../src/cli/sdp.js"; import { @@ -35,10 +44,10 @@ const repoRoot = fileURLToPath(new URL("..", import.meta.url)); const recipesPath = "docs/agent-surface/recipes.md"; // The seam is the production one: `runSdpCli` parses the same argv, compiles the same body, injects -// the same `g` / `graph` / `report` bindings, and shapes the same output. Only the extraction is -// memoized — derived once from the repository root with the standard exclude list (the corpus-oracle -// derivation pattern), because deriving the whole corpus once per recipe buys nothing the single -// derivation does not already prove. Nothing here names a write path, so the suite stays pooled. +// the same `g` / `graph` / `report` / `params` bindings, and shapes the same output. Only the +// extraction is memoized — derived once from the repository root with the standard exclude list (the +// corpus-oracle derivation pattern), because deriving the whole corpus once per recipe buys nothing +// the single derivation does not already prove. Nothing here names a write path, so the suite stays pooled. // The project's own exclusions, stated once. Every documented `sdp q` invocation that names any // exclusion at this root must name all of them: without the full set the corpus reports extraction // errors and the sink refuses the body outright, so a shorter list in a doc is an invocation that @@ -110,7 +119,8 @@ function parseRecipes(markdown: string): readonly Recipe[] { return recipes; } -const source = readFileSync(join(repoRoot, recipesPath), "utf8"); +// LF-normalized, as the build step reads the catalog, so a CRLF checkout parses the same bodies. +const source = readFileSync(join(repoRoot, recipesPath), "utf8").replaceAll("\r\n", "\n"); const recipes = parseRecipes(source); const documentedHeadingCount = source .split("\n") @@ -128,47 +138,44 @@ const queryHooks = { }, }; +/** + * Runs one body through the real argv seam. A recipe parameter travels the way a caller passes + * it, as JSON data on `--params`, never spliced into the body; without one the body runs exactly + * as the catalog prints it and falls back to its sample. + */ async function runRecipe( recipe: Recipe, - changedFiles?: readonly string[], + params?: Readonly<Record<string, unknown>>, extraction: ExtractionResult = derived, ): Promise<unknown> { - const previousChangedFiles = process.env.SDP_CHANGED_FILES_JSON; - if (changedFiles === undefined) { - delete process.env.SDP_CHANGED_FILES_JSON; - } else { - process.env.SDP_CHANGED_FILES_JSON = JSON.stringify(changedFiles); - } - - try { - const capture = createCaptureOutput(); - const exitCode = await runSdpCli( - ["q", recipe.body, "--root", repoRoot, "--json"], - capture.output, - { - query: { - ...queryHooks.query, - extract: () => extraction, - }, + const capture = createCaptureOutput(); + const exitCode = await runSdpCli( + [ + "q", + recipe.body, + "--root", + repoRoot, + "--json", + ...(params === undefined ? [] : ["--params", JSON.stringify(params)]), + ], + capture.output, + { + query: { + ...queryHooks.query, + extract: () => extraction, }, - ); + }, + ); - // The expected stderr is the empty string, not a self-comparison: a recipe run over the green - // corpus has nothing to say on stderr, and the object shape keeps the actual output in the - // failure diff when it does. - expect( - { recipe: recipe.title, exitCode, stderr: capture.readStderr() }, - `recipe ${String(recipe.ordinal)} must run as written`, - ).toEqual({ recipe: recipe.title, exitCode: 0, stderr: "" }); + // The expected stderr is the empty string, not a self-comparison: a recipe run over the green + // corpus has nothing to say on stderr, and the object shape keeps the actual output in the + // failure diff when it does. + expect( + { recipe: recipe.title, exitCode, stderr: capture.readStderr() }, + `recipe ${String(recipe.ordinal)} must run as written`, + ).toEqual({ recipe: recipe.title, exitCode: 0, stderr: "" }); - return JSON.parse(capture.readStdout()) as unknown; - } finally { - if (previousChangedFiles === undefined) { - delete process.env.SDP_CHANGED_FILES_JSON; - } else { - process.env.SDP_CHANGED_FILES_JSON = previousChangedFiles; - } - } + return JSON.parse(capture.readStdout()) as unknown; } function asRecord(value: unknown): Record<string, unknown> { @@ -330,6 +337,41 @@ const registerRecipesTestAnchor = specTest({ }); void registerRecipesTestAnchor; describe("the agent-surface recipe corpus", () => { + // A Spec that cites a recipe by number and heading, as "Recipe 32, architecture crossings," or + // "architecture crossings (recipe 32)", names the heading the catalog gives that number. + it("cites each recipe that a Spec names by number and heading under the catalog's number", () => { + const headings = recipes.map((recipe) => ({ + ordinal: recipe.ordinal, + heading: recipe.title.toLowerCase(), + })); + const citations: { spec: string; cited: number; named: number }[] = []; + + for (const node of derived.graph.nodes) { + if (node.nodeType !== "Primitive") continue; + + const text = `${node.narrative ?? ""}\n${JSON.stringify(node.sections ?? {})}`; + for (const match of text.matchAll(/\brecipe (\d+), ([^,.;:()"\n]+)/giu)) { + const named = headings.find((entry) => + (match[2] ?? "").toLowerCase().startsWith(entry.heading), + ); + if (named !== undefined) { + citations.push({ spec: node.id, cited: Number(match[1]), named: named.ordinal }); + } + } + for (const match of text.matchAll(/([\p{L}\p{N} -]+) \(recipe (\d+)\)/giu)) { + const named = headings.find((entry) => + (match[1] ?? "").toLowerCase().endsWith(entry.heading), + ); + if (named !== undefined) { + citations.push({ spec: node.id, cited: Number(match[2]), named: named.ordinal }); + } + } + } + + expect(citations.length).toBeGreaterThan(0); + expect(citations.filter((citation) => citation.cited !== citation.named)).toEqual([]); + }); + // Given: the catalog as authored. When: its structure is read. Then: every documented recipe // carries exactly one runnable body, so a new recipe cannot dodge the check by omitting one. it("pairs every documented recipe with exactly one fenced body", () => { @@ -368,9 +410,13 @@ describe("the agent-surface recipe corpus", () => { source, selfHosting: true, }); + // A body in double quotes is shell-expanded before the sink sees it. The one lawful + // double-quoted body is a shipped recipe file read whole by command substitution. expect({ source, - otherQuoting: lines.filter((line) => line.includes(' q "') && line.includes("sdp")), + otherQuoting: lines.filter( + (line) => line.includes(' q "') && line.includes("sdp") && !line.includes(' q "$(cat '), + ), }).toEqual({ source, otherQuoting: [] }); expect({ source, adopterForms: adopterLines.length }).toEqual({ @@ -415,6 +461,10 @@ describe("the agent-surface recipe corpus", () => { "twenty-six", "twenty-seven", "twenty-eight", + "twenty-nine", + "thirty", + "thirty-one", + "thirty-two", ] as const; const countWord = countWords[recipes.length]; const lastOrdinal = recipes[recipes.length - 1]?.ordinal; @@ -433,23 +483,18 @@ describe("the agent-surface recipe corpus", () => { authoring: readFileSync(join(repoRoot, ".agents/skills/sdp-authoring/SKILL.md"), "utf8"), sessions: readFileSync(join(repoRoot, ".agents/skills/sdp-sessions/SKILL.md"), "utf8"), }; - const parameterizedRecipes = recipes.filter( - (recipe) => - recipe.ordinal !== 4 && - /^(?:const (?:id|term|subject|scope|addresses) = )/u.test(recipe.body), - ); + const parameterizedRecipes = recipes.filter((recipe) => /\bparams\./u.test(recipe.body)); - // Recipe 4 receives filenames as data. Intro guidance must never teach callers to construct - // executable query source from repository-controlled paths. - const parameterGuidance = - /\*\*Some recipes open with a parameter\.[\s\S]*?(?=\n\n\*\*Recipe 4 is different)/u.exec( - intro, - )?.[0] ?? ""; - expect(parameterGuidance).not.toMatch( - /\b4\b[\s\S]*?(?:changed-file list|substitut(?:e|ion))/iu, - ); - expect(introProse).toContain("Recipe 4 filenames travel via `SDP_CHANGED_FILES_JSON`"); - expect(introProse).toContain("callers never substitute filenames into the JavaScript fence"); + // A parameter is data on `--params`, never source spliced into a body, and no recipe reads + // the environment. The intro teaches exactly that and names no retired channel. + expect(introProse).toContain("Pass it as JSON data with `--params`, never by editing the body"); + expect(source).not.toContain("SDP_CHANGED_FILES_JSON"); + for (const recipe of recipes) { + expect({ recipe: recipe.ordinal, env: recipe.body.includes("process.env") }).toEqual({ + recipe: recipe.ordinal, + env: false, + }); + } expect(countWord).toBeDefined(); if (countWord === undefined) { @@ -487,11 +532,15 @@ describe("the agent-surface recipe corpus", () => { "dependency cycles", "references into a design", "roles, layers and contexts", + "pack design", + "design-change impact", + "decision register", + "architecture crossings", ]) { expect(agentSurfaceProse).toContain(phrase); } - for (const ordinal of [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]) { + for (let ordinal = 12; ordinal <= recipes.length; ordinal += 1) { expect(onRamps.sessions).toContain(`recipe ${String(ordinal)}`); } }); @@ -642,13 +691,14 @@ describe("the agent-surface recipe corpus", () => { const recipe = recipeByOrdinal(4); rmSync(sentinel, { force: true }); - expect(recipe.body).toContain("process.env.SDP_CHANGED_FILES_JSON"); + expect(recipe.body).toContain("params.files"); + expect(recipe.body).not.toContain("process.env"); for (const filename of hostileChangedFiles) { expect(recipe.body).not.toContain(filename); } try { - const result = asRecord(await runRecipe(recipe, hostileChangedFiles)); + const result = asRecord(await runRecipe(recipe, { files: hostileChangedFiles })); expect([...asArray(result.changedFiles)].sort()).toEqual([...hostileChangedFiles].sort()); expect([...asArray(result.coverageUnknownFiles)].sort()).toEqual( @@ -662,7 +712,7 @@ describe("the agent-surface recipe corpus", () => { it("returns the complete diff-to-at-risk bridge", async () => { const normalChangedFiles = ["src/reader/reader.ts", "docs/agent-surface/recipes.md"]; - const result = asRecord(await runRecipe(recipeByOrdinal(4), normalChangedFiles)); + const result = asRecord(await runRecipe(recipeByOrdinal(4), { files: normalChangedFiles })); const changedFiles = asArray(result.changedFiles).map((file) => stringAt({ file }, "file")); const radius = reader.blastRadius(changedFiles); @@ -777,7 +827,7 @@ describe("the agent-surface recipe corpus", () => { edges: derived.graph.edges, }, }; - const result = asRecord(await runRecipe(recipeByOrdinal(4), [file], extraction)); + const result = asRecord(await runRecipe(recipeByOrdinal(4), { files: [file] }, extraction)); expect(result.unlinkedUnits).toEqual([{ id: unit.id, file }]); expect(result.coverageUnknownFiles).toEqual([]); @@ -889,12 +939,6 @@ describe("the agent-surface recipe corpus", () => { it("names the next rung's unmet clause and the dependency that breaks it on a probe", async () => { const recipe = recipeByOrdinal(9); - const catalogLine = 'const id = "spec:model.enrichment-lifecycle";'; - expect(recipe.body).toContain(catalogLine); - const retarget = (id: string) => ({ - ...recipe, - body: recipe.body.replace(catalogLine, `const id = "${id}";`), - }); const ruleNode = ( id: string, readiness: "idea" | "scoped" | "defined" | "ready", @@ -947,7 +991,7 @@ describe("the agent-surface recipe corpus", () => { }; const targetClause = "typed-dependency-targets-are-defined"; - expect(await runRecipe(retarget("spec:probe.subject"), undefined, probe)).toEqual({ + expect(await runRecipe(recipe, { spec: "spec:probe.subject" }, probe)).toEqual({ id: "spec:probe.subject", found: true, statedReadiness: "defined", @@ -971,7 +1015,7 @@ describe("the agent-surface recipe corpus", () => { }); // A failure of any other clause carries no targets field. - const held = asRecord(await runRecipe(retarget("spec:probe.held"), undefined, probe)); + const held = asRecord(await runRecipe(recipe, { spec: "spec:probe.held" }, probe)); expect(held.nextRung).toBe("defined"); expect(held.nextRungFailures).toEqual([ { @@ -1121,10 +1165,7 @@ describe("the agent-surface recipe corpus", () => { satisfiedSpecs, }); - const absent = await runRecipe({ - ...recipe, - body: recipe.body.replace("component:protocol.reader", "component:protocol.nonexistent"), - }); + const absent = await runRecipe(recipe, { component: "component:protocol.nonexistent" }); expect(absent).toEqual({ found: false }); }); @@ -1582,31 +1623,21 @@ describe("the agent-surface recipe corpus", () => { expect(Object.keys(result)).not.toContain("blastRadiusEntryPoints"); const unknownId = "spec:consumers.nonexistent"; - const absent = await runRecipe({ - ...recipe, - body: recipe.body.replace(id, unknownId), - }); + const absent = await runRecipe(recipe, { spec: unknownId }); expect(absent).toEqual({ id: unknownId, found: false }); }); - // Given: catalog recipe 19 with only the opening id retargeted to the structural-anchor - // decision (live graph: one outgoing dependsOn, two inbound dependsOn from MD-34/MD-35). - // When: the otherwise unchanged body runs through real runSdpCli. - // Then: dependencies.dependsOn and dependencies.dependedOnBy are both non-empty and name the - // exact ready decision neighbors — so dropping, reversing, renaming, or readiness-skewing either - // direction reddens this characterization. - it("returns non-empty bidirectional dependencies for the structural-anchor planning slice", async () => { + // Given: catalog recipe 19 with its parameter naming the binding-grain decision. + // Its carrier depends on binding-not-liveness; architectural-annotation depends on it. + // When: the unchanged body runs through the real CLI runner. + // Then: both dependency directions retain their ready decision neighbors. + it("returns non-empty bidirectional dependencies for the binding-grain planning slice", async () => { const recipe = recipeByOrdinal(19); const catalogId = "spec:consumers.agent-surface"; - const id = "spec:decisions.structural-anchor-semantics"; - expect(recipe.body).toContain(`const id = "${catalogId}";`); + const id = "spec:decisions.anchor-binding-grain"; + expect(recipe.body).toContain(`const id = params.spec ?? "${catalogId}";`); - const result = asRecord( - await runRecipe({ - ...recipe, - body: recipe.body.replace(catalogId, id), - }), - ); + const result = asRecord(await runRecipe(recipe, { spec: id })); const dependencies = asRecord(result.dependencies); const dependsOn = asArray(dependencies.dependsOn).map(asRecord); const dependedOnBy = asArray(dependencies.dependedOnBy).map(asRecord); @@ -1617,14 +1648,7 @@ describe("the agent-surface recipe corpus", () => { { id: "spec:decisions.binding-not-liveness", statedReadiness: "ready" }, ]); expect(dependedOnBy).toEqual([ - { - id: "spec:decisions.architectural-significance-rides-primitives", - statedReadiness: "ready", - }, - { - id: "spec:decisions.jsdoc-graph-extraction-refused", - statedReadiness: "ready", - }, + { id: "spec:decisions.architectural-annotation", statedReadiness: "ready" }, ]); }); @@ -1753,6 +1777,43 @@ describe("the agent-surface recipe corpus", () => { // are Object.prototype keys, with no resolving implementation. // When: catalog recipe 1 runs through real runSdpCli on that override. // Then: each hostile family is an own byFamily key carrying the exact synthetic Spec id. + it("reports derived design columns beside the existing backlog fields", async () => { + const extraction = syntheticExtraction( + [ + probeSpec("spec:probe.backlog", "rule", "ready", { + intent: { + outcome: "Build the design.", + openQuestions: [{ question: "Who follows up?", blocking: false, key: "followUp" }], + }, + design: { + fnBuild: "`function build(): void` declaration", + prose: "An approach.", + description: "Design prose.", + }, + ui: { layout: "One page." }, + }), + probeSpec("spec:probe.choice", "decision", "defined"), + ], + [probeEdge("spec:probe.backlog", "decidedBy", "spec:probe.choice")], + ); + const result = asRecord(await runRecipe(recipeByOrdinal(1), undefined, extraction)); + expect(asRecord(result.byFamily).probe).toEqual([ + { + id: "spec:probe.backlog", + kind: "rule", + altitude: "story", + hasVerifier: false, + designColumns: { + entries: 3, + declarations: 1, + openQuestions: 1, + blockingQuestions: 0, + decisions: 1, + }, + }, + ]); + }); + it("groups backlog rows under lawful Object.prototype path segments", async () => { const leaf = "hostile-backlog"; const extraction = extractionWithHostilePrimitives({ leaf, readiness: "ready" }); @@ -1944,7 +2005,7 @@ export const probe = spec({ } describe("register recipe semantics", () => { - it.each([20, 21, 22, 23, 24, 25, 26, 27, 28])( + it.each([20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32])( "runs recipe %s as written in default output", async (ordinal) => { const capture = createCaptureOutput(); @@ -1975,8 +2036,7 @@ describe("register recipe semantics", () => { }, }; const recipe = recipeByOrdinal(23); - const body = recipe.body.replace('const term = "suffix";', 'const term = "retry";'); - const result = asRecord(await runRecipe({ ...recipe, body }, undefined, probe)); + const result = asRecord(await runRecipe(recipe, { term: "retry" }, probe)); expect( asArray(result.matches) .map(asRecord) @@ -1984,9 +2044,11 @@ describe("register recipe semantics", () => { ).toEqual([text, JSON.stringify({ detail: text })]); const capture = createCaptureOutput(); expect( - await runSdpCli(["q", body, "--root", repoRoot], capture.output, { - query: { ...queryHooks.query, extract: () => probe }, - }), + await runSdpCli( + ["q", recipe.body, "--root", repoRoot, "--params", JSON.stringify({ term: "retry" })], + capture.output, + { query: { ...queryHooks.query, extract: () => probe } }, + ), ).toBe(0); expect(capture.readStderr()).toBe(""); expect(capture.readStdout()).not.toContain(text); @@ -2014,27 +2076,26 @@ describe("register recipe semantics", () => { it("searches coined keys of every value shape through a TypeScript carrier", async () => { const recipe = recipeByOrdinal(23); - const result = asRecord( - await runRecipe( - { ...recipe, body: recipe.body.replace('const term = "suffix";', 'const term = "retry";') }, - undefined, - carrierRegisterProbe(), - ), - ); - const expected = [ - ["design", "retryLimit", "3"], - ["design", "retryEnabled", "true"], - ["design", "retryPolicy", '{"mode":"fixed"}'], - ["design", "retryWorkers", '["alpha","beta"]'], - ["design", "retryEmptyList", "[]"], - ["design", "retryEmptyObject", "{}"], - ["design", "retryLong", JSON.stringify({ detail: "x".repeat(300) })], - ["ui", "retryVisible", "false"], - ].map(([section, entry, text]) => ({ + const result = asRecord(await runRecipe(recipe, { term: "retry" }, carrierRegisterProbe())); + // The line is the one the TypeScript carrier records for the entry's property; a nested path + // is no entry of the location table, so it has none. + const expected = ( + [ + ["design", "retryLimit", "3", 8], + ["design", "retryEnabled", "true", 8], + ["design", "retryPolicy", '{"mode":"fixed"}', 8], + ["design", "retryWorkers", '["alpha","beta"]', 9], + ["design", "retryEmptyList", "[]", 9], + ["design", "retryEmptyObject", "{}", 9], + ["design", "retryLong", JSON.stringify({ detail: "x".repeat(300) }), 10], + ["ui", "retryVisible", "false", 12], + ] as const + ).map(([section, entry, text, line]) => ({ id: "spec:probe.carrier", section, entry, - address: `spec:probe.carrier#${section ?? ""}.${entry ?? ""}`, + address: `spec:probe.carrier#${section}.${entry}`, + line, text, matchedIn: ["key"], })); @@ -2045,6 +2106,7 @@ describe("register recipe semantics", () => { section: "design", entry: "nested.steps[0]", address: null, + line: null, text: "retry later", matchedIn: ["text"], }, @@ -2060,8 +2122,8 @@ describe("register recipe semantics", () => { const recipe = recipeByOrdinal(23); const result = asRecord( await runRecipe( - { ...recipe, body: recipe.body.replace('const term = "suffix";', 'const term = "retry";') }, - undefined, + recipe, + { term: "retry" }, { ...extraction, graph: { @@ -2081,12 +2143,14 @@ describe("register recipe semantics", () => { }, ), ); + // The swapped sections keep the probe's location table, which locates both top-level keys. expect(result.matches).toEqual([ { id: node.id, section: "design", entry: "retryWorkers", address: `${node.id}#design.retryWorkers`, + line: 9, matchedIn: ["key"], text: '["retry later"]', }, @@ -2095,6 +2159,7 @@ describe("register recipe semantics", () => { section: "design", entry: "retryWorkers[0]", address: null, + line: null, matchedIn: ["text"], text: "retry later", }, @@ -2103,6 +2168,7 @@ describe("register recipe semantics", () => { section: "design", entry: "retryPolicy", address: `${node.id}#design.retryPolicy`, + line: 8, matchedIn: ["key"], text: '{"mode":"retry fixed","options":["retry soon"]}', }, @@ -2111,6 +2177,7 @@ describe("register recipe semantics", () => { section: "design", entry: "retryPolicy.mode", address: null, + line: null, matchedIn: ["text"], text: "retry fixed", }, @@ -2119,6 +2186,7 @@ describe("register recipe semantics", () => { section: "design", entry: "retryPolicy.options[0]", address: null, + line: null, matchedIn: ["text"], text: "retry soon", }, @@ -2133,8 +2201,8 @@ describe("register recipe semantics", () => { if (node?.nodeType !== "Primitive") throw new Error("missing carrier probe"); const result = asRecord( await runRecipe( - { ...recipe, body: recipe.body.replace('const term = "suffix";', 'const term = "retry";') }, - undefined, + recipe, + { term: "retry" }, { ...extraction, graph: { @@ -2150,6 +2218,8 @@ describe("register recipe semantics", () => { section: "design", entry: "retryNothing", address: `${node.id}#design.retryNothing`, + // The probe's location table never saw this key: not located, never absent. + line: null, matchedIn: ["key"], text: "null", }, @@ -2157,20 +2227,25 @@ describe("register recipe semantics", () => { expect(result.totals).toEqual({ matches: 1, specs: 1, shown: 1 }); }); + // A source line is a location, not a count, so a number under `line` may sit in a row. Recipes + // 29 and 30 are left out: their rows carry per-member design counts and a distance by law. it("keeps register counts under totals and top-level plural nouns as arrays", async () => { - const check = (value: unknown, insideTotals = false): void => { + const check = (value: unknown, insideTotals = false, key = ""): void => { if (typeof value === "number") { - expect(insideTotals).toBe(true); + expect({ key, insideTotals: insideTotals || key === "line" }).toEqual({ + key, + insideTotals: true, + }); } else if (Array.isArray(value)) { value.forEach((entry) => { - check(entry, insideTotals); + check(entry, insideTotals, key); }); } else if (typeof value === "object" && value !== null) { - for (const [key, entry] of Object.entries(value)) - check(entry, insideTotals || key === "totals"); + for (const [name, entry] of Object.entries(value)) + check(entry, insideTotals || name === "totals", name); } }; - for (const ordinal of [20, 21, 22, 23, 24, 25, 26, 27, 28]) { + for (const ordinal of [20, 21, 22, 23, 24, 25, 26, 27, 28, 31, 32]) { const output = asRecord(await runRecipe(recipeByOrdinal(ordinal))); check(output); for (const key of [ @@ -2219,16 +2294,7 @@ describe("register recipe semantics", () => { }, }; const recipe = recipeByOrdinal(23); - const result = asRecord( - await runRecipe( - { - ...recipe, - body: recipe.body.replace('const term = "suffix";', `const term = "${term}";`), - }, - undefined, - extraction, - ), - ); + const result = asRecord(await runRecipe(recipe, { term }, extraction)); expect(asRecord(result.totals).matches).toBe(matchedIn.length === 0 ? 0 : 1); expect(result.matches).toEqual( matchedIn.length === 0 @@ -2239,6 +2305,8 @@ describe("register recipe semantics", () => { section: "design", entry: key, address: `${node.id}#design.${key}`, + // The in-memory probe carries no location table: not located, never absent. + line: null, matchedIn, text, }, @@ -2371,20 +2439,11 @@ describe("register recipe semantics", () => { expect(asRecord(output.totals).byFloorReached).toEqual(expectedFloor); }; assertReadiness(result, derived); - const retarget = (id: string) => ({ - ...recipe, - body: recipe.body.replace( - 'const id = "spec:extraction.derive-graph";', - `const id = "${id}";`, - ), - }); - expect(await runRecipe(retarget("spec:probe.absent"), undefined, registerProbe())).toEqual({ + expect(await runRecipe(recipe, { spec: "spec:probe.absent" }, registerProbe())).toEqual({ id: "spec:probe.absent", found: false, }); - const synthetic = asRecord( - await runRecipe(retarget("spec:probe.b"), undefined, registerProbe()), - ); + const synthetic = asRecord(await runRecipe(recipe, { spec: "spec:probe.b" }, registerProbe())); assertReadiness(synthetic, registerProbe()); expect(asRecord(synthetic.totals).relations).toBe(6); expect(asRecord(synthetic.totals).byType).toEqual({ @@ -2751,16 +2810,7 @@ export const source = spec({ ), }; const recipe = recipeByOrdinal(23); - const result = asRecord( - await runRecipe( - { - ...recipe, - body: recipe.body.replace('const term = "suffix";', 'const term = "needle";'), - }, - undefined, - { ...probe, graph }, - ), - ); + const result = asRecord(await runRecipe(recipe, { term: "needle" }, { ...probe, graph })); const rows = asArray(result.matches).map(asRecord); for (const section of ["design", "ui"]) expect( @@ -2773,20 +2823,7 @@ export const source = spec({ it("scopes mentions by the mentioning Spec only", async () => { const probe = registerProbe(); const recipe = recipeByOrdinal(22); - const scoped = async (scope: string[]) => - asRecord( - await runRecipe( - { - ...recipe, - body: recipe.body.replace( - "const scope = [];", - `const scope = ${JSON.stringify(scope)};`, - ), - }, - undefined, - probe, - ), - ); + const scoped = async (scope: string[]) => asRecord(await runRecipe(recipe, { scope }, probe)); const all = asRecord(await runRecipe(recipe, undefined, probe)); expect(await scoped(["spec:probe.b"])).toEqual(all); for (const scope of [ @@ -2882,16 +2919,7 @@ export const source = spec({ it("searches whole tokens, coined keys, narrative and fence steps", async () => { const recipe = recipeByOrdinal(23); const search = async (term: string, extraction = registerProbe()) => - asRecord( - await runRecipe( - { - ...recipe, - body: recipe.body.replace('const term = "suffix";', `const term = "${term}";`), - }, - undefined, - extraction, - ), - ); + asRecord(await runRecipe(recipe, { term }, extraction)); const retry = await search("retry"); const rows = asArray(retry.matches).map(asRecord); const entries = rows.map((row) => row.entry); @@ -2903,6 +2931,7 @@ export const source = spec({ section: "design", entry: "retryWorker", address: "spec:probe.search#design.retryWorker", + line: null, matchedIn: ["key", "text"], text: "retry-worker", }); @@ -3179,17 +3208,6 @@ relations: {} }); } -/** Recipe 25 with its opening parameter replaced, as the catalog tells a reader to do. */ -function addressResolution(addresses: readonly unknown[]): Recipe { - const recipe = recipeByOrdinal(25); - const opening = /^const addresses = \[\n[\s\S]*?\n\];\n/u; - expect(recipe.body).toMatch(opening); - return { - ...recipe, - body: recipe.body.replace(opening, `const addresses = ${JSON.stringify(addresses)};\n`), - }; -} - /** * The dependency cycles of a graph by mutual reachability, a second derivation independent of the * recipe's own walk: the sets it must report, each with the length of the shortest closed path @@ -3275,6 +3293,13 @@ function expectCyclesToMatchOracle(result: Record<string, unknown>, extraction: describe("address resolution and dependency cycles", () => { it("resolves the catalog's addresses on this corpus as written", async () => { + // The value and the line are read from the raw graph, the node's section and the location + // table, so an edit to the Spec moves the expectation with it. + const holder = primitivesById.get("spec:consumers.design-review"); + const location = derived.graph.locations?.find( + (row) => row.spec === "spec:consumers.design-review" && row.entry === "ui.packPage", + ); + expect(location).toBeDefined(); expect(await runRecipe(recipeByOrdinal(25))).toEqual({ totals: { addresses: 4, resolved: 1, malformed: 1, spec: 1, entry: 1 }, rows: [ @@ -3284,6 +3309,9 @@ describe("address resolution and dependency cycles", () => { id: "spec:consumers.design-review", section: "ui", key: "packPage", + value: holder?.sections?.ui?.packPage, + file: "specs/consumers/design-review.sdp.md", + line: location?.line, reason: null, }, ...[ @@ -3296,6 +3324,9 @@ describe("address resolution and dependency cycles", () => { id: null, section: null, key: null, + value: null, + file: null, + line: null, reason, })), ], @@ -3412,11 +3443,13 @@ relations: {} "authored.sdp.md": designProbeSpec("authored", "constructor: An authored constructor."), }); const result = await runRecipe( - addressResolution([ - "spec:probe.plain#design.constructor", - "spec:probe.authored#design.constructor", - ]), - undefined, + recipeByOrdinal(25), + { + addresses: [ + "spec:probe.plain#design.constructor", + "spec:probe.authored#design.constructor", + ], + }, extraction, ); expect(result).toEqual({ @@ -3428,6 +3461,9 @@ relations: {} id: null, section: null, key: null, + value: null, + file: null, + line: null, reason: "entry", }, { @@ -3436,6 +3472,9 @@ relations: {} id: "spec:probe.authored", section: "design", key: "constructor", + value: "An authored constructor.", + file: "authored.sdp.md", + line: 17, reason: null, }, ], @@ -3444,13 +3483,15 @@ relations: {} it("resolves the design's probe addresses with one row and one reason each", async () => { const result = await runRecipe( - addressResolution([ - "spec:probe.a#design.shape", - "spec:probe.a#question.shapeOpen", - "spec:probe.z#design.shape", - "spec:probe.a#Design.shape", - ]), - undefined, + recipeByOrdinal(25), + { + addresses: [ + "spec:probe.a#design.shape", + "spec:probe.a#question.shapeOpen", + "spec:probe.z#design.shape", + "spec:probe.a#Design.shape", + ], + }, cycleProbe(), ); expect(result).toEqual({ @@ -3462,6 +3503,9 @@ relations: {} id: "spec:probe.a", section: "design", key: "shape", + value: "A shape.", + file: "a.sdp.md", + line: 19, reason: null, }, ...[ @@ -3474,6 +3518,9 @@ relations: {} id: null, section: null, key: null, + value: null, + file: null, + line: null, reason, })), ], @@ -3485,12 +3532,21 @@ relations: {} const sections = createReader(extraction.graph).specContext("spec:probe.keyed")?.sections; expect(sections?.design).toHaveProperty("description"); expect(sections?.ui).toHaveProperty("description"); - const resolved = (address: string, section: string, key: string) => ({ + const resolved = ( + address: string, + section: string, + key: string, + value: string, + line: number, + ) => ({ address, resolves: true, id: "spec:probe.keyed", section, key, + value, + file: "keyed.sdp.md", + line, reason: null, }); const refused = (address: unknown, reason: string) => ({ @@ -3499,37 +3555,60 @@ relations: {} id: null, section: null, key: null, + value: null, + file: null, + line: null, reason, }); const result = await runRecipe( - addressResolution([ - "spec:probe.keyed#question.shapeOpen", - "spec:probe.keyed#question.description", - "spec:probe.keyed#design.description", - "spec:probe.keyed#ui.description", - "spec:probe.keyed#question.ownerOpen", - "spec:probe.keyed#design.shape", - "spec:probe.keyed", - 42, - "pack:probe.keyed#design.shape", - "spec:probe.keyed#question.shapeOpen", - ]), - undefined, + recipeByOrdinal(25), + { + addresses: [ + "spec:probe.keyed#question.shapeOpen", + "spec:probe.keyed#question.description", + "spec:probe.keyed#design.description", + "spec:probe.keyed#ui.description", + "spec:probe.keyed#question.ownerOpen", + "spec:probe.keyed#design.shape", + "spec:probe.keyed", + 42, + "pack:probe.keyed#design.shape", + "spec:probe.keyed#question.shapeOpen", + ], + }, extraction, ); expect(result).toEqual({ totals: { addresses: 10, resolved: 4, malformed: 3, spec: 0, entry: 3 }, rows: [ - resolved("spec:probe.keyed#question.shapeOpen", "question", "shapeOpen"), - resolved("spec:probe.keyed#question.description", "question", "description"), + resolved( + "spec:probe.keyed#question.shapeOpen", + "question", + "shapeOpen", + "Is the shape final?", + 14, + ), + resolved( + "spec:probe.keyed#question.description", + "question", + "description", + "Is description a key?", + 15, + ), refused("spec:probe.keyed#design.description", "entry"), refused("spec:probe.keyed#ui.description", "entry"), refused("spec:probe.keyed#question.ownerOpen", "entry"), - resolved("spec:probe.keyed#design.shape", "design", "shape"), + resolved("spec:probe.keyed#design.shape", "design", "shape", "A shape.", 24), refused("spec:probe.keyed", "malformed"), refused(42, "malformed"), refused("pack:probe.keyed#design.shape", "malformed"), - resolved("spec:probe.keyed#question.shapeOpen", "question", "shapeOpen"), + resolved( + "spec:probe.keyed#question.shapeOpen", + "question", + "shapeOpen", + "Is the shape final?", + 14, + ), ], }); }); @@ -3560,18 +3639,7 @@ relations: {} it("addresses a keyed question's text and matches its key, never the key as a row", async () => { const recipe = recipeByOrdinal(23); const search = async (term: string) => - asArray( - asRecord( - await runRecipe( - { - ...recipe, - body: recipe.body.replace('const term = "suffix";', `const term = "${term}";`), - }, - undefined, - keyedQuestionProbe(), - ), - ).matches, - ) + asArray(asRecord(await runRecipe(recipe, { term }, keyedQuestionProbe())).matches) .map(asRecord) .filter((row) => row.id === "spec:probe.keyed"); expect(await search("shape open")).toEqual([ @@ -3580,13 +3648,15 @@ relations: {} section: "intent", entry: "openQuestions[0].question", address: "spec:probe.keyed#question.shapeOpen", + line: 14, matchedIn: ["key"], text: "Is the shape final?", }, ]); + // An unkeyed question has no address but still a line: the table locates every question. const owner = await search("owns"); - expect(owner.map((row) => [row.entry, row.address])).toEqual([ - ["openQuestions[2].question", null], + expect(owner.map((row) => [row.entry, row.address, row.line])).toEqual([ + ["openQuestions[2].question", null, 16], ]); const description = await search("description"); expect(description.map((row) => [row.entry, row.address, row.matchedIn])).toEqual([ @@ -3741,10 +3811,7 @@ describe("design references and the architecture taxonomy", () => { ), }); - const absent = await runRecipe({ - ...recipe, - body: recipe.body.replace(studioPackId, "pack:absent-v1"), - }); + const absent = await runRecipe(recipe, { pack: "pack:absent-v1" }); expect(absent).toEqual({ id: "pack:absent-v1", found: false }); }); @@ -3926,7 +3993,7 @@ describe("design references and the architecture taxonomy", () => { expect(asArray(valueRow("layers", "domain")?.units)).toContain("component:probe.studio-seam"); }); - // The sink compiles a body this way and calls it with the same three bindings; calling the + // The sink compiles a body this way and calls it with the same four bindings; calling the // compiled body directly keeps the count to the body alone. it("creates each taxonomy bucket once, however many units share the value", async () => { const ids = Array.from( @@ -3951,7 +4018,7 @@ describe("design references and the architecture taxonomy", () => { const body = compileBody(recipeByOrdinal(28).body); const reader = createReader(graph); const report = validateGraph(graph); - const { value, sets } = countMapSets(() => body(reader, graph, report)); + const { value, sets } = countMapSets(() => body(reader, graph, report, {})); const result = asRecord(await value); // Two buckets, `service` and `probe`, two Map sets. Replacing the bucket on every insertion @@ -3962,3 +4029,1598 @@ describe("design references and the architecture taxonomy", () => { expect(result.layers).toEqual([]); }); }); + +const recipeParametersCatalogAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.recipe-parameters-catalog"), + label: "asserts realization of the catalog recipes that read their parameter from params", + satisfies: ref("spec:consumers.agent-surface.recipe-parameters"), +}); +void recipeParametersCatalogAnchor; +const recipeParametersTestAnchor = specTest({ + id: testAnchorId("test:protocol.recipe-parameters"), + label: "recipe checks verify parameters passed as data and the shipped recipe files", + verifies: ref("spec:consumers.agent-surface.recipe-parameters"), +}); +void recipeParametersTestAnchor; + +const shippedRecipesDirectory = join(repoRoot, "dist", "recipes"); + +/** The file the build owes a recipe: its two-digit number, then its heading in lower kebab case. */ +function shippedRecipeName(recipe: Recipe): string { + const slug = recipe.title + .toLowerCase() + .replace(/[^a-z0-9]+/gu, "-") + .replace(/^-+|-+$/gu, ""); + + return `${String(recipe.ordinal).padStart(2, "0")}-${slug}.js`; +} + +function readShippedRecipe(recipe: Recipe): string { + const path = join(shippedRecipesDirectory, shippedRecipeName(recipe)); + + if (!existsSync(path)) { + throw new Error(`${path} is missing: run npm run build before the recipe check`); + } + + return readFileSync(path, "utf8"); +} + +/** The body `sdp q "$(cat FILE)"` hands the sink: command substitution drops trailing newlines. */ +function shippedBody(recipe: Recipe): string { + return readShippedRecipe(recipe).replace(/\n+$/u, ""); +} + +/** The catalog text a recipe owns, heading through the line before the next numbered heading. */ +function recipeProse(recipe: Recipe): string { + const start = source.indexOf(`\n## ${String(recipe.ordinal)}. `); + const next = source.indexOf(`\n## ${String(recipe.ordinal + 1)}. `, start + 1); + + return source.slice(start, next === -1 ? undefined : next).replace(recipe.body, ""); +} + +async function runShipped( + body: string, + params: readonly string[], +): Promise<{ readonly exitCode: number; readonly stdout: string; readonly stderr: string }> { + const capture = createCaptureOutput(); + const exitCode = await runSdpCli( + ["q", body, "--root", repoRoot, "--json", ...params], + capture.output, + queryHooks, + ); + + return { exitCode, stdout: capture.readStdout(), stderr: capture.readStderr() }; +} + +describe("recipe parameters and the shipped recipe files", () => { + it("reads each parameter from params, falls back to the catalog sample, and names it", () => { + const parameterized = recipes.filter((recipe) => /\bparams\./u.test(recipe.body)); + + expect(parameterized.map((recipe) => recipe.ordinal)).toEqual( + expect.arrayContaining([3, 4, 5, 9, 19, 21, 22, 25, 27]), + ); + + for (const recipe of parameterized) { + const names = new Set( + [...recipe.body.matchAll(/\bparams\.([A-Za-z]+)/gu)].map((match) => match[1] ?? ""), + ); + + for (const name of names) { + expect({ + recipe: recipe.ordinal, + fallback: recipe.body.includes(`params.${name} ?? `), + }).toEqual({ recipe: recipe.ordinal, fallback: true }); + expect({ + recipe: recipe.ordinal, + named: recipeProse(recipe).includes(`\`params.${name}\``), + }).toEqual({ recipe: recipe.ordinal, named: true }); + } + } + }); + + it("ships every catalog body as dist/recipes/NN-slug.js, byte for byte", () => { + const shipped = readdirSync(shippedRecipesDirectory) + .filter((entry) => entry.endsWith(".js")) + .sort(); + + expect(shipped).toEqual(recipes.map(shippedRecipeName).sort()); + for (const recipe of recipes) { + expect({ recipe: recipe.ordinal, bytes: readShippedRecipe(recipe) }).toEqual({ + recipe: recipe.ordinal, + bytes: `${recipe.body}\n`, + }); + } + + // The build step derives the same pairing the check parses: one file per numbered heading. + expect(recipeFiles(source).map((file) => file.fileName)).toEqual( + recipes.map(shippedRecipeName), + ); + + const packageJson = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as { + readonly files: readonly string[]; + }; + expect(packageJson.files).toContain("dist/**"); + }); + + it("runs every shipped file, read whole, to the answer its catalog body gives", async () => { + for (const recipe of recipes) { + const shipped = await runRecipe({ ...recipe, body: shippedBody(recipe) }); + + expect({ recipe: recipe.ordinal, shipped }).toEqual({ + recipe: recipe.ordinal, + shipped: await runRecipe(recipe), + }); + } + }); + + it("refuses a catalog whose bodies and headings do not pair, rather than guess a file name", () => { + expect(() => recipeFiles("## 1. One\n\n```js\nreturn 1;\n```\n## 1. Again\n")).toThrow( + "recipe 1 is numbered twice", + ); + expect(() => recipeFiles("```js\nreturn 1;\n```\n")).toThrow("has no numbered heading"); + expect(() => recipeFiles("## 2. Two\n\n## 3. Three\n```js\nreturn 3;\n```\n")).toThrow( + "recipe 2 has no js body", + ); + expect(() => recipeFiles("## 4. Four\n```js\nreturn 4;\n")).toThrow("is never closed"); + expect(() => recipeFiles("## 5. !!!\n```js\nreturn 5;\n```\n")).toThrow("has no heading text"); + expect(recipeFiles("## 7. Roles, layers and contexts\n```js\nreturn 7;\n```\n")).toEqual([ + { ordinal: 7, fileName: "07-roles-layers-and-contexts.js", content: "return 7;\n" }, + ]); + }); + + it("reads a CRLF catalog as its LF bodies, and refuses a catalog with no recipe before publishing", () => { + const crlf = source.replaceAll("\n", "\r\n"); + + expect(recipeFiles(crlf)).toEqual(recipeFiles(source)); + expect(() => recipeFiles("# Agent-surface recipes\r\n\r\nNo bodies yet.\r\n")).toThrow( + "has no numbered recipe", + ); + + // Through the build step: a CRLF catalog publishes every body, and an empty one publishes + // nothing and removes nothing. + const root = mkdtempSync(join(tmpdir(), "sdp-recipe-files-")); + const catalogPath = join(root, recipesPath); + const recipesOut = join(root, "dist", "recipes"); + + try { + mkdirSync(join(root, "docs", "agent-surface"), { recursive: true }); + writeFileSync(catalogPath, crlf); + writeBuildArtifacts({ root, outDir: join(root, "dist"), git: () => "unknown" }); + expect(readdirSync(recipesOut).sort()).toEqual(recipes.map(shippedRecipeName).sort()); + for (const recipe of recipes) { + expect({ + recipe: recipe.ordinal, + bytes: readFileSync(join(recipesOut, shippedRecipeName(recipe)), "utf8"), + }).toEqual({ recipe: recipe.ordinal, bytes: `${recipe.body}\n` }); + } + + writeFileSync(catalogPath, "# Agent-surface recipes\r\n"); + expect(() => { + writeBuildArtifacts({ root, outDir: join(root, "dist"), git: () => "unknown" }); + }).toThrow("has no numbered recipe"); + expect(readdirSync(recipesOut).sort()).toEqual(recipes.map(shippedRecipeName).sort()); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("removes only completed recipe files the catalog no longer owes, never another run's temporary file", () => { + const outDir = mkdtempSync(join(tmpdir(), "sdp-recipe-stale-")); + const directory = join(outDir, "recipes"); + const foreignTemporary = "01-one.js.4242.tmp"; + + try { + mkdirSync(directory, { recursive: true }); + writeFileSync(join(directory, "02-retired.js"), "return 2;\n"); + writeFileSync(join(directory, foreignTemporary), "return 'half written';\n"); + + writeRecipeFiles(outDir, recipeFiles("## 1. One\n```js\nreturn 1;\n```\n")); + + expect(readdirSync(directory).sort()).toEqual(["01-one.js", foreignTemporary]); + } finally { + rmSync(outDir, { recursive: true, force: true }); + } + }); + + it("passes recipe 4 its changed files from a file named with @PATH", async () => { + const directory = mkdtempSync(join(tmpdir(), "sdp-recipe-params-")); + const files = ["src/reader/reader.ts", "docs/agent-surface/recipes.md", "absent/file.ts"]; + + try { + const path = join(directory, "changed.json"); + writeFileSync(path, JSON.stringify({ files })); + + const run = await runShipped(shippedBody(recipeByOrdinal(4)), ["--params", `@${path}`]); + expect({ exitCode: run.exitCode, stderr: run.stderr }).toEqual({ exitCode: 0, stderr: "" }); + + const result = asRecord(JSON.parse(run.stdout)); + const radius = reader.blastRadius(files); + expect(result.changedFiles).toEqual(radius.changedFiles); + expect(asArray(result.impactedSpecs).map((row) => stringAt(asRecord(row), "id"))).toEqual( + radius.impactedSpecs.map((item) => item.id), + ); + expect(result.coverageUnknownFiles).toEqual(radius.coverageUnknown); + expect(result.coverageUnknownFiles).toContain("absent/file.ts"); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }); + + it("resolves the addresses recipe 25 receives as data", async () => { + const holder = reader.specs().find((spec) => { + const design = reader.specContext(spec.id)?.sections?.design; + return ( + design !== undefined && Object.keys(design).some((key) => /^[a-z][A-Za-z0-9]*$/u.test(key)) + ); + }); + if (holder === undefined) throw new Error("the corpus holds no keyed Design entry"); + const key = Object.keys(reader.specContext(holder.id)?.sections?.design ?? {}).find( + (candidate) => candidate !== "description" && /^[a-z][A-Za-z0-9]*$/u.test(candidate), + ); + if (key === undefined) throw new Error(`${holder.id} holds no addressable Design key`); + const addresses = [ + `${holder.id}#design.${key}`, + `${holder.id}#design.noSuchKey`, + "spec:probe.absent#design.anyKey", + holder.id, + ]; + + const run = await runShipped(shippedBody(recipeByOrdinal(25)), [ + "--params", + JSON.stringify({ addresses }), + ]); + expect({ exitCode: run.exitCode, stderr: run.stderr }).toEqual({ exitCode: 0, stderr: "" }); + + const result = asRecord(JSON.parse(run.stdout)); + expect(result.totals).toEqual({ + addresses: 4, + resolved: 1, + malformed: 1, + spec: 1, + entry: 1, + }); + expect(asArray(result.rows).map((row) => asRecord(row).reason)).toEqual([ + null, + "entry", + "spec", + "malformed", + ]); + }); + + it("reads the Pack the Pack recipes review from params.pack", async () => { + const packIds = reader.packs().map((pack) => pack.id); + const chosen = packIds[packIds.length - 1]; + if (chosen === undefined) throw new Error("the corpus holds no Pack"); + const members = reader.packContext(chosen)?.members.length; + + for (const ordinal of [5, 27]) { + const body = shippedBody(recipeByOrdinal(ordinal)); + const run = await runShipped(body, ["--params", JSON.stringify({ pack: chosen })]); + expect({ exitCode: run.exitCode, stderr: run.stderr }).toEqual({ exitCode: 0, stderr: "" }); + + const result = asRecord(JSON.parse(run.stdout)); + expect(result.id).toBe(chosen); + expect(ordinal === 5 ? result.memberCount : asRecord(result.totals).members).toBe(members); + + const absent = await runShipped(body, ["--params", '{"pack":"pack:absent-v1"}']); + expect(JSON.parse(absent.stdout)).toEqual({ id: "pack:absent-v1", found: false }); + } + + // Without the parameter, recipe 5 falls back to its sample: the first Pack in the graph. + expect(asRecord(await runRecipe(recipeByOrdinal(5))).id).toBe(packIds[0]); + }); +}); + +const designRecipesImplementationAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.design-recipes"), + label: "asserts realization of the shipped design recipes", + satisfies: ref("spec:consumers.agent-surface.design-recipes"), +}); +void designRecipesImplementationAnchor; +const designRecipesTestAnchor = specTest({ + id: testAnchorId("test:protocol.design-recipes"), + label: "recipe checks verify the design recipes", + verifies: ref("spec:consumers.agent-surface.design-recipes"), +}); +void designRecipesTestAnchor; + +type ProbeSpecKind = Extract<GraphNode, { nodeType: "Primitive" }>["specKind"]; +type ProbeReadiness = Extract<GraphNode, { nodeType: "Primitive" }>["readiness"]; +type ProbeSections = Extract<GraphNode, { nodeType: "Primitive" }>["sections"]; + +/** One Spec of an in-memory probe graph. */ +function probeSpec( + id: string, + specKind: ProbeSpecKind = "rule", + readiness: ProbeReadiness = "idea", + sections: ProbeSections = { intent: { outcome: "Probe." } }, +): GraphNode { + return { + id, + nodeType: "Primitive", + claim: "declared", + specKind, + altitude: "story", + readiness, + title: `Title of ${id}`, + file: `specs/${id}.sdp.md`, + sections, + }; +} + +/** One code unit of an in-memory probe graph, with its structural attributes. */ +function probeUnit( + id: string, + line: number, + structure: { readonly layer?: "edge" | "domain"; readonly context?: string } = {}, +): GraphNode { + return { id, nodeType: "CodeNode", claim: "anchored", file: "src/probe.ts", line, ...structure }; +} + +function probeEdge( + from: string, + type: GraphEdge["type"], + to: string, + claim: GraphEdge["claim"] = "declared", +): GraphEdge { + return { from, type, to, claim }; +} + +/** + * A synthetic extraction built in memory. The probe is the recipe's reading of a graph, not the + * extractor, so the stub carries only what the query sink reads: the graph, an error-free report, + * and the counts. + */ +function syntheticExtraction( + nodes: readonly GraphNode[], + edges: readonly GraphEdge[], +): ExtractionResult { + return { + graph: { schemaVersion, nodes: [...nodes], edges: [...edges] }, + report: { validatorId: extractValidatorId, findings: [] }, + counts: { + specs: nodes.filter((node) => node.nodeType === "Primitive").length, + packs: nodes.filter((node) => node.nodeType === "Pack").length, + anchors: nodes.filter((node) => node.nodeType === "Anchor" || node.nodeType === "CodeNode") + .length, + }, + }; +} + +const designFollowedTypes = new Set(["refines", "dependsOn", "constrainedBy", "decidedBy"]); + +/** The units bound to a Spec by one edge type, read from the raw graph in code-unit order. */ +function rawUnits(extraction: ExtractionResult, type: "satisfies" | "references", specId: string) { + const nodes = new Map(extraction.graph.nodes.map((node) => [node.id, node] as const)); + return extraction.graph.edges + .filter((edge) => edge.type === type && edge.to === specId) + .map((edge) => nodes.get(edge.from)) + .filter( + (node): node is Extract<GraphNode, { nodeType: "CodeNode" }> => node?.nodeType === "CodeNode", + ) + .sort((left, right) => codeUnitOrder(left.id, right.id)); +} + +/** The resolving verifiers of a Spec from the raw graph: test anchors and enabled examples. */ +function rawEnabledVerifiers(extraction: ExtractionResult, specId: string): string[] { + const nodes = new Map(extraction.graph.nodes.map((node) => [node.id, node] as const)); + const anchoredBy = (id: string) => + extraction.graph.edges.some( + (edge) => + edge.type === "verifies" && + edge.to === id && + edge.claim === "anchored" && + nodes.get(edge.from)?.nodeType === "Anchor", + ); + return extraction.graph.edges + .filter((edge) => edge.type === "verifies" && edge.to === specId) + .filter((edge) => { + const source = nodes.get(edge.from); + if (source?.nodeType === "Anchor") return edge.claim === "anchored"; + return ( + source?.nodeType === "Primitive" && + source.specKind === "example" && + edge.claim === "declared" && + anchoredBy(source.id) + ); + }) + .map((edge) => edge.from) + .sort(codeUnitOrder); +} + +/** + * Design-change impact by relaxation, a second derivation independent of the recipe's walk: the + * shortest number of inbound `refines`, `dependsOn`, `constrainedBy` or `decidedBy` hops from any + * changed Spec to every Spec that rests on it. + */ +function impactOracle(extraction: ExtractionResult, changed: readonly string[]) { + const specIds = new Set( + extraction.graph.nodes.filter((node) => node.nodeType === "Primitive").map((node) => node.id), + ); + const relations = extraction.graph.edges.filter( + (edge) => + designFollowedTypes.has(edge.type) && + edge.claim === "declared" && + specIds.has(edge.from) && + specIds.has(edge.to), + ); + const distance = new Map(changed.filter((id) => specIds.has(id)).map((id) => [id, 0])); + for (let moved = true; moved; ) { + moved = false; + for (const edge of relations) { + const via = distance.get(edge.to); + const current = distance.get(edge.from); + if (via !== undefined && (current === undefined || via + 1 < current)) { + distance.set(edge.from, via + 1); + moved = true; + } + } + } + return { distance, relations }; +} + +/** The `uses` crossings of a graph computed straight from its edges, beside the recipe's own. */ +function crossingOracle(extraction: ExtractionResult) { + const units = new Map( + extraction.graph.nodes + .filter((node) => node.nodeType === "CodeNode") + .map((node) => [node.id, node] as const), + ); + const owner = (id: string) => { + if (id.startsWith("component:")) return units.get(id); + const memberOf = extraction.graph.edges.find( + (edge) => edge.type === "memberOf" && edge.from === id, + ); + return memberOf === undefined || !units.has(id) ? undefined : units.get(memberOf.to); + }; + const uses = extraction.graph.edges.filter((edge) => edge.type === "uses"); + const resolved = uses.filter( + (edge) => owner(edge.from) !== undefined && owner(edge.to) !== undefined, + ); + const rows = resolved + .map((edge) => { + const from = owner(edge.from); + const to = owner(edge.to); + const differs = (["context", "layer"] as const).filter( + (field) => + from?.[field] !== undefined && to?.[field] !== undefined && from[field] !== to[field], + ); + const describe = (node: typeof from) => ({ + id: node?.id, + layer: node?.layer ?? null, + context: node?.context ?? null, + }); + return { + from: edge.from, + to: edge.to, + fromComponent: describe(from), + toComponent: describe(to), + differs: [...differs], + }; + }) + .filter((row) => row.differs.length > 0) + .sort( + (left, right) => codeUnitOrder(left.from, right.from) || codeUnitOrder(left.to, right.to), + ); + return { uses, resolved, rows }; +} + +/** + * The Pack design probe: three members in a Pack, two Specs it rests on, one Spec resting on it, + * comment-form anchors with a component, and, injected after extraction, a member the graph does + * not hold and a `decidedBy` whose target is no Spec. + */ +function packDesignProbe(): ExtractionResult { + const extraction = markdownProbe("sdp-pack-design-probe-", { + "probe.pack.sdp.md": `--- +id: pack:probe-design-v1 +specs: + - spec:probe.page + - spec:probe.store + - spec:probe.note +--- +# Probe design + +The probe's framing. +`, + "page.sdp.md": `--- +id: spec:probe.page +kind: behavior +altitude: story +readiness: scoped +relations: + dependsOn: spec:probe.base + decidedBy: spec:probe.choice +--- +# Probe page + +## Intent +- outcome: Render the probe page. + +### Open questions +- [blocking #layout] Which layout holds? +- [non-blocking] Who reads the page? + +## Behavior +- rule: The page renders. + +## Design +- render: \`render(): string\` returns the page. +- shape: A plain shape. + +## UI +- panel: One panel. +`, + "store.sdp.md": `--- +id: spec:probe.store +kind: behavior +altitude: story +readiness: defined +relations: + refines: spec:probe.page +--- +# Probe store + +## Intent +- outcome: Store the page. + +## Behavior +- rule: The store keeps the page. +`, + "note.sdp.md": `--- +id: spec:probe.note +kind: rule +altitude: story +readiness: idea +relations: + refines: spec:probe.page +--- +# Probe note + +## Intent +- outcome: Note the page. + +## Rule +- The note states one rule. +`, + "base.sdp.md": `--- +id: spec:probe.base +kind: rule +altitude: story +readiness: idea +relations: {} +--- +# Probe base + +## Intent +- outcome: Rest the page on a base. + +## Rule +- The base states one rule. +`, + "choice.sdp.md": `--- +id: spec:probe.choice +kind: decision +altitude: story +readiness: idea +relations: {} +--- +# Probe choice + +## Intent +- outcome: Decide the page. + +## Decision +- context: The page needs a choice. +- decision: The page takes the plain shape. +`, + "reader.sdp.md": `--- +id: spec:probe.reader +kind: behavior +altitude: story +readiness: idea +relations: + refines: spec:probe.store +--- +# Probe reader + +## Intent +- outcome: Read the stored page. + +## Behavior +- rule: The reader reads the page. +`, + "code.ts": `/** + * @sdpAnchor component:probe.web + * @sdpLayer edge + * @sdpContext studio + */ + +/** + * @sdpAnchor impl:probe.page + * @sdpSatisfies spec:probe.page + * @sdpComponent component:probe.web + * @sdpRole projection + */ + +/** + * @sdpAnchor impl:probe.store + * @sdpReferences spec:probe.store + */ + +/** + * @sdpAnchor test:probe.page + * @sdpVerifies spec:probe.page + */ +export const probe = 1; +`, + }); + return { + ...extraction, + graph: { + ...extraction.graph, + nodes: extraction.graph.nodes.map((node) => + node.nodeType === "Pack" + ? { ...node, members: [...node.members, "spec:probe.absent"] } + : node, + ), + edges: [ + ...extraction.graph.edges, + probeEdge("spec:probe.page", "decidedBy", "spec:probe.gone"), + ], + }, + }; +} + +describe("design recipes", () => { + it("returns the Pack design of every corpus Pack as the raw graph states it", async () => { + const locations = derived.graph.locations ?? []; + const pinned = /^(`+)(?!`)([^\r\n]+?)(?<!`)\1(?!`)/u; + const keyed = (content: unknown) => + typeof content === "object" && content !== null + ? Object.entries(content).filter(([key]) => key !== "description") + : []; + const memberOfEdges = new Map( + derived.graph.edges + .filter((edge) => edge.type === "memberOf") + .map((edge) => [edge.from, edge.to] as const), + ); + const nodes = new Map(derived.graph.nodes.map((node) => [node.id, node] as const)); + const unitOf = (unit: Extract<GraphNode, { nodeType: "CodeNode" }>) => { + const componentId = memberOfEdges.get(unit.id); + const component = componentId === undefined ? undefined : nodes.get(componentId); + return { + id: unit.id, + file: unit.file, + line: unit.line ?? null, + role: unit.role ?? null, + component: + componentId === undefined + ? null + : { + id: componentId, + layer: component?.nodeType === "CodeNode" ? (component.layer ?? null) : null, + context: component?.nodeType === "CodeNode" ? (component.context ?? null) : null, + }, + }; + }; + const packs = derived.graph.nodes.filter((node) => node.nodeType === "Pack"); + expect(packs.length).toBeGreaterThan(0); + + for (const pack of packs) { + const result = asRecord(await runRecipe(recipeByOrdinal(29), { pack: pack.id })); + const members = asArray(result.members).map(asRecord); + expect(result.found).toBe(true); + expect(members.map((member) => member.id)).toEqual(pack.members); + + for (const member of members) { + const node = primitivesById.get(stringAt(member, "id")); + if (node === undefined) { + expect(member).toEqual({ id: member.id, resolved: false }); + continue; + } + const questions = node.sections?.intent?.openQuestions ?? []; + const design = keyed(node.sections?.design); + expect({ + id: member.id, + statedReadiness: member.statedReadiness, + statedNextRung: member.statedNextRung, + design: member.design, + questions: member.questions, + decisions: asArray(member.decisions).map((entry) => asRecord(entry).id), + implementedBy: member.implementedBy, + referencedBy: member.referencedBy, + }).toEqual({ + id: node.id, + statedReadiness: node.readiness, + statedNextRung: rungs[rungs.indexOf(node.readiness) + 1] ?? null, + design: { + entries: design.length + keyed(node.sections?.ui).length, + declarations: design.filter( + ([, value]) => typeof value === "string" && pinned.test(value), + ).length, + openQuestions: questions.length, + blockingQuestions: questions.filter( + (entry) => typeof entry !== "string" && entry.blocking === true, + ).length, + }, + questions: questions.map((entry, index) => { + const key = typeof entry === "string" ? undefined : entry.key; + return { + key: key ?? null, + blocking: typeof entry !== "string" && entry.blocking === true, + address: key === undefined ? null : `${node.id}#question.${key}`, + line: + locations.find( + (row) => row.spec === node.id && row.entry === `question[${String(index)}]`, + )?.line ?? null, + question: typeof entry === "string" ? entry : entry.question, + }; + }), + decisions: [ + ...new Set( + derived.graph.edges + .filter((edge) => edge.from === node.id && edge.type === "decidedBy") + .map((edge) => edge.to), + ), + ].sort(codeUnitOrder), + implementedBy: rawUnits(derived, "satisfies", node.id).map(unitOf), + referencedBy: rawUnits(derived, "references", node.id).map(unitOf), + }); + // The stated next rung's clauses pass through from the one floor evaluator unchanged. + const context = reader.packContext(pack.id); + const fromReader = context?.members.find((entry) => entry.id === node.id); + expect(member.statedNextRungFailures).toEqual( + (fromReader?.statedNextRungFailures ?? []).map((failure) => ({ ...failure })), + ); + } + + const inside = new Set(pack.members); + const outside = (ids: readonly string[]) => + [...new Set(ids)] + .filter((id) => !inside.has(id) && primitivesById.has(id)) + .sort(codeUnitOrder); + const memberEdges = derived.graph.edges.filter( + (edge) => edge.claim === "declared" && edge.type !== "belongsTo", + ); + const boundary = asRecord(result.boundary); + expect({ + restsOn: asArray(boundary.restsOn).map((row) => asRecord(row).id), + restedOnBy: asArray(boundary.restedOnBy).map((row) => asRecord(row).id), + verifiedBy: asArray(boundary.verifiedBy).map((row) => asRecord(row).id), + supersededBy: asArray(boundary.supersededBy).map((row) => asRecord(row).id), + }).toEqual({ + restsOn: outside( + memberEdges + .filter((edge) => inside.has(edge.from) && designFollowedTypes.has(edge.type)) + .map((edge) => edge.to), + ), + restedOnBy: outside( + memberEdges + .filter((edge) => inside.has(edge.to) && designFollowedTypes.has(edge.type)) + .map((edge) => edge.from), + ), + verifiedBy: outside( + memberEdges + .filter((edge) => inside.has(edge.to) && edge.type === "verifies") + .map((edge) => edge.from), + ), + supersededBy: outside( + memberEdges + .filter((edge) => inside.has(edge.to) && edge.type === "supersedes") + .map((edge) => edge.from), + ), + }); + } + + // Without the parameter the body reads its sample, the Studio Pack. + expect(asRecord(await runRecipe(recipeByOrdinal(29))).id).toBe(studioPackId); + expect(await runRecipe(recipeByOrdinal(29), { pack: "pack:absent-v1" })).toEqual({ + id: "pack:absent-v1", + found: false, + }); + }, 20_000); + + it("returns the probe Pack's design exactly, member by member", async () => { + const result = await runRecipe( + recipeByOrdinal(29), + { pack: "pack:probe-design-v1" }, + packDesignProbe(), + ); + const bound = (type: string, members: readonly string[]) => ({ type, members }); + const counts = (total: number, enabled: number) => ({ total, enabled }); + const empty = { entries: 0, declarations: 0, openQuestions: 0, blockingQuestions: 0 }; + + expect(result).toEqual({ + found: true, + id: "pack:probe-design-v1", + title: "Probe design", + totals: { + members: 4, + unresolved: 1, + heldBelowNextRung: 2, + waitingForAuthor: 1, + entries: 3, + declarations: 1, + openQuestions: 2, + blockingQuestions: 1, + withImplementations: 1, + withReferences: 1, + restsOn: 2, + restedOnBy: 1, + verifiedBy: 0, + supersededBy: 0, + }, + members: [ + { + id: "spec:probe.page", + resolved: true, + title: "Probe page", + specKind: "behavior", + statedReadiness: "scoped", + floorReached: "scoped", + statedNextRung: "defined", + statedNextRungFailures: [ + { + clauseId: "no-blocking-open-questions", + description: "Spec has no blocking open question in intent.openQuestions.", + }, + ], + design: { entries: 3, declarations: 1, openQuestions: 2, blockingQuestions: 1 }, + questions: [ + { + key: "layout", + blocking: true, + address: "spec:probe.page#question.layout", + line: 16, + question: "Which layout holds?", + }, + { + key: null, + blocking: false, + address: null, + line: 17, + question: "Who reads the page?", + }, + ], + decisions: [ + { id: "spec:probe.choice", statedReadiness: "idea", resolved: true }, + { id: "spec:probe.gone", statedReadiness: null, resolved: false }, + ], + implementedBy: [ + { + id: "impl:probe.page", + file: "code.ts", + line: 7, + role: "projection", + component: { id: "component:probe.web", layer: "edge", context: "studio" }, + }, + ], + referencedBy: [], + verifiers: counts(1, 1), + examples: counts(0, 0), + }, + { + id: "spec:probe.store", + resolved: true, + title: "Probe store", + specKind: "behavior", + statedReadiness: "defined", + floorReached: "defined", + statedNextRung: "ready", + statedNextRungFailures: [ + { + clauseId: "typed-dependency-targets-are-defined", + description: + "Every refines, dependsOn, constrainedBy, and decidedBy target states at least defined.", + targets: [{ type: "refines", id: "spec:probe.page", statedReadiness: "scoped" }], + }, + ], + design: empty, + questions: [], + decisions: [], + implementedBy: [], + referencedBy: [ + { id: "impl:probe.store", file: "code.ts", line: 14, role: null, component: null }, + ], + verifiers: counts(0, 0), + examples: counts(0, 0), + }, + { + // The floor already holds the stated next rung: the rung waits for its author. + id: "spec:probe.note", + resolved: true, + title: "Probe note", + specKind: "rule", + statedReadiness: "idea", + floorReached: "defined", + statedNextRung: "scoped", + statedNextRungFailures: [], + design: empty, + questions: [], + decisions: [], + implementedBy: [], + referencedBy: [], + verifiers: counts(0, 0), + examples: counts(0, 0), + }, + { id: "spec:probe.absent", resolved: false }, + ], + boundary: { + // The dangling `decidedBy` is referential integrity's finding and joins no boundary row. + restsOn: [ + { + id: "spec:probe.base", + title: "Probe base", + statedReadiness: "idea", + implemented: false, + via: [bound("dependsOn", ["spec:probe.page"])], + }, + { + id: "spec:probe.choice", + title: "Probe choice", + statedReadiness: "idea", + implemented: false, + via: [bound("decidedBy", ["spec:probe.page"])], + }, + ], + restedOnBy: [ + { + id: "spec:probe.reader", + title: "Probe reader", + statedReadiness: "idea", + implemented: false, + via: [bound("refines", ["spec:probe.store"])], + }, + ], + verifiedBy: [], + supersededBy: [], + }, + }); + }); + + it("reaches every Spec resting on the corpus sample, by a shortest path of real edges", async () => { + const recipe = recipeByOrdinal(30); + const sample = "spec:validation.readiness-floor"; + const result = asRecord(await runRecipe(recipe)); + const { distance, relations } = impactOracle(derived, [sample]); + const rows = asArray(result.rows).map(asRecord); + + expect(result.changed).toEqual([{ id: sample, resolved: true }]); + expect(rows.map((row) => [row.id, row.distance])).toEqual( + [...distance.entries()].sort( + ([leftId, left], [rightId, right]) => left - right || codeUnitOrder(leftId, rightId), + ), + ); + expect(rows.length).toBeGreaterThan(1); + for (const row of rows) { + const path = asArray(row.path).map(asRecord); + expect(path).toHaveLength(numberAt(row, "distance")); + path.forEach((hop, index) => { + expect(hop.to).toBe(index === 0 ? sample : path[index - 1]?.from); + expect( + relations.some( + (edge) => edge.from === hop.from && edge.type === hop.type && edge.to === hop.to, + ), + ).toBe(true); + }); + if (path.length > 0) expect(path[path.length - 1]?.from).toBe(row.id); + + const id = stringAt(row, "id"); + const located = (type: "satisfies" | "references") => + rawUnits(derived, type, id).map((unit) => ({ + id: unit.id, + file: unit.file, + line: unit.line ?? null, + })); + expect({ + id, + statedReadiness: row.statedReadiness, + implementedBy: row.implementedBy, + referencedBy: row.referencedBy, + enabledVerifiers: row.enabledVerifiers, + packs: row.packs, + }).toEqual({ + id, + statedReadiness: primitivesById.get(id)?.readiness, + implementedBy: located("satisfies"), + referencedBy: located("references"), + enabledVerifiers: rawEnabledVerifiers(derived, id), + packs: derived.graph.edges + .filter((edge) => edge.type === "belongsTo" && edge.from === id) + .map((edge) => edge.to) + .sort(codeUnitOrder), + }); + } + expect(result.totals).toEqual({ + specs: rows.length, + dependents: rows.length - 1, + units: new Set( + rows.flatMap((row) => + [...asArray(row.implementedBy), ...asArray(row.referencedBy)].map((unit) => + stringAt(asRecord(unit), "id"), + ), + ), + ).size, + }); + }); + + it("walks a dependency chain inbound only, once per Spec, and keeps a reference apart", async () => { + const extraction = syntheticExtraction( + [ + probeSpec("spec:probe.choice", "decision"), + probeSpec("spec:probe.root"), + probeSpec("spec:probe.twin"), + probeSpec("spec:probe.join"), + probeSpec("spec:probe.mid"), + probeSpec("spec:probe.leaf"), + probeSpec("spec:probe.far"), + probeSpec("spec:probe.outside"), + probeSpec("spec:probe.side"), + probeSpec("spec:probe.mid.sample", "example"), + probeUnit("impl:probe.root-unit", 3), + probeUnit("impl:probe.mid-ref", 7), + { + id: "test:probe.root", + nodeType: "Anchor", + claim: "anchored", + file: "test/probe.test.ts", + line: 1, + }, + { + id: "pack:probe-v1", + nodeType: "Pack", + claim: "declared", + members: ["spec:probe.mid"], + file: "probe.pack.sdp.md", + }, + ], + [ + probeEdge("spec:probe.root", "decidedBy", "spec:probe.choice"), + probeEdge("spec:probe.twin", "refines", "spec:probe.choice"), + probeEdge("spec:probe.twin", "dependsOn", "spec:probe.root"), + probeEdge("spec:probe.mid", "dependsOn", "spec:probe.root"), + probeEdge("spec:probe.join", "dependsOn", "spec:probe.twin"), + probeEdge("spec:probe.join", "refines", "spec:probe.root"), + probeEdge("spec:probe.leaf", "refines", "spec:probe.mid"), + probeEdge("spec:probe.leaf", "constrainedBy", "spec:probe.mid"), + probeEdge("spec:probe.far", "constrainedBy", "spec:probe.leaf"), + probeEdge("spec:probe.root", "dependsOn", "spec:probe.outside"), + probeEdge("spec:probe.mid.sample", "verifies", "spec:probe.mid"), + probeEdge("test:probe.root", "verifies", "spec:probe.root", "anchored"), + probeEdge("impl:probe.root-unit", "satisfies", "spec:probe.root", "anchored"), + probeEdge("impl:probe.mid-ref", "references", "spec:probe.mid", "anchored"), + probeEdge("spec:probe.mid", "belongsTo", "pack:probe-v1"), + ], + ); + const result = await runRecipe( + recipeByOrdinal(30), + { specs: ["spec:probe.choice", "spec:probe.absent"] }, + extraction, + ); + const hop = (from: string, type: string, to: string) => ({ from, type, to }); + const viaRoot = hop("spec:probe.root", "decidedBy", "spec:probe.choice"); + const viaMid = hop("spec:probe.mid", "dependsOn", "spec:probe.root"); + // Of two relations between one pair, the walk takes the first by type. + const viaLeaf = hop("spec:probe.leaf", "constrainedBy", "spec:probe.mid"); + const row = (id: string, distance: number, path: readonly unknown[], extra = {}) => ({ + id, + distance, + path, + statedReadiness: "idea", + implementedBy: [], + referencedBy: [], + enabledVerifiers: [], + packs: [], + ...extra, + }); + + expect(result).toEqual({ + changed: [ + { id: "spec:probe.choice", resolved: true }, + { id: "spec:probe.absent", resolved: false }, + ], + totals: { specs: 7, dependents: 6, units: 2 }, + rows: [ + row("spec:probe.choice", 0, []), + row("spec:probe.root", 1, [viaRoot], { + implementedBy: [{ id: "impl:probe.root-unit", file: "src/probe.ts", line: 3 }], + enabledVerifiers: ["test:probe.root"], + }), + // Reached once, by its shorter path through `refines`, never again through `root`. + row("spec:probe.twin", 1, [hop("spec:probe.twin", "refines", "spec:probe.choice")]), + // Two shortest paths: `root` is dequeued before `twin`, so its path is the one kept. + row("spec:probe.join", 2, [viaRoot, hop("spec:probe.join", "refines", "spec:probe.root")]), + // The example declares `verifies` but no test anchor binds it, so it is not enabled. + row("spec:probe.mid", 2, [viaRoot, viaMid], { + referencedBy: [{ id: "impl:probe.mid-ref", file: "src/probe.ts", line: 7 }], + packs: ["pack:probe-v1"], + }), + row("spec:probe.leaf", 3, [viaRoot, viaMid, viaLeaf]), + row("spec:probe.far", 4, [ + viaRoot, + viaMid, + viaLeaf, + hop("spec:probe.far", "constrainedBy", "spec:probe.leaf"), + ]), + ], + }); + }); + + it("registers every corpus decision with its supersession, subjects and keyed questions", async () => { + const result = asRecord(await runRecipe(recipeByOrdinal(31))); + const decisions = [...primitivesById.values()] + .filter((node) => node.specKind === "decision") + .sort((left, right) => codeUnitOrder(left.id, right.id)); + const ends = (type: string, side: "from" | "to", id: string) => + derived.graph.edges + .filter((edge) => edge.type === type && edge.claim === "declared" && edge[side] === id) + .map((edge) => (side === "from" ? edge.to : edge.from)) + .sort(codeUnitOrder); + const rows = asArray(result.rows).map(asRecord); + + expect(rows).toEqual( + decisions.map((node) => ({ + id: node.id, + title: node.title ?? null, + statedReadiness: node.readiness, + supersedes: ends("supersedes", "from", node.id), + supersededBy: ends("supersedes", "to", node.id), + narrows: ends("refines", "from", node.id).filter( + (id) => primitivesById.get(id)?.specKind === "decision", + ), + narrowedBy: ends("refines", "to", node.id).filter( + (id) => primitivesById.get(id)?.specKind === "decision", + ), + shapes: ends("decidedBy", "to", node.id), + questions: (node.sections?.intent?.openQuestions ?? []).flatMap((entry) => + typeof entry === "string" || entry.key === undefined + ? [] + : [ + { + key: entry.key, + blocking: entry.blocking === true, + address: `${node.id}#question.${entry.key}`, + }, + ], + ), + packs: ends("belongsTo", "from", node.id), + })), + ); + expect(rows.some((row) => asArray(row.narrowedBy).length > 0)).toBe(true); + const totals = asRecord(result.totals); + expect(totals.decisions).toBe(decisions.length); + expect( + Object.values(asRecord(totals.byRung)).reduce<number>((sum, n) => sum + Number(n), 0), + ).toBe(decisions.length); + }); + + it("registers a supersedes pair both ways and a decision that shapes nothing", async () => { + const extraction = syntheticExtraction( + [ + probeSpec("spec:probe.old", "decision", "ready"), + probeSpec("spec:probe.new", "decision", "defined", { + intent: { + outcome: "Replace the old decision.", + openQuestions: [ + { question: "Which scope?", blocking: true, key: "scope" }, + "A bare question.", + { question: "Later?", blocking: false }, + ], + }, + }), + probeSpec("spec:probe.lone", "decision"), + probeSpec("spec:probe.subject"), + probeSpec("spec:probe.legacy"), + { + id: "pack:probe-v1", + nodeType: "Pack", + claim: "declared", + members: ["spec:probe.new"], + file: "probe.pack.sdp.md", + }, + ], + [ + probeEdge("spec:probe.new", "supersedes", "spec:probe.old"), + probeEdge("spec:probe.subject", "decidedBy", "spec:probe.new"), + probeEdge("spec:probe.legacy", "decidedBy", "spec:probe.old"), + probeEdge("spec:probe.new", "belongsTo", "pack:probe-v1"), + ], + ); + const row = (id: string, statedReadiness: string, extra = {}) => ({ + id, + title: `Title of ${id}`, + statedReadiness, + supersedes: [], + supersededBy: [], + narrows: [], + narrowedBy: [], + shapes: [], + questions: [], + packs: [], + ...extra, + }); + + expect(await runRecipe(recipeByOrdinal(31), undefined, extraction)).toEqual({ + totals: { + decisions: 3, + byRung: { idea: 1, defined: 1, ready: 1 }, + superseded: 1, + shapingNothing: 1, + }, + rows: [ + row("spec:probe.lone", "idea"), + row("spec:probe.new", "defined", { + supersedes: ["spec:probe.old"], + shapes: ["spec:probe.subject"], + questions: [{ key: "scope", blocking: true, address: "spec:probe.new#question.scope" }], + packs: ["pack:probe-v1"], + }), + // A superseded decision stays a row, with the decision that supersedes it. + row("spec:probe.old", "ready", { + supersededBy: ["spec:probe.new"], + shapes: ["spec:probe.legacy"], + }), + ], + }); + }); + + it("registers narrowing decisions both ways and excludes refinement by other kinds", async () => { + const extraction = syntheticExtraction( + [ + probeSpec("spec:probe.freeze", "decision"), + probeSpec("spec:probe.carve", "decision"), + probeSpec("spec:probe.child", "rule"), + probeSpec("spec:probe.parent", "model"), + ], + [ + probeEdge("spec:probe.carve", "refines", "spec:probe.freeze"), + probeEdge("spec:probe.child", "refines", "spec:probe.freeze"), + probeEdge("spec:probe.carve", "refines", "spec:probe.parent"), + ], + ); + const result = asRecord(await runRecipe(recipeByOrdinal(31), undefined, extraction)); + expect(asArray(result.rows)).toMatchObject([ + { + id: "spec:probe.carve", + narrows: ["spec:probe.freeze"], + narrowedBy: [], + supersedes: [], + supersededBy: [], + }, + { + id: "spec:probe.freeze", + narrows: [], + narrowedBy: ["spec:probe.carve"], + supersedes: [], + supersededBy: [], + }, + ]); + }); + + it("reports the corpus's architecture crossings as the raw edges state them", async () => { + const result = asRecord(await runRecipe(recipeByOrdinal(32))); + const oracle = crossingOracle(derived); + const tally = (field: "context" | "layer") => { + const counts: Record<string, number> = {}; + for (const row of oracle.rows.filter((entry) => entry.differs.includes(field))) { + const pair = `${String(row.fromComponent[field])} -> ${String(row.toComponent[field])}`; + counts[pair] = (counts[pair] ?? 0) + 1; + } + return counts; + }; + + expect(result.rows).toEqual(oracle.rows); + expect(result.totals).toEqual({ + usesEdges: oracle.uses.length, + resolvedToComponents: oracle.resolved.length, + crossings: oracle.rows.length, + contextCrossings: oracle.rows.filter((row) => row.differs.includes("context")).length, + layerCrossings: oracle.rows.filter((row) => row.differs.includes("layer")).length, + byContextPair: tally("context"), + byLayerPair: tally("layer"), + }); + expect(Object.keys(asRecord(asRecord(result.totals).byLayerPair))).toEqual( + Object.keys(tally("layer")).sort(codeUnitOrder), + ); + }); + + it("names a crossing of two contexts and of two layers, and skips what cannot cross", async () => { + const extraction = syntheticExtraction( + [ + probeUnit("component:probe.web", 1, { layer: "edge", context: "studio" }), + probeUnit("component:probe.core", 2, { layer: "domain", context: "engine" }), + probeUnit("component:probe.store", 3, { layer: "domain", context: "studio" }), + probeUnit("component:probe.loose", 4, { context: "studio" }), + probeUnit("impl:probe.page", 5), + probeUnit("impl:probe.model", 6), + probeUnit("impl:probe.rule", 7), + probeUnit("impl:probe.free", 8), + ], + [ + probeEdge("impl:probe.page", "memberOf", "component:probe.web", "anchored"), + probeEdge("impl:probe.model", "memberOf", "component:probe.core", "anchored"), + probeEdge("impl:probe.rule", "memberOf", "component:probe.core", "anchored"), + probeEdge("impl:probe.page", "uses", "impl:probe.model", "anchored"), + probeEdge("impl:probe.page", "uses", "component:probe.store", "anchored"), + probeEdge("impl:probe.model", "uses", "impl:probe.rule", "anchored"), + probeEdge("component:probe.web", "uses", "component:probe.loose", "anchored"), + probeEdge("impl:probe.free", "uses", "impl:probe.model", "anchored"), + probeEdge("component:probe.store", "uses", "component:probe.core", "anchored"), + ], + ); + const component = (id: string, layer: string | null, context: string) => ({ + id, + layer, + context, + }); + const web = component("component:probe.web", "edge", "studio"); + const core = component("component:probe.core", "domain", "engine"); + const store = component("component:probe.store", "domain", "studio"); + + expect(await runRecipe(recipeByOrdinal(32), undefined, extraction)).toEqual({ + totals: { + usesEdges: 6, + // `impl:probe.free` belongs to no component, so its edge is counted and never a row. + resolvedToComponents: 5, + crossings: 3, + contextCrossings: 2, + layerCrossings: 2, + byContextPair: { "studio -> engine": 2 }, + byLayerPair: { "edge -> domain": 2 }, + }, + rows: [ + { + from: "component:probe.store", + to: "component:probe.core", + fromComponent: store, + toComponent: core, + differs: ["context"], + }, + { + from: "impl:probe.page", + to: "component:probe.store", + fromComponent: web, + toComponent: store, + differs: ["layer"], + }, + { + from: "impl:probe.page", + to: "impl:probe.model", + fromComponent: web, + toComponent: core, + differs: ["context", "layer"], + }, + ], + }); + }); +}); + +const entryLocationRecipesAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.entry-location-recipes"), + label: + "asserts realization of the entry lines the entry search and address resolution recipes return", + satisfies: ref("spec:extraction.entry-locations"), +}); +void entryLocationRecipesAnchor; + +describe("entry lines in entry search and address resolution", () => { + it("resolves every keyed corpus entry to its value and the line the location table records", async () => { + const lawfulKey = /^[a-z][A-Za-z0-9]*$/u; + const located = (derived.graph.locations ?? []).filter( + (row) => + (row.key !== undefined && row.entry.startsWith("question[")) || + /^(design|ui)\./u.test(row.entry), + ); + const addresses = located.flatMap((row) => { + const [section, key] = row.entry.startsWith("question[") + ? ["question", row.key ?? ""] + : row.entry.split(/\.(.*)/su); + return lawfulKey.test(key ?? "") ? [`${row.spec}#${section ?? ""}.${key ?? ""}`] : []; + }); + expect(addresses.length).toBeGreaterThan(0); + + const result = asRecord(await runRecipe(recipeByOrdinal(25), { addresses })); + const rows = asArray(result.rows).map(asRecord); + expect(asRecord(result.totals).resolved).toBe(addresses.length); + for (const row of rows) { + const id = stringAt(row, "id"); + const section = stringAt(row, "section"); + const key = stringAt(row, "key"); + const node = primitivesById.get(id); + const questions = node?.sections?.intent?.openQuestions ?? []; + const index = questions.findIndex((entry) => typeof entry !== "string" && entry.key === key); + const entry = section === "question" ? `question[${String(index)}]` : `${section}.${key}`; + const location = located.find( + (candidate) => candidate.spec === id && candidate.entry === entry, + ); + const question = questions[index]; + const authored: Record<string, unknown> | undefined = + section === "design" ? node?.sections?.design : node?.sections?.ui; + expect({ address: row.address, value: row.value, file: row.file, line: row.line }).toEqual({ + address: row.address, + value: + section === "question" + ? typeof question === "string" + ? question + : question?.question + : authored?.[key], + file: location?.file, + line: location?.line, + }); + } + }); + + it("gives each corpus entry-search row the line of a located entry and null otherwise", async () => { + const result = asRecord(await runRecipe(recipeByOrdinal(23), { term: "page" })); + const rows = asArray(result.matches).map(asRecord); + const locations = derived.graph.locations ?? []; + const expectedLine = (row: Record<string, unknown>) => { + const entry = typeof row.entry === "string" ? row.entry : ""; + const question = /^openQuestions\[(\d+)\](?:\.question)?$/u.exec(entry)?.[1]; + const name = + row.section === "intent" && question !== undefined + ? `question[${question}]` + : row.section === "design" || row.section === "ui" + ? `${row.section}.${entry}` + : undefined; + return ( + locations.find((location) => location.spec === row.id && location.entry === name)?.line ?? + null + ); + }; + + expect(rows.map((row) => [row.id, row.entry, row.line])).toEqual( + rows.map((row) => [row.id, row.entry, expectedLine(row)]), + ); + expect(rows.some((row) => typeof row.line === "number")).toBe(true); + expect(rows.some((row) => row.line === null)).toBe(true); + }); + + it("never gives a nested entry the line of a literal top-level key its path spells", async () => { + const root = mkdtempSync(join(tmpdir(), "sdp-entry-search-path-collision-")); + + try { + writeFileSync( + join(root, "subject.sdp.ts"), + `import { spec, specId } from "@libar-dev/software-delivery-protocol"; + +export const subject = spec({ + id: specId("spec:probe.collision"), + kind: "behavior", + altitude: "story", + readiness: "idea", + title: "Collision", + intent: { outcome: "Collide." }, + design: { + "shape.detail": "the literal gadget", + shape: { + detail: "the nested gadget", + }, + }, +}); +`, + ); + const extraction = extract({ root }); + expect(extraction.report.findings.filter((finding) => finding.severity === "error")).toEqual( + [], + ); + const located = extraction.graph.locations?.find( + (row) => row.spec === "spec:probe.collision" && row.entry === "design.shape.detail", + )?.line; + expect(located).toBe(11); + + const result = asRecord(await runRecipe(recipeByOrdinal(23), { term: "gadget" }, extraction)); + expect( + asArray(result.matches) + .map(asRecord) + .map((row) => [row.entry, row.text, row.line]), + ).toEqual([ + ["shape.detail", "the literal gadget", 11], + ["shape.detail", "the nested gadget", null], + ]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("counts a verifying Spec of another kind as a verifier, never as an example, in the Pack design", async () => { + const root = mkdtempSync(join(tmpdir(), "sdp-pack-design-verifier-kind-")); + const carrier = (id: string, relations: string) => `--- +id: ${id} +kind: behavior +altitude: story +readiness: idea +relations: ${relations} +--- +# Probe ${id} + +## Intent +- outcome: Probe ${id}. + +## Behavior +- rule: The probe states one rule. +`; + + try { + writeFileSync(join(root, "target.sdp.md"), carrier("spec:probe.target", "{}")); + writeFileSync( + join(root, "checker.sdp.md"), + carrier("spec:probe.checker", "\n verifies: spec:probe.target"), + ); + writeFileSync( + join(root, "kinds.pack.sdp.md"), + `--- +id: pack:probe.kinds +specs: + - spec:probe.target +--- +# Probe kinds +`, + ); + const extraction = extract({ root }); + expect(extraction.report.findings.filter((finding) => finding.severity === "error")).toEqual( + [], + ); + + const result = asRecord( + await runRecipe(recipeByOrdinal(29), { pack: "pack:probe.kinds" }, extraction), + ); + const target = asArray(result.members) + .map(asRecord) + .find((row) => row.id === "spec:probe.target"); + expect(target).toMatchObject({ + verifiers: { total: 1, enabled: 0 }, + examples: { total: 0, enabled: 0 }, + }); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("gives a string-form open question the line its question[n] row records, as the object form", async () => { + const root = mkdtempSync(join(tmpdir(), "sdp-entry-search-string-question-")); + + try { + writeFileSync( + join(root, "subject.sdp.ts"), + `import { spec, specId } from "@libar-dev/software-delivery-protocol"; + +export const subject = spec({ + id: specId("spec:probe.questions"), + kind: "behavior", + altitude: "story", + readiness: "idea", + title: "Questions", + intent: { + outcome: "Ask.", + openQuestions: [ + "Which gadget ships first?", + { question: "Who owns the gadget?", blocking: false }, + ], + }, +}); +`, + ); + const extraction = extract({ root }); + expect(extraction.report.findings).toEqual([]); + const lineOf = (entry: string) => + extraction.graph.locations?.find( + (row) => row.spec === "spec:probe.questions" && row.entry === entry, + )?.line; + expect([lineOf("question[0]"), lineOf("question[1]")]).toEqual([12, 13]); + + const result = asRecord(await runRecipe(recipeByOrdinal(23), { term: "gadget" }, extraction)); + expect( + asArray(result.matches) + .map(asRecord) + .map((row) => [row.entry, row.line]), + ).toEqual([ + ["openQuestions[0]", 12], + ["openQuestions[1].question", 13], + ]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/test/self-hosting-carrier-gherkin.test.ts b/test/self-hosting-carrier-gherkin.test.ts index c6c7e589..ec04c162 100644 --- a/test/self-hosting-carrier-gherkin.test.ts +++ b/test/self-hosting-carrier-gherkin.test.ts @@ -24,7 +24,7 @@ import type { GherkinAuthoringConditions, GherkinAuthoringOutcome, } from "../generated/contracts/carrier.gherkin-authoring.space.js"; -import { extract, generateContracts, serializeGraph, validateGraph } from "../src/index.js"; +import { extract, generateContracts, validateGraph } from "../src/index.js"; import type { Finding, GraphSchema, ValidationReport } from "../src/index.js"; import { registerAuthoredFactRefused } from "./carrier.gherkin-authoring.authored-fact-refused.test.generated.js"; import { registerDescriptionLocationRefused } from "./carrier.gherkin-authoring.description-location-refused.test.generated.js"; @@ -40,6 +40,8 @@ import { registerUnsupportedConstructRefused } from "./carrier.gherkin-authoring import { materializeGherkinCorpus, removeMaterializedCorpus } from "./helpers/extract-corpus.js"; import { paramsForStep } from "./helpers/generated-contract.js"; +import { serializeGraphStructure } from "./helpers/graph-structure.js"; + const temporaryRoots: string[] = []; afterEach(() => { @@ -306,8 +308,14 @@ const bindings = { const right = extract({ root: rightRoot }); expect(left.report.findings).toEqual([]); expect(right.report.findings).toEqual([]); - const leftBytes = serializeGraph(left.graph).replaceAll(params.parityLeft, "twin.carrier"); - const rightBytes = serializeGraph(right.graph).replaceAll(params.parityRight, "twin.carrier"); + const leftBytes = serializeGraphStructure(left.graph).replaceAll( + params.parityLeft, + "twin.carrier", + ); + const rightBytes = serializeGraphStructure(right.graph).replaceAll( + params.parityRight, + "twin.carrier", + ); expect(leftBytes).toBe(rightBytes); }, "the contracts for {parityLeft} equal the contracts for {parityRight}": ( diff --git a/test/self-hosting-duplicate-ids.test.ts b/test/self-hosting-duplicate-ids.test.ts index 9a6fe7b8..288e4982 100644 --- a/test/self-hosting-duplicate-ids.test.ts +++ b/test/self-hosting-duplicate-ids.test.ts @@ -248,6 +248,7 @@ function shimmedPoolRoot(shimBody: string): { root: string; environment: NodeJS. const binDirectory = join(root, "shim-bin"); mkdirSync(binDirectory); + writeFileSync(join(binDirectory, "package.json"), '{"type":"commonjs"}\n'); const shim = join(binDirectory, "vitest"); writeFileSync(shim, `#!/usr/bin/env node\n${shimBody}\nprocess.exit(0);\n`); chmodSync(shim, 0o755); diff --git a/test/self-hosting-graph.test.ts b/test/self-hosting-graph.test.ts index b9bac9cb..43e4bf48 100644 --- a/test/self-hosting-graph.test.ts +++ b/test/self-hosting-graph.test.ts @@ -31,6 +31,7 @@ import { expectedCommentAnchorIds, expectedComponentIds, expectedDeclaredRelations, + expectedDesignManagementPackMembers, expectedMemberOfEdges, expectedPackMembers, expectedStudioPackMembers, @@ -192,7 +193,7 @@ describe("the self-hosting corpus", () => { expect(result.report.findings).toEqual([]); }); - it("reports exactly the five informative honesty gaps and the nine unbacked prose mentions", () => { + it("reports exactly the two informative honesty gaps and the five unbacked prose mentions", () => { expect( validateGraph(result.graph).findings.map( ({ validatorId, family, severity, subjectId, relatedId }) => ({ @@ -210,20 +211,20 @@ describe("the self-hosting corpus", () => { // The literals are the corpus checkpoint. The authored arrays are measured against the same // literals rather than standing in for them, so a transcription slip in an oracle module // cannot certify itself by moving both sides of a comparison at once. - // re-measured under plan 40 - expect(result.counts).toEqual({ specs: 228, packs: 2, anchors: 235 }); - // re-measured under plan 40 - expect(expectedSpecs).toHaveLength(228); - // re-measured under plan 40 - expect(expectedPackMembers).toHaveLength(225); - // re-measured under plan 40 - expect(expectedAnchors).toHaveLength(235); - // re-measured under plan 40 - expect(expectedBindingEdges).toHaveLength(241); - // re-measured under plan 40 - expect(result.graph.nodes).toHaveLength(465); - // re-measured under plan 40 - expect(result.graph.edges).toHaveLength(1076); + // re-measured under plan 41's fold and the owner's second round + expect(result.counts).toEqual({ specs: 250, packs: 3, anchors: 263 }); + // re-measured under plan 41's fold and the owner's second round + expect(expectedSpecs).toHaveLength(250); + // re-measured under plan 41's fold and the owner's second round + expect(expectedPackMembers).toHaveLength(250); + // re-measured under plan 41's fold and the owner's second round + expect(expectedAnchors).toHaveLength(263); + // re-measured under plan 41's fold and the owner's second round + expect(expectedBindingEdges).toHaveLength(274); + // re-measured under plan 41's fold and the owner's second round + expect(result.graph.nodes).toHaveLength(516); + // re-measured under plan 41's fold and the owner's second round + expect(result.graph.edges).toHaveLength(1222); }); it("rosters exactly the authored Spec, Pack, and anchor node ids", () => { @@ -265,7 +266,7 @@ describe("the self-hosting corpus", () => { }); it("holds the frozen stated-readiness distribution", () => { - // re-measured under plan 40 + // re-measured under plan 41's fold and the owner's second round expect( primitiveNodes.reduce<Record<string, number>>( (histogram, node) => ({ @@ -274,7 +275,7 @@ describe("the self-hosting corpus", () => { }), {}, ), - ).toEqual({ defined: 58, idea: 4, ready: 153, scoped: 13 }); + ).toEqual({ defined: 58, idea: 6, ready: 176, scoped: 10 }); }); it("derives the Pack membership edges from the manifest, in manifest order", () => { @@ -284,6 +285,11 @@ describe("the self-hosting corpus", () => { .map((edge) => [edge.from, edge.to, edge.claim]), ).toEqual([ ...expectedStudioPackMembers.map((id) => [id, "pack:spec-studio-v1", "declared"]), + ...expectedDesignManagementPackMembers.map((id) => [ + id, + "pack:design-management-v1", + "declared", + ]), ...expectedPackMembers.map((id) => [id, "pack:self-hosting-v1", "declared"]), ]); }); @@ -313,8 +319,8 @@ describe("the self-hosting corpus", () => { }); it("gives every owned impl/api CodeNode exactly one component", () => { - // re-measured under plan 40 - expect(expectedMemberOfEdges).toHaveLength(87); + // re-measured under plan 41's fold and the owner's second round + expect(expectedMemberOfEdges).toHaveLength(95); const exceptions = new Set<string>(structuralMembershipExceptions); const codeUnits = result.graph.nodes.filter( (node) => @@ -338,8 +344,8 @@ describe("the self-hosting corpus", () => { }); it("derives exactly the sparse authored component uses edges", () => { - // re-measured under plan 40 - expect(expectedUsesEdges).toHaveLength(39); + // re-measured under plan 41's fold and the owner's second round + expect(expectedUsesEdges).toHaveLength(40); expect( result.graph.edges .filter((edge) => edge.type === "uses") @@ -473,8 +479,8 @@ describe("the self-hosting corpus", () => { }); it("finds every converted comment anchor at its block's first line", () => { - // re-measured under plan 40 - expect(expectedCommentAnchorIds).toHaveLength(17); + // re-measured under plan 41's fold and the owner's second round + expect(expectedCommentAnchorIds).toHaveLength(25); expect( expectedAnchors .filter((anchor) => anchor.constant === undefined) diff --git a/test/self-hosting-oracle/anchors.ts b/test/self-hosting-oracle/anchors.ts index f1274b8f..e8bb8681 100644 --- a/test/self-hosting-oracle/anchors.ts +++ b/test/self-hosting-oracle/anchors.ts @@ -9,7 +9,7 @@ export interface ExpectedAnchor { readonly id: string; readonly nodeType: "Anchor" | "CodeNode"; - readonly label: string; + readonly label?: string; readonly file: string; readonly constant?: string; readonly site: string; @@ -18,8 +18,56 @@ export interface ExpectedAnchor { readonly context?: string; } -// re-measured under plan 40 +// re-measured under plan 41 export const expectedAnchors: readonly ExpectedAnchor[] = [ + { + id: "test:protocol.projections-model.rendering", + nodeType: "Anchor", + label: "graph-only rendering tests verify pure disposable projection pages", + file: "test/design-review.test.ts", + constant: "projectionsModelTestAnchor", + site: "const exampleRoot =", + }, + { + id: "test:protocol.projections-model.reader", + nodeType: "Anchor", + label: "reader tests verify composable graph joins without persistence", + file: "test/reader.test.ts", + constant: "projectionReaderTestAnchor", + site: "const exampleRoot =", + }, + { + id: "test:protocol.projections-model.publication", + nodeType: "Anchor", + label: "projection publication tests verify regeneration and diagnostic exit codes", + file: "test/cli.test.ts", + constant: "projectionPublicationTestAnchor", + site: "const repoRoot =", + }, + { + id: "test:protocol.regenerability", + nodeType: "Anchor", + label: "delete and rebuild tests verify byte-identical generated artifacts", + file: "test/cli.test.ts", + constant: "regenerationTestAnchor", + site: "const repoRoot =", + }, + { + id: "test:protocol.core-model", + nodeType: "Anchor", + label: "Spec builders and derivation verify independent coordinates and direct realization", + file: "test/builders.test.ts", + constant: "coreModelTestAnchor", + site: 'describe("builders"', + }, + { + id: "test:protocol.claim-taxonomy", + nodeType: "Anchor", + label: "schema and extraction tests verify claim categories and inheritance", + file: "test/graph-schema.test.ts", + constant: "claimTaxonomyTestAnchor", + site: 'describe("graph schema"', + }, { id: "test:protocol.open-section-order", nodeType: "Anchor", @@ -1998,10 +2046,194 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ constant: "packMemberOrderTestAnchor", site: "registerManifestOrderKept(", }, + // The design-management arc (plan 41), transcribed from each anchor's own text. + { + id: "impl:protocol.entry-locations", + nodeType: "CodeNode", + label: "builds the carrier entry location table", + file: "src/extract/derive.ts", + constant: "entryLocationsAnchor", + site: "function deriveEntryLocations", + role: "extractor", + }, + { + id: "test:protocol.entry-locations", + nodeType: "Anchor", + file: "test/entry-locations.test.ts", + constant: "entryLocationsTestAnchor", + site: "function corpus(", + }, + { + id: "impl:protocol.pack-design", + nodeType: "CodeNode", + label: "the Pack design helpers and the Pack context assembly they serve", + file: "src/reader/reader.ts", + site: "function specDesignColumns(", + role: "reader", + }, + { + id: "test:protocol.pack-design", + nodeType: "Anchor", + label: "the Pack context carries each member's design and the Pack's boundary", + file: "test/pack-design.test.ts", + site: "function packDesign(", + }, + { + id: "impl:protocol.pack-design-page", + nodeType: "CodeNode", + label: "renders the Pack page from the Pack design", + file: "src/projections/design-review-pages.ts", + site: "export function renderPackPage", + role: "renderer", + }, + { + id: "impl:protocol.pack-design-sections", + nodeType: "CodeNode", + label: "renders the Pack page's next-rung, boundary, question and code sections", + file: "src/projections/design-review-pack-sections.ts", + site: "export function lineHref", + role: "renderer", + }, + { + id: "test:protocol.pack-design-page", + nodeType: "Anchor", + label: "the Pack page renders the Pack design in its fixed order", + file: "test/design-review-pack-page.test.ts", + site: "function renderPackPage(", + }, + { + id: "impl:protocol.query-params", + nodeType: "CodeNode", + label: "reads --params as JSON data and injects it as the sink's fourth binding, params", + file: "src/cli/q-command.ts", + constant: "queryParamsAnchor", + site: "function readParams", + role: "service", + }, + { + id: "test:protocol.query-params", + nodeType: "Anchor", + label: "front-door checks verify --params as the fourth binding and its refusals", + file: "test/cli-q.test.ts", + constant: "queryParamsTestAnchor", + site: 'describe("sdp q --params', + }, + { + id: "impl:protocol.build-artifacts", + nodeType: "CodeNode", + label: + "the build step that writes each catalog recipe to dist/recipes and records the build commit beside the CLI", + file: "src/cli/build-artifacts.ts", + site: "export function writeBuildArtifacts", + role: "publisher", + }, + { + id: "impl:protocol.recipe-parameters-catalog", + nodeType: "CodeNode", + label: "asserts realization of the catalog recipes that read their parameter from params", + file: "test/recipes.test.ts", + constant: "recipeParametersCatalogAnchor", + site: "function shippedRecipeName", + }, + { + id: "test:protocol.recipe-parameters", + nodeType: "Anchor", + label: "recipe checks verify parameters passed as data and the shipped recipe files", + file: "test/recipes.test.ts", + constant: "recipeParametersTestAnchor", + site: "function shippedRecipeName", + }, + { + id: "impl:protocol.design-recipes", + nodeType: "CodeNode", + label: "asserts realization of the shipped design recipes", + file: "test/recipes.test.ts", + constant: "designRecipesImplementationAnchor", + site: "function probeSpec(", + }, + { + id: "test:protocol.design-recipes", + nodeType: "Anchor", + label: "recipe checks verify the design recipes", + file: "test/recipes.test.ts", + constant: "designRecipesTestAnchor", + site: "function probeSpec(", + }, + { + id: "impl:protocol.entry-location-recipes", + nodeType: "CodeNode", + label: + "asserts realization of the entry lines the entry search and address resolution recipes return", + file: "test/recipes.test.ts", + constant: "entryLocationRecipesAnchor", + site: 'describe("entry lines in entry search and address resolution",', + }, + { + id: "impl:protocol.engine-provenance-cli", + nodeType: "CodeNode", + label: "sdp --version names the package version and the commit the build recorded", + file: "src/cli/sdp.ts", + site: "export function runSdpCli", + role: "service", + }, + { + id: "test:protocol.engine-provenance", + nodeType: "Anchor", + label: "version checks verify the engine provenance line and the build's commit record", + file: "test/cli.test.ts", + constant: "engineProvenanceTestAnchor", + site: 'describe("sdp --version",', + }, + { + id: "impl:protocol.validate-watch", + nodeType: "CodeNode", + label: "re-runs the one validate path on every carrier change and stays alive after findings", + file: "src/cli/validate-watch.ts", + site: "export async function runValidateWatch", + role: "service", + }, + { + id: "test:protocol.validate-watch", + nodeType: "Anchor", + label: "watch-loop checks verify the validate --watch authoring loop", + file: "test/cli.test.ts", + constant: "validateWatchTestAnchor", + site: 'describe("sdp validate --watch",', + }, + { + id: "test:protocol.markdown-authoring", + nodeType: "Anchor", + label: "TypeScript and Markdown twins derive one graph, verifying Markdown authoring", + file: "test/import-round-trip.test.ts", + constant: "markdownAuthoringTestAnchor", + site: "function materializeMarkdownCorpus", + }, + { + id: "test:protocol.relation-builders", + nodeType: "Anchor", + label: "relation builder tests verify the six declared, directed Spec relations", + file: "test/builders.test.ts", + constant: "relationBuildersTestAnchor", + site: 'describe("builders",', + }, + { + id: "test:protocol.pack-builder", + nodeType: "Anchor", + label: "Pack builder tests verify the truth-free review aggregate", + file: "test/builders.test.ts", + constant: "packBuilderTestAnchor", + site: 'describe("builders",', + }, ] as const; -// re-measured under plan 40 +// re-measured under plan 41 export const expectedBindingEdges = [ + ["test:protocol.projections-model.rendering", "verifies", "spec:consumers.projections-model"], + ["test:protocol.projections-model.reader", "verifies", "spec:consumers.projections-model"], + ["test:protocol.projections-model.publication", "verifies", "spec:consumers.projections-model"], + ["test:protocol.regenerability", "verifies", "spec:extraction.regenerability"], + ["test:protocol.core-model", "verifies", "spec:model.core-model"], + ["test:protocol.claim-taxonomy", "verifies", "spec:extraction.claim-taxonomy"], ["test:protocol.open-section-order", "verifies", "spec:extraction.open-section-order"], ["impl:protocol.open-section-order", "satisfies", "spec:extraction.open-section-order"], ["component:protocol.model", "satisfies", "spec:model.core-model"], @@ -2249,6 +2481,7 @@ export const expectedBindingEdges = [ ["impl:protocol.agent-surface", "satisfies", "spec:consumers.agent-surface"], ["impl:protocol.reader-impact", "satisfies", "spec:consumers.reader"], ["impl:protocol.reader", "satisfies", "spec:consumers.reader"], + ["impl:protocol.reader", "satisfies", "spec:extraction.entry-locations"], ["impl:protocol.design-review", "satisfies", "spec:consumers.design-review"], ["impl:protocol.census-page", "satisfies", "spec:consumers.census-page"], ["impl:protocol.mermaid-view", "satisfies", "spec:consumers.mermaid-view"], @@ -2661,9 +2894,44 @@ export const expectedBindingEdges = [ ["impl:protocol.graph-edge-types", "references", "spec:decisions.anchor-binding-grain"], ["impl:protocol.anchor-model", "references", "spec:decisions.architectural-annotation"], ["impl:protocol.census-page", "references", "spec:decisions.architectural-annotation"], + // The design-management arc (plan 41), transcribed from each anchor's own target list. + ["impl:protocol.entry-locations", "satisfies", "spec:extraction.entry-locations"], + ["test:protocol.entry-locations", "verifies", "spec:extraction.entry-locations"], + ["impl:protocol.pack-design", "satisfies", "spec:consumers.pack-design"], + ["impl:protocol.pack-design", "references", "spec:extraction.entry-locations"], + ["impl:protocol.pack-design", "references", "spec:extraction.contract-declarations"], + ["test:protocol.pack-design", "verifies", "spec:consumers.pack-design"], + ["impl:protocol.pack-design-page", "satisfies", "spec:consumers.design-review.pack-design-page"], + ["impl:protocol.pack-design-page", "references", "spec:decisions.pack-design-page"], + [ + "impl:protocol.pack-design-sections", + "references", + "spec:consumers.design-review.pack-design-page", + ], + ["test:protocol.pack-design-page", "verifies", "spec:consumers.design-review.pack-design-page"], + ["impl:protocol.query-params", "satisfies", "spec:consumers.agent-surface.recipe-parameters"], + ["test:protocol.query-params", "verifies", "spec:consumers.agent-surface.recipe-parameters"], + ["impl:protocol.build-artifacts", "satisfies", "spec:consumers.agent-surface.recipe-parameters"], + ["impl:protocol.build-artifacts", "satisfies", "spec:consumers.engine-provenance"], + [ + "impl:protocol.recipe-parameters-catalog", + "satisfies", + "spec:consumers.agent-surface.recipe-parameters", + ], + ["test:protocol.recipe-parameters", "verifies", "spec:consumers.agent-surface.recipe-parameters"], + ["impl:protocol.design-recipes", "satisfies", "spec:consumers.agent-surface.design-recipes"], + ["test:protocol.design-recipes", "verifies", "spec:consumers.agent-surface.design-recipes"], + ["impl:protocol.entry-location-recipes", "satisfies", "spec:extraction.entry-locations"], + ["impl:protocol.engine-provenance-cli", "satisfies", "spec:consumers.engine-provenance"], + ["test:protocol.engine-provenance", "verifies", "spec:consumers.engine-provenance"], + ["impl:protocol.validate-watch", "satisfies", "spec:consumers.validate-watch"], + ["test:protocol.validate-watch", "verifies", "spec:consumers.validate-watch"], + ["test:protocol.markdown-authoring", "verifies", "spec:carrier.markdown-authoring"], + ["test:protocol.relation-builders", "verifies", "spec:model.relations"], + ["test:protocol.pack-builder", "verifies", "spec:model.pack-aggregate"], ] as const; -// re-measured under plan 40 +// re-measured under plan 41 export const expectedCommentAnchorIds = [ "impl:protocol.reader-impact", "component:protocol.reader", @@ -2682,4 +2950,12 @@ export const expectedCommentAnchorIds = [ "impl:protocol.mermaid-view", "impl:protocol.agent-surface-cli", "impl:protocol.anchors-subpath", + "impl:protocol.pack-design", + "test:protocol.pack-design", + "impl:protocol.pack-design-page", + "impl:protocol.pack-design-sections", + "test:protocol.pack-design-page", + "impl:protocol.build-artifacts", + "impl:protocol.engine-provenance-cli", + "impl:protocol.validate-watch", ] as const; diff --git a/test/self-hosting-oracle/carrier.ts b/test/self-hosting-oracle/carrier.ts index a3dc6736..5c1bee5d 100644 --- a/test/self-hosting-oracle/carrier.ts +++ b/test/self-hosting-oracle/carrier.ts @@ -21,7 +21,7 @@ export const carrierSpecs = [ ], }, }, - deliveryFacts: ["implemented"], + deliveryFacts: ["implemented", "has-verifier"], }, { id: "spec:carrier.gherkin-authoring", @@ -846,7 +846,7 @@ export const carrierSpecs = [ id: "spec:carrier.markdown-body-grammar", specKind: "behavior", altitude: "feature", - readiness: "defined", + readiness: "ready", file: "specs/carrier/markdown-body-grammar.sdp.md", title: "The Markdown body accepts one closed form per section", narrative: null, @@ -861,6 +861,7 @@ export const carrierSpecs = [ question: "The Markdown carrier admits one constraint entry per Spec; the TypeScript model and carrier admit several, and the kind-evidence law reads every entry. The owner's ruling makes one entry the law across carriers. Two reviewers contest it: a `constrainedBy` edge points at one Spec and says nothing about how many entries that Spec holds, and the narrowing would force a Spec with two bounds to promote or bundle them. The revision stays unlanded until that cost is weighed.", blocking: false, + key: "constraintEntryCount", }, ], }, @@ -898,7 +899,7 @@ export const carrierSpecs = [ id: "spec:carrier.inline-code-spans", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/carrier/inline-code-spans.sdp.md", title: "Inline code is content, never markup", narrative: null, @@ -913,11 +914,13 @@ export const carrierSpecs = [ question: "CommonMark lets a code span cross a line ending inside a paragraph, so a renderer and the Protocol can disagree about which text is inside a span. Should the owner keep the line-scoped rule or match spans across a paragraph?", blocking: false, + key: "spanAcrossLines", }, { question: "A backslash-escaped backtick still opens a span, and a tag that starts before a backtick pair loses to the span. CommonMark decides both cases the other way. Should the owner keep these readings or follow CommonMark in these two cases?", blocking: false, + key: "commonMarkCases", }, ], }, diff --git a/test/self-hosting-oracle/consumers.ts b/test/self-hosting-oracle/consumers.ts index a5d6bd62..66502526 100644 --- a/test/self-hosting-oracle/consumers.ts +++ b/test/self-hosting-oracle/consumers.ts @@ -110,7 +110,7 @@ export const consumersSpecs = [ id: "spec:consumers.projections-model", specKind: "model", altitude: "feature", - readiness: "defined", + readiness: "ready", file: "specs/consumers/projections-model.sdp.md", title: "Projections fan out from one graph without becoming truth stores", narrative: null, @@ -123,36 +123,28 @@ export const consumersSpecs = [ question: "Does the pure-projection binding-language law stated in src/projections/design-review.ts commentary promote here or to a story-altitude child under comment promotion?", blocking: false, + key: "bindingLanguagePromotion", }, ], }, model: { terms: { - baseline: - "A named approved snapshot whose signed git tag is the approval artifact, with approval remaining outside the authored model.", "curated graph": "The authored architectural read model of declared intent and anchored bindings, valued for editorial sparsity.", curation: - "The deliberate difference between the sparse curated graph and the code-structure surface; it is not drift.", + "The deliberate difference between the sparse curated graph and the exhaustive code-structure surface `spec:consumers.impact-graph` designs; it is not drift.", "diagnostic publication posture": "After extraction succeeds, a projection publishes its honestly labelled graph view even when validation reports errors, and returns the validation exit code so findings remain both visible and nonzero.", discipline: "A lens or projection that filters or groups Specs by kind or section; it is not a phase to pass through.", - "measured curation": - "In a measured comparison, the curated graph selected from single-digit to about one quarter of the mechanical impact-graph surface.", - "impact graph": - "A separately derived code-structure surface for exhaustive usage and blast-radius questions, valued for exhaustiveness and never promoted into architecture.", - "phase / iteration / milestone": - "Descriptive vocabulary for optional roadmap projections, never gates or enforced sequences.", projection: "A pure, disposable, regenerable function of the graph that produces a consumer artifact without becoming a second source of truth.", reader: "The thin typed front door that decodes graph joins and taxonomy once, returns composable data, and persists nothing.", - release: "A tagged set surfaced as a git-tag projection.", }, }, }, - deliveryFacts: ["implemented"], + deliveryFacts: ["implemented", "has-verifier"], }, { id: "spec:consumers.agent-surface", @@ -222,7 +214,7 @@ export const consumersSpecs = [ "A human may use the review context when stating readiness, while validators check only the structural readiness floor and never record or require review approval.", "The MVP view is deterministic generated Markdown with an index and pages for Specs and Packs; richer visual representations remain outside this behavior.", "The page set is a function of the graph alone — it carries no timestamp, no commit, and no run identity — so two renders of the same corpus are byte-identical.", - "Rendering encodes by Markdown syntax context: prose and table fields escape structural characters, each entry of a `design` or `ui` section renders as a list item in authored order with its key as inline code, as ruled by `spec:decisions.authored-entry-order`, fenced JSON preserves a structured value inside one entry through JSON encoding, and inline code uses a delimiter that preserves literal backticks.", + "Rendering encodes by Markdown syntax context: link targets percent-encode each path segment while preserving directory separators and generated line fragments, prose and table fields escape structural characters, each entry of a `design` or `ui` section renders as a list item in authored order with its key as inline code, as ruled by `spec:decisions.authored-entry-order`, fenced JSON preserves a structured value inside one entry through JSON encoding, and inline code uses a delimiter that preserves literal backticks.", "The realizing entrypoint is `renderDesignReview` in `src/projections/design-review.ts`, which reads the reader and returns pages; writing them is the caller's job.", ], exampleSpace: { @@ -245,7 +237,7 @@ export const consumersSpecs = [ specPage: "A Spec page presents descriptors, readiness, relations, bindings, authored sections, and findings in one context.", packPage: - "A Pack page presents framing, model references, and a member table in the manifest's authored order with each member's kind, altitude, readiness, and implementation and verifier bindings.", + "A Pack page presents the Pack's design as its child `spec:consumers.design-review.pack-design-page` states.", indexPage: "The index presents one sortable-style Markdown table for Specs and a linked bullet list for Packs, with stable links into their detail pages.", }, @@ -1039,7 +1031,7 @@ export const consumersSpecs = [ id: "spec:consumers.graph-first-planning", specKind: "behavior", altitude: "feature", - readiness: "idea", + readiness: "scoped", file: "specs/consumers/graph-first-planning.sdp.md", title: "Arc intent is planned from the graph, not a prose briefs index", narrative: null, @@ -1047,13 +1039,6 @@ export const consumersSpecs = [ intent: { outcome: "Forward arc intent is captured as sub-ready Specs so planning sessions derive the backlog and readiness from the graph, with plans reduced to thin lineage pointers.", - openQuestions: [ - { - question: - "How does an arc boundary stay legible in the graph (a Pack, a relation cluster, a naming convention) without minting a workflow gate or an authored delivery fact? Evidence note: the briefs-index register's rows landed in four homes: tradeoff refusals on `decision`-kind Specs, existing behavior guarantees on their carrying Specs, holds as blocking open questions, and a lawful non-decision in the plan record; that split is observed evidence for this question, not a ruling on arc-boundary representation.", - blocking: true, - }, - ], }, behavior: { rules: [ @@ -1061,8 +1046,10 @@ export const consumersSpecs = [ "Planning remains advisory reading of the graph; no recipe, preflight, or plan document authorizes, blocks, or sequences delivery work.", "A work-item dependency is authored only as a `dependsOn` edge between Specs whose truth genuinely needs the other to hold; independence is the absence of the edge, and no scheduling or sequencing phrase is ever authored.", "A decision record is a `decision`-kind Spec joined to its subject by `decidedBy`; a lawful non-decision lives as decision content or a plan record, never as a decision record, and it never mints an authored delivery fact.", - "A do-not-reopen row's home follows its shape: a tradeoff refusal lives on a `decision`-kind Spec, never a `constraint`, and reopens only through a later decision that `supersedes` it and passes the ADR three-part test; an existing behavior guarantee stays on its carrying Spec and reopens by revising that Spec; a hold stays a blocking open question on its Spec; a lawful non-decision stays in the plan record and mints no Spec.", - "A re-entry trigger is the deferred Spec's own blocking open questions, plus a `dependsOn` edge when a true precondition exists; the plan 35 deferrals name three re-entry triggers for this arc: Spec Studio, the reference projection, and the structural-edge Mermaid; no plan document re-arms deferred work.", + "A do-not-reopen row's home follows its shape: a tradeoff refusal lives on a `decision`-kind Spec, never a `constraint`, and reopens only through a later decision that passes the ADR three-part test and either `supersedes` it, replacing it whole, or `refines` it, narrowing it for one part; an existing behavior guarantee stays on its carrying Spec and reopens by revising that Spec; a hold stays a blocking open question on its Spec; a lawful non-decision stays in the plan record and mints no Spec.", + "A decision that replaces another whole keeps the older record and its `supersedes` edge only while the older text explains something a reader still needs; otherwise it carries what survives as its own consequences and the older record is deleted, with git as its history.", + "An arc is a Pack named for the capability it designs, never for a plan number, listing the arc's Specs in reading order; membership confers no readiness, delivery fact or gate, and the Pack outlives the arc as a review aggregate while the plan file names it as a lineage pointer.", + "A re-entry trigger is the deferred Spec's own blocking open questions, plus a `dependsOn` edge when a true precondition exists; the plan 35 deferrals named four re-entry triggers: Spec Studio, the reference projection, the context bundle and the structural-edge Mermaid, and each deferred Spec now carries its trigger as its own blocking question, except the context bundle, whose trigger the first adopter's design passes have met; no plan document re-arms deferred work.", "A deliverable with exclusive ownership across consumers has exactly one Spec identity; every consumer `dependsOn` that identity instead of restating the deliverable.", "Selection-pressure heuristics stay advisory, carried as behavior rules here or as recipes; they authorize, block, and sequence nothing.", "Session law re-measures from the graph first; a session never inherits readiness, backlog, or placement state from a prior plan, register, or summary.", @@ -1090,6 +1077,7 @@ export const consumersSpecs = [ question: "Which language-neutral identity and extraction boundary can support exhaustive symbol reach without freezing a single compiler's representation into the Protocol?", blocking: true, + key: "symbolIdentity", }, ], }, @@ -1129,7 +1117,7 @@ export const consumersSpecs = [ id: "spec:consumers.adopter-on-ramp", specKind: "behavior", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/consumers/adopter-on-ramp.sdp.md", title: "An adopter learns the ruled graph homes from the shipped on-ramps", narrative: null, @@ -1159,7 +1147,7 @@ export const consumersSpecs = [ id: "spec:consumers.agent-surface.register-recipes", specKind: "behavior", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/consumers/agent-surface.register-recipes.sdp.md", title: "Register questions stay executable graph recipes", narrative: null, @@ -1191,7 +1179,7 @@ export const consumersSpecs = [ id: "spec:consumers.shipped-protocol-corpus", specKind: "behavior", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/consumers/shipped-protocol-corpus.sdp.md", title: "The package ships the Specs its skills cite", narrative: null, @@ -1218,7 +1206,7 @@ export const consumersSpecs = [ id: "spec:consumers.agent-surface.address-and-cycle-recipes", specKind: "behavior", altitude: "story", - readiness: "defined", + readiness: "ready", title: "Address checks and dependency cycles stay executable graph recipes", narrative: null, sections: { @@ -1345,27 +1333,13 @@ export const consumersSpecs = [ id: "spec:consumers.spec-studio.intent-panel", specKind: "behavior", altitude: "story", - readiness: "scoped", + readiness: "defined", title: "A composing panel on every page gathers scoped intent for an agent", narrative: null, sections: { intent: { outcome: "Let a reader gather the changes they want while they read, each bounded by a Spec, its neighbors, a Pack or one open question, and hand them to an agent that edits source.", - openQuestions: [ - { - question: - "How does a static page hand composed intent to an agent: as a file the reader saves, as text the reader copies, or as a pull request? The original exports patch JSON or opens a pull request, and `spec:consumers.intent-composition` names no entrypoint for the composing surface.", - blocking: true, - key: "handOff", - }, - { - question: - "The original stages each change as a JSON patch, checks its schema in the browser and its effect on the graph through the CLI or an agent, and applies it to canonical source; `spec:consumers.edit-model` has a view compose scoped intent that an agent turns into an ordinary source edit, with no patch loop. Does any part of the patch loop return, or is scoped intent the panel's only output?", - blocking: false, - key: "patchLoop", - }, - ], }, behavior: { rules: [ @@ -1374,6 +1348,8 @@ export const consumersSpecs = [ "A reader gathers intents across pages before handing them off.", "Gathered intents persist in the browser's local storage, so refreshing a tab on a phone loses none.", "A composed intent names its scope by id or entry address, so the agent that receives it reads the same Spec, Pack, entry or question through the graph.", + "The panel hands its gathered intents off as one change proposal, on the change-proposal contract, which the reader copies as text or saves as a document. The page opens no pull request, because a static page holds no credentials; any pull request comes from the agent's ordinary source edit.", + "No part of the original's patch loop returns to the page: it stages no patch, applies nothing to source or to a copy of the graph, and the proposal is the reviewable half of that loop, applied as an ordinary edit.", ], exampleSpace: { given: ["the composing panel holds {gathered:number} gathered intents"], @@ -1384,7 +1360,7 @@ export const consumersSpecs = [ ui: { gatheredIntents: "the intents gathered so far, headed with their count, each on one line naming its scope and the change it asks for, such as adding an example or changing a constraint's target.", - panelActions: "discard, hand off, and open as a pull request.", + panelActions: "discard, copy the proposal as text, and save the proposal as a document.", composingActions: "the actions on other panels that compose intent: resolve an open question or promote it to a decision record, propose or edit an example, propose or adjust a constraint target, and propose examples for missing combinations.", }, @@ -1412,7 +1388,7 @@ export const consumersSpecs = [ }, { question: - "The original's architecture lens draws runtime layers, ports and external systems beside components and their dependencies; `spec:decisions.architectural-significance-rides-primitives` admits no structural vocabulary beyond component membership and `uses`. Does the lens draw only what those edges hold?", + "The original's architecture lens draws runtime layers, ports and external systems beside components and their dependencies; `spec:decisions.architectural-annotation` gives a component anchor a layer and a bounded context and any code anchor a role, beside component membership and `uses`, and admits no port or external-system node. Does the lens group components by layer and context and draw only what those anchors hold?", blocking: false, key: "architectureVocabulary", }, @@ -1496,7 +1472,7 @@ export const consumersSpecs = [ id: "spec:consumers.spec-studio", specKind: "behavior", altitude: "feature", - readiness: "scoped", + readiness: "defined", title: "The Spec Studio is a static HTML workbench over the one graph", narrative: null, sections: { @@ -1505,23 +1481,12 @@ export const consumersSpecs = [ "A stakeholder who reviews a design, on a desktop or on a phone, and reads far more than they change.", outcome: "Give a person reviewing a design one generated HTML workbench that explores the whole graph through lenses, shows each Spec and Pack in context, and turns a wanted change into scoped intent rather than an edit.", - openQuestions: [ - { - question: - "Which package carries the Studio: the open-source `@libar-dev/software-delivery-protocol`, or a commercial `@libar-ai/` package? The Studio's deferral re-enters on a recorded ruling that names this home; earlier planning placed the Studio under `@libar-ai/`, and no Spec rules it.", - blocking: true, - key: "packageHome", - }, - { - question: - "Which reader does the Studio serve that the generated Markdown Design Review does not? The first adopter generates its own Pack page beside the Design Review, with the members in reading order, the clause that holds each below its next rung, the Specs outside the Pack they rest on, and their open questions with register rows, while the Design Review's Pack page lists the members and the verifier gaps.", - blocking: true, - key: "unservedReader", - }, - ], }, behavior: { rules: [ + "The Studio serves a person working in the repository who designs a capability ahead of its code and iterates on it in passes, and a reviewer of a pull request who holds only a link. The Markdown Design Review serves neither well: its pages have no search, no filters and no navigation beyond links, and its readers in the first adopter open the carrier files instead.", + "The Studio's first build is accepted when a reader completes, from the Studio's page set and the files it links to, the five reader tasks the first adopter set over its history-view Pack (`libar-platform/docs/feedback/sdp-feedback-01.md`): find the history contract, locate OD-157, explain the aggregate dependency, find one declaration's type comparison, and say whether a paused-rebuild example has native-backend evidence. The first build reads no run evidence, so the last task passes when the reader finds that the example requires the native backend tier and has a bound verifier, and reports that no run result is supplied.", + "The read-only Studio ships in the open-source package `@libar-dev/software-delivery-protocol`, beside the four shipped projections; hosted review, shared comments and hosted previews may later ship under `@libar-ai/`.", "The Studio is a projection of the one graph: its page set under `generated/spec-studio/` is regenerated from the graph, states no truth, and confers nothing back into the graph.", "The Studio is a mostly static single-page application, buildable as an `index.html` page and its assets, and it needs no server.", "The Studio works offline: the same files open from the file system through `file://` for local use and from any static host for sharing.", @@ -1550,32 +1515,25 @@ export const consumersSpecs = [ id: "spec:consumers.spec-studio.sharing", specKind: "behavior", altitude: "story", - readiness: "scoped", + readiness: "defined", title: "A Studio build is stamped and shared from a static host", narrative: null, sections: { intent: { outcome: "Let a stakeholder open the Studio of a pull request from a link, and let its author know which graph and which build the stakeholder read.", - openQuestions: [ - { - question: - "The original stamps the Studio's data with the git commit and the build time; the Studio renders byte-identical files from one graph, and `spec:extraction.determinism` keeps wall-clock timestamps and run-specific values out of generated output. Where does the stamp live, if anywhere?", - blocking: true, - key: "buildStamp", - }, - ], }, behavior: { rules: [ "A build is shared by uploading its page set to a static host, such as S3, Vercel or GitHub Pages.", "Each pull request can publish its Studio as a preview.", "A stakeholder opens the link, navigates the Studio and composes intent, and the pull request's author hands that intent to an agent that edits the source.", + "The Studio's build writes no stamp, so every page and data file it writes stays a function of the graph alone. The publishing step that uploads a build writes one stamp file beside the page set, and the shell shows it as build context when it is present.", ], }, design: { - dataStamp: - "each data file carries the graph's `schemaVersion`, the git commit SHA it was built from or `unknown` for a local build, and the build timestamp.", + buildStamp: + "`stamp.json` beside the page set, written by the publishing step and never by the Studio's build, holding the graph's `schemaVersion`, the git commit the build came from or `unknown` for a local build, the engine `sdp --version` names, and the build time.", previewPath: "a pull request's preview is served at `<host>/preview/<pr-number>/`.", }, }, @@ -1667,7 +1625,7 @@ export const consumersSpecs = [ }, { question: - "The original's runtime panel binds a Fastify route to the Effect program it invokes, as `api:POST:/orders` invokes an Effect program with `R = CreateOrderUseCase`; shows the Effect layer `layer:CreateOrderUseCaseLive`, which provides `port:CreateOrderUseCase`, requires `port:OrderRepository` and `port:EventBus`, has the lifetime `scoped` and the test layer `layer:CreateOrderUseCaseTest`; names the external system `external:postgres`; and, for Awilix, binds the registration `createOrderUseCase` to `impl:CreateOrderUseCase` as `asClass CreateOrderUseCase` with the lifetime `SCOPED`, depending on `impl:OrderRepository` and `impl:EventBus`. An anchor carries identity only, and `component` and `uses` are its only structural fields. Does the panel show more than the structural neighborhood of the Spec's implementation anchors?", + "The original's runtime panel binds a Fastify route to the Effect program it invokes, as `api:POST:/orders` invokes an Effect program with `R = CreateOrderUseCase`; shows the Effect layer `layer:CreateOrderUseCaseLive`, which provides `port:CreateOrderUseCase`, requires `port:OrderRepository` and `port:EventBus`, has the lifetime `scoped` and the test layer `layer:CreateOrderUseCaseTest`; names the external system `external:postgres`; and, for Awilix, binds the registration `createOrderUseCase` to `impl:CreateOrderUseCase` as `asClass CreateOrderUseCase` with the lifetime `SCOPED`, depending on `impl:OrderRepository` and `impl:EventBus`. An anchor carries identity, its Spec targets and optional structure: `component`, `uses` and `role`, with an architectural `layer` and a `context` on a component anchor; none of them records what a program provides or requires, a lifetime or an external system. Does the panel show more than the structural neighborhood of the Spec's implementation anchors?", blocking: false, key: "runtimeComposition", }, @@ -1778,4 +1736,482 @@ export const consumersSpecs = [ deliveryFacts: [], file: "specs/consumers/spec-studio.verification-panels.sdp.md", }, + // The design-management arc (plan 41): the built Specs, then the captures. + { + id: "spec:consumers.pack-design", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + file: "specs/consumers/pack-design.sdp.md", + title: "The Pack context carries the Pack's design", + narrative: + "A capability designed ahead of its code is read as a Pack: its members in reading order, how far each has matured, what design each carries, what it rests on, and which code answers to it. The reader already holds every one of those facts. This Spec assembles them once on the Pack context so a page, a recipe and an adopter's script read one answer.", + sections: { + intent: { + problem: + "Reading a Pack's design means joining member readiness, the floor one rung above the stated rung, design entries, decisions, open questions, code bindings with their components, and the Specs outside the Pack, and every consumer that joins them by hand reads the floor or the boundary a different way.", + outcome: + "Give each Pack member its design columns and give the Pack its boundary on the existing Pack context, so the Design Review, a recipe and an adopter's page read the design from one assembly.", + }, + behavior: { + rules: [ + "The Spec context keeps every field it has and adds `designColumns`, derived counts and lists computed by one helper. The Pack context reads the same columns as each resolved member's `design` and adds the boundary to the Pack. It is an extension of the existing per-Pack context, not a new reader method.", + "A member's stated next rung is the rung above its stated rung, none when it states `ready`. Its unmet clauses come from the one floor evaluator with that rung as the target, each failure with its targets when it has them. The list is empty when the floor already holds the stated next rung, which the page reads as `nothing blocks`: a rung that waits for its author, never a promotion. The floor reached and the context's existing next-rung failures, which read the rung above the floor, stay as they are, so the two readings never share a name.", + "A member's design columns count its keyed `design` and `ui` entries, other than `description`; its pinned declarations, the `design` entries whose value opens with a code span under the rule of the contract-declarations Spec; its open questions and how many of them block, listing each question with its text, its blocking flag, its key when it carries one, and its source line from the location table; and they list the decisions it names through `decidedBy`, each with its stated rung, or unresolved when the target is no Spec. The columns are counts and lists read from the graph, never a score or a rung.", + "A member lists the code units that satisfy it and the code units that reference it, each as a code unit binding. A code unit binding carries its component when the unit has a `memberOf` edge: the component's id with its layer and context when the component anchor states them. The component rides the binding on the Spec context as well, as an addition.", + "A member counts its verifiers and its enabled verifiers, and the examples that declare `verifies` on it with how many of them are enabled, by the reader's existing verifier decoding.", + "The Pack's boundary lists the Specs outside the Pack joined to a member by an authored relation. A Spec a member relates to by `refines`, `dependsOn`, `constrainedBy` or `decidedBy` is one the Pack rests on; a Spec that relates to a member by the same four relations is one that rests on the Pack. The boundary lists inbound `verifies` separately as `verifiedBy` and inbound `supersedes` as `supersededBy`, with the same row shape. Each row carries the outside Spec's id, title, stated rung and whether it carries `implemented`, and for each relation type the members it joins, the types in the closed relation list's order and the members in authored order. A relation whose target is not a Spec in the graph is left to referential integrity and does not appear.", + "Members keep the manifest's authored order, an unresolved member keeps its existing row with no design columns, and boundary rows sort by id in code-unit order.", + "The assembly is a pure function of the graph: it reads no carrier file, records nothing, and confers no delivery fact or readiness.", + "The realizing site is `packContext` in `src/reader/reader.ts`.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:consumers.design-review.pack-design-page", + specKind: "behavior", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-review.pack-design-page.sdp.md", + title: "A Pack page shows where the Pack's design stands", + narrative: null, + sections: { + intent: { + outcome: + "Render a Pack's design assembly as one deterministic Markdown page a reviewer reads before a design pass, between iterations and before stating readiness.", + }, + behavior: { + rules: [ + "The page keeps its title line, framing and vocabulary references, then renders, in this order: the member table, what holds each member below its next rung, the Pack's boundary, the open questions, the code, the verifier coverage gaps, and the findings.", + "The member table has one row per member in authored order: the linked Spec with its position from 1 and its title, kind, altitude, stated rung, floor reached, the stated next rung with `nothing blocks` when its unmet clauses are empty and the first unmet clause otherwise, the design columns as counts (entries, declarations, questions with the blocking count), the decisions it names, and the implementation and verifier binding columns with the present and none values the binding-language rule fixes for every aggregate table, beside a design-reference column that reads present when a code unit references the member and none otherwise. Unit counts stay off the table, which offers existence and never a degree. An unresolved member keeps a row that says so, and the later sections skip it, because the graph holds none of its data.", + "The next-rung section lists each member whose stated next rung has unmet clauses, every clause with the targets it names and their stated rungs. A member whose floor holds its next rung is listed apart as waiting for its author's statement, which the page never presents as approval.", + "The boundary section lists, in order, Rests on, Rested on by, Verified by and Superseded by, from `restsOn`, `restedOnBy`, `verifiedBy` and `supersededBy`. Each empty list reads `None.`; otherwise a table gives each row with the linked Spec, its stated rung, whether it carries `implemented`, and the members it joins under each relation type.", + "The open-questions section groups questions by member in authored order. Each question shows its blocking flag, its entry address when it carries a key, its text, and a link to its line in the carrier file when the location table records one.", + "The code section lists, per member, each realizing and each referencing unit with its id, a link to its file and line, its role, and its component with that component's layer and context, under the binding-language headings the Design Review already uses. A member with no unit says so in one line.", + "A line link is a relative link from the page to the carrier or source file with a `#L<line>` fragment. Every value is escaped by the Design Review's encoding rules, and the page carries no timestamp, commit or run identity, so two renders of one graph are byte-identical.", + "The realizing site is the Pack page renderer under `src/projections/`.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:consumers.agent-surface.recipe-parameters", + specKind: "behavior", + altitude: "story", + readiness: "ready", + file: "specs/consumers/agent-surface.recipe-parameters.sdp.md", + title: "Recipes take parameters as data and ship as runnable files", + narrative: null, + sections: { + intent: { + problem: + "A parameterized recipe takes its input on its opening line, so a caller rewrites the body's source to pass 489 addresses, recipe 4 alone reads an environment variable, and every script that runs a recipe cuts its body out of the Markdown catalog first.", + outcome: + "Pass a recipe's parameter as data through the one evaluation sink, and ship every catalog body as a file a caller runs as written.", + }, + behavior: { + rules: [ + "`sdp q` takes `--params` with a JSON object, or `--params @PATH` with a file holding one, the path resolved from the working directory, and injects it as a fourth binding, `params`, beside `g`, `graph` and `report`. Without the flag `params` is an empty object. A value that is not valid JSON, JSON that is not an object (null, an array, a string, a number or a boolean), or a file that cannot be read, is refused before the body runs, with exit 1, one line on stderr and no output on stdout.", + "The three existing bindings keep their names and meaning. The sink still evaluates one operator-supplied body and adds no query vocabulary: a parameter is input to a body, never a verb.", + "Every recipe that takes a parameter reads it from `params` under the name the catalog states and falls back to the catalog's sample when the name is absent, so each body still runs as written. Recipe 4 reads its changed files from `params.files`, and the environment variable it used before is retired.", + "The build writes each catalog body to `dist/recipes/<NN>-<slug>.js`, where `NN` is the two-digit recipe number and the slug is the recipe's heading in lower kebab case, and the package ships that directory. The catalog stays the one owner of the bodies; the files are derived, and the recipe test checks that each file equals its catalog body byte for byte.", + "The realizing sites are `src/cli/q-command.ts` for the flag and binding, and the build step that writes `dist/recipes/`.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:consumers.agent-surface.design-recipes", + specKind: "behavior", + altitude: "story", + readiness: "ready", + file: "specs/consumers/agent-surface.design-recipes.sdp.md", + title: "Design work reads its answers from catalog recipes", + narrative: null, + sections: { + intent: { + problem: + "Iterating on a design asks questions no recipe answers in one body: where a Pack's design stands, what a change to a Spec asks to follow, which decisions shape what, and which component dependencies cross a context or a layer. The first adopter answers each with its own script.", + outcome: + "Answer each of those questions with a catalog recipe that runs as written, so design work reads them from the graph.", + openQuestions: [ + { + question: + "Example-space coverage, the witnesses and coverage gaps of each literal slot (v0 04 §4), needs the slot notation parsed, and a recipe body that parses it would be a second parser; `spec:consumers.agent-surface.example-space-coverage` holds whether the reader decodes example spaces and bound points instead.", + blocking: false, + key: "exampleSpaceCoverage", + }, + ], + }, + behavior: { + rules: [ + "The build backlog (recipe 1) keeps its existing fields and adds `designColumns` for each listed Spec, read from its Spec context: counts of keyed Design and UI entries, pinned declarations, open and blocking questions, and the decisions it names. These columns are derived, never a score, rung or approval.", + "Pack design (recipe 29) takes a Pack id in `params.pack` and returns the Pack context's design assembly as data: the members in authored order with their stated next rung and its unmet clauses, their design columns, bindings and verifiers, and the Pack's boundary. It returns `found: false` for an id that is no Pack.", + "Design-change impact (recipe 30) takes Spec ids in `params.specs` and returns every Spec that rests on them, transitively, through inbound `refines`, `dependsOn`, `constrainedBy` or `decidedBy` edges, each with the relation path that reaches it and its distance. For the changed Specs and every dependent it lists the units that satisfy it, the units that reference it, its enabled verifiers and its Packs. A `references` edge counts as a unit to notify and confers nothing.", + "Decision register (recipe 31) takes no parameter and returns one row per decision Spec, sorted by id: its title, stated rung, the decisions it supersedes and the decisions that supersede it, the decision-kind Specs it `refines` as `narrows` and those that `refines` it as `narrowedBy`, the Specs that name it through `decidedBy`, its keyed open questions, and its Packs. It reads the graph alone; the ratified names stay in the decision registry document.", + "Architecture crossings (recipe 32) takes no parameter and returns every `uses` edge whose two ends, each resolved to its own component directly or through `memberOf`, have components that both state a context and differ in it, or both state a layer and differ in it. Each row names both units, both components and what differs, and the totals tally the crossings by context pair and by layer pair, `from -> to`, so a corpus reads the direction of its dependencies at a glance. It reports and never refuses: a corpus gates its own architecture rule, as the architectural annotation decision leaves it.", + "Each recipe body lives in the catalog and runs as written in the recipe test, and each reads its parameter as the recipe-parameters Spec states.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:consumers.engine-provenance", + specKind: "behavior", + altitude: "story", + readiness: "ready", + file: "specs/consumers/engine-provenance.sdp.md", + title: "The CLI names the engine that produced a result", + narrative: null, + sections: { + intent: { + problem: + "An adopter pinned to a build of the Protocol cannot ask the CLI which build it is: `sdp --version` prints the usage text and the package version is `0.0.0` at every commit, so when a check changes with no Spec changed, nothing tells the engine moving from the corpus moving.", + outcome: + "Let `sdp --version` name the package version and the commit the build came from, without putting either into the graph.", + }, + behavior: { + rules: [ + "`sdp --version` prints one line, `sdp <version> (<commit>)`, and exits 0. The version is the package's own version; the commit is the full hash the build was made from, or `unknown` when the build could not read one.", + "The build records the commit in a file beside the compiled CLI, and the CLI reads that file; it never runs git at run time.", + "Neither the version nor the commit enters the graph, a projection or a generated contract, so a derived artifact stays a function of the corpus alone.", + "The realizing sites are `src/cli/sdp.ts` and the build step that records the commit.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:consumers.validate-watch", + specKind: "behavior", + altitude: "story", + readiness: "ready", + file: "specs/consumers/validate-watch.sdp.md", + title: "Validation re-runs while an author edits carriers", + narrative: null, + sections: { + intent: { + problem: + "An author editing a Spec re-runs `sdp validate` by hand after every save, and the watch loop that removes that step shipped in plan 35 with no Spec stating what it promises.", + outcome: + "Let `sdp validate --watch` re-run the one validation path on every carrier change and stay alive after findings, as a loop for authoring and never a second validation path.", + }, + behavior: { + rules: [ + "`sdp validate --watch [root]` installs its watcher first, then runs the same validate path `sdp validate` runs, from scratch, once at start and again after every carrier create, change, delete or rename under the root.", + "A carrier is a file ending in `.sdp.md`, `.sdp.gherkin` or `.sdp.ts`. The watcher ignores `generated`, `dist`, `node_modules`, `coverage`, dot-directories, the configured `--exclude` prefixes and every non-carrier path; a removed watched directory and an event with no filename each schedule a run, because the carriers they stand for cannot be named.", + "Events that arrive while a run is in progress coalesce into one pending run.", + "Findings print as `sdp validate` prints them and the process stays alive; an operator stop exits 0.", + "`--watch` is a validate option only and cannot combine with `--check-clean`. It re-runs on carrier edits and never on source edits, so an author re-runs validation after editing an anchor.", + "The realizing site is `runValidateWatch` in `src/cli/validate-watch.ts`.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:consumers.agent-surface.example-space-coverage", + specKind: "behavior", + altitude: "story", + readiness: "defined", + file: "specs/consumers/agent-surface.example-space-coverage.sdp.md", + title: "Example spaces report which values their examples cover", + narrative: + "The original design rendered a coverage heatmap from each example space: which combinations of its typed slots an example witnesses and which none does (`docs/lineage/v0-design/04-authoring-surfaces.md` §4 and §4.1, `06-extraction-and-validation.md` §8). The Protocol ratified the terms witness and coverage gap and computes neither, so a designer cannot see, before code exists, which examples a space still lacks.", + sections: { + intent: { + problem: + "A parent's example space declares literal-union slots and its example children bind one point each, but no recipe or reader value says which literal values the children witness and which no child does.", + outcome: + "Report, for every example space, each literal slot value with the children that witness it and the values no child witnesses, so a design pass sees its missing examples before code exists.", + openQuestions: [ + { + question: + "Is coverage per slot value enough, or does a design pass need the combinations of two or more slots, as the original heatmap drew them?", + blocking: false, + key: "combinationCoverage", + }, + ], + }, + behavior: { + rules: [ + "The engine decodes each Spec's example space into its typed slots, and each example child's steps into its bound point, once, through the notation module that writes the space contracts, and the reader carries both on the Spec context; no recipe or projection parses slot notation a second time.", + "Coverage is a catalog recipe over the decoded values: for each slot typed as a union of literals, each value with the example children whose bound point witnesses it, and the values no child witnesses.", + "A slot that is not a union of literals has no finite set of values, and coverage reports it as not enumerable rather than as covered or as a coverage gap.", + "Coverage reports and confers nothing: it moves no readiness, records no verdict, and a coverage gap is what a design pass reads, never a validation finding.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:consumers.review-perspectives", + specKind: "behavior", + altitude: "feature", + readiness: "defined", + file: "specs/consumers/review-perspectives.sdp.md", + title: "A Pack is read from a named perspective", + narrative: + "The original design gave every reader one reading surface, each reading from their own concern: the analyst, the domain engineer, the architect, the tester, the designer and the agent (`docs/lineage/v0-design/04-authoring-surfaces.md` §6). The first adopter iterates each capability's design through four advisor lenses, its runtime platform, the domain, operations and the product, each a written list of the questions its field asks, run over the same Pack by the same pass brief (`libar-platform/design/advisors/lenses/`, `libar-platform/design/advisors/task-pass.md`). It asks for a lens to be a filter over the same capability that reorders and emphasizes questions, entries, examples and constraints and opens the same Specs, with no new Spec kind and no compulsory sequence of stages (`libar-platform/docs/feedback/sdp-feedback-02.md`, item 28).\n\nThe owner ruled the word: perspective. The Spec Studio keeps lens for its graph views, its Packs, architecture, tests and evidence views, and discipline stays the filter by kind or section. The adopter's advisor lens is a reader's concern, not a view of the graph, so in the Protocol's terms it is a perspective.", + sections: { + intent: { + actor: + "A reviewer or an agent who reads a Pack for one concern, such as the domain model, operations or the runtime platform.", + problem: + "Every reading of a Pack shows every member in the same order with the same emphasis, so a reader with one concern sorts it by hand, and an adopter keeps its perspectives as prose briefs the graph cannot apply.", + outcome: + "Let a reviewer read the same Pack from one perspective at a time, filtered and ordered for that perspective, without a new kind or a stage.", + }, + behavior: { + rules: [ + "A perspective filters and orders a Pack's members and their content and never changes them; every row opens the same Spec, and the whole Pack stays one step away.", + "A perspective adds no kind, readiness, stage or sequence; reading a Pack from one perspective records nothing, and no perspective is required before another or before `ready` is stated.", + "The corpus owns its perspectives and their questions; the Protocol supplies the filter and ships no list of perspectives.", + "A perspective is corpus data: a JSON object the corpus keeps beside its briefs and passes to the perspective filter, a catalog recipe, through `--params`. The reader loads no perspective file, and no validator lists or checks perspectives, so no registry forms.", + "A perspective selects members by Spec id or id prefix, kind, section presence, Design key with an optional value, question key, and authored relation type with a stated direction; it selects component layer or context only through the member's code unit bindings, satisfying or referencing, that the perspective names; it infers nothing from prose, paths or imports.", + "A member selected by a Design key, including the `layer` and `context` keys a component design states, is selected on declared intent; a member selected through a binding's component is selected on anchored structure; each row names which, and the two are never merged into one architecture answer.", + "Values within one selector are alternatives and different selectors combine as requirements; an unsupported selector is a diagnostic in the result, and a valid selector that matches nothing is an explicit empty result.", + "Members keep the manifest's authored order unless the perspective gives an explicit priority order, authored order breaking ties; every selected member's blocking questions stay visible whatever else the perspective filters out.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:consumers.context-bundle", + specKind: "behavior", + altitude: "feature", + readiness: "defined", + file: "specs/consumers/context-bundle.sdp.md", + title: "A context bundle is composed per invocation within a token budget", + narrative: + "The original design cut the graph into slices for language models ahead of time. It wrote one slice per Pack, per capability, around a changed Spec, per readiness level and for failing tests, each under 32k tokens with its nodes, edges, source files, findings, open questions and a summary, and a prompt bundle for one Spec (`docs/lineage/v0-design/07-spec-studio-and-projections.md` §5.5, §13.2). The ratified vocabulary keeps the term and defines a context bundle as a token-budgeted curated slice pushed to an agent.\n\nPlan 35 deferred the bundle until evidence showed agent sessions still hand-assembling the same token-budgeted slice after the catalog recipes (plan 35, \"H leftover projections\"). The first adopter's evidence meets that trigger. Every design pass there starts from the same hand-written brief, which tells the agent to read the Pack's page, run recipes 5, 20, 21 and 24 over the Pack and its members, read the member Specs where the page falls short, and end the memo with what the page did not give (`libar-platform/design/advisors/task-pass.md`). Four advisor lenses run that brief over each Pack, and the adopter asks for the reading material to be generated from a Pack and a lens (`libar-platform/docs/feedback/sdp-feedback-02.md`, item 29). The agent front door refuses a committed artifact that answers in the graph's name, so a bundle is composed on each invocation and never saved as slices. It builds on the Pack design recipe, recipe 29.", + sections: { + intent: { + actor: + "An agent starting one design pass over a Pack, a Spec's neighborhood or a set of open questions.", + problem: + "Each pass assembles the same reading material by running the same recipes and joining their answers by hand, and nothing bounds what it hands the model.", + outcome: + "Give an agent the reading material for one design pass in one composed, token-budgeted slice of the graph, so no session hand-assembles it.", + openQuestions: [ + { + question: + "Which scopes does a bundle take beyond a Pack: a Spec with its neighborhood, the Specs a changeset touches, the open questions of a Pack?", + blocking: false, + key: "bundleScopes", + }, + { + question: + "Does a bundle take a review perspective as a filter, so one call yields the brief for one lens?", + blocking: false, + key: "perspectiveFilter", + }, + ], + }, + behavior: { + rules: [ + "A bundle is a catalog recipe that takes its scope and its budget as recipe parameters; it adds no entrance and no query verb.", + "A bundle is composed from the graph on each invocation by the reader every recipe uses; nothing is precomputed, saved or committed, and two invocations over the same graph with the same parameters compose the same bundle.", + "A bundle for a Pack carries the Pack design assembly, its members in authored order with their design columns and its boundary, then each member's open questions with their addresses and lines, its dependency footing, its pinned declarations and its examples.", + "The budget is a hard limit on a successful bundle, counted in UTF-16 code units, the `length` a recipe body reads, of the compact `JSON.stringify` of the value the recipe returns, its scope, budget and left-out statement included; `sdp q --json` prints a longer indented form, and the budget never measures printed text. The bundle also reports a token estimate labelled as an estimate, so no model's tokenizer binds the Protocol.", + "When the scope exceeds the budget, the bundle cuts in a fixed order: boundary rows down to their ids first, then member narrative, then example step text, then non-blocking questions. It never cuts member ids, stated and next rungs, blocking questions, pinned declarations or entry addresses.", + "When the protected content alone exceeds the budget after every permitted cut, the recipe returns a budget-exceeded result as data and no partial bundle: the requested scope, the budget, and the minimum count the protected content needs in the same unit; `sdp q` exits 0, because the result is the body's return and not a refusal.", + "A bundle gives the file and line of an entry or a binding in place of a source excerpt, because the reader never reads source.", + "A bundle states its scope, its budget and what it left out to stay within the budget, so a reader knows what to read in the Specs themselves.", + "A bundle carries facts from the graph and no instructions of its own; the brief that tells an agent what to look for stays the corpus's own text.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:consumers.design-diff", + specKind: "behavior", + altitude: "feature", + readiness: "defined", + file: "specs/consumers/design-diff.sdp.md", + title: "Designs are compared between two commits at entry grain", + narrative: + "The original design's CLI compared two graph snapshots with `graph diff <ref1> <ref2>` (`docs/lineage/v0-design/08-delivery-evidence-and-tooling.md` §6.1). Today one rule of the impact graph's Spec states that comparison. It derives the graph at each commit and reports the nodes and edges added, removed or changed. That Spec is held by a blocking question on language-neutral symbol identity, which concerns exhaustive code structure and not the curated graph a design lives in, so the comparison a design review needs waits on a question it never asks.\n\nIterating on a design asks what changed since the last reading. The first adopter asks for a comparison by Spec id, section and entry key that tells changed declaration text from changed prose, moved entries, readiness edits, relation edits and new or removed examples, and for what changed since a design pass, read against the commit that pass read (`libar-platform/docs/feedback/sdp-feedback-02.md`, items 30 and 31). Git shows the changed lines; this comparison shows the changed design.", + sections: { + intent: { + actor: + "A reviewer or an agent starting the next design pass over a Pack, holding the commit the last pass read.", + problem: + "A line diff of the carriers shows edits but not which Specs, entries, questions and relations changed, so each pass re-reads the whole Pack to find what moved.", + outcome: + "Show what changed in a design between two commits, down to the Spec, the section and the keyed entry, so a reviewer reads what moved since the last pass.", + openQuestions: [ + { + question: + "Should the comparing rule leave the impact graph's Spec once this Spec is defined, so the impact graph keeps exhaustive code structure only?", + blocking: false, + key: "comparingRuleHome", + }, + { + question: + "A design pass is review context, not a graph fact. Is the base commit always one the caller names, or may a corpus keep the commit each pass read in its own records for the comparison to read?", + blocking: false, + key: "passBase", + }, + ], + }, + behavior: { + rules: [ + "A comparison is a catalog recipe that takes the base graph as a recipe parameter, the graph `sdp build` writes at the base commit, and compares it with the graph the evaluation derives at the head; it adds no verb and no binding.", + "The base graph is input the caller holds, derived from the repository alone at the base commit; it never answers in the graph's name, and the comparison writes nothing.", + "It reports the Specs added and removed, and for a Spec present at both commits the changes to its title, kind, altitude, stated readiness, relations, narrative and each section.", + "Inside the Design and UI sections, the open questions and any other section whose entries carry keys, it compares keyed entries by key and reports each as added, removed, changed in value or moved in order.", + "An unkeyed entry has no identity across two commits, neither its position nor its text, so it is compared as part of its whole section.", + "It reports changes to Pack membership and order, and to the code units, verifiers and delivery facts bound to each Spec, kept apart from the authored changes because they derive from anchors.", + "A comparison states facts about two commits and confers nothing. It grants no approval, raises no staleness flag, moves no readiness and never records that a pass happened.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:consumers.intent-composition.proposal", + specKind: "contract", + altitude: "story", + readiness: "defined", + file: "specs/consumers/intent-composition.proposal.sdp.md", + title: "A change proposal is addressed, based and checked before it lands", + narrative: + "The original design's patch loop made every change from a view reviewable and checked before it touched source. A patch named its target, the graph hash it was made against, a rationale, its operations and who proposed it; a CLI checked its schema and its base, applied it to a copy of the graph, ran the validators and only then rewrote the Spec files (`docs/lineage/v0-design/02-system-architecture.md` §2; `07-spec-studio-and-projections.md` §6). The Protocol refused the patch and the codemod. A view composes scoped intent, an agent edits source as a person would, git records the edit and the same checks evaluate it. That edit model drops two things the patch loop gave: a proposed change a person can review before an agent applies it, and a guard against a proposal made against an older design.\n\nThe first adopter's agents already write proposals in that shape. A design pass returns findings that each name an entry address or a Spec section, state the gap, and propose exact text for a Spec with the entry it replaces or follows (`libar-platform/design/advisors/task-pass.md`), and its register tool applies a JSON patch only after checking all of it (`libar-platform/design/advisors/register.md`). This Spec states the reviewable half of the patch loop without the codemod. A proposal is a request that an agent applies as an ordinary edit, checked by `sdp validate` on a worktree before it merges. It also gives the Spec Studio's composing panel a shape to hand off.", + sections: { + intent: { + problem: + "A proposed design change travels as free prose, so a reviewer cannot tell what it touches, which version of the design it read, or whether it still applies once the design has moved.", + outcome: + "Give a proposed design change a reviewable shape: what it touches by Spec id or entry address, the commit it was read at, the change requested and why, checked on a worktree before merge.", + openQuestions: [ + { + question: + "Does a proposal name who proposed it, as the original's `proposedBy` did, or do git authorship and co-author trailers stand in once it is applied?", + blocking: false, + key: "proposer", + }, + ], + }, + behavior: { + rules: [ + "A proposal is one JSON document on one schema, which a recipe takes as a parameter; a Markdown memo of the proposal is a rendering of that document and never a second carrier.", + "A proposal names its scope as one or more Spec ids or entry addresses, each resolved before anything is applied: a Spec id through the reader's Spec context, an entry address through address resolution.", + "A proposal names the commit whose graph it was read against.", + "A proposal states the requested change in words an agent can apply, such as the exact text for an entry with the address it replaces or follows, a new open question with its flag, or a relation to declare or remove.", + "A proposal states its reason in one line.", + "Before a proposal is applied, the design diff between its base commit and the head is read entry by entry. A change whose scope moved since the base goes to a person instead of being applied, every other change applies, and one moved entry never holds back the whole proposal.", + "Applying a proposal is an ordinary source edit by an agent or a person; no tool rewrites a carrier from it, and nothing is applied to a copy of the graph first.", + "An applied proposal lands only from a worktree on which `sdp validate` reports no error, and the conformance and honesty checks stay the only gate.", + "A proposal carries no status, approval or verdict, and the graph records none; whether it was accepted is git history and the adopter's own record.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:consumers.reference-projection", + specKind: "behavior", + altitude: "feature", + readiness: "idea", + file: "specs/consumers/reference-projection.sdp.md", + title: "A reference projection serves a reader outside the repository", + narrative: + 'The surfaces taxonomy names a reference projection, an interface and API reference kept current from the graph (`docs/concept/06-consumers-and-projections.md`, "The surfaces & projections taxonomy"). The original design generated its nearest relative, OpenAPI and AsyncAPI documents built from route and schema nodes and linked back to their Specs (`docs/lineage/v0-design/07-spec-studio-and-projections.md` §5.2). Plan 35 deferred it, because no candidate named a reader that the four shipped roots, the Design Review, the census, Mermaid and the Gherkin-shaped view, fail to serve (plan 35, "H leftover projections"). The trigger lived in plan prose; this Spec carries it as its own blocking question, the home the planning-truths placement decision gives a re-entry trigger.', + sections: { + intent: { + outcome: + "Publish a current interface and API reference from the graph for a reader outside the repository, once such a reader is named.", + openQuestions: [ + { + question: + "Re-entry trigger from plan 35: a named human consumer outside the repository whom the four shipped roots do not serve, with a way to measure that demand inside the arc that builds it. The taxonomy row is not that consumer.", + blocking: true, + key: "namedOutsideReader", + }, + { + question: + "Would the reference read pinned declarations, contract-kind Specs or `api:` code anchors, and does any of them hold enough to document an interface?", + blocking: false, + key: "referenceSource", + }, + ], + }, + behavior: {}, + }, + deliveryFacts: [], + }, + { + id: "spec:consumers.structural-mermaid", + specKind: "behavior", + altitude: "feature", + readiness: "idea", + file: "specs/consumers/structural-mermaid.sdp.md", + title: "Mermaid diagrams can be rooted at code units", + narrative: + 'The shipped Mermaid projection draws bounded one-hop diagrams rooted at a Spec or a Pack and never the whole graph. Code units, their component membership and their `uses` edges appear in the census tables and in recipes 12 to 14 and 17, never in a diagram. The original design drew container and component maps from its architecture model (`docs/lineage/v0-design/07-spec-studio-and-projections.md` §8.2, §8.3). Plan 35 deferred a Mermaid rendering of structural edges, because no reader needed diagrams rooted at code units that the census and the Spec-rooted diagrams fail to serve (plan 35, "H leftover projections"). Components have since gained a layer and a context, so such a diagram would have more to draw. The shipped projections are frozen, so this view would be a new root or would need a decision that narrows the freeze. If the Spec Studio proceeds, its architecture lens is the first reader that wants code units drawn, and this Spec is expected to fold into that lens rather than become a Mermaid root.', + sections: { + intent: { + outcome: + "Draw component, membership and uses diagrams rooted at code units, once a reader needs them and the existing views do not serve it.", + openQuestions: [ + { + question: + "Re-entry trigger from plan 35: a named reader who needs diagrams rooted at code units, which census tables and Spec-rooted one-hop Mermaid do not serve, plus a recorded decision to extend or abandon the one-generated-view posture.", + blocking: true, + key: "namedStructuralReader", + }, + { + question: + "Would a component diagram group its nodes by context and order them by layer, and would that make it the architecture view the Studio's architecture lens draws?", + blocking: false, + key: "layerAndContext", + }, + ], + }, + behavior: {}, + }, + deliveryFacts: [], + }, + { + id: "spec:consumers.projections-model.roadmap-views", + specKind: "model", + altitude: "story", + readiness: "idea", + file: "specs/consumers/projections-model.roadmap-views.sdp.md", + title: "Roadmap views read releases, baselines and phases from git and the graph", + narrative: + "The projections model once defined release, baseline and roadmap vocabulary beside the projections the Protocol builds, though no unit builds a view for them. The owner ruled that the realized contract stays in the projections model and this vocabulary waits here, deferred, until a reader needs it.", + sections: { + intent: { + outcome: + "Name the release, baseline and roadmap views a delivery reader may want, as optional projections that never gate or sequence work.", + openQuestions: [ + { + question: + "Re-entry trigger: a named reader who needs release, baseline or roadmap views that the shipped projections, the recipes and git do not serve, with a way to measure that need inside the arc that builds them.", + blocking: true, + key: "namedRoadmapReader", + }, + ], + }, + model: { + terms: { + release: "A tagged set surfaced as a git-tag projection.", + baseline: + "A named approved snapshot whose signed git tag is the approval artifact, with approval remaining outside the authored model.", + "phase / iteration / milestone": + "Descriptive vocabulary for optional roadmap projections, never gates or enforced sequences.", + }, + }, + }, + deliveryFacts: [], + }, ] as const; diff --git a/test/self-hosting-oracle/decisions.ts b/test/self-hosting-oracle/decisions.ts index 22c86db2..44543505 100644 --- a/test/self-hosting-oracle/decisions.ts +++ b/test/self-hosting-oracle/decisions.ts @@ -859,47 +859,6 @@ export const decisionsSpecs = [ }, deliveryFacts: [], }, - { - id: "spec:decisions.structural-anchor-semantics", - specKind: "decision", - altitude: "feature", - readiness: "ready", - file: "specs/decisions/structural-anchor-semantics.sdp.md", - title: "Structural anchor semantics", - narrative: null, - sections: { - intent: { - outcome: - "Resolve which intent-free structural relationships code anchors may author without creating a second architecture truth.", - }, - decision: { - context: - "The generic `codeAnchor` already binds realization to a Spec through `satisfies`, while the reserved structural-semantics question leaves component membership and dependencies between anchored code units unresolved. Gen-1 also used `@architect-implements` to join separate test-pattern identities, not code to Spec, and exposed authored `uses` relationships through a broad tag registry.", - decision: - "In order, first, `satisfies` remains the only code-to-Spec realization slot: code that realizes a contract-kind Spec satisfies that Spec by authoring convention, so no `implements` field is admitted. Second, `component` and `uses` enter the `codeAnchor` contract as the only new structural fields. Third, their value forms are closed graph-ID references: `component?: ComponentAnchorId` is one statically reifiable reference, and `uses?: readonly CodeAnchorId[]` is a statically reifiable array of references; neither field accepts free strings or an enum detached from graph identity. Fourth, the structural references derive only the anchored `memberOf` and `uses` edges under the validation rules below. Fifth, the census always derives their taxonomy and rows from the graph and runtime constants; it is generated, disposable, and never hand-maintained.", - rationale: [ - "This is hard to reverse because the anchor authoring API and persisted edge vocabulary become contracts for source, validators, queries, and projections. It is surprising without context that contract realization remains `satisfies`, that a dependency cycle is accepted, and that structural edges confer no realization state. It is a real trade-off: local authored structure and referential checks justify two narrow fields, while rejecting `implements`, inference, lifecycle metadata, and an open tag system gives up looser notation to preserve one graph language. The caution is gen-1's taxonomy drift: a claimed 50 tags was corrected to about 26 while inconsistent counts remained (`reviews/14-executable-verification-design-review.md`, which cites the gen-1 formal-spec findings review); generated census output is therefore evidence, never another registry.", - ], - consequences: [ - "Every `component` and `uses` target must exist as a `CodeNode`; a dangling graph ID is an error. A `memberOf` edge runs only from an `impl:` or `api:` `CodeNode` to a `component:` `CodeNode`, and each source has at most one component, enforcing one-level membership: a `component:` node cannot itself be a member. A `uses` edge runs between `CodeNode` endpoints whose IDs use the `impl:`, `api:`, or `component:` namespace.", - "A present `uses` array must be non-empty and contain unique targets. Structural edges must be unique, and any structural self-reference is an error. Multi-node `uses` cycles remain authored data for census projection, not validator findings; validators do not infer transitive edges or reject cycles.", - "The census renders the admitted structural bindings from graph edges, including component membership and uses relationships, and reports an explicit empty state when none exist. It may group cycles as data, but it neither re-derives validation findings nor owns a manually curated taxonomy.", - "The new fields extend `codeAnchor` only; there are no per-namespace sibling anchor builders.", - "The new edges are anchored claims only, never inferred claims; deriving architecture from imports is refused, because these edges are authored declarations in code. Derivation remains a mechanism and does not add a fourth claim.", - "Anchors carry no intent, readiness, status, or delivery fact. `memberOf` and structural `uses` mint no delivery facts, add no delivery status, and do not change readiness floors.", - "No free-form tag vocabulary, authored lifecycle, or parallel registry is admitted.", - "Foreign fields remain extraction errors; after admitting `component` and `uses`, the anchor envelope remains closed and a malformed structural field refuses the whole anchor rather than yielding a partial declaration.", - "Anchor-required lint remains warn-level and optional; absence of an anchor is useful evidence but never a load-bearing workflow gate.", - ], - alternatives: [ - "Adding `implements` would mirror gen-1 terminology, but would duplicate `satisfies` for contract-kind targets and make code-to-Spec realization ambiguous.", - "Deriving dependencies from imports would increase coverage without annotations, but would turn incidental runtime wiring into authoritative architecture and misclassify an inferred observation as an anchored claim.", - "Closed enums or free-form tags would be easy to author, but would not resolve to graph nodes and would recreate the taxonomy-governance failure this decision is intended to avoid.", - ], - }, - }, - deliveryFacts: [], - }, { id: "spec:decisions.shipped-projections-frozen", specKind: "decision", @@ -917,12 +876,12 @@ export const decisionsSpecs = [ context: "An earlier projection-settling record refused re-specifying the four shipped projections, and the adoption register carried that refusal as a standing do-not-reopen row, but the refusal lived only as plan prose the graph could not check.", decision: - "Re-specifying the shipped Design Review, census, Mermaid, or Gherkin projection is refused. These four projections are the ruled read surfaces over the one graph, and the ruling reopens only through a later decision Spec that supersedes this one and passes the ADR three-part test.", + "Re-specifying the shipped Design Review, census, Mermaid, or Gherkin projection is refused. These four projections are the ruled read surfaces over the one graph. A later decision Spec that supersedes this one replaces the ruling whole; a decision Spec that refines this one narrows it for one projection or one rendering and leaves the rest frozen. Either passes the ADR three-part test.", rationale: [ "The four projections are shipped, curated read surfaces over the one graph. Re-specifying them re-litigates settled law and risks a second truth store, while a graph-checkable decision record carries the refusal where agents and validators can read it.", ], consequences: [ - "Plans treat the four projections as settled read models. Any proposal to reshape one of them arrives as a superseding decision record, never as a follow-up work item.", + "Plans treat the four projections as settled read models. Any proposal to reshape one of them arrives as a decision record that refines this one for that projection, or supersedes it whole, never as a follow-up work item.", ], }, }, @@ -945,80 +904,12 @@ export const decisionsSpecs = [ context: "The prose briefs index carried dependency maps, decision gates, do-not-reopen rows, re-entry triggers, ownership rulings, selection-pressure heuristics, and session law, but none of those truth types had a ruled home in the graph, so plan documents drifted into carrying law the graph could not check.", decision: - "Each planning-truth type lives in its ruled home. Work-item dependency truth rides `dependsOn` edges, with independence as absence of the edge and scheduling phrases never authored. Decision gates live on `decision`-kind Specs, linked by `decidedBy`. Do-not-reopen rows split by what they carry: a tradeoff refusal becomes a `decision`-kind Spec and reopens only through a later decision that `supersedes` it under the ADR three-part test; a row that restates a behavior guarantee already homed on a carrying Spec, including the runnable-modules extraction rows, stays on that Spec and changes by ordinary Spec revision; the bySymbol impact-graph row remains a blocking hold on `spec:consumers.impact-graph` and is never minted as a decision. A lawful non-decision stays in the plan record as evidence. Re-entry triggers are the deferred Spec's own blocking open questions plus `dependsOn` for a true precondition. Exclusive ownership is one Spec identity per deliverable with consumers depending on it. Selection-pressure heuristics are advisory only, carried as behavior rules on the graph-first planning Spec or in recipes. Session law splits separately, across behavior rules on the graph-first planning Spec, the on-ramp handoff rule, and the thin plan file. The briefs index shape is retired as a carrier of law.", + "Each planning-truth type lives in its ruled home. Work-item dependency truth rides `dependsOn` edges, with independence as absence of the edge and scheduling phrases never authored. Decision gates live on `decision`-kind Specs, linked by `decidedBy`. Do-not-reopen rows split by what they carry: a tradeoff refusal becomes a `decision`-kind Spec, which a later decision that `supersedes` it replaces whole and a later decision that `refines` it narrows for one case, either under the ADR three-part test; a row that restates a behavior guarantee already homed on a carrying Spec, including the runnable-modules extraction rows, stays on that Spec and changes by ordinary Spec revision; the bySymbol impact-graph row remains a blocking hold on `spec:consumers.impact-graph` and is never minted as a decision. A lawful non-decision stays in the plan record as evidence. Re-entry triggers are the deferred Spec's own blocking open questions plus `dependsOn` for a true precondition. Exclusive ownership is one Spec identity per deliverable with consumers depending on it. Selection-pressure heuristics are advisory only, carried as behavior rules on the graph-first planning Spec or in recipes. Session law splits separately, across behavior rules on the graph-first planning Spec, the on-ramp handoff rule, and the thin plan file. The briefs index shape is retired as a carrier of law.", rationale: [ "The closed six-relation vocabulary already expresses every ruled home, so the ruling costs no engine work and keeps planning prose free of sequencing authority. New relation types such as `precedes`, `inArc`, or `forbids` were refused: each one is engine surface across the model, parser, extraction, validators, oracle rosters, and agent-surface docs, buying machine-checkable planning relations the corpus does not need. A `constraint`-kind home for refusals was refused because refusals are decisions. A single session-law home was refused because the planning Spec must not own per-session routing.", ], consequences: [ - "Plans stay thin lineage pointers and the do-not-reopen register lives across its ruled homes: tradeoff refusals in decision Specs, already-homed guarantees on their carrying Specs, and the impact-graph row as a blocking hold. This ruling reopens only through a later `decision`-kind Spec that supersedes this one and itself passes the ADR three-part test.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.architectural-significance-rides-primitives", - specKind: "decision", - altitude: "feature", - readiness: "ready", - file: "specs/decisions/architectural-significance-rides-primitives.sdp.md", - title: "Architectural significance rides existing primitives", - narrative: null, - sections: { - intent: { - outcome: - "Rule that Specs carrying architectural significance, and relationships among those Specs, are authored on the existing Spec primitive, relation vocabulary, and structural anchors, so the graph answers architecture questions without a second architecture vocabulary.", - }, - decision: { - context: - '`spec:model.structural-patterns` asked whether a vocabulary beyond `component` and `uses` passes the ADR three-part test, and "pattern" is not a ratified term; gen-1\'s tag-registry drift (~50→~26 tags) is the recorded failure of an open structural vocabulary (MD-30 cites it).', - decision: - 'Architectural significance is authored on existing primitives, in order. First, architectural significance does not select a Spec kind. Choose the kind of truth being stated: behavior for runtime responsibilities, model for concepts, contract for an interface, constraint for a quality bound, and decision for a trade-off that passes the ADR test. The other existing kinds retain their meaning; no "pattern" term or kind is ratified. Second, relationships among those Specs are the existing relations — `dependsOn`, `supersedes`, `refines`, `decidedBy` — with `dependsOn` reserved for genuine semantic need and `supersedes` for actual replacement under the ADR test; scheduling-flavored edges stay refused (MD-33). Third, code linkage rides the `satisfies` → `decidedBy` join: a component or member anchor satisfies the Spec it realizes, and that Spec names its shaping decisions by `decidedBy`; code never satisfies a decision Spec directly (MD-26). Fourth, grouping is derived from id families and the component graph, not new Packs. Fifth, the significance criterion for engine self-binding is exported public surface or cross-component reach; under it `component:protocol.import` and `component:protocol.testing` enter the accepted component set.', - rationale: [ - 'Hard to reverse — it permanently closes the vocabulary question and sets the grain at which architecture is authored; surprising without context — the concept "pattern" dissolves into named coordinates rather than gaining a word; real trade-off — CodeNode-grain roles for Specs carrying architectural significance and machine-checked forbidden dependencies are given up to preserve one graph language.', - ], - consequences: [ - 'The two blocking open questions on `spec:model.structural-patterns` resolve: no vocabulary beyond `component`/`uses` passes the ADR test, and no "pattern" term is ratified.', - "Two documented grain limits stand as named limits, not holes: membership of Specs carrying architectural significance is Spec-grain (one `codeAnchor` carries one `satisfies`), and negative constraints remain declared intent, not machine-enforced graph findings.", - "New `codeAnchor`s minted under this ruling satisfy only Specs the unit already realizes; anchors are never pointed at decision Specs or unfinished Specs to manufacture coverage.", - "`component:protocol.import` and `component:protocol.testing` join the accepted component set; the structural-edges oracle's import exception comment is retired by this ruling.", - ], - alternatives: [ - "A dedicated pattern layer (`participatesIn` fields, `pattern:` ids, an architecture validator) was refused — it recreates the MD-30/MD-33-refused engine surface and the gen-1 taxonomy drift.", - "Deriving architecture from imports stays refused (MD-30): structural edges are authored declarations, never inference.", - ], - }, - }, - deliveryFacts: [], - }, - { - id: "spec:decisions.jsdoc-graph-extraction-refused", - specKind: "decision", - altitude: "feature", - readiness: "ready", - file: "specs/decisions/jsdoc-graph-extraction-refused.sdp.md", - title: "Source commentary never enters the graph", - narrative: null, - sections: { - intent: { - outcome: - "Rule that JSDoc, doc comments, and other source commentary never author graph nodes, relations, membership, or Spec intent, so the statically reified anchor constant remains the only write path from code into the graph.", - }, - decision: { - context: - "The v0 lineage supported three marker styles — decorators, JSDoc tags, and marker constants — all extracted (docs/lineage/v0-design/04-authoring-surfaces.md §2, 06-extraction-and-validation.md). The landed corpus keeps only the statically reified anchor constant; decorator and JSDoc forms remain unextracted representations (`spec:model.anchors`). Law-grade prose now sits in some engine file headers (for example `src/graph/delivery-facts.ts`), which raises whether the extractor should parse that prose into the graph. The ratified base rules which binding syntaxes extract (`spec:model.anchors`, MD-30); whether comment prose may author graph content is unruled until this decision, and the PR #25 review session genuinely deliberated that fork — this rules an open question, it does not repair drift.", - decision: - "Ordinary JSDoc and local commentary never author graph content — no nodes, no relations, no component membership, no delivery facts, no Spec intent. The only write path from source into the graph is the statically reified anchor constant under `spec:model.anchors`, and it is identity only. When a comment states rules other surfaces depend on, those rules promote into a Spec under the promotion law (`spec:model.spec-sections`) and the comment demotes to local commentary plus a Spec pointer.", - rationale: [ - "Hard to reverse — a comment-parsing write path would become a contract for source, extractor, and validators. Surprising without context — the lineage tool extracted identity tags from JSDoc, so the obvious parity move looks like extracting prose too; this refusal is about prose, not identity. Real trade-off — comment-prose extraction would make implementation files authoritative for intent (lineage 04 §2.4: markers are read-only pointers from code to spec, never the reverse) and would create a second, silently divergent read model beside the one extraction path (MD-14); giving that up keeps one graph language.", - ], - consequences: [ - "The extractor never reads comment content; a comment's presence, absence, or wording produces no graph finding.", - "A comment may explain the implementation and point at a Spec id; it confers nothing, and restating promoted law in the comment violates exclusive promotion (MD-10).", - ], - alternatives: [ - "Extracting identity tags from JSDoc (the v0 JSDoc marker style) stays refused with the decorator form — both are unextracted representations, and the anchor constant is the single binding syntax.", - "Parsing law-grade prose comments into Spec sections was refused — it inverts the separation of intent from binding and recreates a shadow intent carrier beside the Specs.", + "Plans stay thin lineage pointers and the do-not-reopen register lives across its ruled homes: tradeoff refusals in decision Specs, already-homed guarantees on their carrying Specs, and the impact-graph row as a blocking hold. This ruling is replaced only by a later `decision`-kind Spec that supersedes it, or narrowed by one that refines it, and either passes the ADR three-part test.", ], }, }, @@ -1028,7 +919,7 @@ export const decisionsSpecs = [ id: "spec:decisions.checked-mentions", specKind: "decision", altitude: "feature", - readiness: "defined", + readiness: "ready", file: "specs/decisions/checked-mentions.sdp.md", title: "A Spec id written in prose is a checked mention", narrative: null, @@ -1036,13 +927,6 @@ export const decisionsSpecs = [ intent: { outcome: "Make a Spec id written in prose resolve, and make one keyed entry of an open section or one keyed open question addressable, so a reference below the Spec level can be checked.", - openQuestions: [ - { - question: - "`mention` and `entry address` are candidate terms, defined in the glossary's flagged list. They move into the ratified tables when this record states `ready`.", - blocking: false, - }, - ], }, decision: { context: @@ -1091,7 +975,7 @@ export const decisionsSpecs = [ }, decision: { context: - "The Design Review prints an open section as one fenced JSON object, so each entry's value is an escaped string. Authored order is restored by `spec:extraction.open-section-order` as an ordinary revision, because no Spec states the sort. Rendering is different: `spec:consumers.design-review` states that fenced JSON preserves authored keys and values, and the shipped projections are frozen, so a list rendering needs a superseding record. The first adopter corpus carries 762 keyed entries, and its reviewers read the carrier files instead of the review.", + "The Design Review prints an open section as one fenced JSON object, so each entry's value is an escaped string. Authored order is restored by `spec:extraction.open-section-order` as an ordinary revision, because no Spec states the sort. Rendering is different: `spec:consumers.design-review` states that fenced JSON preserves authored keys and values, and the shipped projections are frozen, so a list rendering needs a decision that narrows the freeze for this one rendering. The first adopter corpus carries 762 keyed entries, and its reviewers read the carrier files instead of the review.", decision: "The Design Review renders each entry of a `design` or `ui` section as a list item in authored order, its key as inline code. A string value renders as Markdown text through the prose escaping rule; a number or boolean renders as inline code; an array or object renders as a fenced JSON block nested under its item, so a structured value the TypeScript carrier admits survives without loss. The fenced-JSON rule of the Design Review stops covering an open section as a whole and keeps covering a structured value inside one entry. The freeze on the four shipped projections stands for everything else.", rationale: [ @@ -1123,7 +1007,7 @@ export const decisionsSpecs = [ }, decision: { context: - "`spec:model.open-question-keys` gives an open question an optional key in its marker and an entry address `spec:<id>#question.<key>`. The Design Review renders an open question as its escaped text, with the word blocking after a blocking one, and the shipped projections are frozen, so a reader of the page cannot see the key the address needs. The first adopter cites its open questions from a decision register and from generated pages.", + "`spec:model.open-question-keys` gives an open question an optional key in its marker and an entry address `spec:<id>#question.<key>`. The Design Review renders an open question as its escaped text, with the word blocking after a blocking one, and the shipped projections are frozen, so a reader of the page cannot see the key the address needs until a decision narrows the freeze for it. The first adopter cites its open questions from a decision register and from generated pages.", decision: "The Design Review renders an open question that carries a key with the key as inline code, `#<key>`, at the start of its item, before the question's text; a question without a key renders as before. The freeze on the four shipped projections stands for everything else.", rationale: [ @@ -1145,52 +1029,34 @@ export const decisionsSpecs = [ id: "spec:decisions.studio-html-surface", specKind: "decision", altitude: "feature", - readiness: "scoped", + readiness: "ready", title: "The Studio renders HTML, and the HTML stays derived", narrative: null, sections: { intent: { outcome: "Record why the Studio's surface is generated HTML rather than Markdown, and what keeps the HTML out of the diffs a reviewer reads.", - openQuestions: [ - { - question: - "Is the Studio's HTML the primary surface for a person reviewing a design, with Markdown as the fallback, as the original states, or one human projection beside the generated Markdown Design Review, which the consumers concept keeps as the MVP view after agents, the first sink?", - blocking: true, - key: "primarySurface", - }, - { - question: - "The original accepts HTML at about two to four times Markdown's generation time, with a negligible token cost under prompt caching and million-token windows, which prices a page a model writes; a projection is rendered by code from the graph, so does the argument become a bound on page weight or build time, or fall away?", - blocking: false, - key: "generationCost", - }, - { - question: - "The original keeps git diffs over canonical TypeScript Specs; the carrier ruling makes Markdown the default carrier and the TypeScript DSL a lawful per-ID option. Does any part of the case for derived HTML depend on the carrier being TypeScript?", - blocking: false, - key: "canonicalCarrier", - }, - ], }, decision: { context: - "When a model writes the Specs, a person's main act is review, and review reads tables, diagrams, code, comparisons and examples across many Specs. Markdown caps out at about a hundred lines, past which nobody reads it, its authors included; large Specs are common, and Markdown rots into a wall of text quickly.", + "When a model writes the Specs, a person's main act is review, and review reads tables, diagrams, code, comparisons and examples across many Specs. The first adopter's reviewers need search, filters, navigation and progressive disclosure across a Pack, which the generated Markdown pages do not give: the Pack page's member table runs thirteen columns wide, and those readers open the carrier files instead.", decision: "The Studio renders generated HTML as its surface. The HTML is derived and never committed in raw form: git diffs stay over the canonical Spec source, and the Studio's files are regenerated from the graph.", rationale: [ - "HTML conveys more per screen: tables, SVG diagrams, code blocks with syntax highlighting, embedded harnesses, interactive sliders, side-by-side comparisons and tabbed views are all native to it.", - "HTML is shareable: a build uploaded to S3 or any static host is a link, with no rendering-engine mismatch between viewers.", - "HTML is interactive: a reader can move an NFR target on a slider, toggle a scenario harness, or reorder a Pack's members by drag and drop, none of which Markdown can do.", - "When a model writes the Specs, review is the person's main act, and HTML beats Markdown for review by a wide margin.", - "The download is acceptable: HTML takes about two to four times Markdown's generation time, and with prompt caching and million-token windows the token cost is negligible for the value gained.", + "HTML conveys more per screen: collapsible sections, wide tables that scroll in place, SVG diagrams, highlighted code and side-by-side comparisons are native to it.", + "HTML is shareable: a page set uploaded to any static host is a link a reviewer opens without installing anything.", + "HTML is interactive: search, filters and navigation run in the page over values the build wrote, which Markdown cannot do.", + + "The Studio's code renders its HTML from the graph, so the original's argument from generation time and token cost, which priced a page a model writes, does not apply.", ], alternatives: [ - "Markdown as the primary surface diffs cleanly in git, but caps out at about a hundred lines and cannot be interactive.", + "Markdown as the only human surface diffs cleanly in git and renders on GitHub, but it cannot search, filter or collapse, which is why the Markdown Design Review stays beside the Studio rather than in its place.", ], consequences: [ "The principal trade-off of HTML, diffs that are noisier in git, is sidestepped because the HTML is derived and never committed.", - "Markdown remains a fallback surface.", + "The Studio stands beside the Markdown Design Review rather than above it. The Studio is the workbench a person reads, and the frozen Markdown Design Review stays the surface git diffs, GitHub renders and agents read; neither ranks above the other.", + "The case for derived HTML holds for any canonical carrier, because it rests on the HTML being derived and never committed, not on the Specs being written in TypeScript.", + "The richer interactions the original design lists, such as moving a target on a slider, running a scenario harness or reordering members by drag and drop, wait for later phases and their own Specs; the Studio's first build is read-only.", ], }, }, @@ -1244,12 +1110,12 @@ export const decisionsSpecs = [ }, decision: { context: - "The source-commentary ruling it supersedes (`spec:decisions.jsdoc-graph-extraction-refused`, MD-35) refused two things in one record: comment prose authoring graph content, and identity tags in doc comments. Its rationale said the refusal \"is about prose, not identity\", while its operative alternative also refused structured identity tags; the annotation scout under `docs/lineage/` names that mismatch and finds that gen 1's drift was duplicate identities, scan scope, and syntax handling, never an extracted identity tag as such. The first adopter showed the cost of the single form: every one of its code anchors sits in a test or a harness because its runtime code cannot import this package, `byFile` on a source file returns nothing, and a hand-kept join stands in for the binding. The original design (`docs/lineage/v0-design/04-authoring-surfaces.md` §2) supported three marker styles feeding one extractor, and its package shape (`docs/lineage/v0-design/02-system-architecture.md` §3) kept the marker library free of runtime dependencies so product code could import it. The owner's direction opening plan 40 is to move closer to that design.", + "The source-commentary ruling this record replaced (MD-35, deleted because nothing in it still needed its own text; it reads in git at `90c75dd`) refused two things in one record: comment prose authoring graph content, and identity tags in doc comments. Its rationale said the refusal \"is about prose, not identity\", while its operative alternative also refused structured identity tags; the annotation scout under `docs/lineage/` names that mismatch and finds that gen 1's drift was duplicate identities, scan scope, and syntax handling, never an extracted identity tag as such. The first adopter showed the cost of the single form: every one of its code anchors sits in a test or a harness because its runtime code cannot import this package, `byFile` on a source file returns nothing, and a hand-kept join stands in for the binding. The original design (`docs/lineage/v0-design/04-authoring-surfaces.md` §2) supported three marker styles feeding one extractor, and its package shape (`docs/lineage/v0-design/02-system-architecture.md` §3) kept the marker library free of runtime dependencies so product code could import it. The owner's direction opening plan 40 is to move closer to that design.", decision: "An anchor has two extracted representations that feed one closed envelope, the constant form as ruled and a comment form. The comment form is a top-level `/** … */` block in a `.ts` or `.tsx` file carrying reserved camelCase, TSDoc-compatible tags; `@sdpAnchor <id>` opens it, and the id's namespace selects the flavor exactly as the constant builders do. Comment prose still authors nothing: only the reserved tags are read, and a comment that does not open with `/**` is never read. Trust is by reserved grammar, not by builder import, so no import is required. The same id in two forms is a duplicate id. The package also ships a zero-dependency subpath `@libar-dev/software-delivery-protocol/anchors` exporting the id builders and the three anchor builders and nothing else, for runtimes that may import but must not load `node:*` modules or ts-morph; the extractor trusts that specifier as a Protocol builder module.", rationale: [ - "Hard to reverse: a second write path from source becomes a contract for the extractor, the validators, and every adopter's code. Surprising without context: the superseded ruling refused the obvious parity move, and this one admits exactly the half its own rationale left open while keeping the half it was about. Real trade-off: a tag grammar in a comment gets no compiler check and the extractor gains a second parser; in exchange, code that cannot import the package binds at its real site and `byFile` answers for it.", - "What survives from the superseded ruling: comment prose authors nothing; a comment's wording beyond the reserved tags produces no graph finding; a comment that states law other surfaces depend on still promotes into a Spec under the promotion law, and restating promoted law in a comment violates exclusive promotion (MD-10), so the comment demotes to local commentary plus a Spec pointer; one graph language stays, because both forms feed one envelope and one extraction path (one validation path, MD-14).", + "Hard to reverse: a second write path from source becomes a contract for the extractor, the validators, and every adopter's code. Surprising without context: the replaced ruling refused the obvious parity move, and this one admits exactly the half its own rationale left open while keeping the half it was about. Real trade-off: a tag grammar in a comment gets no compiler check and the extractor gains a second parser; in exchange, code that cannot import the package binds at its real site and `byFile` answers for it.", + "What survives from the replaced ruling: comment prose authors nothing; a comment's wording beyond the reserved tags produces no graph finding; a comment that states law other surfaces depend on still promotes into a Spec under the promotion law, and restating promoted law in a comment violates exclusive promotion (MD-10), so the comment demotes to local commentary plus a Spec pointer; one graph language stays, because both forms feed one envelope and one extraction path (one validation path, MD-14).", ], consequences: [ "The reserved tags are `@sdpAnchor`, `@sdpLabel`, `@sdpSatisfies`, `@sdpVerifies`, `@sdpModels`, `@sdpReferences`, `@sdpComponent`, `@sdpUses`, `@sdpRole`, `@sdpLayer`, and `@sdpContext`; the spelling is camelCase so TSDoc tooling reads each as one tag.", @@ -1342,12 +1208,12 @@ export const decisionsSpecs = [ }, decision: { context: - "Two rulings carried the structural half of the anchor: structural anchors confer nothing (`spec:decisions.structural-anchor-semantics`, MD-30) admitted `component` and `uses` as the only structural fields, and architectural significance rides existing primitives (`spec:decisions.architectural-significance-rides-primitives`, MD-34) refused a vocabulary beyond them and gave up CodeNode-grain roles. Both cited gen 1's tag-registry drift, about 50 tags to about 26, as the failure of an open structural vocabulary. The annotation scout under `docs/lineage/` corrects that account: the 50-to-26 collapse was a cleanup of gen 1's formal vocabulary that never shipped as a runtime registry, and what drifted there was duplicate identities, scan scope, and syntax handling. The original design declared a Component with `layer` and `bounded_context` (`docs/lineage/v0-design/03-graph-metamodel.md` §2.5), and gen 1 carried a role on each annotated unit. The owner's direction opening plan 40 is to bring back the value of annotating architecturally significant units and their relationships in code.", + "Two rulings carried the structural half of the anchor: structural anchors confer nothing (MD-30) admitted `component` and `uses` as the only structural fields, and architectural significance rides existing primitives (MD-34) refused a vocabulary beyond them and gave up CodeNode-grain roles. Both cited gen 1's tag-registry drift, about 50 tags to about 26, as the failure of an open structural vocabulary. The annotation scout under `docs/lineage/` corrects that account: the 50-to-26 collapse was a cleanup of gen 1's formal vocabulary that never shipped as a runtime registry, and what drifted there was duplicate identities, scan scope, and syntax handling. The original design declared a Component with `layer` and `bounded_context` (`docs/lineage/v0-design/03-graph-metamodel.md` §2.5), and gen 1 carried a role on each annotated unit. The owner's direction opening plan 40 is to bring back the value of annotating architecturally significant units and their relationships in code.", decision: "Architectural significance is annotated on the anchor of the unit that realizes it. A code anchor may carry `role`, the architectural pattern the unit plays; a `component:` anchor may additionally carry `layer`, one of `edge`, `application`, `domain`, `adapter`, or `infrastructure`, and `context`, its bounded context. The `component:` anchor is anchored architecture: it is the declared component, and there is no separate architecture file, because the anchor sits where the component is realized. It is not the original design's declared Component that could exist before code; an unrealized component lives in Specs and never as an invented CodeNode. A role is a free, corpus-owned vocabulary whose taxonomy the census renders with counts from the graph; `service`, `decider`, `projection`, `read-model`, `codec`, `contract`, `barrel`, and `utility` are the lineage set, not a closed list. `layer` is a closed set, and a value outside it is an envelope error; `layer` or `context` on a non-component anchor is an envelope error. All three attributes are optional, and omission is lawful.", rationale: [ - "Hard to reverse: the anchor envelope and the CodeNode fields are contracts for the extractor, the census, the Design Review, and every adopter's annotations. Surprising without context: the superseded rulings refused exactly these attributes on the strength of a drift account the scout found inaccurate, so the reversal reads as a repeat of gen 1 until the correction is on the table. Real trade-off: a free role vocabulary can drift across a corpus and no validator will say so; in exchange, the role, layer, and context live on the one anchor at the one site, and the census taxonomy makes drift visible without a registry.", - "What survives from both superseded rulings is restated here as consequences: the `component` and `uses` semantics and their validation rules, structural non-conferral, no inference from imports, no status or readiness on code, significance never selects a Spec kind, no `pattern:` namespace, and the engine significance criterion.", + "Hard to reverse: the anchor envelope and the CodeNode fields are contracts for the extractor, the census, the Design Review, and every adopter's annotations. Surprising without context: the replaced rulings refused exactly these attributes on the strength of a drift account the scout found inaccurate, so the reversal reads as a repeat of gen 1 until the correction is on the table. Real trade-off: a free role vocabulary can drift across a corpus and no validator will say so; in exchange, the role, layer, and context live on the one anchor at the one site, and the census taxonomy makes drift visible without a registry.", + "This record replaced both rulings, and both records were deleted because nothing in them still needed its own text; they read in git at `90c75dd`. What survives from them is restated here as consequences: the `component` and `uses` semantics and their validation rules, structural non-conferral, no inference from imports, no status or readiness on code, significance never selects a Spec kind, no `pattern:` namespace, and the engine significance criterion.", ], consequences: [ "`component?: ComponentAnchorId` and `uses?: readonly CodeAnchorId[]` stay closed graph-ID references deriving only anchored `memberOf` and `uses` edges; neither accepts a free string or an enum detached from graph identity, and there are no per-namespace sibling builders.", @@ -1365,6 +1231,8 @@ export const decisionsSpecs = [ "No `pattern` term, kind, or `pattern:` namespace is admitted; a role is an attribute of a CodeNode, not a node; grouping is derived from id families and the component graph, not new Packs.", "The engine significance criterion stays exported public surface or cross-component reach; code never satisfies a decision Spec directly (MD-26), and an anchor is never pointed at an unfinished Spec to manufacture coverage.", "`role` and `context` values are one lowercase kebab token matching `^[a-z][a-z0-9-]*$`, with no normalization, so `read-model`, `readModel`, and `Read Model` cannot coexist as three categories; a value outside the token grammar is an envelope error.", + "The census derives its structural rows and taxonomy from graph edges and runtime constants on every run; it re-derives no validation finding, owns no hand-kept taxonomy, and may group `uses` cycles as data.", + "Code reaches the decisions that shape it through the `satisfies` → `decidedBy` join: a unit satisfies the Spec it realizes, and that Spec names its shaping decisions by `decidedBy`.", "The census links each role, layer, and context value to the units that carry it, so an owner can reconcile two spellings of one category by editing the anchors; nothing forces a label, and an unlabelled unit does not populate the taxonomy.", "The five layers, one line each: `edge` is the transport and presentation boundary; `application` is use cases and orchestration; `domain` is the model and its rules; `adapter` is the implementations of ports toward external systems; `infrastructure` is runtime, persistence, and platform plumbing.", "A package may hold several layers or several contexts; the package-to-component mapping is reviewed, and one component per package is a choice, never a rule.", @@ -1382,5 +1250,38 @@ export const decisionsSpecs = [ }, }, deliveryFacts: [], + }, // The design-management arc (plan 41). + { + id: "spec:decisions.pack-design-page", + specKind: "decision", + altitude: "feature", + readiness: "ready", + file: "specs/decisions/pack-design-page.sdp.md", + title: "The Design Review's Pack page presents the Pack's design", + narrative: null, + sections: { + intent: { + outcome: + "Let a person working in the repository read where a Pack's design stands, what holds each member below its next rung, what the Pack rests on, and which code answers to it, on the page the Protocol already generates.", + }, + decision: { + context: + "The Design Review's Pack page lists the members with their readiness and bindings, and the verifier gaps. The first adopter designs whole capabilities ahead of their code as Packs and generates its own page beside the Design Review, 1,201 lines of script that walks the readiness floor one rung above the stated rung, reads what the members rest on outside the Pack, joins the open questions, and re-parses carrier files for the line of each entry. Its design passes read that page first. The shipped projections are frozen, so the Pack page cannot change until a decision narrows the freeze for it.", + decision: + "The Design Review's Pack page renders the Pack design that `spec:consumers.pack-design` assembles: a member table in authored order with each member's stated rung, floor reached, the rung above the stated rung with the clause that holds it there, its design columns and its bindings; a section naming what holds each member below its next rung; the Specs outside the Pack that members rest on and that rest on members; each member's open questions with their entry addresses and source lines; and each member's realizing and referencing code units with their roles and their components' layers and contexts. The verifier gaps and findings stay. The freeze on the four shipped projections stands for everything else, the Spec page and the index included.", + rationale: [ + "The assembly already exists as graph data and reader values, and an adopter that rebuilds it by hand reads the floor a second way, which the next-rung record showed it gets wrong. One assembly in the reader, rendered on the page the Protocol ships, gives a reviewer the design as it stands and leaves the adopter's script nothing to recompute.", + ], + alternatives: [ + "Starting the Spec Studio with an HTML Pack page is closer to the original design, but the Studio's package home and its reader are blocking questions, and the Pack design data serves the Studio later unchanged.", + "Shipping the assembly as data only keeps every projection frozen and leaves rendering to each adopter, which is the burden this record removes.", + ], + consequences: [ + "The Pack page grows from a member table to a design page, and the golden Design Review Pack pages regenerate.", + "Line links on the page point into carrier files through the graph's location table, so the page stays a function of the graph alone.", + ], + }, + }, + deliveryFacts: [], }, ] as const; diff --git a/test/self-hosting-oracle/declared-relations.ts b/test/self-hosting-oracle/declared-relations.ts index d2560230..2441ceb0 100644 --- a/test/self-hosting-oracle/declared-relations.ts +++ b/test/self-hosting-oracle/declared-relations.ts @@ -140,6 +140,7 @@ export 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", "constrainedBy", "spec:carrier.inline-code-spans"], ["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"], @@ -417,6 +418,11 @@ export const expectedDeclaredRelations = [ ["spec:consumers.edit-model", "refines", "spec:consumers.projections-model"], ["spec:consumers.authoring-on-ramp", "refines", "spec:consumers.edit-model"], ["spec:consumers.delivery-session-on-ramp", "refines", "spec:consumers.authoring-on-ramp"], + [ + "spec:consumers.delivery-session-on-ramp", + "decidedBy", + "spec:decisions.planning-truths-placement", + ], ["spec:consumers.agent-surface.authoring-recipes", "refines", "spec:consumers.agent-surface"], ["spec:consumers.intent-composition", "refines", "spec:consumers.edit-model"], ["spec:model.protocol-domain", "refines", "spec:protocol.self-hosting"], @@ -680,12 +686,6 @@ export const expectedDeclaredRelations = [ ["spec:decisions.carrier-universality", "refines", "spec:decisions.gherkin-carrier-option"], ["spec:decisions.carrier-universality", "dependsOn", "spec:decisions.prose-ownership"], ["spec:decisions.carrier-universality", "dependsOn", "spec:decisions.pack-markdown-carrier"], - ["spec:decisions.structural-anchor-semantics", "refines", "spec:model.anchors"], - [ - "spec:decisions.structural-anchor-semantics", - "dependsOn", - "spec:decisions.binding-not-liveness", - ], ["spec:decisions.adopted-registrars-committed", "refines", "spec:extraction.runnable-modules"], ["spec:extraction.runnable-modules", "decidedBy", "spec:decisions.adopted-registrars-committed"], ["spec:decisions.shipped-projections-frozen", "refines", "spec:consumers.projections-model"], @@ -695,30 +695,9 @@ export const expectedDeclaredRelations = [ ["spec:consumers.gherkin-view", "decidedBy", "spec:decisions.shipped-projections-frozen"], ["spec:consumers.graph-first-planning", "decidedBy", "spec:decisions.planning-truths-placement"], ["spec:decisions.planning-truths-placement", "refines", "spec:model.relations"], - ["spec:decisions.architectural-significance-rides-primitives", "refines", "spec:model.anchors"], - [ - "spec:decisions.architectural-significance-rides-primitives", - "dependsOn", - "spec:decisions.structural-anchor-semantics", - ], - [ - "spec:decisions.architectural-significance-rides-primitives", - "dependsOn", - "spec:decisions.binding-not-liveness", - ], - ["spec:decisions.jsdoc-graph-extraction-refused", "refines", "spec:model.anchors"], - [ - "spec:decisions.jsdoc-graph-extraction-refused", - "dependsOn", - "spec:decisions.one-validation-path", - ], - [ - "spec:decisions.jsdoc-graph-extraction-refused", - "dependsOn", - "spec:decisions.structural-anchor-semantics", - ], ["spec:carrier.markdown-body-grammar", "refines", "spec:carrier.markdown-authoring"], ["spec:carrier.markdown-body-grammar", "dependsOn", "spec:carrier.slot-notation"], + ["spec:carrier.markdown-body-grammar", "constrainedBy", "spec:validation.authored-honesty"], ["spec:carrier.markdown-body-grammar", "decidedBy", "spec:decisions.carrier-ruling"], ["spec:carrier.markdown-body-grammar", "decidedBy", "spec:decisions.prose-ownership"], ["spec:carrier.inline-code-spans", "refines", "spec:carrier.markdown-body-grammar"], @@ -961,6 +940,7 @@ export const expectedDeclaredRelations = [ ], ["spec:consumers.adopter-on-ramp", "dependsOn", "spec:consumers.agent-surface.register-recipes"], ["spec:consumers.adopter-on-ramp", "dependsOn", "spec:consumers.shipped-protocol-corpus"], + ["spec:consumers.adopter-on-ramp", "dependsOn", "spec:carrier.markdown-body-grammar"], [ "spec:consumers.agent-surface.address-and-cycle-recipes", "refines", @@ -987,11 +967,7 @@ export const expectedDeclaredRelations = [ ["spec:consumers.spec-studio.lenses", "dependsOn", "spec:extraction.pack-member-order"], ["spec:consumers.spec-studio.lenses", "dependsOn", "spec:observation.runtime-overlay"], ["spec:consumers.spec-studio.lenses", "decidedBy", "spec:decisions.binding-not-liveness"], - [ - "spec:consumers.spec-studio.lenses", - "decidedBy", - "spec:decisions.architectural-significance-rides-primitives", - ], + ["spec:consumers.spec-studio.lenses", "decidedBy", "spec:decisions.architectural-annotation"], ["spec:consumers.spec-studio.responsive", "refines", "spec:consumers.spec-studio"], ["spec:consumers.spec-studio.responsive", "dependsOn", "spec:extraction.claim-taxonomy"], ["spec:consumers.spec-studio", "refines", "spec:consumers.projections-model"], @@ -1037,18 +1013,10 @@ export const expectedDeclaredRelations = [ "decidedBy", "spec:decisions.verification-posture-not-realization", ], - [ - "spec:decisions.authored-entry-order", - "supersedes", - "spec:decisions.shipped-projections-frozen", - ], + ["spec:decisions.authored-entry-order", "refines", "spec:decisions.shipped-projections-frozen"], ["spec:decisions.question-key-rendering", "refines", "spec:consumers.design-review"], ["spec:decisions.question-key-rendering", "dependsOn", "spec:model.open-question-keys"], - [ - "spec:decisions.question-key-rendering", - "supersedes", - "spec:decisions.shipped-projections-frozen", - ], + ["spec:decisions.question-key-rendering", "refines", "spec:decisions.shipped-projections-frozen"], ["spec:decisions.studio-html-surface", "refines", "spec:consumers.spec-studio"], ["spec:decisions.studio-html-surface", "dependsOn", "spec:extraction.regenerability"], ["spec:decisions.studio-web-components", "refines", "spec:consumers.spec-studio"], @@ -1115,23 +1083,8 @@ export const expectedDeclaredRelations = [ ["spec:decisions.anchor-binding-grain", "refines", "spec:model.anchors"], ["spec:decisions.anchor-binding-grain", "dependsOn", "spec:decisions.binding-not-liveness"], ["spec:decisions.anchor-comment-form", "refines", "spec:model.anchors"], - [ - "spec:decisions.anchor-comment-form", - "supersedes", - "spec:decisions.jsdoc-graph-extraction-refused", - ], ["spec:decisions.anchor-comment-form", "dependsOn", "spec:decisions.one-validation-path"], ["spec:decisions.architectural-annotation", "refines", "spec:model.anchors"], - [ - "spec:decisions.architectural-annotation", - "supersedes", - "spec:decisions.structural-anchor-semantics", - ], - [ - "spec:decisions.architectural-annotation", - "supersedes", - "spec:decisions.architectural-significance-rides-primitives", - ], ["spec:decisions.architectural-annotation", "dependsOn", "spec:decisions.binding-not-liveness"], ["spec:decisions.architectural-annotation", "dependsOn", "spec:decisions.anchor-binding-grain"], ["spec:model.anchors", "decidedBy", "spec:decisions.anchor-comment-form"], @@ -1139,4 +1092,103 @@ export const expectedDeclaredRelations = [ ["spec:model.anchors", "decidedBy", "spec:decisions.architectural-annotation"], ["spec:model.structural-patterns", "decidedBy", "spec:decisions.architectural-annotation"], ["spec:protocol.structural-self-binding", "decidedBy", "spec:decisions.architectural-annotation"], + // The design-management arc (plan 41), transcribed from each carrier's frontmatter. + ["spec:decisions.pack-design-page", "refines", "spec:consumers.design-review"], + ["spec:decisions.pack-design-page", "dependsOn", "spec:consumers.pack-design"], + ["spec:decisions.pack-design-page", "refines", "spec:decisions.shipped-projections-frozen"], + ["spec:consumers.pack-design", "refines", "spec:consumers.reader"], + ["spec:consumers.pack-design", "dependsOn", "spec:validation.next-rung-floor"], + ["spec:consumers.pack-design", "dependsOn", "spec:extraction.pack-member-order"], + ["spec:consumers.pack-design", "dependsOn", "spec:extraction.entry-locations"], + ["spec:consumers.pack-design", "dependsOn", "spec:extraction.contract-declarations"], + ["spec:consumers.design-review.pack-design-page", "refines", "spec:consumers.design-review"], + ["spec:consumers.design-review.pack-design-page", "dependsOn", "spec:consumers.pack-design"], + [ + "spec:consumers.design-review.pack-design-page", + "dependsOn", + "spec:consumers.binding-language-views", + ], + ["spec:consumers.design-review.pack-design-page", "decidedBy", "spec:decisions.pack-design-page"], + ["spec:extraction.entry-locations", "refines", "spec:extraction.derive-graph"], + ["spec:extraction.entry-locations", "dependsOn", "spec:model.open-question-keys"], + ["spec:extraction.entry-locations", "constrainedBy", "spec:extraction.determinism"], + ["spec:consumers.agent-surface.recipe-parameters", "refines", "spec:consumers.agent-surface"], + [ + "spec:consumers.agent-surface.recipe-parameters", + "decidedBy", + "spec:decisions.agent-front-door", + ], + [ + "spec:consumers.agent-surface.design-recipes", + "refines", + "spec:consumers.agent-surface.register-recipes", + ], + ["spec:consumers.agent-surface.design-recipes", "dependsOn", "spec:consumers.pack-design"], + [ + "spec:consumers.agent-surface.design-recipes", + "dependsOn", + "spec:consumers.agent-surface.recipe-parameters", + ], + [ + "spec:consumers.agent-surface.example-space-coverage", + "refines", + "spec:consumers.agent-surface.design-recipes", + ], + [ + "spec:consumers.agent-surface.example-space-coverage", + "dependsOn", + "spec:carrier.slot-notation", + ], + ["spec:consumers.engine-provenance", "refines", "spec:consumers.adopter-on-ramp"], + ["spec:consumers.engine-provenance", "constrainedBy", "spec:extraction.determinism"], + ["spec:consumers.validate-watch", "refines", "spec:consumers.authoring-on-ramp"], + ["spec:consumers.validate-watch", "dependsOn", "spec:decisions.one-validation-path"], + ["spec:model.design-maturity", "refines", "spec:model.core-model"], + ["spec:model.design-maturity", "dependsOn", "spec:consumers.pack-design"], + ["spec:model.planned-architecture", "dependsOn", "spec:model.anchors"], + ["spec:model.planned-architecture", "decidedBy", "spec:decisions.architectural-annotation"], + ["spec:model.open-question-fields", "refines", "spec:model.open-question-keys"], + ["spec:model.open-question-fields", "decidedBy", "spec:decisions.adopt-the-nouns"], + ["spec:model.rule-keys", "refines", "spec:model.spec-sections"], + ["spec:model.rule-keys", "dependsOn", "spec:model.open-question-keys"], + ["spec:model.relation-reasons", "refines", "spec:model.relations"], + ["spec:model.relation-reasons", "decidedBy", "spec:decisions.prose-ownership"], + ["spec:consumers.review-perspectives", "refines", "spec:consumers.projections-model"], + ["spec:consumers.review-perspectives", "dependsOn", "spec:consumers.pack-design"], + ["spec:consumers.context-bundle", "refines", "spec:consumers.agent-surface"], + ["spec:consumers.context-bundle", "dependsOn", "spec:consumers.agent-surface.design-recipes"], + ["spec:consumers.context-bundle", "decidedBy", "spec:decisions.agent-front-door"], + ["spec:consumers.design-diff", "refines", "spec:consumers.projections-model"], + ["spec:consumers.design-diff", "constrainedBy", "spec:extraction.determinism"], + ["spec:consumers.intent-composition.proposal", "refines", "spec:consumers.intent-composition"], + ["spec:consumers.intent-composition.proposal", "dependsOn", "spec:consumers.edit-model"], + [ + "spec:consumers.intent-composition.proposal", + "dependsOn", + "spec:consumers.agent-surface.address-and-cycle-recipes", + ], + ["spec:consumers.reference-projection", "refines", "spec:consumers.projections-model"], + ["spec:consumers.structural-mermaid", "refines", "spec:consumers.projections-model"], + ["spec:consumers.structural-mermaid", "decidedBy", "spec:decisions.shipped-projections-frozen"], + ["spec:validation.superseded-decision-signal", "refines", "spec:validation.warn-level-signals"], + ["spec:validation.superseded-decision-signal", "dependsOn", "spec:model.relations"], + ["spec:validation.architecture-constraints", "dependsOn", "spec:model.anchors"], + [ + "spec:validation.architecture-constraints", + "decidedBy", + "spec:decisions.architectural-annotation", + ], + ["spec:validation.corpus-rules", "dependsOn", "spec:consumers.agent-surface"], + ["spec:validation.corpus-rules", "constrainedBy", "spec:validation.two-check-families"], + ["spec:observation.run-evidence", "dependsOn", "spec:extraction.delivery-facts"], + ["spec:observation.run-evidence", "decidedBy", "spec:decisions.binding-not-liveness"], + ["spec:consumers.projections-model.roadmap-views", "refines", "spec:consumers.projections-model"], + ["spec:consumers.intent-composition.proposal", "dependsOn", "spec:consumers.design-diff"], + [ + "spec:consumers.spec-studio.intent-panel", + "dependsOn", + "spec:consumers.intent-composition.proposal", + ], + ["spec:consumers.spec-studio.sharing", "dependsOn", "spec:consumers.engine-provenance"], + ["spec:validation.architecture-constraints", "dependsOn", "spec:validation.corpus-rules"], ] as const; diff --git a/test/self-hosting-oracle/extraction.ts b/test/self-hosting-oracle/extraction.ts index 89b12781..d3b202d0 100644 --- a/test/self-hosting-oracle/extraction.ts +++ b/test/self-hosting-oracle/extraction.ts @@ -214,7 +214,7 @@ export const extractionSpecs = [ declared: "Human intent explicitly authored in a Spec or Pack; it is authoritative intent.", anchored: - "A human binding from a code, test, or oracle location to one Spec ID; it is authoritative binding and carries no intent.", + "A human binding from a code, test, or oracle location to zero or more Spec IDs, or between code units for structure; it is authoritative binding and carries no intent.", inferred: "Machine-derived structural information; it is advisory and never authoritative.", "claim inheritance": @@ -224,13 +224,13 @@ export const extractionSpecs = [ }, }, }, - deliveryFacts: ["implemented"], + deliveryFacts: ["implemented", "has-verifier"], }, { id: "spec:extraction.regenerability", specKind: "rule", altitude: "feature", - readiness: "defined", + readiness: "ready", file: "specs/extraction/regenerability.sdp.md", title: "Generated artifacts are disposable projections", narrative: null, @@ -244,12 +244,10 @@ export const extractionSpecs = [ "Generated artifacts are disposable: deleting them and rebuilding from the same committed repository produces the same bytes.", "Consumers read the graph or link to source locations recorded in it; they never re-parse source or keep a parallel model.", "The graph is a single JSON projection with in-memory query support; a graph database remains deferred until measured traversal pain establishes a real need.", - "Measured evidence from the self-hosting corpus keeps full rebuilds comfortable below roughly 50 Specs.", - "Measured evidence defers a graph database until the graph reaches roughly 10k+ nodes or traversal pain establishes a real need.", ], }, }, - deliveryFacts: ["implemented"], + deliveryFacts: ["implemented", "has-verifier"], }, { id: "spec:extraction.schema-versioning", @@ -295,8 +293,8 @@ export const extractionSpecs = [ 'a graph derived from the authored spec {specId: "spec:probe.schema-versioning"}', ], when: ["the graph payload is serialized"], - // re-measured under plan 40 - then: ['the payload declares the schema version {schemaVersion: "0.8.0"}'], + // re-measured under plan 41 + then: ['the payload declares the schema version {schemaVersion: "0.9.0"}'], }, ], }, @@ -323,6 +321,7 @@ export const extractionSpecs = [ question: "Do the concreteness-refusal and no-guessing outcome-identity laws stated in src/codegen/contracts.ts commentary promote here or to a story-altitude child under comment promotion?", blocking: false, + key: "contractLawPromotion", }, ], }, @@ -533,6 +532,7 @@ export const extractionSpecs = [ question: "Do the every-step-and-only-the-steps and fresh-world-per-example laws stated in the runner and vitest-adapter commentary promote here or to story-altitude children under comment promotion?", blocking: false, + key: "runnerLawPromotion", }, ], }, @@ -639,6 +639,7 @@ export const extractionSpecs = [ question: "Does the derive-in-process freshness law stated in src/cli/q-command.ts commentary promote here or to a story-altitude child under comment promotion?", blocking: false, + key: "freshnessPromotion", }, ], }, @@ -699,7 +700,7 @@ export const extractionSpecs = [ id: "spec:extraction.open-section-order", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/extraction/open-section-order.sdp.md", title: "Serialization preserves authored entry order", narrative: null, @@ -744,16 +745,19 @@ export const extractionSpecs = [ question: "The contract kind's evidence row in the kind-evidence table is unchanged by this Spec. A closed contract section is not planned; if an adopter asks for one, it enters as a Spec of its own.", blocking: false, + key: "contractSection", }, { question: "A compiled module finds a name used and never declared and a name declared twice. It does not find a bullet whose prose uses a declared name with another type, which is the drift the first adopter met at its merge. Checking a span used inside a step bullet as an expression is deferred until an adopter asks for it.", blocking: false, + key: "bulletTypeDrift", }, { question: "The source report's full shape is kept here so the capture does not narrow it in silence: an opaque language-tagged fence as the value of one keyed entry, then the closed section whose declarations derive one module per corpus, then implementation code importing the derived types so that a Spec which disagrees with its code fails the build. Compiling the derived module alone finds only names used and never declared, and names declared twice.", blocking: false, + key: "reportShape", }, ], }, @@ -789,8 +793,8 @@ export const extractionSpecs = [ 'the serialized Pack node lists the members {serializedMembers: "spec:probe.zeta, spec:probe.alpha, spec:probe.mid"}', 'the serialized belongsTo edges run from {edgeSources: "spec:probe.alpha, spec:probe.mid, spec:probe.zeta"}', 'the reader\'s Pack context lists the members {readerMembers: "spec:probe.zeta, spec:probe.alpha, spec:probe.mid"}', - // re-measured under plan 40 - 'the payload declares the schema version {schemaVersion: "0.8.0"}', + // re-measured under plan 41 + 'the payload declares the schema version {schemaVersion: "0.9.0"}', ], }, ], @@ -810,7 +814,7 @@ export const extractionSpecs = [ id: "spec:extraction.pack-member-order", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", title: "A Pack node keeps its members in authored order", narrative: null, sections: { @@ -844,5 +848,35 @@ export const extractionSpecs = [ }, deliveryFacts: ["implemented", "has-verifier"], file: "specs/extraction/pack-member-order.sdp.md", + }, // The design-management arc (plan 41). + { + id: "spec:extraction.entry-locations", + specKind: "behavior", + altitude: "story", + readiness: "ready", + file: "specs/extraction/entry-locations.sdp.md", + title: "The graph records where each addressable entry is written", + narrative: + "An entry address names one keyed Design or UI entry, or one keyed open question. A reader that follows an address to the text needs the line it sits on, and today only the carrier file knows it, so the first adopter's page parses every member's carrier a second time to find them.", + sections: { + intent: { + problem: + "The graph records the file of a Spec and the line of every anchor, but not the line of an entry inside a Spec, so a page that links an address to its text, or a review that cites one, re-reads carrier syntax the extractor already parsed.", + outcome: + "Record the source line of every keyed Design and UI entry and every open question in the graph, apart from the nodes, so any consumer links an entry to its text without parsing a carrier.", + }, + behavior: { + rules: [ + "The graph carries a location table beside its nodes and edges. Each row names a Spec id, the carrier file, the entry, and the 1-based line the entry starts on in that file. An entry is a `design` or `ui` key other than `description`, written as `design.<key>` or `ui.<key>`, or an open question, written as `question[<index>]` by its position in the Spec's open questions counted from 0, with its key beside it when the question carries one.", + "Primitive and Pack nodes stay free of lines, so a comparison of nodes and edges is unchanged by an edit that only moves an entry, and carrier parity compares nodes and edges and never the location table, whose lines differ by carrier by nature.", + "Every carrier the extractor reads records the lines it can locate: the Markdown carrier records each entry it reifies; the TypeScript carrier records the line of each entry's property; the Gherkin carrier records the open questions it carries. An entry with no row has no recorded line, which a consumer reads as not located and never as absent.", + "Rows sort by Spec id, then by entry, in code-unit order. The table is a deterministic function of the carriers at the extracted commit, as every other part of the graph is.", + "A location names where an entry is written and nothing else: it is not an identity, never an anchor target or a binding, and an edit above an entry moves its line.", + "The reader's Spec context carries the Spec's rows as its entry locations. Address resolution (recipe 25) adds the entry's value, file and line to each resolving row, and entry search (recipe 23) adds the line to each entry it matches.", + "The graph schema version moves to `0.9.0`. The realizing sites are the Markdown, TypeScript and Gherkin extractors in `src/extract/`, the graph schema in `src/graph/schema.ts`, and `specContext` in `src/reader/reader.ts`.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], }, ] as const; diff --git a/test/self-hosting-oracle/index.ts b/test/self-hosting-oracle/index.ts index 85b2cc44..f52c308f 100644 --- a/test/self-hosting-oracle/index.ts +++ b/test/self-hosting-oracle/index.ts @@ -49,47 +49,13 @@ export const expectedSpecs: readonly ExpectedSpec[] = specFamilies.flatMap( (family) => family.specs, ); -// These projections identify the five informative honesty gaps from owner-ratified ready Specs -// without resolving verifier bindings, and the nine informative prose-mention warnings, one per +// These projections identify the one informative honesty gap from owner-ratified ready Specs +// without resolving verifier bindings, and the three informative prose-mention warnings, one per // mentioning and target Spec pair with no declared relation either way. A prose-mention row also // pins its target Spec as relatedId, so two pairs from one Spec differ in the pin. The CLI suite // separately pins each diagnostic's file, validator, and message prefix. -// re-measured under plan 40 +// re-measured under plan 41's fold export const expectedWarnings = [ - { - validatorId: "honesty/gaps", - family: "honesty", - severity: "warning", - subjectId: "spec:carrier.markdown-authoring", - }, - { - validatorId: "conformance/prose-mentions", - family: "conformance", - severity: "warning", - subjectId: "spec:carrier.markdown-body-grammar", - relatedId: "spec:validation.authored-honesty", - }, - { - validatorId: "conformance/prose-mentions", - family: "conformance", - severity: "warning", - subjectId: "spec:carrier.markdown-parser", - relatedId: "spec:carrier.inline-code-spans", - }, - { - validatorId: "conformance/prose-mentions", - family: "conformance", - severity: "warning", - subjectId: "spec:consumers.adopter-on-ramp", - relatedId: "spec:carrier.markdown-body-grammar", - }, - { - validatorId: "conformance/prose-mentions", - family: "conformance", - severity: "warning", - subjectId: "spec:consumers.delivery-session-on-ramp", - relatedId: "spec:decisions.planning-truths-placement", - }, { validatorId: "conformance/prose-mentions", family: "conformance", @@ -97,13 +63,6 @@ export const expectedWarnings = [ subjectId: "spec:consumers.delivery-session-on-ramp", relatedId: "spec:decisions.shipped-projections-frozen", }, - { - validatorId: "conformance/prose-mentions", - family: "conformance", - severity: "warning", - subjectId: "spec:decisions.architectural-significance-rides-primitives", - relatedId: "spec:model.structural-patterns", - }, { validatorId: "conformance/prose-mentions", family: "conformance", @@ -111,13 +70,6 @@ export const expectedWarnings = [ subjectId: "spec:decisions.carrier-ruling", relatedId: "spec:decisions.carrier-universality", }, - { - validatorId: "conformance/prose-mentions", - family: "conformance", - severity: "warning", - subjectId: "spec:decisions.jsdoc-graph-extraction-refused", - relatedId: "spec:model.spec-sections", - }, { validatorId: "conformance/prose-mentions", family: "conformance", @@ -125,24 +77,6 @@ export const expectedWarnings = [ subjectId: "spec:decisions.planning-truths-placement", relatedId: "spec:consumers.impact-graph", }, - { - validatorId: "honesty/gaps", - family: "honesty", - severity: "warning", - subjectId: "spec:extraction.claim-taxonomy", - }, - { - validatorId: "honesty/gaps", - family: "honesty", - severity: "warning", - subjectId: "spec:model.pack-aggregate", - }, - { - validatorId: "honesty/gaps", - family: "honesty", - severity: "warning", - subjectId: "spec:model.relations", - }, { validatorId: "honesty/gaps", family: "honesty", @@ -154,7 +88,12 @@ export const expectedWarnings = [ export type { ExpectedAnchor } from "./anchors.js"; export { expectedAnchors, expectedBindingEdges, expectedCommentAnchorIds } from "./anchors.js"; export { expectedDeclaredRelations } from "./declared-relations.js"; -export { expectedPackMembers, expectedStudioPackMembers, expectedPacks } from "./pack-members.js"; +export { + expectedDesignManagementPackMembers, + expectedPackMembers, + expectedStudioPackMembers, + expectedPacks, +} from "./pack-members.js"; export { acceptedArchitecturalUnits, coarseGrainCoverage, diff --git a/test/self-hosting-oracle/model.ts b/test/self-hosting-oracle/model.ts index 05159c1f..aec67e90 100644 --- a/test/self-hosting-oracle/model.ts +++ b/test/self-hosting-oracle/model.ts @@ -28,7 +28,7 @@ export const modelSpecs = [ id: "spec:model.core-model", specKind: "model", altitude: "feature", - readiness: "defined", + readiness: "ready", file: "specs/model/core-model.sdp.md", title: "The Protocol models delivery with one enrichable Spec", narrative: null, @@ -55,7 +55,7 @@ export const modelSpecs = [ }, }, }, - deliveryFacts: ["implemented"], + deliveryFacts: ["implemented", "has-verifier"], }, { id: "spec:model.spec-sections", @@ -110,6 +110,7 @@ export const modelSpecs = [ question: "After implementation, which design-time detail stays in the Spec and which detail may be removed while preserving one durable home for each explanation?", blocking: true, + key: "detailAfterImplementation", }, ], }, @@ -151,7 +152,7 @@ export const modelSpecs = [ }, }, }, - deliveryFacts: ["implemented"], + deliveryFacts: ["implemented", "has-verifier"], }, { id: "spec:model.stable-ids", @@ -298,7 +299,7 @@ export const modelSpecs = [ }, }, }, - deliveryFacts: ["implemented"], + deliveryFacts: ["implemented", "has-verifier"], }, { id: "spec:model.anchors", @@ -331,11 +332,11 @@ export const modelSpecs = [ "A code anchor with no `satisfies` and no `references`; it mints a CodeNode for structure and for `byFile`, derives no realization edge, and confers nothing.", references: "The anchored CodeNode-to-Spec edge derived from `references?: readonly SpecId[]` on a code anchor: this code answers to that design without claiming to realize it. The unit follows the design or realizes part of it, so a change to the design asks the code to follow. The direction is one way: a design that builds on existing code states that on its own side, by a `dependsOn` or `refines` relation to the Spec the code satisfies, never by a reference from the code. It confers no delivery fact, moves no readiness floor, and the drift alarm ignores it; a target also named in `satisfies` is an error. The reader reports the referencing units on the Spec's context, and blast radius traverses the edge as a binding.", - role: "An optional free string on a code anchor naming the architectural pattern the unit plays; a corpus-owned vocabulary whose taxonomy the census renders with counts, checked against no list.", + role: "An optional one-token value on a code anchor, lowercase kebab and never normalized, naming the architectural pattern the unit plays; a corpus-owned vocabulary whose taxonomy the census renders with counts, checked against no list.", layer: "An optional attribute of a `component:` anchor, one of `edge`, `application`, `domain`, `adapter`, or `infrastructure`; a value outside the set, or the attribute on a non-component anchor, is an envelope error.", context: - "An optional free string on a `component:` anchor naming its bounded context; on a non-component anchor it is an envelope error.", + "An optional one-token value on a `component:` anchor, in the token grammar of a role, naming its bounded context; on a non-component anchor it is an envelope error.", "declared component": "The `component:` anchor is the component's declaration; there is no separate architecture file, because the anchor sits where the component is realized.", "component realization convention": @@ -571,7 +572,7 @@ export const modelSpecs = [ id: "spec:model.open-question-keys", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", title: "An open question may carry a key that addresses it", narrative: null, sections: { @@ -608,5 +609,215 @@ export const modelSpecs = [ }, deliveryFacts: ["implemented", "has-verifier"], file: "specs/model/open-question-keys.sdp.md", + }, // The design-management arc (plan 41): captures ahead of code. + { + id: "spec:model.design-maturity", + specKind: "model", + altitude: "feature", + readiness: "defined", + file: "specs/model/design-maturity.sdp.md", + title: "What a Spec's design carries is derived, beside its readiness", + narrative: + "The original design had seven readiness stages, and its `designed` stage meant the Spec's key abstractions were settled before code (`docs/lineage/v0-design/01-core-primitives.md` §1.4 and §8). The Protocol rules four rungs, and they describe the intent, so readiness alone does not tell a `defined` Spec with no design from a `defined` Spec whose interfaces are settled. When the design-management arc was planned, the owner ruled that it derives design columns beside readiness and adds no rung. Later the owner ruled the rest: what design a Spec carries is read from the derived columns and never stated, so no rung, floor clause or design fact carries it. Readiness stays the author-stated design maturity of the Spec's intent.", + sections: { + intent: { + problem: + "A reviewer or an implementer cannot tell from readiness alone which Specs carry a settled design and which carry only intent, and the first adopter's owner asked for Specs whose key abstractions are designed as stubs before code.", + outcome: + "Show what design a Spec carries in values the graph derives, so the backlog and a review can tell a designed Spec from one that states only its intent.", + assumptions: [ + "The design columns tell a Spec with pinned declarations and keyed entries from one that states only its intent.", + ], + }, + model: { + terms: { + "design columns": + "the counts and lists the reader derives for a Spec beside its readiness: keyed Design and UI entries, pinned declarations, open and blocking questions, and the decisions it names. The Pack design carries them for each member, and the build backlog recipe shows them for each ready Spec no code realizes yet. They are derived on every read and never stated: no readiness rung, floor clause or delivery fact carries the design they show, because a rung would break the four locked readiness values, a floor clause that read Design would close that section's shape, and a design fact would mix design into the facts that say what code and tests bind.", + "designed stage": + "the original design's readiness stage for a Spec whose key abstractions are settled; not a rung of the Protocol. The question reopens if the first adopter's design passes show that the design columns cannot tell a designed Spec from one that states only its intent.", + }, + }, + }, + deliveryFacts: [], + }, + { + id: "spec:model.planned-architecture", + specKind: "model", + altitude: "feature", + readiness: "defined", + file: "specs/model/planned-architecture.sdp.md", + title: "Components are designed ahead of their code", + narrative: + "The original design declared a component before its code, as a Component node with a layer, a bounded context and a capability, kept in an architecture folder and reconciled with the code markers and the Specs (`docs/lineage/v0-design/03-graph-metamodel.md` §2.5; `04-authoring-surfaces.md` §7). Its runtime chapter separated what a unit requires from the provider that supplies it, the provider a composition root selects and the one a test puts in its place, and gave one mechanism the ownership of runtime composition (`05-runtime-anchors.md` §1, §3.3, §7).\n\nThe Protocol records the architecture the code realizes. A `component:` anchor states its layer and context, and anchored `memberOf` and `uses` edges join the code units. Under the architectural annotation decision, a component no code realizes is a Spec, and the graph holds no node for it until an anchor exists. The first adopter states its component list and its context rule in prose (`libar-platform/design/PLAN.md` §2.1) and checks context isolation over anchored `uses` in its own script (`libar-platform/design/tools/check.py`). The owner ruled that the architectural annotation decision stands: a component designed before its code is a component design Spec, and the `component:` anchor that later realizes it satisfies that Spec, which is the join a comparison reads.", + sections: { + intent: { + problem: + "A design ahead of its code cannot state the components it adds, their layer and context and what they use, in a form the graph compares with the components the code later anchors.", + outcome: + "Let a design name the components it will add, with their layer, context and uses, and compare that design with the anchored components once code exists.", + risks: [ + "A planned component kept apart from its realization is a second place for the component to drift, which is why the architectural annotation decision refused a separate architecture file and why the design lives in a Spec the realizing anchor satisfies.", + ], + openQuestions: [ + { + question: + "The original kept apart the interface a unit requires, the providers that supply it, the provider a composition root selects and the one a test substitutes, for Fastify, Effect, Awilix and plain factories alike. Does a component design state these without naming a framework, and does any of them earn graph structure beyond `uses`?", + blocking: false, + key: "providerSelection", + }, + { + question: + "Is the original's one-runtime-truth rule, one composition mechanism per application, a Protocol rule or a corpus's own architecture rule?", + blocking: false, + key: "oneRuntimeOwner", + }, + ], + }, + model: { + terms: { + "component design": + "A Spec that states a component the code does not yet realize and what it is for. Its Design entries are keyed as the anchor's attributes are named: `id` for the planned `component:` anchor's own id, `layer`, `context`, `uses` for the components it will use, and optionally `role`. The keys are a convention a recipe reads; the Protocol closes no Design shape, and the Spec confers nothing and mints no code unit.", + "planned component": + "The component a component design names. It stays intent until a `component:` anchor realizes it, and that anchor satisfies the component design; the graph holds no component node before the anchor exists.", + "realization comparison": + "A catalog recipe that reads each component design beside the `component:` anchor that satisfies it, through the component data recipe 32 joins, and lists a design no anchor satisfies yet, a layer or context that differs, and a `uses` named on one side only. It reports and refuses nothing.", + provider: + "A code unit that supplies an interface another unit requires; a composition selects one provider for each requirement, and a test may select another.", + }, + }, + }, + deliveryFacts: [], + }, + { + id: "spec:model.open-question-fields", + specKind: "rule", + altitude: "story", + readiness: "defined", + file: "specs/model/open-question-fields.sdp.md", + title: "An open question may name who decides it, its links and a provisional reading", + narrative: + "The original design gave an open question an owner and a list of links to decision records, discussions and tickets, beside its text and its blocking flag (`docs/lineage/v0-design/01-core-primitives.md` §2.1). The Protocol gave a question an optional key and an address and stopped there. The first adopter keeps the rest beside the graph. Its decision register (`libar-platform/design/decisions/register.json`, 175 rows, described in `design/advisors/register.md`) records for each open decision its sources, who decides it as a class (`tactical`, `delegated` or `owner`) and an advisor lens, the provisional reading the design proceeds on, which 96 rows carry, and a lean. It also records a status, the work unit it blocks, dates and who checked the lean. This Spec captures the part that states something about the question and leaves the work record outside.", + sections: { + intent: { + problem: + "A question's text and blocking flag do not say who settles it, what it rests on, or what the design assumes until it is settled, so an adopter keeps a register beside the graph that repeats each question.", + outcome: + "Let an open question carry who decides it, the sources it rests on and the reading the design proceeds on, without a status.", + risks: [ + "A field for who decides invites fields for when and for what state the question is in, which would rebuild a workflow tracker inside the Spec.", + ], + openQuestions: [ + { + question: + "May a link name only a Spec id or an entry address, which validation checks, or also an external document, which the `doc:` deferral leaves without an identity?", + blocking: false, + key: "linkTargets", + }, + { + question: + "The adopter's lean has six parts: what to do, what it rests on, the case against it, what it settles, what it changes and what follows if it is wrong. Does any part belong on the question, or is a lean always a draft of a decision Spec?", + blocking: false, + key: "leanHome", + }, + ], + }, + behavior: { + rules: [ + "An open question may carry three optional fields beside its text, flag and key, namely who decides it, the links it rests on and its provisional reading; a question without them stays lawful.", + "Who decides a question names the person, role or body whose ruling settles it. It authorizes nothing, gates no edit, and no validator compares it with the author of a change.", + "Who decides a question is one lowercase kebab token the corpus owns, on the grammar a role uses, such as `owner` or `delegated`. The graph holds no person or role to point at, so the value references nothing, and the Protocol ships no list of values.", + "In the Markdown carrier the three fields ride labelled sub-items indented under the question's own line, `decider:`, `links:` and `provisional:`, each at most once and each one line, so the question line stays as authored. They are the one indented line the body grammar admits, and the TypeScript carrier carries the same three fields on the question object.", + "A link names a Spec id or an entry address the question rests on and resolves as a mention does. A link mints no relation.", + "The provisional reading is one line stating what the design assumes while the question is open. It is intent, never a ruling.", + "An open question carries no status, date, blocked work unit or record of who checked it. A settled question leaves the Spec, a ruling that passes the decision test lands as a decision Spec the shaped Specs name through `decidedBy`, and the rest of the history stays in git and in the adopter's own records.", + "Whether a question holds the readiness floor stays its `[blocking]` flag alone; none of the three fields moves a floor.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:model.rule-keys", + specKind: "rule", + altitude: "story", + readiness: "defined", + file: "specs/model/rule-keys.sdp.md", + title: "A rule or flow entry may carry a key that addresses it", + narrative: + "The original design gave a behavior rule an optional id, so the rule could later be promoted to a Spec of its own, and an optional rationale (`docs/lineage/v0-design/01-core-primitives.md` §2.2). The Protocol kept promotion and dropped the id. An inline rule has no key and no address, so a review finding, a register row or an example cannot name the rule it is about, and the Spec Studio's tests lens cannot name the rules no example exercises until a rule gains an address. Open questions gained optional keys on the Design key grammar; this Spec follows that pattern for rules.", + sections: { + intent: { + problem: + "An inline rule has no identity but its position and its text, so anything outside the Spec that cites one rule breaks silently when the rules above it move or the rule is reworded.", + outcome: + "Let an author key a rule or flow entry, so a review, a register row or an example can name the entry it is about.", + openQuestions: [ + { + question: + "Should an example name the rule it exercises, and where: in its prose, or in a field a rule-coverage view could read?", + blocking: false, + key: "exampleNamesRule", + }, + { + question: + "Does a rule keep the original's rationale, or is the rationale prose the Spec's narrative already owns?", + blocking: false, + key: "ruleRationale", + }, + ], + }, + behavior: { + rules: [ + "Every entry of a Spec's primary behavior owner may carry an optional key on the open-question key grammar: the `rule:` and `flow:` entries of Behavior, the entries of Rule and Contract, and the entries and `rule:` entries of Workflow. One Spec keeps one key namespace across them, unique among its keyed entries, and an unkeyed entry stays lawful and has no address.", + "A key sits in a marker at the start of the entry's text, after the `rule:` or `flow:` label where the entry has one, as in `- rule: [#graphOnly] The Studio is a projection.` and `- [#oneFamily] Every validator belongs to one family.`, the shape an open question's marker uses, so one form serves labelled and plain entries.", + "Every keyed entry is addressed as `spec:<id>#rule.<key>` whatever its section, because a Spec uses one primary behavior owner. The address resolves while the Spec carries an entry with that key and fails validation in every Spec that names it once the key is renamed or removed.", + "A rule address is refused in every id slot, as every entry address is, so no anchor targets a rule and no delivery fact attaches to one.", + "A keyed rule promoted to a Spec of its own leaves its address behind, because promotion moves the content out, and every mention of that address fails until it names the new Spec.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:model.relation-reasons", + specKind: "rule", + altitude: "story", + readiness: "defined", + file: "specs/model/relation-reasons.sdp.md", + title: "An authored relation may carry its reason", + narrative: + "The original design let a relation say why it holds: a rationale on `refines`, a strength on `dependsOn`, an aspect on `constrainedBy` and a reason on `supersedes` (`docs/lineage/v0-design/01-core-primitives.md` §4; `04-authoring-surfaces.md` §1.3). The Protocol's relations carry a type, a target and a claim, and the envelope that holds them is closed, so the reason a Spec rests on another lives, if anywhere, in narrative prose a reader has to find. The prose-ownership decision gives free prose a home on a typed owner and leaves the text on an edge for later; a relation's reason is that text.\n\nThe first adopter meets the reverse case. When its mention audit reported Spec pairs named in prose with no relation behind them, it declared the relations that were missing dependencies and kept twelve pairs as prose, each listed with a one-line reason no typed relation fits (`libar-platform/docs/feedback/sdp-feedback-01.md`, \"The mention warnings were mostly missing edges\"; `libar-platform/design/tools/prose-mentions.json`). A reason on a relation says why an edge holds; a listed mention reason says why none does.", + sections: { + intent: { + problem: + "A reviewer reading a relation cannot see why it holds, and a reviewer reading a mention left as prose cannot tell an intended pointer from a forgotten dependency without a list kept outside the graph.", + outcome: + "Let an author say in one line why a Spec refines, depends on, is constrained by, is decided by or supersedes another, beside the relation itself.", + openQuestions: [ + { + question: + "Should the original's typed attributes return, a strength on `dependsOn` and an aspect on `constrainedBy`, or does one free reason serve every relation type?", + blocking: false, + key: "typedAttributes", + }, + { + question: + "Should a mention left as prose carry its reason in the mentioning Spec, so the mention audit can tell an intended pointer from a missing relation, or does that list stay the adopter's own?", + blocking: false, + key: "mentionReasons", + }, + ], + }, + behavior: { + rules: [ + "An authored relation may carry one optional reason, a single line of prose owned by that relation from that Spec to that target.", + "A reason is intent, as its relation is; it confers no delivery fact, moves no readiness floor and changes no edge, and a relation without one stays lawful.", + "In the envelope a relation target that carries a reason is an object with `target` and `reason` in place of the plain id, and plain ids and objects mix in one relation, so a relation without a reason is written as before and no target is written twice. The TypeScript carrier passes the same reason beside the target.", + "A reason on `supersedes` says what the newer decision replaces, and a reason on a `refines` from one decision to another says which part of the older decision the newer one narrows.", + "A reason reaches readers with its relation through the graph, so a projection or a recipe reads it from the edge and never from the carrier.", + ], + }, + }, + deliveryFacts: [], }, ] as const; diff --git a/test/self-hosting-oracle/observation.ts b/test/self-hosting-oracle/observation.ts index 1b90445c..a5aa49f6 100644 --- a/test/self-hosting-oracle/observation.ts +++ b/test/self-hosting-oracle/observation.ts @@ -19,10 +19,49 @@ export const observationSpecs = [ question: "Which external observation identity and freshness boundary is small enough for the graph while still supporting an honest `observed` delivery fact?", blocking: true, + key: "observationIdentity", }, ], }, }, deliveryFacts: [], }, + // The design-management arc (plan 41): a capture ahead of code. + { + id: "spec:observation.run-evidence", + specKind: "behavior", + altitude: "feature", + readiness: "defined", + file: "specs/observation/run-evidence.sdp.md", + title: "CI run records are joined to verifiers in a view", + narrative: + "The original design ingested test reports into the graph. Each run became a node with its result and duration, and a passing run marked its Spec verified (`docs/lineage/v0-design/06-extraction-and-validation.md` §11.2; `08-delivery-evidence-and-tooling.md` §2, §3.2). The binding-not-liveness decision rules otherwise. `has-verifier` says a resolving verifier binding exists, and pass and fail stay in CI. The vision document says test run verdicts are never ingested (`docs/concept/00-vision-scope-and-mvp-boundary.md`). Read literally, that keeps them out of every view; this Spec reads it as keeping them out of the graph. The runtime overlay holds the other half of the original's evidence layer, runtime observations against measurable targets, which aims at the `observed` fact; run evidence derives no fact at all.\n\nThe first adopter keeps run records anyway, one JSON file per run with its tier, target, commit, whether the tree was clean, and each test's file, name, result and duration (`libar-platform/evidence/runs/`). It asks to see them beside the bindings as a separately identified input, joined by verifier, with commit, tier, composition and target, and a note when the run predates the source being read (`libar-platform/docs/feedback/sdp-feedback-02.md`, item 25). The Spec Studio's verification panels and its tests lens each hold a question on whether to show run results read from CI output; this Spec is the answer they wait on.", + sections: { + intent: { + problem: + "A reader who wants to know whether a bound verifier passed has to leave the Protocol's views for CI, and an adopter that keeps run records has no view that shows them beside the binding without mistaking them for a delivery fact.", + outcome: + "Show a verifier's recorded runs beside its binding in a view, labelled as external input with its commit, while the graph keeps recording only that the binding exists.", + openQuestions: [ + { + question: + "How does a view say a run is stale: by its commit against the commit being read, or by whether the bound files changed between the two?", + blocking: false, + key: "freshness", + }, + ], + }, + behavior: { + rules: [ + "Run records are an input a view reads beside the graph, named by the caller; the graph never holds them, and no node, edge or delivery fact derives from them.", + "A run record joins every verifier whose test anchor sits in the test file the record names, because the file is the binding grain of both anchor forms and every reporter prints it; each test name in the record shows as a row under that verifier. The join is at file grain: a result is evidence from the verifier's file, never the result of that verifier, because a file may hold several suites and a filtered run may exercise only one.", + "The Protocol reads run records in the Common Test Report Format, CTRF, an open JSON schema with reporters for the common test runners, passed to a view as a recipe parameter. A record joins only when its reporter fills CTRF's optional `filePath` field, and an adopter with records of another shape maps them into CTRF itself.", + "A view shows each run beside the verifiers anchored in its file, with its result, its commit and the input it came from, labelled as external run evidence from that file and never as `has-verifier`, `implemented` or a pass verdict for one verifier.", + "A run whose commit differs from the commit being read shows both commits, so a reader sees how old it is.", + "The Design Review stays a function of the graph alone and shows no run; run evidence appears only in a view that admits external input, such as a recipe that takes the records as a parameter, or the Studio.", + ], + }, + }, + deliveryFacts: [], + }, ] as const; diff --git a/test/self-hosting-oracle/pack-members.ts b/test/self-hosting-oracle/pack-members.ts index d563deaf..3f67d0f5 100644 --- a/test/self-hosting-oracle/pack-members.ts +++ b/test/self-hosting-oracle/pack-members.ts @@ -77,6 +77,7 @@ export const expectedPackMembers = [ "spec:validation.authored-honesty", "spec:validation.warn-level-signals", "spec:consumers.projections-model", + "spec:consumers.projections-model.roadmap-views", "spec:consumers.impact-graph", "spec:consumers.agent-surface", "spec:consumers.design-review", @@ -189,12 +190,9 @@ export const expectedPackMembers = [ "spec:decisions.gherkin-carrier-option", "spec:decisions.sdp-gherkin-extension", "spec:decisions.carrier-universality", - "spec:decisions.structural-anchor-semantics", "spec:decisions.adopted-registrars-committed", "spec:decisions.shipped-projections-frozen", "spec:decisions.planning-truths-placement", - "spec:decisions.architectural-significance-rides-primitives", - "spec:decisions.jsdoc-graph-extraction-refused", "spec:decisions.checked-mentions", "spec:decisions.authored-entry-order", "spec:validation.unbound-example-posture.lower-rungs", @@ -227,6 +225,33 @@ export const expectedPackMembers = [ "spec:decisions.studio-web-components", "spec:consumers.spec-studio.responsive", "spec:consumers.spec-studio.sharing", + "spec:decisions.anchor-comment-form", + "spec:decisions.anchor-binding-grain", + "spec:decisions.architectural-annotation", + "spec:decisions.pack-design-page", + "spec:consumers.pack-design", + "spec:consumers.design-review.pack-design-page", + "spec:extraction.entry-locations", + "spec:consumers.agent-surface.recipe-parameters", + "spec:consumers.agent-surface.design-recipes", + "spec:consumers.engine-provenance", + "spec:model.design-maturity", + "spec:consumers.validate-watch", + "spec:consumers.agent-surface.example-space-coverage", + "spec:model.planned-architecture", + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons", + "spec:consumers.review-perspectives", + "spec:consumers.context-bundle", + "spec:consumers.design-diff", + "spec:consumers.intent-composition.proposal", + "spec:consumers.reference-projection", + "spec:consumers.structural-mermaid", + "spec:validation.superseded-decision-signal", + "spec:validation.architecture-constraints", + "spec:validation.corpus-rules", + "spec:observation.run-evidence", ] as const; export const expectedStudioPackMembers = [ @@ -243,6 +268,35 @@ export const expectedStudioPackMembers = [ "spec:consumers.spec-studio.responsive", "spec:consumers.spec-studio.sharing", ] as const; + +// The authored membership of `pack:design-management-v1`, in manifest order. +export const expectedDesignManagementPackMembers = [ + "spec:decisions.pack-design-page", + "spec:consumers.pack-design", + "spec:consumers.design-review.pack-design-page", + "spec:extraction.entry-locations", + "spec:consumers.agent-surface.recipe-parameters", + "spec:consumers.agent-surface.design-recipes", + "spec:consumers.agent-surface.example-space-coverage", + "spec:consumers.engine-provenance", + "spec:consumers.validate-watch", + "spec:model.design-maturity", + "spec:model.planned-architecture", + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons", + "spec:consumers.review-perspectives", + "spec:consumers.context-bundle", + "spec:consumers.design-diff", + "spec:consumers.intent-composition.proposal", + "spec:consumers.reference-projection", + "spec:consumers.structural-mermaid", + "spec:validation.superseded-decision-signal", + "spec:validation.architecture-constraints", + "spec:validation.corpus-rules", + "spec:observation.run-evidence", +] as const; + export const expectedPacks = [ { id: "pack:spec-studio-v1", @@ -255,6 +309,17 @@ export const expectedPacks = [ members: expectedStudioPackMembers, modelRefs: ["spec:consumers.projections-model"], }, + { + id: "pack:design-management-v1", + nodeType: "Pack", + claim: "declared", + title: "Design management", + framing: + "The arc of plan 41, in the order a reviewer reads it: what lets a person working in the repository design a capability ahead of its code and iterate on it. First the Pack page that shows where a Pack's design stands, and the reader assembly it renders; then the graph's record of where each entry is written, the recipe parameters and files, and the recipes design work reads; then the engine's provenance and the authoring watch loop that had no Spec. The captured Specs that follow hold what the original design (`docs/lineage/v0-design/`) promised and the Protocol has not built, each at the rung its structure clears, with the question that holds it there. They run from the model, through the consumers a design pass reads with, to validation and observation.", + file: "specs/design-management.pack.sdp.md", + members: expectedDesignManagementPackMembers, + modelRefs: ["spec:model.design-maturity"], + }, { id: "pack:self-hosting-v1", nodeType: "Pack", diff --git a/test/self-hosting-oracle/structural-edges.ts b/test/self-hosting-oracle/structural-edges.ts index 308c0db1..0970f15e 100644 --- a/test/self-hosting-oracle/structural-edges.ts +++ b/test/self-hosting-oracle/structural-edges.ts @@ -31,6 +31,9 @@ export const structuralMembershipExceptions = [ "impl:protocol.authoring-recipes", "impl:protocol.delivery-session-on-ramp", "impl:protocol.address-and-cycle-recipes", + "impl:protocol.recipe-parameters-catalog", + "impl:protocol.design-recipes", + "impl:protocol.entry-location-recipes", ] as const; export const acceptedArchitecturalUnits = [ @@ -469,9 +472,50 @@ export const acceptedArchitecturalUnits = [ anchorId: "impl:protocol.next-rung-floor", componentId: "component:protocol.validate", }, + // The design-management arc (plan 41): the engine units it added, each with its component. + { + unit: "src/extract/derive.ts#deriveEntryLocations", + anchorId: "impl:protocol.entry-locations", + componentId: "component:protocol.extract", + }, + { + unit: "src/reader/reader.ts#packMemberDesign", + anchorId: "impl:protocol.pack-design", + componentId: "component:protocol.reader", + }, + { + unit: "src/projections/design-review-pages.ts#renderPackPage", + anchorId: "impl:protocol.pack-design-page", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/design-review-pack-sections.ts#renderNextRungSection", + anchorId: "impl:protocol.pack-design-sections", + componentId: "component:protocol.projections", + }, + { + unit: "src/cli/q-command.ts#readParams", + anchorId: "impl:protocol.query-params", + componentId: "component:protocol.cli", + }, + { + unit: "src/cli/sdp.ts#runSdpCli", + anchorId: "impl:protocol.engine-provenance-cli", + componentId: "component:protocol.cli", + }, + { + unit: "src/cli/build-artifacts.ts#writeBuildArtifacts", + anchorId: "impl:protocol.build-artifacts", + componentId: "component:protocol.cli", + }, + { + unit: "src/cli/validate-watch.ts#runValidateWatch", + anchorId: "impl:protocol.validate-watch", + componentId: "component:protocol.cli", + }, ] as const; -// re-measured under plan 40 +// re-measured under plan 41 export const expectedMemberOfEdges = acceptedArchitecturalUnits.map((row) => [ row.anchorId, row.componentId, @@ -479,13 +523,9 @@ export const expectedMemberOfEdges = acceptedArchitecturalUnits.map((row) => [ // Helpers with no honest satisfies target of their own. Coverage rides the nearest honest // realization that consumes them; these rows never mint an anchor. +// re-measured under plan 41: runValidateWatch left this list when it gained its own anchor, +// impl:protocol.validate-watch, and joined the accepted units above. export const coarseGrainCoverage = [ - { - unit: "src/cli/validate-watch.ts#runValidateWatch", - coveredBy: "impl:protocol.agent-surface-cli", - componentId: "component:protocol.cli", - rationale: "src/cli/sdp.ts#runSdpCli value-calls runValidateWatch", - }, { unit: "src/import/markdown-fidelity.ts#assertMarkdownEmissionFidelity", coveredBy: "impl:protocol.sdp-import-markdown-emit", @@ -522,12 +562,13 @@ export const coarseGrainCoverage = [ // A uses edge tracks real imports (value or type) from another component's source files; imports // that exist only to author the anchors themselves (the stable-id and anchor-builder modules) // confer no edge. The convention is stated in spec:protocol.structural-self-binding. -// re-measured under plan 40 +// re-measured under plan 41 export const expectedUsesEdges = [ ["impl:protocol.example-runner-adapter", "impl:protocol.example-runner"], ["impl:protocol.authored-honesty-delivery-facts", "impl:protocol.delivery-facts"], ["impl:protocol.reader", "impl:protocol.delivery-facts"], ["impl:protocol.derive-graph", "impl:protocol.delivery-facts"], + ["impl:protocol.reader", "impl:protocol.next-rung-floor"], ["component:protocol.adapters", "component:protocol.runner"], ["component:protocol.cli", "component:protocol.codegen"], ["component:protocol.cli", "component:protocol.extract"], diff --git a/test/self-hosting-oracle/validation.ts b/test/self-hosting-oracle/validation.ts index e80adff6..58448df3 100644 --- a/test/self-hosting-oracle/validation.ts +++ b/test/self-hosting-oracle/validation.ts @@ -19,6 +19,7 @@ export const validationSpecs = [ question: "Does any remaining law in the src/validate/readiness-floor.ts file header promote here under comment promotion?", blocking: false, + key: "headerLawPromotion", }, ], }, @@ -329,16 +330,17 @@ export const validationSpecs = [ question: "Does the one-validation-path registry law stated in the src/validate/validators.ts file header promote here or to a story-altitude child under comment promotion?", blocking: false, + key: "registryLawPromotion", }, ], }, 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.", + "Every validator the Protocol ships belongs to either the conformance family, which checks meta-model well-formedness, or the honesty family, which rejects authored or overstated derived truth; a corpus rule run through `sdp validate --rules` belongs to neither and reports in the corpus's own section.", + "Errors from the Protocol's validators 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 two families are load-bearing, so an aggregate report spanning both states no family of its own while every finding of the Protocol's own validators names the family it came from.", "The realizing entrypoints are `graphValidatorIds` and `validateGraph` in `src/validate/validators.ts`.", ], exampleSpace: { @@ -1164,7 +1166,7 @@ export const validationSpecs = [ id: "spec:validation.typed-dependency-floor", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/validation/typed-dependency-floor.sdp.md", title: "A ready Spec rests only on settled dependencies", narrative: null, @@ -1201,7 +1203,7 @@ export const validationSpecs = [ id: "spec:validation.unbound-example-posture", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/validation/unbound-example-posture.sdp.md", title: "An unbound example below ready is data, not a warning", narrative: null, @@ -1236,7 +1238,7 @@ export const validationSpecs = [ id: "spec:validation.prose-mentions", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", file: "specs/validation/prose-mentions.sdp.md", title: "Every Spec id written in prose resolves", narrative: null, @@ -1523,7 +1525,7 @@ export const validationSpecs = [ id: "spec:validation.next-rung-floor", specKind: "rule", altitude: "story", - readiness: "defined", + readiness: "ready", title: "The floor names what blocks the next rung", narrative: null, sections: { @@ -1596,4 +1598,117 @@ export const validationSpecs = [ deliveryFacts: ["has-verifier"], file: "specs/validation/next-rung-floor.typed-dependency-targets.sdp.md", }, + // The design-management arc (plan 41): captures ahead of code. + { + id: "spec:validation.superseded-decision-signal", + specKind: "rule", + altitude: "story", + readiness: "defined", + file: "specs/validation/superseded-decision-signal.sdp.md", + title: "A Spec shaped by a superseded decision warns", + narrative: + "The original design checked lifecycle integrity. It flagged a superseded Spec that live Specs still depend on, and a deprecated decision still named by `decidedBy` (`docs/lineage/v0-design/06-extraction-and-validation.md` §6.3.4). The Protocol keeps decision lineage in the graph, as `supersedes` edges between decision Specs and `decidedBy` edges from the Specs they shape, but no check reads the two together. When this arc began, the corpus showed the cost. The Spec Studio's lenses Spec still named a decision the architectural annotation decision had superseded, and its architecture question still read the old ruling after the new one admitted layer and context.\n\nThe rule needed one ruling first. Three decisions once declared `supersedes` on the projections freeze while each replaced only one part of it, so a signal reading every `supersedes` as a whole replacement would have warned on the consumer Specs the freeze still correctly shapes. The owner ruled that `supersedes` replaces a whole decision and that a decision narrowing one part of another `refines` it; the three records now refine the freeze.", + sections: { + intent: { + problem: + "A Spec can stay shaped by a decision that a later decision replaced, and nothing tells its author that the ruling it rests on has moved.", + outcome: + "Surface a live Spec that is still shaped by a decision another decision has superseded, as an informative warning.", + openQuestions: [ + { + question: + "Does the signal read only `decidedBy`, or also a `refines` or `dependsOn` that names a superseded decision?", + blocking: false, + key: "signalReach", + }, + { + question: + "When a superseding decision is itself superseded, does the warning name the decision at the end of the chain?", + blocking: false, + key: "supersededChain", + }, + ], + }, + behavior: { + rules: [ + "A `supersedes` edge replaces the decision it targets whole; a decision that narrows part of another `refines` it instead, and the signal never reads a `refines` between decisions.", + "A replaced decision whose text no reader still needs is deleted rather than kept beside a `supersedes` edge, and a Spec still naming it then fails as an unresolved target, so the signal covers only replaced decisions kept as history.", + "A Spec that names a decision through `decidedBy`, where another decision `supersedes` that decision, carries one warning for each such pair, naming the superseded decision and every decision that supersedes it.", + "The warning is informative and never fails validation, as the orphan and gap signals never do, and its severity is fixed by the Protocol.", + "The `supersedes` edge itself is never warned, and a superseded decision is never warned for being superseded.", + "The signal reads the authored edges of the one graph and nothing else; it judges no content and never asks whether the newer ruling fits the Spec.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:validation.architecture-constraints", + specKind: "rule", + altitude: "feature", + readiness: "defined", + file: "specs/validation/architecture-constraints.sdp.md", + title: "Declared architecture rules are checked against anchored structure", + narrative: + "The original design gave architecture a validation tier of its own. Its rules said that only adapter components depend on external systems, that domain code imports no infrastructure and that layers depend in one direction; dependency-cruiser, architecture tests and lint rules ran them, and their findings joined the one report (`docs/lineage/v0-design/06-extraction-and-validation.md` §6.4). Since the architectural annotation decision, the data those rules read is in the graph with no inference: a component anchor states its layer and its context, and `uses` and `memberOf` edges are anchored. The same decision keeps the check out of the Protocol. A negative architecture constraint stays declared intent in a Spec, and a corpus that wants it enforced checks its declared `uses`, `layer` and `context` in its own gate.\n\nThe first adopter needed one such rule as soon as the attributes landed. No component in its `platform` context may have a `uses` edge to a component in another context, with each end resolved to its component through `memberOf` first. It checks the rule in its own script (`libar-platform/design/tools/check.py`, the context rule of `libar-platform/design/PLAN.md` §2.1). Recipe 32, architecture crossings, reports every `uses` edge whose ends sit in different contexts or layers and refuses nothing. The owner ruled that the architectural annotation decision stands: an architecture rule stays the corpus's own and runs as one of its corpus rules, and the Protocol supplies the walk the adopter writes by hand today.", + sections: { + intent: { + problem: + "A corpus that states an architecture rule in a Spec cannot have it checked by the Protocol, so each adopter writes the same walk over `uses`, `memberOf`, `layer` and `context` in its own gate.", + outcome: + "Check a corpus's declared architecture rules, such as a forbidden uses direction or context isolation, against the layers, contexts and uses its anchors state.", + openQuestions: [ + { + question: + "Which rule forms cover the need: a forbidden `uses` from one layer to another, an isolated context, a component that must state a layer? The original's list also held rules on ports and routes, which have no anchors here.", + blocking: false, + key: "ruleForms", + }, + ], + }, + behavior: { + rules: [ + "The Protocol's validators never judge a corpus's architecture; an architecture rule runs as one of the corpus's own rules, the corpus's gate decides what fails, and that gate may be the corpus's own invocation of `sdp validate --rules` with the severity the rule states.", + "A corpus states each architecture rule as intent in a `constraint` Spec that says what is forbidden and why, and keeps the rule's machine form, its forbidden layer pairs and isolated contexts, as data its corpus rule passes to the walk.", + "The Protocol supplies the walk as a catalog recipe that takes forbidden layer pairs and isolated contexts as recipe parameters, resolves each end of every anchored `uses` edge to its component, directly or through `memberOf`, and returns each edge a rule forbids with both units, both components and the rule.", + "The walk reads the given rules and anchored structure only; it infers no edge from imports, reads no file path, judges no role and refuses nothing.", + "A component that states no layer or context falls outside a rule that names one, and the walk reports it as unclassified rather than as a breach.", + "The corpus binds its rule file to the constraint Spec as that Spec's verifier through a test anchor, so `has-verifier` says a check for the constraint exists; pass and fail stay in the corpus's own run.", + ], + }, + }, + deliveryFacts: [], + }, + { + id: "spec:validation.corpus-rules", + specKind: "behavior", + altitude: "feature", + readiness: "defined", + file: "specs/validation/corpus-rules.sdp.md", + title: "Corpus-authored rules run on the one validation path", + narrative: + "The original design let a team add its own rules beside the built-in validators, each with an id, a severity and the whole graph query API, run in the same pass and reported in the same output (`docs/lineage/v0-design/06-extraction-and-validation.md` §6.5). The Protocol has two check families, conformance and honesty, and every validator belongs to one of them. The architectural annotation decision sends a corpus's own rules to the corpus's own gate.\n\nThe first adopter's gate shows what that costs. Its 363-line check script runs `sdp validate` and parses the summary line, then holds about ten rules of its own: context isolation over declared `uses`, entry addresses written outside the Specs resolved through recipe 25, no `dependsOn` cycle through recipe 26, every open question keyed, a prose mention allowed only with a listed reason, cited laws backed by `constrainedBy`, and the Pack pages current. To read kind, readiness and relations it parses the carriers' frontmatter with regular expressions, the second read model the agent surface warns against (`libar-platform/design/tools/check.py`).", + sections: { + intent: { + problem: + "A corpus with rules of its own checks them off the one validation path, reading the carriers a second way and reporting in a form no Protocol surface reads.", + outcome: + "Let a corpus author its own graph rules and have their findings join the one validation report, so no corpus re-parses its carriers to check itself.", + }, + behavior: { + rules: [ + "A corpus rule is a graph body the corpus authors, given the bindings a recipe body gets, the reader, the raw graph and the report, and returning findings that each carry a rule id, a subject, a message and a severity.", + "A corpus rule reads the one derived graph and nothing else, so no rule reads a carrier, a source file or the network.", + "Each corpus finding names its rule and its corpus as its source and stands apart from the Protocol's own findings; the Protocol states none of these rules and ships none.", + "Corpus findings stand outside the conformance and honesty families, in a section of the report labelled as the corpus's own, so the two families keep their meaning and the guardrail that checks never judge content quality or workflow keeps binding the Protocol's own validators.", + "Validation runs corpus rules only from a file the operator names with `sdp validate --rules PATH`, which may repeat, and never from a file it finds in the corpus or in a configuration; it asks the trust `sdp q` already asks, to run code the operator chose.", + "A corpus finding carries severity `error` or `warning`. When the operator names `--rules`, an `error` corpus finding makes `sdp validate` exit nonzero and a `warning` stays informative; without `--rules` no corpus rule runs and only the Protocol's validators set the exit code.", + "Corpus findings keep their own source and report section, are tallied apart from the Protocol's summary line, and neither add to, suppress nor change a Protocol finding or the exit reason it sets.", + "A rules file that cannot load, a rule that throws, or a rule that returns a finding without a rule id, subject, message and admitted severity is a rule execution failure; `sdp validate` reports it in the corpus section and exits nonzero, so a broken rule never passes as a clean run.", + "A corpus rule changes no node, edge or delivery fact; it reports on the graph it is given.", + ], + }, + }, + deliveryFacts: [], + }, ] as const; diff --git a/test/skills.test.ts b/test/skills.test.ts index c8e907c0..d4ad6d6c 100644 --- a/test/skills.test.ts +++ b/test/skills.test.ts @@ -361,6 +361,10 @@ describe("Protocol skill assets", () => { "twenty-six", "twenty-seven", "twenty-eight", + "twenty-nine", + "thirty", + "thirty-one", + "thirty-two", ] as const; const countWord = countWords[headings.length]; diff --git a/tsup.config.ts b/tsup.config.ts index 68aa6f79..68f1048b 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,8 +1,15 @@ import { defineConfig } from "tsup"; +import { RECIPES_DIRECTORY, writeBuildArtifacts } from "./src/cli/build-artifacts.js"; +import { BUILD_INFO_FILE } from "./src/cli/build-info.js"; + // The CLI entry (`src/cli/sdp.ts`) carries its own source shebang; esbuild preserves a leading // shebang on an entry point, so `dist/cli/sdp.js` stays executable while `dist/index.js` (the // library entry, no shebang) stays clean. No banner / strip-plugin / post-build normalisation. +// +// After the bundle succeeds, the build step writes each catalog recipe to `dist/recipes/` and the +// build commit to `dist/cli/build-info.json`. The clean keeps both, because the step replaces each +// file in one rename: a test reading them while another build runs never finds them missing. export default defineConfig([ { entry: [ @@ -17,9 +24,13 @@ export default defineConfig([ dts: true, platform: "node", target: "es2022", - clean: true, + clean: [`!${RECIPES_DIRECTORY}/**`, `!cli/${BUILD_INFO_FILE}`], splitting: false, outDir: "dist", + onSuccess: () => { + writeBuildArtifacts({ root: process.cwd(), outDir: "dist" }); + return Promise.resolve(); + }, }, { entry: ["src/index.ts"],