diff --git a/.agents/skills/sdp-agent-surface/SKILL.md b/.agents/skills/sdp-agent-surface/SKILL.md index 6f4fe46fd..1bc9a2626 100644 --- a/.agents/skills/sdp-agent-surface/SKILL.md +++ b/.agents/skills/sdp-agent-surface/SKILL.md @@ -70,6 +70,7 @@ pnpm exec sdp q "$(cat node_modules/@libar-dev/software-delivery-protocol/dist/r | Compare stated readiness with the floor | readiness divergence (recipe 7) | | Read warnings, gaps and verifier health | orphans and gaps (recipe 8); declared versus enabled verifiers (recipe 10) | | Review a Pack | the Pack review backbone (recipe 5), Pack design (recipe 29), references into a design (recipe 27) | +| Read a Pack or a set of Specs as one design | the design slice (recipe 54) | | List open questions | the open-question register (recipe 20) | | See what a Spec rests on | dependency footing (recipe 21); dependency cycles (recipe 26) | | Change a Spec others rest on | design-change impact (recipe 30); the planning slice (recipe 19) | @@ -97,6 +98,9 @@ pnpm exec sdp q "$(cat node_modules/@libar-dev/software-delivery-protocol/dist/r | Propose rule keys for review | the rule-key proposal (recipe 48), then the proposal check (recipe 38) | | Check prose tokens against an outside register | external registers (recipe 50) | | Find source and test lines citing an entry | entry citations (recipe 51) | +| Read epic scopes and direct binding evidence | the capability map (recipe 52) | +| Read upcoming work in the owner's order | the roadmap (recipe 53) | +| Render a description's forward section | the roadmap memo (recipe 55) | Choose a recipe from the task sheet, then read its numbered catalog entry for parameters, defaults, diagnostics and answer fields. Run the shipped body unchanged, or call @@ -178,7 +182,8 @@ and column. A recipe that takes `params.detail` returns a summary by default: every total, ids in place of rows, and `leftOut` naming what `detail: true` adds (changed-file impact, recipe 4; Pack design, -recipe 29; the design diff, recipe 35). To filter any shipped answer, run it inside your own body: +recipe 29; the design diff, recipe 35; the design slice, recipe 54). +To filter any shipped answer, run it inside your own body: ```sh pnpm --silent sdp:q 'const answer = await recipes(29, { pack: params.pack }); return answer.members.filter((member) => member.blockingQuestions > 0)' --params '{"pack":"pack:design-iteration-v1"}' --json @@ -252,8 +257,9 @@ binding target. Four node types exist: its title, narrative, reified section content, and the derived `deliveryFacts`. Use case, NFR, decision record, epic, and story are coordinates on this one type, never separate node types. - `Pack` — the review grouping, carrying its title, framing prose, ordered `members` as authored, - and `modelRefs`. `belongsTo` edges re-express membership in the global edge order; read - authored reading order from `members`. A Pack states no system truth. + optional `order`, and `modelRefs`. `belongsTo` edges re-express membership in the global edge + order; read authored order from `members`, advisory priority when `order` is `priority` and + reading order otherwise. A Pack states no system truth. - `Anchor` — a test or oracle binding, with the `file` and `line` of the binding itself. - `CodeNode` — the code unit a code anchor mints, from either form: a `codeAnchor` constant or a comment-form block whose id sits in the `impl:`, `api:`, or `component:` namespace. It carries diff --git a/.agents/skills/sdp-authoring/SKILL.md b/.agents/skills/sdp-authoring/SKILL.md index 761e1017f..460f65992 100644 --- a/.agents/skills/sdp-authoring/SKILL.md +++ b/.agents/skills/sdp-authoring/SKILL.md @@ -205,8 +205,8 @@ so it needs no extra tag, status key, or side table. and a change's review scope from changed-file blast radius (recipe 4). In prose, name the recipe and leave the number out. -- Group a body of work in a Pack named for its capability, never its plan number; keep its Specs in reading order. Membership confers no readiness, delivery fact or gate; the Pack remains a review aggregate after the work ends. -- A plan file points to the Pack and carrying Specs for intended truth and derived state. It may keep an advisory execution and recovery record; plan numbering, recorded work order and staleness confer no graph structure or workflow authority. +- Group a body of work in a Pack named for its capability, never a number or a date; keep its Specs in reading order. Membership confers no readiness, delivery fact or gate; the Pack remains a review aggregate after the work ends. +- Hold advisory priority over the next work in a roadmap, a Pack whose manifest states `order: priority`, and read it with recipe 53; its order confers nothing. Keep an arc's execution record in git and its pull request description. A corpus that keeps plan documents of its own lets them point to Packs and carrying Specs; they confer nothing. ### Say why a relation holds @@ -251,6 +251,54 @@ and a reviewer. the bound code may now carry. Compare each with the code; removing one is your edit, never the recipe's. +### Design in rounds + +A design grows in rounds over the Specs that must be reviewed together, never one Spec at a time +to completion. Open each round with the design slice (recipe 54) over the Pack, or over one +perspective's members, and spend detail where the next round or the next build needs it, first +on the members others rest on. + +What a rung's design typically settles: + +- `idea`: the outcome, and the parent it refines when it has one. Capture the questions you + already see; rules and Design entries wait. +- `scoped`: the actor and the problem, the relations to what it rests on, the first rules or + examples, and its open questions with keys, deciders and provisional readings. A decision it + needs is a decision Spec it names through `decidedBy`. +- `defined`: every behavior stated as a rule, keyed where something outside cites it; every + blocking question answered or moved to the decision Spec that settles it; the names, shapes and + messages two parties share as keyed Design entries, pinned when they are declarations; a + component design for each component the code does not hold yet; and the example space when + examples will exercise the rules. A deferral stays a blocking question with its trigger and + keeps the Spec below `defined`. +- `ready`: no new detail. What it rests on states `defined`, a person reads it in its slice, and + that person states it. + +What stays out, because it belongs to the build: + +- a detail only the builder reads: private names, internal decomposition, file layout, the data + structure behind a stated shape, the algorithm behind a rule that already states its outcome; +- a question only the builder can answer: leave it to the build, or record it `[non-blocking]` + with `decider: delegated` and a provisional reading, so it holds no rung. + +A detail belongs in the Spec when a second party must agree on it: a test author, a reviewer, +another Spec's author or an adopter. Otherwise the builder decides it and the code holds it. + +Read the slice to see both: + +- Read members in depth order, the ones others rest on first: a member others rest on needs its + shared names settled before they can settle theirs. +- Read the design columns beside each rung. Pinned declarations on a member stated `idea` or + `scoped` may run ahead of intent no one has settled; a member stated `defined` that others rest + on, with no keyed entries where they need its names, may leave them guessing. Both are + questions for the author, never findings. +- A blocking question in scope that only the builder can answer belongs to the build. +- The architecture it touches names the code the design builds on through what it rests on; that + code's owners read the slice too. + +No check reads this guidance; how much design is enough stays the author's and the reviewer's +judgment. + ### Key a step or a term To keep a step's binding through later rewording, add its key after the keyword in the parent's @@ -283,9 +331,11 @@ fields, or a relation reason; a Spec that needs one is authored in Markdown. ## Author a Pack -Use a Markdown `*.pack.sdp.md` manifest with frontmatter closed to `id`, `specs`, and optional -`modelRefs`. The H1 is the Pack title; the remaining body prose is its framing. Preserve authored -membership order and point to `spec:carrier.markdown-pack-authoring` for the complete carrier law. +Use a Markdown `*.pack.sdp.md` manifest with frontmatter closed to `id`, `specs`, optional +`modelRefs`, and optional `order`, whose one lawful value is `priority`. The H1 is the Pack title; +the remaining body prose is its framing. Without `order`, membership is reading order; `order: priority` states advisory priority, first +entry first, without a readiness effect or gate. Preserve authored membership order and point to +`spec:carrier.markdown-pack-authoring` for the complete carrier law. ## Bind code, tests, and oracles @@ -451,8 +501,9 @@ follows. The law is `spec:decisions.anchor-binding-grain`. non-empty and unique, and structural self-reference is refused. Multi-node `uses` cycles remain data, never findings. There is no `implements` field; contract realization stays `satisfies`. - `role` names the architectural pattern the unit plays, on any code anchor. The corpus owns the - vocabulary: `service`, `decider`, `projection`, `read-model`, `codec`, `contract`, `barrel`, and - `utility` are common values, not a closed list. A value is one lowercase kebab token matching + vocabulary; run recipe 28 to reuse a value. Common roles include `service`, `decider` and + `projection`; the Protocol's own engine roles (`spec:protocol.role-vocabulary`) are a worked + example, not a list. A value is one lowercase kebab token matching `^[a-z][a-z0-9-]*$` and is never normalized, so `readModel` is refused rather than folded into `read-model`. - `layer` goes on a `component:` anchor only and takes one of five values: @@ -475,13 +526,27 @@ so map packages to components by review rather than one component per package. R ### Curate the anchors -Anchors are curated, never a coverage quota. Anchor the units that carry the architecture, its -exported public surface and its cross-component reach, and leave the rest bare. An identity has -one owner, so an identity that would stand for several files becomes several identities; a site -that realizes several Specs may carry one identity per Spec, each labeled with its own local how. -Each identity chooses `satisfies` or `references` for what its own code does. -A realizing unit's comment documents its local how, never a paraphrase of the Spec's what or why; -the Spec already owns those, and a restatement in code is a second owner that drifts. +A significant unit owns a contract or responsibility a designer or builder must understand and +has exported public surface or cross-component reach; export status or import rank alone is +insufficient. Bind ports, shared shapes, pipeline stages, decision tables, catalogs, layout +policies and adapters when they meet that criterion. Mechanical wrappers, forwarding barrels, +empty registrations and formatting helpers stay bare unless they own an independent contract or +architectural responsibility. Keep the unit's responsibility in its label and the component's +reading guidance in its anchor. A `uses` declaration needs a real value or type import; +anchor-authoring imports and unused re-exports confer none. Run the curation report (recipe 40) +for advice, never a coverage quota; the Protocol's own reviewed boundary, +`spec:protocol.significant-units-review`, is a worked example. + +An identity has one file owner. Several Specs realized by one responsibility normally share an +anchor with plural targets; independent contracts, entry points and semantic policies in one file +may keep distinct identities when their separate design use is clear. Before merging or dropping +an identity, list every promise its targets carry and name the surviving units that still realize +it. Transfer targets only when that set remains true, or explicitly record a false claim being +removed. Review imported value and type contracts when declaring component uses, exclude imports +made only to author anchors and unused re-export reach, and inspect external-input omissions and +unassigned files before deleting an edge. Each identity chooses `satisfies` or `references` for +what its own code does. A realizing unit documents its local how, never a paraphrase of the Spec's +what or why. A Markdown deliverable cannot carry an in-code anchor. Bind it through the document-realization convention: the test suite that asserts the shipped document carries the code anchor, its label diff --git a/.agents/skills/sdp-sessions/SKILL.md b/.agents/skills/sdp-sessions/SKILL.md index 6bb815fc6..4859b084b 100644 --- a/.agents/skills/sdp-sessions/SKILL.md +++ b/.agents/skills/sdp-sessions/SKILL.md @@ -62,6 +62,9 @@ addresses. ### Design +Read the roadmap (recipe 53) for the next design work and the capability map (recipe 52) to +choose an epic's refinement scope and inspect its direct binding evidence; check `damage`, `cycles` and summary omissions before interpreting the answer. + Use promotion preflight (recipe 9) on the target; the planning slice (recipe 19), dependency footing (recipe 21) and dependency cycles (recipe 26) for what it rests on; and readiness divergence (recipe 7) across the corpus. Resolve the target's blocking open questions and review @@ -69,9 +72,11 @@ the carrying Specs. The open-question register (recipe 20) lists the neighbors' statement queue (recipe 46) orders waiting readiness statements and the selected decider's questions; it states no rung. A clear floor is evidence, not an automatic `ready` statement. Pinned declarations (recipe 24) and the key-family register (recipe 41) list the shapes a builder -and a test author share. For a Pack, read Pack design (recipe 29). Before changing a Spec others -rest on, run design-change impact (recipe 30). Follow `sdp-authoring` for capability-based Pack -names and plan pointers. +and a test author share. For a Pack, read Pack design (recipe 29) member by member and the design +slice (recipe 54) as one whole: how the members rest on each other, the architecture they touch, +and the decisions and questions in scope. Before changing a Spec others rest on, run +design-change impact (recipe 30). Follow `sdp-authoring` for capability-based Pack +names and advisory priority. A design pass over a Pack runs in a loop: @@ -92,7 +97,8 @@ show what the next pass could take up. ### Implement -Use the build backlog (recipe 1) to orient the available ready work and the target Spec context +Read the roadmap (recipe 53) for the owner's order and each entry's need; render the roadmap memo +(recipe 55) for a pull request description's forward section. Use the build backlog (recipe 1) to orient the available ready work and the target Spec context (recipe 3) to read guarantees, relations, implementation bindings, and verifiers. Inspect the planning slice (recipe 19) before writing code. In a declared seam, read component membership (recipe 12) and uses fan-in and fan-out (recipe 13). When the code answers to a Pack's design, read @@ -126,7 +132,17 @@ A corpus may select the rules-capable recipes as rules files under `sdp validate `sdp-authoring`'s "Write and run corpus rules". Review findings and gaps as data; the review never becomes a workflow gate. -Report a review as a numbered list, most severe first. Each item names one entry address or Spec +Findings against Specs travel as one change proposal: use the branch-head commit the reviewer +read as its base; a proposal composed in a preview names the stamp's merge commit. Recipe 38 +always reads the graph of the commit that was read. Check the proposal with recipe 38, and post the +proposal memo (recipe 49) to the pull request. Read moved, unresolved or incomparable scopes +again before applying them. Findings against code or other files are review comments on their +lines; a review proposing no Spec edit has no proposal. An open design finding becomes an open question with +`decider`, `provisional` and `links`; a code defect against settled intent stays a code finding. +The applying commit names the proposal's reason and base. The Protocol keeps its record in git +and the draft pull request; an adopter chooses its own record convention. + +Report a Spec proposal as a numbered list, most severe first. Each item names one entry address or Spec section, states the gap in one sentence, and gives the exact text proposed with the address it replaces or follows. That is the shape a change proposal's changes take, so the list becomes a proposal without rewriting. diff --git a/.agents/skills/sdp-studio/SKILL.md b/.agents/skills/sdp-studio/SKILL.md index d219cc9d5..b0140394e 100644 --- a/.agents/skills/sdp-studio/SKILL.md +++ b/.agents/skills/sdp-studio/SKILL.md @@ -121,9 +121,12 @@ available on the runner, and `pnpm exec sdp`. | The task | Open | | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | See where the corpus stands, narrow the Specs, read the findings | `index.html`, the Packs lens | +| See the epics, what each holds and what no epic reaches | `capabilities.html`, the capabilities lens (recipe 52) | | Find a Spec, an entry or a bound file by a word | `search.html`, which needs the script | | Read a Pack's design member by member | `packs/.html` | +| Read a Pack as one design: depths, footing, the architecture it touches | `slices/.html`, the Pack's design slice (recipe 54) | | Read one Spec in context | `specs/.html` | +| See what comes next and what each roadmap entry needs | `roadmap.html`, every roadmap Pack with its memo (recipes 53 and 55) | | Order the waiting statements and the owner's questions | `queue.html`, the statement queue (recipe 46) | | List open questions, decisions, pinned declarations and the design backlog | `questions.html`, `decisions.html`, `declarations.html` with the source lines citing each, `backlog.html` | | Compare designed components with built ones | `architecture.html` | @@ -137,9 +140,10 @@ A page's `` is the id after its namespace, each uppercase letter written a lowercase (`spec:consumers.spec-studio.publisher#rule.pageSet`). Read what a page states and no more. The queue's order sets no priority, and no waiting -statement is approved. A run row is external evidence from a test file, never a verdict on a -verifier. A binding shown as present says code or a test is bound, not that it works. A diagram -over its bound folds, or leaves only its text equivalent, which states every box and edge +statement is approved. A roadmap's order is advisory priority, never a schedule or a gate. A run +row is external evidence from a test file, never a verdict on a verifier. A binding shown as +present says code or a test is bound, not that it works. A diagram over its bound folds, or +leaves only its text equivalent, which states every box and edge (`spec:consumers.spec-studio.diagrams#rule.textEquivalent`). ### Export and print diff --git a/AGENTS.md b/AGENTS.md index 64c7f63f0..1af0c26dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,86 +16,14 @@ 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 46 is EXECUTED**, as PR 35 on `feature/studio-and-adoption-v0`. Designs people can read, -> and an adopter at full speed: the Studio's phase 1 (diagrams, every Spec-page panel, the registers and -> the owner's queue as pages, the design diff, run evidence, the composing panel, previews per -> pull request), the vault, and the adopter's loop at scale (recipes called from rules files, step -> keys, term addresses, checked declarations, bundles in parts, the readiness planner, the upgrade -> kit). The rulings were made on the owner's delegation and the owner confirmed them; the owner -> ratified the Studio's two decisions and the vault's (MD-43 to MD-45) and turned on the per-pull- -> request preview. The plan lists what stays the owner's: the platform's statements and the merges. -> **plan 45 is EXECUTED**, merged as PR 33. Design at the adopter's -> scale: the adopter installs from a commit, the design-pass loop fits a large corpus -> (`declarations: "addresses"`, recipe 41), corpus rules run under `sdp validate --rules` with -> recipes 42 and 43, and the Spec Studio's phase 0 publishes static pages (`sdp studio`; plan 42 -> was its brief). The first adopter moved onto it in its own branch. The rulings were delegated -> and await the owner's review; the plan lists them, the acceptance as measured, and what waits. -> **plan 44 is EXECUTED**, merged as PR 32. A whole-corpus refresh: every Spec read against the -> engine, the other Specs and the vocabulary, cross-family verified, and the findings fixed, -> ruled on the owner's delegation, or homed as keyed questions with provisional answers for the -> next arcs; the record is `reviews/26-whole-corpus-refresh.md`. -> **plan 43 is EXECUTED**, merged as PR 31. Design iteration on a readable graph: answers -> that fit (summaries, both next rungs, explained misses, the design backlog, repeatable -> `--params`), the authoring grammar (rule keys, question fields, relation reasons), the iteration -> loop (design diff, context bundle, perspective filter, change proposal), and gen 1's curation -> and slimming lessons; intent in `pack:design-iteration-v1`, recipes 33 to 40. The design pass's -> rulings and `ready` statements were delegated by the owner and still await the owner's review; -> the plan's last sections list them. Plan 42 follows it. -> **plan 42 is EXECUTED** as the Studio lane above. The Spec Studio's phase 0, a read-only -> static view (`sdp studio`); intent in `pack:spec-studio-v1`. Its design pass split the Studio's -> Specs for honest binding and prototyped the Pack page before the build. -> **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, -> projections, and agent recipes changed together. Counts are derived at close; re-run the -> recipes rather than quoting them. -> **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, 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 -> (`spec:consumers.graph-first-planning`, `spec:model.structural-patterns`, -> `spec:protocol.structural-self-binding`) and the backlog and readiness are read -> from the graph (recipes 1, 9, 11). Settled ground: -> **plan 37 is EXECUTED** — the plan-36 arc is closed; briefs I–K are delivered per -> plan 36, with operational tracking in `.omo/plans/plan-37-settling-arc.md`. -> **Status:** Specs and Packs default to the Markdown carrier (the carrier ruling, MD-18, completed -> by the Pack syntax ruling, MD-25); the TS DSL survives as import source and a lawful per-ID -> option, and the Gherkin carrier option (MD-27) admits a graph-aware lawful per-ID option for -> behavior and example Specs. Settled ground: **plan 35 is EXECUTED** (the plan-34 arc: structural -> recipes, `sdp new spec`, `sdp validate --watch`, first-tranche registrar adoption under MD-31, -> the engine's own component/uses self-binding); **plan 33 is EXECUTED** (the adjudicated plan-31 -> review closed, graph and gate re-measured); **plan 31 is EXECUTED** (carrier universality, -> derived runnable modules, census/Mermaid projections, structural anchor semantics); **plan 30 is -> EXECUTED** (the canonical `.sdp.gherkin` suffix, MD-28); **plan 28 is EXECUTED** (bounded carrier -> pipeline, parity proof, reader-family tracer bullet); **plan 27 is EXECUTED** (v0 lineage -> restored, the Gherkin option selected). Plans 36 and 34 are DRAFTED briefs indexes; plans 32 and -> 29 are briefs-only planning inputs — lineage, never execution plans. Earlier arcs (plans 24–26) -> stand as recorded in `plans/`. Plan 24's inward turn remains the standing practice: forward -> intent lives in the graph, so the live backlog is a graph query, not a document. -> Corpus counts, readiness, and findings are **derived, never quoted** — re-run +> **Next work** is read from the graph: run the roadmap, recipe 53, over `pack:roadmap` +> (`pnpm --silent sdp:q "$(cat dist/recipes/53-the-roadmap.js)" --params '{"pack":"pack:roadmap"}'`), +> follow each entry to its Packs and carrying Specs, and read the open arc's draft pull request for +> its record. `plans/` and `reviews/` are frozen history of earlier arcs until a cleanup arc +> removes or archives them; nothing is added to either. Corpus counts, readiness, and findings are +> **derived, never quoted** — re-run > `pnpm --silent sdp validate . --exclude explorations --exclude examples --exclude test/fixtures/import/parity` -> (or `npm run --silent sdp --` with the same argv) and read recipes 8 and 11. Recipe 1 is the -> operational backlog, not the census. Never invoke a bare `sdp`. Build state lives in -> **`plans/`** — read the highest **primary-numbered** plan's status header, plus any **active -> subplans it (or its parent family) explicitly designates as current**; ignore unnumbered files -> and letter-suffixed plans only when no primary/active plan designates them. If that plan is -> DRAFTED, also read the latest ✅ EXECUTED/RUN plan for settled ground. +> and read recipes 8 and 11. Recipe 1 is the operational backlog, not the census. Never invoke a bare `sdp`. ## The frame @@ -136,9 +64,9 @@ Progressive disclosure — start at the top, follow the pointers down. | `examples/checkout-v1` | **the worked MVP example** (Markdown carriers; originally the TS DSL tracer bullet) — specs, anchors, untracked `generated/` (regenerated in-pipeline); walkthrough in its README | when proving the loop end-to-end | | `explorations/` | **evidence only** (carrier exhibits, executable-example findings) — mapping evidence for design; **never promote spike code into product** | when judging design evidence; not a source tree to ship | | `docs/lineage/` | restored historical design documents — the v0 concept set, verbatim with lineage headers; evidence for design reviews, never intended truth | when a review needs superseded design on the table rather than in git history | -| `plans/` | **the build plan** — what each implementation session does, and why | before writing code — highest primary-numbered plan's status header, plus active subplans it designates; if DRAFTED, also the latest ✅ EXECUTED/RUN plan | +| `pack:roadmap` + recipe 53 | **the next work** — the owner's advisory priority over upcoming capabilities, each entry placed by the graph as to design, to state, to build or built; the open arc's record is its draft pull request | before writing code | | `npm run check` | **the green gate** — `check:temporal` → `lint` → `format:check` → `build` → `generate:self-hosting` → `generate:example` → `typecheck` → `typecheck:examples` → `test` → `check:self-hosting-gates` → `check:self-hosting` → `check:example` → `preflight` | before claiming green / after engine edits | -| `reviews/` | **archived session reviews** (implementation, founding-ideation, adversarial + prompts) — durable findings already folded into plans/DECISIONS; read for provenance | rarely | +| `plans/`, `reviews/` | **frozen history** of earlier arcs: plans, session reviews, prompts and assets; read for provenance; nothing is added | rarely | Key decisions (names first): the executable meta-model (MD-1); one primitive named coordinates (MD-4); the carrier ruling (MD-18); the agent front door (MD-22); the shipped projections stay frozen (MD-32); planning truths live in ruled graph homes (MD-33); the comment form (MD-36); bindings are optional, plural, and may reference a design (MD-37); architectural significance is annotated where it is realized (MD-38). @@ -198,8 +126,8 @@ for the Studio, `sdp-vault` for the vault) are the on-ramps. In this checkout, a The MVP proved the founding principle on **one** bounded context — Order Management, `pack:checkout-v1` — built as thin **end-to-end slices on the Phase 0 foundation**; the worked example lives at `examples/checkout-v1` (documented walkthrough in its README). **Slices 0–5 are -complete** — the per-slice record is plan 10. Live work is the -highest primary-numbered plan under `plans/` — read it before writing code. +complete** — the per-slice record is plan 10. Live work is read from the +roadmap, recipe 53 over `pack:roadmap` — run it before writing code. > **Tracer-bullet discipline.** Author the example specs and anchored code *first*, so the carrier and extractor are > forced to be usable before they are finished. If the example stops extracting or validating, fix the carrier or extractor — not the example. @@ -242,7 +170,7 @@ Every doc honours both — never mistake one half for the other: ladder) is a **bug to fix against the base**, not current usage. - **DECISIONS.md admits sparingly, and reads by name.** An entry must pass the ADR three-part test — *hard to reverse · surprising without context · a real trade-off*. If the "Why" says the base already forced the - answer, it's drift repair, not a decision — its paper trail is git + the plan done-record. Authoring + answer, it's drift repair, not a decision — its paper trail is git and the pull request description. Authoring guidance only, never a validator (checks police conformance and honesty, never content-quality). In prose, lead with meaning: "the typing law (MD-11)," never bare MD-n (the registry at the top of `DECISIONS.md` holds the ratified names). @@ -252,8 +180,8 @@ Every doc honours both — never mistake one half for the other: - **Naming is resolved — use these names.** Product **Libar Software Delivery Protocol** (short form "the Protocol"); CLI **`sdp`**; npm **`@libar-dev/software-delivery-protocol`** (single package); repo `libar-dev/software-delivery-protocol`. Namespaces: `@libar-dev/` (OSS) vs `@libar-ai/` (commercial). -- **Plan vs execution.** Distinguish **PLAN-ONLY** work (designing, deciding) from **execution** (editing code or - docs). For a plan under `plans/`, don't touch its target files unless the session is execution. +- **Design vs execution.** Distinguish design work (Specs, decisions, open questions) from execution + (code, tests, docs). A design session edits carriers and leaves the code its Specs will change alone. - **Lineage is evidence, not template.** Prior art **`@libar-dev/architect`** (local clone when present, e.g. a sibling `architect` checkout) taught us the problem in production — source-first annotations, one graph as the read model, a scriptable graph handle (the direct ancestor of @@ -267,15 +195,35 @@ Every doc honours both — never mistake one half for the other: write it, under OmO's own protocol. Every other agent leaves it alone: it adds no plan, draft or evidence there and never deletes, prunes or overwrites what is there. A Claude session keeps its working material in its own scratchpad. -- **Tests are the evidence.** A change proves itself with a test that fails without it, and the corpus's - state with a recipe anyone can re-run. Keep a separate evidence artifact only for what no test or recipe - can carry, such as a rendered page or an adopter measurement, and commit it beside the record that cites it. -- **Commits are recovery boundaries, not workflow gates.** An execution plan's explicit commit strategy counts - as authorization on its work branch; otherwise ask before committing. Prefer a commit after a coherent - logical unit and its relevant quality gate, but never force one per todo, create empty commits, absorb - unrelated or pre-existing changes, or commit from an unsafe dirty baseline. When no clean boundary exists, - preserve and account for the state in the tracked plan/draft rather than discarding it; commit at the next - safe boundary. Push only on the user's explicit request; never use `git stash`. +- **Tests are the evidence.** A change proves itself with a test that fails without it; recipes + re-derive corpus state. Acceptance links bound examples and run evidence at the measured commit + with the commands, never an inherited verdict. A run's commit must match a preview's stamp to + evidence it; a preview remains a current view. Evidence no test or recipe carries is committed + on the arc's branch with its commands, linked from the description at that commit, and removed + before merge. The Protocol merges its pull requests with merge commits, and GitHub keeps each + pull request's head ref, so links at the evidence commit keep resolving. A squash or rebase merge + would need the evidence published to a durable ref first. +- **Commits are recovery boundaries, not workflow gates.** The owner's instruction that opens an + arc authorizes commits and its push; the convention creates no authority. The first commit message + states the owner's working rules, base and arc's Pack. Open the draft pull request at the first coherent commit, + normally the scaffold, and repeat those rules there with the branch, base, lanes and rulings. + Without such an instruction, ask before committing. Commit each coherent unit after its gate; + never force one per todo, create empty commits, absorb unrelated or pre-existing changes, or + commit from an unsafe dirty baseline. If a session must stop without a clean boundary, commit + its work in progress saying what is incomplete. Resume from + the branch, commits, draft description and graph; git cannot recover uncommitted work. + Outstanding work is read from the arc's Pack, backlogs and open-question register. + Push only on the user's explicit request; never use `git stash`. +- **Reviews travel as proposals.** Findings against Specs form one change proposal + (`schemas/sdp-proposal-1.schema.json`), based on the branch-head commit the reviewer read; + a proposal composed in a preview names the preview's merge commit from its stamp. Check it with + recipe 38 against that base, read moved, unresolved or incomparable scopes again, and post + recipe 49's memo to the pull request. + Findings against code or other files are pull request comments on their lines; a review proposing no Spec edit + has no proposal. An open design finding becomes an open question with `decider`, `provisional` + and `links`; a deferral adds its blocking question's `trigger`. A code defect against settled + intent stays a code finding. An applying commit carries the proposal's reason line and base; + acceptance stays in git and the discussion. Briefs and prompts stay in the session. - **Unreleased OmO installs are user-controlled.** The user switches the official `~/dev-admin/oh-my-openagent` clone to `dev` when needed and owns `~/.omo/omo.jsonc`. Agents may inspect and report that state, but must not checkout, pull, build, globally install, or edit the OmO configuration unless @@ -290,17 +238,17 @@ that reader brings. The standard, set by PR #19 (the universal-Spec arc) buildin #16, and extended by PR #31 (human views, ruling tables, the next PR's lanes): - **Story before inventory.** Lead with what the PR introduces and why: the questions it answers, - the arc it closes, the plans it executes. What-changed lists and review-closure details are + the arc it closes, the capability it delivers. What-changed lists and review-closure details are evidence, never the headline; a description written during a review session must still be re-anchored on the full scope before publication. - **Present new things, don't just name them.** Each new surface gets a short explanation of the idea and a linked representative artifact on the branch: a carrying Spec, a worked example, a - plan. One stop per surface, tour-style. + Pack. One stop per surface, tour-style. - **Show it to a person.** Prose, rendered tables and screenshots are welcome. When the PR moves a human view, such as a Design Review page, show it at the arc's base and at the head. Render a recipe's answer as a short table rather than pasting JSON, and say plainly what no human view - shows yet. `gh` cannot attach images, so commit them under `reviews/assets/pr/` with a - short README naming the commit and the commands. + shows yet. `gh` cannot attach images, so commit them on the arc's branch with a + short README naming the commands, pin links to that commit, and remove them before merge. - **Self-hosted and graph-measured.** The arc's intent is a Pack in the corpus: link it, and describe its state and its changes with the Protocol's own recipes, Pack design (29) and the design diff (35) from the arc's base among them. Check every claim about the Protocol's own @@ -310,14 +258,13 @@ that reader brings. The standard, set by PR #19 (the universal-Spec arc) buildin second. Every registry row the PR adds appears. - **Invite use.** A try-it block with real commands, re-run at the head before publishing, plus a feedback call naming the open questions the PR most wants pressure on. -- **Plan the next PR.** Every description keeps a forward section, and it is a proposal: the next - PR's lanes as a table (what, and why now) with its re-entry condition, then the arc after it, - the statements that wait only for the owner, and the deferred items with their triggers from - recipe 20. Keep the durable refused list. Rewrite the section each time rather than appending - stale rows. +- **Plan the next PR.** The forward section is recipe 55's memo over `pack:roadmap`, rendered at + the head, with the owner's reason for each entry's priority, the re-entry condition and the + durable refused list in the author's words around it. The pull request completing an entry's + selected work removes it in the same change; entries join or move only on the owner's word. - **Numbers are re-measured and marked.** Close measurements are re-derived at close and labeled as such ("re-run the recipes rather than inheriting these"), never quoted from an older record. -- **Audit, then publish.** Check the description against the plan, every ruling record the PR +- **Audit, then publish.** Check the description against the arc's Pack, every ruling record the PR carries out, and the diff stat from the base, so each finished piece of work appears or is left out on purpose. Pin links and images to a commit so they outlive the branch (`blob//…`, `raw.githubusercontent.com////…`; a line anchor on a Markdown file needs diff --git a/CONTEXT.md b/CONTEXT.md index 38b7fbb58..5a6a725d8 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -51,7 +51,7 @@ The familiar delivery nouns are **named coordinates on the one primitive, never | **Scenario** | a low-altitude `example`-kind `Spec` that `refines`/`verifies` a Story | an altitude or a kind | | **NFR** | a `constraint` with a measurable `target`; the *flavor* (performance / security / …) lives on the section | a separate kind | | **Executable Spec** | an `example` that **has a verifier** (a delivery fact) | a readiness rung | -| **capability / domain** | a **`Pack` grouping** and/or a designed-for Capability Map projection over high-altitude `behavior` Specs; no Capability Map is built, and the Studio groups Specs by Pack and draws no capability tree (`spec:consumers.spec-studio.lenses.packs#rule.packGrouping`) | a kind or an altitude | +| **capability / domain** | a **`Pack` grouping** and/or a Capability Map projection over epic Specs and their declared `refines` descendants, of every kind; Pack membership and refinement remain distinct (`spec:consumers.capability-map`, `spec:consumers.spec-studio.lenses.packs#rule.packGrouping`) | a kind or an altitude | ## Sections (→ `spec:model.spec-sections`) @@ -82,7 +82,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, a role, and references to the designs it answers to, with uses declared for its architectural dependencies, normally between components (a unit's own uses is optional); 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) | +| **architecturally significant unit** | a code unit with exported public surface or cross-component reach whose contract or responsibility a designer or builder must understand; export status or import rank alone is insufficient; its anchor, on the code that realizes it, records component membership, a role and references to the designs it answers to, with uses declared for its architectural dependencies, normally between components (a unit's own uses is optional); 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). @@ -178,6 +178,8 @@ validated," never "provably correct."** | **change proposal** | the addressed JSON hand-off of scoped changes against a named base commit, checked before an agent applies ordinary source edits (`spec:consumers.intent-composition.proposal`) | "patch" · "approval" | | **proposal memo** | the Markdown rendering of a change proposal a person reviews, including its exact JSON document, shared by recipe 49 and the composing panel (`spec:consumers.intent-composition.proposal.memo`) | "review verdict" | | **statement queue** | the ordered answer of waiting readiness statements and a selected decider's questions, with the Specs they rest on first; it states no rung (`spec:consumers.readiness-planner`) | "promotion queue" · "approval gate" | +| **roadmap** | a Pack whose manifest states `order: priority`: its members' authored order is the corpus's advisory priority, first entry first, and what each entry needs is read from the graph each time; never a schedule, a date or a gate (a roadmap states advisory priority, MD-46; `spec:consumers.roadmap`) | "plan" · "schedule" · "backlog" (the build and design backlogs are recipes) | +| **need** (of a roadmap entry) | what a roadmap entry needs next, read from its refinement subtree each time: to design, to state (only a person's `ready` statement stands before it), to build, or built; never authored (`spec:consumers.roadmap#rule.entryNeed`) | "lane" (a pull request's work streams) · "step" (an example's steps) · "status" · "stage" | | **upgrade kit** | the recipes and guide that measure capability adoption and propose rule keys for an agent's edit, without editing carriers or stating readiness (`spec:consumers.upgrade-kit`) | "migration tool" · "codemod" | | **capability adoption** | the per-corpus, Pack and Spec counts of places a Protocol capability is used and could apply, with its carrying Spec; a report without a score or quota (`spec:consumers.upgrade-kit.capability-adoption`) | "adoption score" · "coverage target" | | **stamp** | the publishing metadata in `stamp.json` beside a Studio page set, written after the build with its engine, corpus commit, UTC publishing time and optional source template; never graph data (`spec:consumers.spec-studio.sharing`) | "build attestation" · "delivery fact" | @@ -235,7 +237,7 @@ twelve**: these six plus the six authored relations above (`spec:extraction.deri |---|---|---| | **relation reason** | the one optional line of prose an authored relation carries saying why it holds, owned by that relation; intent, never a fact, a floor or an edge (`spec:model.relation-reasons`) | "strength" · "aspect" · "rationale" (on an edge) | -## Delivery-process vocabulary (adopted nouns; any realization is a projection, and none is built — `spec:decisions.adopt-the-nouns`, `spec:consumers.projections-model.roadmap-views`) +## Delivery-process vocabulary (adopted nouns; roadmap readings are built, release and baseline views remain deferred — `spec:decisions.adopt-the-nouns`, `spec:consumers.projections-model.roadmap-views`) **discipline** (a filter over the graph by kind or section, not a phase you pass through) · **phase / iteration / milestone** (descriptive vocabulary, optional roadmap projections, never gates) · **release** (a tagged set — @@ -243,9 +245,10 @@ a git-tag projection) · **baseline** (a named approved snapshot; the **signed g artifact** — approval remains outside the model, never an authored primitive). **Guidance-only labels:** `delivery session` and `work shape` describe an agent interaction and -its advisory entry in `sdp-sessions`; `design pass`, `design diff`, `design backlog` and -`curation report` name a design activity or a recipe's answer, and their carrying -Specs supply their meaning. None of them names a Protocol primitive, descriptor, relation, +its advisory entry in `sdp-sessions`; `design pass`, `design diff`, `design backlog`, `design slice` +and `curation report` name a design activity or a recipe's answer, and their carrying +Specs supply their meaning. A design slice is the design slice recipe's reading of a set of Specs +as one design, composed on each invocation (`spec:consumers.design-slice`). None of them names a Protocol primitive, descriptor, relation, delivery fact, workflow state, or graph state. ## A worked dialogue (the language in use) @@ -296,7 +299,7 @@ Its owned-folder decision, the vault owns one folder (MD-45), is ratified. `question field` · `rule key` · `step key` · `contract key` · `term key` · `term address` · `entry citation` · `marker` (the bracket prefix) · `distillation boundary` · `change proposal` · `proposal memo` · `statement queue` · `upgrade kit` · `capability adoption` · `stamp` · `run evidence` · `Pack export` · `text equivalent` · `vault` · - `corpus rule` · `census` · `curated graph` · `relation reason` — carried by the named Specs + `corpus rule` · `census` · `curated graph` · `relation reason` · `roadmap` · `need` — carried by the named Specs cited in their glossary rows; mention and entry address follow the checked-mentions decision. - **Descriptor values locked:** `kind` ∈ {`behavior`,`workflow`,`example`,`rule`,`constraint`,`model`, `decision`,`contract`} · `altitude` ∈ {`epic`,`feature`,`story`} · `readiness` ∈ diff --git a/check-self-hosting-gates.mjs b/check-self-hosting-gates.mjs index e8c39101e..a5bd90453 100644 --- a/check-self-hosting-gates.mjs +++ b/check-self-hosting-gates.mjs @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -17,9 +17,9 @@ import { fileURLToPath } from "node:url"; // phase-2 disposition. // 3. PACKET AGREEMENT — the ledger's fields agree with the owner-packet // dispositions, embedded here as constants read from those packets. -// 4. CURRENT RECORD — the highest primary-numbered plan has a readable status header, the -// handbook names that plan and status, semantic surfaces carry no plan status, and the -// documented green-gate legs agree with package.json. +// 4. CURRENT RECORD — the handbook names the roadmap Pack and recipe, the graph written by +// generate:self-hosting carries it with priority order, semantic surfaces carry no plan +// status, and the documented green-gate legs agree with package.json. // 5. ADR DISPOSITIONS — both flagged §3 rulings carry explicit three-part-test outcomes: // lean diary entries (the strict consumer-exclusion contract (MD-20); the // envelope-grammar ownership posture (MD-21)) with registry rows, and the plan records @@ -27,6 +27,7 @@ import { fileURLToPath } from "node:url"; // 6. PLAN-16 RECONCILIATION — every scheduled-session docket origin (the 17/18/19 rows) is // dispositioned, and plan 16 keeps its ✅ RUN stamp (reconciled, never rewritten). // +// Run after npm run generate:self-hosting; this gate reads the graph that command wrote. // Usage: node check-self-hosting-gates.mjs [rootDir] // rootDir — tree holding the surfaces; defaults to this repo root. QA passes temp trees of // mutated copies to prove disagreeing surfaces are named. The temporal dry-run @@ -42,6 +43,7 @@ const planPath = ["plans", "17-self-hosting-v1.md"].join("/"); const plan18Path = ["plans", "18-self-hosting-phase-2.md"].join("/"); const plan16Path = ["plans", "16-carrier-ruling.md"].join("/"); const agentsPath = "AGENTS.md"; +const graphPath = "generated/graph.json"; const decisionsPath = "docs/concept/DECISIONS.md"; const glossaryPath = "CONTEXT.md"; const packagePath = "package.json"; @@ -87,24 +89,6 @@ const packageJson = JSON.parse(read(packagePath)); const plan16 = read(plan16Path); const plan18 = existsSync(join(rootDir, plan18Path)) ? read(plan18Path) : null; -const primaryPlans = readdirSync(join(rootDir, "plans"), { withFileTypes: true }) - .filter((entry) => entry.isFile()) - .map((entry) => { - const match = /^(?\d+)-[a-z0-9][a-z0-9-]*\.md$/u.exec(entry.name); - const number = match?.groups?.number; - return number === undefined ? null : { name: entry.name, number: Number(number) }; - }) - .filter((entry) => entry !== null) - .sort((left, right) => right.number - left.number); -const currentPlan = primaryPlans[0]; - -if (currentPlan === undefined) { - failures.push("plans/ — no primary-numbered plan exists"); -} - -const currentPlanPath = currentPlan === undefined ? null : ["plans", currentPlan.name].join("/"); -const currentPlanSource = currentPlanPath === null ? "" : read(currentPlanPath); - // --------------------------------------------------------------------------- // 1. The docket: all 25 obligations are dispositioned. // --------------------------------------------------------------------------- @@ -223,36 +207,22 @@ if (ledgerStart === -1) { } // --------------------------------------------------------------------------- -// 4. Current record: select the current plan by convention and keep status/gate surfaces aligned. +// 4. Current record: roadmap pointers and the documented gate. // --------------------------------------------------------------------------- -const currentStatusHeader = - currentPlanSource.split("\n").find((line) => line.startsWith("> **Status:**")) ?? ""; -const currentStatus = /\b(DRAFTED|EXECUTING|RUN|EXECUTED)\b/u.exec(currentStatusHeader)?.[1]; - -if (currentStatusHeader === "") { - failures.push(`${currentPlanPath ?? "plans/"} — missing a readable blockquoted status header`); -} -if (currentStatus === undefined) { - failures.push( - `${currentPlanPath ?? "plans/"} — status header has no DRAFTED/EXECUTING/RUN/EXECUTED state`, - ); -} -if (currentPlan !== undefined) { - expectContains( - agentsPath, - agents, - `plan ${String(currentPlan.number)}`, - "the handbook does not name the current primary plan", - ); -} -if (currentStatus !== undefined) { - expectContains( - agentsPath, - agents, - `plan ${String(currentPlan?.number)} is ${currentStatus}`, - "the handbook status disagrees with the current primary plan", +expectContains(agentsPath, agents, "pack:roadmap", "the handbook does not name the roadmap Pack"); +expectContains(agentsPath, agents, "recipe 53", "the handbook does not name the roadmap recipe"); +try { + const graph = JSON.parse(read(graphPath)); + const roadmap = graph.nodes.find( + (node) => node.nodeType === "Pack" && node.id === "pack:roadmap", ); + if (roadmap === undefined) + failures.push(`${graphPath} — the roadmap Pack pack:roadmap is missing`); + else if (roadmap.order !== "priority") + failures.push(`${graphPath} — the roadmap Pack pack:roadmap does not state order: priority`); +} catch { + failures.push(`${graphPath} — the generated graph is missing or unreadable`); } const checkScript = packageJson?.scripts?.check; @@ -404,11 +374,12 @@ const report = { decisions: decisionsPath, glossary: glossaryPath, phase2Plan: plan18 === null ? null : plan18Path, - currentPlan: currentPlanPath, + graph: graphPath, package: packagePath, }, currentRecord: { - status: currentStatus ?? null, + roadmap: "pack:roadmap", + recipe: 53, gateLegs: actualGateLegs, }, temporal, diff --git a/contract-dependent-suites.mjs b/contract-dependent-suites.mjs index 6dd8bc68b..4f7747a6c 100644 --- a/contract-dependent-suites.mjs +++ b/contract-dependent-suites.mjs @@ -24,6 +24,32 @@ export const contractDependentSuites = [ contracts: "generated/contracts", generation: "npm run generate:self-hosting", testPaths: [ + "test/self-hosting-design-slice.test.ts", + "test/consumers.design-slice.outside-components.test.generated.ts", + "test/consumers.design-slice.chain.test.generated.ts", + "test/consumers.design-slice.decision-backlink.test.generated.ts", + "test/consumers.design-slice.dependency-cycle.test.generated.ts", + "test/consumers.design-slice.folded-example.test.generated.ts", + "test/consumers.design-slice.independent-example.test.generated.ts", + "test/consumers.design-slice.component-absent.test.generated.ts", + "test/consumers.design-slice.component-unrelated.test.generated.ts", + "test/consumers.design-slice.component-matching.test.generated.ts", + "test/consumers.design-slice.component-differing.test.generated.ts", + "test/consumers.design-slice.component-unreadable.test.generated.ts", + "test/consumers.design-slice.outside-satisfied.test.generated.ts", + "test/consumers.design-slice.outside-unbound.test.generated.ts", + "test/consumers.design-slice.outside-holding.test.generated.ts", + "test/roadmap-examples.test.ts", + "test/consumers.roadmap.built-work.test.generated.ts", + "test/consumers.roadmap.buildable-part.test.generated.ts", + "test/consumers.roadmap.statement-part.test.generated.ts", + "test/consumers.roadmap.design-part.test.generated.ts", + "test/consumers.roadmap.drifting-part.test.generated.ts", + "test/consumers.roadmap.missing-evidence.test.generated.ts", + "test/consumers.roadmap.blocking-part.test.generated.ts", + "test/consumers.roadmap.outside-parent.test.generated.ts", + "test/consumers.roadmap.statement-order.test.generated.ts", + "test/studio-s5.test.ts", "test/consumers.spec-studio.intent-panel.refresh-keeps-intents.test.generated.ts", "test/consumers.intent-composition.proposal.memo.shared.test.generated.ts", diff --git a/docs/agent-surface/recipes.md b/docs/agent-surface/recipes.md index f1b5c7a45..4058274b1 100644 --- a/docs/agent-surface/recipes.md +++ b/docs/agent-surface/recipes.md @@ -53,6 +53,10 @@ | 49 | The proposal memo | proposal | Render a proposal as the memo the composing panel hands off | | 50 | External registers | register, pattern, severity, uncited | Check prose tokens against a supplied register | | 51 | Entry citations | pack or specs, severity | Find anchored citations and unresolved addresses | +| 52 | The capability map | epic, detail | Read capability epics, distinct descendants and direct binding evidence | +| 53 | The roadmap | pack, decider | Read upcoming work in authored order and what each entry needs | +| 54 | The design slice | pack or specs, members, detail | Read a set of Specs as one design | +| 55 | The roadmap memo | pack, decider | Render a roadmap as a description's forward section | Runnable bodies for the agent front door. Each recipe below is a body you can pass verbatim to `sdp q`, unchanged. In the Protocol source checkout, pass one to the repository wrapper: @@ -164,10 +168,10 @@ not suppress `console.*`, so a body a machine reads, every recipe below included to the console. Returning a conclusion costs a fraction of returning a dump, so every recipe below returns counts, ids, and decoded reasons rather than whole nodes. -Changed-file impact, Pack design and the design diff, recipes 4, 29 and 35, accept `detail: true` -and default to summaries that preserve every total and name omitted detail in `leftOut`. Pack -design also accepts member ids in `detail` for selected full rows. Other recipes follow their -own documented result shapes. +Changed-file impact, Pack design, the design diff and the design slice, recipes 4, 29, 35 and 54, +accept `detail: true` and default to summaries that preserve every total and name omitted detail +in `leftOut`. Pack design and the design slice also accept member ids in `detail` for selected +full rows. Other recipes follow their own documented result shapes. **Trust stance.** `sdp q` evaluates local operator-supplied code with the trust of any local developer tool — no sandbox is claimed and none exists. A body is code you author yourself; never @@ -3185,11 +3189,20 @@ const packs = []; for (const id of [...new Set([...basePacks.keys(), ...headPacks.keys()])].sort()) { if (packId !== undefined && id !== packId) continue; const a = basePacks.get(id), b = headPacks.get(id); - const members = delta(a?.members ?? [], b?.members ?? []); + const orderOf = (node, side) => { + if (node === undefined) return null; + if (node.order === undefined) return "reading"; + if (node.order === "priority") return "priority"; + diagnostics.push({ part: "order", id, side, value: node.order, problem: "Part is outside the common schema." }); + return null; + }; + const orderBefore = orderOf(a, "base"), orderAfter = orderOf(b, "head"); + const orderChanged = a !== undefined && b !== undefined && orderBefore !== null && orderAfter !== null && orderBefore !== orderAfter; + const members = { added: (b?.members ?? []).filter((member) => !a?.members.includes(member)), removed: (a?.members ?? []).filter((member) => !b?.members.includes(member)) }; const common = new Set((a?.members ?? []).filter((member) => b?.members.includes(member))); const reordered = !same((a?.members ?? []).filter((member) => common.has(member)), (b?.members ?? []).filter((member) => common.has(member))); - if (!a || !b || members.added.length || members.removed.length || reordered) packs.push({ id, - change: !a ? "added" : !b ? "removed" : "changed", membersAdded: members.added, membersRemoved: members.removed, reordered }); + if (!a || !b || members.added.length || members.removed.length || reordered || orderChanged) packs.push({ id, + change: !a ? "added" : !b ? "removed" : "changed", membersAdded: members.added, membersRemoved: members.removed, reordered, orderChanged, orderBefore, orderAfter }); } const bound = (value, id, type) => { const nodes = new Map(value.nodes.map((node) => [node.id, node])); @@ -3237,6 +3250,12 @@ For a probe whose two rules were keyed and whose second rule was also reworded, ``` `packs` reports membership additions, removals and relative order changes among retained members. +`membersAdded` and `membersRemoved` keep their respective manifest order. +`orderChanged` reports a change in what the order means, with `orderBefore` and `orderAfter` +reading `"reading"` or `"priority"`; an absent `order` reads as `"reading"` in every schema version. +An absent Pack reads null on that side. An unknown value produces an order diagnostic +`{ part: "order", id, side, value, problem }`, where `side` is `"base"` or `"head"`, and reads null +on that side; the Pack's membership and member order still compare, with `orderChanged: false`. `bindings` keeps anchor-derived changes apart, with added and removed ids for realizing units, referencing units, enabled verifiers and delivery facts. Each list has a count in `totals`. A schema difference is diagnostic data. Parts outside the schemas' shared capabilities are @@ -5162,3 +5181,1094 @@ return { findings, }; ``` + +## 52. The capability map + +Read every epic and its distinct refinement descendants, or pass `params.epic` to select one +epic and its descendants. Every kind participates. The default summary (`params.detail` absent +or false) keeps every total, each epic's title, direct epic parents and rollup, unreached ids, +cycles and damage. `leftOut` names what `detail: true` adds: node rows, edges, descendant ids +and unreached roots. A parameter refusal returns `diagnostics` without a map; unknown names +return `unknownParams` and `accepts`. Inspect `damage` and `cycles` on a returned map. +The contract is `spec:consumers.capability-map#design.mapRecipe` through its keyed answer entries. + +```js +const accepts = ["epic", "detail"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; +const detail = params.detail ?? false; +const diagnostics = []; +if (params.detail !== undefined && typeof params.detail !== "boolean") { + diagnostics.push({ parameter: "detail", problem: "Expected a boolean" }); +} +const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0; +const byEnds = (a, b) => compare(a.from, b.from) || compare(a.to, b.to); +const specs = g.specs().sort((a, b) => compare(a.id, b.id)); +const byId = new Map(specs.map((spec) => [spec.id, spec])); +if (params.epic !== undefined) { + if (typeof params.epic !== "string") { + diagnostics.push({ parameter: "epic", problem: "Expected a Spec id string" }); + } else { + const epic = byId.get(params.epic); + if (!epic) { + diagnostics.push({ parameter: "epic", problem: `No resolving Spec for ${params.epic}` }); + } else if (epic.altitude !== "epic") { + diagnostics.push({ parameter: "epic", problem: `${params.epic} is a ${epic.altitude} Spec, not an epic` }); + } + } +} +if (diagnostics.length > 0) return { diagnostics }; +const allEdges = graph.edges.filter((edge) => edge.type === "refines" && edge.claim === "declared"); +const edges = allEdges.filter((edge) => byId.has(edge.from) && byId.has(edge.to)).sort(byEnds); +const parents = new Map(specs.map((spec) => [spec.id, new Set()])); +const children = new Map(specs.map((spec) => [spec.id, new Set()])); +for (const edge of edges) { + parents.get(edge.from).add(edge.to); + children.get(edge.to).add(edge.from); +} +for (const adjacency of [parents, children]) { + for (const [id, neighbors] of adjacency) adjacency.set(id, [...neighbors].sort(compare)); +} +const reach = (root) => { + const seen = new Set(); + const todo = [root]; + while (todo.length > 0) { + const id = todo.pop(); + if (seen.has(id)) continue; + seen.add(id); + for (const child of children.get(id)) todo.push(child); + } + return seen; +}; +const epicIds = specs.filter((spec) => spec.altitude === "epic").map((spec) => spec.id); +const reaches = new Map(epicIds.map((id) => [id, reach(id)])); +const selected = params.epic === undefined ? new Set(byId.keys()) : reaches.get(params.epic); +const nodes = specs.filter((spec) => selected.has(spec.id)).map((spec) => ({ + id: spec.id, + title: spec.title ?? null, + specKind: spec.specKind, + altitude: spec.altitude, + statedReadiness: spec.statedReadiness, + floorReached: spec.derivedReadiness ?? "none", + deliveryFacts: spec.deliveryFacts, + questions: g.specContext(spec.id).designColumns.openQuestions.map((question) => ({ + index: question.index, + address: question.key ? `${spec.id}#question.${question.key}` : null, + question: question.question, + blocking: question.blocking, + })), + parents: parents.get(spec.id), + children: children.get(spec.id), + epicIds: epicIds.filter((id) => reaches.get(id).has(spec.id)), +})); +const nodeById = new Map(nodes.map((node) => [node.id, node])); +const count = (ids) => { + const zeroes = (keys) => Object.fromEntries(keys.map((key) => [key, 0])); + const counts = { + specs: ids.length, + byStatedReadiness: zeroes(["idea", "scoped", "defined", "ready"]), + byFloorReached: zeroes(["none", "idea", "scoped", "defined", "ready"]), + byKind: zeroes(["behavior", "workflow", "example", "rule", "constraint", "model", "decision", "contract"]), + byAltitude: zeroes(["epic", "feature", "story"]), + withImplementationBinding: 0, + withVerifierBinding: 0, + openQuestions: 0, + blockingQuestions: 0, + }; + for (const id of ids) { + const node = nodeById.get(id); + counts.byStatedReadiness[node.statedReadiness]++; + counts.byFloorReached[node.floorReached]++; + counts.byKind[node.specKind]++; + counts.byAltitude[node.altitude]++; + counts.withImplementationBinding += Number(node.deliveryFacts.includes("implemented")); + counts.withVerifierBinding += Number(node.deliveryFacts.includes("has-verifier")); + counts.openQuestions += node.questions.length; + counts.blockingQuestions += node.questions.filter((question) => question.blocking).length; + } + return counts; +}; +// Two iterative passes keep cycles finite without using the call stack for long branches. +const components = (ids) => { + const allowed = new Set(ids); + const seen = new Set(); + const finished = []; + for (const root of ids) { + if (seen.has(root)) continue; + seen.add(root); + const stack = [{ id: root, next: 0 }]; + while (stack.length > 0) { + const frame = stack[stack.length - 1]; + const neighbors = children.get(frame.id); + if (frame.next === neighbors.length) { + finished.push(frame.id); + stack.pop(); + continue; + } + const child = neighbors[frame.next++]; + if (allowed.has(child) && !seen.has(child)) { + seen.add(child); + stack.push({ id: child, next: 0 }); + } + } + } + seen.clear(); + const groups = []; + for (const root of finished.reverse()) { + if (seen.has(root)) continue; + const group = []; + const todo = [root]; + seen.add(root); + while (todo.length > 0) { + const id = todo.pop(); + group.push(id); + for (const parent of parents.get(id)) { + if (allowed.has(parent) && !seen.has(parent)) { + seen.add(parent); + todo.push(parent); + } + } + } + groups.push(group.sort(compare)); + } + return groups.sort((a, b) => compare(a[0], b[0])); +}; +const ids = nodes.map((node) => node.id); +const unreached = nodes.filter((node) => node.epicIds.length === 0).map((node) => node.id); +const unreachedSet = new Set(unreached); +const roots = components(unreached).filter((group) => { + const members = new Set(group); + return !group.some((id) => parents.get(id).some((parent) => unreachedSet.has(parent) && !members.has(parent))); +}).map((group) => group[0]).sort(compare); +const cycles = components(ids).filter((group) => group.length > 1 || children.get(group[0]).includes(group[0])).map((members) => ({ members })); +const epics = epicIds.filter((id) => selected.has(id)).map((id) => { + const descendantIds = [...reaches.get(id)].filter((child) => child !== id && selected.has(child)).sort(compare); + return { + id, + title: byId.get(id).title ?? null, + parentEpicIds: parents.get(id).filter((parent) => parent !== id && byId.get(parent).altitude === "epic"), + descendantIds, + rollup: count(descendantIds), + }; +}); +const damage = allEdges.filter((edge) => (!byId.has(edge.from) || !byId.has(edge.to)) && + (params.epic === undefined || selected.has(edge.from) || selected.has(edge.to))) + .map((edge) => ({ code: "unresolved-refinement", from: edge.from, to: edge.to })).sort(byEnds); +const input = { epic: params.epic === undefined ? null : params.epic }; +const totals = { ...count(ids), epics: epics.length, reached: ids.length - unreached.length, unreached: unreached.length }; +if (detail) { + return { + input, + totals, + nodes, + edges: edges.filter((edge) => selected.has(edge.from) && selected.has(edge.to)).map((edge) => ({ + from: edge.from, to: edge.to, claim: "declared", reason: edge.reason ?? null, + })), + epics, + unreached: { ids: unreached, roots }, + cycles, + damage, + }; +} +return { + input, + totals, + epics: epics.map(({ id, title, parentEpicIds, rollup }) => ({ id, title, parentEpicIds, rollup })), + unreached: { ids: unreached }, + cycles, + damage, + leftOut: [ + "nodes: full Spec rows and memberships", + "edges: selected refinement edges and reasons", + "epics.descendantIds: distinct descendant ids", + "unreached.roots: roots of the unreached refinement subgraph", + ], +}; +``` + +## 53. The roadmap + +Read every Pack stating `order: priority`, in Pack-id order, or the Pack named by `params.pack`. +`params.decider` defaults to `owner`. The answer keeps member places, derives each entry's need +over its refinement subtree, and follows the statement queue for waiting statements. Invalid +Pack ids and decider tokens return diagnostics without roadmaps. An explicitly selected ordinary +Pack reads as reading order. + +The answer is `{ decider, roadmaps }`. Each roadmap is +`{ pack, title, framing, order, totals, entries, apart, unresolved, waiting }`: `entries` carries the work, +dependencies and earlier statements at each place; `apart` carries entries with no work; +`unresolved` lists unresolved manifest members as `{ place, id }`, with every place kept as its +manifest position; `totals.unresolved` counts them. `waiting` carries questions, questions without +a decider and readiness statements. The row and total fields are defined by +`spec:consumers.roadmap#design.roadmapAnswer` and its following entries. + +```js +const accepts = ["pack", "decider"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; +const decider = params.decider ?? "owner"; +const diagnostics = []; +if (params.decider === null || typeof decider !== "string" || !/^[a-z][a-z0-9-]*$/u.test(decider)) diagnostics.push({ parameter: "decider", problem: "Expected a lowercase token." }); +const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0; +const packs = graph.nodes.filter((node) => node.nodeType === "Pack"); +let selected = []; +if (params.pack !== undefined) { + const pack = typeof params.pack === "string" ? packs.find((node) => node.id === params.pack) : undefined; + if (pack === undefined) diagnostics.push({ parameter: "pack", problem: "Expected the id of a Pack in this graph." }); + else selected = [pack]; +} else selected = packs.filter((node) => node.order === "priority").sort((a, b) => compare(a.id, b.id)); +if (diagnostics.length > 0) return { diagnostics }; + +const rungs = ["idea", "scoped", "defined", "ready"]; +const contexts = new Map(g.specs().map((spec) => [spec.id, g.specContext(spec.id)])); +const children = new Map(); +for (const edge of graph.edges) { + if (edge.type !== "refines" || edge.claim !== "declared" || !contexts.has(edge.from) || !contexts.has(edge.to)) continue; + children.set(edge.to, [...(children.get(edge.to) ?? []), edge.from]); +} +const settledKinds = new Set(["example", "decision"]); +const needOf = (spec) => spec.statedReadiness !== "ready" ? (spec.derivedReadiness === "ready" ? "to state" : "to design") + : settledKinds.has(spec.specKind) ? "settled" + : spec.deliveryFacts.includes("implemented") ? "built" : "to build"; +const open = (need) => need === "to design" || need === "to state" || need === "to build"; +const waits = (spec) => rungs.indexOf(spec.derivedReadiness) > rungs.indexOf(spec.statedReadiness); +const drifts = (spec) => spec.statedReadiness !== "ready" && spec.deliveryFacts.includes("implemented"); +const holds = (spec) => spec.statedNextRungFailures.map((failure) => ({ clause: failure.clauseId, targets: (failure.targets ?? []).map((target) => target.id) })); +const questionRow = (spec, question) => ({ address: question.key === undefined ? null : `${spec.id}#question.${question.key}`, spec: spec.id, + index: question.index, question: question.question, blocking: question.blocking, decider: question.decider ?? null, provisional: question.provisional ?? null }); +const scopeOf = (id) => { + const seen = new Set([id]); + const queue = [id]; + for (let at = 0; at < queue.length; at += 1) for (const child of children.get(queue[at]) ?? []) if (!seen.has(child)) { seen.add(child); queue.push(child); } + return [...seen].sort(compare); +}; + +// The statement queue owns dependency order: its places order every waiting statement, and its +// `restsOn` names the waiting Specs each statement rests on. +const queue = await recipes(46, { decider }); +const queuePlace = new Map((queue.statements ?? []).map((row) => [row.id, row.place])); +const queueRestsOn = new Map((queue.statements ?? []).map((row) => [row.id, row.restsOn])); +const byQueue = (a, b) => (queuePlace.get(a) ?? Infinity) - (queuePlace.get(b) ?? Infinity) || compare(a, b); + +const roadmaps = selected.map((pack) => { + const members = pack.members; + const scopes = members.map((id) => new Set(contexts.has(id) ? scopeOf(id) : [])); + const placeOf = (id) => { const at = scopes.findIndex((scope) => scope.has(id)); return at === -1 ? null : at + 1; }; + const entries = [], apart = [], unresolved = []; + const questionRows = new Map(), undecided = new Map(), statements = new Map(); + const touch = (map, key, row, place) => { const existing = map.get(key) ?? { ...row, places: [] }; if (!existing.places.includes(place)) existing.places.push(place); map.set(key, existing); }; + members.forEach((id, index) => { + const place = index + 1; + const spec = contexts.get(id); + if (spec === undefined) { unresolved.push({ place, id }); return; } + const scope = scopes[index]; + const scopeIds = [...scope]; + const work = scopeIds.filter((member) => !settledKinds.has(contexts.get(member).specKind)); + const needs = work.map((member) => needOf(contexts.get(member))); + + // Decisions the entry holds or names by decidedBy, and what waits on a person over both. + const decisionIds = new Set(scopeIds.filter((member) => contexts.get(member).specKind === "decision")); + for (const member of scopeIds) for (const end of contexts.get(member).relationsOut) if (end.claim === "declared" && end.type === "decidedBy" && contexts.has(end.otherId)) decisionIds.add(end.otherId); + const shaping = [...decisionIds].filter((decision) => !scope.has(decision)).sort(compare); + const ownStatements = []; + for (const member of [...scopeIds, ...shaping]) { + const context = contexts.get(member); + for (const question of context.designColumns.openQuestions) { + const row = questionRow(context, question); + if (question.decider === decider) touch(questionRows, `${member}\u0000${question.index}`, row, place); + else if (question.decider === undefined) touch(undecided, `${member}\u0000${question.index}`, row, place); + } + if (waits(context)) { touch(statements, member, { id: member, statedReadiness: context.statedReadiness, floorReached: context.derivedReadiness }, place); ownStatements.push(member); } + } + if (work.length === 0) { apart.push({ place, id, specKind: spec.specKind, statedReadiness: spec.statedReadiness, need: needOf(spec) }); return; } + + const need = ["to design", "to state", "to build"].find((value) => needs.includes(value)) ?? "built"; + const decisions = [...decisionIds].sort(compare).map((decision) => contexts.get(decision)).filter((decision) => decision.statedReadiness !== "ready") + .map((decision) => ({ id: decision.id, title: decision.title ?? null, statedReadiness: decision.statedReadiness, + blockingQuestions: decision.designColumns.openQuestions.filter((question) => question.blocking).map((question) => questionRow(decision, question)) })); + const blockingQuestions = scopeIds.flatMap((member) => contexts.get(member).designColumns.openQuestions.filter((question) => question.blocking).map((question) => questionRow(contexts.get(member), question))); + + // What the entry rests on: any Spec of its scope reaching outside it, whichever part reaches. + const restsOn = new Map(); + for (const member of scopeIds) for (const end of contexts.get(member).relationsOut) { + if (end.claim !== "declared" || scope.has(end.otherId) || !contexts.has(end.otherId)) continue; + if (!(end.type === "dependsOn" || end.type === "constrainedBy" || end.type === "refines")) continue; + const target = contexts.get(end.otherId); + const targetNeed = needOf(target); + if (!open(targetNeed)) continue; + const key = `${end.otherId}\u0000${end.type}`; + const row = restsOn.get(key) ?? { id: end.otherId, via: end.type, from: [], need: targetNeed, statedReadiness: target.statedReadiness, holdsReady: rungs.indexOf(target.statedReadiness) < rungs.indexOf("defined"), onRoadmap: placeOf(end.otherId) }; + if (!row.from.includes(member)) row.from.push(member); + restsOn.set(key, row); + } + for (const row of restsOn.values()) row.from.sort(compare); + const restedOnBy = new Map(); + for (const member of scopeIds) for (const end of contexts.get(member).relationsIn) { + if (end.claim !== "declared" || scope.has(end.otherId) || !contexts.has(end.otherId)) continue; + if (!(end.type === "dependsOn" || end.type === "constrainedBy" || end.type === "decidedBy")) continue; + const source = contexts.get(end.otherId); + const sourceNeed = needOf(source); + if (!open(sourceNeed)) continue; + restedOnBy.set(`${end.otherId}\u0000${end.type}`, { id: end.otherId, via: end.type, need: sourceNeed, onRoadmap: placeOf(end.otherId) }); + } + + const byIdThenVia = (a, b) => compare(a.id, b.id) || compare(a.via, b.via); + entries.push({ + place, id, title: spec.title ?? null, specKind: spec.specKind, altitude: spec.altitude, need, + statedReadiness: spec.statedReadiness, floorReached: spec.derivedReadiness ?? null, + statedNextRung: spec.statedNextRung ?? null, holdsNextRung: holds(spec), + work: { specs: work.length, toDesign: needs.filter((value) => value === "to design").length, toState: needs.filter((value) => value === "to state").length, toBuild: needs.filter((value) => value === "to build").length, + built: needs.filter((value) => value === "built").length, drift: work.filter((member) => drifts(contexts.get(member))).length }, + workRows: work.map((member) => { const context = contexts.get(member); return { id: member, need: needOf(context), statedReadiness: context.statedReadiness, + floorReached: context.derivedReadiness ?? null, statedNextRung: context.statedNextRung ?? null, holdsNextRung: holds(context), drift: drifts(context) }; }), + buildNow: work.filter((member) => needOf(contexts.get(member)) === "to build"), + blockingQuestions, decisions, + restsOn: [...restsOn.values()].sort(byIdThenVia), restedOnBy: [...restedOnBy.values()].sort(byIdThenVia), + earlierStatements: [], own: new Set([...scopeIds, ...shaping]), ownStatements, + packs: [...spec.packs].filter((packId) => packId !== pack.id).sort(compare), + sharesScopeWith: scopes.map((other, at) => at !== index && scopeIds.some((member) => other.has(member)) ? at + 1 : null).filter((value) => value !== null), + }); + }); + // Collect all touches before classifying earlier statements: later entries can name a target. + for (const row of entries) { + const earlier = new Set(); + for (const statement of row.ownStatements) for (const target of queueRestsOn.get(statement) ?? []) earlier.add(target); + row.earlierStatements = [...earlier].sort(byQueue).map((target) => { + const touched = statements.get(target)?.places ?? []; + const where = row.own.has(target) ? "entry" : touched.length > 0 ? "roadmap" : "outside"; + return { id: target, where, onRoadmap: where === "roadmap" ? Math.min(...touched) : null }; + }); + delete row.own; + delete row.ownStatements; + } + // Only questions sort by roadmap place; statements keep the queue rank. + const byQuestion = (a, b) => a.places[0] - b.places[0] || Number(b.blocking) - Number(a.blocking) || compare(a.spec, b.spec) || a.index - b.index; + const waiting = { + questions: [...questionRows.values()].sort(byQuestion), + withoutDecider: [...undecided.values()].sort(byQuestion), + statements: [...statements.values()].sort((a, b) => byQueue(a.id, b.id)), + }; + return { + pack: pack.id, title: pack.title ?? null, framing: pack.framing ?? null, order: pack.order === "priority" ? "priority" : "reading", + totals: { entries: members.length, toDesign: entries.filter((row) => row.need === "to design").length, toState: entries.filter((row) => row.need === "to state").length, toBuild: entries.filter((row) => row.need === "to build").length, + built: entries.filter((row) => row.need === "built").length, apart: apart.length, unresolved: unresolved.length, + questions: waiting.questions.length, blockingQuestions: waiting.questions.filter((row) => row.blocking).length, + withoutDecider: waiting.withoutDecider.length, blockingWithoutDecider: waiting.withoutDecider.filter((row) => row.blocking).length, + statements: waiting.statements.length }, + entries, apart, unresolved, waiting, + }; +}); +return { decider, roadmaps }; +``` + +## 54. The design slice + +*When you need this: you are reviewing features that must be reviewed together and want the whole at once.* + +Supply exactly one scope: `params.pack`, a resolving Pack id, or `params.specs`, a non-empty +ordered list of distinct resolving Spec ids. Beside a Pack, `params.members` narrows the scope to +an ordered, non-empty list of distinct resolved members. The Pack scope names unresolved +manifest members, preserves authored order, and marks a narrowed scope as a subset. + +`params.detail` defaults to `false` for a summary, is `true` for every row in full, or lists +member ids whose full rows appear beside summary rows. Unknown detail ids add +`{ id, reason: "not-member" }` diagnostics. A summary preserves every total and names omitted +detail in `leftOut`. Unknown parameter names return `{ unknownParams, accepts }` alone. + +Input checks accumulate in this order; a check needing an invalid earlier value is skipped. +A refused input returns `{ diagnostics }` without a slice: + +| Input | Diagnostic scope | Problem | +| --- | --- | --- | +| Detail is neither boolean nor a list of strings, including null | detail | Expected true, false or a list of member ids. | +| Neither or both scopes supplied | scope | Supply exactly one of pack or specs. | +| Pack does not resolve | pack | Pack does not resolve. | +| Members is not a non-empty list of distinct strings | members | Expected a non-empty list of distinct member ids. | +| Each unresolved or outside Pack member, in list order | the id | Not a resolved member of the Pack. | +| Specs is not a non-empty list of distinct strings | specs | Expected a non-empty list of distinct Spec ids. | +| Each unresolved Spec, in list order | the id | Spec does not resolve. | +| Members supplied beside Specs | members | members narrows only a Pack scope. | + +The answer is `{ scope, totals, members, structure, boundary, architecture, decisions, questions }`. +Members stay in scope order, with their depth, next-rung holds and design columns. Structure +lists typed relations with their ordering mark and reasons, and cycles with a closed path. +`structure.unresolved` lists unresolved outgoing typed targets apart from the resolved boundary, +with their relation type and optional reason, and no invented title or rung. +Boundary rows extend Pack design with floors and held members, marking excluded Pack members +with `inPack`; a full row's `via` includes `reasons: [{ member, reason }]` only for relations that +state a reason. Architecture keeps touched anchored components, member component designs, +anchored component pairs with unit edges, planned uses, one-step outside components, and +unplaced units apart. A member's bound unit whose component membership does not resolve stays +unplaced and touches no invented component. Decisions include member decisions and those members name; questions +include members', decisions', and blocking questions of outside Specs holding a member. +Totals count these readings without a score, finding, schedule or gate. The law and complete +row shapes are `spec:consumers.design-slice`. + +```js +const accepts = ["pack", "specs", "members", "detail"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; +const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0); +const strings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string"); +const idList = (value) => strings(value) && value.length > 0 && new Set(value).size === value.length; +const diagnostics = []; +const bad = (scope, problem) => diagnostics.push({ scope, problem }); +const packId = params.pack; +const specIds = params.specs; +const subset = params.members; +const detail = params.detail === undefined ? false : params.detail; +// Collect independent failures before composing any answers. +if (typeof detail !== "boolean" && !strings(detail)) bad("detail", "Expected true, false or a list of member ids."); +let pack = null; +let ids = []; +const unresolved = []; +if ((packId !== undefined) === (specIds !== undefined)) bad("scope", "Supply exactly one of pack or specs."); +else if (packId !== undefined) { + pack = typeof packId === "string" ? g.packContext(packId) ?? null : null; + if (pack === null) bad("pack", "Pack does not resolve."); + else { + for (const member of pack.members) (member.resolved ? ids : unresolved).push(member.id); + if (subset !== undefined) { + if (!idList(subset)) bad("members", "Expected a non-empty list of distinct member ids."); + else { + const strangers = subset.filter((id) => !ids.includes(id)); + for (const id of strangers) bad(id, "Not a resolved member of the Pack."); + if (strangers.length === 0) ids = subset; + } + } + } +} else { + if (!idList(specIds)) bad("specs", "Expected a non-empty list of distinct Spec ids."); + else { + for (const id of specIds) if (!g.specContext(id)) bad(id, "Spec does not resolve."); + ids = specIds; + } + if (subset !== undefined) bad("members", "members narrows only a Pack scope."); +} +if (diagnostics.length) return { diagnostics }; + +const inSlice = new Set(ids); +const position = new Map(ids.map((id, index) => [id, index])); +const context = new Map(ids.map((id) => [id, g.specContext(id)])); +const rungs = ["idea", "scoped", "defined", "ready"]; +const typed = ["refines", "dependsOn", "constrainedBy", "decidedBy"]; + +// Keep the unresolvable outgoing ends apart from the resolved boundary. +const unresolvedEdges = ids.flatMap((id) => context.get(id).relationsOut + .filter((end) => end.claim === "declared" && typed.includes(end.type) && !end.resolved) + .map((end) => ({ from: id, to: end.otherId, type: end.type, ...(end.reason === undefined ? {} : { reason: end.reason }) }))); + +// Structure inside the slice: the four typed relations between members. +const edgeKeys = new Set(); +const edges = []; +for (const id of ids) { + for (const end of context.get(id).relationsOut) { + if (end.claim !== "declared" || !typed.includes(end.type) || !inSlice.has(end.otherId) || end.otherId === id) continue; + const key = `${id}\u0000${end.type}\u0000${end.otherId}`; + if (edgeKeys.has(key)) continue; + edgeKeys.add(key); + edges.push({ from: id, to: end.otherId, type: end.type, orders: true, ...(end.reason === undefined ? {} : { reason: end.reason }) }); + } +} +edges.sort((a, b) => compare(a.from, b.from) || compare(a.type, b.type) || compare(a.to, b.to)); +const names = (from, to, types) => edges.some((edge) => edge.from === from && edge.to === to && types.includes(edge.type)); +const restsOn = new Map(ids.map((id) => [id, new Set()])); +for (const edge of edges) { + // Keep the edge even when its reverse dependency determines the order. + edge.orders = !(edge.type === "refines" && names(edge.to, edge.from, typed.slice(1))); + if (edge.orders) restsOn.get(edge.from).add(edge.to); +} +const restedOnBy = new Map(ids.map((id) => [id, new Set()])); +for (const [from, targets] of restsOn) for (const to of targets) restedOnBy.get(to).add(from); + +// Strongly connected sets, with an explicit frame stack, as the statement queue finds them. +const sorted = [...ids].sort(compare); +const targetsOf = (id) => [...restsOn.get(id)].sort(compare); +const order = new Map(); +const low = new Map(); +const stack = []; +const onStack = new Set(); +const sets = []; +const visit = (id) => { order.set(id, order.size); low.set(id, order.get(id)); stack.push(id); onStack.add(id); }; +for (const root of sorted) { + if (order.has(root)) continue; + visit(root); + const frames = [{ id: root, at: 0 }]; + while (frames.length) { + const frame = frames[frames.length - 1]; + const list = targetsOf(frame.id); + if (frame.at < list.length) { + const next = list[frame.at++]; + if (!order.has(next)) { visit(next); frames.push({ id: next, at: 0 }); } + else if (onStack.has(next)) low.set(frame.id, Math.min(low.get(frame.id), order.get(next))); + continue; + } + frames.pop(); + if (frames.length) { + const parent = frames[frames.length - 1].id; + low.set(parent, Math.min(low.get(parent), low.get(frame.id))); + } + if (low.get(frame.id) === order.get(frame.id)) { + const members = []; + let popped; + do { popped = stack.pop(); onStack.delete(popped); members.push(popped); } while (popped !== frame.id); + sets.push(members.sort(compare)); + } + } +} +const setOf = new Map(); +sets.forEach((members, index) => { for (const id of members) setOf.set(id, index); }); +// Tarjan emits a set only after every set it rests on, so one pass in emission order assigns depth. +const setDepth = []; +sets.forEach((members, index) => { + let deepest = 0; + for (const id of members) for (const target of restsOn.get(id)) { + const other = setOf.get(target); + if (other !== index) deepest = Math.max(deepest, setDepth[other]); + } + setDepth[index] = deepest + 1; +}); +const depthOf = (id) => setDepth[setOf.get(id)]; +const cycleThrough = (members) => { + const inside = new Set(members); + const start = members[0]; + const parent = new Map(); + const seen = new Set([start]); + const queue = [start]; + while (queue.length) { + const current = queue.shift(); + for (const next of targetsOf(current).filter((id) => inside.has(id))) { + if (next === start) { + const path = [start]; + for (let step = current; step !== start; step = parent.get(step)) path.splice(1, 0, step); + return [...path, start]; + } + if (!seen.has(next)) { seen.add(next); parent.set(next, current); queue.push(next); } + } + } + return []; +}; +const cycles = sets.filter((members) => members.length > 1).sort((a, b) => compare(a[0], b[0])) + .map((members) => ({ members, cycle: cycleThrough(members) })); + +// Inspect both directions before choosing a parent for the example. +const exampleOf = new Map(); +for (const id of ids) { + const spec = context.get(id); + if (spec.specKind !== "example") continue; + const typedEnds = (list) => [...new Map(list.filter((end) => end.claim === "declared" && typed.includes(end.type)).map((end) => [`${end.type}\u0000${end.otherId}`, end])).values()]; + const out = typedEnds(spec.relationsOut); + const into = typedEnds(spec.relationsIn); + if (out.length === 1 && into.length === 0 && out[0].type === "refines" && out[0].otherId !== id && inSlice.has(out[0].otherId)) exampleOf.set(id, out[0].otherId); +} + +// Count the primary owner rules and flows without evaluating coverage for each member. +const primitive = new Map(graph.nodes.filter((node) => node.nodeType === "Primitive").map((node) => [node.id, node])); +const ruleCounts = new Map(ids.map((id) => { + const behavior = primitive.get(id)?.sections?.behavior; + return [id, { total: (behavior?.rules?.length ?? 0) + (behavior?.flows?.length ?? 0), keyed: Object.keys(behavior?.ruleKeys ?? {}).length }]; +})); +const architectureMap = await recipes(17); +const componentRow = new Map(architectureMap.components.filter((row) => row.declared).map((row) => [row.id, row])); +const unitComponent = new Map(); +for (const row of componentRow.values()) for (const member of row.members) unitComponent.set(member.id, row.id); +const componentOfUnit = (unitId) => (componentRow.has(unitId) ? unitId : unitComponent.get(unitId)); +const comparison = await recipes(43); +const decisionRegister = await recipes(31); +const decisionRow = new Map(decisionRegister.rows.map((row) => [row.id, row])); +const questionRegister = await recipes(20); +const questionsOf = new Map(questionRegister.specs.map((row) => [row.id, row.questions])); + +// Member rows. +const members = ids.map((id) => { + const spec = context.get(id); + const at = rungs.indexOf(spec.statedReadiness); + const statedNextRung = at >= 0 && at < rungs.length - 1 ? rungs[at + 1] : null; + const components = new Set(); + for (const unit of [...spec.implementations, ...spec.references]) { + const componentId = componentOfUnit(unit.codeId); + if (componentId !== undefined) components.add(componentId); + } + const verifiers = spec.verifiers; + const examples = verifiers.filter((verifier) => verifier.via === "example" && g.specContext(verifier.verifierId)?.specKind === "example"); + return { + id, + title: spec.title ?? null, + specKind: spec.specKind, + altitude: spec.altitude, + statedReadiness: spec.statedReadiness, + floorReached: spec.derivedReadiness ?? "none", + statedNextRung, + holds: spec.statedNextRungFailures.map((failure) => ({ clauseId: failure.clauseId, description: failure.description, + targets: (failure.targets ?? []).map((target) => ({ type: target.type, id: target.id, statedReadiness: target.statedReadiness ?? null })) })), + depth: depthOf(id), + restsOn: [...restsOn.get(id)].sort(compare), + restedOnBy: [...restedOnBy.get(id)].sort(compare), + exampleOf: exampleOf.get(id) ?? null, + exampleMembers: ids.filter((other) => exampleOf.get(other) === id), + design: { + entries: spec.designColumns.entries, + declarations: spec.designColumns.declarations, + openQuestions: spec.designColumns.openQuestions.length, + blockingQuestions: spec.designColumns.blockingQuestions, + decisions: spec.designColumns.decisions.length, + }, + rules: ruleCounts.get(id), + examples: { total: examples.length, enabled: examples.filter((example) => example.enabled).length }, + verifiers: { total: verifiers.length, enabled: verifiers.filter((verifier) => verifier.enabled).length }, + implementedBy: spec.implementations.map((unit) => unit.codeId).sort(compare), + referencedBy: spec.references.map((unit) => unit.codeId).sort(compare), + components: [...components].sort(compare), + }; +}); +const memberRow = new Map(members.map((row) => [row.id, row])); + +// The boundary over the slice members, as Pack design builds the Pack boundary. +const directions = { restsOn: new Map(), restedOnBy: new Map(), verifiedBy: new Map(), supersededBy: new Map() }; +const join = (direction, otherId, type, memberId) => { + const row = directions[direction].get(otherId) ?? new Map(); + const list = row.get(type) ?? []; + if (!list.includes(memberId)) list.push(memberId); + row.set(type, list); + directions[direction].set(otherId, row); +}; +const isSpec = (end) => end.resolved && end.otherNodeType === "Primitive"; +for (const id of ids) { + const spec = context.get(id); + for (const end of spec.relationsOut) if (end.claim === "declared" && typed.includes(end.type) && isSpec(end) && !inSlice.has(end.otherId)) join("restsOn", end.otherId, end.type, id); + for (const end of spec.relationsIn) { + if (end.claim !== "declared" || !isSpec(end) || inSlice.has(end.otherId)) continue; + if (typed.includes(end.type)) join("restedOnBy", end.otherId, end.type, id); + else if (end.type === "verifies") join("verifiedBy", end.otherId, end.type, id); + else if (end.type === "supersedes") join("supersededBy", end.otherId, end.type, id); + } +} +const relationOrder = ["refines", "dependsOn", "constrainedBy", "decidedBy", "verifies", "supersedes"]; +const packMembers = new Set(pack === null ? [] : pack.members.map((member) => member.id)); +const holdsTarget = (otherId) => members.filter((row) => row.holds.some((hold) => hold.targets.some((target) => target.id === otherId))).map((row) => row.id); +const boundaryRows = (direction) => [...directions[direction]].sort(([a], [b]) => compare(a, b)).map(([otherId, byType]) => { + const other = g.specContext(otherId); + return { + id: otherId, + title: other.title ?? null, + specKind: other.specKind, + statedReadiness: other.statedReadiness, + floorReached: other.derivedReadiness ?? "none", + implemented: other.deliveryFacts.includes("implemented"), + via: [...byType].sort(([a], [b]) => relationOrder.indexOf(a) - relationOrder.indexOf(b)) + .map(([type, list]) => { const reasons = list.flatMap((member) => { + const ends = direction === "restsOn" ? context.get(member).relationsOut : context.get(member).relationsIn; + return ends.filter((end) => end.claim === "declared" && end.type === type && end.otherId === otherId && end.reason !== undefined).map((end) => ({ member, reason: end.reason })); + }); return { type, members: [...list].sort((a, b) => position.get(a) - position.get(b)), ...(reasons.length ? { reasons } : {}) }; }), + holds: direction === "restsOn" ? holdsTarget(otherId) : [], + ...(packMembers.has(otherId) ? { inPack: true } : {}), + }; +}); +const boundary = Object.fromEntries(Object.keys(directions).map((direction) => [direction, boundaryRows(direction)])); + +// Parse declared values with the same opening-span grammar as recipe 43. +const read = (value) => (typeof value === "string" ? (/^(`+)(?!`)([^\r\n]+?)(? node.nodeType === "CodeNode" && node.claim === "anchored" && node.id.startsWith("component:")).map((node) => node.id)); +const satisfiesPairs = new Set(graph.edges.filter((edge) => edge.type === "satisfies" && edge.claim === "anchored").map((edge) => `${edge.from}\u0000${edge.to}`)); +const designs = []; +for (const id of ids) { + const entries = primitive.get(id)?.sections?.design; + const componentId = read(entries?.id); + if (componentId === null || !componentForm.test(componentId)) continue; + const declared = { layer: null, context: null, role: null, uses: null }; + for (const key of ["layer", "context", "role", "uses"]) { + if (entries[key] === undefined) continue; + const value = read(entries[key]); + if (key === "uses") { + const list = value === "none" ? [] : value?.split(",").map((item) => item.trim()); + if (list !== undefined && list.every((item) => componentForm.test(item))) declared.uses = [...new Set(list)].sort(compare); + } else if (value !== null && (key === "layer" ? layers.includes(value) : token.test(value))) declared[key] = value; + } + const anchorExists = anchoredComponents.has(componentId); + designs.push({ + spec: id, + componentId, + ...declared, + anchorExists, + realized: anchorExists && satisfiesPairs.has(`${componentId}\u0000${id}`), + differing: comparison.differing.filter((row) => row.spec === id).map(({ field, planned, realized }) => ({ field, planned, realized })), + unreadable: comparison.unreadable.filter((row) => row.spec === id).map(({ key, value }) => ({ key, value })), + }); +} +designs.sort((a, b) => compare(a.spec, b.spec)); + +// The anchored components the slice touches: through member bindings, through the code that +// satisfies what the slice rests on, and through member designs that name an anchored component. +const touched = new Map(); +const touch = (componentId) => { + if (!touched.has(componentId)) touched.set(componentId, { satisfies: new Set(), references: new Set(), restsOn: new Set(), designs: new Set() }); + return touched.get(componentId); +}; +const unplaced = []; +for (const id of ids) { + const spec = context.get(id); + for (const [via, units] of [["satisfies", spec.implementations], ["references", spec.references]]) { + for (const unit of units) { + const componentId = componentOfUnit(unit.codeId); + if (componentId === undefined) unplaced.push({ unit: unit.codeId, member: id, via }); + else touch(componentId)[via].add(id); + } + } +} +for (const row of boundary.restsOn) { + for (const unit of g.specContext(row.id).implementations) { + const componentId = componentOfUnit(unit.codeId); + if (componentId !== undefined) touch(componentId).restsOn.add(row.id); + } +} +for (const design of designs) if (anchoredComponents.has(design.componentId)) touch(design.componentId).designs.add(design.spec); +const byPosition = (list) => [...list].sort((a, b) => position.get(a) - position.get(b)); +const components = [...touched].sort(([a], [b]) => compare(a, b)).map(([componentId, reach]) => { + const anchored = componentRow.get(componentId); + return { + id: componentId, + label: anchored?.label ?? null, + layer: anchored?.layer ?? null, + context: anchored?.context ?? null, + members: { satisfies: byPosition(reach.satisfies), references: byPosition(reach.references) }, + restsOn: [...reach.restsOn].sort(compare), + designs: byPosition(reach.designs), + }; +}); + +// Anchored uses aggregated to component pairs, as the architecture map counts them, each with the +// unit edges behind its count; pairs with one end outside the touched set are kept and marked. +const pairs = new Map(); +for (const edge of architectureMap.usesEdges) { + const from = componentOfUnit(edge.from); + const to = componentOfUnit(edge.to); + if (from === undefined || to === undefined || from === to || (!touched.has(from) && !touched.has(to))) continue; + const key = `${from}\u0000${to}`; + if (!pairs.has(key)) pairs.set(key, { from, to, count: 0, outside: !(touched.has(from) && touched.has(to)), units: [] }); + const pair = pairs.get(key); + pair.count += 1; + pair.units.push({ from: edge.from, to: edge.to, claim: edge.claim }); +} +const uses = [...pairs.values()].sort((a, b) => compare(a.from, b.from) || compare(a.to, b.to)); +// Planned uses: each component a member design declares, kept apart from the anchored pairs. +const designed = new Set(designs.map((design) => design.componentId)); +const plannedUses = designs.flatMap((design) => (design.uses ?? []).filter((to) => to !== design.componentId) + .map((to) => ({ design: design.spec, from: design.componentId, to, outside: !(touched.has(to) || designed.has(to)) }))) + .sort((a, b) => compare(a.from, b.from) || compare(a.to, b.to) || compare(a.design, b.design)); +// Components one step outside: reached by an anchored pair or named by a planned use; no further reach. +const reachOrder = ["touchedUses", "usesTouched", "plannedUse"]; +const outsideReach = new Map(); +const reachOutside = (id, how) => outsideReach.set(id, (outsideReach.get(id) ?? new Set()).add(how)); +for (const pair of uses) { + if (!touched.has(pair.to)) reachOutside(pair.to, "touchedUses"); + if (!touched.has(pair.from)) reachOutside(pair.from, "usesTouched"); +} +for (const use of plannedUses) if (use.outside) reachOutside(use.to, "plannedUse"); +const outside = [...outsideReach].sort(([a], [b]) => compare(a, b)).map(([id, how]) => ({ + id, + label: componentRow.get(id)?.label ?? null, + layer: componentRow.get(id)?.layer ?? null, + context: componentRow.get(id)?.context ?? null, + anchored: anchoredComponents.has(id), + reachedBy: reachOrder.filter((name) => how.has(name)), +})); + +// Decisions in scope: decision members and every decision a member names through decidedBy. +const decisionIds = new Set(ids.filter((id) => context.get(id).specKind === "decision")); +for (const id of ids) for (const decision of context.get(id).designColumns.decisions) if (decision.resolved) decisionIds.add(decision.id); +const decisions = [...decisionIds].sort(compare).map((decisionId) => { + const row = decisionRow.get(decisionId); + const spec = g.specContext(decisionId); + return { + id: decisionId, + title: spec.title ?? null, + statedReadiness: spec.statedReadiness, + floorReached: spec.derivedReadiness ?? "none", + member: inSlice.has(decisionId), + shapes: (row?.shapes ?? []).filter((id) => inSlice.has(id)).sort((a, b) => position.get(a) - position.get(b)), + refines: row?.refines ?? [], + supersedes: row?.supersedes ?? [], + supersededBy: row?.supersededBy ?? [], + openQuestions: spec.designColumns.openQuestions.length, + blockingQuestions: spec.designColumns.blockingQuestions, + }; +}); + +// Open questions in scope: members, decisions, and those of outside Specs that hold a member. +const holdingOutside = boundary.restsOn.filter((row) => row.holds.length > 0).map((row) => row.id); +const questionSources = [ + ...ids.map((id) => [id, "member"]), + ...decisions.filter((row) => !row.member).map((row) => [row.id, "decision"]), + ...holdingOutside.filter((id) => !decisionIds.has(id)).map((id) => [id, "restsOn"]), +]; +const questions = []; +for (const [specId, origin] of questionSources) { + const holdsOwn = memberRow.get(specId)?.holds.some((hold) => hold.clauseId === "no-blocking-open-questions") ?? false; + const heldMembers = holdsTarget(specId); + (questionsOf.get(specId) ?? []).forEach((question, index) => { + if (origin === "restsOn" && !question.blocking) return; + questions.push({ + address: question.key === null ? null : `${specId}#question.${question.key}`, + spec: specId, + origin, + index, + question: question.question, + blocking: question.blocking, + deferral: question.deferral, + decider: question.decider, + links: question.links, + provisional: question.provisional, + trigger: question.trigger, + holds: question.blocking ? [...(holdsOwn ? [specId] : []), ...heldMembers] : [], + }); + }); +} +questions.sort((a, b) => Number(b.blocking) - Number(a.blocking) || b.holds.length - a.holds.length || compare(a.spec, b.spec) || a.index - b.index); + +const count = (test) => members.filter(test).length; +const answer = { + scope: pack === null ? { specs: ids } : { pack: packId, title: pack.title ?? null, framing: pack.framing ?? null, subset: subset !== undefined, members: ids, unresolved }, + totals: { + members: members.length, + exampleMembers: exampleOf.size, + relations: edges.length, + depth: members.reduce((deepest, row) => Math.max(deepest, row.depth), 0), + cycles: cycles.length, + heldBelowNextRung: count((row) => row.holds.length > 0), + waitingForAuthor: count((row) => row.statedNextRung !== null && row.holds.length === 0), + restsOn: boundary.restsOn.length, + holdingOutside: holdingOutside.length, + restedOnBy: boundary.restedOnBy.length, + verifiedBy: boundary.verifiedBy.length, + supersededBy: boundary.supersededBy.length, + components: { + touched: components.length, + throughMembers: components.filter((row) => row.members.satisfies.length + row.members.references.length > 0).length, + throughRestsOn: components.filter((row) => row.restsOn.length > 0).length, + throughDesigns: components.filter((row) => row.designs.length > 0).length, + outside: outside.length, + }, + designs: { + total: designs.length, + realized: designs.filter((row) => row.realized).length, + matching: designs.filter((row) => row.realized && row.differing.length === 0 && row.unreadable.length === 0).length, + unrealized: designs.filter((row) => !row.realized).length, + differing: designs.reduce((total, row) => total + row.differing.length, 0), + unreadable: designs.reduce((total, row) => total + row.unreadable.length, 0), + }, + componentUses: { + inside: uses.filter((row) => !row.outside).length, + outside: uses.filter((row) => row.outside).length, + planned: plannedUses.length, + }, + decisions: decisions.length, + decisionsBelowReady: decisions.filter((row) => row.statedReadiness !== "ready").length, + openQuestions: questions.length, + blockingQuestions: questions.filter((row) => row.blocking).length, + }, + members, + structure: { edges, cycles, unresolved: unresolvedEdges }, + boundary, + architecture: { components, designs, uses, plannedUses, outside, unplaced }, + decisions, + questions, +}; +if (detail === true) return answer; + +// The summary keeps every total and names what it leaves out. +const selected = new Set(Array.isArray(detail) ? detail : []); +const memberSummary = (row) => selected.has(row.id) ? row : { + id: row.id, specKind: row.specKind, statedReadiness: row.statedReadiness, floorReached: row.floorReached, + statedNextRung: row.statedNextRung, holds: row.holds.map((hold) => ({ clauseId: hold.clauseId, targets: hold.targets.map((target) => target.id) })), depth: row.depth, restsOn: row.restsOn, exampleOf: row.exampleOf, + design: row.design, rules: row.rules, implementedBy: row.implementedBy.length, referencedBy: row.referencedBy.length, +}; +return { + ...answer, + members: members.map(memberSummary), + structure: { edges: edges.map(({ from, to, type, orders }) => ({ from, to, type, orders })), cycles, unresolved: unresolvedEdges.map(({ from, to, type }) => ({ from, to, type })) }, + boundary: Object.fromEntries(Object.entries(boundary).map(([direction, rows]) => [direction, rows.map((row) => ({ id: row.id, statedReadiness: row.statedReadiness, holds: row.holds, ...(row.inPack ? { inPack: true } : {}) }))])), + architecture: { + components: components.map((row) => ({ id: row.id, layer: row.layer, context: row.context, + members: { satisfies: row.members.satisfies.length, references: row.members.references.length }, restsOn: row.restsOn.length, designs: row.designs.length })), + designs: designs.map((row) => ({ spec: row.spec, componentId: row.componentId, anchorExists: row.anchorExists, realized: row.realized, + differing: row.differing.length, unreadable: row.unreadable.length })), + uses: uses.map(({ from, to, count, outside: beyond }) => ({ from, to, count, outside: beyond })), + plannedUses, + outside: outside.map(({ id, anchored, reachedBy }) => ({ id, anchored, reachedBy })), + unplaced, + }, + decisions: decisions.map((row) => ({ id: row.id, statedReadiness: row.statedReadiness, member: row.member, shapes: row.shapes.length })), + questions: questions.map((row) => (row.blocking ? row : { address: row.address, spec: row.spec, index: row.index })), + leftOut: [ + "Titles, altitudes, rested-on-by and example member lists, example and verifier counts, and unit and component ids of unselected members; hold descriptions, target types and rungs; unit counts and hold target ids kept.", + "Relation reasons; relation types kept.", + "Boundary titles, kinds, floors, implementation bindings and joined members; ids, stated rungs and holds kept.", + "Component labels and the Specs that reach each component; counts kept.", + "Declared values, differing rows and unreadable rows of each member design; their counts kept.", + "The unit edges behind each component pair; counts kept.", + "Labels, layers and contexts of outside components.", + "Decision titles, floors, refinements, supersessions and question counts; shaped member ids, with their count kept.", + "Non-blocking question text and fields; addresses kept.", + ], + ...(Array.isArray(detail) ? { diagnostics: detail.filter((id) => !inSlice.has(id)).map((id) => ({ id, reason: "not-member" })) } : {}), +}; +``` + +## 55. The roadmap memo + +Render the roadmap selected by `params.pack` and `params.decider` as a Markdown forward section. +Without a Pack, exactly one priority Pack must exist. The roadmap and open-question register +supply the reading. Purpose and reach describes intent and structural reach; the author supplies +the reason for an entry's place around the memo. + +The answer is `{ diagnostics: [], memo }`, with `memo` one Markdown string. A missing or ambiguous +Pack selection or an invalid decider returns `{ diagnostics: [{ parameter, problem }], memo: null }`. +The answer and section formats are defined by `spec:consumers.roadmap.memo#design.memoRecipe` +and its following entries. + +```js +const accepts = ["pack", "decider"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; +let pack = params.pack; +if (pack === undefined) { + const priority = graph.nodes.filter((node) => node.nodeType === "Pack" && node.order === "priority"); + if (priority.length !== 1) return { diagnostics: [{ parameter: "pack", problem: priority.length === 0 ? "No Pack states order: priority; name one." : "Several Packs state order: priority; name one." }], memo: null }; + pack = priority[0].id; +} +const decider = params.decider === null ? null : params.decider ?? "owner"; +const answer = await recipes(53, { pack, decider }); +if (answer.diagnostics !== undefined) return { diagnostics: answer.diagnostics, memo: null }; +const roadmap = answer.roadmaps[0]; +const register = await recipes(20); + +const span = (text) => { + const value = String(text); + const longest = Math.max(0, ...[...value.matchAll(/`+/gu)].map((match) => match[0].length)); + const ticks = "`".repeat(longest + 1); + const pad = value.startsWith("`") || value.endsWith("`") ? " " : ""; + return `${ticks}${pad}${value}${pad}${ticks}`; +}; +const html = (value) => value.replace(/[<>&]/gu, (char) => ({ "<": "<", ">": ">", "&": "&" })[char]); +const text = (value) => { + const source = String(value).replace(/\r\n|\r|\n/gu, " "); + const runs = [...source.matchAll(/`+/gu)]; + let at = 0, out = ""; + for (let i = 0; i < runs.length; i += 1) { + const open = runs[i]; + const closing = runs.findIndex((run, j) => j > i && run[0].length === open[0].length); + if (closing < 0) continue; + const close = runs[closing]; + out += html(source.slice(at, open.index)) + source.slice(open.index, close.index + close[0].length); + at = close.index + close[0].length; + i = closing; + } + return out + html(source.slice(at)); +}; +const cell = (value) => text(value).replace(/\|/gu, "\\|"); +const lines = []; +const push = (...values) => lines.push(...values); +const contexts = new Map(roadmap.entries.map((entry) => [entry.id, g.specContext(entry.id)])); +const purposeAndReach = (entry) => { + const intent = contexts.get(entry.id).sections?.intent ?? {}; + const line = (intent.problem ?? intent.outcome ?? "").trim(); + const own = line === "" || line.endsWith(".") ? line : `${line}.`; + const derived = []; + const waitingSpecs = new Set(entry.restedOnBy.map((row) => row.id)).size; + if (waitingSpecs > 0) derived.push(`Waited on by ${waitingSpecs}.`); + if (entry.buildNow.length > 0 && entry.need !== "to build") derived.push(`${entry.buildNow.length} of its parts ready to build now.`); + return [own, ...derived].filter((part) => part !== "").join(" "); +}; +const holdText = (clauses) => clauses.length === 0 ? "nothing holds it" : `held by ${clauses.map((row) => row.targets.length === 0 ? row.clause : `${row.clause} (${row.targets.map(span).join(", ")})`).join(", ")}`; +const entryCell = (entry) => `${entry.title ?? entry.id} ${span(entry.id)}`; + +push("## The next work, from the roadmap", ""); +push(`Roadmap: ${text(roadmap.title ?? roadmap.pack)} ${span(roadmap.pack)}`, ""); +push("To state means the floor allows ready and a person still judges the statement. Built means every Spec of the entry\u0027s work states ready and has an implementation binding; it reports no passing test and no deployment.", ""); +for (const paragraph of (roadmap.framing ?? "").split(/\n\s*\n/u).map((part) => part.trim()).filter((part) => part !== "")) push(text(paragraph), ""); +if (roadmap.order !== "priority") push("This Pack\u0027s order is reading order, not a stated priority.", ""); + +const need = (name) => roadmap.entries.filter((entry) => entry.need === name); +push("### To design", ""); +if (need("to design").length === 0) push("No entry needs design.", ""); +else { + push("| Place | Entry | Next | Purpose and reach |", "| --- | --- | --- | --- |"); + for (const entry of need("to design")) { + const next = []; + // Put the entry before its descendants, then truncate. + const parts = entry.workRows.filter((row) => row.need === "to design"); + const shown = [...parts.filter((row) => row.id === entry.id), ...parts.filter((row) => row.id !== entry.id)].slice(0, 3); + for (const row of shown) next.push(`${row.id === entry.id ? "itself" : span(row.id)} ${span(row.statedReadiness)} → ${span(row.statedNextRung)}, ${holdText(row.holdsNextRung)}`); + if (parts.length > shown.length) next.push(`and ${parts.length - shown.length} more parts to design`); + const blocked = entry.blockingQuestions.map((question) => question.address === null ? span(question.spec) : span(question.address)); + if (blocked.length > 0) next.push(`blocked by ${blocked.join(", ")}`); + if (entry.decisions.length > 0) next.push(`decided by ${entry.decisions.map((decision) => `${span(decision.id)} (${decision.statedReadiness})`).join(", ")}`); + push(`| ${entry.place} | ${cell(entryCell(entry))} | ${cell(next.join("; "))} | ${cell(purposeAndReach(entry))} |`); + } + push(""); +} +if (need("to state").length > 0) { + push("### To state", ""); + push("| Place | Entry | Earlier waiting statements | Purpose and reach |", "| --- | --- | --- | --- |"); + for (const entry of need("to state")) { + const rows = entry.earlierStatements; + const parts = []; + const inside = rows.filter((row) => row.where === "entry"); + const onRoadmap = rows.filter((row) => row.where === "roadmap"); + const outside = rows.filter((row) => row.where === "outside"); + if (inside.length > 0) parts.push(`in this entry ${inside.map((row) => span(row.id)).join(", ")}`); + if (onRoadmap.length > 0) parts.push(`on the roadmap ${onRoadmap.map((row) => `${span(row.id)} (place ${row.onRoadmap})`).join(", ")}`); + if (outside.length > 0) parts.push(`${outside.length} outside the roadmap`); + const earlier = parts.length === 0 ? "No preceding waiting statement in the statement queue." : parts.join("; "); + push(`| ${entry.place} | ${cell(entryCell(entry))} | ${cell(earlier)} | ${cell(purposeAndReach(entry))} |`); + } + push(""); +} +push("### To build", ""); +if (need("to build").length === 0) push("No entry is ready to build.", ""); +else { + push("| Place | Entry | Purpose and reach | Rests on |", "| --- | --- | --- | --- |"); + for (const entry of need("to build")) { + const rests = [...new Map(entry.restsOn.map((row) => [row.id, row])).values()].map((row) => `${span(row.id)} (${row.need})`); + push(`| ${entry.place} | ${cell(entryCell(entry))} | ${cell(purposeAndReach(entry))} | ${cell(rests.length === 0 ? "nothing unbuilt" : rests.join(", "))} |`); + } + push(""); +} +const questions = roadmap.waiting.questions; +const statements = roadmap.waiting.statements; +const questionLine = (question) => `- ${question.address === null ? span(question.spec) : span(question.address)}${question.blocking ? " (blocking)" : ""}, place ${question.places.join(", ")}: ${text(question.question)}`; +if (questions.length + statements.length > 0) { + push(`### Waiting on the ${answer.decider}`, ""); + for (const question of questions) push(questionLine(question)); + for (const statement of statements) push(`- State ${span(statement.id)}: it states ${span(statement.statedReadiness)} and its floor allows ${span(statement.floorReached)}; place ${statement.places.join(", ")}`); + push(""); +} +if (roadmap.waiting.withoutDecider.length > 0) { + push("### Questions without a decider", "", "These questions have no named decider.", ""); + // Addresses keep this list short; the page carries the question text. + for (const question of roadmap.waiting.withoutDecider) push(`- ${question.address === null ? `${span(question.spec)} question ${question.index + 1}` : span(question.address)}${question.blocking ? " (blocking)" : ""}, place ${question.places.join(", ")}`); + push(""); +} +const built = need("built"); +if (built.length > 0) { + push("### Built and still on the roadmap", ""); + for (const entry of built) push(`- Place ${entry.place}: ${text(entryCell(entry))}`); + push(""); +} +if (roadmap.unresolved.length > 0) { + push("### Unresolved members", ""); + for (const row of roadmap.unresolved) push(`- Place ${row.place}: ${span(row.id)}`); + push(""); +} +const deferrals = register.specs.flatMap((spec) => spec.questions.filter((question) => question.deferral).map((question) => ({ spec: spec.id, question }))); +if (deferrals.length > 0) { + push("### Deferred, with their triggers", ""); + for (const { spec, question } of deferrals) push(`- ${question.key === undefined || question.key === null ? span(spec) : span(`${spec}#question.${question.key}`)}: ${text(question.question)} Trigger: ${text(question.trigger)}`); + push(""); +} +while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop(); +return { diagnostics: [], memo: `${lines.join("\n")}\n` }; +``` diff --git a/docs/agent-surface/upgrade-guide.md b/docs/agent-surface/upgrade-guide.md index 2f858fd99..4287fa721 100644 --- a/docs/agent-surface/upgrade-guide.md +++ b/docs/agent-surface/upgrade-guide.md @@ -23,6 +23,45 @@ Keep the result as your starting measurement. Add your project's `--root` and `- arguments where needed. The catalog is `node_modules/@libar-dev/software-delivery-protocol/docs/agent-surface/recipes.md`. +## Organize features in epics + +Here a capability is one of your corpus's own intended outcomes, not a Protocol capability +that capability adoption counts. Follow `spec:consumers.capability-map` for its law. Read the capability-map summary, recipe 52, before choosing new parents: + +```sh +pnpm exec sdp q "$(cat node_modules/@libar-dev/software-delivery-protocol/dist/recipes/52-the-capability-map.js)" --json +pnpm exec sdp q "$(cat node_modules/@libar-dev/software-delivery-protocol/dist/recipes/07-readiness-divergence.js)" --json +pnpm exec sdp q "$(cat node_modules/@libar-dev/software-delivery-protocol/dist/recipes/11-the-lower-ladder.js)" --json +``` + +Pass `--params '{"detail":true}'` for the complete map, or combine it with `epic` to read one +epic. Inspect `damage` and `cycles`; a refused parameter returns `diagnostics` without a map. + +An epic states an enduring intended outcome. When an existing hub already owns that outcome, +change its altitude to `epic` and preserve its id, kind and intent. Author a new epic only when +no existing Spec owns the outcome. Choose parents by reading intended truth; paths, Pack +membership and dependencies do not establish refinement. + +Keep genuine intermediate parents and multiple-parent relationships. A capability map can show +an epic inside another epic. Examples and decisions remain visible, and descendant counts do +not confer readiness or delivery facts on their parent. + +Before reparenting a ready Spec, run promotion preflight, recipe 9, for the proposed parent. +That parent must state at least `defined` and clear its floor. Preserve existing readiness +statements that remain supported; the map never states a rung for you. + +Review the carrier diff, validate, then re-run recipes 7, 11 and 52. Compare every previously ready Spec's floor. Read the Studio's Capabilities page and use its Epic filter to inspect the +new scope. Unreached Specs are information, not a requirement to invent a parent. A single +broad epic is lawful; divide it only when the intended outcomes warrant separate identities. + +## Model upcoming work in the graph; keep review history in git and pull requests + +Author your next bodies of work as Specs and group each in a Pack named for its capability. Make one Pack your roadmap by stating `order: priority` in its manifest and listing the next few entries, first entry first; recipe 53 places each by what it needs, and recipe 55 renders the forward section of a pull request description. Turn each open review finding about a Spec into an open question with `decider`, `provisional` and `links`, or into a change proposal checked by recipe 38. Unresolved intended design lives in open questions. Review disposition and acceptance remain in git and the pull request; neither becomes a graph field. Keep your plan and state documents if you want them; they confer nothing, so point them at Packs and recipes instead of copying counts. + +The roadmap law is `spec:consumers.roadmap`; the Protocol's own record convention is +`spec:protocol.roadmap-replaces-plans` and imposes none on an adopter. A defect against settled +intent remains a code finding rather than an invented design question. + ## Propose and apply rule keys Choose one Pack first. Write its base graph with `pnpm exec sdp build .` and save diff --git a/docs/concept/DECISIONS.md b/docs/concept/DECISIONS.md index 678436266..222b9c8dd 100644 --- a/docs/concept/DECISIONS.md +++ b/docs/concept/DECISIONS.md @@ -54,6 +54,9 @@ positions and are never reused. | MD-43 | the build renders Studio views | durable | The build writes each view into markup, and its custom element adds interaction; a host supplies that markup rather than graph data. | [Spec](../../specs/decisions/studio-build-renders.sdp.md) (`spec:decisions.studio-build-renders`) | | MD-44 | the engine lays out Studio diagrams | durable | Deterministic build-time layout writes inline SVG with a text equivalent; the element adds pan, zoom and pinch. | [Spec](../../specs/decisions/studio-diagram-renderer.sdp.md) (`spec:decisions.studio-diagram-renderer`) | | MD-45 | the vault owns one folder | durable | A guarded file list owns `sdp/`; synchronization preserves unchanged files, keeps a Base a person changed, and refuses other edits or unknown files before writing. | [Spec](../../specs/decisions/vault-owned-folder.sdp.md) (`spec:decisions.vault-owned-folder`) | +| MD-46 | a roadmap states advisory priority | durable | A Pack whose manifest states `order: priority` holds the corpus's advisory priority as its member order; no relation, date or gate. Narrows MD-33 for this one order and opens the Pack manifest (MD-25) to one optional field. | [Spec](../../specs/decisions/roadmap-priority.sdp.md) (`spec:decisions.roadmap-priority`) | +| MD-47 | the roadmap and the pull request over plan and review files | durable | Forward planning is read from the roadmap; an arc's record is its branch until its first coherent commit is pushed, then its draft pull request, which carries its rulings, review findings as change proposals and memos, and its acceptance; `plans/` and `reviews/` are frozen history awaiting a cleanup. Narrows MD-33. | [Spec](../../specs/decisions/roadmap-over-plans.sdp.md) (`spec:decisions.roadmap-over-plans`) | +| MD-48 | epics of any kind | durable | Any Spec at epic altitude is an epic and keeps its kind, as MD-4's named coordinates imply. | [Spec](../../specs/decisions/epics-of-any-kind.sdp.md) (`spec:decisions.epics-of-any-kind`) | ### Decision Specs without a registry code diff --git a/examples/checkout-v1/README.md b/examples/checkout-v1/README.md index cebca99ad..82fa9c1cf 100644 --- a/examples/checkout-v1/README.md +++ b/examples/checkout-v1/README.md @@ -139,6 +139,20 @@ Revert the edit afterwards. The next `build` regenerates `generated/`. `honesty/readiness-floor`, citing `kind-evidence-complete`, and contract generation withholds that example's step contract. +## Read the next work + +The [roadmap Pack](specs/roadmap.pack.sdp.md) states advisory priority with `order: priority`. +Run the roadmap recipe and its forward memo from the repository root: + +```sh +npm run --silent sdp -- q 'return recipes(53, params)' --root examples/checkout-v1 --params '{"pack":"pack:checkout-roadmap"}' --json +npm run --silent sdp -- q 'return recipes(55, params)' --root examples/checkout-v1 --params '{"pack":"pack:checkout-roadmap"}' --json +``` + +Recipe 53 places each entry by what it needs, read from the graph each time. +The memo copies purpose and dependency reach; the owner's reason for priority belongs in the +pull request author's own words. Membership and priority change no readiness or delivery fact. + ## Where the concepts live | Concept | Read | diff --git a/examples/checkout-v1/specs/roadmap.pack.sdp.md b/examples/checkout-v1/specs/roadmap.pack.sdp.md new file mode 100644 index 000000000..b331baf7f --- /dev/null +++ b/examples/checkout-v1/specs/roadmap.pack.sdp.md @@ -0,0 +1,11 @@ +--- +id: pack:checkout-roadmap +order: priority +specs: + - spec:orders.order-placement-flow + - spec:orders.order-model +--- +# Checkout roadmap + +Clarify how a customer reaches order creation, then review the shared order vocabulary. +The order is advisory priority; needs come from the graph and confer no permission. diff --git a/plans/47-roadmaps-capability-maps-and-slices.md b/plans/47-roadmaps-capability-maps-and-slices.md new file mode 100644 index 000000000..26a2cd0bc --- /dev/null +++ b/plans/47-roadmaps-capability-maps-and-slices.md @@ -0,0 +1,168 @@ +# Plan 47. Roadmaps, capability maps and design slices + +> **Status:** EXECUTED on `feature/roadmap-and-slices-v0`, from `main` at `da4b141`, the merge of +> PR 35, as PR 36. The last numbered plan: forward work reads from the roadmap, `pack:roadmap` +> (recipe 53), and each arc's record is its branch and then its pull request (MD-47). A thin +> pointer: the arc's intent is `pack:roadmaps-and-slices-v1`, and readiness, backlog and open +> questions are read from the graph, never from this file. + +## Why + +The owner asked for one well-rounded PR on how a corpus reads where it stands and what comes next: + +1. Epics. The Protocol's corpus stated one epic over 106 features and 298 stories, and no view drew + refinement, so filtering by altitude showed one epic and a flat list. +2. A roadmap with priority, tied to the PR descriptions: what to design next and what to build + next, in order. The planning truths decision (MD-33) refuses recorded work order as graph + structure, and the roadmap views capture waited for a named reader. The owner is that reader. +3. More diagram kinds, legible at desktop width, and an annotation review that leaves a curated + register of the engine's significant patterns and abstractions, never a generated inventory. +4. Architectural slices: features that must be reviewed together, read as one design. +5. Plans become the roadmap: forward planning read from the graph, `plans/` frozen as history. +6. Housekeeping the owner adds while reviewing the Studio. + +The owner's direction for the arc: set a solid example of each aspect; the roadmap holds enough +work for the next few PRs, updated as work lands, never every feature; the annotation review is +as thorough as possible and its result curated. + +## The design wave + +Five lanes, each designed by one model family, reviewed by another, and answered by the first, +run as Relay jobs and Claude agents through one Workflow. Every review finding was fixed or +rebutted with a reason. The records were removed before merge and stay at commit `d02c4062`: +`reviews/plan47-design/` (briefs, designs, reviews, answers), `reviews/plan47-annotation-review.md` +and `reviews/plan47-epics-integration-proposals.md`. Prototypes of every new page and drawing, on +the Protocol's corpus and on a copy of libar-platform at `b038bd4`, are at +`reviews/assets/plan47-prototypes/` in the same commit, and PR 36's screenshots at +`evidence/pr36/`. + +| Lane | Designed by, reviewed by | Carrying Specs | +| --- | --- | --- | +| A annotations | GPT-6.1-Sol; Opus 5.5 and GPT-6-Astra | `spec:protocol.significant-units-review`, `spec:protocol.role-vocabulary`, `spec:consumers.spec-studio.lenses.architecture.unit-register` | +| E epics | GPT-6-Astra; Opus 5.5 | `spec:consumers.capability-map`, `spec:consumers.spec-studio.capability-map-page`, `spec:protocol.capability-epics`, `spec:extraction.executable-examples`, `spec:decisions.epics-of-any-kind` | +| G diagrams | GPT-6.1-Sol; Opus 5.5 | `spec:consumers.spec-studio.diagram-kinds` and its eight children | +| S slices | Opus 5.5; GPT-6.1-Sol | `spec:consumers.design-slice`, `spec:consumers.spec-studio.slice-page` | +| R roadmap and plans | Opus 5.5; GPT-6-Astra | `spec:decisions.roadmap-priority`, `spec:consumers.roadmap` with its page and memo, `spec:decisions.roadmap-over-plans`, `spec:protocol.roadmap-replaces-plans`, `pack:roadmap` | + +Recipe numbers: 52 the capability map, 53 the roadmap, 54 the design slice, 55 the roadmap memo. + +## Added, dropped and reshaped against the starting list + +| Change | Why | +| --- | --- | +| Added: two captures from PR 35's forward lanes (the cited line's words, an outside register as a Studio input) | The first roadmap needs real upcoming work beyond this arc | +| Added: a ninth epic, executable examples | The runner and the contracts each own a narrower outcome; no Spec stated the capability | +| Added: a decision on epics of any kind | The glossary limits the capability map to behavior Specs, while three hubs are model or rule Specs; broadening a ratified term is the owner's | +| Added: a "to state" step on the roadmap, between to design and to build | A Spec whose floor allows `ready` waits only for a person's statement, which is neither design nor build | +| Added: the roadmap memo (recipe 55) | Ties the roadmap to the PR description's forward section, as the owner asked | +| Added: a significant-units panel on the architecture page | The curated register needs a reading place a contributor opens | +| Added: edge routing around boxes, Mermaid source to copy, a requirement drawing on the Spec page | PR 35's forward lane D, the owner's Mermaid ask, and a trace reading the Spec page lacked | +| Reshaped: the trace graph | It is already built, one hop, on every Spec page; what was missing is a multi-hop dependency drawing over a Pack, slice or roadmap | +| Reshaped: a slice is a reading, never an artifact | A Pack, a Pack narrowed to members, or an ordered list of Specs, composed on each run; no new grouping | +| Reshaped: the roadmap's split is derived | Only the order is authored; to design, to state, to build and built are read from the graph each time | +| Dropped: sequence views from flow entries | A flow is one prose line; the graph holds no participants or message order, and drawing one would infer from prose. Re-entry: a carrier grammar for flow steps | +| Dropped: state, Gantt, journey and similar Mermaid kinds | No honest data (states, dates, journeys); each is named with its trigger in the diagram catalog | +| Deferred: a class-style view of roles and uses | Waits for the role vocabulary to settle and a reader to ask | + +## Rulings made on the owner's delegation + +The executing thread's, and each lane's, after review; each awaits the owner's review. + +| Ruling | Carrying entry | +| --- | --- | +| A significant unit owns a contract or responsibility a designer must understand; export status or import rank alone never qualifies | `spec:protocol.significant-units-review#rule.significantAbstractions` | +| Mechanical wrappers, forwarding barrels, empty registrations and formatting helpers stay bare | `spec:protocol.significant-units-review#rule.curatedBoundary` | +| A component `uses` needs a direct value or type import; anchor-authoring imports and unused re-exports confer none | `spec:protocol.significant-units-review#rule.dependencyEvidence` | +| The role vocabulary is a Model of the corpus's own words, read by reviewers, checked by no validator | `spec:protocol.role-vocabulary` | +| Nine epics: seven hubs enriched in place, executable examples new, self-hosting kept; every Spec is reached and every `ready` floor kept | `spec:protocol.capability-epics` | +| The capability map keeps every parent, counts distinct descendants, includes examples and decisions, and answers a summary by default | `spec:consumers.capability-map` | +| Capabilities is a lens after Packs, and the index narrows by epic | `spec:consumers.spec-studio.capability-map-page` | +| Labels lead with the title at fourteen pixels, ids shorten with a marked elision, a drawing keeps its natural size within an 880-pixel budget and scrolls inside its figure | `spec:consumers.spec-studio.diagram-kinds.legibility` | +| Edges route around boxes with distinct ports and lanes, deterministically; the search is the build's | `spec:consumers.spec-studio.diagram-kinds.edge-routing` | +| Mermaid source is selectable text copied by the existing copy element; it needs no decision refining MD-44 or MD-32 | `spec:consumers.spec-studio.diagram-kinds.mermaid-source` | +| The maturity map is a grid of counts with member marks, never a score | `spec:consumers.spec-studio.diagram-kinds.maturity-picture` | +| The design slice owns depth and rests-on; drawings consume its answer; a relation that orders nothing is drawn apart | `spec:consumers.design-slice#rule.restsOn`, `spec:consumers.spec-studio.diagram-kinds.dependency` | +| An example folds into its parent only when nothing is lost, and folding lends the parent nothing | `spec:consumers.design-slice#rule.examplesFold` | +| A planned component is a design row of its own, never merged with the anchored component | `spec:consumers.design-slice#rule.componentDesigns` | +| A roadmap entry's scope is its refinement subtree; its step is the first open one among its work, and each part that holds it is named | `spec:consumers.roadmap#rule.entryNeed`, `#rule.workRows` | +| Waiting statements keep the statement queue's order across the whole roadmap | `spec:consumers.roadmap#rule.waiting` | +| One living roadmap; an entry joins on the owner's word and leaves by a manifest edit | `spec:protocol.roadmap-replaces-plans#rule.oneRoadmap` | +| The epic-kind decision lives in the decisions family, `spec:decisions.epics-of-any-kind` | the executing thread, at integration | + +## Owner's rulings + +The owner ruled the checkpoint's decisions on 2026-10-22, taking the coordinating session's +high-confidence opinions as rulings, left the nine epic boundaries as designed, and delegated the +`ready` statements at the floor recipe 9 reports. Two advisors (Opus 5.5 and GPT-6-Astra, at +extra-high effort) review the remaining items; the executing thread rules on those after reading +both. + +| Ruling | Carrying Spec | +| --- | --- | +| Ratified: an optional Pack manifest field `order: priority` makes member order an advisory priority; no relation, no date, no gate; it refines the planning truths decision (MD-46) | `spec:decisions.roadmap-priority` | +| Ratified: `plans/` freezes at plan 47; the handbook names `pack:roadmap` and recipe 53, and the gate checks the roadmap Pack instead of a current plan; no check refuses a new plan file, which would police the workflow, not conformance (MD-47) | `spec:decisions.roadmap-over-plans` | +| Ratified: any Spec at epic altitude is an epic and keeps its kind, as the one-primitive decision's named coordinates imply; the glossary row broadens (MD-48) | `spec:decisions.epics-of-any-kind` | +| Accepted: the annotation review's change table; the contract decoder joins the reader component only if wave 1 shows, with recipe 32 and the self-binding tests, that it adds no layer crossing, otherwise it stays and the reason is recorded | `spec:protocol.significant-units-review#rule.acceptedChanges`, `#rule.decoderBoundary` | +| Words: "roadmap"; "need" for what a roadmap entry needs next ("step" collides with example steps, "lane" with a pull request's lanes); "to state"; "design slice" a guidance-only label | `CONTEXT.md`, `spec:consumers.roadmap#rule.entryNeed` | +| Release and baseline views stay deferred; trigger: the first published release | `spec:consumers.projections-model.roadmap-views` | +| `ready` stated on the arc's 22 Specs whose floor allowed it | the Specs, recipe 29 | + +The owner also asked, directly in the executing thread, that this arc set the precedent for +retiring `reviews/` as well as `plans/`, with canonical SDP-native examples of the alternatives, +leaving the removal to a dedicated cleanup PR, now first on the roadmap. The owner then ruled the +record's home: before the first push the record is the branch itself, the first commit message +stating the owner's working rules and the arc's Pack, with no record file anywhere; an arc pushes +and opens its draft pull request at its first coherent commit; the draft pull request holds the +narrative, review findings as change proposals with their memos, and the acceptance. The advisory +round of this PR, posted to its conversation as two opinions, two proposals checked by recipe 38 +with their recipe-49 memos, and the executing thread's rulings, is the canonical example. + +## How it was built + +Each lane was built by GPT-6.1-Sol in its own worktree, reviewed by Opus 5.5 at extra-high effort, +and fixed test-first by Sol; each wave was integrated in one pass and checked on CI's route (Node 20, +npm 10, a fresh clone) before its push. + +| Wave | Lanes | +| --- | --- | +| 1 | The `order: priority` field and the design diff's order change; the diagram core (labels, routing, Mermaid source); the accepted roster on the anchors, the decoder moved to reader with no added layer crossing; recipes 52 to 55 with executable examples; the reach examples' heavy-test timeout, test-first, for a flaky CI run | +| 2 | The Capabilities lens and epic filter; the roadmap page; a slice page per Pack; the component uses table, the significant-units panel, the maturity picture and the requirement drawing | +| 3 | The handbook's roadmap pointer and the gate's roadmap check; skills, glossary, upgrade guide and the worked example's roadmap; the diagram catalog bound; this arc's six built entries removed from the roadmap and the delivery-record cleanup captured first | + +Rulings the executing thread made during the build, on the owner's delegation: the dependency +drawing's Mermaid source is `flowchart TB`; the scoped architecture's legend names uses, planned +uses and realization; the requirement drawing gives examples and decisions no absence label; the +design-rounds guidance joins the authoring skill and its on-ramp. + +## The close review + +Four independent reviews of the built branch: an adversarial code review (Opus 5.5), a +design-honesty review (GPT-6-Astra), CodeRabbit's CLI by directory (25 runs under its file limit), +and Greptile's CLI on throwaway commits from `main` holding one directory group each (the whole +branch exceeds its limit; findings that only the split produced were set aside). Every remaining +finding was fixed test-first in two Sol lanes, each reviewed by Opus: the requirement drawing's false +absence on 63 of 78 pages; drawings past their budget; Mermaid that failed to parse on a backtick +title; recipe 53's renumbering after an unresolved member; priority shown as reading order on the +Pack page, the vault and the design diff; the gate reading the roadmap from the graph; the memo's +escaping; the upgrade guide's wording; evidence durability; and a Studio build 2.7 times slower than +`main`'s, back within about five percent, with the Studio tests building their page set once. + +## Acceptance, measured at close + +Re-run the recipes rather than inheriting these. + +- `npm run check` passes, and CI's route on Node 20 with npm 10 passes in a fresh clone. +- 456 Specs in 9 epics; no Spec outside an epic (110 at `main`); 447 stated `ready`, every one at + a `ready` floor; drift alarm empty; three honest verifier gaps. +- The design diff from `main`: 51 Specs added, 51 changed, none removed; 4 Packs changed; 88 binding + changes. +- The roadmap ran its own cycle: this arc's six entries read built and left; it now holds the + delivery-record cleanup, the two Studio captures (to design) and the composed-intent model (to + build), which is the whole build backlog with its child. + +## Working rules + +- Commit after each coherent unit and its gate; never `git stash`; never touch `.omo/`. +- Before every push, run CI's route in a fresh clone on Node 20 and npm 10: `npm ci + --ignore-scripts`, `npm run check`, `npm run check:git-install`. +- Never merge to `main`; libar-platform is read through a copy and never edited. diff --git a/specs/carrier/markdown-authoring.sdp.md b/specs/carrier/markdown-authoring.sdp.md index f4fe8c722..adca5564a 100644 --- a/specs/carrier/markdown-authoring.sdp.md +++ b/specs/carrier/markdown-authoring.sdp.md @@ -1,7 +1,7 @@ --- id: spec:carrier.markdown-authoring kind: behavior -altitude: feature +altitude: epic readiness: ready relations: dependsOn: spec:carrier.markdown-parser diff --git a/specs/carrier/markdown-pack-authoring.sdp.md b/specs/carrier/markdown-pack-authoring.sdp.md index ba0de4177..fb7618482 100644 --- a/specs/carrier/markdown-pack-authoring.sdp.md +++ b/specs/carrier/markdown-pack-authoring.sdp.md @@ -8,7 +8,9 @@ relations: dependsOn: - spec:carrier.markdown-parser - spec:decisions.carrier-ruling - decidedBy: spec:decisions.pack-markdown-carrier + decidedBy: + - spec:decisions.pack-markdown-carrier + - spec:decisions.roadmap-priority --- # A Pack has a Markdown manifest carrier @@ -17,7 +19,7 @@ relations: ## Behavior - rule: Routing is by envelope id namespace: `pack:` reifies as a Pack manifest; `spec:` keeps the Spec path; any other namespace is refused as `extract/invalid-id`. -- rule: The pack envelope is closed to exactly `id` (required, `pack:` namespace), `specs` (required YAML list of `spec:` ids, manifest order; may be empty), and `modelRefs` (optional YAML list of `spec:` ids). A derived-name key is refused as `extract/reserved-property`. Any other key — including the Spec envelope keys `kind`, `altitude`, `readiness` and `relations`, and `title` — refuses with `extract/unrecognized-property`. Symmetrically, `specs` or `modelRefs` on a `spec:` carrier refuse the same way. +- rule: The pack envelope is closed to exactly `id` (required, `pack:` namespace), `specs` (required YAML list of `spec:` ids, manifest order; may be empty), `modelRefs` (optional YAML list of `spec:` ids), and `order` (optional; its one lawful value is `priority`, and any other value refuses with `extract/invalid-frontmatter`). A derived-name key is refused as `extract/reserved-property`. Any other key — including the Spec envelope keys `kind`, `altitude`, `readiness` and `relations`, and `title` — refuses with `extract/unrecognized-property`. Symmetrically, `specs` or `modelRefs` on a `spec:` carrier refuse the same way. - rule: The body H1 is the Pack title. All remaining body prose is the framing, owned by the Pack node. Any `##` section heading refuses with `extract/unrecognized-heading` — a Pack has no section tier. - rule: A Markdown manifest and a TypeScript manifest of the same Pack derive the identical Pack node (the `file` field aside) and identical `belongsTo` edges. The same id authored in both carriers is the standing duplicate-id refusal. - rule: The TypeScript `pack()` builder stays a lawful per-ID option and the import source; `sdp import` remains Spec-only and does not convert pack manifests. diff --git a/specs/consumers/agent-surface.answer-shapes.sdp.md b/specs/consumers/agent-surface.answer-shapes.sdp.md index bc3d4eadb..9ab1da372 100644 --- a/specs/consumers/agent-surface.answer-shapes.sdp.md +++ b/specs/consumers/agent-surface.answer-shapes.sdp.md @@ -22,7 +22,7 @@ A usability trial gave agents only the skills, the recipe catalog and `sdp q` an ## Behavior -- rule: Changed-file impact, Pack design and design diff, recipes 4, 29 and 35, accept `detail: true` and default to summaries preserving every total and explicitly naming omitted detail. +- rule: Changed-file impact, Pack design, design diff and the design slice, recipes 4, 29, 35 and 54, accept `detail: true` and default to summaries preserving every total and explicitly naming omitted detail. - rule: The guarantees recipe (recipe 3) returns each section's entries as the Spec states them and the binding fields the Spec context carries, not only the names of the sections. - rule: The orphans and gaps recipe (recipe 8) reports each kind the verifier-gap signal exempts and how many ready Specs it exempts, as the build backlog reports its exclusions. - rule: A design-backlog recipe lists the Specs stated `defined` that no code realizes, with their design columns, excluding examples and decisions as the build backlog does. @@ -30,7 +30,7 @@ A usability trial gave agents only the skills, the recipe catalog and `sdp q` an ## Design -- detailParameter: recipes 4, 29 and 35 read `params.detail`, absent or `false` for the summary and `true` for every row in full; recipe 29's `detail` may also be a list of member ids, which returns full rows for those members and summary rows for the rest of the Pack, and an id that is not a member is a diagnostic in the result. A summary keeps `totals` as the full answer counts them and adds `leftOut`, one line for each kind of detail it omits. +- detailParameter: recipes 4, 29, 35 and 54 read `params.detail`, absent or `false` for the summary and `true` for every row in full; recipe 29's and recipe 54's `detail` may also be a list of member ids, which returns full rows for those members and summary rows for the rest of the Pack, and an id that is not a member is a diagnostic in the result. A summary keeps `totals` as the full answer counts them and adds `leftOut`, one line for each kind of detail it omits. - impactSummary: `{ changedFiles, totals, impactedSpecs: [id], impactedPacks: [id], atRisk: { incomingSpecs: [id], outgoingSpecs: [id], other: [id] }, coverageUnknownFiles, unlinkedUnits: [id], leftOut }` is recipe 4's summary. Incoming and outgoing name the connecting edge's direction relative to the impacted node and assert no dependency, since the edge may be any type; a Spec reached both ways is listed in both, and `other` holds the at-risk nodes that are not Specs. - impactDetail: With `detail: true`, recipe 4 returns full rows and reasons; every at-risk reason carries `direction`, `incoming` or `outgoing`, relative to its impacted end. - packDesignSummary: `{ id, specKind, statedReadiness, floorReached, statedNextRung, holds: [{ clauseId, targets: [id] }], openQuestions, blockingQuestions, implementedBy, referencedBy }` is recipe 29's summary row, with counts for the last four; the summary's boundary lists ids per direction, and `totals` is unchanged. `holds` is the summary of `statedNextRungFailures`, retaining each `clauseId` and target ids. diff --git a/specs/consumers/agent-surface.sdp.md b/specs/consumers/agent-surface.sdp.md index bfc3962ed..6c5af204d 100644 --- a/specs/consumers/agent-surface.sdp.md +++ b/specs/consumers/agent-surface.sdp.md @@ -1,7 +1,7 @@ --- id: spec:consumers.agent-surface kind: behavior -altitude: feature +altitude: epic readiness: ready relations: refines: spec:consumers.projections-model diff --git a/specs/consumers/authoring-on-ramp.sdp.md b/specs/consumers/authoring-on-ramp.sdp.md index 2e0d2eec5..95585a78d 100644 --- a/specs/consumers/authoring-on-ramp.sdp.md +++ b/specs/consumers/authoring-on-ramp.sdp.md @@ -1,7 +1,7 @@ --- id: spec:consumers.authoring-on-ramp kind: behavior -altitude: feature +altitude: epic readiness: ready relations: dependsOn: @@ -30,6 +30,7 @@ relations: - rule: Cheap capture starts with `sdp new spec` (or the equivalent hand-authored idea carrier) in the family found through concept search; the scaffolder emits envelope, Intent outcome, and the kind's bare typed heading, never invented content, and for `constraint` emits envelope, title, and Intent only with no twin section. Every later readiness edit is preceded by the promotion-preflight recipe and remains a human statement. - rule: The skill teaches question keys and their optional fields through `spec:model.open-question-keys` and `spec:model.open-question-fields`, rule keys and an example's `exercises` through `spec:model.rule-keys`, and optional relation reasons through `spec:model.relation-reasons`. - rule: The skill teaches the least design two parties need to agree: keyed Design entries for shared names and shapes, code spans for pinned declarations, rule keys for outside citations, one prose owner for why, and no derived counts, delivery state or implementation sequence authored as design. +- rule: [#designRounds] The skill teaches design in rounds over a design slice: what each rung's design typically settles, that a detail only the builder reads and a question only the builder answers belong to the build, and how a reviewer reads the design columns beside each rung; no check reads this guidance. - rule: The skill points behavior and example authors to `spec:carrier.gherkin-authoring` for the lawful Gherkin alternative and Pack authors to `spec:carrier.markdown-pack-authoring` for namespace routing, framing and ordered membership. - rule: Binding is taught for code, test and oracle anchors in the constant form and the comment form, following `spec:decisions.anchor-comment-form` and `spec:decisions.anchor-binding-grain`. The constant builders are `codeAnchor`, `specTest` and `specOracle`; a code anchor's `satisfies` is optional and plural, a test anchor's `verifies` is non-empty and plural, and an oracle anchor has one `models` target. The skill teaches `references` as the code-to-design binding that claims no realization, and names the two silent non-binding hazards, a constant builder without a Protocol builder binding and a comment that does not open with `/**`. - rule: The skill teaches the collective promise test for `satisfies` and the one-way meaning of `references`, following `spec:decisions.anchor-binding-grain`; every promise is realized by the satisfying units together, and each satisfying unit realizes at least one promise. diff --git a/specs/consumers/capability-map.sdp.md b/specs/consumers/capability-map.sdp.md new file mode 100644 index 000000000..154fb56dc --- /dev/null +++ b/specs/consumers/capability-map.sdp.md @@ -0,0 +1,58 @@ +--- +id: spec:consumers.capability-map +kind: behavior +altitude: feature +readiness: ready +relations: + refines: + target: spec:consumers.projections-model + reason: the capability map projects authored refinement without introducing a grouping + decidedBy: spec:decisions.epics-of-any-kind + dependsOn: + - target: spec:validation.readiness-floor + reason: each node reads the existing floor beside the author's statement + - target: spec:extraction.delivery-facts + reason: rollups count direct bindings without conferring facts on ancestors +--- +# The capability map reads every epic and its refinement descendants + +The map answers what a corpus intends to do before a reader chooses a Pack. Refinement can skip an altitude, repeat an altitude, or give a Spec several parents. The map keeps that structure. An epic's descendants are a set of Specs, not a measure of completion. + +## Intent + +- actor: A person or agent choosing which capability's design to read. +- problem: An altitude filter gives a flat list and Pack membership does not explain refinement. +- outcome: Read every epic with its refinement descendants, its design maturity and direct binding evidence, and expose every Spec no epic reaches. + +## Behavior + +- rule: [#graphOnly] The map follows only declared `refines` edges between resolving Specs, from parent to child for reading. It reads every kind at every altitude, preserves intermediate Specs, and derives no parent from a title, path, Pack, dependency or binding. +- rule: [#epics] Every Spec at epic altitude is an entry, whatever its kind. An epic may itself refine another epic; both remain selectable, and a parent's descendant set includes the nested epic and its descendants. +- rule: [#nodeEvidence] Each Spec carries its own stated readiness, structural floor reached, delivery facts, and open questions with their blocking flags. A missing floor reads `none`. No value propagates from a child to its parent. +- rule: [#multipleParents] Every refining parent remains in the answer; a Spec appears once in nodes with all its parents, and a page decides where it expands. +- rule: [#rollup] An epic's rollup counts distinct descendants, excluding the epic itself, by stated rung, floor reached, kind and altitude; it counts those with implementation bindings and those with verifier bindings separately. Open and blocking question totals count the questions on those distinct descendants. Rollups for overlapping epics are never added to form a corpus total. +- rule: [#examplesAndDecisions] Examples and decisions participate in reach, counts and edges like every other kind. The page may fold them into labelled disclosures, but exposes their rows and any descendants they parent. A verifier count means a binding exists, never that a test passed; the map states no percent complete, inherited fact or work order. +- rule: [#unreached] A Spec is unreached when no epic reaches it, counting an epic as reaching itself. The answer lists every unreached id, including examples and decisions, and groups the unreached subgraph by its roots. A branch that is also reached remains outside that subgraph, with its cross-edge retained in the full answer. +- rule: [#damagedGraph] Unresolved or non-Spec refinement endpoints are reported in damage and excluded from traversal. Cyclic refinement is reported as sorted groups whose members all reach one another, including self-links. Traversal visits each id once per root. The map displays the damage without inventing ancestry or withholding the rest. +- rule: [#scope] With an epic selected, the answer contains that epic and its distinct descendants. Node parent and epic memberships still name their full-corpus memberships so narrowing never claims exclusive ownership. Without an epic the answer includes every Spec, including an empty corpus or a corpus with no epics. + +## Design + +- mapRecipe: recipe 52 takes optional `params.epic`, a resolving epic Spec id, and `params.detail`, a boolean. Omitted epic means the whole corpus. An unknown parameter returns `{ unknownParams, accepts: ["epic", "detail"] }`; a wrong type, absent id or non-epic returns `{ diagnostics: [{ parameter, problem }] }` without a map, naming `epic` or `detail` as appropriate. There is no sample epic and no filtering by kind. +- mapAnswer: the full answer is `{ input: { epic }, totals, nodes, edges, epics, unreached: { ids, roots }, cycles, damage }`; `epic` is the selected id or null. The selected node set defines all totals; unselected nodes are never counted. Full-corpus memberships on a selected node are context, not extra selected nodes. +- mapDetail: recipe 52 reads `params.detail`, absent or `false` for the summary and `true` for the full answer; the summary keeps `input`, `totals`, `epics` as `{ id, title, parentEpicIds, rollup }`, `unreached.ids`, `cycles` and `damage`, and adds `leftOut`, one line for each omitted field. `detail: true` adds `nodes`, `edges`, each epic's `descendantIds` and `unreached.roots`, and omits `leftOut`. +- nodeRow: `{ id, title, specKind, altitude, statedReadiness, floorReached, deliveryFacts, questions, parents, children, epicIds }`; parents and children are all resolving direct refinement neighbors in the corpus, and epicIds lists all epics that reach this node, including itself when epic. Questions preserve authored order as `{ index, address, question, blocking }`, with a keyed question's entry address or null. +- edgeRow: `{ from, to, claim: "declared", reason }`, child to parent, for each resolving refinement whose ends are selected. An absent reason is null. Other relation types never enter this list. +- epicRow: `{ id, title, parentEpicIds, descendantIds, rollup }` for every selected epic, with its distinct descendants in the selected set. `parentEpicIds` lists resolving direct refinement parents at epic altitude in the full corpus, in id order. Selection by one epic includes every descendant of a nested epic, so its rollup stays complete. +- countShape: totals and each rollup carry `{ specs, byStatedReadiness, byFloorReached, byKind, byAltitude, withImplementationBinding, withVerifierBinding, openQuestions, blockingQuestions }`; every descriptor value has a key including zero, floor counts add `none`, and totals additionally carry `{ epics, reached, unreached }`. Reached and unreached partition selected nodes, using their full-corpus epic membership. The epic's own evidence stays on its node row, outside its rollup. +- unreachedRoots: a root is an unreached Spec with no unreached parent; a group of unreached Specs that refine one another in a cycle, with no unreached parent outside the group, uses its smallest id as its root. Reach may overlap between roots; unreached.ids stays unique and complete. In an epic selection both lists are empty. +- graphDamage: `damage` lists `{ code: "unresolved-refinement", from, to }` for a missing or non-Spec endpoint touching the selected scope, or every such edge in whole-corpus scope. Cycles are separate `{ members }` rows for cyclic groups in the selected graph, members sorted by id; they are graph damage, not parameter failures or a new validator. +- stableOrder: ids, nodes, epic rows, descendant ids, memberships and unreached roots use code-unit id order; edges and damage sort by from then to; cycles sort by first member. Question order stays authored. The answer has no clock, recorded work order or mutable state. + +## Verification — reviewed + +- A diamond retains both parent edges, lists the shared child once in nodes, and counts it once in that epic's descendants. +- A corpus with no epics still exposes every Spec; a cyclic unreached component has a root and finite traversal. +- An epic with a bound example and a ratified decision counts both descendants while retaining its own empty delivery facts. +- Summary and full answers have identical totals, epic rollups, cycles and damage; the summary names every omitted field. +- An epic selection includes its whole reachable set and no unrelated roots; invalid parameters return diagnostics without a plausible empty map. diff --git a/specs/consumers/delivery-session-on-ramp.sdp.md b/specs/consumers/delivery-session-on-ramp.sdp.md index 1c5ac24fb..f61cf9859 100644 --- a/specs/consumers/delivery-session-on-ramp.sdp.md +++ b/specs/consumers/delivery-session-on-ramp.sdp.md @@ -32,7 +32,7 @@ A handoff names the Specs that carry the work, so the next session re-measures t - rule: Capture or refinement uses concept search, the lower ladder, and promotion preflight; design uses promotion preflight and readiness divergence. The listed recipes are starting points, not exhaustive recipe sets. - rule: The skill teaches an advisory Pack design loop: read the design diff from the last pass, compose a context bundle within a stated budget, narrow by perspective when needed, and return a change proposal for a base check before an agent applies it. - rule: Implementation uses the build backlog and the target Spec context; review uses the Pack backbone and warn-level signals, or the target Spec context and warn-level signals when no Pack exists. The listed recipes are starting points, not exhaustive recipe sets. -- rule: The skill teaches a review report as a numbered list ordered by severity, with each item naming one entry address or Spec section, stating the gap in one sentence, and proposing exact text with the address it replaces or follows. +- rule: The skill teaches a Spec proposal report as a numbered list ordered by severity, with each item naming one entry address or Spec section, stating the gap in one sentence, and proposing exact text with the address it replaces or follows. Findings against code or other files stay review comments on their lines; a review proposing no Spec edit has no proposal. - rule: Close uses the drift alarm and changed-file blast radius; slimming is optional and preserves durable law and one prose owner. For a Design entry, the skill teaches the distillation boundary stated by `spec:model.enrichment-lifecycle`, compares the entry with its bound artifact before removal, and keeps every explanation of why. - rule: A handoff names targets, changed files, current readiness, findings or open questions, and commands or evidence locations to re-run; it never carries an inherited verification verdict. - rule: [#arcHandoff] An arc-scale handoff names the Spec ids that carry the arc's standing refusals, guarantees and holds in the homes ruled by `spec:decisions.planning-truths-placement`: tradeoff refusals in decision Specs, already-homed guarantees on their carrying Specs, and deferrals or holds in their blocking open questions. diff --git a/specs/consumers/design-diff.sdp.md b/specs/consumers/design-diff.sdp.md index ca885a49b..e815e54d8 100644 --- a/specs/consumers/design-diff.sdp.md +++ b/specs/consumers/design-diff.sdp.md @@ -4,6 +4,7 @@ kind: behavior altitude: feature readiness: ready relations: + decidedBy: spec:decisions.roadmap-priority refines: spec:consumers.projections-model dependsOn: target: spec:consumers.agent-surface.recipe-parameters @@ -33,7 +34,7 @@ Iterating on a design asks what changed since the last reading. The first adopte - rule: Inside the Design and UI sections, the open questions and other keyed sections apart from rules and flows, it compares keyed entries by key and reports each as added, removed, changed in value or moved in order. - rule: Rule and flow entries match first by unchanged text, preferring the same key for repeated wording, then by retained key; remaining unkeyed base entries pair in list order, including an entry reworded while adding a key. An unmatched keyed base entry is removed, and an unmatched head entry is added. Other unkeyed entries have no identity across the two graphs and compare as part of their whole section. - rule: A rule or flow with unchanged text whose key was added, removed or changed is one `keyed` change naming its old and new address, or none; only a text change is `changed`. An unchanged entry moving between rules and flows is `moved`; when one graph keeps no order across rules and flows, keyed entries compare by order within each list and a `ruleOrder` diagnostic names the Spec. -- 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: It reports changes to Pack membership, member order and what a Pack's order means, 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 the two graphs and confers nothing. It grants no approval, raises no staleness flag, moves no readiness and never records that a pass happened. ## Design @@ -43,6 +44,6 @@ Iterating on a design asks what changed since the last reading. The first adopte - changedSpecRow: `{ id, parts: [part] }` in the summary and `{ id, parts: [{ part, change, from, to }] }` with `detail: true`; `change` is `added`, `removed`, `changed`, `moved` or `keyed`. For `keyed`, `from` and `to` carry the old and new entry addresses, `null` meaning none; other changes carry the values on each side, a rule or flow value as `{ list, text }`. A text change while adding a key is one `changed` entry. - diffParts: a part is `title`, `specKind`, `altitude`, `statedReadiness`, `narrative`, `relation.:`, a keyed entry as `design.`, `ui.`, `question.`, `rule.` or `term.`, an unkeyed rule or flow as `behavior.rules[]` or `behavior.flows[]`, using its head index; a removed unkeyed entry uses its base index with a ` (base)` mark, such as `behavior.rules[1] (base)`, so it cannot share a name with a head entry at that index; an unkeyed question list as `intent.openQuestions`, and any other section as its name. - movedEntry: a keyed entry whose value is unchanged and whose position among the keyed entries of its section changed is `moved`; an entry whose value changed is `changed` whatever its position. For rules and flows, unchanged text with a different key takes `keyed` precedence, then a list change or relative order change among retained entries is `moved`; anonymous entries compare order within their list. Adding or removing a key alone moves no other entry. -- packRows: `packs` rows are `{ id, change, membersAdded, membersRemoved, reordered }` for each Pack added, removed or changed in membership or order. +- packRows: `packs` rows are `{ id, change, membersAdded, membersRemoved, reordered, orderChanged, orderBefore, orderAfter }` for each Pack added, removed or changed in membership, member order or order meaning; `orderBefore` and `orderAfter` are `"reading"` or `"priority"`, null on a side where the Pack is absent, and `orderChanged` is true when both sides hold the Pack and the two differ. An absent `order` reads as `"reading"` in every schema version; a value the head's engine does not know is outside the common schema, listed in `diagnostics` and not compared; that Pack's membership and member order still compare, its `orderChanged` is false, and the side holding the unknown value reads null. `membersAdded` and `membersRemoved` keep their respective manifest order. - bindingRows: `bindings` rows are `{ id, implementedBy, referencedBy, verifiers, deliveryFacts }`, each an `{ added, removed }` pair of ids or fact names, kept apart from `specs` because they derive from anchors. - baseRecipe: The catalog shows how to derive the base graph in a worktree at the base commit with the corpus's exclusions. When constant-form anchors use relative engine imports, as in this repository, run the base worktree's own engine, then pass its `generated/graph.json` as `base` and remove the worktree. diff --git a/specs/consumers/design-slice.chain.sdp.md b/specs/consumers/design-slice.chain.sdp.md new file mode 100644 index 000000000..98b7a354f --- /dev/null +++ b/specs/consumers/design-slice.chain.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.chain +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [depth, scope, memberColumns] +--- +# Members rest on a chain + +## Intent + +- outcome: Members rest on a chain. + +```gwt +Given a probe Pack whose members {structure: "rest on each other in a chain"} +Given one member depends on an outside Spec {outside: "that no code satisfies"} +Given one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"} +When the design slice runs over the probe Pack +Then the members read in depth order {order: "spec:probe.base, spec:probe.design, spec:probe.top"} +``` diff --git a/specs/consumers/design-slice.component-absent.sdp.md b/specs/consumers/design-slice.component-absent.sdp.md new file mode 100644 index 000000000..26cde2ea9 --- /dev/null +++ b/specs/consumers/design-slice.component-absent.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.component-absent +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [componentDesigns, touchedComponents, touchedUses, outsideNeighbors] +--- +# An unanchored component design stays visible + +## Intent + +- outcome: An unanchored component design stays visible. + +```gwt +Given a probe Pack whose members {structure: "rest on each other in a chain"} +Given one member depends on an outside Spec {outside: "that a component's unit satisfies"} +Given one member is a component design whose component {anchor: "no anchor holds"} +When the design slice runs over the probe Pack +Then the member design reads {realized: "unrealized"} with {differing: 0} differing and {unreadable: 0} unreadable rows +``` diff --git a/specs/consumers/design-slice.component-differing.sdp.md b/specs/consumers/design-slice.component-differing.sdp.md new file mode 100644 index 000000000..887d44eeb --- /dev/null +++ b/specs/consumers/design-slice.component-differing.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.component-differing +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [componentDesigns, touchedComponents, touchedUses, outsideNeighbors] +--- +# A realized component keeps its differing layer and uses apart + +## Intent + +- outcome: A realized component keeps its differing layer and uses apart. + +```gwt +Given a probe Pack whose members {structure: "rest on each other in a chain"} +Given one member depends on an outside Spec {outside: "that a component's unit satisfies"} +Given one member is a component design whose component {anchor: "an anchor satisfies, with a differing layer and uses"} +When the design slice runs over the probe Pack +Then the member design reads {realized: "realized"} with {differing: 2} differing and {unreadable: 0} unreadable rows +``` diff --git a/specs/consumers/design-slice.component-matching.sdp.md b/specs/consumers/design-slice.component-matching.sdp.md new file mode 100644 index 000000000..2af18525f --- /dev/null +++ b/specs/consumers/design-slice.component-matching.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.component-matching +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [componentDesigns, touchedComponents, touchedUses, outsideNeighbors] +--- +# A realized component matches its declared values + +## Intent + +- outcome: A realized component matches its declared values. + +```gwt +Given a probe Pack whose members {structure: "rest on each other in a chain"} +Given one member depends on an outside Spec {outside: "that a component's unit satisfies"} +Given one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"} +When the design slice runs over the probe Pack +Then the member design reads {realized: "realized"} with {differing: 0} differing and {unreadable: 0} unreadable rows +``` diff --git a/specs/consumers/design-slice.component-unreadable.sdp.md b/specs/consumers/design-slice.component-unreadable.sdp.md new file mode 100644 index 000000000..5cacb24a9 --- /dev/null +++ b/specs/consumers/design-slice.component-unreadable.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.component-unreadable +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [componentDesigns, touchedComponents, touchedUses, outsideNeighbors] +--- +# An unreadable declared layer stays visible + +## Intent + +- outcome: An unreadable declared layer stays visible. + +```gwt +Given a probe Pack whose members {structure: "rest on each other in a chain"} +Given one member depends on an outside Spec {outside: "that a component's unit satisfies"} +Given one member is a component design whose component {anchor: "an anchor satisfies, with an unreadable layer"} +When the design slice runs over the probe Pack +Then the member design reads {realized: "realized"} with {differing: 0} differing and {unreadable: 1} unreadable rows +``` diff --git a/specs/consumers/design-slice.component-unrelated.sdp.md b/specs/consumers/design-slice.component-unrelated.sdp.md new file mode 100644 index 000000000..8c612d560 --- /dev/null +++ b/specs/consumers/design-slice.component-unrelated.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.component-unrelated +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [componentDesigns, touchedComponents, touchedUses, outsideNeighbors] +--- +# An anchor alone does not realize a component design + +## Intent + +- outcome: An anchor alone does not realize a component design. + +```gwt +Given a probe Pack whose members {structure: "rest on each other in a chain"} +Given one member depends on an outside Spec {outside: "that a component's unit satisfies"} +Given one member is a component design whose component {anchor: "an anchor holds without satisfying the design"} +When the design slice runs over the probe Pack +Then the member design reads {realized: "unrealized"} with {differing: 0} differing and {unreadable: 0} unreadable rows +``` diff --git a/specs/consumers/design-slice.decision-backlink.sdp.md b/specs/consumers/design-slice.decision-backlink.sdp.md new file mode 100644 index 000000000..38b7536e9 --- /dev/null +++ b/specs/consumers/design-slice.decision-backlink.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.decision-backlink +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [restsOn, depth, decisionsInScope] +--- +# A shaping decision comes before its member + +## Intent + +- outcome: A shaping decision comes before its member. + +```gwt +Given a probe Pack whose members {structure: "include a decision that refines the member it shapes"} +Given one member depends on an outside Spec {outside: "that no code satisfies"} +Given one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"} +When the design slice runs over the probe Pack +Then the members read in depth order {order: "spec:probe.base, spec:probe.design, spec:probe.top"} +``` diff --git a/specs/consumers/design-slice.dependency-cycle.sdp.md b/specs/consumers/design-slice.dependency-cycle.sdp.md new file mode 100644 index 000000000..44166ab5f --- /dev/null +++ b/specs/consumers/design-slice.dependency-cycle.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.dependency-cycle +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [depth, orderAdvisory] +--- +# A cycle shares a depth + +## Intent + +- outcome: A cycle shares a depth. + +```gwt +Given a probe Pack whose members {structure: "include two members that depend on each other"} +Given one member depends on an outside Spec {outside: "that no code satisfies"} +Given one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"} +When the design slice runs over the probe Pack +Then the slice reports {cycles: 1} cycles and {exampleMembers: 0} folded example members +``` diff --git a/specs/consumers/design-slice.folded-example.sdp.md b/specs/consumers/design-slice.folded-example.sdp.md new file mode 100644 index 000000000..dde76af34 --- /dev/null +++ b/specs/consumers/design-slice.folded-example.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.folded-example +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [examplesFold, memberColumns] +--- +# An example folds without lending its readiness + +## Intent + +- outcome: An example folds without lending its readiness. + +```gwt +Given a probe Pack whose members {structure: "include an example that refines a member"} +Given one member depends on an outside Spec {outside: "that no code satisfies"} +Given one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"} +When the design slice runs over the probe Pack +Then the slice reports {cycles: 0} cycles and {exampleMembers: 1} folded example members +``` diff --git a/specs/consumers/design-slice.independent-example.sdp.md b/specs/consumers/design-slice.independent-example.sdp.md new file mode 100644 index 000000000..e343e9a15 --- /dev/null +++ b/specs/consumers/design-slice.independent-example.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.independent-example +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [examplesFold, boundary] +--- +# An example with its own dependency stays unfolded + +## Intent + +- outcome: An example with its own dependency stays unfolded. + +```gwt +Given a probe Pack whose members {structure: "include an example that refines a member and depends on an outside Spec"} +Given one member depends on an outside Spec {outside: "that no code satisfies"} +Given one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"} +When the design slice runs over the probe Pack +Then the slice reports {cycles: 0} cycles and {exampleMembers: 0} folded example members +``` diff --git a/specs/consumers/design-slice.outside-components.sdp.md b/specs/consumers/design-slice.outside-components.sdp.md new file mode 100644 index 000000000..3977ae3c1 --- /dev/null +++ b/specs/consumers/design-slice.outside-components.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.outside-components +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [outsideNeighbors] +--- +# Outside components stop at the touched components' neighbors + +## Intent + +- outcome: Outside components stop at the touched components' neighbors. + +```gwt +Given a probe Pack whose members {structure: "include a decision that refines the member it shapes"} +Given one member depends on an outside Spec {outside: "that a component's unit satisfies"} +Given one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"} +When the design slice runs over the probe Pack +Then the outside components are {outsideComponents: "component:probe.neighbor"} +``` diff --git a/specs/consumers/design-slice.outside-holding.sdp.md b/specs/consumers/design-slice.outside-holding.sdp.md new file mode 100644 index 000000000..c452bcc0c --- /dev/null +++ b/specs/consumers/design-slice.outside-holding.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.outside-holding +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [boundary, questionsInScope] +--- +# An outside prerequisite names the member it holds + +## Intent + +- outcome: An outside prerequisite names the member it holds. + +```gwt +Given a probe Pack whose members {structure: "include a decision that refines the member it shapes"} +Given one member depends on an outside Spec {outside: "stated below defined"} +Given one member is a component design whose component {anchor: "an anchor holds without satisfying the design"} +When the design slice runs over the probe Pack +Then the outside Spec holds {held: "spec:probe.base"} +``` diff --git a/specs/consumers/design-slice.outside-satisfied.sdp.md b/specs/consumers/design-slice.outside-satisfied.sdp.md new file mode 100644 index 000000000..00625acdd --- /dev/null +++ b/specs/consumers/design-slice.outside-satisfied.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.outside-satisfied +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [touchedComponents, outsideNeighbors, notDownstream] +--- +# A prerequisite reaches its satisfying component + +## Intent + +- outcome: A prerequisite reaches its satisfying component. + +```gwt +Given a probe Pack whose members {structure: "include a decision that refines the member it shapes"} +Given one member depends on an outside Spec {outside: "that a component's unit satisfies"} +Given one member is a component design whose component {anchor: "an anchor holds without satisfying the design"} +When the design slice runs over the probe Pack +Then the touched components are {components: "component:probe.dependency, component:probe.designed"}, {throughRestsOn: 1} of them reached through what the slice rests on +``` diff --git a/specs/consumers/design-slice.outside-unbound.sdp.md b/specs/consumers/design-slice.outside-unbound.sdp.md new file mode 100644 index 000000000..37fad9e7e --- /dev/null +++ b/specs/consumers/design-slice.outside-unbound.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.design-slice.outside-unbound +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.design-slice + verifies: + target: spec:consumers.design-slice + exercises: [touchedComponents, graphOnly] +--- +# An unbound prerequisite invents no component + +## Intent + +- outcome: An unbound prerequisite invents no component. + +```gwt +Given a probe Pack whose members {structure: "include a decision that refines the member it shapes"} +Given one member depends on an outside Spec {outside: "that no code satisfies"} +Given one member is a component design whose component {anchor: "an anchor holds without satisfying the design"} +When the design slice runs over the probe Pack +Then the touched components are {components: "component:probe.designed"}, {throughRestsOn: 0} of them reached through what the slice rests on +``` diff --git a/specs/consumers/design-slice.sdp.md b/specs/consumers/design-slice.sdp.md new file mode 100644 index 000000000..c92da1c27 --- /dev/null +++ b/specs/consumers/design-slice.sdp.md @@ -0,0 +1,101 @@ +--- +id: spec:consumers.design-slice +kind: behavior +altitude: feature +readiness: ready +relations: + refines: + target: spec:consumers.pack-design + reason: a slice reads a Pack's design as one whole, with its architecture and open decisions beside it + dependsOn: + - target: spec:consumers.review-perspectives + reason: a slice read for one concern takes the members a perspective selects + - target: spec:consumers.context-bundle + reason: a slice takes its scope as the bundle does, and builds a Spec list's boundary the same way + - target: spec:consumers.readiness-planner + reason: members rest on each other as the statement queue reads the four typed relations, and a cycle reports in its shape + - target: spec:model.planned-architecture + reason: planned components and differing designs come from the realization comparison + - target: spec:model.rule-keys + reason: each member's rule count is the count rule coverage reports + - target: spec:consumers.agent-surface.design-recipes + reason: decisions in scope read the decision register's rows + - target: spec:model.open-question-fields + reason: each question in scope carries its fields + decidedBy: + - target: spec:decisions.anchor-binding-grain + reason: the code a design builds on is reached through the design's own relations, never through a reference from that code + - target: spec:decisions.architectural-annotation + reason: a touched component carries the layer and context its anchor states +--- +# A slice reads a Pack as one design, with the architecture it touches and what each member needs next + +Features that must be reviewed together are already grouped as a Pack, and the Pack page shows each member in turn. A design pass also needs the whole at once: how the members rest on each other, which components they touch, realized or planned, which decisions and open questions bound them, and what each member needs for its next rung. Rounds of design add detail a little at a time; the floor sets how little a rung needs, and how much is enough stays a judgment the authoring guidance supports and no check makes. + +The first generation kept a slice as an authored file: a list of members and the one question they answer together, a saved view for design and review that delivered nothing itself (`@libar-dev/architect`, `formal-spec/04-tag-registry.md`, the hierarchy level `slice`). The original design saved AI context slices per Pack, per capability and per changed Spec as files (`docs/lineage/v0-design/07-spec-studio-and-projections.md` §5.5). The Protocol holds the first as a Pack, its members in reading order and its framing saying what they answer together, and it refused the second: a context bundle is composed on each invocation and never saved. A design slice is therefore a reading and not an artifact. It is a Pack read whole, narrowed to one perspective's members when a pass reads for one concern, or an ordered list of Specs, composed from the graph each time, and it adds no grouping, kind, stage or gate. A Spec's planning slice reads one Spec's neighborhood; widening it hop by hop would pick a reach no one authored, so a design slice starts from the set a person chose. + +The reading answers what a card-by-card page leaves a reviewer to assemble. A design that builds on existing code says so through its own `dependsOn` or `refines` to the Spec that code satisfies, so the architecture a design ahead of its code will touch is read from those relations and the anchors of the Specs they name. The first adopter asked for the same separation: the design under review apart from the contracts and decisions it rests on, and the outside Spec whose rung holds a member shown at the boundary with what it holds (`libar-platform/docs/feedback/sdp-feedback-02.md`, items 11 and 12). + +## Intent + +- actor: A person or an agent running a design pass over features that must be reviewed together. +- problem: The Pack page reads member by member, so a reviewer assembles the dependency order, the touched architecture and the open decisions by hand before judging the design as a whole. +- outcome: Read a Pack as one design slice: its members and their relations, the components they touch, realized and planned, the decisions and open questions that bound them, and what each member needs for its next rung. +- value: A design pass judges the whole before its parts, starting from the members others rest on, and sees how much design each member carries beside its rung without a score. + +## Behavior + +- rule: [#readingNotArtifact] A design slice is a reading of a set of Specs that a catalog recipe composes from the graph on each invocation; it is never authored, saved or committed, it adds no grouping, namespace, kind, stage or gate, and reading it records nothing. +- rule: [#scope] A slice takes one scope: a Pack, its resolved members read in authored order, or an ordered list of Spec ids; a Pack scope may be narrowed to an ordered subset of its members, such as the members the perspective filter selects, and then says that it is a subset. +- rule: [#restsOn] Inside the slice a member rests on each other member it names by `refines`, `dependsOn`, `constrainedBy` or `decidedBy`, except a `refines` to a member that names it back by one of the other three, as the statement queue reads them; every relation of those four types between two members is listed with its type, its reason and whether it orders, so a reading tells an ordering link from a relation that orders nothing. +- rule: [#depth] A member that rests on no other member has depth 1, as the statement queue's rounds start at 1, and any other member has the depth after the deepest member it rests on; members that reach each other through those links form a cycle, share the depth after the deepest member outside the cycle that any of them rests on, and are listed with one closed path, and the slice refuses nothing. +- rule: [#examplesFold] An example member folds into the member it refines when that `refines` is its only relation of the four types, in either direction, inside the slice or across its boundary: it is listed among that member's example members and keeps its own row, with its own stated rung, floor reached, stated next rung and holds, and folding lends the member none of them; an example with any other such relation is a member like any other. +- rule: [#memberColumns] Each member carries its stated rung, the floor reached, its stated next rung with the clauses that hold it, its design columns, its rule count as rule coverage counts it, its verifiers and verifying examples with how many are enabled, the units that implement and reference it, and the components those units belong to; the columns are counts and lists beside the rung, never a score, a verdict or a rung. +- rule: [#boundary] The slice's boundary is built over its members as Pack design builds a Pack's, so a whole Pack's slice has the Pack's boundary; each Spec the slice rests on also carries the floor it reaches and the members whose stated next rung it holds, and a Pack member left out of a narrowed slice is marked as in the Pack. +- rule: [#touchedComponents] The components a slice touches are the anchored components it reaches three ways, each named on the component's row and never merged: a unit that satisfies or references a member belongs to it; a unit that satisfies a Spec the slice rests on belongs to it; or a member component design names it. +- rule: [#componentDesigns] Every member that is a component design, as the planned-architecture convention selects it, is its own row whether or not code realizes it: the component it names, the layer, context, role and uses it declares, whether an anchor holds that component, whether that anchor satisfies the design, and the realization comparison's differing and unreadable rows for it; a declared value never stands in for an anchored one, nor an anchored value for a declared one. +- rule: [#touchedUses] Between touched components the slice lists the anchored `uses` aggregated to component pairs, as the architecture map counts them, a use within one component left out, each pair with the unit edges behind its count; each component a member design declares in `uses` is a planned use of that design, listed apart from the anchored pairs and never added to their counts. +- rule: [#outsideNeighbors] A component one `uses` step from a touched component, in either direction, or named in a member design's `uses`, that the slice does not touch is listed as outside the slice with how it is reached, and its anchored pairs with touched components are listed and marked outside; the slice reaches no further, and outside components and pairs are counted apart from the touched ones. +- rule: [#unplaced] A unit that implements or references a member and belongs to no component is listed apart, so no binding drops out of the reading. +- rule: [#notDownstream] The slice does not count the code of Specs that rest on it: that code follows a change to the slice, and design-change impact reads it; a component that uses a touched component appears only as an outside neighbor. +- rule: [#decisionsInScope] The decisions in scope are the decision members and every decision a member names through `decidedBy`, each with its stated rung, the floor reached, whether it is a member, the members it shapes, the decisions it refines and supersedes, and its open and blocking question counts. +- rule: [#questionsInScope] The open questions in scope are those of every member and of every decision in scope, and the blocking questions of every Spec outside the slice that holds a member's stated next rung, blocking questions first; each carries its address, its fields and the members whose stated next rung its Spec holds. +- rule: [#noDesignVerdict] The slice shows how much design each member carries and never whether it is enough: it reports no over- or under-designed member, no ratio of design to rung and no finding, because how much to write is the authoring guidance's judgment and checks police conformance and honesty, never content quality. +- rule: [#graphOnly] The slice reads declared relations, anchored bindings and component designs only; it infers no dependency, component or decision from prose, paths, imports or names. +- rule: [#orderAdvisory] Depth gives a reading order, the members others rest on first; it records, schedules and gates nothing, and a member read or designed out of that order is never a finding. + +## Design + +- sliceRecipe: recipe 54, the design slice, takes exactly one scope, `params.pack` or `params.specs`, an optional `params.members` beside `pack`, and an optional `params.detail`; an unknown parameter name returns `{ unknownParams, accepts }` alone, and any input `#design.sliceDiagnostics` refuses returns `{ diagnostics }` and no slice. +- sliceDiagnostics: the checks run in this order and every failure is kept, a check that reads an earlier value being skipped when that value failed: a `detail` that is present and neither a boolean nor a list of strings, null included, gives `{ scope: "detail", problem: "Expected true, false or a list of member ids." }`; neither or both of `pack` and `specs` gives `{ scope: "scope", problem: "Supply exactly one of pack or specs." }`; a `pack` that is not a string naming a Pack gives `{ scope: "pack", problem: "Pack does not resolve." }`; beside a resolved Pack, a `members` that is not a non-empty list of distinct strings gives `{ scope: "members", problem: "Expected a non-empty list of distinct member ids." }`, and otherwise each id that is no resolved member gives `{ scope: , problem: "Not a resolved member of the Pack." }` in list order; a `specs` that is not a non-empty list of distinct strings gives `{ scope: "specs", problem: "Expected a non-empty list of distinct Spec ids." }`, and otherwise each id the graph does not hold gives `{ scope: , problem: "Spec does not resolve." }` in list order, as the context bundle names an unresolved id; and a `members` beside `specs` gives `{ scope: "members", problem: "members narrows only a Pack scope." }`. +- sliceDetail: `params.detail` is absent or `false` for the summary, `true` for every row in full, or a list of member ids for full rows of those members beside summary rows of the rest, an id that is no member adding `{ id, reason: "not-member" }` to the slice's `diagnostics`, as Pack design reads it; a summary keeps `totals` whole and adds `leftOut`, one line for each kind of detail it omits. +- sliceAnswer: `{ scope, totals, members, structure, boundary, architecture, decisions, questions }`, where `scope` is `{ pack, title, framing, subset, members, unresolved }` for a Pack, `unresolved` naming the manifest ids the graph does not hold, and `{ specs }` for a Spec list. +- sliceTotals: `{ members, exampleMembers, relations, depth, cycles, heldBelowNextRung, waitingForAuthor, restsOn, holdingOutside, restedOnBy, verifiedBy, supersededBy, components: { touched, throughMembers, throughRestsOn, throughDesigns, outside }, designs: { total, realized, matching, unrealized, differing, unreadable }, componentUses: { inside, outside, planned }, decisions, decisionsBelowReady, openQuestions, blockingQuestions }`, where `exampleMembers` counts folded example members, `depth` is the deepest member's depth, `holdingOutside` counts the outside Specs that hold a member, `heldBelowNextRung` and `waitingForAuthor` count as Pack design counts them, `components.touched` counts distinct anchored components however reached and `components.outside` the outside components, `designs` counts member designs as the realization comparison's totals count designs, and `componentUses` counts the anchored pairs inside, the anchored pairs marked outside, and the planned uses. +- memberRow: `{ id, title, specKind, altitude, statedReadiness, floorReached, statedNextRung, holds, depth, restsOn, restedOnBy, exampleOf, exampleMembers, design, rules: { total, keyed }, examples: { total, enabled }, verifiers: { total, enabled }, implementedBy, referencedBy, components }` in the scope's order, where `holds` lists the stated-next-rung failures as `[{ clauseId, description, targets: [{ type, id, statedReadiness }] }]`, `restsOn` and `restedOnBy` are member ids, `exampleOf` is the member an example member refines or null, `exampleMembers` the folded example members, `design` is the design columns as `{ entries, declarations, openQuestions, blockingQuestions, decisions }` counts, `examples` the verifying examples bound to the member wherever they sit, apart from `exampleMembers`, `rules.total` is the keyed and unkeyed rule and flow entries of the primary behavior owner, `implementedBy` and `referencedBy` are unit ids, and `components` the ids of the components those units belong to. +- memberSummary: `{ id, specKind, statedReadiness, floorReached, statedNextRung, holds, depth, restsOn, exampleOf, design, rules, implementedBy, referencedBy }` is a member's summary row, with `holds` as `[{ clauseId, targets: [id] }]`, as Pack design's summary reads it, and `implementedBy` and `referencedBy` as counts. +- structureShape: `{ edges: [{ from, to, type, orders, reason }], cycles: [{ members, cycle }], unresolved: [{ from, to, type, reason }] }`, one edge per declared typed relation between two members, sorted by `from`, `type` and `to`, `orders` false only for a `refines` the target names back, and `reason` present only when the relation states one; `cycles` in the statement queue's cycle row; a summary drops each edge's `reason`; `unresolved` keeps unresolved outgoing typed targets apart from the resolved boundary, with no invented title or rung. +- boundaryRow: Pack design's boundary row `{ id, title, statedReadiness, implemented, via }` with a `via` that has relation reasons also carrying `reasons: [{ member, reason }]` for those relations, and with `specKind`, `floorReached`, `holds`, the member ids whose stated-next-rung failures name it as a target, empty outside `restsOn`, and `inPack: true` on a Pack member left out of a narrowed slice, in `{ restsOn, restedOnBy, verifiedBy, supersededBy }` sorted by id; a summary keeps `{ id, statedReadiness, holds }` and `inPack`. +- componentRow: `{ id, label, layer, context, members: { satisfies, references }, restsOn, designs }` sorted by id, one per touched anchored component, where `members.satisfies` and `members.references` are the members whose units belong to it by each binding, `restsOn` the outside Specs the slice rests on whose satisfying units belong to it, `designs` the member component designs naming it, and `label`, `layer` and `context` come from the architecture map's component row. +- componentSummary: `{ id, layer, context, members: { satisfies, references }, restsOn, designs }`, each list replaced by its count and `label` left out. +- designRow: `{ spec, componentId, layer, context, role, uses, anchorExists, realized, differing, unreadable }` sorted by `spec`, one per member component design, where the declared values are read as spec:model.planned-architecture#design.designKeys reads them, null when unstated or unreadable and `uses` an empty list for `none`, `realized` holds as spec:model.planned-architecture#design.realizedWhen says, `differing` lists the comparison's `{ field, planned, realized }` rows for the design and `unreadable` its `{ key, value }` rows; a summary keeps `{ spec, componentId, anchorExists, realized }` and the two lists as counts. +- componentUse: `{ from, to, count, outside, units }` sorted by `from` and `to`, one per pair of components with an anchored `uses` between them and at least one end touched, where `count` is the architecture map's count of unit `uses` edges for the pair, `units` lists those edges as `{ from, to, claim }` from the architecture map's `usesEdges`, and `outside` is true when one end is not touched; a summary drops `units`. +- plannedUse: `{ design, from, to, outside }` sorted by `from`, `to` and `design`, one per component a member design declares in `uses` other than its own, where `from` is the component the design names and `outside` is true when `to` is neither touched nor named by a member design. +- outsideRow: `{ id, label, layer, context, anchored, reachedBy }` sorted by id, where `reachedBy` lists, in this order, `touchedUses` when a touched component uses it, `usesTouched` when it uses a touched component, and `plannedUse` when a member design declares it, and `anchored` says whether an anchor holds it; a summary keeps `{ id, anchored, reachedBy }`. +- architectureShape: `{ components, designs, uses, plannedUses, outside, unplaced }`, where `unplaced` lists `{ unit, member, via }`, `via` being `satisfies` or `references`, for each unit of a member that belongs to no component. +- decisionRow: `{ id, title, statedReadiness, floorReached, member, shapes, refines, supersedes, supersededBy, openQuestions, blockingQuestions }` sorted by id, where `shapes` lists the members that name the decision through `decidedBy` and the next three lists come from its decision register row; a summary keeps `{ id, statedReadiness, member, shapes }` with `shapes` as a count. +- questionRow: `{ address, spec, origin, index, question, blocking, deferral, decider, links, provisional, trigger, holds }` with the open-question register's fields, where `origin` is `member`, `decision` or `restsOn`, `index` the question's authored position from 0, `address` null without a key, and `holds` the members whose stated next rung its Spec holds when the question blocks, its own Spec first when that Spec is a member held by the floor's blocking-question clause, empty for a non-blocking question; rows sort blocking first, then by how many members they hold, then by Spec id and position; a summary keeps a non-blocking question as `{ address, spec, index }`. + +## Example space + +```gwt-vocabulary +Given a probe Pack whose members {structure:"rest on each other in a chain"|"include a decision that refines the member it shapes"|"include two members that depend on each other"|"include an example that refines a member"|"include an example that refines a member and depends on an outside Spec"} +Given one member depends on an outside Spec {outside:"that a component's unit satisfies"|"that no code satisfies"|"stated below defined"} +Given one member is a component design whose component {anchor:"no anchor holds"|"an anchor holds without satisfying the design"|"an anchor satisfies, matching every declared value"|"an anchor satisfies, with a differing layer and uses"|"an anchor satisfies, with an unreadable layer"} +When the design slice runs over the probe Pack +Then the members read in depth order {order:string} +Then the slice reports {cycles:number} cycles and {exampleMembers:number} folded example members +Then the touched components are {components:string}, {throughRestsOn:number} of them reached through what the slice rests on +Then the member design reads {realized:"realized"|"unrealized"} with {differing:number} differing and {unreadable:number} unreadable rows +Then the outside components are {outsideComponents:string} +Then the outside Spec holds {held:string} +``` diff --git a/specs/consumers/engine-provenance.sdp.md b/specs/consumers/engine-provenance.sdp.md index 79a3abe9e..d4aa4bc38 100644 --- a/specs/consumers/engine-provenance.sdp.md +++ b/specs/consumers/engine-provenance.sdp.md @@ -4,6 +4,9 @@ kind: behavior altitude: story readiness: ready relations: + refines: + target: spec:consumers.command-line + reason: the version flag is one more answer the command line gives an adopter dependsOn: target: spec:consumers.adopter-on-ramp reason: an adopter pinned to a build asks the CLI the adopter on-ramp points it to which build it runs diff --git a/specs/consumers/graph-first-planning.sdp.md b/specs/consumers/graph-first-planning.sdp.md index a6bd6f649..062a3e450 100644 --- a/specs/consumers/graph-first-planning.sdp.md +++ b/specs/consumers/graph-first-planning.sdp.md @@ -28,4 +28,4 @@ relations: - rule: A body of work is grouped in a Pack named for the capability it designs, never for a plan number, listing its Specs in reading order; membership confers no readiness, delivery fact or gate, and the Pack remains a review aggregate after the work ends. - rule: Heuristics for choosing work remain advisory behavior rules here or 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. -- rule: A plan file points to the Pack and carrying Specs for intended truth and derived state; it may keep an advisory execution and recovery record, while plan numbering, recorded work order and staleness never become graph structure or workflow authority. +- rule: A corpus that keeps plan documents lets each point to its Packs and carrying Specs for intended truth and derived state; a plan document confers nothing, and plan numbering, recorded work order and staleness never become graph structure or workflow authority. Advisory priority over the next work lives in a roadmap Pack. diff --git a/specs/consumers/obsidian-vault.notes.sdp.md b/specs/consumers/obsidian-vault.notes.sdp.md index c8cf99263..f2860ab11 100644 --- a/specs/consumers/obsidian-vault.notes.sdp.md +++ b/specs/consumers/obsidian-vault.notes.sdp.md @@ -42,14 +42,14 @@ Two findings from the prototypes shape the names and properties. Obsidian refuse - blockId: `
-`, written as ` ^
-` at the end of the entry's line, so `spec:#design.limit` opens as `[[sdp/specs//.md#^design-limit|spec:#design.limit]]`. - propertyEncoding: the properties sit between two `---` lines at the top of the note, one per line as `: `, in the order listed below; a property the graph does not hold is left out. - specProperties: `aliases` (the id, then the title), `title`, `specKind`, `altitude`, `statedReadiness`, `derivedReadiness` (`none` when no rung derives), `statedNextRung` (left out when the Spec states `ready`), `statedNextRungFailures` (the failing clause ids), `implementationBinding`, `verifierBinding`, `designEntries`, `declarations`, `openQuestions` (a count), `blockingQuestions`, `decisions` (links to the notes of the resolving decisions the design columns name), `packs` (links to the Pack notes), `carrier` (the root-relative carrier path). -- packProperties: `aliases` (the id, then the title), `title`, `manifest` (the root-relative manifest path). +- packProperties: `aliases` (the id, then the title), `title`, `manifest` (the root-relative manifest path), and optional `order`, present as `"priority"` only when the Pack states it. - codeProperties: `aliases` (the id), `label`, `role`, `layer`, `context`, `component` (a link to the component's note), `sourceFile` (root-relative), `sourceLine`. - termProperties: `aliases` (the term, then its address), `definedIn` (a link to the defining Spec's note). - banner: the first line after the properties, `` > [!info] Generated by `sdp vault` from the graph. Edit the ``, never this note: a regeneration refuses while a note it wrote has changed. ``, naming the Spec's carrier, the Pack's manifest or the unit's source file. - headLine: under a Spec note's title, `` `` · · · stated · floor ``, then ` · : ` when the Spec states less than `ready`. - specNoteBody: the banner, `# `, the head line, the narrative's paragraphs, each section under `## <Section>` in the graph's order with Intent's open questions under `### Open questions` and an example's steps or an example space's vocabulary in a fenced `gherkin` block, then `## Relations` with `### From this Spec` and `### Toward this Spec`, `## Bindings`, `## Packs` and `## Findings`, each heading left out when it has nothing under it. - bindingLines: under `## Bindings`, `- Implementation binding: present` or `none` with each realizing unit, the units that reference the Spec, `- Verifier binding: present` or `none` with each verifier, `- Expected-outcome oracle: present` or `none`, and `- Runtime observation: not tracked`. -- packNoteBody: the banner, `# <title>`, the framing, `## Members, filterable` with the Pack's Base embedded, `## Members, in reading order` as a numbered list in manifest order, each a link with its stated rung, floor and stated next rung or `nothing blocks`, then `## Boundary` with the reader's four boundary lists and `## Findings`. +- packNoteBody: the banner, `# <title>`, the framing, `## Members, filterable` with the Pack's Base embedded, `## Members, in advisory priority order` when `order` is `"priority"`, otherwise `## Members, in reading order`, as a numbered list in manifest order, each a link with its stated rung, floor and stated next rung or `nothing blocks`, then `## Boundary` with the reader's four boundary lists and `## Findings`. A roadmap states `Priority confers no permission or sequencing requirement.` before its ordered list heading. - codeNoteBody: the banner, `# <id>`, the label, `` `<file>:<line>` ``, then link lists under `## Member of`, `## Members`, `## Uses`, `## Used by`, `## Satisfies`, `## References`, `## Verifies` and `## Models`, each left out when empty. - termNoteBody: the banner, `# <term>`, the definition, and `Defined in [[<Spec note>.md#^term-<key>|<term address>]].` - caseCollisionMessage: `sdp vault: <first id> and <second id> name notes whose paths differ only in case; nothing was written`, one line per colliding pair, the ids in code-unit order. diff --git a/specs/consumers/projections-model.roadmap-views.sdp.md b/specs/consumers/projections-model.roadmap-views.sdp.md index 8df605dad..a6df5d4f0 100644 --- a/specs/consumers/projections-model.roadmap-views.sdp.md +++ b/specs/consumers/projections-model.roadmap-views.sdp.md @@ -2,13 +2,16 @@ id: spec:consumers.projections-model.roadmap-views kind: model altitude: story -readiness: idea +readiness: scoped relations: refines: spec:consumers.projections-model + dependsOn: + target: spec:consumers.roadmap + reason: the roadmap view the owner asked for is the roadmap reading, its page and its memo --- # Roadmap views read releases, baselines and phases from git and the graph -Release, baseline and roadmap vocabulary belongs to optional projections that wait until a reader needs them. +Release, baseline and roadmap vocabulary belongs to optional projections that wait until a reader needs them. The owner, choosing what the next pull requests take up, is the reader the roadmap view waited for, and the roadmap reading answers it: a Pack whose manifest states a priority order, read with what each entry still needs, shown on the Studio's roadmap page and rendered as a pull request's forward section. Release and baseline views still wait for a reader of their own. ## Intent @@ -16,12 +19,14 @@ Release, baseline and roadmap vocabulary belongs to optional projections that wa ### Open questions -- [blocking #namedRoadmapReader] Does a reader need release, baseline or roadmap views that the shipped projections, the recipes and git do not serve? +- [blocking #releaseReader] Does a reader need release or baseline views that the roadmap, the recipes and git tags do not serve? - decider: owner - - trigger: a named reader with that need, and a way to measure it inside the arc that builds the views + - provisional: Keep release and baseline views deferred; the trigger opens this question and does not authorize either view's build. + - trigger: the first published release ## 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. +- **roadmap** — A Pack whose manifest states a priority order, read with what each entry still needs next; its order is an advisory preference, never a schedule or a gate. +- **phase / iteration / milestone** — Descriptive words a roadmap's framing may use, never an entry field, a date, a gate or an enforced sequence. diff --git a/specs/consumers/projections-model.sdp.md b/specs/consumers/projections-model.sdp.md index 358f669bc..8b5c67879 100644 --- a/specs/consumers/projections-model.sdp.md +++ b/specs/consumers/projections-model.sdp.md @@ -1,10 +1,9 @@ --- id: spec:consumers.projections-model kind: model -altitude: feature +altitude: epic readiness: ready relations: - refines: spec:protocol.self-hosting decidedBy: - target: spec:decisions.mcp-deferred reason: MCP integration waits for a concrete caller to establish its boundary and contract diff --git a/specs/consumers/review-perspectives.sdp.md b/specs/consumers/review-perspectives.sdp.md index ba158442c..d96567bfd 100644 --- a/specs/consumers/review-perspectives.sdp.md +++ b/specs/consumers/review-perspectives.sdp.md @@ -11,7 +11,7 @@ relations: 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 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 Protocol takes the member filter and the order and leaves each member's content whole; emphasis inside a member stays the reader's. -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. +The owner ruled the word: perspective. The Spec Studio keeps lens for its graph views, its Packs, capabilities, 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. diff --git a/specs/consumers/roadmap.blocking-part.sdp.md b/specs/consumers/roadmap.blocking-part.sdp.md new file mode 100644 index 000000000..132e568e5 --- /dev/null +++ b/specs/consumers/roadmap.blocking-part.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.roadmap.blocking-part +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.roadmap + verifies: + target: spec:consumers.roadmap + exercises: [entryNeed, entryShows, workRows] +--- +# A descendant question holds the entry for design + +## Intent +- outcome: Execute one roadmap reading over a probe corpus. +- assumption: The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The descendant states scoped, refines the ready root, has one rule and a blocking question. The roadmap reads the shipped body over the extracted probe corpus. + +```gwt +Given a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"} +Given that work {state: "has a ready root and one descendant held by a blocking question"} +When the roadmap recipe reads the probe roadmap +Then the entry names {part: "spec:probe.part"} among the parts with its need, held by {clause: "no-blocking-open-questions"} +``` diff --git a/specs/consumers/roadmap.buildable-part.sdp.md b/specs/consumers/roadmap.buildable-part.sdp.md new file mode 100644 index 000000000..d4f472967 --- /dev/null +++ b/specs/consumers/roadmap.buildable-part.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.roadmap.buildable-part +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.roadmap + verifies: + target: spec:consumers.roadmap + exercises: [scope, specNeed, entryNeed, workRows] +--- +# A ready unbound part needs building + +## Intent +- outcome: Execute one roadmap reading over a probe corpus. +- assumption: The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The root has a binding and its ready descendant has none. The roadmap reads the shipped body over the extracted probe corpus. + +```gwt +Given a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"} +Given that work {state: "has one part ready and not implemented"} +When the roadmap recipe reads the probe roadmap +Then the entry's need reads {need: "to build"} +``` diff --git a/specs/consumers/roadmap.built-work.sdp.md b/specs/consumers/roadmap.built-work.sdp.md new file mode 100644 index 000000000..96662af2b --- /dev/null +++ b/specs/consumers/roadmap.built-work.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.roadmap.built-work +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.roadmap + verifies: + target: spec:consumers.roadmap + exercises: [scope, specNeed, entryNeed, workRows] +--- +# Built work stays on the roadmap + +## Intent +- outcome: Execute one roadmap reading over a probe corpus. +- assumption: The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. Every part states ready and has a direct implementation binding. The roadmap reads the shipped body over the extracted probe corpus. + +```gwt +Given a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"} +Given that work {state: "is ready and implemented throughout"} +When the roadmap recipe reads the probe roadmap +Then the entry's need reads {need: "built"} +``` diff --git a/specs/consumers/roadmap.design-part.sdp.md b/specs/consumers/roadmap.design-part.sdp.md new file mode 100644 index 000000000..43a7b6521 --- /dev/null +++ b/specs/consumers/roadmap.design-part.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.roadmap.design-part +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.roadmap + verifies: + target: spec:consumers.roadmap + exercises: [scope, specNeed, entryNeed, workRows] +--- +# An unfinished part needs design + +## Intent +- outcome: Execute one roadmap reading over a probe corpus. +- assumption: The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The descendant states scoped and has a blocking question. The roadmap reads the shipped body over the extracted probe corpus. + +```gwt +Given a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"} +Given that work {state: "has one part below ready whose floor stops short"} +When the roadmap recipe reads the probe roadmap +Then the entry's need reads {need: "to design"} +``` diff --git a/specs/consumers/roadmap.drifting-part.sdp.md b/specs/consumers/roadmap.drifting-part.sdp.md new file mode 100644 index 000000000..432009b35 --- /dev/null +++ b/specs/consumers/roadmap.drifting-part.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.roadmap.drifting-part +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.roadmap + verifies: + target: spec:consumers.roadmap + exercises: [drift, workRows] +--- +# A binding below ready remains drift + +## Intent +- outcome: Execute one roadmap reading over a probe corpus. +- assumption: The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The descendant states scoped, has a blocking question and an implementation binding. The roadmap reads the shipped body over the extracted probe corpus. + +```gwt +Given a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"} +Given that work {state: "has one part implemented below ready"} +When the roadmap recipe reads the probe roadmap +Then the entry counts {drift: 1} parts as drift +``` diff --git a/specs/consumers/roadmap.memo.sdp.md b/specs/consumers/roadmap.memo.sdp.md new file mode 100644 index 000000000..ada430c2a --- /dev/null +++ b/specs/consumers/roadmap.memo.sdp.md @@ -0,0 +1,46 @@ +--- +id: spec:consumers.roadmap.memo +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.roadmap + dependsOn: + - target: spec:consumers.intent-composition.proposal.memo + reason: the memo writes its code spans by the proposal memo's markup rules + - target: spec:consumers.agent-surface.register-recipes + reason: the deferred items come from the open-question register +--- +# A roadmap renders as the forward section of a pull request description + +A pull request description ends with a forward section: the next work in priority order, what waits on the owner, and what is deferred until its trigger. Written by hand, it drifts from the Specs it names and is rewritten each time. The roadmap memo renders it from the roadmap reading at the head of the branch, so the forward section a description carries is the roadmap the graph holds. The memo states what the graph holds and nothing more: it copies each entry's purpose from its own intent and derives its dependency reach, and the owner's reason for an entry's place, the re-entry condition of the next pull request and the durable refused list stay the description author's words around it. + +## Intent + +- actor: The author of a pull request description, and the owner reading it. +- problem: A description's forward section is written by hand from plan files and recipe answers, so it drifts from the roadmap and the Specs it names. +- outcome: Render a roadmap's entries, what waits on the owner and the deferred questions as Markdown a pull request description carries as its forward section, so the plan a description proposes is the roadmap the graph holds. + +## Behavior + +- rule: [#memoRecipe] Recipe 55 renders one roadmap's reading, recipe 53's answer for that Pack, as one Markdown memo a pull request description carries as its forward section; the roadmap page shows the same bytes. +- rule: [#sections] The memo holds, in order: the roadmap's title, a line saying what to state and built mean, and its framing; the entries to design, to state and to build, each need in priority order with every entry's place; what waits on the owner; the questions that name no decider; the entries built and still listed; the unresolved members with their original places and ids; and the deferred open questions with their triggers. The to design and to build sections always appear and say when they hold no entry; every other section is left out when it holds no row. +- rule: [#purposeAndReach] An entry's purpose and reach is read, never authored: its intent's problem, or its outcome when it states no problem, then how many Specs outside its scope wait on it and how many of its parts are ready to build now. These values state its purpose and its place in the graph's structure, never why the owner selected the entry or gave it its place; the description's author may state that reason around the memo, and the memo adds no status, date, estimate or assignment to a pull request. +- rule: [#holds] An entry to design names the parts that set its need, its own first, each with its stated next rung and the clauses and dependency targets that hold it, then its blocking questions and its decisions below `ready`; an entry to state names the waiting statements the statement queue puts before its own, inside the entry, elsewhere on the roadmap and outside it, as advice on the order of statements and never as a readiness requirement; an entry to build names each Spec it rests on that is not built. +- rule: [#authorsOwn] The memo derives no reason for the priority and carries no re-entry condition or refused list; the description's author writes those around it. +- rule: [#readingOrderNoted] A memo of a Pack whose order is reading order says so under the framing, so no description presents it as a priority. +- rule: [#deterministic] One graph and one set of parameters render one memo, byte for byte. + +## Design + +- memoRecipe: recipe 55, the roadmap memo, takes `params.pack` and `params.decider` as recipe 53 does; without `pack` it reads the one Pack whose manifest states `order: priority`, and with none or several it returns `{ diagnostics: [{ parameter, problem }], memo: null }`; otherwise it answers `{ diagnostics: [], memo }`, the memo one string. +- memoHead: the memo opens with the line `## The next work, from the roadmap`, a blank line, the line `Roadmap: ` followed by the Pack's title and its id as a code span, a blank line, the line `To state means the floor allows ready and a person still judges the statement. Built means every Spec of the entry's work states ready and has an implementation binding; it reports no passing test and no deployment.` and a blank line, each paragraph of the framing as authored followed by a blank line, and for a reading-order Pack the line `This Pack's order is reading order, not a stated priority.` and a blank line. +- memoDesign: the section `### To design` holds a table with the columns `Place`, `Entry`, `Next` and `Purpose and reach`; `Next` reads, for at most three of the work rows with the entry's need, the entry's own first and the rest in Spec-id order, the row's id as a code span or `itself` for the entry's own, its stated rung and stated next rung as code spans joined by ` → `, then `held by` and each clause id followed by its targets as code spans in parentheses, or `nothing holds it`; then `and <n> more parts to design` when rows are left out; then `blocked by` and each blocking question's address as a code span, or its Spec id when unkeyed; then `decided by` and each decision below `ready` as a code span with its stated rung; the parts joined by `; `. +- memoState: the section `### To state` holds a table with the columns `Place`, `Entry`, `Earlier waiting statements` and `Purpose and reach`; the third column reads `No preceding waiting statement in the statement queue.` when the entry's `earlierStatements` is empty, and otherwise `in this entry` followed by those inside it as code spans, `on the roadmap` followed by those at another entry as code spans each with `(place <n>)`, and `<n> outside the roadmap`, each part present only when it holds a row and the parts joined by `; `. +- memoBuild: the section `### To build` holds a table with the columns `Place`, `Entry`, `Purpose and reach` and `Rests on`; `Rests on` lists each Spec the entry rests on as a code span with its need, or reads `nothing unbuilt`. +- memoEntry: an `Entry` cell is the entry's title followed by its id as a code span; the `Purpose and reach` cell is the intent line, ending with a full stop when it ends with none, then `Waited on by <n>.` when Specs wait on it and `<n> of its parts ready to build now.` for an entry not to build that has such parts. +- memoWaiting: the section `### Waiting on the <decider>` lists one line per row of recipe 53's `waiting.questions` and `waiting.statements`, questions first and statements in the order the answer gives: a question as its address as a code span, or its Spec id when unkeyed, `(blocking)` when it blocks, `place` and the places, and its text; a statement as `State `, the Spec id as a code span, its stated rung and the rung its floor allows, and the places. +- memoUndecided: the section `### Questions without a decider` opens with the line `These questions have no named decider.` and a blank line, then lists one line per row of recipe 53's `waiting.withoutDecider`: its address as a code span, or its Spec id as a code span followed by `question` and its authored position from 1 when unkeyed, then `(blocking)` when it blocks, `place` and the places; the text stays on the roadmap page, so a corpus with many such questions keeps a short memo. +- memoBuilt: the section `### Built and still on the roadmap` lists each built entry as its place, its title and its id as a code span. The section `### Unresolved members` follows when recipe 53 reports unresolved members, listing each as `- Place <place>: <id>`, the id as a code span. +- memoDeferred: the section `### Deferred, with their triggers` lists each open question recipe 20 marks as a deferral, in the register's order, as its address as a code span, or its Spec id when unkeyed, its text, and `Trigger:` with its trigger. +- memoMarkup: code spans follow the proposal memo's rule; outside code spans, authored `<`, `>` and `&` are written as `<`, `>` and `&`, respectively; other authored text is written verbatim, with each line break written as a space and, inside a table cell, each `|` written `\|`; every line ends with a line feed and the memo with one. diff --git a/specs/consumers/roadmap.missing-evidence.sdp.md b/specs/consumers/roadmap.missing-evidence.sdp.md new file mode 100644 index 000000000..daaa89e01 --- /dev/null +++ b/specs/consumers/roadmap.missing-evidence.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.roadmap.missing-evidence +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.roadmap + verifies: + target: spec:consumers.roadmap + exercises: [entryNeed, entryShows, workRows] +--- +# A descendant without evidence names its holding clause + +## Intent +- outcome: Execute one roadmap reading over a probe corpus. +- assumption: The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The descendant states scoped, refines the ready root and has no behavior evidence. The roadmap reads the shipped body over the extracted probe corpus. + +```gwt +Given a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"} +Given that work {state: "has a ready root and one descendant without its kind's evidence"} +When the roadmap recipe reads the probe roadmap +Then the entry names {part: "spec:probe.part"} among the parts with its need, held by {clause: "kind-evidence-present"} +``` diff --git a/specs/consumers/roadmap.outside-parent.sdp.md b/specs/consumers/roadmap.outside-parent.sdp.md new file mode 100644 index 000000000..57afd286a --- /dev/null +++ b/specs/consumers/roadmap.outside-parent.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.roadmap.outside-parent +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.roadmap + verifies: + target: spec:consumers.roadmap + exercises: [scope, restsOn, workRows] +--- +# A descendant names its outside parent + +## Intent +- outcome: Execute one roadmap reading over a probe corpus. +- assumption: The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The defined descendant refines both the ready root and an outside scoped Spec. The roadmap reads the shipped body over the extracted probe corpus. + +```gwt +Given a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"} +Given that work {state: "has one descendant that also refines a Spec outside the roadmap"} +When the roadmap recipe reads the probe roadmap +Then the entry rests on {target: "spec:probe.outside"} through {origin: "spec:probe.part"} +``` diff --git a/specs/consumers/roadmap.sdp.md b/specs/consumers/roadmap.sdp.md new file mode 100644 index 000000000..a6acc4158 --- /dev/null +++ b/specs/consumers/roadmap.sdp.md @@ -0,0 +1,81 @@ +--- +id: spec:consumers.roadmap +kind: behavior +altitude: feature +readiness: ready +relations: + refines: + target: spec:consumers.graph-first-planning + reason: a roadmap is advisory reading of the graph for choosing the next work + dependsOn: + - target: spec:validation.next-rung-floor + reason: an entry's stated next rung and the clauses that hold it come from the one floor evaluator + - target: spec:validation.typed-dependency-floor + reason: what an entry rests on is read through the relations the ready floor reads + - target: spec:model.open-question-fields + reason: the questions waiting on a person are selected by their decider field + - target: spec:consumers.readiness-planner + reason: the statements waiting on a person follow the statement queue's order + decidedBy: spec:decisions.roadmap-priority +--- +# A roadmap orders the next bodies of work, and the graph says what each still needs + +The Protocol's planning reads the graph: the build backlog lists ready work no code realizes, the design backlog lists defined work, and the statement queue orders what a person can state. None of them says which capability comes first. A roadmap is that one authored choice, the order of the next few bodies of work. Everything else about an entry, whether it still needs design, waits only for a person's statement, is ready to build, or is built, is read from the graph each time, so a roadmap never goes stale in what it says about state. It goes stale only in its order, which its owner edits. + +An entry stands for its whole refinement subtree: an epic for its features and stories, a feature for its stories. The reading counts the subtree's work by what each part needs, so a large entry shows how much of it is designed and built, and the parts that could be built now. + +## Intent + +- actor: The owner choosing what the next pull requests take up, and an agent opening the next body of work. +- problem: Priority lives in prose, in each pull request's forward section and in the plan files, which the graph cannot read and which go stale as work lands. +- outcome: Read a roadmap as an ordered list of upcoming capabilities, each placed by the graph in what it needs next, to design, to state, to build or built, with the design it lacks and the work it rests on. + +## Behavior + +- rule: [#entries] A roadmap's entries are its Pack's members in authored order, each numbered by its place from 1; an entry may be an epic, a feature or a story, and its place states its owner's preference and nothing else. +- rule: [#scope] An entry's scope is the entry and every Spec that reaches it through one or more `refines` edges, each counted once whichever parent it reaches through; its work is the Specs of its scope whose kind is neither `example` nor `decision`, the build backlog's exclusions. +- rule: [#specNeed] Each Spec in a scope has a need read from the graph: to design when it states a rung below `ready` and its floor does not reach `ready`; to state when it states a rung below `ready` and its floor reaches `ready`, so only a person's statement stands before it; to build when it states `ready`, is neither an example nor a decision, and is not `implemented`; built when it states `ready` and is `implemented`; settled when it is an example or a decision that states `ready`. +- rule: [#drift] A Spec of an entry's work that is `implemented` while it states below `ready` keeps its need and counts as drift, as the drift alarm reads it. +- rule: [#entryNeed] An entry needs the first of to design, to state and to build that any Spec of its work needs, and is built when every Spec of its work is built; an entry reads built only when every Spec of its work states `ready` and is `implemented`. +- rule: [#apart] An entry that is itself an example or a decision, whose scope holds no work, takes no need: it is listed apart with its place and its own need, and it is read through the entries it verifies or shapes. +- rule: [#entryShows] Each entry shows its stated rung, the rung its floor reaches, its stated next rung with the floor clauses that hold it, its work counted by need with the parts to build now, every blocking open question in its scope, and every decision below `ready` that its scope holds or names by `decidedBy`, with that decision's blocking questions, keyed or not. +- rule: [#workRows] Each entry lists every Spec of its work once, with its need, its stated rung, the rung its floor reaches, its stated next rung with the floor clauses and the dependency targets that hold it, and whether it is drift; the parts with the entry's need are the ones that set it, so a descendant that holds an entry back is named with what holds it, and the entry's own readiness stays labelled apart. +- rule: [#restsOn] An entry rests on each resolving Spec outside its scope that any Spec of its scope names by `dependsOn`, `constrainedBy` or `refines`, while that Spec is to design, to state or to build, so a descendant's second parent outside the entry counts as the entry's own parent does; shaping decisions stay in the decision rows. Each row names the relation, the Specs of the scope that name it, the Spec's need and stated rung, whether it holds a `ready` statement in the scope because it states below `defined`, and the place of the first entry whose scope holds it, or none when it lies outside the roadmap. +- rule: [#restedOnBy] An entry is rested on by each Spec outside its scope, to design, to state or to build, that names a Spec of its scope by `dependsOn`, `constrainedBy` or `decidedBy`, so a reader sees what waits on it. +- rule: [#waiting] Apart from the entries, the reading lists what waits on the person the caller names, the owner by default: each open question whose `decider` names that person, on a Spec of any entry's scope or on a decision a scope names by `decidedBy`; and each such Spec or decision whose floor reaches a rung above the rung it states, a statement only a person makes, which for a decision is its ratification. Each row carries the places of the entries it touches. Questions follow their first place; statements keep their relative order in the statement queue across the whole roadmap, so a Spec others rest on comes first whatever its place, and their places never reorder them. +- rule: [#withoutDecider] The open questions over the same Specs that name no `decider` are listed apart and counted, blocking and not, so a question nobody is named to settle never reads as an empty workload; the reading never infers a decider from a question's prose. +- rule: [#earlier] Each entry lists the waiting statements the statement queue puts before its own: every Spec that the queue says one of the entry's waiting statements rests on, its work's and its shaping decisions' alike, each once, in the queue's order, marked as inside the entry, at the first place of another entry whose scope or shaping decisions it is among, or outside the roadmap. The list advises the order of statements and is never a readiness requirement. +- rule: [#builtStays] A built entry keeps its place and reads built until someone edits the roadmap; nothing removes it, and no check reports it. +- rule: [#overlap] Two entries may share Specs, as an epic and a feature beneath it do; each reads its own scope and names the places of the entries it shares a Spec with. +- rule: [#readingOrder] A Pack whose manifest does not state `order: priority` reads the same way when it is named, and the answer says its order is reading order, so no view presents it as a priority. +- rule: [#statesNothing] The reading states, records and gates nothing: it moves no rung, authors no fact and reports no finding, and work taken up out of its place is never a finding. + +## Design + +- roadmapRecipe: recipe 53, the roadmap, takes `params.pack`, one Pack id, and `params.decider`, a token on the `decider` field's grammar, `"owner"` by default; without `pack` it reads every Pack whose manifest states `order: priority`, in id order; a `pack` that names no Pack, or a `decider` off the grammar, returns `{ diagnostics: [{ parameter, problem }] }` and no roadmaps. +- roadmapAnswer: `{ decider, roadmaps }`, one roadmap per Pack read: `{ pack, title, framing, order, totals, entries, apart, unresolved, waiting }`, where `order` is `"priority"` or `"reading"` and `totals` is `{ entries, toDesign, toState, toBuild, built, apart, unresolved, questions, blockingQuestions, withoutDecider, blockingWithoutDecider, statements }`, the entries with each need and the rows waiting. `entries` in `totals` counts every manifest member, including unresolved ones, and `totals.unresolved` counts the unresolved members; `unresolved` lists `{ place, id }` in manifest order for members that do not resolve, and every place is the original manifest position, never renumbered. +- needValues: an entry's `need` is `"to design"`, `"to state"`, `"to build"` or `"built"`; a Spec's need is one of those or `"settled"`. +- entryRow: `{ place, id, title, specKind, altitude, need, statedReadiness, floorReached, statedNextRung, holdsNextRung, work, workRows, buildNow, blockingQuestions, decisions, restsOn, restedOnBy, earlierStatements, packs, sharesScopeWith }` in place order, where `statedNextRung` is null at `ready`, `holdsNextRung` lists the holding rows that keep the floor from the stated next rung, `work` is `{ specs, toDesign, toState, toBuild, built, drift }` counted over the entry's work, `buildNow` lists the ids of its work to build, `packs` lists the entry's Packs other than the roadmap, and `sharesScopeWith` lists places. +- workRow: `workRows` lists every Spec counted in `work` once, in Spec-id order, as `{ id, need, statedReadiness, floorReached, statedNextRung, holdsNextRung, drift }`, with `statedNextRung` and `holdsNextRung` read as on the entry row and `drift` a boolean; the rows whose `need` equals the entry's are the ones that set it. +- holdsRow: a holding row is `{ clause, targets }`, `clause` the id of a floor clause that fails at the stated next rung, in the floor evaluator's order, and `targets` the ids that the typed-dependency clause names, in its order, empty for any other clause. +- questionRow: `{ address, spec, index, question, blocking, decider, provisional }` for each blocking question in the entry's scope, `address` null when the question carries no key, `index` its authored position from 0, and `decider` and `provisional` null when absent, sorted by Spec id and authored position. +- decisionRow: `{ id, title, statedReadiness, blockingQuestions }` for each decision below `ready` the entry's scope holds or names by `decidedBy`, sorted by id, `blockingQuestions` listing a question row for each of its blocking questions, keyed or not. +- restsOnRow: `{ id, via, from, need, statedReadiness, holdsReady, onRoadmap }`, where `via` is `"dependsOn"`, `"constrainedBy"` or `"refines"`, `from` lists in id order the Specs of the scope that name it by that relation, `holdsReady` is true when the Spec states below `defined`, and `onRoadmap` is the place of the first entry whose scope holds it or null; rows sort by id, then `via`. +- restedOnByRow: `{ id, via, need, onRoadmap }`, where `via` is the relation it names the scope by; rows sort by id, then `via`. +- earlierRow: `earlierStatements` lists `{ id, where, onRoadmap }` in the statement queue's order, where `where` is `"entry"` for a Spec of the entry's scope or one of its shaping decisions, `"roadmap"` for a waiting statement another entry touches through its scope or its shaping decisions, and `"outside"` otherwise, and `onRoadmap` is the first place of the entries that statement touches, null unless `where` is `"roadmap"`. +- apartRow: `{ place, id, specKind, statedReadiness, need }` in place order. +- waitingRows: `waiting` is `{ questions, withoutDecider, statements }`: a question in either list is a question row with `places`; a statement is `{ id, statedReadiness, floorReached, places }`. Questions sort by their first place, then blocking ones first, then by Spec id and authored position. Statements keep their relative order in the statement queue, recipe 46, across the whole roadmap; their `places` name the entries they touch and never reorder them. + +## Example space + +```gwt-vocabulary +Given a probe roadmap whose entry holds the work {work:string} +Given that work {state:"is ready and implemented throughout"|"has one part ready and not implemented"|"has one part below ready whose floor reaches ready"|"has one part below ready whose floor stops short"|"has one part implemented below ready"|"has a ready root and one descendant without its kind's evidence"|"has a ready root and one descendant held by a blocking question"|"has one descendant that also refines a Spec outside the roadmap"} +Given a probe roadmap whose first entry rests on a Spec of its second entry, both waiting for a statement +When the roadmap recipe reads the probe roadmap +Then the entry's need reads {need:"to design"|"to state"|"to build"|"built"} +Then the entry counts {drift:number} parts as drift +Then the entry names {part:string} among the parts with its need, held by {clause:string} +Then the entry rests on {target:string} through {origin:string} +Then the waiting statements list {earlier:string} before {later:string} +``` diff --git a/specs/consumers/roadmap.statement-order.sdp.md b/specs/consumers/roadmap.statement-order.sdp.md new file mode 100644 index 000000000..7d771d8fe --- /dev/null +++ b/specs/consumers/roadmap.statement-order.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:consumers.roadmap.statement-order +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.roadmap + verifies: + target: spec:consumers.roadmap + exercises: [waiting, earlier] +--- +# Waiting statements keep dependency order against roadmap places + +## Intent +- outcome: Execute one roadmap reading over a probe corpus. +- assumption: The world writes two defined behavior Specs with complete evidence and resolving relations. The first roadmap entry depends on the second; both floors reach ready. The roadmap reads the shipped body over the extracted probe corpus. + +```gwt +Given a probe roadmap whose entry holds the work {work: "spec:probe.dependent"} +Given that work {state: "has one part below ready whose floor reaches ready"} +Given a probe roadmap whose first entry rests on a Spec of its second entry, both waiting for a statement +When the roadmap recipe reads the probe roadmap +Then the waiting statements list {earlier: "spec:probe.base"} before {later: "spec:probe.dependent"} +``` diff --git a/specs/consumers/roadmap.statement-part.sdp.md b/specs/consumers/roadmap.statement-part.sdp.md new file mode 100644 index 000000000..8816cd8f1 --- /dev/null +++ b/specs/consumers/roadmap.statement-part.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:consumers.roadmap.statement-part +kind: example +altitude: story +readiness: ready +relations: + refines: spec:consumers.roadmap + verifies: + target: spec:consumers.roadmap + exercises: [scope, specNeed, entryNeed, workRows] +--- +# A complete part waits for a statement + +## Intent +- outcome: Execute one roadmap reading over a probe corpus. +- assumption: The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The descendant states defined with complete behavior evidence and a ready parent. The roadmap reads the shipped body over the extracted probe corpus. + +```gwt +Given a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"} +Given that work {state: "has one part below ready whose floor reaches ready"} +When the roadmap recipe reads the probe roadmap +Then the entry's need reads {need: "to state"} +``` diff --git a/specs/consumers/spec-studio.capability-map-page.sdp.md b/specs/consumers/spec-studio.capability-map-page.sdp.md new file mode 100644 index 000000000..35b068be1 --- /dev/null +++ b/specs/consumers/spec-studio.capability-map-page.sdp.md @@ -0,0 +1,54 @@ +--- +id: spec:consumers.spec-studio.capability-map-page +kind: behavior +altitude: story +readiness: ready +relations: + refines: + - target: spec:consumers.spec-studio.lenses + reason: the Capabilities page is a lens that reads refinement + - target: spec:consumers.capability-map + reason: the page is the capability map's reading for a person + dependsOn: + - target: spec:consumers.spec-studio.index-page + reason: the Epic filter extends the index's Specs filters + - target: spec:consumers.binding-language-views + reason: aggregate rows use the same binding labels as Spec pages + - target: spec:consumers.capability-map + reason: the page and index filter render the capability map's answer + - target: spec:consumers.spec-studio.diagram-kinds + reason: the refinement drawing shares the Studio's diagram layout and accessible text contract +--- +# The capability map page makes refinement readable by epic + +## Intent + +- actor: A reviewer looking for a capability before choosing a Spec or Pack. +- problem: A flat altitude filter hides how a capability is decomposed, and a large refinement drawing becomes unreadable when shrunk to the page width. +- outcome: Read an epic overview, inspect its refinement structure and evidence, and narrow the index's Specs to its descendants. + +## Behavior + +- rule: [#page] The Studio publishes `capabilities.html` on every build and links it as `Capabilities` immediately after `Packs` in the top navigation. The Packs lens keeps its membership order and the index remains the landing page. +- rule: [#recipeData] The page reads recipe 52 with `{ detail: true }` and no epic parameter. It renders every epic's own evidence beside its descendant rollup and names the rollup's scope. Examples and decisions have separate counts and expandable rows. There is no combined total obtained by adding epic rows. +- rule: [#drawing] Each epic has a refinement drawing supplied by the shared diagram implementation. It keeps parent-to-child reading order while arrows, if drawn, point child to parent and the legend says `refines`. Repeated nodes link to their first occurrence within that epic section. A nested epic remains a labelled epic, not a new altitude. +- rule: [#bounded] A large branch folds at the shared diagram bound and links to its complete refinement text. At desktop width it never scales labels below the shared legibility size to fit an entire tree; below the responsive breakpoint it follows the shared legibility rule's narrow fit. The text includes every node and edge, repeats and cyclic links labelled in words, and stays readable without script. +- rule: [#links] Every node title links to that Spec's Studio page. A separate `Show these Specs` link goes to the index's Specs section with the epic selected. Keyed questions link to their entry on the Spec page; unkeyed questions link to the Spec page's `section.intent`. Unresolved endpoints are text with their diagnostics, never plausible Spec links. +- rule: [#filter] The index's Specs filter gains `Epic`, with `Any`, `No epic` and every epic by title and id. An epic selects its own Spec and every Spec whose full-corpus epicIds contains it. It intersects with the existing filters, resets with them, and never changes Pack membership or hides Pack cards. +- rule: [#withoutScript] Native links, disclosures, complete refinement text and all Spec rows work without script. Index filtering is an enhancement: before script applies a requested epic, the index says filtering needs script and shows all rows. A supplied id that is absent or not epic reports `Unknown epic filter` and leaves all rows visible. +- rule: [#emptyAndDamage] The page always has `No epic reaches these Specs`, including an explicit empty message. With no epics it explains that authors may organize Specs through refinement and still lists the whole corpus. Cycles and unresolved edges are visible diagnostics beside the complete text, without a new validation rule. + +## Design + +- route: `capabilities.html`; an epic's section id is its Spec id. The filtered-index route is `index.html?epic=<percent-encoded Spec id>#specs`; `epic=none` selects unreached Specs, absence selects Any. An unknown or repeated epic parameter reports the unknown-filter message without applying it. +- diagramInput: recipe 52's node, edge, epic and unreached rows supply the drawing and its text; layout derives no reach, floor or delivery fact. The shared drawing implementation owns folding, label metrics and SVG or Mermaid representation. A capability drawing must support same-altitude edges, skipped altitudes, multiple parents, nested epics, and cycles without pretending to be a strict three-level tree. +- disclosureOrder: epic overview cards and sections follow recipe order. Each section opens with the epic's own rung, floor, binding evidence and questions, then the rollup labelled `Distinct descendants, excluding this epic`. Refinement text expands a depth-first walk with children in id order and renders later visits as references; it follows the recipe's root order and uses per-section visited ids. Examples and decisions fold through native disclosures without pruning their descendants. +- rowEvidence: each row names kind and altitude, `stated <rung>`, `floor <rung>`, the implementation binding and the verifier binding as `present` or `none`, and open and blocking question counts with their text available. Facts that are absent stay absent; a folded branch's counts never become the parent's badges. +- nestedEpics: a card for an epic that refines another epic says `Inside <title>`, using `parentEpicIds`, and a card whose descendants include an epic names it, so overlapping counts read as overlap. +- responsive: overview cards reflow to one column on a narrow viewport; refinement text wraps ids and titles, caps visual indentation, and names the parent in words so deep trees remain readable. At desktop width diagram labels use the shared readable size and bounded branches. Below the responsive breakpoint the drawing follows the legibility rule's narrow fit; the complete refinement text is the readable form. Print opens the disclosures and retains the complete text. + +## Verification — reviewed + +- Read the same page on a corpus with one broad epic, several epics, and no epics, with script disabled and at a narrow viewport. +- Follow a shared descendant from both parents and verify that the rollup counts it once and the index filter lists it once. +- Select an epic on the index, combine it with kind and readiness filters, reset it, and open the filtered URL directly; invalid and repeated epic parameters never show a false empty result. diff --git a/specs/consumers/spec-studio.cited-line-text.sdp.md b/specs/consumers/spec-studio.cited-line-text.sdp.md new file mode 100644 index 000000000..008796964 --- /dev/null +++ b/specs/consumers/spec-studio.cited-line-text.sdp.md @@ -0,0 +1,37 @@ +--- +id: spec:consumers.spec-studio.cited-line-text +kind: behavior +altitude: story +readiness: scoped +relations: + refines: spec:consumers.spec-studio + dependsOn: + - target: spec:extraction.entry-citations + reason: the words belong to a citation the graph already records by file and line + - target: spec:consumers.spec-studio.registers + reason: the declarations page lists each citing line beside its declaration +--- +# A pinned declaration shows the words of each test line that cites it + +A reader checking a pinned declaration reaches the test line that cites it and then follows a source link to read the assertion. The words of that line would close the task on the page. The graph records where a line cites an entry and never what the line says, and the Studio reads only the graph and its named inputs, so one of them has to hold the words. The extractor already reads the line when it records the citation, which makes the citation row the cheapest honest home; the choice is the owner's, because it puts source text in the graph for the first time. + +## Intent + +- actor: A reviewer with only a link, checking that a pinned declaration is held to its code. +- problem: The declarations page names each citing line by file and line only, so the reader leaves the page to read it, and has no route at all when the page set carries no source link. +- outcome: Show beside each pinned declaration the text of every test line that cites it, so a reviewer reads the check without leaving the page. + +### Open questions + +- [blocking #lineText] May the Studio show source text the graph does not hold, and if so is it recorded in the graph beside the citation or read as a named input? + - decider: owner + - provisional: the extractor records the citing line's text, trimmed and bounded, on the citation row it already writes, as inferred data that confers nothing, and the Studio reads no source +- [non-blocking #lineBound] How long may a recorded line be, and how does a page show a line that was cut? + - decider: delegated + - provisional: a bound of a few hundred code units, cut at a word boundary and marked as cut, the whole line one source link away + +## Behavior + +- rule: [#lineShown] Beside each citing line, the declarations page and the Spec page show the line's words as code after its file and line, and the reader's citation row carries them. +- rule: [#wordsNotVerdict] The words are what the citing line said at the commit the graph was derived from; no page says the line asserts, checks or passes anything because of them. +- rule: [#withoutWords] A citation whose words are not recorded shows its file and line as it does now. diff --git a/specs/consumers/spec-studio.components.sdp.md b/specs/consumers/spec-studio.components.sdp.md index aab393c8e..4a02a0a69 100644 --- a/specs/consumers/spec-studio.components.sdp.md +++ b/specs/consumers/spec-studio.components.sdp.md @@ -33,7 +33,7 @@ The original's harness element has no subject in the graph, which holds example The elements in the original's order, with their attributes. - specCard: `sdp-spec-card` with `spec-id` and `variant`: one Spec as a card; `full` is a Pack page's member card, `short` one line with the title linked, the id, the rung badge and the floor in words; no behavior. - traceGraph: `sdp-trace-graph` with `spec-id`: the Spec's relations one hop each way, drawn in relation bands inside an `sdp-diagram`, then its text equivalent; no behavior of its own. -- maturityMap: `sdp-maturity-map` with `pack-id`: the Pack's members placed by stated rung and floor reached, as a table of short cards with its axis explanation in the caption; an empty cell shows `—` alone and a filled cell names its count and Specs; no behavior. +- maturityMap: `sdp-maturity-map` with `pack-id`: the Pack's members placed by stated rung and floor reached, drawn as a static SVG grid of counts, then its count table and each populated cell's member list; no behavior. - exampleSpace: `sdp-example-space` with `spec-id`, naming the Spec that owns the example space: the space's slots with their types and the step that declares each, its children's bound points as a table with one row per example and one column per slot, an unbound slot written `unbound`, its expected-outcome oracle binding as present or none, and recipe 44's coverage; its coverage is a report, never a verifier run or a delivery fact; no behavior beyond the composing actions inside it. - diagramView: `sdp-diagram` with `subject`, the element `spec:consumers.spec-studio.diagrams#design.diagramElement` states. - scenarioEditor: `sdp-scenario-editor` with `spec-id` and `mode`: in `edit` mode a form holding the example's given, when and then steps, in `propose` mode the parent's step vocabulary as a starting point; submitting gathers an intent scoped to the example or to the parent, with the steps as its exact text; without script it shows the steps as text and says that composing needs the script. diff --git a/specs/consumers/spec-studio.data.recipe-answers.sdp.md b/specs/consumers/spec-studio.data.recipe-answers.sdp.md index 6294dc978..dda0bcf40 100644 --- a/specs/consumers/spec-studio.data.recipe-answers.sdp.md +++ b/specs/consumers/spec-studio.data.recipe-answers.sdp.md @@ -11,7 +11,7 @@ relations: --- # A page takes each recipe answer from the recipe's shipped body -The lenses, the registers, the owner queue, the diff and runs pages and the panels of a Spec page show recipe answers. Each takes its answer from the recipe's shipped body, evaluated over the graph the build derived, so a page and `sdp q` give one answer. The two Design entries name every evaluation a build runs and which page reads each answer, so a builder and a test author read one list. +The lenses, the registers, the owner queue, the slice and roadmap pages, the diff and runs pages and the panels of a Spec page show recipe answers. Each takes its answer from the recipe's shipped body, evaluated over the graph the build derived, so a page and `sdp q` give one answer. The two Design entries name every evaluation a build runs and which page reads each answer, so a builder and a test author read one list. ## Intent - outcome: Compose every recipe answer a Studio page shows by evaluating the recipe's shipped body over the graph the build derived, so a page and `sdp q` give one answer. @@ -21,5 +21,5 @@ The lenses, the registers, the owner queue, the diff and runs pages and the pane - [#oncePerBuild] Each evaluation the recipe evaluations name runs once per build, and every page that shows its answer reads that one answer. ## Design -- recipeEvaluations: a build evaluates each of these once, with no parameter unless one is named: recipes 17, 20, 24, 31, 33, 43, 44, 46 and 51; recipe 29 with `{ pack: id }` for every Pack; recipe 21 with `{ spec: id }` and recipe 30 with `{ specs: [id] }` for every Spec; recipe 34 with `{ spec: id }` for every Spec that is not an example, Specs in no Pack included; given a base graph, recipe 35 with `{ base, detail: true }`, and with `{ base, pack: id, detail: true }` for every Pack; given run records, recipe 45 with `{ runs }`. -- pageRecipes: the index and the glossary render reader values only; the architecture lens renders recipes 17 and 43; the tests lens renders every recipe 34 answer, recipe 44, and recipe 45 given run records; a Spec page renders its recipe 21, 30 and 34 answers, its own space from recipe 44, the recipe 51 rows that cite its entries, the recipe 45 files whose `specs` hold its id, and its own row of recipe 35's whole-corpus answer; a Pack page renders the totals of its recipe 29 answer and, given a base graph, its own recipe 35 answer; the registers render recipes 20, 31, 24 beside recipe 51's rows, and 33; the owner queue renders recipe 46; the diff page renders recipe 35's whole-corpus answer; the runs page renders recipe 45; an export renders the values of the pages it holds. +- recipeEvaluations: a build evaluates each of these once, with no parameter unless one is named: recipes 17, 20, 24, 31, 33, 43, 44, 46, 51 and 53; recipe 52 with `{ detail: true }`; recipe 29 with `{ pack: id }` and recipe 54 with `{ pack: id, detail: true }` for every Pack; recipe 55 with `{ pack: id }` for every Pack whose manifest states `order: priority`; recipe 21 with `{ spec: id }` and recipe 30 with `{ specs: [id] }` for every Spec; recipe 34 with `{ spec: id }` for every Spec that is not an example, Specs in no Pack included; given a base graph, recipe 35 with `{ base, detail: true }`, and with `{ base, pack: id, detail: true }` for every Pack; given run records, recipe 45 with `{ runs }`. +- pageRecipes: the index renders reader values and each Spec's `epicIds` from recipe 52; the glossary renders reader values only; the capabilities lens renders recipe 52; the architecture lens renders recipes 17 and 43; the tests lens renders every recipe 34 answer, recipe 44, and recipe 45 given run records; a Spec page renders its recipe 21, 30 and 34 answers, its own space from recipe 44, the recipe 51 rows that cite its entries, the recipe 45 files whose `specs` hold its id, and its own row of recipe 35's whole-corpus answer; a Pack page renders the totals of its recipe 29 answer and, given a base graph, its own recipe 35 answer; a slice page renders its Pack's recipe 54 answer; the roadmap page renders recipe 53 and each roadmap's recipe 55 memo; the registers render recipes 20, 31, 24 beside recipe 51's rows, and 33; the owner queue renders recipe 46; the diff page renders recipe 35's whole-corpus answer; the runs page renders recipe 45; an export renders the values of the pages it holds. diff --git a/specs/consumers/spec-studio.data.sdp.md b/specs/consumers/spec-studio.data.sdp.md index fad62f2fb..dd70f263f 100644 --- a/specs/consumers/spec-studio.data.sdp.md +++ b/specs/consumers/spec-studio.data.sdp.md @@ -21,10 +21,10 @@ The build writes each value into the page's markup, so a page reads in full as H - outcome: Render every Studio page from the values the reader returns for the graph the agent surface reads, so a page derives nothing of its own and carries only the data its script needs. ## Contract -- [#readerValues] The build renders the index from `specs()`, `packs()`, `findings()` and each Spec's design columns, a Pack page from `packContext` and the Pack totals, a Spec page from `specContext`, and the search page from the search index; every page's rail renders the rail values. A page states no value the reader did not return or a named recipe did not answer, apart from counts the build takes of those values. +- [#readerValues] The build renders the index from `specs()`, `packs()`, `findings()`, each Spec's design columns and the epic memberships recipe 52 answers, a Pack page from `packContext` and the Pack totals, a Spec page from `specContext`, and the search page from the search index; every page's rail renders the rail values. A page states no value the reader did not return or a named recipe did not answer, apart from counts the build takes of those values. - [#embeddedData] A page whose script reads data embeds it in one `script` element of type `application/json` with the id `sdp-data`, serialized as JSON with every `<` then written as the six characters `\u003c`, so no text can close the element. The search page embeds the search index there; every other page embeds no data, because its values are in its markup. - [#noDataFiles] The page set writes no data file of its own: an agent reads the graph through `sdp q`, and `sdp build` writes the serialized graph. -- [#noSlices] The Studio's data holds no context bundle or other AI slice: the context bundle is composed on each invocation and never saved, and a design pass composes one with its recipe when it needs it. +- [#noSlices] The Studio's data holds no context bundle or other AI slice: the context bundle is composed on each invocation and never saved, and a design pass composes one with its recipe when it needs it. A slice page renders the design slice's answer into its markup, as every page renders its values, and embeds no slice data. ## Design - packTotals: `{ members: number; unresolved: number; heldBelowNextRung: number; waitingForAuthor: number; entries: number; declarations: number; openQuestions: number; blockingQuestions: number; withImplementations: number; withReferences: number; restsOn: number; restedOnBy: number; verifiedBy: number; supersededBy: number }`, the totals recipe 29 returns for the Pack, each counted as that recipe counts it. diff --git a/specs/consumers/spec-studio.diagram-kinds.capability-tree.sdp.md b/specs/consumers/spec-studio.diagram-kinds.capability-tree.sdp.md new file mode 100644 index 000000000..3c37d0c07 --- /dev/null +++ b/specs/consumers/spec-studio.diagram-kinds.capability-tree.sdp.md @@ -0,0 +1,25 @@ +--- +id: spec:consumers.spec-studio.diagram-kinds.capability-tree +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.spec-studio.diagram-kinds + dependsOn: spec:consumers.capability-map +--- +# A refinement drawing follows the capability map + +## Intent +- outcome: Read the capability map as a hierarchy without inventing parentage from names or hiding multiple parents. + +## Behavior +- rule: [#mapInput] Draw the roots, refinement edges, unreached Specs and repeated reach supplied by the capability map. Root selection and reach belong to that reader, never to label prefixes, files or the drawing. +- rule: [#multipleParents] Place a Spec at its first visit in the capability map page's traversal of the epic and draw each later visit as a repeat box that links to the first occurrence and keeps its own refines arrow. A cycle is a labelled group whose internal relations stay in the text equivalent; the drawing never cuts an edge silently. +- rule: [#fold] Above the shared bound, retain each root and fold the largest descendant branch below a non-root parent first, ties by root order then id, linking to its full branch list. If siblings still exceed the bound, keep their first entries in supplied order and replace the remaining siblings of one parent together with their descendant boxes with a labelled folded remainder, keeping that parent's relation explicit and counting every hidden unique Spec. Count unique Specs and rendered boxes separately where a cycle or repeated reach needs an occurrence. If no lawful fold fits, omit the SVG and open the text equivalent. + +## Design +- layout: Roots lead, then depth bands of children; label each box with its title, altitude and shortened id. A refines arrow still points child to parent. Unreached Specs occupy a labelled band apart. Within a depth band, children sit together under their placement parent as one labelled sibling group, in parent order; a band never interleaves siblings of different parents. +- wordsBesideStyle: The legend says `Arrows: refines (declared). Later visits repeat a Spec and link to its first occurrence.` A group says `cycle` or `folded` in words and names its unique member count. +- textEquivalent: List every root and reachable Spec, all declared refines edges, cycle membership and unreached ids using full identities. Grouping confers no delivery fact or readiness. +- elementIds: Use `diagram.<subject>.capability.text`, `.capability.arrow` and `.capability.branch.<encoded-root-id>`; encoded identities are injective and stable, not truncated labels. +- mermaidKind: `mindmap` for a strict tree, with title and full id per node. Use `flowchart BT` with exact refines edges whenever there is repeated parentage, a cycle, an unreached band or a fold that mindmap cannot state losslessly. TreeView is an optional equivalent export of a strict tree with icons disabled. diff --git a/specs/consumers/spec-studio.diagram-kinds.dependency.sdp.md b/specs/consumers/spec-studio.diagram-kinds.dependency.sdp.md new file mode 100644 index 000000000..507f5b34d --- /dev/null +++ b/specs/consumers/spec-studio.diagram-kinds.dependency.sdp.md @@ -0,0 +1,28 @@ +--- +id: spec:consumers.spec-studio.diagram-kinds.dependency +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.spec-studio.diagram-kinds + dependsOn: spec:consumers.design-slice +--- +# A dependency drawing shows the declared footing of a Spec set + +## Intent +- outcome: See multi-hop dependency chains and cycles within a Pack, slice or roadmap without reading them as an execution schedule. + +## Behavior +- rule: [#declaredOnly] Draw the design slice answer's structure edges and rests-on boundary, preserving each declared refines, dependsOn, constrainedBy and decidedBy relation's exact direction, reason and claim. The design slice owns its rests-on exception for a refines back to a member that names it by another typed relation: such an edge carries `orders: false`, is drawn dashed and says in its legend and text that it orders nothing. Membership is scope, never a dependency. +- rule: [#depth] Band each member by the depth its supplied answer gives, the design slice's `depth` for a Pack, slice or roadmap scope; the layout never recomputes it. A cycle's members share one band and are named as a cycle in words. +- rule: [#boundary] Show immediate dependencies outside the supplied scope in a separate boundary band, without traversing their dependencies. An unresolved target is named unresolved and has no invented title or link. +- rule: [#fold] The boundary band always folds into one box per relation type that names its count, keeping as its own box each outside Spec that holds a member's stated next rung or states below defined, and every folded Spec stays a row of the text equivalent; above the shared bound, whole depth bands fold in descending box count, ties by ascending depth; if the drawing still exceeds the bound, the SVG is omitted and the text equivalent opens. +- rule: [#examplesInParent] An example member the design slice folds into its parent is counted on that parent's box and is never drawn as its own box; it keeps its row in the text equivalent. An example the slice does not fold is a box like any other member's. + +## Design +- inputs: `subject` and the design slice answer supply ordered members, exampleOf, exampleMembers, depth, cycles, structure edges and the rests-on boundary with its holds and relation reasons; a roadmap passes its explicitly selected ordered Spec set to that same reader. Duplicate or unknown scope ids are that reader's diagnostics, never repaired by layout. +- bands: Label bands `Depth <n>` from the deepest at the top down to depth 1, then `Outside this scope` at the bottom, so a member sits above what it rests on and boundary edges stay short. Within a band keep supplied order. Depths are structural, never phases, dates or priority. +- edgeWords: Show refines, dependsOn, constrainedBy and decidedBy beside their arrow or as a keyed legend where several kinds occur, and in the text equivalent with claim and optional reason. Mixed relations cannot become an unlabeled dependency arrow. Arrows with the same ends, claim and ordering may combine into one arrow naming every relation; the text equivalent retains each original relation and reason. +- textEquivalent: List every scoped and boundary Spec with its full id and title, then every original edge and every cycle's members; folding removes no text row. +- elementIds: Use `diagram.<subject>.dependencies.text`, `.dependencies.arrow` and `.dependencies.fold.<depth>`; a second scope on one page uses a distinct subject occurrence key supplied by the page. +- mermaidKind: `flowchart TB` with depth subgraphs and exact relation labels; export the displayed folds and name the full text equivalent beside the source. diff --git a/specs/consumers/spec-studio.diagram-kinds.edge-routing.sdp.md b/specs/consumers/spec-studio.diagram-kinds.edge-routing.sdp.md new file mode 100644 index 000000000..7ad08acd4 --- /dev/null +++ b/specs/consumers/spec-studio.diagram-kinds.edge-routing.sdp.md @@ -0,0 +1,30 @@ +--- +id: spec:consumers.spec-studio.diagram-kinds.edge-routing +kind: contract +altitude: story +readiness: ready +relations: + refines: + - spec:consumers.spec-studio.diagrams + - target: spec:consumers.spec-studio.diagram-kinds + reason: arrows that cover no label serve the readable drawings the diagram kinds state +--- +# Edges route around boxes with deterministic orthogonal paths + +## Intent +- outcome: Follow an arrow without it crossing a box or covering a label, using the engine's own static layout. + +## Contract +- [#obstacles] After box placement and label wrapping, reserve each box and band label rectangle as an obstacle with clearance. An edge may touch only its own source and destination ports; no other segment may intersect a reserved rectangle's interior. +- [#routes] Route edges deterministically in code-unit order by from, to, kind and claim with orthogonal whole-pixel paths around the reserved obstacles. Add no layout dependency and read no browser measurement. The build owns the search representation, weights and tie-break implementation. +- [#ports] Choose source and destination sides deterministically for forward, same-band and backward edges. Every incident edge has its own port. A cycle and a backward edge remain directed and route around intervening boxes. +- [#labels] Reserve an edge label's rectangle beside a free segment. If the exact relation and claim words do not fit, use a stable edge key and name its full wording in the adjacent legend and text equivalent. Every placed label rectangle is an obstacle for later labels and routes; no two labels overlap. An edge carries a label only where spec:consumers.spec-studio.diagrams#rule.wordsBesideStyle asks for one; a drawing whose edges share one relation and one claim names them once in its legend. +- [#noPath] If any required route, distinct lane or label cannot be placed within the deterministic routing budget, omit that SVG and open its text equivalent with an explicit routing explanation. Never draw a straight line through a box as fallback. + +## Design +- clearance: `12` logical pixels around boxes and text, with ports connected through their own clearance by a stub. Lane spacing is `8` pixels and gutter margin is `24` pixels; all coordinates and costs are integers. +- cost: Crossings remain permitted at free points, never through nodes or labels. Two edges never share a collinear segment: they take distinct ports and lanes `8` pixels apart, and an edge that cannot get a lane is a routing failure under the no-path rule. +- searchBound: The build chooses a finite deterministic search budget and tests its routing-failure explanation; tuning the search changes no graph meaning. The shared box and edge bound applies first. +- overflow: Routing gutters may increase SVG width beyond the box layout budget to preserve distinct lanes; the desktop scroll instruction follows that final width. No label is shrunk to fit. +- routeModel: Each route retains from, to, kind, claim, optional aggregation count and relation reason, plus its integer point list; ports and routes are presentation values, never graph edges. +- elementIds: Preserve `.edge` data-from, data-to and data-kind, add data-claim when present and a stable data-edge-key for legend lookup; arrow marker ids remain scoped to subject and view. diff --git a/specs/consumers/spec-studio.diagram-kinds.legibility.sdp.md b/specs/consumers/spec-studio.diagram-kinds.legibility.sdp.md new file mode 100644 index 000000000..67672173d --- /dev/null +++ b/specs/consumers/spec-studio.diagram-kinds.legibility.sdp.md @@ -0,0 +1,28 @@ +--- +id: spec:consumers.spec-studio.diagram-kinds.legibility +kind: contract +altitude: story +readiness: ready +relations: + refines: + - spec:consumers.spec-studio.diagrams + - target: spec:consumers.spec-studio.diagram-kinds + reason: readable labels are part of the outcome the diagram kinds state + dependsOn: spec:consumers.spec-studio.responsive +--- +# A diagram preserves readable labels before zoom + +## Intent +- outcome: Read a drawing's labels on first view while allowing a wide graph to scroll inside its figure. + +## Contract +- [#firstView] At 720 CSS pixels of viewport width and wider, a diagram's SVG starts at its natural size inside its figure's scroll region and is never shrunk to fit. Overflow scrolls inside the figure, never the page. The build writes `Scroll horizontally to read the whole drawing.` above every drawing wider than the width budget. Below that viewport width the responsive contract's narrow drawing applies. +- [#titleFirst] A box starts with its title or recorded label at fourteen pixels, then a shortened id at twelve pixels, with full identity in its link title and text equivalent. Shortening changes no identity, target or element id; collisions extend the displayed suffix until it is unique within the figure. The box's remaining words, such as a component's member count, follow the id. + +## Design +- widthBudget: `880` logical pixels for a band or grid, band labels included, which stay above their band; `264` per context cell when several contexts share a grid, the full band budget when only one context is shown, and `24` between boxes. A drawing wider than `880` scrolls inside its figure. Layout reads no browser or font measurement. +- labelMetrics: Title lines hold at most `28` characters at fourteen pixels with an eight-pixel character advance; ids hold at most `30` characters at twelve pixels with an eight-pixel advance. Wrap at spaces or id punctuation, then by code point, with whole-pixel coordinates and height calculated from all lines. +- minimumScale: `1` on first desktop view at viewport widths of at least `720` CSS pixels; below that width the responsive contract fits the same drawing to the screen. Zooming out on desktop remains an explicit reader choice. +- shortId: Drop the namespace and the leading dotted segments every id in the figure shares, and mark the elision with a leading `…`, so a label may show `…markdown-authoring`; keep more segments on collisions. A displayed id never carries a namespace unless it is shown in full. Full ids remain in the link title, the text equivalent and the source. +- viewport: `.diagram-scroll` is a keyboard-focusable named region with overflow auto and max-width one hundred percent. At desktop widths its SVG has natural width and height with no max-width fit; below `720` CSS pixels the SVG fits the region. The caption and legend stay outside. The build emits the scroll instruction only when SVG width exceeds the fixed width budget; CSS leaves the region discoverable on narrower columns. +- elementIds: Keep each kind's text and arrow ids; the viewport uses `diagram.<subject>[.<view>].viewport` and its visible instruction uses `.scroll-help`, inserting the view name exactly where the diagram's own ids do. diff --git a/specs/consumers/spec-studio.diagram-kinds.maturity-picture.sdp.md b/specs/consumers/spec-studio.diagram-kinds.maturity-picture.sdp.md new file mode 100644 index 000000000..a30b9531c --- /dev/null +++ b/specs/consumers/spec-studio.diagram-kinds.maturity-picture.sdp.md @@ -0,0 +1,26 @@ +--- +id: spec:consumers.spec-studio.diagram-kinds.maturity-picture +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.spec-studio.diagram-kinds +--- +# The maturity map is drawn as a grid of counts + +## Intent +- outcome: See whether a Pack's statements match, lag or exceed its floors without a wall of member cards inside the picture. + +## Behavior +- rule: [#discreteCells] Draw one equal-sized cell per stated-rung and floor pair. A populated cell shows its exact count and one mark per member, up to `100` marks, beyond which it is filled solid, and links to that cell's full ordered member list; an empty cell is a faint outline with no number. `matches`, `floor above statement` and `statement above floor` are named once each in the legend, and the diagonal carries an outline. Floor none is distinct from idea, and unresolved members are listed apart. +- rule: [#noScore] Use categorical cell position, never normalized coordinates, a quality score, area proportional to maturity, completion percentage or a recommendation to implement. Marks count members and score nothing. +- rule: [#fullMembership] Every resolved member appears in exactly one cell list in authored order. The picture holds cell counts, not per-member cards. Without script every populated cell links to its already-built list and every member links to its card. +- rule: [#oneCell] When every resolved member falls in one cell, one sentence before the grid names that cell's stated rung and floor with its count and links to the cell's member list, and the grid sits in a closed disclosure; when two or more cells hold members, the grid shows with no disclosure. The sentence scores nothing, and print opens the disclosure. + +## Design +- matrix: Rows run stated idea, scoped, defined, ready from top to bottom; columns run floor none, idea, scoped, defined, ready from left to right. The grid including axes fits the shared width budget. Empty cells show only a faint outline; the matching diagonal has a stronger outline. Relation phrases appear once in the legend. +- bound: The fixed categorical cells fit the shared box bound and have no edges. Membership volume does not grow the picture; complete lists follow in closed disclosures and print open. Overlong lists are never truncated. +- oneCellDisclosure: the disclosure that holds a one-cell grid reads `Stated readiness by floor reached` as its summary; the grid inside it keeps its element ids. +- textEquivalent: Keep the accessible table of cell counts and complete member lists as the diagram's text equivalent; identify every member by its full id, title, stated rung and floor. The axis explanation belongs to the caption. +- elementIds: Use `diagram.<subject>.maturity.text` and `diagram.<subject>.maturity.cell.<stated>.<floor>`; cell links target the built list, with unique member-card targets supplied by the Pack page. +- mermaidKind: `block` with a fixed categorical grid, cell labels and counts. This is not a quadrant chart; source includes each cell's full membership as comments and names its accessible table. diff --git a/specs/consumers/spec-studio.diagram-kinds.mermaid-source.sdp.md b/specs/consumers/spec-studio.diagram-kinds.mermaid-source.sdp.md new file mode 100644 index 000000000..de2b7a8ec --- /dev/null +++ b/specs/consumers/spec-studio.diagram-kinds.mermaid-source.sdp.md @@ -0,0 +1,33 @@ +--- +id: spec:consumers.spec-studio.diagram-kinds.mermaid-source +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.spec-studio.diagram-kinds + dependsOn: spec:consumers.spec-studio.page-elements + decidedBy: + - spec:decisions.studio-diagram-renderer + - spec:decisions.shipped-projections-frozen +--- +# A diagram offers Mermaid source as copyable text + +Mermaid source is a second serialization of the drawing's supplied values. It changes neither the static SVG renderer nor the separately shipped Mermaid root, so it requires no decision refining either ruling. A future Mermaid runtime, layout library or change to that root would need its own decision. + +## Intent +- outcome: Copy a drawing into another document while keeping the Studio usable without script or a Mermaid runtime. + +## Behavior +- rule: [#sourceOnly] Build escaped Mermaid text from the same selected and folded diagram values as the SVG. Source is an optional export, never a browser rendering path, a network request, a new command or a write to the shipped Mermaid root. +- rule: [#losslessMeaning] Each kind names its preferred syntax. Use a labelled flowchart fallback whenever that syntax cannot state the drawing's exact relations, claims or folds. State the chosen syntax and fallback reason beside the source. Preserve full ids in labels or comments and every aggregation count and folded member identity in comments; the text equivalent remains authoritative for the diagram's reading. +- rule: [#copy] A closed source disclosure contains a read-only text area with a visible label. Without script a reader selects and copies it. The page elements' copy control enhances this same text with its Copy button, Copied feedback and select-the-text fallback. The diagram element keeps only its viewport controls. +- rule: [#escaping] Treat every title, label, id and reason as data. Escape HTML at the page boundary and Mermaid syntax at the source boundary; never emit source directives, executable links, raw HTML or configuration supplied by corpus text. +- rule: [#omission] A drawing omitted for bounds or routing offers its text equivalent and a reason; it offers no partial Mermaid source that appears complete. Source follows the same explicit scope and folds as its figure. + +## Design +- disclosure: Summary `Mermaid source`, a paragraph `Copyable source; the Studio draws static SVG.`, a syntax label and optional fallback explanation, then a labelled read-only textarea and the script-added copy button. +- sourceElement: The disclosure's copy button is the page elements' `sdp-copy`, given a `for` attribute that names the source text area so a long source is not repeated in an attribute; no new element is added, and `sdp-diagram` still adds only pan, zoom and pinch. +- sourceEscaping: Inside a quoted label, `"`, `#`, `%`, `<` and `>` become Mermaid entity codes (`#quot;`, `#35;`, `#37;`, `#lt;`, `#gt;`); a backtick becomes `#96;`, a line break becomes a space, and every other character is kept. +- sourceIds: Stable safe aliases assigned in displayed box order, with full graph ids in quoted labels or escaped comments. Aliases are local serialization names, never graph identities. No click directive or initialization directive is emitted. +- elementIds: Use `diagram.<subject>[.<view>].source` and `.source-text`, inserting the view name exactly where the diagram's own ids do, so a trace graph's source is `diagram.<subject>.source`. +- export: The self-contained Pack export retains source as selectable text and omits the copy script; print expands the text equivalent and leaves source closed unless the reader opened it. diff --git a/specs/consumers/spec-studio.diagram-kinds.requirement-trace.sdp.md b/specs/consumers/spec-studio.diagram-kinds.requirement-trace.sdp.md new file mode 100644 index 000000000..37fd1f976 --- /dev/null +++ b/specs/consumers/spec-studio.diagram-kinds.requirement-trace.sdp.md @@ -0,0 +1,28 @@ +--- +id: spec:consumers.spec-studio.diagram-kinds.requirement-trace +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.spec-studio.diagram-kinds + dependsOn: spec:consumers.spec-studio.data +--- +# A requirement drawing separates intent from realization and verification + +## Intent +- outcome: Read a Spec's refinement, satisfying code and verifier bindings in one picture without mistaking a declared example for a running test. + +## Behavior +- rule: [#exactEdges] For the Spec page's selected set, draw declared refines, declared and anchored verifies, and anchored satisfies edges incident on the set, plus anchored verifies on its declared verifying examples. Draw code references and oracle models only as their own named kinds, never as realization or successful verification. +- rule: [#claimsApart] Every box and edge names its claim. A declared verifying example with no resolving test says `no test binding`; an anchored verifier says `binding present`, never passed. Missing satisfies says `no satisfying binding`, never unimplemented by inference through a child. Examples and decision records, which code never satisfies, carry no satisfying-binding label. +- rule: [#fold] Combine arrows with the same ends, direction and claim into one labelled arrow naming every relation. Fold verifying examples together with their exclusive test bindings beyond a small readable count, naming both claims and counts. Above the shared bound or readable density, fold the largest remaining group within each band by edge kind, direction and claim, ties in that lexical order and then by band order, until it fits. A fold names its relation and claim and hides its internal identity. Keep every original edge and box in the text equivalent. If the fold still cannot fit, omit the drawing and open its text. + +## Design +- placement: On a Spec page the requirement drawing replaces the realization drawing under its heading; its set is the Spec, the Specs that refine it, and the examples that verify any of them. +- bands: Place selected non-example Specs, declared verifying examples, satisfying or referencing units, and test or oracle bindings in labelled bands. An example in the selected set always sits in the examples band, including when it also refines a selected Spec. Place Specs outside the selected set in a separate labelled band. Each node has one placement and keeps every edge. Derive each absence statement from the selected examples and recorded edge kinds, never from which bands contain boxes. +- exampleFoldCount: Fold more than `4` selected verifying examples, excluding the subject, with their exclusive test bindings; retain the fold only when it lowers layout height. A generic examples-band fold is named `verifying examples`, never `refining Specs`. +- foldTitles: Name incoming and outgoing refinement folds `refining Specs` and `refined Specs`; incoming satisfies, references, anchored verifies and models folds read `satisfying units`, `referencing units`, `test bindings` and `oracle bindings`. Every fold includes its claim and hides its internal identity. +- wordsBesideStyle: Box subtitles say `declared Spec`, `anchored code`, `anchored test binding` or `anchored oracle binding`; edge labels carry both the exact relation and claim. Verification modes, risk levels and assertion results are not invented. +- textEquivalent: List every full id and title or binding label, each binding's file and line, every exact edge with its claim and reason, and examples with no test binding. This drawing replaces the realization drawing and complements the detailed verification panels. +- elementIds: Use `diagram.<subject>.requirement.text`, `.requirement.arrow` and `.requirement.fold.<kind>.<direction>.<claim>`; when the same group occurs in multiple bands, append `.band.<band-index>` in the stated band order. +- mermaidKind: `requirementDiagram` for the exact refines, satisfies and verifies subset, generic requirement nodes and element nodes for code and bindings; omit risk and verifyMethod. Serialize the original exact relations separately even when the SVG combines their arrow. Put claim words in node text and per-edge comments. Use `flowchart TB` with exact labels when references, models or folds require relations requirement syntax cannot express; never rename them traces. diff --git a/specs/consumers/spec-studio.diagram-kinds.scoped-architecture.sdp.md b/specs/consumers/spec-studio.diagram-kinds.scoped-architecture.sdp.md new file mode 100644 index 000000000..949c837ef --- /dev/null +++ b/specs/consumers/spec-studio.diagram-kinds.scoped-architecture.sdp.md @@ -0,0 +1,29 @@ +--- +id: spec:consumers.spec-studio.diagram-kinds.scoped-architecture +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.spec-studio.diagram-kinds + dependsOn: + - spec:consumers.design-slice + - spec:model.planned-architecture + - spec:consumers.spec-studio.lenses.architecture +--- +# A Spec set draws the architecture it touches + +## Intent +- outcome: Review a Pack, slice or roadmap's realized and planned seams together while seeing exactly why each component is present. + +## Behavior +- rule: [#scopeInput] Read the design slice's `architecture` answer: `components` with how the slice reaches each, in words, members' satisfying code, members' referencing code or what the slice rests on; `designs` with their declared values, realization and comparison rows, each drawn as a planned box; `uses` with the unit edges behind each count and its outside mark; `plannedUses`; `outside`; and `unplaced`. The drawing resolves no unit to a component itself and recomputes none of them; the unplaced units remain listed apart. +- rule: [#separatePlans] Draw anchored components and component designs as separate identities, even when the realization comparison joins them. Planned layer, context and uses stay declared; realized structure stays anchored; a difference is labelled rather than merged away. +- rule: [#boundary] Draw only the uses boundary the supplied design-slice architecture answer includes; never traverse it in layout. Its `uses` includes anchored pairs between touched components and pairs with one immediate outside neighbour, marked `outside: true`, as `spec:consumers.design-slice#rule.outsideNeighbors` states. Fold supplied outside neighbours into one `outside scope` box per context and layer while keeping their identities and original unit edges in the text equivalent; never add pairs between two outside components. Planned uses remain separate and never increase an anchored count. +- rule: [#bound] Apply the shared architecture folds by context then layer to this selected graph. If no component or planned design resolves, show the empty explanation and unassigned units instead of a blank or invented architecture. + +## Design +- layout: Use the architecture grid's context columns and layer bands, with the legibility child's title-first boxes, cell wrapping and local scroll. Prefix planned boxes with `planned`; outside-scope boxes say `outside scope`. +- edgeWords: The legend distinguishes `uses (anchored)`, `plans to use (declared)` and `realizes (anchored)` when present. A drawing containing uses and planned uses alone carries no per-edge label. When a realization correspondence is present, label the distinct relation families beside each link or through a keyed legend; realization stays apart from uses and confers no delivery fact. +- textEquivalent: List each component's full id, label, layer, context and the scoped Spec and binding kind that brought it in; list unassigned units, unreadable declarations, planned-to-realized comparisons and every original uses edge. +- elementIds: Use `diagram.<subject>.architecture.text` and `.architecture.arrow`; context and layer folds append their encoded full key before the suffix. +- mermaidKind: `block` with generic component boxes and uses arrows for a purely anchored grid; use `flowchart TB` with explicit declared and anchored labels when planned, comparison or folded edges cannot be preserved by block syntax. Architecture icons never infer deployment meaning. diff --git a/specs/consumers/spec-studio.diagram-kinds.sdp.md b/specs/consumers/spec-studio.diagram-kinds.sdp.md new file mode 100644 index 000000000..286196d46 --- /dev/null +++ b/specs/consumers/spec-studio.diagram-kinds.sdp.md @@ -0,0 +1,66 @@ +--- +id: spec:consumers.spec-studio.diagram-kinds +kind: behavior +altitude: feature +readiness: ready +relations: + refines: spec:consumers.spec-studio.diagrams + decidedBy: + target: spec:decisions.studio-diagram-renderer + reason: every new kind is laid out by the engine at build time as the renderer decision rules +--- +# The Studio chooses diagram kinds from graph values + +Dependency chains across a Pack, capability refinement, the components a design touches, and differences between stated readiness and the floor need different pictures. A diagram kind earns its place through the question it answers and the graph values that answer it. Mermaid supplies a useful catalog of visual forms, not the Studio's layout runtime or an instruction to draw every form. + +## Intent + +- outcome: Choose useful drawings from graph values, keep their labels readable on first view, and offer source a reader can copy without changing the shipped Mermaid projection. + +## Behavior + +- rule: [#graphData] A drawing reads the graph through the page's reader values and recipe answers. A visual form never supplies missing intent, order, cardinality, scores, dates or runtime evidence from prose, paths or imports. +- rule: [#kindCatalog] The catalog names each visual family, whether the graph holds its data, its subject, the question it answers, and a refusal or deferral's re-entry trigger. Renderer aliases share their family's row; diagnostic renderers are named apart. +- rule: [#drawnKinds] The Studio adds dependency depth, capability refinement, scoped architecture and the maturity map as a grid of counts, and a component uses table beside the architecture lens's drawing; requirement traceability replaces the realization drawing on a Spec page. Equivalent Mermaid forms share one Studio drawing. +- rule: [#honestScope] A Pack, slice or roadmap passes an explicit ordered set of Spec ids to a drawing. A grouping or a diagram never authors a delivery fact, ranks work, grants permission or sets a workflow gate. +- rule: [#sharedContract] Every new drawing keeps the diagram contract's bound, deterministic static SVG, links, words beside style, text equivalent and unique element ids. Layout and copying add no browser layout dependency. + +## Design + +- kindCatalog: `draw`, `equivalent`, `defer` and `refuse` describe catalog choices only. They are not graph fields or delivery states. A deferred form has honest inputs but adds no needed reading beyond the selected views; a refused form lacks typed inputs. +- legibleLabels: The figure keeps the Studio's navigation rail. Title-first labels and width-based cell wrapping reduce width; natural-size SVG with local horizontal scrolling preserves readable labels on desktop. Below the responsive breakpoint the same drawing fits the screen. The legibility child pins these measurements and behavior. +- flowchart: Draw for a Spec set, Pack, slice or roadmap from declared relations; dependency depth reveals chains and cycles the one-hop trace cannot show. Arrows retain their exact relation and direction, never execution order. The existing trace and members remain equivalent flowchart subjects. +- mindmap: Draw capability refinement from declared refines for an epic or capability map. The tree shows nesting absent from a flat list; repeated parents and cycles stay explicit rather than becoming an invented hierarchy. +- treeView: Equivalent to the capability tree for a strict refinement tree. Folder icons and file paths would invent meaning; use plain labels. Repeated parents use the capability view's cross-links or its flowchart source. +- block: Draw scoped architecture and the discrete maturity matrix. Components and anchored uses answer which seams a Spec set touches; stated and reached rungs answer where statements lag or overreach. Cell position is categorical, never a score. +- architecture: Equivalent to the scoped architecture drawing for a component or Spec set, from component, memberOf and uses plus planned-component declarations. Generic boxes keep optional context and layer; cloud, service, network and deployment icons need explicit typed declarations before they can mean anything. +- usesTable: Draw a component uses table, a design structure matrix, beside the architecture lens's drawing: one row per realized component as the user and one column per realized component as the used, in one order, a cell for each pair. A table stays legible at any number of uses, where routed edges between a dozen components run in parallel bundles. It is an HTML table read from anchored uses, needs no layout, bound or fold, and offers no Mermaid source because Mermaid has no matrix form. +- requirement: Draw a Spec's or bounded Spec set's refinement, satisfying units and declared versus anchored verifiers together. This shows the distinction between intent and binding in one picture; it supplies no risk, test method or passing result. +- class: Defer a role view for a component or slice: units as classes stereotyped by their recorded role, with anchored uses as dependency arrows and no fields, methods or inheritance. Role, memberOf and uses are honest inputs, but the role vocabulary is under review and the architecture page lists roles as text. Re-enter when the role vocabulary settles and a reader needs pattern relationships drawn. Refuse fields, methods, inheritance and multiplicities; Model prose is not a schema. +- entityRelationship: Refuse for a Spec, component or corpus. Terms and relations are not entity attributes, keys or cardinalities. Re-enter with an explicit typed entity and cardinality contract. +- sequence: Refuse for a Spec or slice. Uses and dependsOn hold no messages, participants or temporal order. Re-enter with typed ordered interactions or a separately admitted runtime input. +- zenUml: Refuse for the same missing interaction data as sequence; the optional external renderer adds no graph evidence. Re-enter when ordered interactions have a lawful graph home. +- state: Refuse for a Spec or roadmap. Readiness is a position, not a transition machine, and the graph holds no domain states, events or guards. Re-enter with a typed domain state contract unrelated to delivery gating. +- journey: Refuse for a Spec, Pack or corpus. Intent actors do not define ordered journey tasks or satisfaction scores. Re-enter with typed journey steps and a reader who needs them. +- gantt: Refuse for a roadmap or corpus. No durations, start dates or schedules are held, and dependency depth is not time. Re-enter only with a separately ruled temporal input that never gates work. +- pie: Defer for a Pack or corpus. Counts by kind or readiness are honest, but slices repeat the census while hiding exact comparisons. Re-enter when a reader needs part-to-whole comparison that the census cannot answer. +- quadrant: Refuse normalized quadrant coordinates for a Pack or roadmap. Readiness rungs have no numeric distances and priority is not a score. The maturity map uses a discrete matrix instead. Re-enter with explicitly typed independent numeric measures. +- gitGraph: Refuse for a Spec, Pack or corpus. Git is the event log, but commits and branch ancestry are not nodes in the current graph. Re-enter with a ruled history input and a history reader. +- c4: Refuse C4 semantics for a component or slice. Component anchors do not state people, software systems, containers, trust boundaries or deployment. Re-enter with typed C4 meanings; generic component structure remains in the architecture drawing. +- timeline: Refuse for a roadmap or corpus. A dependency chain and authored Pack order are neither dated events nor time periods. Re-enter with a lawful history input. +- sankey: Refuse for a component, slice or corpus. Uses counts are discrete bindings, not a conserved flow or its magnitude. Re-enter with typed flow units and weights. +- xyChart: Defer categorical count bars for a Pack or corpus because the census already gives the exact counts; refuse a trend without time-series input. Re-enter when a reader needs a count comparison or a separately ruled time series. +- packet: Refuse for a Spec or component. Contract prose contains no typed bit offsets, field widths or packet layout. Re-enter with a typed wire-format layout. +- kanban: Defer a separate readiness board because the maturity map shows both stated and reached rungs; refuse workflow states or inferred work order. The roadmap page's HTML lists by need already provide the categorical board over explicit graph values. Re-enter for Mermaid kanban export when a reader needs to copy that board. +- radar: Refuse for a Spec, Pack or corpus. There are no comparable numeric quality scores or justified common scales; cardinalities cannot manufacture them. Re-enter with typed independent measures and their units. +- treemap: Defer for a Pack or corpus. Spec counts and membership are honest, but area repeats the census and a multi-parent refinement graph cannot be partitioned without duplication. Re-enter with a named size-comparison reader and an explicit partition and measure. +- swimlanes: Refuse process swimlanes for a slice or component. Context and layer do not define actors, tasks or control flow. The architecture grid remains an equivalent grouping only. Re-enter with typed process participants and flow. +- useCase: Refuse UML use-case associations for an epic or Spec. Intent.actor is prose, not an actor identity or an include, extend or association edge. Re-enter with explicit typed actor associations; behavior altitude alone supplies none. +- eventModeling: Refuse for a component or slice. The graph holds no ordered commands, events, screens or event streams. Re-enter with typed event-modeling declarations. +- venn: Defer Pack-overlap sets for the corpus. Membership allows honest intersections, but no current task needs a bounded overlap picture. Re-enter with named Packs and an overlap question whose intersections remain legible. +- ishikawa: Refuse for a Spec or corpus. A dependency is not a causal explanation and the graph holds no typed cause categories. Re-enter with authored causal relations admitted by the Protocol. +- wardley: Refuse for a capability map or roadmap. Refinement and dependencies hold no value-chain visibility or evolution coordinates. Re-enter with a ruled typed strategy input; never map readiness to evolution. +- cynefin: Refuse for a Spec or roadmap. Kind and readiness are not complexity classifications. Re-enter with explicitly authored complexity assessments in an admitted contract. +- agentFlow: Refuse agent-flow semantics for a component or slice. Anchored uses do not identify agents, tools, prompts or routing decisions. Re-enter with typed agent and routing declarations. +- railroad: Refuse railroad, EBNF, ABNF and PEG forms for a Spec or corpus. A vocabulary's step strings and parameter slots do not state a complete grammar production graph. Re-enter with typed productions and a grammar reader; never parse prose into a grammar. +- diagnostics: Mermaid's info and error renderers are diagnostics, not corpus diagram kinds. Keep them outside the Studio catalog of graph drawings; renderer variants such as flowchart ELK, class and state versions share their semantic family's row. diff --git a/specs/consumers/spec-studio.diagrams.sdp.md b/specs/consumers/spec-studio.diagrams.sdp.md index bfb19abd2..095cdb6ec 100644 --- a/specs/consumers/spec-studio.diagrams.sdp.md +++ b/specs/consumers/spec-studio.diagrams.sdp.md @@ -15,41 +15,44 @@ relations: --- # A diagram is laid out from graph values into SVG with a text equivalent -The Studio draws four diagrams: the architecture lens's components by context and layer, one component's members and what they use, one Spec's relations, and on a Spec page the code that realizes it. Each is laid out in the engine when the page set is built, so a page draws it with no script and the same graph draws the same bytes. Words carry every reading the drawing shows: bands and columns are labelled, a planned or folded box says so, and a text equivalent after the drawing states every box and edge. +Every Studio diagram is laid out in the engine when the page set is built, so a page draws it with no script and the same inputs draw the same bytes. The architecture grid, component members and one-hop trace retain their readings; the Spec page's requirement drawing replaces realization, and other pages name their supplied drawing answers. Words carry every reading the picture shows, and its text equivalent states every box and edge. ## Intent - outcome: Draw the Studio's diagrams as inline SVG that reads without script, prints, and stays the same bytes for the same graph, with every box and edge also stated in words. ## Contract - [#buildTime] The build lays out each diagram and writes it into its page as inline SVG; no diagram is laid out in the browser, and a page draws every diagram with no script. -- [#layoutInputs] A layout reads only the diagram's boxes, each with its id and its words, and its edges, each with its two ends and its kind, taken from the reader values and recipe answers its page names; it reads no font, file, clock or random value, measures text by character count, and writes whole-pixel coordinates. +- [#layoutInputs] A layout reads only the boxes, each with its id, words and supplied grouping, and edges, each with its two ends, exact kind and claim, optional reason and aggregation count, taken from the reader values and recipe answers its page names; it reads no font, file, clock or random value, measures text by character count and writes whole-pixel coordinates. Depth, reach and component resolution belong to those answers, never layout. - [#sameBytes] One graph and the same inputs draw byte-identical SVG on every build and every machine. - [#architectureGrid] The architecture diagram places each component in the column of its recorded context, contexts in code-unit order and then a column for components that state none, and in the band of its layer, in the order edge, application, domain, adapter, infrastructure, then a band for components that state none; a cell holds its components in id order. - [#aggregatedUses] An architecture edge joins two components and carries the count of unit `uses` edges between them, as the component rows of `spec:consumers.spec-studio.lenses.architecture#design.componentRows` count them: each end of a unit `uses` edge resolves to its component, directly or through `memberOf`, and a use within one component draws nothing. - [#relationBands] A trace graph draws the Spec in a band of its own, the Specs its declared relations name above it in one band per relation type, in the order refines, dependsOn, constrainedBy, decidedBy, verifies, supersedes, and the Specs whose declared relations name it below in bands of the same order; a Spec related more than one way sits in the band of its first relation, and each relation is its own edge. - [#memberBands] A member diagram draws a component's members in bands by how deep `uses` among members reaches, a member that uses another sitting above it, and the units and components outside the component that members use in a last band of their own. -- [#realizationBands] A realization diagram draws the units that satisfy the Spec, then their components, then what those units use, in three bands. - [#bound] A diagram draws at most the boxes and edges the bound names. - [#foldByContext] An architecture diagram over the bound with more than one context draws one box per context, with the uses between contexts aggregated and counted, and then each context's own diagram in a section of its own, where every other context it uses or is used by is one folded box in a band of its own. - [#foldByLayer] A diagram of one context still over the bound draws one box per layer of that context, and then each layer's components in a section of their own. - [#boundFallback] A drawing still over the bound after these folds, the context overview, one context's diagram or one layer's, writes no SVG; its text equivalent lists every box and edge the drawing would hold and then says the drawing was omitted because it exceeds the bound. - [#foldGroups] A trace graph over the bound folds its largest relation group first, ties broken by direction and then by relation type, into one box that names the count and the relation and links to the Spec page's relations, until the drawing fits. -- [#noFoldBelowComponent] A member or realization diagram over the bound writes no SVG; its text equivalent lists every member or unit and every use or realization edge, and then says the drawing was omitted because it exceeds the bound. +- [#noFoldBelowComponent] A member diagram over the bound writes no SVG; its text equivalent lists every member and every use edge, and then says the drawing was omitted because it exceeds the bound. - [#wordsBesideStyle] Every reading a fill, outline or line style marks is also in words: bands and columns carry labels, a planned component reads `planned`, a folded box reads `folded`, and an aggregated edge's count is in its text equivalent. A drawing whose edges all have one relation names it once in its legend and draws no edge label; solid `uses` and dashed `plans to use` share the use relation, with the planned wording also in the legend. A drawing with several relations keeps its labelled bands and edge labels. - [#textEquivalent] Each diagram is followed by its text equivalent, which states every box with its words and every edge as a sentence naming its two ends and its kind, and the SVG names it as its description. -- [#panZoomOnly] The diagram element adds only pan, zoom and pinch, by rewriting the SVG's view box; it never moves a box or an edge. With or without script the SVG is never wider than its container or larger than its own size on first view at every viewport width; pan and zoom enlarge it for reading. +- [#panZoomOnly] The diagram element adds only pan, zoom and pinch by changing the SVG's view box; it never moves a box or edge. At viewport widths of at least 720 CSS pixels the SVG starts at natural size inside a keyboard-accessible scroll region, with overflow inside the figure. Below that width the responsive contract fits the same drawing to the screen. Reset restores the initial view and scroll position. Without script the drawing, scrolling, links and text equivalent remain usable. Print fits the drawing to paper and exposes its text equivalent. ## Design - bound: `40` boxes and `100` edges per diagram, counting every box a drawing holds, folded and planned boxes included, and every edge it draws. -- labelMetrics: a label line holds at most `30` characters, an id longer than that wraps after a dot or a colon and then after a hyphen, a character advances `7.5` pixels at the 12 pixel monospace label size, and a box is as wide as its longest line plus padding. -- cellWrap: a grid cell or a band places at most `6` boxes on one line and wraps onto the next. +- density: For requirement, trace, capability and dependency drawings, trigger the existing folds before the hard bound when deterministic layout height exceeds `2000` logical pixels. Measure height from the box and lane layout before routing, using the same label metrics and width budget as the drawing. A fold triggered by these limits must strictly lower that height and hold at least two boxes, keeping the same scope, claims and complete text equivalent. If no lawful fold fits the height budget, omit the SVG and open the text equivalent. +- labelMetrics: A title or recorded label leads at fourteen pixels, followed by the displayed id at twelve pixels and remaining words such as a component's member count. Title lines hold at most `28` characters and id lines `30`, with an eight-pixel character advance. Wrap at spaces or id punctuation, then by code point, and calculate box height from every line. +- cellWrap: Wrap a cell's boxes within the fixed logical width budget instead of a fixed box count; retain context columns with local scrolling when they exceed it. Layout reads no browser or font measurement. +- widthBudget: `880` logical pixels for a band or grid, band labels included above their band; `264` per context cell when several contexts share a grid, the full band budget when only one context is shown, and `24` between boxes. Routing gutters may widen the SVG to preserve distinct lanes. A drawing wider than `880` scrolls inside its figure and receives the build-written instruction `Scroll horizontally to read the whole drawing.` +- shortId: Drop the namespace and leading dotted segments every id in the figure shares, marking elision with `…`; keep more segments on collisions. A displayed id carries a namespace only when shown in full. Full ids remain in link titles, the text equivalent and source. +- edgeRouting: Reserve boxes and labels as obstacles, route orthogonally with distinct ports and lanes eight pixels apart, and never share a collinear segment or overlap labels. Crossings are permitted only at free points. Keep exact relations and claims through the words-beside-style rule. A deterministic finite routing failure omits the SVG, opens its text equivalent and explains the failure; the build owns search mechanics. - edgeKinds: `uses` for a realized use, drawn solid; `plans to use` for a use a component design states, drawn dashed; on a trace graph the relation's type; an arrow points from the unit or Spec that uses or declares to the one it names. -- svgMarkup: the `svg` carries `role="img"`, a `title`, `aria-describedby` naming its text equivalent and a `viewBox` of its own size; a box is a `g` with class `node` and `data-id`, plus `data-planned="true"` or `data-folded="true"` when it is planned or folded, holding a link to the box's target; an edge is a `g` with class `edge`, `data-from`, `data-to` and `data-kind`; a band is a `g` with class `band` and `data-label`, a column a `g` with class `lane` and `data-label`, each also drawing its label as text. +- svgMarkup: the `svg` carries `role="img"`, a `title`, `aria-describedby` naming its text equivalent and a `viewBox` of its own size; a box is a `g` with class `node` and `data-id`, plus `data-planned="true"` or `data-folded="true"` when it is planned or folded, holding a link to the box's target; an edge is a `g` with class `edge`, `data-from`, `data-to` and `data-kind`; a band is a `g` with class `band` and `data-label`, a column a `g` with class `lane` and `data-label`, each also drawing its label as text. An edge also carries `data-claim` and a stable `data-edge-key` when its label uses a legend key; route and label coordinates are presentation values and mint no graph edge. - boxTargets: a component's box links to its detail on the architecture lens, a planned component's to its component design's Spec page, a context's or layer's folded box to its section on the same page, a Spec's to its page, and a folded relation group's to the Spec page's relations. -- textEquivalent: a `details` element closed by default, summary `The diagram as text`, holding one `li` per box, `<id>: <words>`, and then one `li` per edge, `<from> <kind> <to>`, followed by `(<n> unit edges)` when the edge aggregates more than one. -- diagramElement: `sdp-diagram` with `subject`, the Spec, Pack, component or context id the diagram is rooted at, or `architecture`, wrapping one `figure` that holds the caption, a legend `Arrows: <relation>.` when all edges share one relation, followed by `Dashed arrows: plans to use.` when that use relation includes planned uses, the SVG and its text equivalent; on upgrade it adds `Zoom in`, `Zoom out` and `Reset` buttons and handles drag, a wheel turned with Control or Command held, and a two-finger pinch. +- textEquivalent: a `details` element closed by default, summary `The diagram as text`, holding one `li` per box, `<id>: <words>`, followed by `· folded members: <ids>` when folded identities are present, and then one `li` per edge, `<from> <kind> <to>`, followed by `· <claim>` when present, `(<n> unit edges)` when the edge aggregates more than one, and `— <reason>` when present. +- diagramElement: `sdp-diagram` with `subject`, the rooted Spec, Pack, component or context id, or `architecture`, wraps one figure containing its caption and legend, any build-written scroll instruction, a keyboard-focusable named `.diagram-scroll` region containing the SVG, its text equivalent and, when offered, its closed Mermaid-source disclosure with labelled read-only textarea and `sdp-copy`. Caption, legend and source stay outside the scroll region. On upgrade it adds Zoom in, Zoom out and Reset buttons and handles drag, a wheel with Control or Command held, and pinch; it adds no layout or copy behavior. - elementIds: a diagram's text equivalent has the id `diagram.<subject>.text` and its arrow has the id `diagram.<subject>.arrow`. A page that draws a second view of one subject inserts that view's name before `text` or `arrow`, so the Spec page's realization drawing uses `diagram.<subject>.realization.text` and `diagram.<subject>.realization.arrow`; its trace graph keeps the unqualified ids. Every id is unique on its page. -- omittedDrawing: a drawing over the bound keeps its `figure` and caption, holds the sentence `Shown as text: this drawing exceeds the bound of <b> boxes and <e> edges.`, with the bound's two numbers, where the SVG would be, and writes its text equivalent open. +- omittedDrawing: A drawing over the bound keeps its figure and caption, writes `Shown as text: this drawing exceeds the bound of <b> boxes and <e> edges.`, and opens its text equivalent. A routing failure instead writes `Shown as text: a distinct route or label could not be placed within the routing budget.` and opens the same complete box-and-edge reading. A drawing still above the height budget after lawful folds instead writes `Shown as text: this drawing exceeds the height budget of <h> pixels after folding.` and opens its text equivalent. No omission offers partial Mermaid source. ## Example space ```gwt-vocabulary diff --git a/specs/consumers/spec-studio.diff-page.sdp.md b/specs/consumers/spec-studio.diff-page.sdp.md index 1f1e15752..2975aae34 100644 --- a/specs/consumers/spec-studio.diff-page.sdp.md +++ b/specs/consumers/spec-studio.diff-page.sdp.md @@ -21,9 +21,9 @@ Iterating on a design asks what changed since the last pass, or what a change pr ## Behavior - rule: [#onlyWithBase] The page set holds `diff.html` only when the build was given a base graph; without one, no page, link or change note names a diff. - rule: [#namesTheBase] The page opens by naming its input: the base graph, its label when the build was given one, its schema version and its Spec count beside the built graph's, and a sentence that the comparison reads the two graphs only. -- rule: [#diffSections] The page renders recipe 35's answer with `detail: true` over the whole corpus, in sections in this order: the totals as sentences; the Specs added, linked; the Specs removed, as their ids in text, since no page holds them; the Specs changed, each linked, with each changed part, its change in words, and for a changed value the base value and the built value as readable text blocks or lists, one above the other, never a raw JSON array; keyed changes list in the head's rule and flow order, each as one line with the entry's text rendered as on its Spec page beside its old and new address or `none`; the Packs whose membership or order changed; the bindings changed, kept apart and headed as derived from anchors; and the diagnostics. +- rule: [#diffSections] The page renders recipe 35's answer with `detail: true` over the whole corpus, in sections in this order: the totals as sentences; the Specs added, linked; the Specs removed, as their ids in text, since no page holds them; the Specs changed, each linked, with each changed part, its change in words, and for a changed value the base value and the built value as readable text blocks or lists, one above the other, never a raw JSON array; keyed changes list in the head's rule and flow order, each as one line with the entry's text rendered as on its Spec page beside its old and new address or `none`; the Packs whose membership, order or order meaning changed, an added Pack whose order is priority read as `added as a roadmap`, its added members in manifest order, a removed Pack whose order was priority read as `removed as a roadmap`, and a known change to priority read as `now a roadmap` or from priority as `no longer a roadmap`, stating the before and after order values for other changes of meaning; the bindings changed, kept apart and headed as derived from anchors; and the diagnostics. - rule: [#specChangeNotes] Given a base graph, each changed Spec's page adds a line under its header that names the base input, counts keyed parts apart as `<n> rules keyed, wording unchanged` beside any other changed parts it lists, and links to its row on the diff page; each added Spec's page says it is new since the base. -- rule: [#packChangeNotes] Given a base graph, each Pack page adds a `Changed since the base` section after its maturity map, from recipe 35's answer for that Pack, listing its changed and added members, linked, and its own membership change; a member whose only changes are keying is marked `keyed only`. +- rule: [#packChangeNotes] Given a base graph, each Pack page adds a `Changed since the base` section after its maturity map, from recipe 35's answer for that Pack, listing its changed and added members, linked, and its own membership change, with the same roadmap and order-meaning wording as the diff page; a member whose only changes are keying is marked `keyed only`. - rule: [#confersNothing] The page and the change notes state differences between two graphs and confer nothing: no approval, staleness or readiness, and no record that a design pass happened. ## UI diff --git a/specs/consumers/spec-studio.export.sdp.md b/specs/consumers/spec-studio.export.sdp.md index 9edb0de2c..190d5c13c 100644 --- a/specs/consumers/spec-studio.export.sdp.md +++ b/specs/consumers/spec-studio.export.sdp.md @@ -28,7 +28,7 @@ A design is sent and archived as well as browsed: attached to a message, handed - rule: [#exportLinks] Inside an export, a link to the Pack or to a member points at its section in the file, a link to an element of the Pack page or of a member's page points at that element in the file, and a link to anything else becomes its id as text, so no link leaves the file. - rule: [#uniqueExportIds] Within an export, every element id copied from the Pack page takes the prefix `pack/` and every id copied from a member's page the prefix `<Spec id>/`, while each member's section keeps its Spec id as its own id; every fragment link and every attribute that names an element id, such as `aria-describedby` or a label's `for`, is rewritten to the prefixed id, so no two elements in the file share an id. - rule: [#exportFromPackPage] Each Pack page links its export under the header, as `Download this Pack as one file`. -- rule: [#printAsDocument] Every page prints as a document: the print style hides the navigation, the rail, the search field, the composing panel and every control, gives the canvas the page's width, prints a diagram whole at that width, and prints every disclosure open, the expander opening each before printing and restoring it after. +- rule: [#printAsDocument] Every page prints as a document: the print style hides the navigation, the rail, the search field, the composing panel and every control, gives the canvas the page's width, prints a diagram whole at that width, and prints every disclosure open except Mermaid source, which prints only when the reader opened it, the expander opening each before printing and restoring it after. - rule: [#exportBytes] An export is a function of the graph like every page: the same graph writes the same bytes, and an export holds no time, commit or stamp. ## UI @@ -38,7 +38,7 @@ A design is sent and archived as well as browsed: attached to a message, handed ## Design - exportIds: a copied id is written `pack/<id>` for the Pack page and `<Spec id>/<id>` for a member's page, so a member's `section.behavior` becomes its Spec id, a slash and `section.behavior`; the slash appears in no graph id, so a prefixed id never equals a member's section id or another prefixed id. -- expanderPrint: `sdp-expander`, before the page prints, opens every `details` inside it, and after printing restores each one's state. +- expanderPrint: `sdp-expander`, before the page prints, opens every `details` inside it except a drawing's or the maturity map's Mermaid source, which keeps the state the reader left, and after printing restores each one's state. ## Example space ```gwt-vocabulary diff --git a/specs/consumers/spec-studio.index-page.sdp.md b/specs/consumers/spec-studio.index-page.sdp.md index 69dfaa168..791aea53e 100644 --- a/specs/consumers/spec-studio.index-page.sdp.md +++ b/specs/consumers/spec-studio.index-page.sdp.md @@ -25,7 +25,7 @@ A reader of a large design needs two ways in: by the task they came to do and by - rule: [#taskRoutes] A short list then routes by task, in fixed words that quote nothing from the corpus: read a Pack's design, find a Spec, entry or file by a word, narrow the Specs, and read the findings; each route links to its place in the page set. - rule: [#packsLens] The Packs section lists every Pack in id order with its title and its members in authored order, each member with its stated rung beside its floor; it draws no capability tree. - rule: [#specRows] The Specs section lists every Spec in id order, one row each: its title and id, kind and altitude, stated rung beside the floor reached, implementation binding and verifier binding as present or none, open and blocking questions, and its Packs. -- rule: [#filters] Filters narrow the Specs section by kind, altitude, stated rung, floor reached, implementation binding, verifier binding and Pack; each filter takes one value or any, the filters combine, and the section says how many of its rows it shows. Without script every row shows. +- rule: [#filters] Filters narrow the Specs section by kind, altitude, stated rung, floor reached, implementation binding, verifier binding, Pack and epic; each filter takes one value or any, the epic filter also takes no epic, the filters combine, and the section says how many of its rows it shows. Without script every row shows. - rule: [#findings] The findings section lists the validation report's findings in its order, each with its severity in words, its validator id, its subject when it has one, linked to its page when it is a Spec or a Pack and as text otherwise, its message, and its recorded file and line as text. ## UI diff --git a/specs/consumers/spec-studio.lenses.architecture.sdp.md b/specs/consumers/spec-studio.lenses.architecture.sdp.md index 07f7402f6..b75e846c0 100644 --- a/specs/consumers/spec-studio.lenses.architecture.sdp.md +++ b/specs/consumers/spec-studio.lenses.architecture.sdp.md @@ -24,6 +24,7 @@ The original's architecture lens drew runtime layers, ports and external systems ## Behavior - rule: [#contextAndLayer] The lens draws every component the anchors realize, from recipe 17's component rows and the attributes the `componentRows` entry adds to them, grouped by its recorded context and layer as the architecture grid of `spec:consumers.spec-studio.diagrams` places it, and folds above the bound as that Spec states. - rule: [#membershipAndUses] The lens draws component membership, as the member count in each box, and `uses`, as edges between components, and nothing else: no port, external system, runtime layer or import the impact graph infers. +- rule: [#usesTable] Beside the architecture drawing comes the component uses table, read from recipe 17's component rows and their counts of unit `uses` edges: one row per realized component as the user and one column per realized component as the used. A nonzero cell holds the count of anchored unit `uses` edges behind the pair and links to that use in the row component's detail; a zero cell between two components shows `·`, with `no anchored use recorded` as hidden text and the legend `· means no anchored use recorded`, and the cell where a component meets itself stays empty. Planned uses, unresolved endpoints and uses within one component stay in their own readings and never enter the table. The table is not a drawing, and it reads in full without script and in print. - rule: [#plannedApart] Each component design no anchor realizes, a row of recipe 43's `unrealized` list, draws in the same grid by its planned context and layer as a planned box, with the uses it plans drawn dashed to the components it names, and is listed again in a `Designed, not built` section apart from the realized components; a planned use naming a component the lens draws no box for draws no edge and stays listed in its design's row. - rule: [#differences] A realized component whose design differs from it lists each differing field, as recipe 43's `differing` rows give it, with its planned and realized values in its detail, and a design value recipe 43 cannot read lists in the `Designed, not built` section as unreadable. - rule: [#componentDetail] Each realized component has a detail section on the lens page whose element id is its id, which its box and every link to it from another page open: its id, label, layer, context and role; its recorded file and line as text with a copy action; its members, each with its id, label, role and recorded file and line; the components it uses and is used by, each with the count of unit edges; the Specs it and its members satisfy and reference, linked, a component design its anchor satisfies among them; and its member diagram, every value read from its recipe 17 row. @@ -32,9 +33,10 @@ The original's architecture lens drew runtime layers, ports and external systems - rule: [#lensSummary] The page opens with sentences the build counts from the two recipes' answers: the components realized and planned, the contexts and layers they state, the uses edges between components, and the designs that differ from their components. ## UI -- architecturePage: in order, the page title `Architecture`, the summary sentences, the architecture diagram or its fold with one section per context, the `Designed, not built` section, the component details in id order, and the `Unresolved structure` section when it has rows. +- architecturePage: in order, the page title `Architecture`, the summary sentences, the architecture diagram or its fold with one section per context, the component uses table, the `Designed, not built` section, the component details in id order, and the `Unresolved structure` section when it has rows. +- usesTable: a table captioned `Which components use which` in a named, keyboard-focusable scroll region, one row header and one column header per realized component, both in one order: by layer from `edge` to `infrastructure`, unlayered components last, then by id; each header names the component's label with its id as code. - designedNotBuilt: a table with one row per unrealized design in Spec id order: the component design's Spec, linked; the planned component's id as code; its planned context, layer and role, or `not stated`; the components it plans to use; and whether its component anchor exists without satisfying it; a design whose value recipe 43 cannot read adds a row naming the design, the key and the unreadable value. -- componentDetail: a heading with the component's id as code and its label, a line with its layer, context and role in words, its file and line with a copy action, then lists headed Members, Uses, Used by, Satisfies and References, an empty list reading None, then the member diagram. +- componentDetail: a heading with the component's id as code and its label, a line with its layer, context and role in words, its source binding apart from its members, then Members grouped by recorded role, Uses with supporting unit edges at the table's fragment targets, Used by, any design differences, Satisfies and References, an empty list reading None, then the member diagram. ## Design - componentRows: `{ label: string | null; file: string | null; line: number | null; role: string | null; layer: string | null; context: string | null; referencedSpecs: string[]; usesCounts: { component: string; edges: number }[]; usedByCounts: { component: string; edges: number }[] }`, the fields recipe 17 adds to each component row: the component's own label, file, line, role, layer and context as its CodeNode records them, each null when the node records none or no node declares the component; the Specs the component and its members reference, sorted; and, sorted by component, the count of unit `uses` edges behind each pair `usesOut` and `usedBy` name. Each member row gains `role: string | null`, and every field the recipe returns today keeps its meaning. diff --git a/specs/consumers/spec-studio.lenses.architecture.unit-register.sdp.md b/specs/consumers/spec-studio.lenses.architecture.unit-register.sdp.md new file mode 100644 index 000000000..ffe7583fb --- /dev/null +++ b/specs/consumers/spec-studio.lenses.architecture.unit-register.sdp.md @@ -0,0 +1,27 @@ +--- +id: spec:consumers.spec-studio.lenses.architecture.unit-register +kind: behavior +altitude: story +readiness: ready +relations: + refines: spec:consumers.spec-studio.lenses.architecture +--- +# The architecture page lists each component's significant units by role + +A contributor about to change the engine needs the curated register of its significant units: the ports, contracts, pipeline stages and catalogs each component owns, and the designs each unit realizes or answers to. The graph already records them on the anchors. This panel reads them there, grouped by component and role, and shows what no anchor states rather than guessing it. + +## Intent + +- outcome: Let a contributor find recorded units and their design bindings through their component and role, with omitted structure visible. + +## Behavior + +- rule: [#recordedOnly] The architecture page groups recorded members by role within each recorded component, and shows a component's own source binding apart from its members. A row shows the recorded id, label, file and line and its satisfies and references targets separately. The page reads graph bindings only, never an annotation review or oracle source file. +- rule: [#omissions] Keep a not-stated role group and a no-component section. A binding confers no review approval or execution verdict, and the page shows no inferred accepted badge. +- rule: [#roleTokens] Show each recorded role token without interpreting it through a Protocol-corpus Spec id. An adopter's unfamiliar roles remain visible and produce no finding. +- rule: [#stableOrder] Sort components by id, role groups by token and members by id in code-unit order, with not-stated roles last and the no-component section after the components. +- rule: [#staticAccess] Every row is available without script through native disclosures. The component index links to the existing component section ids, and narrow screens retain readable source paths and separate realization and reference lists. + +## Verification — reviewed + +- Render the self-hosted corpus and an adopter with unfamiliar roles, omitted roles and omitted membership. Check ordering, component source rows, target separation, keyboard disclosures and narrow-screen overflow, with no inferred approval badge. diff --git a/specs/consumers/spec-studio.lenses.packs.sdp.md b/specs/consumers/spec-studio.lenses.packs.sdp.md index 3922a55f9..cbf879109 100644 --- a/specs/consumers/spec-studio.lenses.packs.sdp.md +++ b/specs/consumers/spec-studio.lenses.packs.sdp.md @@ -11,14 +11,14 @@ relations: --- # The Packs lens groups every Spec by the Packs that hold it -A capability is read as a Pack, or as a projection over high-altitude behavior Specs that no view draws yet, so the lens groups by Pack and draws no capability tree. The index page holds it, because the Packs are where a reader of a design starts. +Packs collect Specs for review, while the separate Capabilities page reads authored refinement. The index remains the landing page and keeps each Pack's members in authored order. ## Intent - outcome: Let a person reach every Spec through the Packs that hold it, each Pack's members in the order its manifest gives. ## Behavior - rule: [#landing] The Packs lens is the index page, the page a person lands on. -- rule: [#packGrouping] The Packs lens groups Specs by Pack, each Pack's members in authored order, and draws no capability tree. +- rule: [#packGrouping] The Packs lens groups Specs by Pack, each Pack's members in authored order. The separate Capabilities page reads refinement; selecting an epic narrows the index's Specs section without changing any Pack. - rule: [#everySpec] A Spec in no Pack is still listed, in the index's Specs section, so the lens hides no Spec. ## UI diff --git a/specs/consumers/spec-studio.lenses.sdp.md b/specs/consumers/spec-studio.lenses.sdp.md index a5b856cf4..ae8275751 100644 --- a/specs/consumers/spec-studio.lenses.sdp.md +++ b/specs/consumers/spec-studio.lenses.sdp.md @@ -7,17 +7,17 @@ relations: refines: spec:consumers.spec-studio dependsOn: spec:consumers.spec-studio.data --- -# Four lenses read the graph as Packs, architecture, tests and evidence +# Five lenses read the graph as Packs, capabilities, architecture, tests and evidence -Each lens is a page with a Spec of its own, so each builds on the graph it reads today: the Packs, architecture and tests lenses read what the graph records, and the evidence lens reads not tracked until the graph records an observation. This Spec keeps what the four share. +Each lens is a page with a Spec of its own, so each builds on the graph it reads today: the Packs, capabilities, architecture and tests lenses read what the graph records, and the evidence lens reads not tracked until the graph records an observation. This Spec keeps what the five share. ## Intent -- outcome: Let a person see the graph four ways, by Pack, by architecture, by verification and by evidence, from one top navigation. +- outcome: Let a person see the graph five ways, by Pack, by capability, by architecture, by verification and by evidence, from one top navigation. ## Behavior -- rule: [#order] The lenses are pages of the page set, reached from the top navigation in the order Packs, architecture, tests, evidence; the index page is the Packs lens and the default landing page. +- rule: [#order] The lenses are pages of the page set, reached from the top navigation in the order Packs, capabilities, architecture, tests, evidence; the index page is the Packs lens and the default landing page. - rule: [#lensValues] A lens renders reader values and the recipe answers its own Spec names, and computes none of them in the page. - rule: [#readOnly] A lens writes nothing and states nothing: it lists, groups, draws and links, and a change a person wants from it leaves through the composing panel as scoped intent. ## UI -- lensPages: `index.html` for the Packs lens, `architecture.html`, `tests.html` and `evidence.html`, each opening with its name and one sentence saying what it reads. +- lensPages: `index.html` for the Packs lens, `capabilities.html` for the capabilities lens, `architecture.html`, `tests.html` and `evidence.html`, each opening with its name and one sentence saying what it reads. diff --git a/specs/consumers/spec-studio.navigation.sdp.md b/specs/consumers/spec-studio.navigation.sdp.md index 517f52e9c..7e190fcdf 100644 --- a/specs/consumers/spec-studio.navigation.sdp.md +++ b/specs/consumers/spec-studio.navigation.sdp.md @@ -21,6 +21,6 @@ The shell's top navigation reaches the index and each lens, and the index routes - outcome: Let a person reach every page of the page set from the top navigation of any page, and from the index by the task they came to do. ## Behavior -- rule: [#topEntries] After the lens entries, the top navigation lists Queue, Questions, Decisions, Declarations, Backlog and Glossary, then Diff when the build was given a base graph and Runs when it was given run records, each linking its page; the search field follows them. +- rule: [#topEntries] After the lens entries, the top navigation lists Roadmap, Queue, Questions, Decisions, Declarations, Backlog and Glossary, then Diff when the build was given a base graph and Runs when it was given run records, each linking its page; the search field follows them. - rule: [#currentPage] The top-navigation link to the page being read carries `aria-current="page"` and an underline, so it shows by more than color. -- rule: [#indexRoutes] After its four routes, the index's `Start here` list adds See what the owner can state first, linking the owner queue; See the components by context and layer, linking the architecture lens; and, given a base graph, See what changed since the base, linking the diff page. +- rule: [#indexRoutes] After its four routes, the index's `Start here` list adds See what comes next, linking the roadmap page; See what the owner can state first, linking the owner queue; See the components by context and layer, linking the architecture lens; and, given a base graph, See what changed since the base, linking the diff page. diff --git a/specs/consumers/spec-studio.pack-page.maturity-map.sdp.md b/specs/consumers/spec-studio.pack-page.maturity-map.sdp.md index b2fbf1c96..ab1d5789c 100644 --- a/specs/consumers/spec-studio.pack-page.maturity-map.sdp.md +++ b/specs/consumers/spec-studio.pack-page.maturity-map.sdp.md @@ -6,18 +6,20 @@ readiness: ready relations: refines: spec:consumers.spec-studio.pack-page dependsOn: - target: spec:consumers.derived-readiness-banner - reason: the map sets each stated rung beside the floor reached, as the banner reads them + - target: spec:consumers.derived-readiness-banner + reason: the map sets each stated rung beside the floor reached, as the banner reads them + - target: spec:consumers.spec-studio.diagram-kinds.maturity-picture + reason: the map is drawn as the picture defines --- # A Pack page's maturity map places every member by its stated rung and its floor -A Pack of many members takes a reviewer a long scroll through its cards before they know where the design stands. The maturity map answers that first: one table, a row per stated rung and a column per floor reached, each member a short card in its cell. A member right of its row's own rung waits only for a statement the floor already allows; a member left of it states more than its floor holds. +A Pack's maturity map shows its distribution before its member lists. Equal-sized cells place each resolved member by stated readiness and floor reached. Populated cells show exact counts and member marks; faint empty cells show no number. The count table and ordered lists retain the reading in text. ## Intent - outcome: Let a reviewer see at a glance where every member of a Pack stands, by the rung it states and the floor it reaches. ## Behavior -- rule: [#maturityMap] After the summary, the maturity map places each resolved member by its stated rung and the floor it reaches: one row per stated rung, idea to ready, and one column per floor, none to ready, each filled cell listing its members in authored order as short cards linked to their member cards, with its count, and an empty cell showing `—` alone; the table caption explains in words where the floor sits above the statement and where the statement sits above the floor. +- rule: [#maturityMap] After the summary, show the categorical grid and full member lists defined by spec:consumers.spec-studio.diagram-kinds.maturity-picture. Every resolved member belongs to one stated-rung and floor cell; unresolved members are named apart. Populated cells link to their built lists, whose members stay in authored order. ## UI -- maturityMap: an `sdp-maturity-map` holding a table under a `Maturity map` heading, rows headed `stated idea` to `stated ready`, columns headed `floor none` to `floor ready`, each filled cell its count and its members as short cards, an empty cell reading `—` alone, and the axis explanation in the table's caption, never inside a header cell: right of the matching rung the floor is above the statement and already allows it; left of it the statement is above the floor. +- maturityMap: An `sdp-maturity-map` holds the static SVG grid, caption and accessible count table, followed by populated cells' full member lists in disclosures that print open. Equal-sized cells have explicit axes; the legend names matches, floor above statement and statement above floor once. The diagonal has an outline, empty cells are faint, and populated cells carry exact counts and member marks, up to `100` marks and solid fill above that. Every member links to its card. diff --git a/specs/consumers/spec-studio.pack-page.sdp.md b/specs/consumers/spec-studio.pack-page.sdp.md index 9c17bce00..3f83510d5 100644 --- a/specs/consumers/spec-studio.pack-page.sdp.md +++ b/specs/consumers/spec-studio.pack-page.sdp.md @@ -22,8 +22,8 @@ The Markdown Pack page renders the Pack design as one wide member table, and the - outcome: Show where a Pack's design stands, member by member in reading order, on one page a reviewer scans quickly and opens where they need detail. ## Behavior -- rule: [#header] The page opens with the Pack's title, its id and its carrier file as text with a copy action, then the summary; the framing follows as prose under the heading Framing, with the vocabulary references, linked. -- rule: [#summary] The summary states each Pack total in one sentence that opens with its count, never as a table: the members and the unresolved ones, the members held below their stated next rung and those waiting only for their author, the design entries and pinned declarations, the open and blocking questions, the members with an implementation binding and with a design reference, and the boundary in each direction. +- rule: [#header] The page opens with the Pack's title, its id and its carrier file as text with a copy action, and a link to the Pack's slice page, then the summary; the framing follows as prose under the heading Framing, with the vocabulary references, linked. +- rule: [#summary] The summary states each Pack total in one sentence that opens with its count, never as a table: the members and the unresolved ones, the members held below their stated next rung and those waiting only for their author, the design entries and pinned declarations, the open and blocking questions, the members with an implementation binding and with a design reference, and the boundary in each direction. The member sentence says `in priority order` when the Pack states `order: priority`, otherwise `in authored reading order`. - rule: [#holds] The summary's held sentence links each held member to its card with its count of unmet clauses, and its waiting sentence names the members whose stated next rung waits only for their author's statement unless every member does; the page never presents a rung as approved. - rule: [#memberCards] The members follow in authored order, one closed card each. A card's summary line shows the member's position, title and kind, its stated rung beside the floor reached, its stated next rung with `nothing blocks` or how many of its clauses are unmet, its open and blocking questions, and its implementation and verifier bindings as present or none. - rule: [#cardBody] An open card shows the member's id, linked to its Spec page, then the parts the next five rules state, in their order. @@ -38,7 +38,7 @@ The Markdown Pack page renders the Pack design as one wide member table, and the - rule: [#openAll] `Open all` and `Close all` open or close every card and section at once, and opening the page at a card's fragment opens that card. ## UI -- headerBlock: the title as the page heading; below it the id and the carrier file, each with a copy action; then the summary; then the framing under a `Framing` heading, and the vocabulary references. +- headerBlock: the title as the page heading; below it the id and the carrier file, each with a copy action, and the link `Read this Pack as one design` to its slice page; then the summary; then the framing under a `Framing` heading, and the vocabulary references. - summaryList: the totals as a short list of sentences under a `Where the design stands` heading, each opening with its count, a zero written as No. - memberCard: a disclosure whose element id is the member's Spec id; its summary line reads `<position>. <title>`, then the kind, the rung badge, the floor in words, the next rung in words unless the member states ready, the question counts and the two binding values, all but the badge as plain text, wrapping onto further lines on a narrow screen. - cardBody: in order, the id link, the unmet clauses, the design counts and decisions, the open questions, the code, and the verifier binding. diff --git a/specs/consumers/spec-studio.pack.sdp.md b/specs/consumers/spec-studio.pack.sdp.md index 943eec9c0..499df02e5 100644 --- a/specs/consumers/spec-studio.pack.sdp.md +++ b/specs/consumers/spec-studio.pack.sdp.md @@ -18,6 +18,7 @@ specs: - spec:consumers.spec-studio.lenses.packs - spec:consumers.spec-studio.index-page - spec:consumers.spec-studio.lenses.architecture + - spec:consumers.spec-studio.lenses.architecture.unit-register - spec:consumers.spec-studio.lenses.architecture.planned-apart - spec:consumers.spec-studio.lenses.tests - spec:consumers.spec-studio.lenses.tests.coverage-row diff --git a/specs/consumers/spec-studio.page-elements.sdp.md b/specs/consumers/spec-studio.page-elements.sdp.md index 294126b91..5012b37f4 100644 --- a/specs/consumers/spec-studio.page-elements.sdp.md +++ b/specs/consumers/spec-studio.page-elements.sdp.md @@ -27,7 +27,7 @@ The build renders every page whole, so these elements add behavior to markup tha ## Design - search: `sdp-search` with `page-root`, the relative path from its page to the page-set root: wraps the search field's form and, on submit, opens `<page-root>search.html#q=<term>` with the term percent-encoded. - searchResults: `sdp-search-results`: on the search page, reads the index from the `sdp-data` element, matches the fragment's term and each term the person types, writes the results `spec:consumers.spec-studio.search` designs, and keeps the term in the fragment. -- filter: `sdp-filter`: wraps a hidden form of labelled `select` elements, each with `data-filter="<name>"` and an `any` option, and the rows marked `data-row`. On upgrade it shows the form, hides each row whose `data-<name>` differs from a chosen value, and writes `<shown> of <total> shown` in its `output` element; a row whose attribute holds several space-separated values, such as its Packs, matches when one of them equals the choice. +- filter: `sdp-filter`: wraps a hidden form of labelled `select` elements, each with `data-filter="<name>"` and an `any` option, and the rows marked `data-row`. On upgrade it shows the form, hides each row whose `data-<name>` differs from a chosen value, and writes `<shown> of <total> shown` in its `output` element; a row whose attribute holds several space-separated values, such as its Packs, matches when one of them equals the choice. When a followed link or a fragment change targets an element inside a row it hides, it returns every control to `any`, adds to its output that the filter was cleared to show that target, and scrolls the target into view. - expander: `sdp-expander`: adds `Open all` and `Close all` buttons for the `details` elements inside it, and on load and on each fragment change opens every `details` that holds the fragment's target and scrolls the target into view. -- copy: `sdp-copy` with `text`: a `Copy` button whose accessible name includes the text; it copies the text to the clipboard and says `Copied`, and where the clipboard is unavailable it selects the text so the person can copy it. +- copy: `sdp-copy` with `text`, or with `for` naming a text area on its page: a `Copy` button whose accessible name includes the text or the text area's label; it copies that text and says `Copied`, and where the clipboard is unavailable it selects the text so the person can copy it. - stamp: `sdp-stamp` with `page-root`: served over http or https, it requests `<page-root>stamp.json` and, when it loads, shows its corpus commit, engine and build time as text; otherwise it shows nothing. diff --git a/specs/consumers/spec-studio.publisher.sdp.md b/specs/consumers/spec-studio.publisher.sdp.md index c21375ee7..bddf214c3 100644 --- a/specs/consumers/spec-studio.publisher.sdp.md +++ b/specs/consumers/spec-studio.publisher.sdp.md @@ -29,7 +29,7 @@ The Studio has its own verb and root beside the four shipped publishers, and is - rule: [#buildInvalidates] Every command that enters the shared build, `sdp studio` among them, removes `generated/spec-studio/` and its temporary sibling before extraction, beside the four roots `spec:consumers.wholesale-view-rewrite#rule.buildInvalidates` names; a removal failure refuses the command before extraction. - rule: [#checkClean] `sdp studio --check-clean` renders an independent twin and refuses divergent renders, then compares the current root with the new render; missing or drifted output returns nonzero and is removed, and clean output is replaced wholesale with byte-identical content. - rule: [#findingsShown] Findings never withhold the page set: a run over a graph with validation findings still publishes and returns the validation exit code, and each finding shows on the index, and also on its subject's page when its subject is a Spec or a Pack. A run whose extraction fails publishes nothing and returns nonzero. -- rule: [#pageSet] The page set holds `index.html`, `search.html`, one `packs/<name>.html` per Pack, one `specs/<name>.html` per Spec, and `assets/studio.css` and `assets/studio.js`. `<name>` is the id after its namespace, with each uppercase ASCII letter written as `_` and its lowercase, so no two pages collide on a file system that ignores case. +- rule: [#pageSet] The page set holds `index.html`, `search.html`, one `packs/<name>.html` and one `slices/<name>.html` per Pack, one `specs/<name>.html` per Spec, and `assets/studio.css` and `assets/studio.js`. `<name>` is the id after its namespace, with each uppercase ASCII letter written as `_` and its lowercase, so no two pages collide on a file system that ignores case. - rule: [#deterministic] Two runs over one graph and the same inputs write byte-identical files, and no file holds a time, a commit, an absolute path or a host name of the run's own. - rule: [#linksResolve] Every link a page writes stays inside the page set: it resolves to a page the run wrote and, with a fragment, to an element id that page carries, apart from the search page's term fragment `search.html#q=<term>`, which names no element. No page links a carrier or source file, an editor URI or an external address; the only link that leaves the page set is one the stamp element adds in the browser from a present stamp's source template. diff --git a/specs/consumers/spec-studio.register-input.sdp.md b/specs/consumers/spec-studio.register-input.sdp.md new file mode 100644 index 000000000..05d66d615 --- /dev/null +++ b/specs/consumers/spec-studio.register-input.sdp.md @@ -0,0 +1,40 @@ +--- +id: spec:consumers.spec-studio.register-input +kind: behavior +altitude: story +readiness: scoped +relations: + refines: spec:consumers.spec-studio + dependsOn: + - target: spec:validation.external-registers + reason: the input is the register file and pattern the external-registers recipe already reads + - target: spec:consumers.spec-studio.data.inputs + reason: the register joins the base graph and the run records as a named input +--- +# An outside register is a named Studio input, so a cited token links to its entry + +A corpus that cites an outside register, an open-decisions list or an earlier system's references, writes its tokens in prose. The external-registers recipe checks them against a register file the corpus passes; a reader on the page still sees only the token. On the first adopter's pages a reader met a cited open decision and found no route to its entry. The register is the corpus's own data, so it reaches the Studio the way the base graph and the run records do: named on the command line, named on every page that shows it, and never graph data. + +## Intent + +- actor: A reviewer with only a link who meets a cited token, such as an open decision's number, in a Spec's text. +- problem: A cited token reads as bare text on every page, and the register that defines it is neither linked nor shown. +- outcome: Let the Studio take an outside register as a named input, so each token a corpus cites from it links to its entry, named as outside evidence and never as graph data. + +### Open questions + +- [blocking #registerEntry] What does a register file give the Studio beyond what the external-registers recipe reads, so a token's entry can be shown? + - decider: delegated + - provisional: an optional `title` and an optional `href` on a token object; the Studio writes one page per register listing each token with its title, retired tokens marked, each linking to its `href` when present +- [blocking #inputForm] How does the studio verb name a register, its pattern and the words that name it on a page? + - decider: delegated + - provisional: a repeatable `--register NAME=PATH` with a matching `--register-pattern NAME=SOURCE`, the name naming the register in words wherever it shows +- [non-blocking #textLinks] Does a cited token become a link inside the rendered text of a Spec, or only in a list beside it? + - decider: delegated + - provisional: inside the text, matched by the register's pattern exactly as the recipe matches it, so the page and the check agree on what is cited + +## Behavior + +- rule: [#registerNamed] A register is a named input like the base graph and the run records: a page that shows it names it in words, and it never becomes graph data, a finding, a fact or a rung. +- rule: [#tokenRoute] Each token the register's pattern finds in a Spec's text routes to that token's entry on the register's page, and a token the register does not list, or retires, reads so beside it. +- rule: [#withoutRegister] Without the input every page is what the graph alone writes. diff --git a/specs/consumers/spec-studio.roadmap-page.sdp.md b/specs/consumers/spec-studio.roadmap-page.sdp.md new file mode 100644 index 000000000..04c44bfa1 --- /dev/null +++ b/specs/consumers/spec-studio.roadmap-page.sdp.md @@ -0,0 +1,53 @@ +--- +id: spec:consumers.spec-studio.roadmap-page +kind: behavior +altitude: story +readiness: ready +relations: + refines: + - spec:consumers.spec-studio + - target: spec:consumers.roadmap + reason: the page is the roadmap's reading for a person + dependsOn: + - spec:consumers.spec-studio.data + - spec:consumers.spec-studio.shell + - target: spec:consumers.spec-studio.publisher + reason: an entry's element id carries the name the page set gives its roadmap's Pack page + - target: spec:consumers.roadmap + reason: the page renders the roadmap reading + - target: spec:consumers.roadmap.memo + reason: the page carries the roadmap memo for a description's author to copy +--- +# The roadmap page shows the upcoming work in priority order, split by what it needs + +The owner reads a roadmap to choose what the next pull requests take up, and a reviewer reads it to see where an arc stands. The page shows each roadmap at a glance as a board, its entries in the column of what each needs next, and then each entry with what holds it. It ends with the forward section a pull request description carries, ready to copy. The page orders by the owner's places and links; it states nothing. + +## Intent + +- actor: The owner choosing the next work, and a reviewer with only a link. +- problem: No page shows what comes next in priority order, so the owner reads it from the last pull request description, and a reviewer cannot tell which upcoming work needs design, a statement or a build. +- outcome: Give the Studio a page that shows each roadmap in its priority order, with what to design, what to state, what to build and what is built, and for each entry what it lacks. + +## Behavior + +- rule: [#roadmapPage] The page set holds `roadmap.html`, which renders recipe 53 over every Pack whose manifest states `order: priority`, one section per roadmap in id order, each ending with recipe 55's memo for that Pack. +- rule: [#noRoadmap] With no such Pack, the page says the corpus states no roadmap and that a Pack becomes one when its manifest states `order: priority`. +- rule: [#roadmapSummary] Each roadmap's section opens with its title linked to its Pack page, its framing, and sentences the build counts from the answer: the entries with each need, those listed apart and any unresolved members, said to be in priority order only when the answer's `order` is `priority` and in authored reading order otherwise; the questions waiting on the owner, blocking and not, and those that name no decider; and the statements waiting, with a link to the owner queue. +- rule: [#board] A board follows: four columns, to design, to state, to build and built, each headed by its need and its count and listing its entries in place order, each as its place and its title linked to the entry's row below; an empty column reads `none`. +- rule: [#readingLegend] Beside the board, a legend reads `To state means the floor allows ready and a person still judges the statement. Built means every Spec of the entry's work states ready and has an implementation binding; it reports no passing test and no deployment. The entry's own readiness is shown separately.` +- rule: [#needLists] Four lists follow in the same order, each keeping the roadmap's order and numbering each entry by its place, so a gap in the numbers shows an entry with another need; an empty list says no entry has that need. +- rule: [#entryRow] Each entry shows its place and title, its kind and altitude, its stated rung and the rung its floor reaches, and its work by need with any drift; opened, it shows its Spec linked, its stated next rung with the clauses that hold it or `nothing blocks`, the parts with its need, each linked with its rungs and what holds its next rung, its own labelled `itself`, with its other parts folded under one line that counts them, the parts to build now linked, its blocking questions linked to their Spec pages, its decisions below `ready` with their blocking questions, what it rests on with the parts that reach it, what waits on it, for an entry to state the waiting statements the statement queue puts before its own, its other Packs and the entries it shares Specs with. +- rule: [#restsOnShown] Among what an entry rests on, the Specs to design, the Specs on the roadmap and the Specs that hold a `ready` statement show first, each with its relation, its need and its place on the roadmap or `outside the roadmap`; the rest fold under one line that counts them. +- rule: [#waitingList] After the lists, the page lists what waits on the owner, recipe 53's `waiting`: each question with its blocking flag, its address linked to the question on its Spec page with a copy action, its text and its provisional reading when present, and each waiting statement with its stated rung and the rung its floor allows, in the order the answer gives, each row linking the entries it touches. The questions that name no decider follow under the sentence `These questions have no named decider.`, apart from the owner's. +- rule: [#apartList] The entries listed apart follow the lists, each with its place, kind, stated rung and need. Unresolved members follow under `Unresolved members`, each with its original place and its id as code, with no invented title, rung or need. +- rule: [#forwardSection] Each roadmap ends with a disclosure holding recipe 55's memo as preformatted text with a copy action, so a description's author pastes the forward section the graph holds. +- rule: [#needFilter] A filter narrows the lists by need; without script every row shows. +- rule: [#statesNothing] The page orders by the roadmap's places and links: it states no rung, approves nothing, and never presents a place as a date, a deadline or permission to start. + +## UI + +- boardColumn: a column headed `To design`, `To state`, `To build` or `Built` with its count, listing `<place>` and the title as a link to the entry's element id; the four columns sit side by side at desktop width and stack at phone width. +- entryRow: a disclosure whose summary reads `<place>. <title>` on its first line and, on its second, the kind and altitude, the stated rung as a badge, `floor <rung>`, and the work as `<n> Specs:` followed by the nonzero counts of `to design`, `to state`, `to build` and `built`, then `<d> drift` when present; its element id is `roadmap-<name>-entry-<place>`, `<name>` the name the page set gives the roadmap's Pack page, and every link to an entry, from the board, a rests-on row, the waiting list or a shared-Specs line, uses the owning roadmap's prefix, so two roadmaps on the page never share an id. +- entryFields: labelled lines Id, Next, Parts, Build now, Blocked by, Decided by, Rests on, Waited on by, Earlier statements, Also in and Shares Specs with, an empty line left out; Parts appears when the entry has more than one part or a part other than itself sets its need. +- restsOnItem: the Spec linked, its relation in words, followed by `from` and the parts that reach it when any part other than the entry itself does, its need, `holds ready` when it states below `defined`, and `entry <place>` linked to that row or `outside the roadmap`. +- memoDisclosure: a disclosure whose summary reads `The forward section, as Markdown`, holding a copy action and the memo in a `pre` element. diff --git a/specs/consumers/spec-studio.sdp.md b/specs/consumers/spec-studio.sdp.md index 4195cdd8d..9eb07b68f 100644 --- a/specs/consumers/spec-studio.sdp.md +++ b/specs/consumers/spec-studio.sdp.md @@ -26,7 +26,7 @@ relations: ## Behavior - 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: [#staticPageSet] The Studio is a static page set: the index page, which is the Packs lens; a search page; a page for each other lens, each register, the owner queue and the glossary; one page per Pack and per Spec; one export file per Pack; and, when the build is given their inputs, a diff page and a runs page; with their local assets. Its build renders each page whole, so a page needs no server, and every page but the search page reads without its script, which adds filtering, search, copying, pan and zoom, and the composing panel. +- rule: [#staticPageSet] The Studio is a static page set: the index page, which is the Packs lens; a search page; a page for each other lens, each register, the owner queue, the roadmap and the glossary; one page and one slice page per Pack, one page per Spec; one export file per Pack; and, when the build is given their inputs, a diff page and a runs page; with their local assets. Its build renders each page whole, so a page needs no server, and every page but the search page reads without its script, which adds filtering, search, copying, pan and zoom, and the composing panel. - 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. - rule: The Studio is reproducible: one graph and the same inputs render byte-identical files, and an asset whose filename embeds a hash of its content gets the same name from the same graph. - rule: The Studio is read-rich: everything the graph holds about a Spec or a Pack is explorable in it. diff --git a/specs/consumers/spec-studio.slice-page.sdp.md b/specs/consumers/spec-studio.slice-page.sdp.md new file mode 100644 index 000000000..cd47eb434 --- /dev/null +++ b/specs/consumers/spec-studio.slice-page.sdp.md @@ -0,0 +1,82 @@ +--- +id: spec:consumers.spec-studio.slice-page +kind: behavior +altitude: story +readiness: ready +relations: + refines: + - spec:consumers.spec-studio + - target: spec:consumers.design-slice + reason: the page is the design slice's reading for a person + dependsOn: + - target: spec:consumers.design-slice + reason: the page renders the slice's answer + - target: spec:consumers.spec-studio.data.recipe-answers + reason: the build evaluates the slice recipe once for each Pack, and the page reads that answer + - target: spec:consumers.spec-studio.pack-page + reason: each member links to its card on the Pack page, whose element id is the member's id + - target: spec:consumers.spec-studio.diagram-kinds.dependency + reason: the dependency drawing given the slice's order is a drawing kind, which owns layout, folding and the text equivalent + - target: spec:consumers.spec-studio.diagram-kinds.scoped-architecture + reason: the scoped architecture drawing is a drawing kind, which owns layout, folding and the text equivalent + - target: spec:consumers.spec-studio.diagrams + reason: both drawings keep the diagram contract, bound and text equivalent included + - spec:consumers.derived-readiness-banner + - spec:consumers.binding-language-views + - spec:model.open-question-fields +--- +# A slice page shows a Pack's design as one whole + +The Pack page reads a Pack card by card, in the order its author wrote. A reviewer judging the design needs the other reading first: which members the rest stand on, what the design will touch in the code, and what decisions and questions bound it. The slice page gives that reading on a page of its own beside the Pack page, one link apart. It keeps the Pack page whole, because a Pack of a hundred members already makes a long page of cards, and the two readings answer different questions in different orders: the author's narrative there, the members others rest on first here. + +## Intent + +- outcome: Give the Studio a page per Pack that shows its design slice: the members' dependency drawing, the components they touch, the decisions and open questions in scope, and each member's next rung. + +### Open questions + +- [non-blocking #exportCarriesSlice] Does a Pack export also carry the Pack's slice page? + - decider: delegated + - provisional: no; the export keeps the Pack page and its members, and the slice page stays one link away in the page set + +## Behavior + +- rule: [#slicePerPack] The page set holds one slice page per Pack, `slices/<name>.html`, its name formed from the Pack's id as the Pack page's name is; it renders the build's design slice answer for that Pack in full and states no value the answer does not hold, apart from counts the build takes of it. +- rule: [#header] The page opens with the heading `Design slice` and the Pack's title, the Pack's id as text with a copy action, and a link back to the Pack page, then the summary and a reading note. +- rule: [#summary] The summary states each slice total in one sentence that opens with its count: the members and the examples counted in their parents; when the manifest names members that resolve to no Spec, how many and each one's id, so an incomplete Pack never reads as complete; the relations among members, the deepest depth and any cycle; the members held below their stated next rung and those waiting only for their author; the outside Specs the slice rests on, how many of them hold a member, and the outside Specs that rest on it; the components touched, through the members' code, through what the slice rests on, and named by member designs, with the outside components shown beside them; the member component designs, how many are realized and how many of those match; the decisions in scope and how many state below `ready`; and the open and blocking questions. +- rule: [#readingNote] The reading note says in words that members read with the ones others rest on first, that the design columns count what each member carries beside its rung and never judge whether it is enough, and that the page records no review and approves nothing. +- rule: [#dependencyDrawing] Under `How the members rest on each other` comes the slice's dependency drawing, `spec:consumers.spec-studio.diagram-kinds.dependency` given the slice's order: it consumes the answer's members, depths, rests-on links, structure edges, cycles and boundary rows and recomputes no scope, depth or order. Each member that is not a folded example is a box with its title, id, stated rung and floor, and its count of folded example members; every supplied relation keeps its type and direction, and a relation that orders nothing reads apart from one that orders. The drawing kind owns layout, bounded folding and the complete text equivalent, which lists every folded example with its `refines`; a fold changes no value of the answer. +- rule: [#memberTable] Under `What each member needs next` comes one row per member that is not a folded example, by depth and then in the scope's order: the member's title linked to its card on the Pack page with its id, its depth, its stated rung beside the floor reached, its stated next rung with `nothing blocks` or each unmet clause and the Specs it names with their stated rungs, then the design it carries, its rules with how many are keyed, its design entries, pinned declarations, open questions with how many block, decisions, and verifying examples with how many are bound, each count on its own line, and its implementation and reference bindings as counts; the table has no total row, and no count is combined with another. +- rule: [#exampleRows] A folded example member has no row of its own and stays readable in a disclosure within its parent's row, whose summary counts the folded examples and how many are held below their stated next rung; inside, each example shows its title linked to its card on the Pack page, its id, its own stated rung, floor reached and stated next rung with each unmet clause and the Specs it names. Folding lends the parent no rung, hold or binding, and the folded count stays apart from the count of examples that verify the parent. +- rule: [#architecture] Under `The architecture it touches` comes the scoped architecture drawing of the diagram kinds, given the answer's components, designs, uses, planned uses and outside components, then one row per touched component: its id linked to its detail on the architecture lens; its label, layer and context; and how the slice reaches it, the members whose code satisfies it, the members whose code references it, the Specs the slice rests on whose code it holds, and the member designs naming it, each on its own line in words. +- rule: [#designs] After the component rows comes one row per member component design: the design linked to its Spec page, the component it names, the layer, context, role and uses it declares, whether an anchor holds that component and whether that anchor satisfies the design, in words, and its differing rows with the declared and anchored values side by side and its unreadable rows; declared entries stay in the Declared cell, and every anchored value in the Comparison cell is labelled anchored. +- rule: [#outside] Then come the components outside the slice, each with how it is reached and whether an anchor holds it, under a heading that says they are shown for context and not counted as touched. +- rule: [#unplaced] After them come the units that implement or reference a member and belong to no component, reading None when empty. +- rule: [#noArchitecture] A slice that touches no component and holds no component design says so in one sentence and names, by count, the Specs it rests on that no code satisfies, so an empty architecture never reads as an architecture nobody needs. +- rule: [#decisions] Under `Decisions in scope` comes one row per decision: its title linked to its Spec page, its stated rung beside the floor reached, whether it is a member, the members it shapes, the decisions it refines or supersedes, and its open and blocking question counts. +- rule: [#questions] Under `Open questions in scope` comes each question, blocking ones first, with its blocking flag in words, its address linked to the question on its Spec page when it is keyed, its Spec, its text, its decider, links, provisional reading and trigger when present, and the members it holds. +- rule: [#boundary] Under `Where the slice meets the rest` come four closed sections, Rests on, Rested on by, Verified by and Superseded by, each headed with its count; a Rests on row shows the outside Spec linked, its stated rung beside the floor reached, its implementation binding as present or none, the members it joins under each relation type, and the members it holds. +- rule: [#bindingWords] The page speaks binding language: a unit implements or references a member, a verifier is bound, and no cell reads built, done, verified or approved. +- rule: [#openAll] `Open all` and `Close all` open or close every closed section and text equivalent at once, and the page reads in full without script. + +## UI + +- pagePath: `slices/<name>.html`, `<name>` formed from the Pack id as `packs/<name>.html` forms it, so the Pack page and its slice page share a name. +- headerBlock: the page heading `Design slice: <Pack title>`; below it the Pack id with a copy action and the link `Read the Pack member by member` to its Pack page; then the summary under `The design as a whole`, then the reading note. +- readingNoteText: `Members read with the ones others rest on first. The columns count the design each member carries beside its rung; they never judge whether it is enough. This page records no review and approves nothing.` +- drawingIds: Both drawings take the Pack id as their subject and use their drawing kind's element-id contract. The dependency text and arrow ids are `diagram.<pack id>.dependencies.text` and `diagram.<pack id>.dependencies.arrow`; scoped architecture uses `diagram.<pack id>.architecture.text` and `diagram.<pack id>.architecture.arrow`. The kinds also own viewport, source, fold and occurrence suffixes; the page invents no singular dependency or scoped-architecture alias. +- memberTable: a table in a scroll region with the columns `Member`, `Depth`, `Rung`, `Next rung`, `Design carried` and `Code`, so it fits a desktop column without scrolling; `Member` holds the title link, the id and the kind; `Rung` the rung badge and `floor <rung>`; `Design carried` six lines, `<n> rules, <k> keyed`, `<n> entries`, `<n> declarations`, `<n> questions, <b> blocking`, `<n> decisions` and `<bound> of <n> verifying examples bound`, each count a plain number, a zero written `0`, and a zero part such as `, 0 keyed` left out; `Code` two lines, `<n> implementing` and `<n> referencing`. Below the phone breakpoint each member reads as a stacked block: its title, id and kind, its rung badge and floor, and its next rung with each unmet clause stay together at the top, and its depth, design counts and binding counts follow as labelled runs that wrap; no part of a member sits beyond a horizontal scroll. +- foldedExamples: a row under the parent's row spanning the table, holding a closed disclosure whose summary reads `<n> example members folded in`, followed by `, <h> held below the next rung` when any is held; inside, one line per example with its title link, its id, its rung badge, `floor <rung>` and `next <rung>:` with `nothing blocks` or each unmet clause. +- componentTable: a table with the columns `Component`, `Layer and context` and `Reached through`, the last listing each way the slice reaches the component on its own labelled line. +- designTable: a table with the columns `Design`, `Component`, `Declared`, `Anchor` and `Comparison`; `Declared` one labelled line per key, `not stated` for a key the design leaves out; `Anchor` reads `absent`, `present, does not satisfy the design` or `satisfies the design`; `Comparison` reads `not compared` for a design no anchor satisfies, `matches` for a realized design with no differing or unreadable row, and otherwise one line per differing field as `<field>: declared <value>; anchored <value>`, a `uses` line reading `uses: only declared <components>; only anchored <components>` as the comparison names them, and one line per unreadable key as `<key>: unreadable <value>, not compared`. +- outsideList: a list headed `Outside the slice, shown for context and not counted as touched`, each component with its id, linked when anchored, its label, `anchored` or `not anchored`, and how it is reached in words. +- decisionTable: a table with the columns `Decision`, `Rung`, `In the slice`, `Shapes` and `Questions`. +- questionItem: the blocking flag in words; for a keyed question, `question.<key>` as a link to its address with a copy action for the full address; its Spec; the text; each present field on its own labelled line; then `Holds: ` and the held members, or nothing for a question that holds none. +- boundaryTable: five columns, Spec, rung, implementation binding, joined members and holds, the joined members listing each relation type with its members. + +## Verification — manual + +- A person given only the slice page of the first adopter's history-view Pack names the member the most others rest on, the components the Pack's code touches and the outside Specs whose code the Pack builds on, from the page alone. +- The same person, given the slice page of a Pack whose members no code realizes yet, names the components the design will touch through what it rests on, and the members carrying pinned declarations while they state below `defined`. +- The same person, given the slice page of a Pack holding member component designs, says for each design whether code realizes it and where its declared values differ from the anchored ones, and reads no declared value as anchored. +- The same person, given the same page on a phone, names each member's stated next rung and what holds it without scrolling sideways, including an example member folded into its parent. diff --git a/specs/consumers/spec-studio.spec-page.sdp.md b/specs/consumers/spec-studio.spec-page.sdp.md index d24e75dbe..ce90c7a3a 100644 --- a/specs/consumers/spec-studio.spec-page.sdp.md +++ b/specs/consumers/spec-studio.spec-page.sdp.md @@ -7,6 +7,8 @@ relations: refines: spec:consumers.spec-studio dependsOn: - spec:consumers.spec-studio.data + - target: spec:consumers.spec-studio.diagram-kinds.requirement-trace + reason: the Design view reads the selected set and claims the requirement drawing defines - spec:consumers.derived-readiness-banner - spec:consumers.binding-language-views - spec:model.open-question-keys @@ -16,7 +18,7 @@ relations: - spec:extraction.entry-locations - spec:consumers.pack-design - target: spec:consumers.spec-studio.diagrams - reason: the design view and the impact panel draw the realization diagram and the trace graph + reason: the design view and impact panel draw requirement traceability and the one-hop trace graph - target: spec:extraction.entry-citations reason: the design section shows the source lines that cite each entry - target: spec:model.term-addresses @@ -44,7 +46,7 @@ The page shows only what the graph and the build's named inputs hold. The origin - rule: [#sectionAdditions] Inside the core's sections the page adds a composing action beside each keyed entry that proposes new text for it, the composing actions beside each open question, the rule coverage beside each keyed rule or flow and the example cards after the rules, each constraint's target, the citations beside each cited entry, and an element id on each keyed term, as the rules below state. - rule: [#termIds] Each keyed Model term in the core's Model section carries the element id `term.<key>`, its address after the `#`, and shows its key and its recorded line, as the core's entry ids do. - rule: [#citations] Beside each keyed entry an anchored source file cites, its section lists every citing file and line recipe 51 records, as text with a copy action; a pinned declaration no file cites reads `no citation recorded`. -- rule: [#designView] The Design view panel draws the realization diagram of the units that satisfy the Spec, their components and what they use, and then lists the decision records the Spec names through `decidedBy`, each with its title linked and its decision text inline. +- rule: [#designView] The Design view panel draws the requirement drawing defined by spec:consumers.spec-studio.diagram-kinds.requirement-trace in place of realization, then lists the decision records the Spec names through decidedBy with linked titles and decision text inline. The Runtime panel retains the implementation anchors' components and uses. - rule: [#runtime] The Runtime panel shows the structural neighborhood of the Spec's implementation anchors: each anchor's component with its layer and context, and the units it uses, each with its file and line; runtime observation reads not tracked. - rule: [#impact] The Impact panel draws the Spec's trace graph, one hop each way, and then lists what it rests on with each target's stated and derived rung, from recipe 21, and every Spec that rests on it to any depth, by distance, with the units, enabled verifiers and Packs each brings, from recipe 30. - rule: [#diffChangeNote] Given a base graph, a line under the header names the base and lists the Spec's changed parts, as `spec:consumers.spec-studio.diff-page` states. @@ -57,7 +59,7 @@ The panels the core does not hold, in page order. - constraintsAdditions: each constraint with its target, laid out as `spec:consumers.spec-studio.verification-panels` states. - citationList: under a cited entry, `Cited at` and each citing file and line with a copy action, the citing anchor's id as code when the citation names one. - verificationPanel: the verifier list, with run evidence given run records, then the coverage grid when the Spec owns an example space, laid out as `spec:consumers.spec-studio.verification-panels` states; the panel's element id is `panel.verification`. -- designViewPanel: the realization diagram, drawn as SVG whose nodes show their id, source file and line as the source-text rule states, then the decision records with their decision text; the panel's element id is `panel.design-view`. +- designViewPanel: The requirement drawing, with the text equivalent naming each binding's id, source file and line under the source-text rule, then the decision records and their decision text; its element id remains `panel.design-view`. - runtimePanel: the structural neighborhood of the Spec's implementation anchors, each anchor's component and the code units it uses, with file and line, then `Runtime observation: not tracked`; the panel's element id is `panel.runtime`. - evidencePanel: the evidence fields and absence labels `spec:consumers.spec-studio.verification-panels` states, with no invented build, deployment, measurement or bill-of-materials data; the panel's element id is `panel.evidence`. - impactPanel: the trace graph, then `Rests on` and `Rested on by` lists, the second grouped by distance, each row with its units, enabled verifiers and Packs; the panel's element id is `panel.impact`. diff --git a/specs/consumers/upgrade-kit.sdp.md b/specs/consumers/upgrade-kit.sdp.md index d063c7ee8..8fb8d4dd4 100644 --- a/specs/consumers/upgrade-kit.sdp.md +++ b/specs/consumers/upgrade-kit.sdp.md @@ -12,6 +12,10 @@ relations: reason: a proposed upgrade hands off on the one change-proposal shape and is applied after its check - target: spec:consumers.edit-model reason: a carrier changes only through an agent's or a person's edit, recorded by git + - target: spec:consumers.capability-map + reason: the guide teaches epic organization through the capability map + - target: spec:consumers.roadmap + reason: the guide teaches advisory priority and the roadmap memo as planning records --- # The upgrade kit shows a corpus what it could adopt @@ -30,4 +34,4 @@ An adopter learns what its corpus does not use yet only by writing its own queri ## Design - guideFile: `docs/agent-surface/upgrade-guide.md`, shipped in the package beside the recipe catalog, listed in the package's files and named by `sdp --help` among the shipped on-ramps. -- guideSections: in this order: re-pin the package and check `sdp --version`; measure the corpus with capability adoption (recipe 47); key the rules with the rule-key proposal (recipe 48), check the proposal with the proposal check (recipe 38) and apply it through an agent's edit; for every capability in recipe 47's answer, one subsection citing its `carriedBy` Spec and recipe 47 as the measurement, naming a further recipe only when the subsection teaches a distinct next action with it and otherwise sending the reader to the carrying Spec; move rules files onto shipped recipes, recipes 25, 26, 42, 50 and 51 and the `recipes` binding; read large Packs in parts and order the waiting statements, recipes 36 and 46; publish every root with `sdp generate`; and pass a recipe's subject as `--params` data, the recipe-caller guidance the README carries. +- guideSections: in this order: re-pin the package and check `sdp --version`; measure the corpus with capability adoption (recipe 47); organize features in epics, reading the capability map (recipe 52) and citing `spec:consumers.capability-map`; `Model upcoming work in the graph; keep review history in git and pull requests`, a roadmap Pack read with recipes 53 and 55 and intended design proposed as a change proposal checked by recipe 38, citing `spec:consumers.roadmap`, with the guidance `Unresolved intended design lives in open questions. Review disposition and acceptance remain in git and the pull request; neither becomes a graph field.`; key the rules with the rule-key proposal (recipe 48), check the proposal with the proposal check (recipe 38) and apply it through an agent's edit; for every capability in recipe 47's answer, one subsection citing its `carriedBy` Spec and recipe 47 as the measurement, naming a further recipe only when the subsection teaches a distinct next action with it and otherwise sending the reader to the carrying Spec; move rules files onto shipped recipes, recipes 25, 26, 42, 50 and 51 and the `recipes` binding; read large Packs in parts and order the waiting statements, recipes 36 and 46; publish every root with `sdp generate`; and pass a recipe's subject as `--params` data, the recipe-caller guidance the README carries. diff --git a/specs/decisions/architectural-annotation.sdp.md b/specs/decisions/architectural-annotation.sdp.md index 8f38fdd73..14ab55981 100644 --- a/specs/decisions/architectural-annotation.sdp.md +++ b/specs/decisions/architectural-annotation.sdp.md @@ -35,7 +35,7 @@ relations: - consequence: Architectural significance never selects a Spec kind; the Spec states the kind of truth it carries, and relationships among such Specs are the existing relations, with `dependsOn` reserved for genuine semantic need, `supersedes` for actual replacement, and scheduling-flavored edges refused (planning truths live in ruled graph homes, MD-33). - consequence: Negative architecture constraints, a layer that must not use another, stay declared intent in a Spec, never a finding of the Protocol's validators; a corpus that wants one enforced checks its declared `uses`, `layer`, and `context` in its own gate. - 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 (the binding-grain ruling, MD-37), and an anchor is never pointed at an unfinished Spec to manufacture coverage. +- consequence: The engine significance criterion requires exported public surface or cross-component reach whose contract or responsibility a designer or builder must understand, as `spec:model.structural-patterns` defines it; export status or import rank alone is insufficient, so the criterion asks for no anchor on every export and changes no validator. Code never satisfies a decision Spec directly (the binding-grain ruling, MD-37), 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 a decision that shapes it through the `satisfies` to `decidedBy` join, and a unit may name a decision it follows directly through `references`. diff --git a/specs/decisions/epics-of-any-kind.sdp.md b/specs/decisions/epics-of-any-kind.sdp.md new file mode 100644 index 000000000..391f107f3 --- /dev/null +++ b/specs/decisions/epics-of-any-kind.sdp.md @@ -0,0 +1,27 @@ +--- +id: spec:decisions.epics-of-any-kind +kind: decision +altitude: feature +readiness: ready +relations: + refines: + target: spec:consumers.capability-map + reason: settles which Specs the capability map reads as epics + dependsOn: + target: spec:decisions.one-primitive + reason: kind and altitude are independent named coordinates on the one primitive +--- +# Capability epics keep kind independent of altitude + +## Intent + +- outcome: Decide whether a capability map admits epics of every kind or only behavior epics. + +## Decision + +- context: The glossary row for capability and domain names a Capability Map over high-altitude behavior Specs, while kind and altitude are independent descriptors on the one primitive, and the hubs that already state the Protocol's capabilities include a domain model, a projection model and the rule that separates the two check families. +- decision: Any Spec stated at epic altitude is an epic, whatever its kind, and keeps its kind: the capability map reads every epic and its refinement descendants, and the glossary row broadens from high-altitude behavior Specs to epic Specs of every kind. +- rationale: Hard to reverse: corpora organize their capabilities around which Specs may be epics, and the capability map, its page and the upgrade guide read that choice. Surprising without context: the ratified glossary row limited the map to behavior Specs, so a reader needs to know why a model or a rule now heads a capability. A real trade-off: an epic of any kind keeps the Spec that already owns a capability's outcome as its head, as the one-primitive decision's named coordinates already imply, against a map whose roots differ in kind and a glossary row that changes. +- alternative: Keep behavior-only capability epics and introduce behavior wrappers over the model and rule hubs: each wrapper would restate an outcome another Spec already owns, a second owner that drifts, only so that a view can read it. +- consequence: The capability map draws epics of every kind, each labelled with its kind, and a Spec's kind never decides whether it heads a capability. +- consequence: The glossary row for capability and domain names a Capability Map over epic Specs of every kind and their refinement descendants. diff --git a/specs/decisions/roadmap-over-plans.sdp.md b/specs/decisions/roadmap-over-plans.sdp.md new file mode 100644 index 000000000..80ea0c1e2 --- /dev/null +++ b/specs/decisions/roadmap-over-plans.sdp.md @@ -0,0 +1,41 @@ +--- +id: spec:decisions.roadmap-over-plans +kind: decision +altitude: feature +readiness: ready +relations: + refines: + target: spec:decisions.planning-truths-placement + reason: narrows what a plan file may still carry once a roadmap holds priority + dependsOn: + target: spec:decisions.roadmap-priority + reason: plan files give up forward planning only once a roadmap holds priority in the graph +--- +# Plan and review files stop carrying the delivery record once the roadmap and the pull request do + +Each arc's intent already lives in its capture Specs and its Pack, its rulings in their carrying Specs, its open items in keyed questions, and its story in its pull request description. A plan file still carries the status line the handbook copies and agents read first, the arc's lanes and waves, copies of its ruling tables, and its acceptance as measured. Beside it, committed review reports and their assets carry a review's findings, what became of each, and the screenshots and prototypes the review cites, while a change proposal already gives a finding against a Spec its reviewable shape. Once a roadmap holds priority in the graph and the pull request holds the arc's story and its reviews, the forward role and the record each have one home; what remains of a plan or a review file is history, or a second copy of what the graph, git and the pull request description already hold. Retiring the files removes competing records, never durable evidence. + +## Intent + +- outcome: Decide whether the Protocol retires its numbered plan files and its committed review files, reading its next work from the roadmap and keeping each arc's record in git and its pull request, and what replaces the gate that requires a current plan. + +## Decision + +- context: Plan files grew as the Protocol's working record: every arc adds one, the handbook keeps a status line for each, and the self-hosting gate requires the highest-numbered plan to carry a status header the handbook names. Review reports grew beside them under `reviews/`, with their prompts and assets. Since the graph-first planning arc, a plan is a thin pointer to its Pack and capture Specs, and the pull request description standard already asks each description for the arc's story, its rulings as tables, its acceptance as measured and a forward section. The owner asked whether plans can be replaced by Specs: no prose in the plan, but designed, grouped and prioritized roadmaps and sets of features or epics. The owner then retired both folders as carriers of the delivery record, leaving their removal or archival to a separate cleanup change. +- decision: The Protocol reads its forward planning from its roadmap and keeps its delivery record in git and its pull requests; it adds no plan file and no review file, and `plans/` and `reviews/` stand as frozen history until a later arc removes or archives them. An agent opening the next work runs the roadmap recipe over the roadmap Pack the handbook names and follows each entry to its Packs and carrying Specs. Before an arc's first push its record is its branch, git being the event log: the arc's first commit message states the owner's working rules for the arc and the arc's Pack, each later message says what its unit did, and outstanding work is read from the arc's Pack, the backlogs and the open-question register; no record file is kept under `reviews/` or anywhere else. The arc pushes and opens its draft pull request at its first coherent commit, normally the scaffold, so the window in which only local git holds the record stays short and a session restarted on another machine sees the work. The draft pull request description then holds the arc's narrative: its story, lanes, rulings with who made them, review findings as change proposals and their memos, acceptance as measured, deferrals and refusals. A review's findings against Specs travel as a change proposal whose base is the commit the reviewer read: the branch head, or, for a proposal composed in a preview, the merge commit its stamp names, checked against that base and posted to the pull request as its memo; a finding against code or another file is a review comment on its line; a finding left open becomes an open question with its decider, provisional reading and links. Evidence no test or recipe carries is committed on the arc's branch with the commands that made it, linked from the description at that commit, and removed before the merge; the Protocol merges its pull requests with merge commits, and GitHub keeps each pull request's head ref, so links at the evidence commit keep resolving. A squash or rebase merge would need the evidence published to a durable ref first. Briefs and prompts stay in the session. The handbook names the roadmap Pack and the roadmap recipe where it named the current plan, and the self-hosting gate checks that instead of a current plan. +- rationale: Hard to reverse: the handbook, the skills, the description standard and a gate point every session at the newest plan and at committed review files, and every session's first read changes. Surprising without context: a reader finds two folders that stopped. A real trade-off: one forward-planning carrier in the graph and one record per arc in git and its pull request, with nothing to keep in step, against review findings and pre-push state living in commit messages and pull request threads rather than in files a person can search in the tree. +- alternative: Keep plan files for forward planning beside the roadmap: priority would live in two places, and the plan's copy would drift. +- alternative: Keep a thin plan file per arc for its execution record alone, with no status gate: the pull request description already carries the same record by the description standard, so the file would be a second copy that the next session must reconcile. +- alternative: Record an arc's execution in the graph, as an arc Spec or Pack fields: who ruled what and when is history that git and the description keep, and that a review happened is never a graph fact. +- alternative: A gate check that refuses a numbered plan past the last frozen one: the cleanup arc removes the folder, so the check would guard a short window, and refusing a kind of file polices the workflow, not conformance; the handbook saying the plans are frozen history is enough. +- alternative: Keep an arc's record in a file under `reviews/` until its first push, then only its link: the file repeats what the commits already say, outlives the arc as a stub, and keeps `reviews/` growing after its retirement. +- alternative: Keep review reports as committed files beside the change proposal: the proposal is the reviewable shape, an applied edit is git history and an open finding is an open question, so a report file repeats all three and goes stale with the next edit. +- alternative: Keep evidence no test or recipe carries in a committed evidence folder on the main line: the folder becomes a new store that grows with every arc, while a link pinned to the arc's commit already lasts after the merge. +- alternative: Link only the latest preview or an expiring CI artifact as evidence: a later run replaces what a historical description cites. +- consequence: The handbook drops its stack of plan statuses for a short pointer to the roadmap and its recipe. The self-hosting gate's current-record check refuses a handbook that does not name the roadmap Pack and recipe, and a roadmap Pack manifest that is missing or does not state priority order; its frozen historical checks, its green-gate legs and its ban on plan status in the registry and the glossary stay until the cleanup arc removes the folders they read. It adds no check of plan numbers, roadmap content, work order, freshness or review disposition. +- consequence: The graph-first planning Spec's plan-file rule narrows to a plan document a corpus chooses to keep, which points to its Packs and carrying Specs and confers nothing; an adopter keeps whatever planning documents it wants, and a roadmap is open to it as to the Protocol. +- consequence: The description standard renders its forward section with the roadmap memo; the pull request that completes an entry's selected work removes it from the roadmap in the same change, and entries join or move only on the owner's word. +- consequence: The owner's instruction that opens an arc authorizes commits on its branch, as a plan's commit strategy did, and its push; this record convention creates no authority of its own. The arc's first commit message records the owner's working rules and the draft description repeats them. A session that must stop with no clean boundary commits its work in progress on the arc's branch with a message saying what is incomplete, rather than accounting for it in a file; a session resuming without its predecessor reads the branch, its commits, the draft pull request and the graph, and never claims git kept what was never committed. +- consequence: The proposal schema still scopes Specs only: a finding that changes no Spec takes no invented Spec scope, and a review that proposes no Spec edit needs no empty proposal. An accepted change is an ordinary carrier edit, and whether a change was accepted stays in the pull request's discussion and git, never in the proposal or the graph. +- consequence: Tests and recipes carry repeatable evidence. A preview is the current human view of a branch, never an immutable acceptance record, so the description names the commit it measured and the commands that re-derive each number. +- consequence: The plan and review files stay as they are until a dedicated cleanup arc removes or archives them and updates the historical checks and links that read them; until then nothing is added to either, and a file in either changes only to correct the record it keeps. This decision sets no record convention for an adopter. diff --git a/specs/decisions/roadmap-priority.sdp.md b/specs/decisions/roadmap-priority.sdp.md new file mode 100644 index 000000000..02319d374 --- /dev/null +++ b/specs/decisions/roadmap-priority.sdp.md @@ -0,0 +1,39 @@ +--- +id: spec:decisions.roadmap-priority +kind: decision +altitude: feature +readiness: ready +relations: + refines: + - target: spec:decisions.planning-truths-placement + reason: narrows the refusal of recorded work order for one case, the advisory priority of a roadmap + - target: spec:decisions.pack-markdown-carrier + reason: opens the closed Pack manifest envelope to one optional field, `order` +--- +# A roadmap states advisory priority in the graph + +Dependency order already has a graph home: `dependsOn`, which the statement queue reads to order the statements a person can make. Preference has none: which body of work the owner wants next when nothing forces one before another. It lives in prose, in each pull request's forward section and in the plan files, and the graph reads neither. The roadmap views waited for a named reader; the owner, choosing what the next pull requests take up, is that reader. A Pack already keeps its members in authored order and states no truth of its own, so a Pack whose manifest says its order is a priority holds the preference with one optional field and no new relation. + +## Intent + +- outcome: Decide whether and how the graph may hold an advisory priority over upcoming work, a roadmap, without dates, without a new relation, and without gating or sequencing any work. + +## Decision + +- context: The planning truths decision gives each planning truth a ruled graph home and keeps recorded work order out of the graph, so dependency rides `dependsOn` and everything else about order stays in plan files and pull request descriptions. The owner now reads and edits that order and asks for it in the graph: what to design next and a prioritized list of what to build, tied to the pull requests that carry it out. A Pack keeps its members in authored order and states no system truth; the Pack manifest's envelope is closed to `id`, `specs` and `modelRefs`. +- decision: A roadmap is a Pack whose manifest states `order: priority`; its members' authored order is the corpus's advisory priority, first entry first. `order` is an optional manifest field whose one lawful value is `priority`, and a Pack without it keeps the reading order every Pack has. Priority is preference among bodies of work and nothing more: it carries no date, no dependency, no status and no gate, and no check, recipe or page treats a place on a roadmap as permission, obligation or sequence. Everything a roadmap shows about an entry, what it still needs and what holds it, is read from the graph each time. A corpus may keep several roadmaps, and a Spec may be listed on any number of them. +- rationale: Hard to reverse: the manifest envelope gains a field that both manifest carriers, the graph's Pack node and the reader carry, and corpora author roadmaps against it. Surprising without context: the planning truths decision kept recorded work order out of the graph, so a reader needs to know why one order is now graph data and why it still sequences nothing. A real trade-off: a priority held in the graph is checked where membership is checked, so every entry resolves; it shows in the design diff, as an added, removed or reordered member and as a Pack that becomes or stops being a roadmap; and it reaches the Studio, the vault and a pull request's forward section with no extra input. It costs one optional field and the risk that an order reads as a schedule, which this decision bounds by giving the order no consequence. +- rationale: A Pack is the right home because a roadmap says nothing about the system. It is an authored grouping, as a Pack is, whose members already carry an authored order; the field states what that order means and leaves the rest of the Pack law unchanged. +- alternative: A Pack read as a roadmap by a naming convention, such as a reserved id: the Protocol reads no meaning from an id anywhere else, a reserved name would collide with a corpus's own Packs, and nothing in the manifest would tell its reader that the order means priority. +- alternative: Any Pack's order read as a priority, the roadmap named to each recipe and page as a parameter: the Studio takes no corpus configuration, so it could not tell a roadmap from a Pack in reading order, and a reading order would show as a priority nobody stated. +- alternative: Priority outside the graph, as corpus data a recipe reads, like a perspective's `order`: an entry that no longer resolves is caught only by a rules file the corpus writes, the design diff, the Studio, the vault and the preview workflow would each need a new input to see it, and planning would gain a second store beside the graph. A perspective suits a reader's own concern; a roadmap is one authored choice for the whole corpus. +- alternative: A new relation such as `precedes`, or a rank on each Spec: the planning truths decision refused `precedes` for its engine surface across the model, the carriers, extraction, validators and the agent surface, and a rank on the truth primitive is a status that goes stale with every edit and invites scoring. +- alternative: A boolean field such as `roadmap: true`: it names the noun but not what differs, which is the meaning of the order; `order: priority` says that directly. +- alternative: Prose, as today: the graph cannot read it, and each forward section is rewritten by hand and drifts from the Specs it names. +- consequence: The planning truths decision's refusal of recorded work order narrows for this one case: a roadmap's priority is graph structure, carried as its Pack's member order, and never workflow authority. Plan numbering, staleness and every other recorded work order stay out of the graph. +- consequence: The Pack manifest admits `order`, in the Markdown and the TypeScript manifest alike; the Pack node carries `order` when the manifest states it, after `modelRefs` in the serialized node; any other value refuses at extraction as a malformed envelope field does; and the Pack model names the priority order. These touch the Pack carrier, the Pack model and the Pack node's member-order law, each enriched in place when the field is built. +- consequence: No check reads a roadmap's content. An entry already built, an order that disagrees with dependency, an empty roadmap and several roadmaps are never findings, because a place carries no authority and staleness is never a finding. +- consequence: Where priority and dependency disagree, a roadmap view shows what an entry rests on and never reorders it; readiness statements keep the statement queue's dependency order, in a roadmap's list of what waits on a person as everywhere else. +- consequence: The design diff reports a change in what a Pack's order means apart from a change in its members' order, so a Pack that becomes or stops being a roadmap shows in a comparison even when its members stay. A base graph whose schema predates the field carries no `order` and reads as reading order, as an absent field does in every schema version; a value the head's engine does not know lies outside the common schema and is reported as a diagnostic, never compared. This touches the design diff and its Studio page when the field is built. +- consequence: A place on a roadmap confers what membership of any Pack confers, which is nothing: no readiness, no delivery fact and no gate. +- consequence: The roadmap reading, its page and its forward-section memo read a roadmap this way, under `spec:consumers.roadmap`. diff --git a/specs/extraction/build-pipeline.sdp.md b/specs/extraction/build-pipeline.sdp.md index 164529fc0..7c01f0915 100644 --- a/specs/extraction/build-pipeline.sdp.md +++ b/specs/extraction/build-pipeline.sdp.md @@ -4,7 +4,9 @@ kind: workflow altitude: feature readiness: ready relations: - refines: spec:protocol.self-hosting + refines: + target: spec:extraction.derive-graph + reason: the build orchestrates derivation and publication of the one graph dependsOn: spec:extraction.derive-graph --- # The build pipeline has one ordered flow diff --git a/specs/extraction/derive-graph.sdp.md b/specs/extraction/derive-graph.sdp.md index 7be87dda4..92d52a286 100644 --- a/specs/extraction/derive-graph.sdp.md +++ b/specs/extraction/derive-graph.sdp.md @@ -1,7 +1,7 @@ --- id: spec:extraction.derive-graph kind: behavior -altitude: feature +altitude: epic readiness: ready relations: dependsOn: @@ -11,7 +11,6 @@ relations: reason: the relations Spec owns the kind restrictions on authored relations - target: spec:model.anchors reason: the anchors Spec owns binding eligibility and structural endpoint restrictions - refines: spec:protocol.self-hosting constrainedBy: spec:extraction.determinism decidedBy: - spec:decisions.one-validation-path diff --git a/specs/extraction/determinism.sdp.md b/specs/extraction/determinism.sdp.md index d9cdc2cc6..a4de09525 100644 --- a/specs/extraction/determinism.sdp.md +++ b/specs/extraction/determinism.sdp.md @@ -4,7 +4,9 @@ kind: constraint altitude: feature readiness: ready relations: - refines: spec:protocol.self-hosting + refines: + target: spec:extraction.derive-graph + reason: deterministic output constrains the derivation capability --- # Committed source derives byte-identical output diff --git a/specs/extraction/example-runner.sdp.md b/specs/extraction/example-runner.sdp.md index 4a59b79c3..cd6a1e68d 100644 --- a/specs/extraction/example-runner.sdp.md +++ b/specs/extraction/example-runner.sdp.md @@ -4,6 +4,9 @@ kind: behavior altitude: feature readiness: ready relations: + refines: + target: spec:extraction.executable-examples + reason: step dispatch and failure rendering execute a bound example dependsOn: target: spec:extraction.executable-contracts reason: runs the step contracts the build emits diff --git a/specs/extraction/executable-contracts.sdp.md b/specs/extraction/executable-contracts.sdp.md index 55a0f8cdf..e27453207 100644 --- a/specs/extraction/executable-contracts.sdp.md +++ b/specs/extraction/executable-contracts.sdp.md @@ -4,7 +4,10 @@ kind: behavior altitude: feature readiness: ready relations: - refines: spec:extraction.build-pipeline + refines: + - spec:extraction.build-pipeline + - target: spec:extraction.executable-examples + reason: typed contracts carry the authored steps and example space into execution dependsOn: target: spec:carrier.step-keys reason: a keyed step's contract key is its step key diff --git a/specs/extraction/executable-examples.sdp.md b/specs/extraction/executable-examples.sdp.md new file mode 100644 index 000000000..f692625ce --- /dev/null +++ b/specs/extraction/executable-examples.sdp.md @@ -0,0 +1,31 @@ +--- +id: spec:extraction.executable-examples +kind: behavior +altitude: epic +readiness: ready +relations: + dependsOn: + target: spec:extraction.derive-graph + reason: executable contracts read the one graph rather than another carrier path +--- +# Bound examples execute their authored steps + +The capability connects authored intent to an example's execution. Contract generation and the runner own narrower parts of that outcome; neither alone owns it. A change to the authored example must reach its test through the generated contract. + +## Intent + +- actor: An author binding an example to a test of the intended behavior. +- problem: A separately handwritten test can drift from the Spec's own steps and expected outcome. +- outcome: Run an example against its authored intent through graph-derived contracts and report failures in the words of its steps. + +## Behavior + +- rule: [#authoredSteps] Execution reads the steps and bound values from contracts derived from the graph; it adds no second authored description of the example. +- rule: [#binding] A bound test supplies the world and step handlers, and the runner executes the contract's steps in authored order. Contract generation and execution keep their separate responsibilities. +- rule: [#failure] A failing step names the authored intent it exercised. An unspecified expected outcome remains explicit and cannot become a passing comparison by assumption. +- rule: [#facts] A verifier binding says the example has a verifier. Execution results stay outside the graph and confer no readiness or delivery fact on this epic. + +## Verification — reviewed + +- Trace a bound example from its graph steps through generated contracts to runner dispatch, then change an expected outcome and check that its test fails in the example's words. +- Read the epic's own binding facts separately from the bindings on its contract-generation and runner descendants. diff --git a/specs/extraction/pack-member-order.sdp.md b/specs/extraction/pack-member-order.sdp.md index 7e9b58272..7f0c90210 100644 --- a/specs/extraction/pack-member-order.sdp.md +++ b/specs/extraction/pack-member-order.sdp.md @@ -4,6 +4,7 @@ kind: rule altitude: story readiness: ready relations: + decidedBy: spec:decisions.roadmap-priority refines: spec:extraction.derive-graph dependsOn: spec:carrier.markdown-pack-authoring constrainedBy: spec:extraction.determinism @@ -16,7 +17,7 @@ relations: ## Rule - [#authoredMembers] A Pack node carries `members`, the manifest's `specs` list in authored order, a repeat included where the carrier admits one. The `belongsTo` edges re-express the same list, one edge per entry, so the members of a Pack node and the sources of its `belongsTo` edges are one list read twice and never disagree in a derived graph. -- `members` is always present on a Pack node, and an empty manifest list derives `members: []`. The serialized Pack node writes its fields in the order `id`, `nodeType`, `claim`, `title`, `framing`, `file`, `members`, `modelRefs`, omitting an absent optional field. +- `members` is always present on a Pack node, and an empty manifest list derives `members: []`. The serialized Pack node writes its fields in the order `id`, `nodeType`, `claim`, `title`, `framing`, `file`, `members`, `modelRefs`, `order`, omitting an absent optional field. - Nodes keep their sort by id and edges keep their sort by from, type, and to. Authored member order is content of the Pack node, as authored entry order is content of a Spec's open sections, so two derivations of one commit stay byte-identical and two manifests that list the same members in different orders derive different Pack nodes. - The reader's Pack context lists one member row per entry of `members`, in that order, and its verifier gaps follow the same order. The Design Review's Pack page renders its member table and its verifier gap list in that order. - A repeated member is kept only where its carrier admits it. TypeScript keeps repetitions in `members` and reader rows, and pack-coherence reports the duplicate `belongsTo` edge. Markdown refuses a repeated manifest member with `extract/invalid-frontmatter` and the message `duplicate entry in specs`. diff --git a/specs/extraction/schema-versioning.declared-version.sdp.md b/specs/extraction/schema-versioning.declared-version.sdp.md index d28ed55e2..b9d0a8b27 100644 --- a/specs/extraction/schema-versioning.declared-version.sdp.md +++ b/specs/extraction/schema-versioning.declared-version.sdp.md @@ -17,5 +17,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.11.0"} +Then the payload declares the schema version {schemaVersion: "0.12.0"} ``` diff --git a/specs/model/core-model.sdp.md b/specs/model/core-model.sdp.md index 3cce3d09f..32e2fccec 100644 --- a/specs/model/core-model.sdp.md +++ b/specs/model/core-model.sdp.md @@ -1,12 +1,9 @@ --- id: spec:model.core-model kind: model -altitude: feature +altitude: epic readiness: ready relations: - refines: - target: spec:protocol.self-hosting - reason: the self-hosted corpus models itself with this primitive decidedBy: - spec:decisions.one-primitive - spec:decisions.example-realization-posture diff --git a/specs/model/pack-aggregate.sdp.md b/specs/model/pack-aggregate.sdp.md index cb1f9c59d..0582cf0b0 100644 --- a/specs/model/pack-aggregate.sdp.md +++ b/specs/model/pack-aggregate.sdp.md @@ -5,7 +5,9 @@ altitude: story readiness: ready relations: refines: spec:model.core-model - decidedBy: spec:decisions.pack-reified + decidedBy: + - spec:decisions.pack-reified + - spec:decisions.roadmap-priority --- # A Pack is a truth-free review aggregate @@ -17,4 +19,5 @@ relations: - **framing** — A plain descriptive note explaining why a Pack exists; it is not Spec intent. - **membership** — A declared manifest reference that derives a belongsTo edge; a Spec may belong to many Packs. - **modelRefs** — References from a Pack to standalone model Specs that carry shared vocabulary. +- **priority order** — The authored member order of a Pack whose manifest states `order: priority`, read as its owner's advisory preference among its members; it confers nothing and sequences nothing. - **refinement** — A truth-bearing parent-child relation, distinct from the cross-cutting Pack aggregate. diff --git a/specs/model/planned-architecture.sdp.md b/specs/model/planned-architecture.sdp.md index 5c70adfb1..34b91d4f2 100644 --- a/specs/model/planned-architecture.sdp.md +++ b/specs/model/planned-architecture.sdp.md @@ -4,6 +4,9 @@ kind: model altitude: feature readiness: ready relations: + refines: + target: spec:model.core-model + reason: planned components enrich Specs before code binds them dependsOn: target: spec:model.anchors reason: a planned component is realized by a component anchor that satisfies its design diff --git a/specs/model/structural-patterns.sdp.md b/specs/model/structural-patterns.sdp.md index fb2438272..706003ae3 100644 --- a/specs/model/structural-patterns.sdp.md +++ b/specs/model/structural-patterns.sdp.md @@ -15,6 +15,6 @@ relations: ## Model -- **architecturally significant unit** — A code unit with exported public surface or cross-component reach that warrants graph-visible structural binding: component membership, a role, and `references` to the designs it answers to. An architectural dependency is declared as `uses`, normally between components; a unit's own `uses` is optional. +- **architecturally significant unit** — A code unit with exported public surface or cross-component reach whose contract or responsibility a designer or builder must understand; export status or import rank alone is insufficient. Ports, shared data shapes, pipeline stages, decision tables, catalogs, layout policies and adapters may qualify. Its anchor records component membership, a role and `references` to the designs it answers to. An architectural dependency is declared as `uses`, normally between components; a unit's own `uses` is optional. - **structural attributes** — `role` on any code anchor; `layer` and `context` on a `component:` anchor; they describe the unit's architecture and carry no readiness or delivery fact, and the census renders their taxonomy from the graph. - **accepted set** — The set of architecturally significant units in a corpus is an owner-reviewed declaration, never derived from imports or exports; anchors are curated, never a coverage quota. diff --git a/specs/observation/run-evidence.sdp.md b/specs/observation/run-evidence.sdp.md index 367f2782f..91facedc1 100644 --- a/specs/observation/run-evidence.sdp.md +++ b/specs/observation/run-evidence.sdp.md @@ -4,6 +4,9 @@ kind: behavior altitude: feature readiness: ready relations: + refines: + target: spec:consumers.projections-model + reason: run records inform a view without becoming graph facts dependsOn: - target: spec:extraction.delivery-facts reason: the view reads verifier bindings while leaving their delivery facts unchanged diff --git a/specs/observation/runtime-overlay.sdp.md b/specs/observation/runtime-overlay.sdp.md index 87181c2e5..fcfcaaeb7 100644 --- a/specs/observation/runtime-overlay.sdp.md +++ b/specs/observation/runtime-overlay.sdp.md @@ -4,7 +4,9 @@ kind: behavior altitude: feature readiness: idea relations: - refines: spec:protocol.self-hosting + refines: + target: spec:extraction.derive-graph + reason: an observed fact would be derived beside implemented and has-verifier dependsOn: spec:model.core-model --- # Runtime observations can close the liveness loop diff --git a/specs/protocol/capability-epics.sdp.md b/specs/protocol/capability-epics.sdp.md new file mode 100644 index 000000000..b6fbc4e63 --- /dev/null +++ b/specs/protocol/capability-epics.sdp.md @@ -0,0 +1,42 @@ +--- +id: spec:protocol.capability-epics +kind: behavior +altitude: feature +readiness: ready +relations: + refines: spec:protocol.self-hosting + decidedBy: spec:decisions.epics-of-any-kind + dependsOn: + - target: spec:consumers.capability-map + reason: the capability map exposes the corpus's authored refinement + - target: spec:validation.readiness-floor + reason: reparenting preserves the floor of each existing ready child +--- +# The Protocol organizes its own features in capability epics + +An epic states an enduring intended outcome. Where an existing hub owns an outcome, enlarging its altitude preserves its identity, kind, evidence and bindings. Self-hosting describes the Protocol using itself; it is not the parent of every capability offered to an adopter. + +## Intent + +- outcome: Organize the Protocol's capabilities through explicit refinement so a reader can choose a capability and inspect its design and bindings. + +## Behavior + +- rule: [#epicHome] Enrich an existing hub to epic altitude when its intended outcome already names the capability. Keep its id, kind and truth; author a new epic only when no existing Spec owns that outcome. A model or rule can be an epic without becoming behavior merely to appear in the map. +- rule: [#refinement] Each feature belongs under the epic whose intended outcome it makes more precise, directly or through intermediate Specs. Existing genuine refinement remains, including a nested epic and multiple parents. A Pack, path or dependency alone does not establish that relationship. +- rule: [#selfHosting] The self-hosting epic keeps the Protocol's own use of the carrier, structural binding, roadmap and capability organization, and the decisions about that practice and the Protocol's identity. General model, derivation, validation and projection capabilities no longer refine self-hosting merely because the corpus exercises them. +- rule: [#preserveEvidence] A re-altituded hub keeps its readiness statement when its content still supports it; reparenting confers no delivery fact and adds no anchor. + +## Design + +- epicHome: reuse an existing outcome owner so altitude changes do not duplicate intent; add an epic only for an outcome no hub already states. Design iteration has no epic: no Spec owns that outcome, its features are views and reader recipes that refine the projection and agent capabilities, and its Pack already groups them for review. +- carrierHome: the Markdown hub names the default authoring entry, while its rule puts Markdown and TypeScript on one graph path and its carrier ruling admits Gherkin. Its existing descendants refine that shared authoring outcome, so it keeps its identity and title. +- executableHome: the runner owns step dispatch and failure rendering, and contract generation owns typed contracts. Neither owns the whole outcome of executing an example against its authored intent, so a separate executable-examples epic joins those refinements. Their other genuine refinement and dependency relations remain. +- nestedEpic: the agent capability remains a refinement of the projection capability because scripted graph access is a consumer-specific view. Its descendants belong to both epic scopes, and the map reports that overlap explicitly. +- corpusChecks: corpus-selected rules refine the agent capability because their intended operation is scripting the graph; the validation command is their host, while their findings remain outside the Protocol's conformance and honesty families. A refinement states the narrower outcome and a dependency states what must hold: both relations intentionally connect the build pipeline to derivation, corpus rules to the agent capability, and architecture and external-register checks to corpus rules. + +## Verification — reviewed + +- Re-measure the corpus's refinement transitions, roots, overlapping parents and epic reach from the graph. +- Every previously ready Spec still states ready and reaches the ready floor after the edits; the drift alarm stays empty. +- Compare the capability map before and after authoring the epics, and read the same projection on an adopter with one broad epic without changing its intent. diff --git a/specs/protocol/delivery-record-cleanup.sdp.md b/specs/protocol/delivery-record-cleanup.sdp.md new file mode 100644 index 000000000..2166a8cb7 --- /dev/null +++ b/specs/protocol/delivery-record-cleanup.sdp.md @@ -0,0 +1,25 @@ +--- +id: spec:protocol.delivery-record-cleanup +kind: behavior +altitude: feature +readiness: idea +relations: + refines: + target: spec:protocol.roadmap-replaces-plans + reason: carries out the removal the roadmap and the pull request record made possible for frozen history + decidedBy: spec:decisions.roadmap-over-plans +--- +# The Protocol removes its frozen plans, review reports and superseded concept material + +Since the roadmap and each pull request carry the Protocol's delivery record, `plans/` and `reviews/` stand as frozen history, and the concept documents keep material the Specs already carry. Before they go, the durable content they still hold that no Spec, test, decision or recipe carries moves into the graph; git keeps the rest. + +## Intent + +- outcome: Remove `plans/`, `reviews/` and the superseded concept material from the tree, after moving into the graph the durable content they still hold, so the repository keeps one delivery record: the graph, git and the pull requests. +- risk: The self-hosting gate's frozen historical checks read three plan files, and the temporal guard exempts both folders, so the removal changes those checks in the same change. + +### Open questions + +- [blocking #durableContent] Which content in `plans/`, `reviews/` and the concept documents does no Spec, test, decision or recipe carry yet, and where does each piece belong? + - decider: owner + - provisional: a measured inventory by the arc that removes them, reviewed before anything is deleted diff --git a/specs/protocol/roadmap-replaces-plans.sdp.md b/specs/protocol/roadmap-replaces-plans.sdp.md new file mode 100644 index 000000000..4f0e4eba6 --- /dev/null +++ b/specs/protocol/roadmap-replaces-plans.sdp.md @@ -0,0 +1,54 @@ +--- +id: spec:protocol.roadmap-replaces-plans +kind: behavior +altitude: feature +readiness: ready +relations: + refines: spec:protocol.self-hosting + dependsOn: + - target: spec:consumers.roadmap + reason: the Protocol reads its own next work from its roadmap + - target: spec:consumers.roadmap.memo + reason: each description's forward section is the roadmap memo + - target: spec:consumers.intent-composition.proposal + reason: a review's findings against Specs travel as a checked change proposal + - target: spec:consumers.intent-composition.proposal.memo + reason: the pull request carries a review's proposal as its memo + - target: spec:observation.run-evidence + reason: an arc's acceptance reads run records as evidence from test files at their recorded commit + decidedBy: spec:decisions.roadmap-over-plans +--- +# The Protocol reads its next work from its roadmap and keeps its delivery record in git and its pull requests + +Each arc's intent already lives in its capture Specs and Pack, its rulings in their carrying Specs, and its story in its pull request description. What a plan file still carries is the status line agents read first, the lanes and waves of the arc, and copies of ruling tables; what a committed review file carries, a change proposal, its memo and git already hold. Once a roadmap holds priority, the Protocol reads its next work from the graph, keeps each arc's record in git and its pull request, with review findings and evidence beside the story, and lets the plan and review files stand as the record of how earlier arcs were run. The Protocol keeps one living roadmap: its owner selects what it lists, and the pull requests that complete that work remove it. + +## Intent + +- actor: An agent or a person opening, reviewing or resuming the Protocol's work, and the owner keeping its order. +- problem: The handbook points every session at the newest plan file and keeps a status line for each, and a gate requires both, so forward planning lives in prose beside the graph that already holds the arc's intent, and committed review files repeat what change proposals, git and the pull request already carry. +- outcome: Let an agent opening the Protocol's next work read it from the roadmap recipe, review and recover an arc from git and its pull request, and keep the plan and review files as frozen history no gate requires to grow. + +## Behavior + +- rule: [#readsRoadmap] An agent opening the Protocol's next work runs the roadmap recipe over the roadmap Pack the handbook names and follows each entry to its Packs and carrying Specs; no plan file answers for forward work. +- rule: [#oneRoadmap] The Protocol keeps one living roadmap Pack, edited in place. Its owner selects captured work for the upcoming pull requests; landing a capture does not add it. Entries leave by an ordinary manifest edit when the selected work is complete or no longer belongs among the next work, and move on the owner's word, with git as history. +- rule: [#plansFrozen] `plans/` and `reviews/` keep the plans and review records of the arcs that had them as history until a cleanup arc removes or archives them; no file is added to either, and a file in either changes only to correct the record it keeps. +- rule: [#executionRecord] Before an arc's first push its record is its branch, git being the event log: the first commit message states the owner's working rules for the arc and the arc's Pack, each later message says what its unit did, and outstanding work is read from the arc's Pack, the backlogs and the open-question register; no record file is kept under `reviews/` or anywhere else. The arc pushes and opens its draft pull request at its first coherent commit, normally the scaffold, so the window in which only local git holds the record stays short and a session restarted on another machine sees the work. The draft pull request description then holds the record: its branch and base, working rules, lanes, the rulings with who made them, review findings as change proposals and their memos, acceptance as measured, deferrals and refusals. A session resuming without its predecessor's context reads the branch, its commits, the draft pull request and the graph, and never claims git kept what was never committed. The authority to commit and push comes from the owner's instruction for the arc, never from this rule, and no check reads the record. +- rule: [#reviewFindings] A review's findings against Specs travel as one change proposal in the shape `spec:consumers.intent-composition.proposal` states, whose base is the branch-head commit the reviewer read; a proposal composed in a preview names the preview's merge commit from its stamp; the proposal check runs it against the graph at that base, a moved, unresolved or incomparable scope is read again before its change applies, and its memo, as `spec:consumers.intent-composition.proposal.memo` renders it, is posted to the pull request. A finding against code or another file is a review comment on its line, and a review that proposes no Spec edit has no proposal. The commit that applies a proposal carries its reason line and base, and whether a change was accepted stays in the pull request's discussion and git, never in the proposal or the graph. No review file is committed, and a brief or prompt stays in the session that used it. +- rule: [#openFindings] A finding left open against intended design becomes an open question on its carrying Spec with its `decider`, `provisional` reading and `links`, and a deferral is a blocking question with its re-entry `trigger`; a code defect against settled intent stays a code finding until it is repaired and never becomes an invented design question. +- rule: [#evidence] Evidence no test or recipe carries, such as a prototype of a view not yet built, an adopter measurement's raw output or a screenshot for the description, is committed on the arc's branch with the commands that made it, linked from the description at that commit, and removed before the merge; the Protocol merges its pull requests with merge commits, and GitHub keeps each pull request's head ref, so links at the evidence commit keep resolving. A squash or rebase merge would need the evidence published to a durable ref first. +- rule: [#acceptance] An arc's acceptance links its bound examples and their run evidence, and re-derives each recipe measurement at the commit the description names, with its commands, never inheriting an earlier result. A run record is evidence from its test file as `spec:observation.run-evidence` reads it, never a verdict for one verifier, and it counts as evidence for a preview only when its recorded commit is the commit the preview's stamp names; a preview stays a current view, never an acceptance record. +- rule: [#forwardSection] Each description's forward section is the roadmap memo rendered at the branch's head, with the re-entry condition, the owner's reason for each entry's place and the refused list in the author's words around it; the pull request that completes an entry's selected work removes it from the roadmap in the same change, entries join or move only on the owner's word, and nothing checks either. +- rule: [#handbookPointer] The handbook names the roadmap Pack and the roadmap recipe where it named the current plan, and keeps no status line for any plan. +- rule: [#gateRecord] The self-hosting gate's current-record check refuses a handbook that does not name the roadmap Pack or the roadmap recipe, and a generated graph without a Pack node `pack:roadmap` whose `order` is `priority`; it keeps the frozen historical checks, the documented green-gate legs, and the ban on plan status in the decision registry and the glossary until the cleanup arc removes the folders they read. It checks no plan number, roadmap content, freshness, work order or review disposition. + +## Design + +- roadmapPack: `pack:roadmap`, carried by `specs/roadmap.pack.sdp.md`. +- handbookTokens: the gate reads the handbook for the strings `pack:roadmap` and `recipe 53`, and the generated graph at `generated/graph.json` for a Pack node `pack:roadmap` whose `order` is `"priority"`. +- gateMessages: `AGENTS.md — the handbook does not name the roadmap Pack`, `AGENTS.md — the handbook does not name the roadmap recipe`, `generated/graph.json — the roadmap Pack pack:roadmap is missing`, `generated/graph.json — the roadmap Pack pack:roadmap does not state order: priority`, and `generated/graph.json — the generated graph is missing or unreadable`. + +## Verification — reviewed + +- Resume an arc from its branch and draft pull request alone, reading the tree, its commits and the graph, and name any context that lived only in the earlier session. +- Run the gate with a handbook that does not name the roadmap Pack and with a roadmap manifest that does not state `order: priority`, and see each refused; an empty roadmap, a built entry and an order contrary to dependency add no finding. diff --git a/specs/protocol/role-vocabulary.sdp.md b/specs/protocol/role-vocabulary.sdp.md new file mode 100644 index 000000000..80511d02f --- /dev/null +++ b/specs/protocol/role-vocabulary.sdp.md @@ -0,0 +1,46 @@ +--- +id: spec:protocol.role-vocabulary +kind: model +altitude: feature +readiness: ready +relations: + refines: + target: spec:protocol.structural-self-binding + reason: names what each role value the engine's significant units carry means + decidedBy: spec:decisions.architectural-annotation +--- +# The engine's architectural roles each name one pattern + +Authors choose a role by the architectural responsibility a unit owns. The same responsibility uses the same word across the engine. These definitions are this corpus's design vocabulary, not a Protocol enum or a registry a validator checks. An adopter may use other roles for its own architecture. + +## Intent + +- outcome: Define each architectural role the engine's anchors use, so the accepted set of significant units reads as a curated register of the engine's patterns and abstractions. + +## Model + +- **adapter** — [#adapter] Connects an external tool or runtime to an engine contract, such as a test runner or a TypeScript import reader. +- **codec** — [#codec] Parses, normalizes or encodes a representation while preserving the meaning owned by its contract, such as ids, carrier text, parameter input or source links. +- **contract** — [#contract] Owns a shared interface, data shape, closed vocabulary or public boundary that independently developed callers and providers must agree on. +- **decider** — [#decider] Computes a semantic answer from explicit inputs without reading or writing external state, such as delivery facts or example eligibility. +- **extractor** — [#extractor] Reads authored carriers or source bindings, or composes their reified results, into the graph and its located evidence. +- **interaction** — [#interaction] Adds browser controls and feedback to build-rendered content without owning its graph joins or layout. This is a corpus role for optional element behavior. +- **projection** — [#projection] Derives read-only presentation or generated contract content from the graph, the reader or an explicitly supplied answer. A page fragment and a complete page use the same role when both own presentation. +- **publisher** — [#publisher] Writes or synchronizes an owned artifact set, including its path checks, replacement, recovery and clean comparison. +- **index** — [#index] Builds a disposable lookup structure over the graph for the units that query it; it holds no authored state and is never a second read model. +- **reader** — [#reader] Answers queries over graph content, resolving or joining recorded identities and bindings without reading source files. +- **service** — [#service] Coordinates a use case or pipeline across collaborators, including execution and failure handling, while each collaborator owns its own contract. +- **validator** — [#validator] Evaluates a stated contract and reports its failures, including readiness clauses, import fidelity and corpus-selected checks. A role does not determine a Protocol validator family. + +## Design + +- roleChoice: Use the dominant responsibility of the anchored unit. A catalog of shared shapes is a contract, a table computing a semantic answer is a decider, and a failure-producing check is a validator. File size, caller count and naming style never select a role. +- componentRole: A component anchor's role names the dominant architectural responsibility its members serve together, read from their reviewed responsibilities rather than a count of role tokens. Omit the role when no responsibility dominates. +- presentationWord: Use projection for pure page, fragment and text-equivalent rendering. Use publisher for filesystem publication, codec for representation encoding and interaction for optional DOM behavior; a unit that presents uses projection, never renderer. +- helperWord: Choose a specific responsibility after reading the code. Graph indexes use index, concept queries use reader, parameter and path encoders use codec, browser controls use interaction and test-runtime integration uses adapter. +- freeVocabulary: These Model terms explain the engine's role values. Token grammar remains the only Protocol check on role, omission remains lawful, and this Model is not an adopter-wide allowlist; the self-hosting suite pins each accepted engine unit's role. + +## Verification — reviewed + +- Compare the graph's role taxonomy with these meanings and review every renamed unit against its implementation; keep an unfamiliar adopter role visible without reporting a Protocol finding. +- Check that pure presentation uses projection across the Markdown and Studio implementations, and that publication and optional browser interaction retain their distinct responsibilities. diff --git a/specs/protocol/significant-units-review.sdp.md b/specs/protocol/significant-units-review.sdp.md new file mode 100644 index 000000000..d4ca2f065 --- /dev/null +++ b/specs/protocol/significant-units-review.sdp.md @@ -0,0 +1,43 @@ +--- +id: spec:protocol.significant-units-review +kind: behavior +altitude: feature +readiness: ready +relations: + refines: + target: spec:protocol.structural-self-binding + reason: states what the independently reviewed accepted set must contain and exclude + dependsOn: + - spec:model.structural-patterns + - spec:protocol.role-vocabulary + decidedBy: + - spec:decisions.anchor-binding-grain + - spec:decisions.architectural-annotation +--- +# The engine's accepted set covers its significant abstractions and only those + +A designer needs to find the contracts, decisions and stages a change must preserve. The accepted set records those units on the code that owns them. Exporting a helper, appearing in an import ranking or giving a file an impact answer does not by itself earn an identity. + +## Intent + +- outcome: Keep the accepted set focused on the engine's significant contracts, decisions and stages, with meaningful roles and architectural uses, and exclude units anchored only for coverage. + +## Behavior + +- rule: [#significantAbstractions] Apply the structural-patterns criterion to the public contract or cross-component responsibility a unit owns. Ports, shared data shapes, pipeline stages, decision tables, catalogs, layout policies and adapters qualify when changing them requires a designer or builder to understand that responsibility; being exported alone is insufficient. +- rule: [#curatedBoundary] Leave forwarding barrels, empty element registrations, mechanical wrappers, formatting helpers and generated files unanchored unless they own an independent contract or architectural responsibility. A coverage target, test expectation, file count or desired impact answer never establishes significance. +- rule: [#dependencyEvidence] Declare a component uses for a value or type import the importing file itself makes from another component's file. An import made only to author anchors, and a pair the imports adapter reports only through a re-export the importer does not use, confer no edge. Before calling a declared edge stale, inspect input omissions and every file the comparison leaves unassigned. Import evidence proposes an edit and never authors an edge. +- rule: [#rosterOracle] The self-hosting suite pins the reviewed component identities, unit ownership, roles, uses and deliberate membership exceptions independently of extraction. It compares the graph with those declarations and rejects unrostered membership. It never derives its expected roster from paths, exports, imports or the graph under test. +- rule: [#acceptedChanges] The accepted set is the annotation review's change table as the owner accepted it: its additions, merges, drop, role changes and component uses, each realization claim it names kept. +- rule: [#decoderBoundary] The example-space decoder joins the reader component, leaving notation a leaf codec, only when the architecture crossings recipe and the self-binding tests show that the move adds no layer crossing; otherwise it stays in notation, its real imports are declared, and the uses cycle they form stays lawful data. +- rule: [#roleMeaning] Read role meanings from the engine's role-vocabulary Model when reviewing its anchors. The vocabulary guides authors and reviewers; the Protocol never validates membership in it. + +## Design + +- rosterSource: The self-hosting oracle holds the accepted roster: each row names an accepted unit's anchor, its component and its file, and its expectations for components, membership, uses, exceptions, coarse-grain coverage and roles sit beside it. A symbol suffix is a review locator, never extracted symbol identity. + +## Verification — reviewed + +- Compare the independently reviewed accepted roster with the source responsibilities it names, including dependencies that an imports comparison cannot assign to components. +- Check that every proposed merge or drop preserves the whole-Spec realization set it claims to preserve and that every architectural uses pair has supporting value or type imports beyond anchor authoring. +- Compare the extracted graph with the independently reviewed oracle roster after anchor changes; exercise a missing accepted member and an extra unrostered membership to show that the roster comparison detects both. diff --git a/specs/protocol/structural-self-binding.sdp.md b/specs/protocol/structural-self-binding.sdp.md index 58d0a9f05..5d84513f7 100644 --- a/specs/protocol/structural-self-binding.sdp.md +++ b/specs/protocol/structural-self-binding.sdp.md @@ -20,7 +20,7 @@ relations: ## Behavior -- rule: [#criterion] The engine applies the structural-patterns model's significance criterion: exported public surface or cross-component reach. +- rule: [#criterion] The engine applies the structural-patterns model's significance criterion: exported public surface or cross-component reach whose contract or responsibility a designer or builder must understand; export status or import rank alone is insufficient. - rule: [#membership] Every architecturally significant engine unit carries component membership through a realizing anchor; a helper with no realization target of its own is covered through the nearest realization anchor that consumes it. - rule: [#architecturalUses] An architectural dependency of the engine is declared as `uses`, normally from one engine component to another; a unit's own `uses` is optional. - rule: [#componentUses] A component-level `uses` declaration tracks real value or type imports from another component's source files; imports made only to author anchors through the stable-id or anchor-builder modules confer no edge. diff --git a/specs/roadmap.pack.sdp.md b/specs/roadmap.pack.sdp.md new file mode 100644 index 000000000..ed575dd7d --- /dev/null +++ b/specs/roadmap.pack.sdp.md @@ -0,0 +1,14 @@ +--- +id: pack:roadmap +order: priority +specs: + - spec:protocol.delivery-record-cleanup + - spec:consumers.spec-studio.cited-line-text + - spec:consumers.spec-studio.register-input + - spec:consumers.edit-model +--- +# The Protocol's roadmap + +The Protocol's next work, first entry first. The order is a preference: it dates, gates and sequences nothing, and what each entry still needs is read from the graph each time. + +First the cleanup that removes the frozen plans, review reports and superseded concept material, as the roadmap and the pull request record make possible. Then the two steps a reader of the Studio's pages could not take, the words of a test line that checks a declaration and an outside register's entries, and then the composed-intent model the build backlog holds. What the first adopter's mass update asks for joins when the owner selects it; a capture landing adds nothing here. An entry leaves by an edit to this manifest once its selected work is complete or no longer belongs among the next work. diff --git a/specs/roadmaps-and-slices.pack.sdp.md b/specs/roadmaps-and-slices.pack.sdp.md new file mode 100644 index 000000000..adfd67946 --- /dev/null +++ b/specs/roadmaps-and-slices.pack.sdp.md @@ -0,0 +1,56 @@ +--- +id: pack:roadmaps-and-slices-v1 +specs: + - spec:protocol.significant-units-review + - spec:protocol.role-vocabulary + - spec:consumers.spec-studio.lenses.architecture.unit-register + - spec:consumers.capability-map + - spec:decisions.epics-of-any-kind + - spec:protocol.capability-epics + - spec:extraction.executable-examples + - spec:consumers.spec-studio.capability-map-page + - spec:consumers.spec-studio.diagram-kinds + - spec:consumers.spec-studio.diagram-kinds.legibility + - spec:consumers.spec-studio.diagram-kinds.edge-routing + - spec:consumers.spec-studio.diagram-kinds.mermaid-source + - spec:consumers.spec-studio.diagram-kinds.dependency + - spec:consumers.spec-studio.diagram-kinds.capability-tree + - spec:consumers.spec-studio.diagram-kinds.scoped-architecture + - spec:consumers.spec-studio.diagram-kinds.maturity-picture + - spec:consumers.spec-studio.diagram-kinds.requirement-trace + - spec:consumers.design-slice + - spec:consumers.design-slice.outside-components + - spec:consumers.design-slice.chain + - spec:consumers.design-slice.decision-backlink + - spec:consumers.design-slice.dependency-cycle + - spec:consumers.design-slice.folded-example + - spec:consumers.design-slice.independent-example + - spec:consumers.design-slice.component-absent + - spec:consumers.design-slice.component-unrelated + - spec:consumers.design-slice.component-matching + - spec:consumers.design-slice.component-differing + - spec:consumers.design-slice.component-unreadable + - spec:consumers.design-slice.outside-satisfied + - spec:consumers.design-slice.outside-unbound + - spec:consumers.design-slice.outside-holding + - spec:consumers.spec-studio.slice-page + - spec:decisions.roadmap-priority + - spec:consumers.roadmap + - spec:consumers.roadmap.built-work + - spec:consumers.roadmap.buildable-part + - spec:consumers.roadmap.statement-part + - spec:consumers.roadmap.design-part + - spec:consumers.roadmap.drifting-part + - spec:consumers.roadmap.missing-evidence + - spec:consumers.roadmap.blocking-part + - spec:consumers.roadmap.outside-parent + - spec:consumers.roadmap.statement-order + - spec:consumers.spec-studio.roadmap-page + - spec:consumers.roadmap.memo + - spec:decisions.roadmap-over-plans + - spec:protocol.roadmap-replaces-plans + - spec:consumers.projections-model.roadmap-views +--- +# Roadmaps, capability maps and design slices + +How a corpus reads where it stands and what comes next, drawn from the graph. First the architecture the views stand on: a review of which engine units are significant enough to anchor, and the meaning of each role they carry. Then the capability map, epics with their features and stories, and the Protocol's own capabilities stated as epics. Then the diagrams the graph has data for, and the design slice, a Pack read as one design with the architecture it touches. Last the roadmap: the decision that lets the graph hold an advisory priority, the roadmap reading and its page and memo, and the decision that lets the roadmap replace the Protocol's numbered plans. diff --git a/specs/self-hosting.pack.sdp.md b/specs/self-hosting.pack.sdp.md index 202c85e49..446395e45 100644 --- a/specs/self-hosting.pack.sdp.md +++ b/specs/self-hosting.pack.sdp.md @@ -105,6 +105,7 @@ specs: - spec:extraction.regenerability - spec:extraction.schema-versioning - spec:extraction.schema-versioning.declared-version + - spec:extraction.executable-examples - spec:extraction.executable-contracts - spec:extraction.executable-contracts.concreteness-refusal - spec:extraction.executable-contracts.multi-entry-example @@ -276,6 +277,7 @@ specs: - spec:consumers.spec-studio.navigation - spec:consumers.spec-studio.lenses.packs - spec:consumers.spec-studio.lenses.architecture + - spec:consumers.spec-studio.lenses.architecture.unit-register - spec:consumers.spec-studio.lenses.architecture.planned-apart - spec:consumers.spec-studio.lenses.tests - spec:consumers.spec-studio.lenses.tests.coverage-row @@ -356,12 +358,58 @@ specs: - spec:consumers.command-line.generate-check-failure - spec:consumers.command-line.generate-writes-every-root - spec:consumers.command-line.verb-help-touches-nothing + - spec:consumers.capability-map + - spec:consumers.spec-studio.capability-map-page + - spec:consumers.spec-studio.diagram-kinds + - spec:consumers.spec-studio.diagram-kinds.legibility + - spec:consumers.spec-studio.diagram-kinds.edge-routing + - spec:consumers.spec-studio.diagram-kinds.mermaid-source + - spec:consumers.spec-studio.diagram-kinds.dependency + - spec:consumers.spec-studio.diagram-kinds.capability-tree + - spec:consumers.spec-studio.diagram-kinds.scoped-architecture + - spec:consumers.spec-studio.diagram-kinds.maturity-picture + - spec:consumers.spec-studio.diagram-kinds.requirement-trace + - spec:consumers.design-slice + - spec:consumers.design-slice.outside-components + - spec:consumers.design-slice.chain + - spec:consumers.design-slice.decision-backlink + - spec:consumers.design-slice.dependency-cycle + - spec:consumers.design-slice.folded-example + - spec:consumers.design-slice.independent-example + - spec:consumers.design-slice.component-absent + - spec:consumers.design-slice.component-unrelated + - spec:consumers.design-slice.component-matching + - spec:consumers.design-slice.component-differing + - spec:consumers.design-slice.component-unreadable + - spec:consumers.design-slice.outside-satisfied + - spec:consumers.design-slice.outside-unbound + - spec:consumers.design-slice.outside-holding + - spec:consumers.spec-studio.slice-page + - spec:consumers.roadmap + - spec:consumers.roadmap.built-work + - spec:consumers.roadmap.buildable-part + - spec:consumers.roadmap.statement-part + - spec:consumers.roadmap.design-part + - spec:consumers.roadmap.drifting-part + - spec:consumers.roadmap.missing-evidence + - spec:consumers.roadmap.blocking-part + - spec:consumers.roadmap.outside-parent + - spec:consumers.roadmap.statement-order + - spec:consumers.spec-studio.roadmap-page + - spec:consumers.roadmap.memo + - spec:consumers.spec-studio.cited-line-text + - spec:consumers.spec-studio.register-input - spec:observation.runtime-overlay - spec:observation.run-evidence - spec:observation.run-evidence.absolute-path-joins - spec:observation.run-evidence.unanchored-file-unjoined - spec:protocol.self-hosting - spec:protocol.structural-self-binding + - spec:protocol.significant-units-review + - spec:protocol.role-vocabulary + - spec:protocol.capability-epics + - spec:protocol.roadmap-replaces-plans + - spec:protocol.delivery-record-cleanup - spec:decisions.plain-language-references - spec:decisions.concept-docs-dissolve - spec:decisions.one-validation-path @@ -406,6 +454,9 @@ specs: - spec:decisions.architectural-annotation - spec:decisions.pack-design-page - spec:decisions.vault-owned-folder + - spec:decisions.roadmap-priority + - spec:decisions.roadmap-over-plans + - spec:decisions.epics-of-any-kind modelRefs: - spec:model.core-model --- diff --git a/specs/validation/architecture-constraints.sdp.md b/specs/validation/architecture-constraints.sdp.md index b8166a3aa..aac25db60 100644 --- a/specs/validation/architecture-constraints.sdp.md +++ b/specs/validation/architecture-constraints.sdp.md @@ -4,6 +4,9 @@ kind: rule altitude: feature readiness: ready relations: + refines: + target: spec:validation.corpus-rules + reason: the architecture walk is a corpus-selected rule over anchored structure dependsOn: - spec:model.anchors - spec:validation.corpus-rules diff --git a/specs/validation/corpus-rules.sdp.md b/specs/validation/corpus-rules.sdp.md index 63cddb12b..2fdf81660 100644 --- a/specs/validation/corpus-rules.sdp.md +++ b/specs/validation/corpus-rules.sdp.md @@ -4,6 +4,9 @@ kind: behavior altitude: feature readiness: ready relations: + refines: + target: spec:consumers.agent-surface + reason: a corpus scripts the graph through the evaluation sink to report its own findings dependsOn: - spec:consumers.agent-surface - target: spec:validation.diagnostic-rendering diff --git a/specs/validation/duplicate-ids.sdp.md b/specs/validation/duplicate-ids.sdp.md index 4166204f2..8ba03ea49 100644 --- a/specs/validation/duplicate-ids.sdp.md +++ b/specs/validation/duplicate-ids.sdp.md @@ -4,7 +4,9 @@ kind: behavior altitude: feature readiness: ready relations: - refines: spec:protocol.self-hosting + refines: + target: spec:validation.two-check-families + reason: duplicate identity is a conformance refusal dependsOn: target: spec:carrier.markdown-parser reason: duplicate exclusion includes the default Markdown carrier diff --git a/specs/validation/external-registers.sdp.md b/specs/validation/external-registers.sdp.md index b9d6b3a76..e77946e76 100644 --- a/specs/validation/external-registers.sdp.md +++ b/specs/validation/external-registers.sdp.md @@ -4,6 +4,9 @@ kind: behavior altitude: story readiness: ready relations: + refines: + target: spec:validation.corpus-rules + reason: external-register checks run as corpus-selected rules dependsOn: - target: spec:validation.corpus-rules reason: the recipe answers with findings and runs as a rules file diff --git a/specs/validation/readiness-floor.sdp.md b/specs/validation/readiness-floor.sdp.md index 20527d80e..88614964d 100644 --- a/specs/validation/readiness-floor.sdp.md +++ b/specs/validation/readiness-floor.sdp.md @@ -4,7 +4,9 @@ kind: rule altitude: feature readiness: ready relations: - refines: spec:protocol.self-hosting + refines: + target: spec:validation.two-check-families + reason: the readiness floor checks honesty of the stated rung dependsOn: target: spec:model.core-model reason: the floor reads the Protocol descriptor model diff --git a/specs/validation/two-check-families.sdp.md b/specs/validation/two-check-families.sdp.md index 06fab5b75..55916f8fc 100644 --- a/specs/validation/two-check-families.sdp.md +++ b/specs/validation/two-check-families.sdp.md @@ -1,10 +1,9 @@ --- id: spec:validation.two-check-families kind: rule -altitude: feature +altitude: epic readiness: ready relations: - refines: spec:protocol.self-hosting decidedBy: spec:decisions.one-validation-path --- # Validation separates well-formedness from non-pretending diff --git a/src/adapters/vitest.ts b/src/adapters/vitest.ts index dcdbf0c1c..bd1f75d98 100644 --- a/src/adapters/vitest.ts +++ b/src/adapters/vitest.ts @@ -15,18 +15,24 @@ import { codeAnchor } from "../model/code-anchor.js"; * framework-free. This subpath needs Vitest 2 or later installed beside the package; the runner * core and other entry points do not load it. */ +/** + * Start with bindExample here for Vitest registration and fresh-world isolation. + * Read typescript-imports.ts for selected roots and barrel reach in the external + * curation input; that inferred input does not author structural bindings. + */ const adaptersComponentAnchor = codeAnchor({ id: codeAnchorId("component:protocol.adapters"), - label: "Protocol adapters: Vitest examples and TypeScript file imports", + label: "external runtime and source adapters", satisfies: ref("spec:extraction.example-runner"), uses: [componentAnchorId("component:protocol.runner")], layer: "adapter", context: "protocol", + role: "adapter", }); const exampleRunnerAdapterAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.example-runner-adapter"), label: "registers one test per example and owns the fresh world lifecycle", - satisfies: ref("spec:extraction.example-runner"), + satisfies: [ref("spec:extraction.example-runner"), ref("spec:extraction.executable-examples")], component: componentAnchorId("component:protocol.adapters"), uses: [codeAnchorId("impl:protocol.example-runner")], role: "adapter", diff --git a/src/cli/build-args.ts b/src/cli/build-args.ts index 79c798f83..360e92a23 100644 --- a/src/cli/build-args.ts +++ b/src/cli/build-args.ts @@ -8,6 +8,13 @@ import { InvalidExcludePathError, normalizeExcludes } from "../extract/discover. import type { CliOutput } from "./output.js"; import { writeStderr } from "./output.js"; +/** + * @sdpAnchor impl:protocol.build-args + * @sdpLabel shared build arguments and corpus-rule inputs + * @sdpSatisfies spec:consumers.command-line + * @sdpComponent component:protocol.cli + * @sdpRole codec + */ export interface BuildArgs { readonly studioInputs?: StudioInputs; readonly studioBaseFile?: string; diff --git a/src/cli/build-command.ts b/src/cli/build-command.ts index 5598d3f00..eb4471754 100644 --- a/src/cli/build-command.ts +++ b/src/cli/build-command.ts @@ -23,9 +23,15 @@ import type { BuildArgs } from "./build-args.js"; import type { CliOutput } from "./output.js"; import { errorMessage, formatFinding, writeStderr, writeStdout } from "./output.js"; +/** + * Start with runBuild here for invalidation, deterministic comparison and + * registrar confinement. Read build-args.ts for shared flags, q-command.ts for + * query input and output, corpus-rules.ts for isolated rule results, and + * import-publish.ts for exclusive staging, publication and rollback. + */ const cliComponentAnchor = codeAnchor({ id: codeAnchorId("component:protocol.cli"), - label: "Protocol CLI seam", + label: "entry points, shared build and publication", satisfies: ref("spec:extraction.build-pipeline"), uses: [ componentAnchorId("component:protocol.extract"), @@ -40,6 +46,7 @@ const cliComponentAnchor = codeAnchor({ ], layer: "edge", context: "protocol", + role: "service", }); export interface BuildHooks { @@ -241,55 +248,26 @@ function readRegistrarManifest(path: string, root: string, rootReal: string): re * @sdpRole publisher */ -const wholesaleViewBuildInvalidationAnchor = codeAnchor({ - id: codeAnchorId("impl:protocol.wholesale-view-build-invalidation"), - label: "invalidates the prior Design Review before every build attempt", - satisfies: ref("spec:consumers.wholesale-view-rewrite"), - component: componentAnchorId("component:protocol.cli"), - role: "service", -}); const buildPipelineEmitAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.build-pipeline-emit"), - label: - "runBuild stages graph, contracts and registrar outputs in temporary paths before publishing them, repeating derivation under --check-clean", - satisfies: ref("spec:extraction.build-pipeline"), - component: componentAnchorId("component:protocol.cli"), - role: "service", -}); -const determinismAnchor = codeAnchor({ - id: codeAnchorId("impl:protocol.extraction-determinism"), - label: "repeats and byte-compares graph and contract generation under --check-clean", - satisfies: ref("spec:extraction.determinism"), - component: componentAnchorId("component:protocol.cli"), - role: "service", -}); -const regenerabilityAnchor = codeAnchor({ - id: codeAnchorId("impl:protocol.regenerability"), - label: "repeats graph and contract producers for deterministic regeneration", - satisfies: ref("spec:extraction.regenerability"), + label: "stages build outputs, checks determinism and invalidates projections", + satisfies: [ + ref("spec:extraction.build-pipeline"), + ref("spec:consumers.wholesale-view-rewrite"), + ref("spec:consumers.spec-studio.publisher"), + ref("spec:extraction.determinism"), + ref("spec:extraction.regenerability"), + ], component: componentAnchorId("component:protocol.cli"), role: "service", }); -/** - * @sdpAnchor impl:protocol.studio-build-invalidation - * @sdpLabel removes the Studio root and temporary sibling before shared extraction - * @sdpSatisfies spec:consumers.spec-studio.publisher - * @sdpComponent component:protocol.cli - * @sdpRole service - */ export function runBuild( parsed: BuildArgs, output: CliOutput, command: string, hooks: BuildHooks, ): BuildOutcome { - void [ - cliComponentAnchor, - determinismAnchor, - regenerabilityAnchor, - wholesaleViewBuildInvalidationAnchor, - buildPipelineEmitAnchor, - ]; + void [cliComponentAnchor, buildPipelineEmitAnchor]; const { root: resolvedRoot, exclude, checkClean } = parsed; const extractionOptions = { root: resolvedRoot, exclude }; const runExtract = hooks.extract ?? extract; diff --git a/src/cli/corpus-rules.ts b/src/cli/corpus-rules.ts index cb9e10875..7c733e42e 100644 --- a/src/cli/corpus-rules.ts +++ b/src/cli/corpus-rules.ts @@ -62,7 +62,15 @@ function copyFinding(value: Record<string, unknown>, rulesFile: string): CorpusF }; } -/** Runs each operator-selected body against independent copies and commits only a lawful answer. */ +/** + * Runs each operator-selected body against independent copies and commits only a lawful answer. + * + * @sdpAnchor impl:protocol.corpus-rule-runner + * @sdpLabel runs selected corpus rules and separates their failures + * @sdpSatisfies spec:validation.corpus-rules + * @sdpComponent component:protocol.cli + * @sdpRole service + */ export async function runCorpusRules( graph: GraphSchema, report: ValidationReport, diff --git a/src/cli/help.ts b/src/cli/help.ts index 05aaee96e..942c9f602 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -169,7 +169,7 @@ export const NEW_SPEC_HELP_TEXT = verbHelpText("new") ?? ""; * @sdpLabel SDP_HELP_TEXT indexes the shipped skills, recipe catalog, schema, walkthrough, glossary and specs/ by package-relative path * @sdpSatisfies spec:consumers.adopter-on-ramp, spec:consumers.command-line * @sdpComponent component:protocol.cli - * @sdpRole service + * @sdpRole projection */ export const SDP_HELP_TEXT = `sdp — Libar Software Delivery Protocol diff --git a/src/cli/import-publish.ts b/src/cli/import-publish.ts index 531f89b18..7cdc71a83 100644 --- a/src/cli/import-publish.ts +++ b/src/cli/import-publish.ts @@ -23,9 +23,9 @@ interface PreparedImport extends PlannedImport { /** * @sdpAnchor impl:protocol.sdp-import-publish * @sdpLabel stages Markdown siblings and publishes them with atomic hard links - * @sdpReferences spec:carrier.sdp-import * @sdpComponent component:protocol.cli - * @sdpRole service + * @sdpRole publisher + * @sdpSatisfies spec:carrier.sdp-import */ export function publishImports( planned: readonly PlannedImport[], diff --git a/src/cli/import-scan.ts b/src/cli/import-scan.ts index c8cd7aa22..2b4036348 100644 --- a/src/cli/import-scan.ts +++ b/src/cli/import-scan.ts @@ -62,9 +62,9 @@ function collectDirectorySources(directory: string, collected: string[]): void { /** * @sdpAnchor impl:protocol.sdp-import-scan * @sdpLabel collects .sdp.ts paths and reports scan failures - * @sdpReferences spec:carrier.sdp-import * @sdpComponent component:protocol.cli * @sdpRole service + * @sdpSatisfies spec:carrier.sdp-import */ export function scanImportPaths( paths: readonly string[], diff --git a/src/cli/output.ts b/src/cli/output.ts index 30cc1ddfa..153f5979f 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -25,7 +25,7 @@ const diagnosticCliAnchor = codeAnchor({ label: "composes structured finding fields into the one command-line diagnostic form", satisfies: ref("spec:validation.diagnostic-rendering"), component: componentAnchorId("component:protocol.cli"), - role: "renderer", + role: "codec", }); void diagnosticCliAnchor; diff --git a/src/cli/params.ts b/src/cli/params.ts index 896dd671e..96d867c32 100644 --- a/src/cli/params.ts +++ b/src/cli/params.ts @@ -118,6 +118,13 @@ function readParams( return { params: parsed as Readonly<Record<string, unknown>> }; } +/** + * @sdpAnchor impl:protocol.parameter-codec + * @sdpLabel decodes recipe parameters and refuses duplicate keys + * @sdpSatisfies spec:consumers.agent-surface.recipe-parameters + * @sdpComponent component:protocol.cli + * @sdpRole codec + */ export function mergeParams( forms: readonly string[], output: CliOutput, diff --git a/src/cli/sdp.ts b/src/cli/sdp.ts index e700fc099..2faed5395 100644 --- a/src/cli/sdp.ts +++ b/src/cli/sdp.ts @@ -104,7 +104,7 @@ export function readEngineProvenance(read: EngineFileReader = readShippedFile): * @sdpLabel prints sdp version and the commit parsed from the build-info file beside the compiled CLI * @sdpSatisfies spec:consumers.engine-provenance * @sdpComponent component:protocol.cli - * @sdpRole service + * @sdpRole projection */ /** diff --git a/src/cli/studio-inputs.ts b/src/cli/studio-inputs.ts index f4c82063f..2c4d2b665 100644 --- a/src/cli/studio-inputs.ts +++ b/src/cli/studio-inputs.ts @@ -11,6 +11,7 @@ import { writeStderr } from "./output.js"; * @sdpSatisfies spec:consumers.spec-studio.data.inputs * @sdpComponent component:protocol.cli * @sdpRole codec + * @sdpLabel reads named Studio base graphs and run inputs */ export function readStudioBase( path: string, diff --git a/src/cli/validate-view-command.ts b/src/cli/validate-view-command.ts index fb0113b7f..62c62cb63 100644 --- a/src/cli/validate-view-command.ts +++ b/src/cli/validate-view-command.ts @@ -25,7 +25,7 @@ const validateSummaryAnchor = codeAnchor({ label: "prints validate's findings and its summary line", satisfies: ref("spec:validation.diagnostic-rendering"), component: componentAnchorId("component:protocol.cli"), - role: "renderer", + role: "codec", }); void validateSummaryAnchor; diff --git a/src/codegen/contracts.ts b/src/codegen/contracts.ts index 52564af75..15af004dd 100644 --- a/src/codegen/contracts.ts +++ b/src/codegen/contracts.ts @@ -625,29 +625,40 @@ function renderRunnableRegistrar( /* ----- the stage ----- */ +/** + * Start with GeneratedContracts and generateContracts here. + * Read space and step rendering for parameter types and outcome unions, then + * registrar rendering for dispatch, suite adoption and sibling output paths. + */ const codegenComponentAnchor = codeAnchor({ id: codeAnchorId("component:protocol.codegen"), - label: "Protocol runnable code generation seam", + label: "contracts and registrar output", satisfies: ref("spec:extraction.runnable-modules"), uses: [ componentAnchorId("component:protocol.graph"), componentAnchorId("component:protocol.notation"), + componentAnchorId("component:protocol.reader"), componentAnchorId("component:protocol.validate"), ], layer: "application", context: "protocol", + role: "projection", }); const executableContractsAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.executable-contracts"), label: "derives step and example-space contracts from the graph", - satisfies: [ref("spec:extraction.executable-contracts"), ref("spec:carrier.step-keys")], + satisfies: [ + ref("spec:extraction.executable-contracts"), + ref("spec:carrier.step-keys"), + ref("spec:extraction.executable-examples"), + ], component: componentAnchorId("component:protocol.codegen"), role: "projection", }); const runnableModulesAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.runnable-modules"), label: "derives and publishes runnable registrar modules from the graph", - satisfies: ref("spec:extraction.runnable-modules"), + satisfies: [ref("spec:extraction.runnable-modules"), ref("spec:extraction.executable-examples")], component: componentAnchorId("component:protocol.codegen"), role: "projection", }); diff --git a/src/extract/derive.ts b/src/extract/derive.ts index f2ceebbed..dd37ec512 100644 --- a/src/extract/derive.ts +++ b/src/extract/derive.ts @@ -154,6 +154,7 @@ const packMemberOrderAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.pack-member-order"), label: "keeps manifest member order on the Pack node", satisfies: ref("spec:extraction.pack-member-order"), + references: [ref("spec:decisions.roadmap-priority")], component: componentAnchorId("component:protocol.extract"), role: "extractor", }); @@ -173,6 +174,7 @@ function derivePackNode(entry: ReifiedPack, memberIds: readonly string[]): PackN file: entry.file, members: [...memberIds], ...(Array.isArray(modelRefs) ? { modelRefs: modelRefs as readonly string[] } : {}), + ...(entry.data.order === "priority" ? { order: entry.data.order } : {}), }; } diff --git a/src/extract/gherkin-kind-honesty.ts b/src/extract/gherkin-kind-honesty.ts index 8b8df12dc..905d8965a 100644 --- a/src/extract/gherkin-kind-honesty.ts +++ b/src/extract/gherkin-kind-honesty.ts @@ -3,6 +3,14 @@ * Gherkin remains a lawful canonical per-ID option only for behavior and example. * The other six kinds stay Markdown because a Gherkin mapping would lie. */ +/** + * @sdpAnchor impl:protocol.gherkin-kind-policy + * @sdpLabel shared Gherkin kind-refusal and loss explanations + * @sdpSatisfies spec:carrier.gherkin-authoring, spec:consumers.gherkin-view + * @sdpReferences spec:decisions.carrier-universality + * @sdpComponent component:protocol.extract + * @sdpRole decider + */ export const GHERKIN_KIND_LIE_REASONS = { workflow: "workflow has no distinct Gherkin root and Feature mapping erases the kind", rule: "rule collides with Rule: already consumed as inline behavior.rules", diff --git a/src/extract/gherkin.ts b/src/extract/gherkin.ts index 3a599b0ee..1a54474f7 100644 --- a/src/extract/gherkin.ts +++ b/src/extract/gherkin.ts @@ -1326,6 +1326,7 @@ export const gherkinAuthoringAnchor = codeAnchor({ satisfies: ref("spec:carrier.gherkin-authoring"), component: componentAnchorId("component:protocol.extract"), role: "extractor", + uses: [codeAnchorId("impl:protocol.gherkin-kind-policy")], }); export function reifyGherkinCarrier(sourceText: string, relativePath: string): CarrierReification { diff --git a/src/extract/index.ts b/src/extract/index.ts index 31a1ec30b..e12b24a8f 100644 --- a/src/extract/index.ts +++ b/src/extract/index.ts @@ -183,9 +183,15 @@ function fileParses(program: Program, source: ParsedSourceFile, findings: Findin * adapters and file-level impact) resolve off the curated layers (`06` §2), so the first inferred * producer is the aspirational impact graph. */ +/** + * Start with extract here for discovery, stage ordering and duplicate exclusion. + * Read carrier.ts for the common reification port, then anchors.ts for binding input. + * Markdown owner mapping lives in markdown-body.ts; scalar and relation forms + * live in markdown-yaml-policy.ts. Read those policies before changing a provider. + */ const extractComponentAnchor = codeAnchor({ id: codeAnchorId("component:protocol.extract"), - label: "Protocol extraction seam", + label: "the carrier and source pipeline", satisfies: ref("spec:extraction.derive-graph"), uses: [ componentAnchorId("component:protocol.graph"), @@ -195,6 +201,7 @@ const extractComponentAnchor = codeAnchor({ ], layer: "application", context: "protocol", + role: "extractor", }); void extractComponentAnchor; @@ -204,6 +211,11 @@ export const extractAnchor = codeAnchor({ satisfies: ref("spec:extraction.derive-graph"), component: componentAnchorId("component:protocol.extract"), role: "extractor", + uses: [ + codeAnchorId("impl:protocol.discover-files"), + codeAnchorId("impl:protocol.carrier-reification"), + codeAnchorId("impl:protocol.derive-graph"), + ], }); export function extract(options: ExtractOptions): ExtractionResult { diff --git a/src/extract/markdown-body-content.ts b/src/extract/markdown-body-content.ts index bf6a453d4..3dd522fe7 100644 --- a/src/extract/markdown-body-content.ts +++ b/src/extract/markdown-body-content.ts @@ -4,6 +4,13 @@ import { parseSlots } from "../notation/slots.js"; import type { Finding } from "../validate/contracts.js"; import { addMarkdownFinding, markdownFinding } from "./markdown-support.js"; +/** + * @sdpAnchor impl:protocol.markdown-content + * @sdpLabel decodes located Markdown entries and typed fences + * @sdpSatisfies spec:carrier.markdown-body-grammar + * @sdpComponent component:protocol.extract + * @sdpRole codec + */ export interface MarkdownLine { readonly text: string; readonly line: number; diff --git a/src/extract/markdown-body.ts b/src/extract/markdown-body.ts index e10f78acd..cd9222432 100644 --- a/src/extract/markdown-body.ts +++ b/src/extract/markdown-body.ts @@ -119,8 +119,8 @@ function narrative(lines: readonly MarkdownLine[], file: string, findings: Findi const markdownBodyGrammarAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.markdown-body-grammar"), - label: "enforces the closed Markdown body grammar", - satisfies: ref("spec:carrier.markdown-body-grammar"), + label: "maps Markdown body content to typed prose owners", + satisfies: [ref("spec:carrier.markdown-body-grammar"), ref("spec:carrier.prose-ownership-rule")], component: componentAnchorId("component:protocol.extract"), role: "codec", }); diff --git a/src/extract/markdown-frontmatter.ts b/src/extract/markdown-frontmatter.ts index 17b0a38eb..17362b103 100644 --- a/src/extract/markdown-frontmatter.ts +++ b/src/extract/markdown-frontmatter.ts @@ -21,7 +21,7 @@ import { entryAddressSlotReason } from "./reify.js"; const relationTypes = new Set<string>(SPEC_RELATION_TYPES); const specEnvelopeKeys = new Set(["id", "kind", "altitude", "readiness", "relations"]); -const packEnvelopeKeys = new Set(["id", "specs", "modelRefs"]); +const packEnvelopeKeys = new Set(["id", "specs", "modelRefs", "order"]); const specKinds = new Set<string>(SPEC_KINDS); const specAltitudes = new Set<string>(SPEC_ALTITUDES); const specReadiness = new Set<string>(SPEC_READINESS); @@ -30,6 +30,13 @@ function refusal(file: string, message: string): MarkdownFrontmatterResult { return { ok: false, findings: [markdownFinding(file, 1, message)] }; } +/** + * @sdpAnchor impl:protocol.envelope-contract + * @sdpLabel parses the bounded Markdown frontmatter envelope + * @sdpSatisfies spec:carrier.envelope-contract + * @sdpComponent component:protocol.extract + * @sdpRole codec + */ export function parseMarkdownFrontmatter( sourceText: string, file: string, @@ -232,6 +239,8 @@ export function parseMarkdownFrontmatter( "readiness is not a recognized spec readiness", extractFindingIds.nonStaticEnvelope, ); + else if (name === "order" && value !== "priority") + report(findings, file, line, 'order must be "priority"'); else data[name] = value; } for (const required of requiredFields) diff --git a/src/extract/markdown-yaml-policy.ts b/src/extract/markdown-yaml-policy.ts index 637f465aa..3633f127e 100644 --- a/src/extract/markdown-yaml-policy.ts +++ b/src/extract/markdown-yaml-policy.ts @@ -30,6 +30,13 @@ const reservedEnvelopeKeys = new Set([ "models", ]); +/** + * @sdpAnchor impl:protocol.markdown-yaml-policy + * @sdpLabel bounded YAML scalars and relation-target forms + * @sdpSatisfies spec:carrier.envelope-contract, spec:carrier.markdown-pack-authoring, spec:extraction.pack-member-order + * @sdpComponent component:protocol.extract + * @sdpRole codec + */ export function isReservedEnvelopeKey(value: string): boolean { return reservedEnvelopeKeys.has(value); } diff --git a/src/extract/markdown.ts b/src/extract/markdown.ts index 7e2ff24b1..afb0caf85 100644 --- a/src/extract/markdown.ts +++ b/src/extract/markdown.ts @@ -16,14 +16,6 @@ export type { MarkdownFrontmatterResult, } from "./markdown-types.js"; -export const envelopeContractAnchor = codeAnchor({ - id: codeAnchorId("impl:protocol.envelope-contract"), - label: "parses the bounded Markdown frontmatter envelope", - satisfies: ref("spec:carrier.envelope-contract"), - component: componentAnchorId("component:protocol.extract"), - role: "codec", -}); - export function parseMarkdownFrontmatter( sourceText: string, file: string, @@ -31,14 +23,6 @@ export function parseMarkdownFrontmatter( return parseFrontmatter(sourceText, file); } -export const proseOwnershipAnchor = codeAnchor({ - id: codeAnchorId("impl:protocol.prose-ownership"), - label: "reads Markdown body content through its prose owners", - satisfies: ref("spec:carrier.prose-ownership-rule"), - component: componentAnchorId("component:protocol.extract"), - role: "codec", -}); - export function readMarkdownBody( sourceText: string, file: string, @@ -50,18 +34,10 @@ export function readMarkdownBody( return parseMarkdownBody(envelope.body, envelope.bodyBaseLine, file, kind); } -export const markdownAuthoringAnchor = codeAnchor({ - id: codeAnchorId("impl:protocol.markdown-authoring"), - label: "reifies Markdown authoring into the one carrier path", - satisfies: ref("spec:carrier.markdown-authoring"), - component: componentAnchorId("component:protocol.extract"), - role: "extractor", -}); - export const markdownParserAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.markdown-parser"), - label: "reifies the ruled Markdown parser input", - satisfies: ref("spec:carrier.markdown-parser"), + label: "reifies Markdown Specs and Packs through one carrier boundary", + satisfies: [ref("spec:carrier.markdown-parser"), ref("spec:carrier.markdown-authoring")], component: componentAnchorId("component:protocol.extract"), role: "extractor", }); diff --git a/src/extract/reify.ts b/src/extract/reify.ts index 0a847c48d..2573d1437 100644 --- a/src/extract/reify.ts +++ b/src/extract/reify.ts @@ -2022,6 +2022,27 @@ function reifyPackCall( continue; } + if (name === "order") { + const result = reifyStaticString(initializer, name); + if (!result.ok || result.value !== "priority") { + findings.push( + createExtractFinding( + extractFindingIds.invalidFrontmatter, + "error", + 'order must be the static string "priority"', + file, + property.getStartLineNumber(), + subjectId, + name, + ), + ); + envelopeOk = false; + continue; + } + data.order = result.value; + continue; + } + if (name === "title" || name === "framing") { const result = reifyStaticString(initializer, name); @@ -2068,7 +2089,7 @@ function reifyPackCall( createExtractFinding( extractFindingIds.unrecognizedProperty, "warning", - `property "${name}" is outside the pack manifest shape (id · specs · modelRefs · title · framing) and is dropped — authored content must never silently fall out of the graph (L2)`, + `property "${name}" is outside the pack manifest shape (id · specs · modelRefs · order · title · framing) and is dropped — authored content must never silently fall out of the graph (L2)`, file, property.getStartLineNumber(), subjectId, @@ -2121,6 +2142,7 @@ const staticReificationAnchor = codeAnchor({ satisfies: [ ref("spec:extraction.determinism"), ref("spec:carrier.markdown-parser"), + ref("spec:carrier.markdown-pack-authoring"), ref("spec:model.open-question-keys"), ], component: componentAnchorId("component:protocol.extract"), diff --git a/src/extract/serialize.ts b/src/extract/serialize.ts index 57fd6ca0a..9d3fe300f 100644 --- a/src/extract/serialize.ts +++ b/src/extract/serialize.ts @@ -104,9 +104,10 @@ function canonicalConstraint(constraint: ConstraintSection): Record<string, unkn const openSectionOrderAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.open-section-order"), - label: "preserves authored open-section and model-term order", + label: "preserves authored open-section and model-term order and Pack field order", satisfies: [ ref("spec:extraction.open-section-order"), + ref("spec:extraction.pack-member-order"), ref("spec:model.open-question-keys"), ref("spec:carrier.step-keys"), ref("spec:model.term-addresses"), @@ -221,6 +222,7 @@ function canonicalNode(node: GraphNode): Record<string, unknown> { file: node.file, members: [...node.members], ...(node.modelRefs === undefined ? {} : { modelRefs: node.modelRefs }), + ...(node.order === undefined ? {} : { order: node.order }), }; case "Anchor": return { diff --git a/src/graph/delivery-facts.ts b/src/graph/delivery-facts.ts index 89e2a2142..b313569bb 100644 --- a/src/graph/delivery-facts.ts +++ b/src/graph/delivery-facts.ts @@ -65,7 +65,7 @@ export function isEnabledExampleVerify( const deliveryFactsAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.delivery-facts"), label: "computes delivery facts from resolving binding edges", - satisfies: ref("spec:extraction.delivery-facts"), + satisfies: [ref("spec:extraction.delivery-facts"), ref("spec:extraction.executable-examples")], component: componentAnchorId("component:protocol.graph"), role: "decider", }); diff --git a/src/graph/entry-address.ts b/src/graph/entry-address.ts index 3b8672ac9..13165b47d 100644 --- a/src/graph/entry-address.ts +++ b/src/graph/entry-address.ts @@ -8,7 +8,7 @@ const entryAddressAnchor = codeAnchor({ label: "resolves entry addresses for prose validation and citation readers", satisfies: [ref("spec:extraction.entry-citations"), ref("spec:validation.prose-mentions")], component: componentAnchorId("component:protocol.graph"), - role: "utility", + role: "reader", }); void entryAddressAnchor; diff --git a/src/graph/schema.ts b/src/graph/schema.ts index a0b006a64..1aa5e52df 100644 --- a/src/graph/schema.ts +++ b/src/graph/schema.ts @@ -4,14 +4,19 @@ 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.11.0" as const; +export const schemaVersion = "0.12.0" as const; const schemaVersionAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.schema-version"), - label: "declares the graph schema version", - satisfies: ref("spec:extraction.schema-versioning"), + label: "versioned graph nodes, claims, edges and located evidence", + satisfies: [ + ref("spec:extraction.schema-versioning"), + ref("spec:extraction.claim-taxonomy"), + ref("spec:extraction.derive-graph"), + ], component: componentAnchorId("component:protocol.graph"), role: "contract", + references: [ref("spec:decisions.anchor-binding-grain")], }); void schemaVersionAnchor; @@ -21,9 +26,14 @@ export type GraphNodeType = (typeof graphNodeTypes)[number]; export const graphClaims = ["declared", "anchored", "inferred"] as const; export type GraphClaim = (typeof graphClaims)[number]; +/** + * Start with GraphSchema here for nodes, edge claims and located evidence. + * Read delivery-facts.ts and oracle-bindings.ts for resolving-binding decisions, + * then entry-address.ts for the address joins shared by mentions and citations. + */ const graphComponentAnchor = codeAnchor({ id: codeAnchorId("component:protocol.graph"), - label: "Protocol graph seam", + label: "the one graph and its semantic decisions", satisfies: ref("spec:extraction.claim-taxonomy"), uses: [ componentAnchorId("component:protocol.model"), @@ -31,16 +41,9 @@ const graphComponentAnchor = codeAnchor({ ], layer: "domain", context: "protocol", -}); -const graphClaimsAnchor = codeAnchor({ - id: codeAnchorId("impl:protocol.graph-claims"), - label: "declares the graph claim taxonomy", - satisfies: ref("spec:extraction.claim-taxonomy"), - component: componentAnchorId("component:protocol.graph"), role: "contract", }); void graphComponentAnchor; -void graphClaimsAnchor; export const deliveryFactNames = ["implemented", "has-verifier", "observed"] as const; export type DeliveryFactName = (typeof deliveryFactNames)[number]; @@ -75,16 +78,6 @@ export type DerivedEdgeType = (typeof derivedEdgeTypes)[number]; export const graphEdgeTypes = [...authoredEdgeTypes, ...derivedEdgeTypes] as const; export type GraphEdgeType = (typeof graphEdgeTypes)[number]; -const graphEdgeTypesAnchor = codeAnchor({ - id: codeAnchorId("impl:protocol.graph-edge-types"), - label: "declares the closed list of graph edge types", - satisfies: ref("spec:extraction.derive-graph"), - component: componentAnchorId("component:protocol.graph"), - references: [ref("spec:decisions.anchor-binding-grain")], - role: "contract", -}); -void graphEdgeTypesAnchor; - interface GraphNodeBase { readonly id: string; readonly nodeType: GraphNodeType; @@ -123,6 +116,7 @@ export interface PackNode extends GraphNodeBase { /** The manifest's `specs` list as authored, duplicates included; the `belongsTo` edges re-express it. */ readonly members: readonly string[]; readonly modelRefs?: readonly string[]; + readonly order?: "priority"; readonly file: string; } diff --git a/src/import/import.ts b/src/import/import.ts index 4b3bfc441..8c53bd697 100644 --- a/src/import/import.ts +++ b/src/import/import.ts @@ -37,16 +37,23 @@ function importFinding( }; } +/** + * Start with importTypeScriptSpec here for source acceptance and orchestration. + * Read emit-markdown.ts for section and relation serialization, then + * markdown-fidelity.ts for the reparse comparison before emission returns. + */ const importComponentAnchor = codeAnchor({ id: codeAnchorId("component:protocol.import"), - label: "Protocol import seam", + label: "canonical conversion with a fidelity boundary", satisfies: ref("spec:carrier.sdp-import"), uses: [ componentAnchorId("component:protocol.extract"), componentAnchorId("component:protocol.validate"), + componentAnchorId("component:protocol.notation"), ], layer: "application", context: "protocol", + role: "service", }); const sdpImportCoreAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.sdp-import-core"), diff --git a/src/import/markdown-fidelity.ts b/src/import/markdown-fidelity.ts index 87a7fb5ca..205ab4b4f 100644 --- a/src/import/markdown-fidelity.ts +++ b/src/import/markdown-fidelity.ts @@ -99,6 +99,13 @@ function firstDivergentPath(left: unknown, right: unknown, path = ""): string | return path.length === 0 ? "document" : path; } +/** + * @sdpAnchor impl:protocol.import-fidelity + * @sdpLabel refuses Markdown emission that changes authored meaning + * @sdpSatisfies spec:carrier.sdp-import + * @sdpComponent component:protocol.import + * @sdpRole validator + */ export function assertMarkdownEmissionFidelity(reified: ReifiedSpec, document: string): void { const result = reifyMarkdownCarrier(document, reified.file.replace(/\.sdp\.ts$/u, ".sdp.md")); const emitted = result.specs[0]; diff --git a/src/model/anchors.ts b/src/model/anchors.ts index 1ac00e276..5a9cf831e 100644 --- a/src/model/anchors.ts +++ b/src/model/anchors.ts @@ -92,12 +92,18 @@ export function specOracle(anchor: SpecOracleAnchor): SpecOracleAnchor { }; } +/** + * Start with spec.ts for the envelope and sections.ts for typed content owners. + * Read anchors.ts for both binding forms and ids.ts for accepted id text. + * Shape changes here must agree with carrier normalization and consumer contracts. + */ const modelComponentAnchor = createCodeAnchor({ id: codeAnchorId("component:protocol.model"), - label: "Protocol model seam", + label: "the authored contracts and stable ids", layer: "domain", context: "protocol", satisfies: ref("spec:model.core-model"), + role: "contract", }); const anchorModelAnchor = createCodeAnchor({ id: codeAnchorId("impl:protocol.anchor-model"), diff --git a/src/model/pack.ts b/src/model/pack.ts index ce5532631..4c27b6d9b 100644 --- a/src/model/pack.ts +++ b/src/model/pack.ts @@ -9,6 +9,7 @@ export interface Pack { readonly framing?: string; readonly specs: readonly SpecId[]; readonly modelRefs?: readonly SpecId[]; + readonly order?: "priority"; } export function pack(definition: Pack): Pack { @@ -23,6 +24,7 @@ const packAggregateAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.pack-aggregate"), label: "Pack aggregate and model references", satisfies: ref("spec:model.pack-aggregate"), + references: [ref("spec:decisions.roadmap-priority")], component: componentAnchorId("component:protocol.model"), role: "contract", }); diff --git a/src/notation/example-space.ts b/src/notation/example-space.ts index 54684c2c8..0e7c1f24e 100644 --- a/src/notation/example-space.ts +++ b/src/notation/example-space.ts @@ -474,7 +474,7 @@ function decode(graph: GraphSchema) { * @sdpAnchor impl:protocol.example-space-decoding * @sdpLabel shared example-space interpretation for contracts and reader contexts * @sdpSatisfies spec:consumers.agent-surface.example-space-coverage, spec:carrier.step-keys - * @sdpComponent component:protocol.notation + * @sdpComponent component:protocol.reader * @sdpRole codec */ export function decodeExampleSpaces(graph: GraphSchema): ReturnType<typeof decode> { diff --git a/src/notation/slots.ts b/src/notation/slots.ts index 76d79b638..bae5dbdc2 100644 --- a/src/notation/slots.ts +++ b/src/notation/slots.ts @@ -253,12 +253,18 @@ function parseGroup(group: RawGroup): SlotGroup | undefined { : { form: "bound", name, value: parsed.value, raw: group.raw }; } +/** + * Start with parseSlots here for slot groups, declared types and step skeletons. + * Read SlotGroup and SlotDeclaredType here before changing slot interpretation. + * Graph-to-vocabulary decoding in example-space.ts belongs to the reader component. + */ const notationComponentAnchor = codeAnchor({ id: codeAnchorId("component:protocol.notation"), - label: "Protocol slot notation seam", + label: "the leaf step codec", satisfies: ref("spec:carrier.slot-notation"), layer: "domain", context: "protocol", + role: "codec", }); const slotNotationAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.slot-notation"), diff --git a/src/projections/design-review-context.ts b/src/projections/design-review-context.ts index b65056826..12141de36 100644 --- a/src/projections/design-review-context.ts +++ b/src/projections/design-review-context.ts @@ -20,7 +20,7 @@ import { * @sdpLabel renders stated readiness beside the structural floor and dishonest divergence * @sdpSatisfies spec:consumers.derived-readiness-banner * @sdpComponent component:protocol.projections - * @sdpRole renderer + * @sdpRole projection */ export function renderReadiness(context: SpecContext): readonly string[] { @@ -97,7 +97,7 @@ function renderCodeUnitRow(binding: CodeUnitBinding, page: string): string { * @sdpLabel renders implementation, verifier, oracle, and observation binding language * @sdpSatisfies spec:consumers.binding-language-views * @sdpComponent component:protocol.projections - * @sdpRole renderer + * @sdpRole projection */ export function renderBindings(context: SpecContext, page: string): readonly string[] { const present = (fact: "implemented" | "has-verifier"): string => @@ -210,7 +210,7 @@ export function renderRelationsAndImpact(context: SpecContext, page: string): re * @sdpLabel renders structured finding locations in the Design Review table * @sdpSatisfies spec:validation.diagnostic-rendering * @sdpComponent component:protocol.projections - * @sdpRole renderer + * @sdpRole projection */ export function renderFindings(findings: readonly Finding[]): readonly string[] { diff --git a/src/projections/design-review-markdown.ts b/src/projections/design-review-markdown.ts index 436bde81f..63662273a 100644 --- a/src/projections/design-review-markdown.ts +++ b/src/projections/design-review-markdown.ts @@ -1,6 +1,14 @@ import { escapeRenderedField } from "./owned-prose.js"; -/** `spec:orders.create-order` → `spec/orders.create-order.md` — bijective by the id grammar (one `:`). */ +/** + * `spec:orders.create-order` → `spec/orders.create-order.md` — bijective by the id grammar (one `:`). + * + * @sdpAnchor impl:protocol.design-review-codec + * @sdpLabel shared Design Review Markdown paths and links + * @sdpSatisfies spec:consumers.design-review + * @sdpComponent component:protocol.projections + * @sdpRole codec + */ export function pagePathOf(id: string): string { const colonIndex = id.indexOf(":"); diff --git a/src/projections/design-review-pack-sections.ts b/src/projections/design-review-pack-sections.ts index 1e5da44a8..e0f1be4a2 100644 --- a/src/projections/design-review-pack-sections.ts +++ b/src/projections/design-review-pack-sections.ts @@ -24,7 +24,7 @@ import { * @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 + * @sdpRole projection */ /** A relative link from a view page to one line of a repository file. */ diff --git a/src/projections/design-review-pages.ts b/src/projections/design-review-pages.ts index 6794abbf5..7d766976a 100644 --- a/src/projections/design-review-pages.ts +++ b/src/projections/design-review-pages.ts @@ -117,14 +117,6 @@ function renderMemberRow(member: PackMemberSummary, number: number, page: string return `| ${cells.join(" | ")} |`; } -/** - * @sdpAnchor impl:protocol.binding-language-pack-table - * @sdpLabel renders Pack member implementation and verifier bindings as present or none - * @sdpSatisfies spec:consumers.binding-language-views - * @sdpComponent component:protocol.projections - * @sdpRole renderer - */ - /** * `renderPackPage` writes the header and member table, calls the section renderers in * `design-review-pack-sections.ts`, then writes the verifier-gap list and the findings @@ -132,10 +124,10 @@ function renderMemberRow(member: PackMemberSummary, number: number, page: string * * @sdpAnchor impl:protocol.pack-design-page * @sdpLabel renders the Pack page from the Pack design - * @sdpSatisfies spec:consumers.design-review.pack-design-page + * @sdpSatisfies spec:consumers.design-review.pack-design-page, spec:extraction.pack-member-order * @sdpReferences spec:decisions.pack-design-page * @sdpComponent component:protocol.projections - * @sdpRole renderer + * @sdpRole projection */ export function renderPackPage( @@ -208,10 +200,10 @@ export function renderPackPage( /** * @sdpAnchor impl:protocol.binding-language-index-table - * @sdpLabel renders index implementation and verifier bindings as present or none + * @sdpLabel renders binding presence in index and Pack tables * @sdpSatisfies spec:consumers.binding-language-views * @sdpComponent component:protocol.projections - * @sdpRole renderer + * @sdpRole projection */ export function renderIndexPage(reader: Reader, specs: readonly SpecSummary[]): DesignReviewPage { diff --git a/src/projections/design-review.ts b/src/projections/design-review.ts index 259cd7b13..955cd777f 100644 --- a/src/projections/design-review.ts +++ b/src/projections/design-review.ts @@ -27,12 +27,18 @@ export interface DesignReviewPage { } /** + * Start with studio/diagrams/model.ts and svg.ts for diagram inputs and layout. + * Read studio/data.ts for cache and browser search shapes, then recipes.ts for + * shared evaluations and pages/index.ts for page selection and Pack exports. + * Read vault/sync.ts separately for manifest guards and publication recovery. + * * @sdpAnchor component:protocol.projections - * @sdpLabel Protocol projections seam + * @sdpLabel read-only output, layout and owned publication * @sdpSatisfies spec:consumers.projections-model * @sdpUses component:protocol.reader, component:protocol.graph, component:protocol.extract, component:protocol.model, component:protocol.notation, component:protocol.validate * @sdpLayer edge * @sdpContext protocol + * @sdpRole projection */ /** diff --git a/src/projections/gherkin-view.ts b/src/projections/gherkin-view.ts index a47147950..58b99114f 100644 --- a/src/projections/gherkin-view.ts +++ b/src/projections/gherkin-view.ts @@ -22,6 +22,7 @@ export interface GherkinViewPage { * @sdpSatisfies spec:consumers.gherkin-view * @sdpComponent component:protocol.projections * @sdpRole projection + * @sdpUses impl:protocol.gherkin-kind-policy */ const CANONICAL_KINDS = new Set<SpecKind>(["behavior", "example"]); diff --git a/src/projections/studio/data.ts b/src/projections/studio/data.ts index 0ef61fb9b..30540e556 100644 --- a/src/projections/studio/data.ts +++ b/src/projections/studio/data.ts @@ -1,7 +1,7 @@ import type { StudioInputs } from "./inputs.js"; import { studioRecipeAnswers } from "./recipes.js"; import type { StudioRecipeAnswers } from "./recipes.js"; -import type { GraphNodeType } from "../../graph/schema.js"; +import type { CodeNode, GraphNodeType } from "../../graph/schema.js"; import type { PackContext, Reader, SpecContext } from "../../reader/reader.js"; import { graphReportId } from "../../validate/validators.js"; import type { Finding, ValidationReport } from "../../validate/contracts.js"; @@ -77,6 +77,7 @@ export interface StudioData { readonly findings: readonly Finding[]; readonly rail: RailValues; readonly search: SearchIndex; + readonly codeUnits: readonly CodeNode[]; } export function railValues(reader: Reader): RailValues { @@ -280,7 +281,7 @@ function specContext(reader: Reader, id: string): SpecContext { * @sdpLabel assembles reader contexts, Pack totals, rails and search records for static pages * @sdpSatisfies spec:consumers.spec-studio.data, spec:consumers.spec-studio * @sdpComponent component:protocol.projections - * @sdpRole projection + * @sdpRole contract */ export async function createStudioData( source: Reader, @@ -318,5 +319,6 @@ export async function createStudioData( findings: reader.findings(), rail: railValues(reader), search: searchIndex(reader), + codeUnits: reader.graph.nodes.filter((node): node is CodeNode => node.nodeType === "CodeNode"), }; } diff --git a/src/projections/studio/diagrams/architecture.ts b/src/projections/studio/diagrams/architecture.ts index e20883305..309561050 100644 --- a/src/projections/studio/diagrams/architecture.ts +++ b/src/projections/studio/diagrams/architecture.ts @@ -37,6 +37,7 @@ export function architectureDiagrams(input: DiagramInput): { words: `${c}: ${String(boxes.filter((b) => contextOf(b) === c).length)} components, folded`, target: `#${contextId(c)}`, folded: true, + foldedIds: boxes.filter((b) => contextOf(b) === c).map((b) => b.id), band: "Contexts", })), edges: aggregate(input.edges, ids), @@ -63,6 +64,7 @@ export function architectureDiagrams(input: DiagramInput): { target: `#${contextId(c)}`, folded: true, band: "Other contexts", + foldedIds: boxes.filter((b) => contextOf(b) === c).map((b) => b.id), })); const diagram: Diagram = { subject: contextId(context), @@ -92,6 +94,7 @@ export function architectureDiagrams(input: DiagramInput): { target: `#${layerId(l)}`, folded: true, band: "Layers", + foldedIds: own.filter((b) => layerOf(b) === l).map((b) => b.id), })), ...external, ], @@ -120,6 +123,7 @@ export function architectureDiagrams(input: DiagramInput): { target: `#${layerId(l)}`, folded: true, band: "Other layers", + foldedIds: own.filter((b) => layerOf(b) === l).map((b) => b.id), })); const outgoing = aggregate(layerEdges, mapping); const ext = external.filter((b) => outgoing.some((e) => e.from === b.id || e.to === b.id)); diff --git a/src/projections/studio/diagrams/bands.ts b/src/projections/studio/diagrams/bands.ts index 6e8fefbd5..5695497a1 100644 --- a/src/projections/studio/diagrams/bands.ts +++ b/src/projections/studio/diagrams/bands.ts @@ -1,4 +1,5 @@ import { aggregate, compare, withinBound } from "./model.js"; +import { needsFold, reducesHeight, foldHeightBudget } from "./layout.js"; import type { Diagram, DiagramBox, DiagramInput } from "./model.js"; const relations = ["refines", "dependsOn", "constrainedBy", "decidedBy", "verifies", "supersedes"]; const relationIndex = (kind: string): number => { @@ -8,7 +9,7 @@ const relationIndex = (kind: string): number => { type Rooted = DiagramInput & Pick<Diagram, "subject" | "sectionId">; /** * @sdpAnchor impl:protocol.studio-relation-diagrams - * @sdpLabel relation, member and realization bands and bounded relation folds + * @sdpLabel relation and member bands and bounded relation folds * @sdpSatisfies spec:consumers.spec-studio.diagrams * @sdpComponent component:protocol.projections * @sdpRole projection @@ -30,7 +31,13 @@ export function traceDiagram(input: Rooted): Diagram { groups.set(band, members); return { ...box, band }; }); - let drawing: Diagram = { ...input, boxes, caption: `Relations of ${input.subject}` }; + let drawing: Diagram = { + ...input, + boxes, + caption: `Relations of ${input.subject}`, + heightBudget: foldHeightBudget, + textEquivalent: input, + }; const ids = new Map(boxes.map((b) => [b.id, b.id])); const groupOrder = (band: string) => (band.startsWith("Outgoing ") ? 0 : relations.length + 1) + @@ -39,10 +46,12 @@ export function traceDiagram(input: Rooted): Diagram { ([a, aa], [b, bb]) => bb.length - aa.length || groupOrder(a) - groupOrder(b) || compare(a, b), ); for (const [band, members] of sorted) { - if (withinBound(drawing)) break; + if (!needsFold(drawing)) break; + if (members.length < 2) continue; const id = `${input.subject}.fold.${band.replaceAll(" ", ".")}`; - for (const member of members) ids.set(member.id, id); - drawing = { + const candidateIds = new Map(ids); + for (const member of members) candidateIds.set(member.id, id); + const candidate = { ...drawing, boxes: [ ...drawing.boxes.filter((b) => !members.some((m) => m.id === b.id)), @@ -51,11 +60,15 @@ export function traceDiagram(input: Rooted): Diagram { words: `${String(members.length)} ${band}, folded`, target: "#relations", folded: true, + foldedIds: members.flatMap((m) => m.foldedIds ?? [m.id]), band, }, ], - edges: aggregate(input.edges, ids), + edges: aggregate(input.edges, candidateIds), }; + if (!reducesHeight(drawing, candidate)) continue; + for (const member of members) ids.set(member.id, id); + drawing = candidate; } const order = [ ...relations.map((r) => `Outgoing ${r}`), @@ -103,14 +116,3 @@ export function memberDiagram(input: Rooted): Diagram { ), }; } -export function realizationDiagram(input: Rooted): Diagram { - return { - ...input, - caption: `Realization of ${input.subject}`, - boxes: [...input.boxes].sort( - (a, b) => - ["Satisfying units", "Components", "Uses"].indexOf(a.band ?? "") - - ["Satisfying units", "Components", "Uses"].indexOf(b.band ?? "") || compare(a.id, b.id), - ), - }; -} diff --git a/src/projections/studio/diagrams/capability.ts b/src/projections/studio/diagrams/capability.ts new file mode 100644 index 000000000..b020bc1c5 --- /dev/null +++ b/src/projections/studio/diagrams/capability.ts @@ -0,0 +1,323 @@ +import { plainTitle } from "../pages/code-spans.js"; +import type { Diagram, DiagramBox, DiagramEdge } from "./model.js"; +import { compare } from "./model.js"; +import { needsFold, reducesHeight, foldHeightBudget } from "./layout.js"; +import { escape, code } from "../pages/html.js"; + +export interface CapabilityNode { + readonly id: string; + readonly title: string | null; + readonly specKind: string; + readonly altitude: string; + readonly statedReadiness: string; + readonly floorReached: string; + readonly deliveryFacts: readonly string[]; + readonly questions: readonly { + index: number; + address: string | null; + question: string; + blocking: boolean; + }[]; + readonly parents: readonly string[]; + readonly children: readonly string[]; + readonly epicIds: readonly string[]; +} +export interface CapabilityCounts { + readonly specs: number; + readonly byKind: Readonly<Record<string, number>>; + readonly byAltitude: Readonly<Record<string, number>>; + readonly byStatedReadiness: Readonly<Record<string, number>>; + readonly byFloorReached: Readonly<Record<string, number>>; + readonly withImplementationBinding: number; + readonly withVerifierBinding: number; + readonly openQuestions: number; + readonly blockingQuestions: number; +} +export interface CapabilityMap { + readonly nodes: readonly CapabilityNode[]; + readonly edges: readonly { from: string; to: string; claim: string; reason: string | null }[]; + readonly epics: readonly { + id: string; + title: string | null; + parentEpicIds: readonly string[]; + descendantIds: readonly string[]; + rollup: CapabilityCounts; + }[]; + readonly unreached: { ids: readonly string[]; roots: readonly string[] }; + readonly cycles: readonly { members: readonly string[] }[]; + readonly damage: readonly { code: string; from: string; to: string }[]; +} +export interface CapabilityVisit { + readonly node: CapabilityNode; + readonly parent: string | null; + readonly depth: number; + readonly repeat: boolean; + readonly cycle: boolean; +} + +// Placement is a finite traversal of the recipe's ordered adjacency, not a second reach calculation. +export function capabilityWalk( + map: CapabilityMap, + roots: readonly string[], + scope?: ReadonlySet<string>, +): CapabilityVisit[] { + const nodes = new Map(map.nodes.map((n) => [n.id, n])); + const seen = new Set<string>(); + const active = new Set<string>(); + const result: CapabilityVisit[] = []; + const stack = roots + .slice() + .reverse() + .map((id) => ({ id, parent: null as string | null, depth: 0, exit: false })); + while (stack.length) { + const next = stack.pop(); + if (!next) break; + if (next.exit) { + active.delete(next.id); + continue; + } + const node = nodes.get(next.id); + if (!node || (scope && !scope.has(node.id))) continue; + const repeat = seen.has(next.id); + result.push({ + node, + parent: next.parent, + depth: next.depth, + repeat, + cycle: active.has(next.id), + }); + if (repeat) continue; + seen.add(next.id); + active.add(next.id); + stack.push({ ...next, exit: true }); + for (const id of node.children.slice().reverse()) + stack.push({ id, parent: node.id, depth: next.depth + 1, exit: false }); + } + return result; +} +export const capabilityBranchId = (subject: string, id: string): string => + `diagram.${subject}.capability.branch.${encodeURIComponent(id)}`; + +/** + * @sdpAnchor impl:protocol.studio-capability-diagrams + * @sdpLabel places refinement occurrences and bounded branches from the capability map + * @sdpSatisfies spec:consumers.spec-studio.diagram-kinds.capability-tree, spec:consumers.spec-studio.diagram-kinds + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function capabilityDiagrams( + map: CapabilityMap, + roots: readonly string[], + subject: string, + target: (id: string) => string = (id) => + `#${encodeURIComponent(capabilityBranchId(subject, id))}`, +): readonly [Diagram] { + const walk = capabilityWalk( + map, + roots, + subject === "unreached" ? new Set(map.unreached.ids) : undefined, + ); + const cycleOf = new Map( + map.cycles.flatMap((c) => c.members.map((id) => [id, c.members] as const)), + ); + let boxes: DiagramBox[] = []; + let edges: DiagramEdge[] = []; + const first = new Map<string, string>(); + const parentOccurrences = new Map<string, string>(); + const cyclePlaced = new Set<readonly string[]>(); + const depths = new Map<string, number>(); + const rootOrder = new Map<string, number>(); + let rootIndex = -1; + for (const [i, v] of walk.entries()) { + if (v.parent === null) rootIndex++; + const cycle = cycleOf.get(v.node.id); + if (cycle && cyclePlaced.has(cycle) && (!v.repeat || v.cycle)) { + const representative = cycle[0]; + const group = + representative === undefined ? undefined : parentOccurrences.get(representative); + if (group === undefined) throw new Error("A placed cycle has no occurrence"); + parentOccurrences.set(v.node.id, group); + const parent = v.parent === null ? undefined : parentOccurrences.get(v.parent); + if (parent && parent !== group) + edges.push({ from: group, to: parent, kind: "refines", claim: "declared" }); + continue; + } + const id = `${capabilityBranchId(subject, v.node.id)}${v.repeat ? `.repeat.${String(i)}` : ""}`; + const parent = v.parent === null ? undefined : parentOccurrences.get(v.parent); + const placedDepth = parent === undefined ? 0 : (depths.get(parent) ?? 0) + 1; + const placementParent = boxes.find((b) => b.id === parent)?.identity ?? v.parent; + const firstId = first.get(v.node.id); + const members = cycle ?? [v.node.id]; + boxes.push({ + id, + identity: v.node.id, + words: `${plainTitle(v.node.title ?? v.node.id)} · ${v.node.altitude}${cycle ? ` · cycle · ${String(cycle.length)} unique Specs` : ""}${v.repeat ? " · repeat" : ""}`, + title: plainTitle(v.node.title ?? v.node.id), + detail: `${v.node.altitude}${cycle ? ` · cycle · ${String(cycle.length)} unique Specs` : ""}${v.repeat ? " · repeat" : ""}`, + target: v.repeat + ? `#${encodeURIComponent(firstId ?? capabilityBranchId(subject, v.node.id))}` + : target(v.node.id), + band: + placedDepth === 0 + ? subject === "unreached" + ? "Unreached roots" + : "Roots" + : `Depth ${String(placedDepth)} · Children of ${placementParent ?? ""}`, + ...(cycle ? { foldedIds: members, cycleIds: members } : {}), + ...(parent ? { parent } : {}), + }); + depths.set(id, placedDepth); + rootOrder.set(id, rootIndex); + if (parent && parent !== id) + edges.push({ from: id, to: parent, kind: "refines", claim: "declared" }); + if (!v.repeat) { + first.set(v.node.id, id); + parentOccurrences.set(v.node.id, id); + if (cycle) { + cyclePlaced.add(cycle); + for (const member of cycle) { + first.set(member, id); + parentOccurrences.set(member, id); + } + } + } + } + // Replace the largest proper branch first. Roots and their relationship to a remainder survive. + let current = { boxes, edges }; + const fold = (parent: DiagramBox, removed: readonly DiagramBox[]) => { + const ids = new Set(removed.map((b) => b.id)); + const memberIds = [...new Set(removed.flatMap((b) => b.foldedIds ?? [b.identity ?? b.id]))]; + const id = `${parent.id}.folded`; + const remainder: DiagramBox = { + id, + identity: `branch of ${parent.identity ?? parent.id}`, + words: `folded · ${String(memberIds.length)} unique Specs`, + title: `Folded branch`, + detail: `${String(memberIds.length)} unique Specs`, + folded: true, + foldedIds: memberIds, + target: `#${encodeURIComponent(parent.id)}`, + parent: parent.id, + band: `Depth ${String((depths.get(parent.id) ?? 0) + 1)} · Children of ${parent.identity ?? parent.id}`, + }; + const at = boxes.findIndex((b) => ids.has(b.id)); + const candidateBoxes = boxes.flatMap((b, i) => + i === at ? [remainder] : ids.has(b.id) ? [] : [b], + ); + const candidateEdges = [ + ...edges.filter((e) => !ids.has(e.from) && !ids.has(e.to)), + { from: id, to: parent.id, kind: "refines", claim: "declared" }, + ]; + const candidate = { boxes: candidateBoxes, edges: candidateEdges }; + if (!reducesHeight(current, candidate)) return false; + boxes = candidateBoxes; + edges = candidateEdges; + current = candidate; + depths.set(id, (depths.get(parent.id) ?? 0) + 1); + rootOrder.set(id, rootOrder.get(parent.id) ?? 0); + return true; + }; + const descendants = (parent: DiagramBox): DiagramBox[] => { + const ids = new Set([parent.id]); + return boxes.filter((b) => { + if (!b.parent || !ids.has(b.parent)) return false; + ids.add(b.id); + return true; + }); + }; + while (needsFold(current)) { + const branches = boxes + .filter((b) => b.parent && !b.folded) + .map((parent) => ({ parent, removed: descendants(parent) })) + .filter((b) => b.removed.length > 1) + .sort( + (a, b) => + b.removed.length - a.removed.length || + (rootOrder.get(a.parent.id) ?? 0) - (rootOrder.get(b.parent.id) ?? 0) || + compare(a.parent.identity ?? a.parent.id, b.parent.identity ?? b.parent.id), + ); + if (branches.some((branch) => fold(branch.parent, branch.removed))) continue; + const siblings = boxes + .map((parent) => ({ parent, children: boxes.filter((b) => b.parent === parent.id) })) + .filter((g) => g.children.length > 1) + .sort( + (a, b) => + b.children.length - a.children.length || + boxes.indexOf(a.parent) - boxes.indexOf(b.parent), + ); + const improved = siblings.some((group) => { + const removed = new Set<DiagramBox>(); + for (const sibling of group.children.slice().reverse()) { + removed.add(sibling); + for (const child of descendants(sibling)) removed.add(child); + if ( + removed.size >= Math.max(2, boxes.length - 39) && + fold( + group.parent, + boxes.filter((b) => removed.has(b)), + ) + ) + return true; + } + return false; + }); + if (!improved) break; + } + const placementOrder = new Map(boxes.map((b, i) => [b.id, i])); + boxes.sort( + (a, b) => + (depths.get(a.id) ?? 0) - (depths.get(b.id) ?? 0) || + (placementOrder.get(a.parent ?? a.id) ?? 0) - (placementOrder.get(b.parent ?? b.id) ?? 0) || + (placementOrder.get(a.id) ?? 0) - (placementOrder.get(b.id) ?? 0), + ); + const strict = + roots.length === 1 && + !walk.some((v) => v.repeat) && + !walk.some((v) => cycleOf.has(v.node.id)) && + subject !== "unreached" && + !boxes.some((b) => b.folded); + const identities = new Set(walk.map((v) => v.node.id)); + const fallbackReasons = [ + ...(roots.length !== 1 ? ["multiple roots"] : []), + ...(walk.some((v) => v.repeat && !v.cycle) ? ["repeated parentage"] : []), + ...(walk.some((v) => cycleOf.has(v.node.id)) ? ["a cycle"] : []), + ...(subject === "unreached" ? ["an unreached band"] : []), + ...(boxes.some((b) => b.folded) ? ["a fold"] : []), + ]; + const complete = `<p>${String(identities.size)} unique Specs; ${String(boxes.length)} rendered boxes.</p><ul>${walk.map((v) => `<li>${code(v.node.id)}: ${escape(v.node.title)} · ${escape(v.node.altitude)}${v.repeat ? ` · ${v.cycle ? "cyclic link" : "repeat"}` : ""}</li>`).join("")}${map.edges + .filter((e) => identities.has(e.from) && identities.has(e.to)) + .map( + (e) => + `<li>${code(e.from)} refines ${code(e.to)} · declared${e.reason ? ` — ${escape(e.reason)}` : ""}</li>`, + ) + .join("")}${map.cycles + .filter((c) => c.members.some((id) => identities.has(id))) + .map((c) => `<li>cycle: ${c.members.map(code).join(", ")}</li>`) + .join( + "", + )}${subject === "unreached" ? `<li>Unreached ids: ${map.unreached.ids.map(code).join(", ")}</li>` : ""}</ul>`; + return [ + { + subject, + heightBudget: foldHeightBudget, + sectionId: null, + caption: "Refinement · parent to child reading order", + boxes, + edges, + boxOrder: "supplied", + mermaidKind: strict ? "mindmap" : "flowchart BT", + mermaidExplanation: `Preferred syntax: mindmap. Chosen syntax: ${strict ? "mindmap" : `flowchart BT because the drawing has ${fallbackReasons.join(", ")}`}.`, + mermaidNotes: map.edges + .filter( + (e) => + identities.has(e.from) && + identities.has(e.to) && + (cycleOf.has(e.from) || cycleOf.has(e.to)), + ) + .map((e) => `${e.from} refines ${e.to} · ${e.claim}${e.reason ? ` — ${e.reason}` : ""}`), + legend: + "Arrows: refines (declared). Later visits repeat a Spec and link to its first occurrence.", + textEquivalentHtml: complete, + }, + ]; +} diff --git a/src/projections/studio/diagrams/index.ts b/src/projections/studio/diagrams/index.ts index 398cad348..427a2fee6 100644 --- a/src/projections/studio/diagrams/index.ts +++ b/src/projections/studio/diagrams/index.ts @@ -1,4 +1,6 @@ export { architectureDiagrams } from "./architecture.js"; -export { traceDiagram, memberDiagram, realizationDiagram } from "./bands.js"; +export { traceDiagram, memberDiagram } from "./bands.js"; export { renderDiagram, labelLines } from "./svg.js"; export type { Diagram, DiagramInput, DiagramBox, DiagramEdge } from "./model.js"; + +export { dependencyDiagram, scopedArchitectureDiagrams } from "./slice.js"; diff --git a/src/projections/studio/diagrams/layout.ts b/src/projections/studio/diagrams/layout.ts new file mode 100644 index 000000000..d57f629ba --- /dev/null +++ b/src/projections/studio/diagrams/layout.ts @@ -0,0 +1,289 @@ +import { compare, contextOf, layerOf, layers, needsEdgeLabels, withinBound } from "./model.js"; +import type { Diagram, DiagramBox, DiagramEdge, DiagramInput } from "./model.js"; + +export const widthBudget = 880; +export const foldHeightBudget = 2000; +const measuredHeights = new WeakMap<DiagramInput, number>(); +export const measuredHeight = (d: DiagramInput & Partial<Diagram>): number => { + const known = measuredHeights.get(d); + if (known !== undefined) return known; + const height = layoutDiagram({ subject: "", caption: "", ...d }).height; + measuredHeights.set(d, height); + return height; +}; +export const needsFold = (d: DiagramInput & Partial<Diagram>): boolean => + !withinBound(d) || measuredHeight(d) > foldHeightBudget; +export const reducesHeight = ( + before: DiagramInput & Partial<Diagram>, + after: DiagramInput & Partial<Diagram>, +): boolean => measuredHeight(after) < measuredHeight(before); +export interface Rectangle { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} +export interface Positioned extends Rectangle { + readonly box: DiagramBox; + readonly title: string[]; + readonly identity: string[]; + readonly detail: string[]; +} +export interface Layout { + readonly boxes: Positioned[]; + readonly labels: (Rectangle & { kind: "lane" | "band"; label: string; lines: string[] })[]; + readonly width: number; + readonly height: number; + readonly packedLanes?: boolean; +} +export const snap = (n: number): number => Math.ceil(n / 8) * 8; +export function labelLines(text: string, limit = 30): string[] { + const result: string[] = []; + let chars = Array.from(text); + while (chars.length > limit) { + const prefix = chars.slice(0, limit); + let cut = prefix.lastIndexOf(" "); + if (cut < 0) cut = Math.max(...[".", ":", "-"].map((c) => prefix.lastIndexOf(c))); + const n = cut < 0 ? limit : cut + 1; + result.push(chars.slice(0, n).join("").trim()); + chars = chars.slice(n); + } + if (chars.length) result.push(chars.join("").trim()); + return result; +} +export function shortIds(ids: readonly string[]): Map<string, string> { + const parts = ids.map((id) => id.slice(id.indexOf(":") + 1).split(".")); + let shared = 0; + while ( + parts.length && + parts.every((p) => p.length > shared + 1 && p[shared] === parts[0]?.[shared]) + ) + shared++; + const result = new Map(ids.map((id, i) => [id, `…${parts[i]?.slice(shared).join(".") ?? id}`])); + const collisions = new Map<string, Set<string>>(); + for (const [id, value] of result) { + const group = collisions.get(value) ?? new Set<string>(); + group.add(id); + collisions.set(value, group); + } + for (const group of collisions.values()) + if (group.size > 1) for (const id of group) result.set(id, id); + return result; +} +export const foldedBand = (b: DiagramBox): boolean => + b.folded === true && (b.band === "Other contexts" || b.band === "Other layers"); +interface LaneEnd { + readonly id: string; + readonly side: "top" | "bottom"; + readonly port: number; + readonly offset: number; + readonly size: number; +} +// Share a horizontal lane only when the endpoint spans are disjoint, including label room. +export function edgeLanes( + boxes: readonly Positioned[], + edges: readonly DiagramEdge[], + packed = true, +) { + const byId = new Map(boxes.map((b) => [b.box.id, b])); + const ports = new Map<string, number>(); + const lanes = new Map<string, { start: number; end: number; offset: number; size: number }[]>(); + const labelled = needsEdgeLabels(edges); + const dense = + !packed || boxes.some((b) => b.box.context !== undefined || b.box.layer !== undefined); + return [...edges] + .sort( + (a, b) => + compare(a.from, b.from) || + compare(a.to, b.to) || + compare(a.kind, b.kind) || + compare(a.claim ?? "", b.claim ?? "") || + compare(a.reason ?? "", b.reason ?? ""), + ) + .map((edge) => { + const from = byId.get(edge.from), + to = byId.get(edge.to); + if (!from || !to) return { edge, a: null, b: null }; + const endpoint = (box: Positioned, side: "top" | "bottom", label: boolean): LaneEnd => { + const key = `${box.box.id}/${side}`, + port = ports.get(key) ?? 0; + ports.set(key, port + 1); + const laneKey = `${String(box.y)}/${side}`; + const spans = lanes.get(laneKey) ?? []; + const start = dense ? 0 : Math.min(from.x, to.x), + end = dense + ? Number.MAX_SAFE_INTEGER + : Math.max(from.x + from.width, to.x + to.width) + (label ? 200 : 24); + const size = label ? 48 : 8; + let offset = 0; + while ( + spans.some( + (s) => + start < s.end + 16 && + end + 16 > s.start && + offset < s.offset + s.size && + offset + size > s.offset, + ) + ) + offset += 8; + spans.push({ start, end, offset, size }); + lanes.set(laneKey, spans); + return { id: box.box.id, side, port, offset, size }; + }; + return { + edge, + a: endpoint(from, to.y > from.y ? "bottom" : "top", labelled), + b: endpoint(to, from.y >= to.y ? "bottom" : "top", false), + }; + }); +} +/** + * @sdpAnchor impl:protocol.studio-diagram-layout + * @sdpLabel title-first box placement, wrapping and band sizing for desktop and phone + * @sdpSatisfies spec:consumers.spec-studio.diagram-kinds.legibility, spec:consumers.spec-studio.diagram-kinds + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function layoutDiagram(d: Diagram, packedLanes = true): Layout { + const short = shortIds(d.boxes.map((b) => b.identity ?? b.id)); + const boxes: Positioned[] = [], + labels: Layout["labels"] = []; + const degrees = new Map<string, number>(); + for (const e of d.edges) { + degrees.set(e.from, (degrees.get(e.from) ?? 0) + 1); + degrees.set(e.to, (degrees.get(e.to) ?? 0) + 1); + } + const margin = 24; + const multipleContexts = + d.grid && new Set(d.boxes.filter((b) => !foldedBand(b)).map(contextOf)).size > 1; + const measure = (box: DiagramBox) => { + const title = labelLines(box.title ?? box.words, 28), + identity = box.hideIdentity ? [] : labelLines(short.get(box.identity ?? box.id) ?? box.id), + detail = labelLines(box.detail ?? ""); + return { + title, + identity, + detail, + width: snap( + Math.max( + 96, + ...[...title, ...identity, ...detail].map((s) => Array.from(s).length * 8 + 24), + Math.min(multipleContexts ? 264 : 832, (degrees.get(box.id) ?? 0) * 8 + 32), + ), + ), + height: snap(24 + (title.length + identity.length + detail.length) * 18), + }; + }; + const columns = d.grid + ? [...new Set(d.boxes.filter((b) => !foldedBand(b)).map(contextOf))].sort((a, b) => + a === "not stated" ? 1 : b === "not stated" ? -1 : compare(a, b), + ) + : [""]; + const bands = d.grid + ? layers.filter((l) => d.boxes.some((b) => !foldedBand(b) && layerOf(b) === l)) + : [...new Set(d.boxes.map((b) => b.band ?? "Units"))]; + const cell = (c: string, b: string) => + d.boxes + .filter((box) => + d.grid + ? !foldedBand(box) && contextOf(box) === c && layerOf(box) === b + : (box.band ?? "Units") === b, + ) + .sort((a, b) => (d.boxOrder === "supplied" || d.preserveOrder ? 0 : compare(a.id, b.id))); + const cellWidth = d.grid && columns.length > 1 ? 264 : widthBudget - 48; + const place = (items: readonly DiagramBox[], x: number, y: number, width: number) => { + let nextX = x, + nextY = y, + rowHeight = 0; + for (const box of items) { + const size = measure(box); + if (nextX > x && nextX + size.width > x + width) { + nextX = x; + nextY += rowHeight + 64; + rowHeight = 0; + } + boxes.push({ box, x: nextX, y: nextY, ...size }); + nextX += size.width + 24; + rowHeight = Math.max(rowHeight, size.height); + } + return nextY + rowHeight; + }; + const label = (kind: "lane" | "band", text: string, x: number, y: number) => { + const lines = labelLines(text, 28); + labels.push({ + kind, + label: text, + lines, + x, + y, + width: Math.max(8, ...lines.map((s) => Array.from(s).length * 8)), + height: lines.length * 18, + }); + return lines.length * 18; + }; + const laneHeight = d.grid + ? Math.max(18, ...columns.map((c) => labelLines(c, 28).length * 18)) + 32 + : 0; + let y = snap(margin + laneHeight), + maxX = margin; + columns.forEach((c, i) => { + if (d.grid) label("lane", c, margin + i * (cellWidth + 24), margin); + }); + for (const band of bands) { + const heights = columns.map((c, i) => { + const x = margin + i * (cellWidth + 24); + const h = label("band", band, x, y); + const bottom = place(cell(c, band), x, snap(y + h + 80), cellWidth); + maxX = Math.max(maxX, ...boxes.map((b) => b.x + b.width)); + return bottom; + }); + y = snap(Math.max(y + 64, ...heights) + 96); + } + if (d.grid) + for (const band of ["Other layers", "Other contexts"]) { + const items = d.boxes + .filter((b) => foldedBand(b) && b.band === band) + .sort((a, b) => (d.boxOrder === "supplied" || d.preserveOrder ? 0 : compare(a.id, b.id))); + if (!items.length) continue; + const h = label("band", band, margin, y); + y = place(items, margin, snap(y + h + 80), widthBudget - 48) + 64; + maxX = Math.max(maxX, ...boxes.map((b) => b.x + b.width)); + } + const reservations = edgeLanes(boxes, d.edges, packedLanes); + const reservationsById = new Map<string, { above: number; below: number }>(); + for (const end of reservations.flatMap((r) => [r.a, r.b])) { + if (!end) continue; + const space = reservationsById.get(end.id) ?? { above: 0, below: 0 }; + const side = end.side === "top" ? "above" : "below"; + space[side] = Math.max(space[side], end.offset + end.size); + reservationsById.set(end.id, space); + } + const events = [...new Set([...boxes.map((b) => b.y), ...labels.map((l) => l.y)])].sort( + (a, b) => a - b, + ); + let cursor = margin; + const movedBoxes: Positioned[] = [], + movedLabels: Layout["labels"] = []; + for (const at of events) { + const ls = labels.filter((l) => l.y === at), + bs = boxes.filter((b) => b.y === at); + if (ls.length) { + movedLabels.push(...ls.map((l) => ({ ...l, y: cursor }))); + cursor += snap(Math.max(...ls.map((l) => l.height)) + 24); + } + if (bs.length) { + const above = Math.max(0, ...bs.map((b) => reservationsById.get(b.box.id)?.above ?? 0)); + const below = Math.max(0, ...bs.map((b) => reservationsById.get(b.box.id)?.below ?? 0)); + cursor += above + 48; + movedBoxes.push(...bs.map((b) => ({ ...b, y: cursor }))); + cursor += Math.max(...bs.map((b) => b.height)) + below + 48; + } + } + return { + packedLanes, + boxes: movedBoxes, + labels: movedLabels, + width: snap(Math.max(240, maxX + margin, ...labels.map((l) => l.x + l.width + 24))), + height: snap(Math.max(120, cursor + margin)), + }; +} diff --git a/src/projections/studio/diagrams/mermaid.ts b/src/projections/studio/diagrams/mermaid.ts new file mode 100644 index 000000000..4df6ee182 --- /dev/null +++ b/src/projections/studio/diagrams/mermaid.ts @@ -0,0 +1,76 @@ +import type { Diagram } from "./model.js"; +import { layoutDiagram } from "./layout.js"; +import { edgeWords } from "./routing.js"; +export const mermaidText = (value: string): string => + value + .replace(/#/gu, "#35;") + .replace(/%/gu, "#37;") + .replace(/`/gu, "#96;") + .replace(/"/gu, "#quot;") + .replace(/</gu, "#lt;") + .replace(/>/gu, "#gt;") + .replace(/[\r\n\u2028\u2029]/gu, " "); +/** + * @sdpAnchor impl:protocol.studio-diagram-source + * @sdpLabel escaped Mermaid flowchart and mindmap source in placed box order + * @sdpSatisfies spec:consumers.spec-studio.diagram-kinds.mermaid-source, spec:consumers.spec-studio.diagram-kinds.capability-tree, spec:consumers.spec-studio.diagram-kinds + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function mermaidSource(d: Diagram): string { + const displayed = layoutDiagram(d).boxes.map((p) => p.box); + const aliases = new Map(displayed.map((b, i) => [b.id, `n${String(i)}`])); + if (d.mermaidKind === "mindmap") { + const lines = ["mindmap"]; + const append = (id: string, depth: number): void => { + const box = d.boxes.find((b) => b.id === id); + if (!box) throw new Error(`Missing tree box: ${id}`); + lines.push( + `${" ".repeat(depth + 1)}${aliases.get(id) ?? ""}["${mermaidText(`${box.words} · ${box.identity ?? box.id}`)}"]`, + ); + for (const child of d.boxes.filter((b) => b.parent === id)) append(child.id, depth + 1); + }; + for (const root of d.boxes.filter((b) => !b.parent)) append(root.id, 0); + return lines.join("\n"); + } + return [ + d.mermaidKind === "dependencies" + ? "flowchart TB" + : d.mermaidKind === "block" + ? "block-beta\n columns 1" + : (d.mermaidKind ?? "flowchart TB"), + ...(d.mermaidKind === "dependencies" + ? [...new Set(displayed.map((b) => b.band))].flatMap((band, i) => [ + ` subgraph depth${String(i)}["${mermaidText(band ?? "Units")}"]`, + ...displayed + .filter((b) => b.band === band) + .map( + (b) => + ` ${aliases.get(b.id) ?? ""}["${mermaidText(b.hideIdentity ? b.words : `${b.words} · ${b.identity ?? b.id}`)}"]`, + ), + " end", + ]) + : []), + ...displayed.flatMap((b) => [ + ...(d.mermaidKind === "dependencies" + ? [] + : [ + ` ${aliases.get(b.id) ?? ""}["${mermaidText(b.hideIdentity ? b.words : `${b.words} · ${b.identity ?? b.id}`)}"]`, + ]), + ...(b.folded + ? [` %% folded ${mermaidText(b.id)}: ${(b.foldedIds ?? []).map(mermaidText).join(", ")}`] + : []), + ...(b.cycleIds + ? [` %% cycle ${mermaidText(b.id)}: ${b.cycleIds.map(mermaidText).join(", ")}`] + : []), + ]), + ...d.edges.flatMap((e) => [ + ...(d.mermaidKind === "block" ? [` %% ${mermaidText(edgeWords(e))}`] : []), + ` ${aliases.get(e.from) ?? ""} ${e.kind === "plans to use" || e.orders === false ? "-.->" : "-->"}${d.mermaidKind === "block" ? "" : `|"${mermaidText(edgeWords(e))}"|`} ${aliases.get(e.to) ?? ""}`, + ...(e.count === undefined + ? [] + : [` %% count: ${String(e.count)}; ${mermaidText(e.from)} to ${mermaidText(e.to)}`]), + ]), + ...(d.mermaidNotes ?? []).map((note) => ` %% ${mermaidText(note)}`), + ].join("\n"); +} diff --git a/src/projections/studio/diagrams/model.ts b/src/projections/studio/diagrams/model.ts index 7a60e0cde..c6c3690d8 100644 --- a/src/projections/studio/diagrams/model.ts +++ b/src/projections/studio/diagrams/model.ts @@ -1,6 +1,22 @@ +/** + * @sdpAnchor impl:protocol.studio-diagram-model + * @sdpLabel shared diagram boxes, edges and drawing bounds + * @sdpSatisfies spec:consumers.spec-studio.diagrams + * @sdpComponent component:protocol.projections + * @sdpRole contract + */ export interface DiagramBox { readonly id: string; readonly words: string; + readonly identity?: string; + readonly parent?: string; + readonly source?: { readonly file?: string; readonly line?: number }; + readonly title?: string; + readonly elementId?: string; + readonly hideIdentity?: boolean; + readonly detail?: string; + readonly foldedIds?: readonly string[]; + readonly cycleIds?: readonly string[]; readonly edgeLabel?: string; readonly target: string; readonly context?: string | null; @@ -14,6 +30,10 @@ export interface DiagramEdge { readonly to: string; readonly kind: string; readonly count?: number; + readonly countLabel?: string; + readonly claim?: string; + readonly reason?: string; + readonly orders?: boolean; } export interface DiagramInput { readonly boxes: readonly DiagramBox[]; @@ -23,12 +43,56 @@ export interface Diagram extends DiagramInput { readonly subject: string; readonly caption: string; readonly grid?: boolean; + readonly boxOrder?: "supplied"; + readonly preserveOrder?: boolean; + readonly mermaidKind?: "mindmap" | "flowchart BT" | "dependencies" | "block"; + readonly mermaidExplanation?: string; + readonly mermaidNotes?: readonly string[]; + readonly legend?: string; + readonly textEquivalentHtml?: string; + readonly textEquivalent?: DiagramInput; + readonly textRows?: readonly string[]; + readonly note?: string; + readonly occurrenceKey?: string; + readonly source?: { readonly text: string; readonly explanation: string }; readonly sectionId?: string | null; + readonly heightBudget?: number; } export const layers = ["edge", "application", "domain", "adapter", "infrastructure", "not stated"]; export const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0); export const withinBound = (d: DiagramInput): boolean => d.boxes.length <= 40 && d.edges.length <= 100; + +// Keep direction, claim and ordering apart; original relations remain in the text equivalent. +export function combinePairs(edges: readonly DiagramEdge[]): DiagramEdge[] { + const groups = new Map<string, DiagramEdge[]>(); + for (const edge of edges) { + const key = JSON.stringify([edge.from, edge.to, edge.claim, edge.orders]); + groups.set(key, [...(groups.get(key) ?? []), edge]); + } + return [...groups.values()].map((group) => { + const first = group[0]; + if (!first) throw new Error("Empty edge group"); + if (group.length === 1) return first; + const sameCount = group.every((e) => (e.count ?? 1) === (first.count ?? 1)); + return { + from: first.from, + to: first.to, + ...(first.claim === undefined ? {} : { claim: first.claim }), + ...(first.orders === undefined ? {} : { orders: first.orders }), + kind: group + .map((e) => `${e.kind}${!sameCount && (e.count ?? 1) > 1 ? ` (${String(e.count)})` : ""}`) + .sort(compare) + .join(", "), + ...(sameCount && first.count !== undefined + ? { count: first.count, countLabel: "per relation" } + : {}), + ...(group.every((e) => e.reason === first.reason) && first.reason !== undefined + ? { reason: first.reason } + : {}), + }; + }); +} export const contextOf = (b: DiagramBox): string => b.context ?? "not stated"; export const layerOf = (b: DiagramBox): string => b.layer ?? "not stated"; export function aggregate( @@ -40,11 +104,17 @@ export function aggregate( const from = ids.get(e.from), to = ids.get(e.to); if (from === undefined || to === undefined || from === to) continue; - const key = JSON.stringify([from, to, e.kind]); + const key = JSON.stringify([from, to, e.kind, e.claim, e.orders]); + const previous = pairs.get(key); + const reason = previous && previous.reason !== e.reason ? undefined : e.reason; pairs.set(key, { from, to, kind: e.kind, + ...(e.claim === undefined ? {} : { claim: e.claim }), + ...(reason === undefined ? {} : { reason }), + ...(e.orders === undefined ? {} : { orders: e.orders }), + ...(e.countLabel === undefined ? {} : { countLabel: e.countLabel }), count: (pairs.get(key)?.count ?? 0) + (e.count ?? 1), }); } @@ -52,3 +122,9 @@ export function aggregate( (a, b) => compare(a.from, b.from) || compare(a.to, b.to) || compare(a.kind, b.kind), ); } + +// The dashed form states the planned half of the same architectural use relation. +export const relationFamily = (e: DiagramEdge): string => + `${e.kind === "plans to use" ? "uses" : e.kind}/${e.kind === "plans to use" && e.claim === "declared" ? "anchored" : (e.claim ?? "")}`; +export const needsEdgeLabels = (edges: readonly DiagramEdge[]): boolean => + new Set(edges.map(relationFamily)).size > 1; diff --git a/src/projections/studio/diagrams/requirement.ts b/src/projections/studio/diagrams/requirement.ts new file mode 100644 index 000000000..2f352709a --- /dev/null +++ b/src/projections/studio/diagrams/requirement.ts @@ -0,0 +1,178 @@ +import { aggregate, compare, combinePairs } from "./model.js"; +import { needsFold, reducesHeight, foldHeightBudget } from "./layout.js"; +import type { Diagram, DiagramBox, DiagramInput } from "./model.js"; +import { mermaidSource, mermaidText } from "./mermaid.js"; +const bandOrder = [ + "Intended Specs", + "Declared verifying examples", + "Satisfying or referencing units", + "Test or oracle bindings", + "Outside the selected set", +]; +/** + * @sdpAnchor impl:protocol.studio-requirement-diagram + * @sdpLabel folds requirement bindings by relation direction and claim and serializes exact relations + * @sdpSatisfies spec:consumers.spec-studio.diagram-kinds.requirement-trace, spec:consumers.spec-studio.diagram-kinds + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function requirementDiagram(input: DiagramInput & { subject: string }): Diagram { + const order = (boxes: readonly DiagramBox[]) => + [...boxes].sort( + (a, b) => + bandOrder.indexOf(a.band ?? "") - bandOrder.indexOf(b.band ?? "") || compare(a.id, b.id), + ); + let drawing: Diagram = { + ...input, + boxes: order(input.boxes), + sectionId: null, + caption: `Requirement drawing for ${input.subject}`, + textEquivalent: input, + edges: combinePairs(input.edges), + heightBudget: foldHeightBudget, + }; + const ids = new Map(input.boxes.map((b) => [b.id, b.id])); + const examples = input.boxes.filter( + (b) => b.id !== input.subject && b.band === "Declared verifying examples", + ); + if (examples.length > 4) { + const exampleIds = new Set(examples.map((b) => b.id)); + const tests = input.boxes.filter( + (b) => + b.band === "Test or oracle bindings" && + input.edges.some( + (e) => + e.from === b.id && + exampleIds.has(e.to) && + e.kind === "verifies" && + e.claim === "anchored", + ) && + input.edges.every((e) => e.from !== b.id || exampleIds.has(e.to)), + ); + const members = [...examples, ...tests]; + const id = `diagram.${input.subject}.requirement.fold.verifies.incoming.declared`; + const candidateIds = new Map(ids); + for (const b of members) candidateIds.set(b.id, id); + const candidate = { + ...drawing, + boxes: order([ + ...drawing.boxes.filter((b) => candidateIds.get(b.id) === b.id), + { + id, + title: `${String(examples.length)} verifying examples · declared`, + words: `${String(examples.length)} verifying examples · declared; ${String(tests.length)} test bindings · anchored, folded`, + detail: `${String(tests.length)} test bindings · anchored · folded`, + hideIdentity: true, + folded: true, + foldedIds: members.map((b) => b.id), + target: `#diagram.${input.subject}.requirement.text`, + band: "Declared verifying examples", + }, + ]), + edges: combinePairs(aggregate(input.edges, candidateIds)), + }; + if (reducesHeight(drawing, candidate)) { + for (const b of members) ids.set(b.id, id); + drawing = candidate; + } + } + const protectedIds = new Set([input.subject]); + // A peripheral box has one fold group, even when several bindings reach it. + const groups = new Map<string, DiagramBox[]>(); + for (const box of input.boxes.filter((b) => !protectedIds.has(b.id) && ids.get(b.id) === b.id)) { + const edge = input.edges + .filter((e) => e.from === box.id || e.to === box.id) + .sort((a, b) => + compare( + `${a.kind}.${a.from === box.id ? "incoming" : "outgoing"}.${a.claim ?? ""}`, + `${b.kind}.${b.from === box.id ? "incoming" : "outgoing"}.${b.claim ?? ""}`, + ), + )[0]; + const key = edge + ? `${box.band === "Declared verifying examples" ? "verifies" : edge.kind}.${edge.from === box.id ? "incoming" : "outgoing"}.${edge.claim ?? ""}` + : `unbound.incoming.declared`; + const groupKey = `${key}|${box.band ?? ""}`; + groups.set(groupKey, [...(groups.get(groupKey) ?? []), box]); + } + for (const [groupKey, members] of [...groups].sort( + ([a, aa], [b, bb]) => + bb.length - aa.length || + compare(a.split("|")[0] ?? "", b.split("|")[0] ?? "") || + bandOrder.indexOf(aa[0]?.band ?? "") - bandOrder.indexOf(bb[0]?.band ?? ""), + )) { + if (!needsFold(drawing)) break; + if (members.length < 2) continue; + const [key = ""] = groupKey.split("|"); + const [kind, direction, claim] = key.split("."); + const names: Record<string, readonly [string, string]> = { + refines: ["refining Specs", "refined Specs"], + verifies: [ + members[0]?.band === "Declared verifying examples" ? "verifying examples" : "test bindings", + "verified Specs", + ], + satisfies: ["satisfying units", "satisfied Specs"], + references: ["referencing units", "referenced Specs"], + models: ["oracle bindings", "modeled Specs"], + unbound: ["unbound units", "unbound Specs"], + }; + const title = `${String(members.length)} ${names[kind ?? ""]?.[direction === "incoming" ? 0 : 1] ?? kind ?? "related units"} · ${claim ?? ""}`; + const splitBands = [...groups.keys()].filter((k) => k.startsWith(`${key}|`)).length > 1; + const id = `diagram.${input.subject}.requirement.fold.${key}${splitBands ? `.band.${String(bandOrder.indexOf(members[0]?.band ?? ""))}` : ""}`; + const candidateIds = new Map(ids); + for (const b of members) candidateIds.set(b.id, id); + const candidate = { + ...drawing, + boxes: order([ + ...drawing.boxes.filter((b) => !members.some((m) => m.id === b.id)), + { + id, + title, + hideIdentity: true, + words: `${title}, folded`, + target: `#diagram.${input.subject}.requirement.text`, + folded: true, + foldedIds: members.map((m) => m.id), + band: members[0]?.band ?? bandOrder[3], + }, + ]), + edges: combinePairs(aggregate(input.edges, candidateIds)), + }; + if (!reducesHeight(drawing, candidate)) continue; + for (const b of members) ids.set(b.id, id); + drawing = candidate; + } + // Pair labels belong to the SVG; requirement syntax keeps each exact relation. + const exactEdges = aggregate(input.edges, ids); + const unsupported = [ + ...new Set( + exactEdges + .filter((e) => !["refines", "satisfies", "verifies"].includes(e.kind)) + .map((e) => e.kind), + ), + ].sort(compare); + const folded = drawing.boxes.some((b) => b.folded); + const fallback = unsupported.length > 0 || folded; + const aliases = new Map(drawing.boxes.map((b, i) => [b.id, `n${String(i)}`])); + const source = fallback + ? mermaidSource(drawing) + : [ + "requirementDiagram", + ...drawing.boxes.map( + (b) => + ` ${b.id.startsWith("spec:") ? "requirement" : "element"} ${aliases.get(b.id) ?? ""} {\n ${b.id.startsWith("spec:") ? "text" : "type"}: "${mermaidText(`${b.words} · ${b.id}`)}"\n }`, + ), + ...exactEdges.flatMap((e) => [ + ` %% claim: ${mermaidText(e.claim ?? "")}${e.reason ? `; reason: ${mermaidText(e.reason)}` : ""}`, + ` ${aliases.get(e.from) ?? ""} - ${e.kind} -> ${aliases.get(e.to) ?? ""}`, + ]), + ].join("\n"); + return { + ...drawing, + source: { + text: source, + explanation: fallback + ? `Preferred syntax: requirementDiagram. Chosen syntax: flowchart TB because ${[...unsupported, ...(folded ? ["folds"] : [])].join(", ")} require exact labels unavailable in requirement syntax.` + : "Preferred and chosen syntax: requirementDiagram; exact refines, satisfies and verifies, with claim words in nodes and per-edge comments.", + }, + }; +} diff --git a/src/projections/studio/diagrams/routing.ts b/src/projections/studio/diagrams/routing.ts new file mode 100644 index 000000000..775f69e3f --- /dev/null +++ b/src/projections/studio/diagrams/routing.ts @@ -0,0 +1,242 @@ +import { compare, needsEdgeLabels } from "./model.js"; +import type { DiagramEdge } from "./model.js"; +import type { Layout, Rectangle } from "./layout.js"; +import { edgeLanes, snap, widthBudget } from "./layout.js"; +export interface Point { + readonly x: number; + readonly y: number; +} +export interface Route { + readonly edge: DiagramEdge; + readonly points: Point[]; + readonly key: string; + readonly label?: Rectangle & { text: string }; +} +export interface Routed { + readonly routes: Route[]; + readonly width: number; + readonly height: number; +} +export const edgeWords = (e: DiagramEdge): string => + `${e.kind}${e.orders === false ? " (orders nothing)" : ""}${e.claim ? ` · ${e.claim}` : ""}${(e.count ?? 1) > 1 ? ` (${String(e.count)} ${e.countLabel ?? "unit edges"})` : ""}${e.reason ? ` — ${e.reason}` : ""}`; +export const edgeOrder = (a: DiagramEdge, b: DiagramEdge): number => + compare(a.from, b.from) || + compare(a.to, b.to) || + compare(a.kind, b.kind) || + compare(a.claim ?? "", b.claim ?? "") || + compare(a.reason ?? "", b.reason ?? ""); +export const intersects = (a: Point, b: Point, r: Rectangle, clearance = 12): boolean => + a.x === b.x + ? a.x > r.x - clearance && + a.x < r.x + r.width + clearance && + Math.max(a.y, b.y) > r.y - clearance && + Math.min(a.y, b.y) < r.y + r.height + clearance + : a.y > r.y - clearance && + a.y < r.y + r.height + clearance && + Math.max(a.x, b.x) > r.x - clearance && + Math.min(a.x, b.x) < r.x + r.width + clearance; +/** + * @sdpAnchor impl:protocol.studio-diagram-routing + * @sdpLabel port, channel and overflow-rail edge routes around boxes and labels + * @sdpSatisfies spec:consumers.spec-studio.diagram-kinds.edge-routing + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function routeDiagram( + layout: Layout, + edges: readonly DiagramEdge[], + budget = 20_000_000, +): Routed | null { + if (!edges.length) return { routes: [], width: layout.width, height: layout.height }; + const boxes = new Map(layout.boxes.map((b) => [b.box.id, b])); + const multiple = needsEdgeLabels(edges); + const routes: Route[] = []; + const bottoms = new Map( + layout.boxes.map((b) => [ + b.y, + Math.max( + ...layout.boxes.filter((other) => other.y === b.y).map((other) => other.y + other.height), + ), + ]), + ); + const end = (reservation: ReturnType<typeof edgeLanes>[number]["a"]) => { + if (!reservation) return null; + const box = boxes.get(reservation.id); + if (!box || 16 + reservation.port * 8 >= box.width - 12) return null; + return { + port: { + x: box.x + 16 + reservation.port * 8, + y: reservation.side === "top" ? box.y : box.y + box.height, + }, + lane: + reservation.side === "top" + ? box.y - 32 - reservation.offset + : (bottoms.get(box.y) ?? 0) + 24 + reservation.offset + reservation.size, + }; + }; + const allocated = edgeLanes(layout.boxes, edges, layout.packedLanes).map(({ edge, a, b }) => { + const from = end(a), + to = end(b); + return from && to ? { edge, a: from, b: to } : null; + }); + if (allocated.some((e) => e === null)) return null; + const stubs = allocated.flatMap((e) => + e + ? [ + { a: e.a.port, b: { x: e.a.port.x, y: e.a.lane }, edge: e.edge }, + { a: e.b.port, b: { x: e.b.port.x, y: e.b.lane }, edge: e.edge }, + ] + : [], + ); + const segments = (points: readonly Point[]) => + points.slice(1).flatMap((p, i) => { + const previous = points[i]; + return previous ? [[previous, p] as const] : []; + }); + const overlaps = (a: Point, b: Point, c: Point, d: Point) => + a.x === b.x && c.x === d.x && a.x === c.x + ? Math.min(Math.max(a.y, b.y), Math.max(c.y, d.y)) > + Math.max(Math.min(a.y, b.y), Math.min(c.y, d.y)) + : a.y === b.y && + c.y === d.y && + a.y === c.y && + Math.min(Math.max(a.x, b.x), Math.max(c.x, d.x)) > + Math.max(Math.min(a.x, b.x), Math.min(c.x, d.x)); + type Segment = readonly [Point, Point]; + const placedSegments: Segment[] = []; + const verticals = new Map<number, Segment[]>(), + horizontals = new Map<number, Segment[]>(); + const remember = ([a, b]: Segment) => { + const index = a.x === b.x ? verticals : horizontals, + key = a.x === b.x ? a.x : a.y; + index.set(key, [...(index.get(key) ?? []), [a, b]]); + placedSegments.push([a, b]); + }; + const obstacleRows = new Map<number, Rectangle[]>(); + const indexObstacle = (r: Rectangle) => { + for ( + let row = Math.floor((r.y - 12) / 128); + row <= Math.floor((r.y + r.height + 12) / 128); + row++ + ) + obstacleRows.set(row, [...(obstacleRows.get(row) ?? []), r]); + }; + for (const r of [...layout.boxes, ...layout.labels]) indexObstacle(r); + const obstacles = (minY: number, maxY: number): Rectangle[] => { + const found = new Set<Rectangle>(); + for (let row = Math.floor(minY / 128); row <= Math.floor(maxY / 128); row++) + for (const r of obstacleRows.get(row) ?? []) found.add(r); + return [...found]; + }; + const stubVerticals = new Map<number, typeof stubs>(); + for (const stub of stubs) + stubVerticals.set(stub.a.x, [...(stubVerticals.get(stub.a.x) ?? []), stub]); + const clear = (points: Point[], edge: DiagramEdge) => { + for (const [j, [a, b]] of segments(points).entries()) { + for (const obstacle of obstacles(Math.min(a.y, b.y), Math.max(a.y, b.y))) { + if (--budget < 0) return false; + if ( + (j === 0 && obstacle === boxes.get(edge.from)) || + (j === points.length - 2 && obstacle === boxes.get(edge.to)) + ) + continue; + if (intersects(a, b, obstacle)) return false; + } + if ( + a.x === b.x && + (stubVerticals.get(a.x) ?? []).some( + (stub) => stub.edge !== edge && overlaps(a, b, stub.a, stub.b), + ) + ) + return false; + for (const [c, d] of (a.x === b.x ? verticals.get(a.x) : horizontals.get(a.y)) ?? []) + if (--budget < 0 || overlaps(a, b, c, d)) return false; + } + return true; + }; + const labelFor = (text: string, points: Point[]): Route["label"] => { + const width = Array.from(text).length * 8; + for (const [a, b] of segments(points).slice(1, 2)) { + if (a.y !== b.y || Math.abs(b.x - a.x) < width + 24) continue; + for (let x = Math.min(a.x, b.x) + 12; x + width + 12 <= Math.max(a.x, b.x); x += 8) { + if (--budget < 0) return undefined; + const label = { x, y: a.y - 28, width, height: 16, text }; + if ( + obstacles(label.y - 12, label.y + label.height + 12).some( + (r) => + label.x < r.x + r.width + 12 && + label.x + label.width > r.x - 12 && + label.y < r.y + r.height + 12 && + label.y + label.height > r.y - 12, + ) + ) + continue; + if (stubs.some((stub) => intersects(stub.a, stub.b, label))) continue; + if ([...placedSegments, ...segments(points)].some(([c, d]) => intersects(c, d, label))) + continue; + return label; + } + } + return undefined; + }; + for (const [i, entry] of allocated.entries()) { + if (!entry) return null; + const { edge, a, b } = entry; + const key = `E${String(i + 1)}`; + // Try the direct channel, then free interior rails, before spending overflow width. + const rails = [ + ...new Set([ + b.port.x, + a.port.x, + ...Array.from({ length: Math.floor(layout.width / 8) }, (_, n) => (n + 1) * 8), + ...Array.from({ length: edges.length + 1 }, (_, n) => layout.width + 24 + n * 8), + // Earlier labels can consume the fixed gutter. Keep one free rail beyond them. + snap( + Math.max( + layout.width, + ...routes.flatMap((r) => [ + ...r.points.map((p) => p.x), + ...(r.label ? [r.label.x + r.label.width + 12] : []), + ]), + ) + 48, + ), + ]), + ]; + let chosen: Route | undefined; + const wordings = multiple ? [`${edge.kind}${edge.claim ? ` · ${edge.claim}` : ""}`, key] : [""]; + const candidates = wordings + .flatMap((wording, i) => + rails.map((rail) => ({ + wording, + rail, + rank: Number(rail > Math.max(layout.width - 24, widthBudget - 24)) * 2 + i, + })), + ) + .sort((a, b) => a.rank - b.rank); + for (const { wording, rail } of candidates) { + if (budget < 0) return null; + const points = [ + a.port, + { x: a.port.x, y: a.lane }, + { x: rail, y: a.lane }, + { x: rail, y: b.lane }, + { x: b.port.x, y: b.lane }, + b.port, + ].filter((p, j, ps) => j === 0 || p.x !== ps[j - 1]?.x || p.y !== ps[j - 1]?.y); + if (!clear(points, edge)) continue; + const label = multiple ? labelFor(wording, points) : undefined; + if (multiple && !label) continue; + chosen = { edge, points, key, ...(label ? { label } : {}) }; + break; + } + if (!chosen) return null; + routes.push(chosen); + for (const segment of segments(chosen.points)) remember(segment); + if (chosen.label) indexObstacle(chosen.label); + } + return { + routes, + width: snap(Math.max(layout.width, ...routes.flatMap((r) => r.points.map((p) => p.x + 24)))), + height: layout.height, + }; +} diff --git a/src/projections/studio/diagrams/slice.ts b/src/projections/studio/diagrams/slice.ts new file mode 100644 index 000000000..95934c03e --- /dev/null +++ b/src/projections/studio/diagrams/slice.ts @@ -0,0 +1,353 @@ +import { plainTitle } from "../pages/code-spans.js"; +import type { SliceAnswer, SliceArchitecture } from "../slice-answer.js"; +import type { Diagram, DiagramBox, DiagramEdge } from "./model.js"; +import { aggregate, combinePairs } from "./model.js"; +import { needsFold, reducesHeight, foldHeightBudget } from "./layout.js"; +import { architectureDiagrams } from "./architecture.js"; +type Target = (id: string) => string; +const rung = (row: { statedReadiness: string | null; floorReached: string | null }) => + `stated ${row.statedReadiness ?? "not recorded"}, floor ${row.floorReached ?? "none"}`; +/** + * @sdpAnchor impl:protocol.studio-slice-dependencies + * @sdpLabel supplied depth bands with protected boundary boxes and lossless text + * @sdpSatisfies spec:consumers.spec-studio.diagram-kinds.dependency, spec:consumers.spec-studio.diagram-kinds + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function dependencyDiagram( + subject: string, + answer: SliceAnswer, + target: Target, + diagramId = `${subject}.dependencies`, +): Diagram { + const shown = answer.members.filter((m) => m.exampleOf === null); + const depths = [...new Set(shown.map((m) => m.depth))].sort((a, b) => b - a); + const boxes: DiagramBox[] = depths.flatMap((depth) => + shown + .filter((m) => m.depth === depth) + .map((m) => ({ + id: m.id, + title: plainTitle(m.title ?? m.id), + words: `${plainTitle(m.title ?? m.id)}, ${rung(m)}, ${String(m.exampleMembers.length)} example members folded in`, + detail: `${rung(m)}; ${String(m.exampleMembers.length)} example members folded in`, + target: target(m.id), + band: `Depth ${String(depth)}`, + })), + ); + const own = new Set(shown.map((m) => m.id)); + const edges: DiagramEdge[] = answer.structure.edges + .filter((e) => own.has(e.from) && own.has(e.to)) + .map((e) => ({ + from: e.from, + to: e.to, + kind: e.type, + claim: "declared", + countLabel: "relations", + orders: e.orders, + ...(e.reason === undefined ? {} : { reason: e.reason }), + })); + const mapping = new Map(boxes.map((b) => [b.id, b.id])); + const groups = new Map<string, string[]>(); + for (const row of answer.boundary.restsOn) { + const keep = + row.holds.length > 0 || row.statedReadiness === "idea" || row.statedReadiness === "scoped"; + if (keep) { + boxes.push({ + id: row.id, + title: plainTitle(row.title ?? row.id), + words: `${plainTitle(row.title ?? row.id)}, ${rung(row)}; holds ${String(row.holds.length)} members`, + detail: rung(row), + target: target(row.id), + band: "Outside this scope", + }); + mapping.set(row.id, row.id); + } + for (const via of row.via) { + const id = keep ? row.id : `diagram.${diagramId}.outside.${via.type}`; + if (!keep) { + const ids = groups.get(via.type) ?? []; + if (!ids.includes(row.id)) ids.push(row.id); + groups.set(via.type, ids); + } + for (const member of via.members) { + const reason = via.reasons?.find((r) => r.member === member)?.reason; + if (own.has(member)) + edges.push({ + from: member, + to: id, + kind: via.type, + countLabel: "relations", + claim: "declared", + ...(reason === undefined ? {} : { reason }), + }); + } + } + } + for (const [type, ids] of groups) { + const id = `diagram.${diagramId}.outside.${type}`; + mapping.set(id, id); + boxes.push({ + id, + words: `${String(ids.length)} outside Specs, ${type}, folded`, + title: `${String(ids.length)} outside Specs`, + detail: `${type}, folded`, + folded: true, + hideIdentity: true, + foldedIds: ids, + target: "#boundary.restsOn", + band: "Outside this scope", + }); + } + for (const end of answer.structure.unresolved) { + if (!mapping.has(end.to)) { + mapping.set(end.to, end.to); + boxes.push({ + id: end.to, + title: end.to, + words: `${end.to}, unresolved`, + detail: "unresolved", + target: "", + band: "Outside this scope", + }); + } + if (own.has(end.from)) + edges.push({ + from: end.from, + to: end.to, + kind: end.type, + countLabel: "relations", + claim: "declared", + ...(end.reason ? { reason: end.reason } : {}), + }); + } + let drawing: Diagram = { + subject, + caption: "Declared relations by depth", + sectionId: null, + preserveOrder: true, + heightBudget: foldHeightBudget, + mermaidKind: "dependencies", + boxes, + edges: combinePairs(aggregate(edges, mapping)), + }; + for (const depth of [...depths].sort( + (a, b) => + shown.filter((m) => m.depth === b).length - shown.filter((m) => m.depth === a).length || + a - b, + )) { + if (!needsFold(drawing)) break; + const members = shown.filter((m) => m.depth === depth), + id = `diagram.${diagramId}.fold.${String(depth)}`; + if (members.length < 2) continue; + const candidateMapping = new Map(mapping); + for (const m of members) candidateMapping.set(m.id, id); + const at = drawing.boxes.findIndex((b) => b.band === `Depth ${String(depth)}`); + const remaining = drawing.boxes.filter((b) => b.band !== `Depth ${String(depth)}`); + remaining.splice(at, 0, { + id, + elementId: id, + title: `${String(members.length)} members, folded`, + words: `${String(members.length)} members at depth ${String(depth)}, folded`, + target: "#slice.members", + band: `Depth ${String(depth)}`, + folded: true, + hideIdentity: true, + foldedIds: members.map((m) => m.id), + }); + const candidate = { + ...drawing, + boxes: remaining, + edges: combinePairs(aggregate(edges, candidateMapping)), + }; + if (!reducesHeight(drawing, candidate)) continue; + for (const m of members) mapping.set(m.id, id); + drawing = candidate; + } + return { + ...drawing, + note: "Depths are structural, never phases, dates or priority. Dashed relations order nothing. The text keeps every member and original relation.", + textRows: [ + ...answer.members.map( + (m) => + `${m.id}: ${plainTitle(m.title ?? m.id)}, depth ${String(m.depth)}, ${rung(m)}${m.exampleOf ? `; example folded into ${m.exampleOf}` : ""}`, + ), + ...answer.boundary.restsOn.map( + (m) => `Outside this scope: ${m.id}: ${plainTitle(m.title ?? m.id)}, ${rung(m)}`, + ), + ...answer.structure.edges.map( + (e) => + `${e.from} ${e.type} ${e.to} · declared${!e.orders ? ", orders nothing" : ""}${e.reason ? ` — ${e.reason}` : ""}`, + ), + ...answer.boundary.restsOn.flatMap((row) => + row.via.flatMap((via) => + via.members.map((member) => { + const reason = via.reasons?.find((r) => r.member === member)?.reason; + return `${member} ${via.type} ${row.id} · declared${reason === undefined ? "" : ` — ${reason}`}`; + }), + ), + ), + ...answer.structure.unresolved.map( + (e) => + `${e.from} ${e.type} ${e.to} · declared, unresolved${e.reason ? ` — ${e.reason}` : ""}`, + ), + ...answer.structure.cycles.map( + (c) => `Cycle: ${c.members.join(", ")}; ${c.cycle.join(" → ")}`, + ), + ], + }; +} +const value = (v: unknown): string => + v === null || v === undefined + ? "not stated" + : Array.isArray(v) + ? v + .map((part: unknown) => (typeof part === "string" ? part : JSON.stringify(part))) + .join(", ") + : typeof v === "string" + ? v + : JSON.stringify(v); +// A realization link makes the relations mixed, so each link carries its own label and the +// legend names every family drawn. +const realizationLegend = (edges: readonly DiagramEdge[]): string => + [ + ...(edges.some((e) => e.kind === "uses") ? ["Arrows: uses (anchored)."] : []), + ...(edges.some((e) => e.kind === "plans to use") + ? ["Dashed arrows: plans to use (declared)."] + : []), + "Labelled links: realizes (anchored), a component paired with the design it satisfies, never a use; it confers no delivery fact.", + ].join(" "); +/** + * @sdpAnchor impl:protocol.studio-slice-architecture + * @sdpLabel selected component grid with distinct planned identities and complete unit edges + * @sdpSatisfies spec:consumers.spec-studio.diagram-kinds.scoped-architecture, spec:consumers.spec-studio.diagram-kinds + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function scopedArchitectureDiagrams( + subject: string, + a: SliceArchitecture, + target: Target, +): Diagram[] { + if (!a.components.length && !a.designs.length) return []; + const boxes: DiagramBox[] = a.components.map((c) => ({ + id: c.id, + title: c.label ?? c.id, + words: `${c.label ?? c.id}, anchored, layer ${c.layer ?? "not stated"}, context ${c.context ?? "not stated"}`, + target: target(c.id), + context: c.context, + layer: c.layer, + })); + boxes.push( + ...a.designs.map((d) => ({ + id: d.spec, + title: `planned ${d.componentId}`, + words: `planned ${d.componentId}, declared layer ${d.layer ?? "not stated"}, context ${d.context ?? "not stated"}`, + target: target(d.spec), + planned: true, + context: d.context, + layer: d.layer, + })), + ); + const outsideId = (c: SliceArchitecture["outside"][number]) => + `outside.${encodeURIComponent(c.context ?? "")}.${encodeURIComponent(c.layer ?? "")}`; + const mapping = new Map(boxes.map((b) => [b.id, b.id])); + for (const c of a.outside) mapping.set(c.id, outsideId(c)); + for (const id of new Set(a.outside.map(outsideId))) { + const rows = a.outside.filter((c) => outsideId(c) === id), + c = rows[0]; + if (c === undefined) continue; + boxes.push({ + id, + title: `outside scope: ${c.context ?? "not stated"}`, + words: `outside scope, ${String(rows.length)} components, ${c.layer ?? "not stated"}, folded`, + target: "#slice.outside", + context: c.context, + layer: c.layer, + folded: true, + hideIdentity: true, + foldedIds: rows.map((c) => c.id), + }); + } + const edges: DiagramEdge[] = [ + ...a.uses.map((e) => ({ + from: e.from, + to: e.to, + kind: "uses", + claim: "anchored", + count: e.count, + })), + ...a.plannedUses.flatMap((e) => { + const anchored = mapping.get(e.to); + const destinations = [ + ...(anchored === undefined ? [] : [anchored]), + ...a.designs.filter((d) => d.componentId === e.to).map((d) => d.spec), + ]; + return destinations.map((to) => ({ + from: e.design, + to, + kind: "plans to use", + claim: "declared", + })); + }), + ...a.designs + .filter((d) => d.realized) + .map((d) => ({ from: d.componentId, to: d.spec, kind: "realizes", claim: "anchored" })), + ]; + const original = [ + ...a.components.map( + (c) => + `${c.id}: ${c.label ?? ""}, anchored, layer ${c.layer ?? "not stated"}, context ${c.context ?? "not stated"}; satisfies members ${c.members.satisfies.join(", ") || "none"}; references members ${c.members.references.join(", ") || "none"}; code for rests on ${c.restsOn.join(", ") || "none"}; member designs ${c.designs.join(", ") || "none"}`, + ), + ...a.outside.map( + (c) => + `${c.id}: ${c.label ?? ""}, outside scope, ${c.anchored ? "anchored" : "not anchored"}, layer ${c.layer ?? "not stated"}, context ${c.context ?? "not stated"}`, + ), + ...a.designs.flatMap((d) => [ + `${d.spec}: planned ${d.componentId}; layer ${value(d.layer)}, context ${value(d.context)}, role ${value(d.role)}, uses ${value(d.uses)}; anchor ${d.realized ? "satisfies the design" : d.anchorExists ? "present, does not satisfy the design" : "absent"}`, + ...d.differing.map( + (f) => `${f.field}: declared ${value(f.planned)}; anchored ${value(f.realized)}`, + ), + ...d.unreadable.map((u) => `${u.key}: unreadable ${u.value}, not compared`), + ]), + ...a.uses.flatMap((e) => e.units.map((u) => `${u.from} uses ${u.to} · ${u.claim}`)), + ...a.plannedUses.map((e) => `${e.design} plans to use ${e.to} · declared`), + ...a.unplaced.map( + (u) => + `${u.unit} ${u.via === "satisfies" ? "implements" : "references"} ${u.member}; no component`, + ), + ]; + const selected = architectureDiagrams({ + boxes, + edges: aggregate(edges, new Map([...mapping, ...boxes.map((b) => [b.id, b.id] as const)])), + }); + const sectionIds = new Map( + selected.drawings.map((d, i) => [ + d.subject, + i === 0 + ? "slice.architecture" + : `diagram.${subject}.architecture.${encodeURIComponent(d.subject)}`, + ]), + ); + return selected.drawings.map((d, i) => ({ + ...d, + subject, + sectionId: + i === 0 + ? null + : (sectionIds.get(d.subject) ?? + `diagram.${subject}.architecture.${encodeURIComponent(d.subject)}`), + ...(i === 0 ? {} : { occurrenceKey: d.subject }), + boxes: d.boxes.map((b) => ({ + ...b, + target: b.target.startsWith("#context.") + ? `#${sectionIds.get(b.target.slice(1)) ?? "slice.outside"}` + : b.target, + })), + caption: i === 0 ? "Anchored components and member designs" : d.caption, + ...(d.edges.some((e) => e.kind === "realizes") ? { legend: realizationLegend(d.edges) } : {}), + textRows: original, + ...(selected.fold === "nothing" && !a.outside.length && !a.designs.length + ? { mermaidKind: "block" as const } + : {}), + })); +} diff --git a/src/projections/studio/diagrams/svg.ts b/src/projections/studio/diagrams/svg.ts index 2dcba0a65..a26432d6e 100644 --- a/src/projections/studio/diagrams/svg.ts +++ b/src/projections/studio/diagrams/svg.ts @@ -1,163 +1,28 @@ -import { escape } from "../pages/html.js"; -import { compare, contextOf, layerOf, layers, withinBound } from "./model.js"; +import { escape, sourceCode, copy, location } from "../pages/html.js"; +import { contextOf, layerOf, withinBound, needsEdgeLabels } from "./model.js"; import type { Diagram, DiagramBox } from "./model.js"; - -export function labelLines(text: string): string[] { - const result: string[] = []; - let rest = text; - while (rest.length > 30) { - const prefix = rest.slice(0, 30); - const breakAt = prefix.includes(" ") - ? prefix.lastIndexOf(" ") - : Math.max(prefix.lastIndexOf("."), prefix.lastIndexOf(":")); - const fallback = prefix.lastIndexOf("-"); - const n = breakAt >= 0 ? breakAt + 1 : fallback >= 0 ? fallback + 1 : 30; - result.push(rest.slice(0, n)); - rest = rest.slice(n); - } - if (rest) result.push(rest); - return result; -} -interface Positioned { - readonly box: DiagramBox; - readonly x: number; - readonly y: number; - readonly width: number; - readonly height: number; -} -const foldedBand = (b: DiagramBox): boolean => - b.folded === true && (b.band === "Other contexts" || b.band === "Other layers"); -function layout(d: Diagram) { - const boxes: Positioned[] = []; - const labels: { kind: "lane" | "band"; label: string; x: number; y: number }[] = []; - const columns = d.grid - ? [...new Set(d.boxes.filter((b) => !foldedBand(b)).map(contextOf))].sort((a, b) => - a === "not stated" ? 1 : b === "not stated" ? -1 : compare(a, b), - ) - : [""]; - const bands = d.grid - ? layers.filter((l) => d.boxes.some((b) => !foldedBand(b) && layerOf(b) === l)) - : [...new Set(d.boxes.map((b) => b.band ?? "Units"))]; - const cell = (c: string, b: string) => - d.boxes.filter((box) => - d.grid - ? !foldedBand(box) && contextOf(box) === c && layerOf(box) === b - : (box.band ?? "Units") === b, - ); - const dimensions = (box: DiagramBox) => { - const lines = [...labelLines(box.id), ...labelLines(box.words)]; - return { - width: Math.ceil(Math.max(1, ...lines.map((l) => l.length)) * 7.5) + 24, - height: lines.length * 17 + 24, - }; - }; - const boxStride = Math.max(180, ...d.boxes.map((box) => dimensions(box).width + 24)); - const widths = new Map( - columns.map((c) => [ - c, - Math.max( - 180, - ...bands.map((b) => { - const items = cell(c, b); - return Math.min(6, items.length) * boxStride; - }), - ), - ]), - ); - const heights = new Map( - bands.map((b) => [ - b, - Math.max( - 80, - ...columns.map((c) => { - const items = cell(c, b); - return ( - Math.ceil(items.length / 6) * - Math.max(60, ...items.map((item) => dimensions(item).height + 26)) + - 36 - ); - }), - ), - ]), - ); - let x = 20; - for (const column of columns) { - if (d.grid) labels.push({ kind: "lane", label: column, x: x + 12, y: 24 }); - let y = d.grid ? 60 : 12; - for (const band of bands) { - labels.push({ kind: "band", label: band, x: x + 12, y: y + 20 }); - const items = cell(column, band).sort((a, b) => compare(a.id, b.id)); - const maxW = boxStride; - const maxH = Math.max(60, ...items.map((item) => dimensions(item).height + 26)); - items.forEach((box, i) => - boxes.push({ - box, - x: x + 12 + (i % 6) * maxW, - y: y + 36 + Math.floor(i / 6) * maxH, - ...dimensions(box), - }), - ); - y += heights.get(band) ?? 80; - } - x += (widths.get(column) ?? 180) + 28; - } - let bottom = (d.grid ? 60 : 12) + [...heights.values()].reduce((a, b) => a + b, 0); - if (d.grid) - for (const band of ["Other layers", "Other contexts"]) { - const items = d.boxes - .filter((b) => foldedBand(b) && b.band === band) - .sort((a, b) => compare(a.id, b.id)); - if (items.length === 0) continue; - labels.push({ kind: "band", label: band, x: 32, y: bottom + 20 }); - const stride = Math.max(60, ...items.map((b) => dimensions(b).height + 26)); - items.forEach((box, i) => - boxes.push({ - box, - x: 32 + (i % 6) * boxStride, - y: bottom + 36 + Math.floor(i / 6) * stride, - ...dimensions(box), - }), - ); - bottom += Math.ceil(items.length / 6) * stride + 36; - x = Math.max(x, 44 + Math.min(6, items.length) * boxStride); - } - return { - boxes, - labels, - width: Math.max(240, x), - boxStride, - height: Math.max(120, bottom + 20), - }; -} -const relationOf = (kind: string): string => (kind === "plans to use" ? "uses" : kind); -function svg(d: Diagram, textId: string, diagramId: string): string { - const l = layout(d), - positions = new Map(l.boxes.map((b) => [b.box.id, b])); +import { layoutDiagram, foldedBand, widthBudget } from "./layout.js"; +import type { Layout, Positioned } from "./layout.js"; +import { routeDiagram, edgeOrder, edgeWords } from "./routing.js"; +import type { Routed } from "./routing.js"; +import { mermaidSource } from "./mermaid.js"; +export { labelLines } from "./layout.js"; +function svg(d: Diagram, textId: string, diagramId: string, l: Layout, routed: Routed): string { const marker = `diagram.${diagramId}.arrow`; - const singleRelation = new Set(d.edges.map((edge) => relationOf(edge.kind))).size === 1; - const edges = d.edges - .map((e) => { - const a = positions.get(e.from), - b = positions.get(e.to); - if (!a || !b) return ""; - const ax = a.x + Math.round(a.width / 2), - ay = a.y + a.height, - bx = b.x + Math.round(b.width / 2), - by = b.y; - const mid = ay + 12; - const rail = a.x + l.boxStride - 12; - const approach = by - 12; - return `<g class="edge" data-from="${escape(e.from)}" data-to="${escape(e.to)}" data-kind="${escape(e.kind)}"><path d="M ${String(ax)} ${String(ay)} V ${String(mid)} H ${String(rail)} V ${String(approach)} H ${String(bx)} V ${String(by)}" fill="none"${e.kind === "plans to use" ? ' stroke-dasharray="6 4"' : ""} marker-end="url(#${escape(marker)})"/>${singleRelation ? "" : `<text x="${String(Math.round((ax + bx) / 2))}" y="${String(mid - 3)}">${escape(e.kind)}${(e.count ?? 1) > 1 ? ` (${String(e.count)} unit edges)` : ""}</text>`}</g>`; - }) + const edges = routed.routes + .map( + (r) => + `<g class="edge" data-from="${escape(r.edge.from)}" data-to="${escape(r.edge.to)}" data-kind="${escape(r.edge.kind)}"${r.edge.orders === false ? ' data-orders="false"' : ""}${r.edge.claim ? ` data-claim="${escape(r.edge.claim)}"` : ""}${r.label?.text === r.key ? ` data-edge-key="${r.key}"` : ""}><path d="${r.points.map((p, i) => `${i === 0 ? "M" : "L"} ${String(p.x)} ${String(p.y)}`).join(" ")}" fill="none"${r.edge.kind === "plans to use" || r.edge.orders === false ? ' stroke-dasharray="6 4"' : ""} marker-end="url(#${escape(marker)})"/>${r.label ? `<text x="${String(r.label.x)}" y="${String(r.label.y + 12)}">${escape(r.label.text)}</text>` : ""}</g>`, + ) .join(""); const node = (p: Positioned) => - `<g class="node" data-id="${escape(p.box.id)}"${p.box.planned ? ' data-planned="true"' : ""}${p.box.folded ? ' data-folded="true"' : ""}><a href="${escape(p.box.target)}"><rect x="${String(p.x)}" y="${String(p.y)}" width="${String(p.width)}" height="${String(p.height)}" rx="4"${p.box.planned ? ' stroke-dasharray="6 4"' : ""}/><text x="${String(p.x + 12)}" y="${String(p.y + 23)}">${[...labelLines(p.box.id), ...labelLines(p.box.words)].map((line, i) => `<tspan x="${String(p.x + 12)}" dy="${String(i === 0 ? 0 : 17)}">${escape(line)}</tspan>`).join("")}</text></a></g>`; + `<g class="node"${d.boxOrder === "supplied" && !p.box.elementId ? ` id="${escape(p.box.id)}.box"` : ""} data-id="${escape(p.box.id)}"${p.box.elementId ? ` id="${escape(p.box.elementId)}"` : ""}${p.box.planned ? ' data-planned="true"' : ""}${p.box.folded ? ' data-folded="true"' : ""}>${p.box.target ? `<a href="${escape(p.box.target)}">` : "<g>"}<title>${escape(p.box.hideIdentity ? p.box.words : p.box.id)}${[...p.title.map((text) => ({ text, cls: "box-title" })), ...p.identity.map((text) => ({ text, cls: "box-id" })), ...p.detail.map((text) => ({ text, cls: "box-detail" }))].map((line, i) => `${escape(line.text)}`).join("")}${p.box.target ? "" : ""}`; const groups = d.grid ? l.labels .filter((label) => label.kind === "lane") .map( (lane) => - `${escape(lane.label)}${l.labels + `${lane.lines.map((line, i) => `${escape(line)}`).join("")}${l.labels .filter( (b) => b.kind === "band" && @@ -167,7 +32,7 @@ function svg(d: Diagram, textId: string, diagramId: string): string { ) .map( (band) => - `${escape(band.label)}${l.boxes + `${band.lines.map((line, i) => `${escape(line)}`).join("")}${l.boxes .filter( (p) => !foldedBand(p.box) && @@ -188,7 +53,7 @@ function svg(d: Diagram, textId: string, diagramId: string): string { ) .map( (band) => - `${escape(band.label)}${l.boxes + `${band.lines.map((line, i) => `${escape(line)}`).join("")}${l.boxes .filter((p) => foldedBand(p.box) && p.box.band === band.label) .map(node) .join("")}`, @@ -197,41 +62,75 @@ function svg(d: Diagram, textId: string, diagramId: string): string { : l.labels .map( (band) => - `${escape(band.label)}${l.boxes + `${band.lines.map((line, i) => `${escape(line)}`).join("")}${l.boxes .filter((p) => (p.box.band ?? "Units") === band.label) .map(node) .join("")}`, ) .join(""); - return `${escape(d.caption)}${edges}${groups}`; + return `${escape(d.caption)}${edges}${groups}`; } /** * @sdpAnchor impl:protocol.studio-diagrams * @sdpLabel deterministic grid and band layouts with bounded folds and full text equivalents - * @sdpSatisfies spec:consumers.spec-studio.diagrams + * @sdpSatisfies spec:consumers.spec-studio.diagrams, spec:consumers.spec-studio.diagram-kinds.edge-routing, spec:consumers.spec-studio.diagram-kinds.legibility, spec:consumers.spec-studio.diagram-kinds.mermaid-source, spec:consumers.spec-studio.diagram-kinds * @sdpReferences spec:decisions.studio-diagram-renderer, spec:decisions.studio-build-renders * @sdpComponent component:protocol.projections * @sdpRole projection + * @sdpUses impl:protocol.studio-diagram-model */ export function renderDiagram(d: Diagram, diagramId = d.subject): string { const textId = `diagram.${diagramId}.text`, fits = withinBound(d); - const edgeLabels = new Map(d.boxes.map((box) => [box.id, box.edgeLabel ?? box.id])); const words = (b: DiagramBox) => `${b.words}${b.planned && !b.words.includes("planned") ? ", planned" : ""}${b.folded && !b.words.includes("folded") ? ", folded" : ""}`; const normalized = { ...d, boxes: d.boxes.map((b) => ({ ...b, words: words(b) })), - edges: [...d.edges].sort( - (a, b) => compare(a.from, b.from) || compare(a.to, b.to) || compare(a.kind, b.kind), - ), + edges: [...d.edges].sort(edgeOrder), + }; + let l = fits ? layoutDiagram(normalized) : null; + const tooTall = l !== null && d.heightBudget !== undefined && l.height > d.heightBudget; + let routed = l && !tooTall ? routeDiagram(l, normalized.edges) : null; + if (l && !routed && !tooTall) { + l = layoutDiagram(normalized, false); + routed = routeDiagram(l, normalized.edges); + } + const equivalent = d.textEquivalent ?? normalized; + const edgeLabels = new Map(equivalent.boxes.map((b) => [b.id, b.edgeLabel ?? b.id])); + const text = + d.textEquivalentHtml === undefined + ? `The diagram as text
    ${d.textRows ? d.textRows.map((row) => `
  • ${escape(row)}
  • `).join("") : equivalent.boxes.map((b) => `
  • ${escape(b.id)}: ${escape(b.words)}${b.source ? ` · ${sourceCode(b.source.file, b.source.line)}${b.source.file ? copy(location(b.source.file, b.source.line)) : ""}` : ""}${b.foldedIds?.length ? ` · folded members: ${b.foldedIds.map(escape).join(", ")}` : ""}
  • `).join("")}${d.textRows ? "" : equivalent.edges.map((e) => `
  • ${escape(edgeLabels.get(e.from) ?? e.from)} ${escape(e.kind)} ${escape(edgeLabels.get(e.to) ?? e.to)}${e.claim ? ` · ${escape(e.claim)}` : ""}${(e.count ?? 1) > 1 ? ` (${String(e.count)} unit edges)` : ""}${e.reason ? ` — ${escape(e.reason)}` : ""}
  • `).join("")}
` + : `The diagram as text${d.textEquivalentHtml}`; + const relationWords = (kind: string) => { + const claims = [ + ...new Set(d.edges.filter((e) => e.kind === kind).flatMap((e) => (e.claim ? [e.claim] : []))), + ].sort(); + return `${kind}${claims.length ? ` (${claims.join(", ")})` : ""}`; }; - const text = `The diagram as text
    ${normalized.boxes.map((b) => `
  • ${escape(b.id)}: ${escape(b.words)}
  • `).join("")}${normalized.edges.map((e) => `
  • ${escape(edgeLabels.get(e.from) ?? e.from)} ${escape(e.kind)} ${escape(edgeLabels.get(e.to) ?? e.to)}${(e.count ?? 1) > 1 ? ` (${String(e.count)} unit edges)` : ""}
  • `).join("")}
`; - const relation = new Set(d.edges.map((edge) => relationOf(edge.kind))); - const legend = - relation.size === 1 && fits - ? `

Arrows: ${escape([...relation][0] ?? "")}.${d.edges.some((edge) => edge.kind === "plans to use") ? " Dashed arrows: plans to use." : ""}

` + const keyed = routed?.routes.some((r) => r.label?.text === r.key) + ? `
    ${routed.routes + .filter((r) => r.label?.text === r.key) + .map((r) => `
  • ${r.key}: ${escape(edgeWords(r.edge))}
  • `) + .join("")}
` + : ""; + // A supplied legend names the relation families; edge keys still need their words. + const legend = d.legend + ? `

${escape(d.legend)}

${keyed}` + : d.edges.length && !needsEdgeLabels(d.edges) && routed + ? `

Arrows: ${escape(relationWords(d.edges.some((e) => e.kind === "uses") ? "uses" : (d.edges[0]?.kind ?? "")))}.${d.edges.some((e) => e.kind === "plans to use") ? ` Dashed arrows: ${escape(relationWords("plans to use"))}.` : ""}

` + : keyed; + const viewportId = `diagram.${diagramId}.viewport`, + helpId = `diagram.${diagramId}.scroll-help`; + const help = + routed && routed.width > widthBudget + ? `

Scroll horizontally to read the whole drawing.

` : ""; + const sourceId = `diagram.${diagramId}.source`, + areaId = `${sourceId}-text`; + const source = routed + ? `
Mermaid source

Copyable source; the Studio draws static SVG.${d.mermaidKind === "dependencies" ? ` See the full text equivalent for every original relation and folded member.` : ""}

${d.source ? escape(d.source.explanation) : d.mermaidExplanation ? escape(d.mermaidExplanation) : d.mermaidKind === "block" ? "Chosen syntax: block, with anchored component boxes and uses arrows." : d.mermaidKind === "mindmap" || d.mermaidKind === "flowchart BT" ? `Chosen syntax: ${d.mermaidKind}; full identities and declared refinement.` : d.grid ? "Preferred syntax: block; flowchart fallback because block cannot carry edge claims and counts." : "Preferred syntax: flowchart. Chosen syntax: labelled flowchart, preserving exact relations, claims and folds."}

` + : ""; const sectionId = d.sectionId === undefined ? d.subject : d.sectionId; - return `
${escape(d.caption)}
${legend}${fits ? `
${svg(normalized, textId, diagramId)}
` : "

Shown as text: this drawing exceeds the bound of 40 boxes and 100 edges.

"}${text}
`; + return `
${escape(d.caption)}
${d.note ? `

${escape(d.note)}

` : ""}${legend}${help}${routed && l ? `
${svg(normalized, textId, diagramId, l, routed)}
` : `

${tooTall ? `Shown as text: this drawing exceeds the height budget of ${String(d.heightBudget)} pixels after folding.` : fits ? "Shown as text: a distinct route or label could not be placed within the routing budget." : "Shown as text: this drawing exceeds the bound of 40 boxes and 100 edges."}

`}${text}${source}
`; } diff --git a/src/projections/studio/inputs.ts b/src/projections/studio/inputs.ts index abc749ee5..36b2dfd76 100644 --- a/src/projections/studio/inputs.ts +++ b/src/projections/studio/inputs.ts @@ -1,3 +1,10 @@ +/** + * @sdpAnchor impl:protocol.studio-inputs + * @sdpLabel named base graph and run input contract + * @sdpSatisfies spec:consumers.spec-studio.data.inputs + * @sdpComponent component:protocol.projections + * @sdpRole contract + */ export interface StudioInputs { readonly base?: { readonly graph: unknown; readonly label: string | null }; readonly runs: readonly unknown[]; diff --git a/src/projections/studio/pages/architecture-page.ts b/src/projections/studio/pages/architecture-page.ts index 7617a3abd..ca3b723f0 100644 --- a/src/projections/studio/pages/architecture-page.ts +++ b/src/projections/studio/pages/architecture-page.ts @@ -1,8 +1,9 @@ +import { unitRegister } from "./architecture-register.js"; import { codeSpans, plainTitle } from "./code-spans.js"; import { architectureDiagrams, memberDiagram, renderDiagram } from "../diagrams/index.js"; import type { DiagramBox, DiagramEdge } from "../diagrams/index.js"; import { frame } from "./frame.js"; -import { code, sourceCode, copy, count, escape, link, list } from "./html.js"; +import { code, count, escape, link, list } from "./html.js"; import type { PageContext } from "./html.js"; interface Member { id: string; @@ -46,13 +47,10 @@ interface Plans { }[]; unreadable: { spec: string; key: string; value: unknown }[]; } -const location = (m: Member) => - m.file === null ? "not recorded" : `${m.file}${m.line === null ? "" : `:${String(m.line)}`}`; - /** * @sdpAnchor impl:protocol.studio-architecture-lens * @sdpLabel renders recipe component rows and planned designs as architecture and member diagrams - * @sdpSatisfies spec:consumers.spec-studio.lenses.architecture, spec:consumers.spec-studio.lenses + * @sdpSatisfies spec:consumers.spec-studio.lenses.architecture, spec:consumers.spec-studio.lenses, spec:consumers.spec-studio.diagram-kinds * @sdpReferences spec:decisions.studio-build-renders * @sdpComponent component:protocol.projections * @sdpRole projection @@ -60,9 +58,14 @@ const location = (m: Member) => export function architecturePage(ctx: PageContext): string { const architecture = ctx.data.recipes.all.get(17) as Architecture; const plans = ctx.data.recipes.all.get(43) as Plans; - const components = architecture.components.filter((c) => c.declared); + const components = architecture.components + .filter((c) => c.declared) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); + const register = unitRegister(ctx, architecture); const boxes: DiagramBox[] = components.map((c) => ({ id: c.id, + title: plainTitle(c.label ?? c.id), + detail: `${String(c.members.length)} ${c.members.length === 1 ? "member" : "members"}`, words: `${c.label === null ? "" : `${plainTitle(c.label)}, `}${String(c.members.length)} ${c.members.length === 1 ? "member" : "members"}`, context: c.context, layer: c.layer, @@ -91,7 +94,13 @@ export function architecturePage(ctx: PageContext): string { const edges: DiagramEdge[] = components.flatMap((c) => c.usesCounts .filter((e) => components.some((t) => t.id === e.component)) - .map((e) => ({ from: c.id, to: e.component, kind: "uses", count: e.edges })), + .map((e) => ({ + from: c.id, + to: e.component, + kind: "uses", + claim: "anchored", + count: e.edges, + })), ); const targetsOf = (id: string): string[] => { if (components.some((c) => c.id === id)) return [id]; @@ -100,7 +109,7 @@ export function architecturePage(ctx: PageContext): string { for (const p of plans.unrealized) for (const use of p.uses ?? []) for (const target of targetsOf(use)) - edges.push({ from: plannedKey(p), to: target, kind: "plans to use" }); + edges.push({ from: plannedKey(p), to: target, kind: "plans to use", claim: "declared" }); const diagrams = architectureDiagrams({ boxes, edges }); const contexts = new Set( boxes.flatMap((b) => (b.context === null || b.context === undefined ? [] : [b.context])), @@ -121,6 +130,7 @@ export function architecturePage(ctx: PageContext): string { ) .join(""); const designed = `

Designed, not built

${plannedRows || unreadable ? `
${plannedRows}${unreadable}
DesignComponentContextLayerRolePlans to useAnchor exists without satisfying
` : "

None.

"}
`; + const matrix = usesTable(components); const details = components .map((c) => { const own = new Set(c.members.map((m) => m.id)); @@ -129,6 +139,8 @@ export function architecturePage(ctx: PageContext): string { const memberBoxes: DiagramBox[] = [ ...c.members.map((m) => ({ id: m.id, + title: plainTitle(m.label ?? m.id), + detail: m.role === null ? "" : `role ${m.role}`, words: `${plainTitle(m.label ?? m.id)}${m.role === null ? "" : `, role ${m.role}`}`, target: `#${c.id}`, })), @@ -153,7 +165,7 @@ export function architecturePage(ctx: PageContext): string { ), ); const diff = plans.differing.filter((d) => d.componentId === c.id); - return `

${code(c.id)}${c.label === null ? "" : ` ${codeSpans(c.label)}`}

Layer ${escape(c.layer ?? "not stated")}, context ${escape(c.context ?? "not stated")}, role ${escape(c.role ?? "not stated")}.

${sourceCode(c.file ?? undefined, c.line ?? undefined)}${c.file === null ? "" : copy(location(c))}

Members

${list(c.members.map((m) => `${code(m.id)} ${codeSpans(m.label ?? "")} · role ${escape(m.role ?? "not stated")} · ${sourceCode(m.file ?? undefined, m.line ?? undefined)}${m.file === null ? "" : copy(location(m))}`))}

Uses

${directions(c.usesCounts)}

Used by

${directions(c.usedByCounts)}

Satisfies

${list(c.satisfiedSpecs.map((s) => link(ctx, s)))}

References

${list(c.referencedSpecs.map((s) => link(ctx, s)))}${diff.length ? `

Design differences

${list(diff.map((d) => `${link(ctx, d.spec)} ${code(d.field)}: planned ${code(JSON.stringify(d.planned))}; realized ${code(JSON.stringify(d.realized))}`))}` : ""}${c.members.length === 0 ? "

None.

" : renderDiagram(diagram)}
`; + return `

${code(c.id)}${c.label === null ? "" : ` ${codeSpans(c.label)}`}

Layer ${escape(c.layer ?? "not stated")}, context ${escape(c.context ?? "not stated")}, role ${escape(c.role ?? "not stated")}.

${register.source(c.id)}

Members

${register.members(c.id)}

Uses

${directions(c.usesCounts)}${supportingUses(architecture, c, components)}

Used by

${directions(c.usedByCounts)}${diff.length ? `

Design differences

${list(diff.map((d) => `${link(ctx, d.spec)} ${code(d.field)}: planned ${code(JSON.stringify(d.planned))}; realized ${code(JSON.stringify(d.realized))}`))}` : ""}

Satisfies

${list(c.satisfiedSpecs.map((s) => link(ctx, s)))}

References

${list(c.referencedSpecs.map((s) => link(ctx, s)))}${c.members.length === 0 ? "

None.

" : renderDiagram(diagram)}
`; }) .join(""); const unresolved = [ @@ -168,9 +180,51 @@ export function architecturePage(ctx: PageContext): string { return frame( ctx, "Architecture", - `

Architecture

Components, their members and uses, and planned component designs.

${String(components.length)} components realized and ${String(plans.unrealized.length)} planned. They state ${count(contexts.size, "context", "contexts", false)} and ${count(layers.size, "layer", "layers", false)}. ${String(edges.filter((e) => e.kind === "uses").length)} uses edges between components. ${String(new Set(plans.differing.map((d) => d.spec)).size)} designs differ from their components.

${diagrams.drawings.map((drawing) => renderDiagram(drawing)).join("")}${designed}${details}${unresolved.length ? `

Unresolved structure

${list(unresolved)}
` : ""}`, + `

Architecture

Components, their members and uses, and planned component designs.

${String(components.length)} components realized and ${String(plans.unrealized.length)} planned. They state ${count(contexts.size, "context", "contexts", false)} and ${count(layers.size, "layer", "layers", false)}. ${String(edges.filter((e) => e.kind === "uses").length)} uses edges between components. ${String(new Set(plans.differing.map((d) => d.spec)).size)} designs differ from their components.

${diagrams.drawings.map((drawing) => renderDiagram(drawing)).join("")}${matrix}${designed}${register.index}${details}${register.unowned}${unresolved.length ? `

Unresolved structure

${list(unresolved)}
` : ""}`, ); } function linkTarget(ctx: PageContext, id: string): string { return /href="([^"]+)"/u.exec(link(ctx, id))?.[1] ?? "#main"; } + +const useId = (from: string, to: string) => `uses.${from}.${to}`; +function usesTable(components: Component[]): string { + const layers = ["edge", "application", "domain", "adapter", "infrastructure"]; + const rank = (c: Component) => (c.layer === null ? layers.length : layers.indexOf(c.layer)); + const rows = [...components].sort( + (a, b) => rank(a) - rank(b) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0), + ); + const header = (c: Component) => `${codeSpans(c.label ?? c.id)} ${code(c.id)}`; + return `

Component uses

Rows use columns. · means no anchored use recorded; a component's own cell is empty.

${rows.map((c) => ``).join("")}${rows + .map( + (from) => + `${rows + .map((to) => { + if (from.id === to.id) return ''; + const n = from.usesCounts.find((u) => u.component === to.id)?.edges ?? 0; + return n + ? `` + : ''; + }) + .join("")}`, + ) + .join("")}
Which components use which
Using / used${header(c)}
${header(from)}${String(n)}no anchored use recorded
`; +} +function supportingUses( + architecture: Architecture, + component: Component, + components: Component[], +): string { + const owner = new Map( + components.flatMap( + (c) => [[c.id, c.id], ...c.members.map((m) => [m.id, c.id])] as [string, string][], + ), + ); + return component.usesCounts + .filter((u) => u.component !== component.id && components.some((c) => c.id === u.component)) + .map( + (u) => + `

Uses ${code(u.component)} · ${String(u.edges)} unit edges

${list(architecture.usesEdges.filter((e) => owner.get(e.from) === component.id && owner.get(e.to) === u.component).map((e) => `${code(e.from)} uses ${code(e.to)} · anchored`))}
`, + ) + .join(""); +} diff --git a/src/projections/studio/pages/architecture-register.ts b/src/projections/studio/pages/architecture-register.ts new file mode 100644 index 000000000..82ee5cafa --- /dev/null +++ b/src/projections/studio/pages/architecture-register.ts @@ -0,0 +1,66 @@ +import type { Architecture } from "./architecture-page.js"; +import type { PageContext } from "./html.js"; +import { code, copy, escape, link, list, location, sourceCode } from "./html.js"; +import { codeSpans } from "./code-spans.js"; +import { compare } from "../diagrams/model.js"; + +/** + * @sdpAnchor impl:protocol.studio-unit-register + * @sdpLabel renders component source bindings and role-grouped recorded units + * @sdpSatisfies spec:consumers.spec-studio.lenses.architecture.unit-register + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function unitRegister( + ctx: PageContext, + architecture: Architecture, +): { + index: string; + source: (id: string) => string; + members: (id: string) => string; + unowned: string; +} { + const units = ctx.data.codeUnits; + const row = (id: string): string => { + const unit = units.find((r) => r.id === id); + if (!unit) return code(id); + const bindings = (edge: string) => + list( + (ctx.data.search.records.find((r) => r.id === id)?.bindings ?? []) + .filter((b) => b.edge === edge) + .map((b) => link(ctx, b.specId)), + ); + return `
  • ${code(id)} ${codeSpans(unit.label ?? "")} · ${sourceCode(unit.file, unit.line)}${copy(location(unit.file, unit.line))}

    Satisfies

    ${bindings("satisfies")}

    References

    ${bindings("references")}
  • `; + }; + const groups = (ids: string[]): string => { + const role = (id: string) => units.find((r) => r.id === id)?.role ?? null; + const tokens = [...new Set(ids.map(role))].sort((a, b) => + a === null ? 1 : b === null ? -1 : compare(a, b), + ); + return ( + tokens + .map( + (token) => + `
    Role ${escape(token ?? "not stated")}
      ${ids + .filter((id) => role(id) === token) + .sort(compare) + .map(row) + .join("")}
    `, + ) + .join("") || "

    None.

    " + ); + }; + const declared = architecture.components + .filter((c) => c.declared) + .sort((a, b) => compare(a.id, b.id)); + const members = new Set(declared.flatMap((c) => c.members.map((m) => m.id))); + const unowned = units + .filter((u) => !members.has(u.id) && !declared.some((c) => c.id === u.id)) + .map((u) => u.id); + return { + index: `

    Significant units

    Recorded bindings state no review approval or execution verdict.

    ${list(declared.map((c) => `${code(c.id)}`))}
    `, + source: (id) => `

    Component source binding

      ${row(id)}
    `, + members: (id) => groups(declared.find((c) => c.id === id)?.members.map((m) => m.id) ?? []), + unowned: `

    No component

    ${groups(unowned)}
    `, + }; +} diff --git a/src/projections/studio/pages/capabilities-page.ts b/src/projections/studio/pages/capabilities-page.ts new file mode 100644 index 000000000..754de116f --- /dev/null +++ b/src/projections/studio/pages/capabilities-page.ts @@ -0,0 +1,126 @@ +import { text } from "./text.js"; +import type { PageContext } from "./html.js"; +import { code, count, escape, link, present } from "./html.js"; +import { frame } from "./frame.js"; +import { codeSpans } from "./code-spans.js"; +import { capabilityBranchId, capabilityDiagrams, capabilityWalk } from "../diagrams/capability.js"; +import type { CapabilityMap, CapabilityNode, CapabilityCounts } from "../diagrams/capability.js"; +import { renderDiagram } from "../diagrams/svg.js"; +import { pagePath } from "../paths.js"; + +function specPath(id: string): string { + const path = pagePath(id); + if (path === null) throw new Error(`Spec has no Studio path: ${id}`); + return path; +} +function evidence(ctx: PageContext, node: CapabilityNode): string { + const questions = node.questions + .map((q) => { + const path = specPath(node.id); + const fragment = q.address?.split("#")[1] ?? "section.intent"; + return `
  • ${q.blocking ? "blocking" : "non-blocking"}: ${text(ctx, q.question)}
  • `; + }) + .join(""); + return `${link(ctx, node.id, node.title ?? node.id, codeSpans(node.title ?? node.id))} · ${code(node.id)} · ${escape(node.specKind)} · ${escape(node.altitude)} · stated ${escape(node.statedReadiness)} · floor ${escape(node.floorReached)} · implementation binding ${present(node.deliveryFacts.includes("implemented"))} · verifier binding ${present(node.deliveryFacts.includes("has-verifier"))} · ${count(node.questions.length, "open question")} · ${count(node.questions.filter((q) => q.blocking).length, "blocking question")}${questions ? `
      ${questions}
    ` : ""}`; +} +function rollup(value: CapabilityCounts): string { + return `

    Distinct descendants, excluding this epic: ${count(value.specs, "Spec")} · ${count(value.byKind.example ?? 0, "example")} · ${count(value.byKind.decision ?? 0, "decision")}.

    ${( + [ + ["Stated rungs", value.byStatedReadiness], + ["Floors reached", value.byFloorReached], + ["Kinds", value.byKind], + ["Altitudes", value.byAltitude], + ] as const + ) + .map( + ([label, counts]) => + `
    ${label}
    ${Object.entries(counts) + .map(([key, n]) => `${escape(key)} ${String(n)}`) + .join(" · ")}
    `, + ) + .join( + "", + )}
    Binding evidence
    ${count(value.withImplementationBinding, "Spec")} with implementation binding present · ${count(value.withVerifierBinding, "Spec")} with verifier binding present
    Questions
    ${count(value.openQuestions, "open question")} · ${count(value.blockingQuestions, "blocking question")}
    `; +} +function completeText( + ctx: PageContext, + map: CapabilityMap, + roots: readonly string[], + subject: string, +): string { + const visits = capabilityWalk( + map, + roots, + subject === "unreached" ? new Set(map.unreached.ids) : undefined, + ); + return `
    Complete refinement text
      ${visits + .map((v) => { + const first = capabilityBranchId(subject, v.node.id); + const row = `${evidence(ctx, v.node)}${v.repeat ? ` · ${v.cycle ? "Cyclic link" : "Repeat of"} first occurrence` : ""}`; + const parent = v.parent === null ? "Root" : `Parent: ${link(ctx, v.parent)}`; + return `

      ${parent}

      ${v.node.specKind === "example" || v.node.specKind === "decision" ? `
      ${escape(v.node.specKind === "example" ? "Example" : "Decision")} · ${escape(v.node.title ?? v.node.id)}${row}
      ` : row}`; + }) + .join("")}
    `; +} +const damage = (map: CapabilityMap, ids: ReadonlySet): string => { + const rows = map.damage.filter((d) => ids.has(d.from) || ids.has(d.to)); + const cycles = map.cycles.filter((c) => c.members.some((id) => ids.has(id))); + return ( + (cycles.length + ? `

    Refinement cycles

      ${cycles.map((c) => `
    • Refinement cycle: ${c.members.map(code).join(", ")}
    • `).join("")}
    ` + : "") + + (rows.length + ? `

    Unresolved refinement endpoints

      ${rows.map((d) => `
    • ${escape(d.code)}: ${code(d.from)} refines ${code(d.to)} · unresolved endpoint
    • `).join("")}
    ` + : "") + ); +}; + +/** + * @sdpAnchor impl:protocol.studio-capabilities-page + * @sdpLabel renders epic evidence, refinement and unreached Specs from the capability map + * @sdpSatisfies spec:consumers.spec-studio.capability-map-page + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function capabilitiesPage(ctx: PageContext): string { + const map = ctx.data.recipes.all.get(52) as CapabilityMap; + const nodes = new Map(map.nodes.map((n) => [n.id, n])); + const label = (id: string) => nodes.get(id)?.title ?? id; + const overview = map.epics + .map( + (epic) => + `

    ${codeSpans(label(epic.id))}

    ${code(epic.id)}

    ${epic.parentEpicIds.map((id) => `

    Inside ${link(ctx, id, label(id))}.

    `).join("")}${map.epics + .filter((e) => epic.descendantIds.includes(e.id)) + .map((e) => `

    Contains epic ${link(ctx, e.id, label(e.id))}.

    `) + .join( + "", + )}

    ${count(epic.rollup.specs, "distinct descendant")} · ${count(epic.rollup.byKind.example ?? 0, "example")} · ${count(epic.rollup.byKind.decision ?? 0, "decision")}.

    `, + ) + .join(""); + const sections = map.epics + .map((epic) => { + const ids = new Set([epic.id, ...epic.descendantIds]); + const own = nodes.get(epic.id); + if (!own) throw new Error(`Epic has no capability row: ${epic.id}`); + return `

    ${codeSpans(label(epic.id))}

    Epic's own evidence

    ${evidence(ctx, own)}
    ${rollup(epic.rollup)}

    Show these Specs

    ${capabilityDiagrams( + map, + [epic.id], + epic.id, + (id) => specPath(id), + ) + .map((d) => renderDiagram(d, `${epic.id}.capability`)) + .join("")}${completeText(ctx, map, [epic.id], epic.id)}${damage(map, ids)}
    `; + }) + .join(""); + const unreached = + (map.unreached.ids.length + ? `${capabilityDiagrams(map, map.unreached.roots, "unreached", (id) => specPath(id)) + .map((d) => renderDiagram(d, "unreached.capability")) + .join("")}${completeText(ctx, map, map.unreached.roots, "unreached")}` + : "

    No unreached Specs.

    ") + damage(map, new Set(map.unreached.ids)); + return frame( + ctx, + "Capabilities", + `

    Capabilities

    Epics and their declared refinement, from the capability map.

    Each rollup counts distinct descendants of that epic. Scopes may overlap; epic rows are not added together.

    ${map.epics.length ? `
    ${overview}
    ` : "

    No epics. Authors may organize Specs through declared refinement; the whole corpus is listed below.

    "}${sections}

    No epic reaches these Specs

    Show these Specs

    ${unreached}
    ${damage(map, new Set(map.damage.flatMap((d) => [d.from, d.to])))}`, + ); +} diff --git a/src/projections/studio/pages/code-spans.ts b/src/projections/studio/pages/code-spans.ts index 39b677d2a..552a14e5f 100644 --- a/src/projections/studio/pages/code-spans.ts +++ b/src/projections/studio/pages/code-spans.ts @@ -12,6 +12,7 @@ export function escapeHtml(value: string): string { * @sdpSatisfies spec:consumers.spec-studio.shell * @sdpComponent component:protocol.projections * @sdpRole codec + * @sdpLabel escapes and maps code spans across Studio text */ export function mapCodeSpans( value: string, diff --git a/src/projections/studio/pages/diff-page.ts b/src/projections/studio/pages/diff-page.ts index 86cf1825c..e24fc02d9 100644 --- a/src/projections/studio/pages/diff-page.ts +++ b/src/projections/studio/pages/diff-page.ts @@ -40,7 +40,15 @@ export function packChangeNote(ctx: PageContext, id: string): string { return `

    Changed since the base

    Compared with ${baseLabel(ctx)}. Read the diff.

    ${baseMetadata(ctx)}${changed.length || compared ? `

    Changed members

    ${list(changed.map((s) => `${link(ctx, s.id)}${s.parts.length > 0 && s.parts.every((p) => p.change === "keyed") ? " · keyed only" : ""}`))}` : ""}${added.length || compared ? `

    Added members

    ${list(added.map((s) => link(ctx, s)))}` : ""}${pack !== undefined ? `

    Pack membership

    ${packDelta(ctx, pack)}` : compared ? "

    Pack membership

    No membership or order change.

    " : ""}${diff.diagnostics.length ? `

    Diagnostics

    ${diagnostics(diff)}` : ""}
    `; } function packDelta(ctx: PageContext, pack: NonNullable[number]): string { - return `

    ${escape(pack.change)} · ${link(ctx, pack.id)}

    Members added: ${pack.membersAdded.map((id) => link(ctx, id)).join(", ") || "None"}

    Members removed: ${pack.membersRemoved.map(code).join(", ") || "None"}

    Order ${pack.reordered ? "changed" : "unchanged"}.

    `; + const meaning = + pack.change === "added" && pack.orderAfter === "priority" + ? "

    added as a roadmap

    " + : pack.change === "removed" && pack.orderBefore === "priority" + ? "

    removed as a roadmap

    " + : pack.change === "changed" && pack.orderBefore !== pack.orderAfter + ? `

    ${pack.orderChanged && pack.orderAfter === "priority" ? "now a roadmap" : pack.orderChanged && pack.orderBefore === "priority" ? "no longer a roadmap" : `Order meaning: ${escape(pack.orderBefore ?? "unknown")} → ${escape(pack.orderAfter ?? "unknown")}`}

    ` + : ""; + return `

    ${escape(pack.change)} · ${link(ctx, pack.id)}

    Members added: ${pack.membersAdded.map((id) => link(ctx, id)).join(", ") || "None"}

    Members removed: ${pack.membersRemoved.map(code).join(", ") || "None"}

    Member order ${pack.reordered ? "changed" : "unchanged"}.

    ${meaning}`; } function valueBlock(value: unknown): string { if (Array.isArray(value)) return list(value.map(valueBlock)); @@ -85,6 +93,7 @@ function partValues(part: NonNullable["changed"][number]["parts"] * @sdpSatisfies spec:consumers.spec-studio.diff-page * @sdpComponent component:protocol.projections * @sdpRole projection + * @sdpLabel renders declared design changes and binding changes separately */ export function diffPage(ctx: PageContext): string { const diff = answer(ctx, 35), @@ -96,7 +105,7 @@ export function diffPage(ctx: PageContext): string { return frame( ctx, "Design diff", - `

    Design diff

    ${names}

    The comparison reads the two graphs only. Recipe 35 with detail enabled.

    ${t === undefined ? "" : summary([`${total(t.added, "Spec")} added.`, `${total(t.removed, "Spec")} removed.`, `${total(t.changed, "Spec")} changed.`, `${total(t.packs, "Pack")} with membership or order changes.`, `${total(t.bindings, "Spec")} with binding changes.`])}

    Specs added

    ${list((diff.specs?.added ?? []).map((id) => `${titleLink(ctx, id)} · ${code(id)}`))}

    Specs removed

    ${list((diff.specs?.removed ?? []).map(code))}

    Specs changed

    ${ + `

    Design diff

    ${names}

    The comparison reads the two graphs only. Recipe 35 with detail enabled.

    ${t === undefined ? "" : summary([`${total(t.added, "Spec")} added.`, `${total(t.removed, "Spec")} removed.`, `${total(t.changed, "Spec")} changed.`, `${total(t.packs, "Pack")} with membership, member order or order meaning changes.`, `${total(t.bindings, "Spec")} with binding changes.`])}

    Specs added

    ${list((diff.specs?.added ?? []).map((id) => `${titleLink(ctx, id)} · ${code(id)}`))}

    Specs removed

    ${list((diff.specs?.removed ?? []).map(code))}

    Specs changed

    ${ (diff.specs?.changed ?? []) .map( (s) => @@ -109,7 +118,7 @@ export function diffPage(ctx: PageContext): string { .join("")}`, ) .join("") || "

    None.

    " - }

    Packs with membership or order changes

    ${(diff.packs ?? []).map((p) => `
    ${packDelta(ctx, p)}
    `).join("") || "

    None.

    "}

    Bindings, derived from anchors

    ${ + }

    Packs with membership, member order or order meaning changes

    ${(diff.packs ?? []).map((p) => `
    ${packDelta(ctx, p)}
    `).join("") || "

    None.

    "}

    Bindings, derived from anchors

    ${ (diff.bindings ?? []) .map( (b) => diff --git a/src/projections/studio/pages/export-page.ts b/src/projections/studio/pages/export-page.ts index 942b9d5f8..e90788a63 100644 --- a/src/projections/studio/pages/export-page.ts +++ b/src/projections/studio/pages/export-page.ts @@ -19,6 +19,19 @@ function decode(value: string): string { } const mainContent = (html: string): string => /
    ([\s\S]*)<\/main>/u.exec(html)?.[1] ?? ""; +function pageIdentities(pages: readonly StudioPage[]): ReadonlyMap { + return new Map( + pages + .map( + (page) => + [ + page.path, + /
    ([^<]*)<\/code>/u.exec(page.content)?.[1], + ] as const, + ) + .reverse(), + ); +} /** * @sdpAnchor impl:protocol.studio-pack-export * @sdpLabel copies rendered canvas content into one inline-styled document with prefixed element references @@ -30,18 +43,21 @@ export function packExport( pack: PackContext, pages: readonly StudioPage[], stylesheet: string, + linkedPages: readonly StudioPage[] = pages, + linkedIdentities = pageIdentities(linkedPages), ): StudioPage { + const byPath = new Map(pages.map((page) => [page.path, page] as const).reverse()); const packPath = pagePath(pack.id) ?? ""; const contents = [ { path: packPath, prefix: "pack/", - html: pages.find((p) => p.path === packPath)?.content ?? "", + html: byPath.get(packPath)?.content ?? "", id: pack.id, }, ...pack.members.flatMap((member) => { const path = pagePath(member.id); - const page = pages.find((p) => p.path === path); + const page = path === null ? undefined : byPath.get(path); return page ? [{ path: page.path, prefix: `${member.id}/`, html: page.content, id: member.id }] : []; @@ -130,9 +146,7 @@ export function packExport( decode(href), `https://studio.invalid/${content.path}`, ).pathname.slice(1); - const graphId = pages - .find((p) => p.path === path) - ?.content.match(/
    ([^<]*)<\/code>/u)?.[1]; + const graphId = linkedIdentities.get(path); return `${escape(graphId ? decode(graphId) : dest.text)}`; }, ); @@ -177,5 +191,8 @@ export function exportPages( return { path, content: specPage({ data: graphOnly, path }, spec) }; }), ]; - return data.packs.map(({ context }) => packExport(context, sources, stylesheet)); + const identities = pageIdentities(pages); + return data.packs.map(({ context }) => + packExport(context, sources, stylesheet, pages, identities), + ); } diff --git a/src/projections/studio/pages/frame.ts b/src/projections/studio/pages/frame.ts index 30a363208..230a02c3d 100644 --- a/src/projections/studio/pages/frame.ts +++ b/src/projections/studio/pages/frame.ts @@ -6,9 +6,10 @@ import { codeSpans, plainTitle } from "./code-spans.js"; /** * Writes the shared navigation and the rail appropriate to the selected graph node. * @sdpAnchor impl:protocol.studio-frame - * @sdpSatisfies spec:consumers.spec-studio.shell, spec:consumers.spec-studio.responsive, spec:consumers.spec-studio.intent-panel, spec:consumers.spec-studio, spec:consumers.spec-studio.lenses, spec:consumers.spec-studio.navigation + * @sdpSatisfies spec:consumers.spec-studio.shell, spec:consumers.spec-studio.responsive, spec:consumers.spec-studio.intent-panel, spec:consumers.spec-studio, spec:consumers.spec-studio.lenses, spec:consumers.spec-studio.navigation, spec:consumers.spec-studio.capability-map-page * @sdpComponent component:protocol.projections * @sdpRole projection + * @sdpLabel renders Studio navigation, page rails and composing controls */ export function frame(ctx: PageContext, title: string, body: string, selected?: string): string { const prefix = relative(ctx, "index.html").replace(/index\.html$/u, ""); @@ -19,6 +20,7 @@ export function frame(ctx: PageContext, title: string, body: string, selected?: : selectedSpec ? ctx.data.rail.packs.filter((pack) => selectedSpec.packs.includes(pack.id)) : ctx.data.rail.packs; + const specIds = new Set(ctx.data.specs.map((spec) => spec.id)); const packRail = packs .map( (pack) => @@ -27,7 +29,7 @@ export function frame(ctx: PageContext, title: string, body: string, selected?: ? `
      ${pack.members .map((member) => { const path = pagePath(member.id); - const resolved = ctx.data.specs.some((s) => s.id === member.id); + const resolved = specIds.has(member.id); const label = codeSpans(member.title ?? member.id); return `
    1. ${isPack ? `${label}` : path !== null && resolved ? `${label}` : label}
    2. `; }) @@ -47,9 +49,11 @@ export function frame(ctx: PageContext, title: string, body: string, selected?: return `\n${title === "Spec Studio" ? "Spec Studio" : `${escape(plainTitle(title))} · Spec Studio`}
      Spec Studio
    `; + const lists = populated + .map( + (c) => + `
    stated ${c.stated}, floor ${c.floor} · ${count(c.members.length, "member", "members", false)}
    `, + ) + .join(""); + const unresolved = pack.members.filter((m) => !m.resolved); + const sourceId = `${prefix}.source-text`; + const source = [ + "block-beta", + " columns 6", + ' axis["Stated / floor"]', + ...floors.map((f, i) => ` h${String(i)}["${f}"]`), + ...SPEC_READINESS.flatMap((s, row) => [ + ` r${String(row)}["${s}"]`, + ...cells + .filter((c) => c.stated === s) + .flatMap((c) => [ + ` c${String(c.row)}${String(c.column)}["${String(c.members.length)}"]`, + ...c.members.map( + (m) => + ` %% ${mermaidText(c.id)}: ${mermaidText(m.id)} · ${mermaidText(plainTitle(m.title ?? m.id))}`, + ), + ]), + ]), + ` %% Accessible count table: ${prefix}.text`, + ].join("\n"); + return `

    Maturity map

    ${sentence}${uniform ? `
    Stated readiness by floor reached${grid}
    ` : grid}
    Maturity map as text${table}
    ${lists}${unresolved.length ? `

    Unresolved members

    ` : ""}
    Mermaid source

    Preferred syntax: block. Fixed categorical cells; membership appears in comments.

    `; } diff --git a/src/projections/studio/pages/pack-page.ts b/src/projections/studio/pages/pack-page.ts index 32d55ad7e..461e2024a 100644 --- a/src/projections/studio/pages/pack-page.ts +++ b/src/projections/studio/pages/pack-page.ts @@ -1,3 +1,4 @@ +import { slicePath } from "../paths.js"; import { exportPath } from "./export-page.js"; import { maturityMap } from "./maturity-map.js"; import { packChangeNote } from "./diff-page.js"; @@ -70,6 +71,7 @@ function memberCard(ctx: PageContext, member: PackMemberSummary, index: number): * @sdpSatisfies spec:consumers.spec-studio.pack-page, spec:consumers.spec-studio.export, spec:consumers.spec-studio.components * @sdpComponent component:protocol.projections * @sdpRole projection + * @sdpLabel renders Pack members, maturity and boundary joins */ export function packPage(ctx: PageContext, pack: PackContext, totals: PackTotals): string { const held = pack.members.filter((m) => (m.statedNextRungFailures?.length ?? 0) > 0); @@ -84,7 +86,7 @@ export function packPage(ctx: PageContext, pack: PackContext, totals: PackTotals ) .join(", "); const summary = [ - `${count(totals.members, "member")} in authored reading order; ${count(totals.unresolved, "unresolved member", "unresolved members", false)}.`, + `${count(totals.members, "member")} ${pack.order === "priority" ? "in priority order" : "in authored reading order"}; ${count(totals.unresolved, "unresolved member", "unresolved members", false)}.`, `${count(totals.heldBelowNextRung, "member")} held below their stated next rung${held.length ? `: ${cards(held, true)}` : ""}.`, `${count(totals.waitingForAuthor, "member")} waiting only for their author's statement${waiting.length && waiting.length !== pack.members.length ? `: ${cards(waiting)}` : ""}.`, `${count(totals.entries, "design entry", "design entries")} and ${count(totals.declarations, "pinned declaration", "pinned declarations", false)}.`, @@ -110,12 +112,12 @@ export function packPage(ctx: PageContext, pack: PackContext, totals: PackTotals return frame( ctx, pack.title ?? pack.id, - `

    ${codeSpans(pack.title ?? pack.id)}

    ${code(pack.id)}${copy(pack.id)}${code(pack.file)}${copy(pack.file)}

    Download this Pack as one file

    ${codeSpans(pack.title ?? pack.id)}

    ${code(pack.id)}${copy(pack.id)}${code(pack.file)}${copy(pack.file)}

    Read this Pack as one design

    Download this Pack as one file

    Where the design stands

      ${summary.map((s) => `
    • ${s}
    • `).join("")}
    ${maturityMap(pack)}

    Framing

    ${prose(ctx, pack.framing)}${pack.modelRefs.length === 0 ? "" : `

    Vocabulary: ${pack.modelRefs.map((id) => link(ctx, id)).join(", ")}

    `}${packChangeNote(ctx, pack.id)}

    Members

    ${pack.members.map((m, i) => memberCard(ctx, m, i)).join("")}

    Boundary

    ${boundary}

    Members with no verifier binding

    The graph records bindings and never runs${ctx.data.recipes.all.has(45) ? "" : ", so no page shows a run result"}. Decision records are left out: a decision never needs a verifier binding.${ctx.data.recipes.all.has(45) ? ` The runs page, the tests lens and the Spec pages of the Specs their files verify show the run records as external evidence from test files.` : ""}

    ${list(gaps.map((g) => `${link(ctx, g.id, ctx.data.specs.find((s) => s.id === g.id)?.title ?? g.id, codeSpans(ctx.data.specs.find((s) => s.id === g.id)?.title ?? g.id))} · ${code(g.id)} · stated ${escape(g.statedReadiness ?? "not recorded")}`))}

    Findings

    ${findingList( + )}">

    Where the design stands

      ${summary.map((s) => `
    • ${s}
    • `).join("")}
    ${maturityMap(pack)}

    Framing

    ${prose(ctx, pack.framing)}${pack.modelRefs.length === 0 ? "" : `

    Vocabulary: ${pack.modelRefs.map((id) => link(ctx, id)).join(", ")}

    `}${packChangeNote(ctx, pack.id)}

    Members

    ${pack.members.map((m, i) => memberCard(ctx, m, i)).join("")}

    Boundary

    ${boundary}

    Members with no verifier binding

    The graph records bindings and never runs${ctx.data.recipes.all.has(45) ? "" : ", so no page shows a run result"}. Decision records are left out: a decision never needs a verifier binding.${ctx.data.recipes.all.has(45) ? ` The runs page, the tests lens and the Spec pages of the Specs their files verify show the run records as external evidence from test files.` : ""}

    ${list(gaps.map((g) => `${link(ctx, g.id, ctx.data.specs.find((s) => s.id === g.id)?.title ?? g.id, codeSpans(ctx.data.specs.find((s) => s.id === g.id)?.title ?? g.id))} · ${code(g.id)} · stated ${escape(g.statedReadiness ?? "not recorded")}`))}

    Findings

    ${findingList( ctx, ctx.data.findings.filter((f) => f.subjectId === pack.id), )}
    `, diff --git a/src/projections/studio/pages/queue-page.ts b/src/projections/studio/pages/queue-page.ts index cd9fa2de7..3ef895632 100644 --- a/src/projections/studio/pages/queue-page.ts +++ b/src/projections/studio/pages/queue-page.ts @@ -11,6 +11,7 @@ import { SPEC_READINESS } from "../../../model/descriptors.js"; * @sdpSatisfies spec:consumers.spec-studio.owner-queue * @sdpComponent component:protocol.projections * @sdpRole projection + * @sdpLabel renders waiting readiness statements and decider questions */ export function queuePage(ctx: PageContext): string { const q = answer(ctx, 46), diff --git a/src/projections/studio/pages/register-answers.ts b/src/projections/studio/pages/register-answers.ts index 59fcb520e..6b88bf7dc 100644 --- a/src/projections/studio/pages/register-answers.ts +++ b/src/projections/studio/pages/register-answers.ts @@ -1,6 +1,13 @@ import type { PageContext } from "./html.js"; // The shipped bodies own these answers. Pages select and format fields, without recomputing them. +/** + * @sdpAnchor impl:protocol.studio-register-contracts + * @sdpLabel shared recipe answer shapes for Studio registers + * @sdpReferences spec:consumers.spec-studio.data.recipe-answers + * @sdpComponent component:protocol.projections + * @sdpRole contract + */ interface RegisterAnswers { 20: Questions; 24: Declarations; @@ -9,6 +16,113 @@ interface RegisterAnswers { 35: Diff; 46: Queue; 51: Citations; + 53: Roadmaps; +} + +export type RoadmapNeed = "to design" | "to state" | "to build" | "built"; +interface RoadmapHold { + readonly clause: string; + readonly targets: readonly string[]; +} +export interface RoadmapPart { + readonly id: string; + readonly need: RoadmapNeed; + readonly statedReadiness: string; + readonly floorReached: string | null; + readonly statedNextRung: string | null; + readonly holdsNextRung: readonly RoadmapHold[]; + readonly drift: boolean; +} +export interface RoadmapQuestion { + readonly spec: string; + readonly address: string | null; + readonly index: number; + readonly question: string; + readonly blocking: boolean; + readonly decider: string | null; + readonly provisional: string | null; +} +interface RoadmapRelation { + readonly id: string; + readonly via: string; + readonly need: string; + readonly onRoadmap: number | null; +} +export interface RoadmapEntry extends Omit { + readonly place: number; + readonly title: string | null; + readonly specKind: string; + readonly altitude: string; + readonly work: { + readonly specs: number; + readonly toDesign: number; + readonly toState: number; + readonly toBuild: number; + readonly built: number; + readonly drift: number; + }; + readonly workRows: readonly RoadmapPart[]; + readonly buildNow: readonly string[]; + readonly blockingQuestions: readonly RoadmapQuestion[]; + readonly decisions: readonly { + readonly id: string; + readonly statedReadiness: string; + readonly blockingQuestions: readonly RoadmapQuestion[]; + }[]; + readonly restsOn: readonly (RoadmapRelation & { + readonly from: readonly string[]; + readonly holdsReady: boolean; + })[]; + readonly restedOnBy: readonly RoadmapRelation[]; + readonly earlierStatements: readonly { + readonly id: string; + readonly where: string; + readonly onRoadmap: number | null; + }[]; + readonly packs: readonly string[]; + readonly sharesScopeWith: readonly number[]; +} +export interface Roadmap { + readonly pack: string; + readonly title: string | null; + readonly framing: string | null; + readonly order: "priority" | "reading"; + readonly totals: { + readonly entries: number; + readonly toDesign: number; + readonly toState: number; + readonly toBuild: number; + readonly built: number; + readonly apart: number; + readonly questions: number; + readonly blockingQuestions: number; + readonly withoutDecider: number; + readonly blockingWithoutDecider: number; + readonly statements: number; + }; + readonly entries: readonly RoadmapEntry[]; + readonly unresolved: readonly { readonly place: number; readonly id: string }[]; + readonly apart: readonly { + readonly place: number; + readonly id: string; + readonly specKind: string; + readonly statedReadiness: string; + readonly need: string; + }[]; + readonly waiting: { + readonly questions: readonly (RoadmapQuestion & { readonly places: readonly number[] })[]; + readonly withoutDecider: readonly (RoadmapQuestion & { readonly places: readonly number[] })[]; + readonly statements: readonly { + readonly id: string; + readonly statedReadiness: string; + readonly floorReached: string; + readonly places: readonly number[]; + }[]; + }; +} +export interface Roadmaps { + readonly decider: string; + readonly roadmaps: readonly Roadmap[]; } export function answer( ctx: PageContext, @@ -170,6 +284,9 @@ export interface Diff { readonly membersAdded: readonly string[]; readonly membersRemoved: readonly string[]; readonly reordered: boolean; + readonly orderChanged: boolean; + readonly orderBefore: "reading" | "priority" | null; + readonly orderAfter: "reading" | "priority" | null; }[]; readonly bindings?: readonly { readonly id: string; diff --git a/src/projections/studio/pages/registers-page.ts b/src/projections/studio/pages/registers-page.ts index 94101d886..1cb671ebc 100644 --- a/src/projections/studio/pages/registers-page.ts +++ b/src/projections/studio/pages/registers-page.ts @@ -49,6 +49,7 @@ function decisionRelations( * @sdpSatisfies spec:consumers.spec-studio.registers * @sdpComponent component:protocol.projections * @sdpRole projection + * @sdpLabel renders question, decision, declaration and backlog registers */ export function questionsPage(ctx: PageContext): string { const result = answer(ctx, 20); diff --git a/src/projections/studio/pages/requirement-view.ts b/src/projections/studio/pages/requirement-view.ts new file mode 100644 index 000000000..2139d7133 --- /dev/null +++ b/src/projections/studio/pages/requirement-view.ts @@ -0,0 +1,112 @@ +import type { SpecContext } from "../../../reader/reader.js"; +import { requirementDiagram } from "../diagrams/requirement.js"; +import { renderDiagram } from "../diagrams/svg.js"; +import type { DiagramBox, DiagramEdge } from "../diagrams/model.js"; +import { code, link, list } from "./html.js"; +import type { PageContext } from "./html.js"; +import { plainTitle } from "./code-spans.js"; +/** + * @sdpAnchor impl:protocol.studio-requirement-view + * @sdpLabel selects Spec refiners, verifying examples and their recorded bindings + * @sdpSatisfies spec:consumers.spec-studio.diagram-kinds.requirement-trace, spec:consumers.spec-studio.spec-page + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function requirementView(ctx: PageContext, spec: SpecContext): string { + const intended = new Set([ + spec.id, + ...spec.relationsIn.filter((r) => r.type === "refines").map((r) => r.otherId), + ]); + const examples = new Set( + ctx.data.specs + .filter( + (s) => + s.specKind === "example" && + s.relationsOut.some((r) => r.type === "verifies" && intended.has(r.otherId)), + ) + .map((s) => s.id), + ); + const selected = new Set([...intended, ...examples]); + const edges: DiagramEdge[] = ctx.data.specs.flatMap((s) => + s.relationsOut + .filter( + (r) => + ["refines", "verifies"].includes(r.type) && + (selected.has(s.id) || selected.has(r.otherId)), + ) + .map((r) => ({ + from: s.id, + to: r.otherId, + kind: r.type, + claim: r.claim, + ...(r.reason === undefined ? {} : { reason: r.reason }), + })), + ); + for (const unit of ctx.data.search.records) + for (const binding of unit.bindings) + if ( + selected.has(binding.specId) && + ["satisfies", "verifies", "references", "models"].includes(binding.edge) + ) + edges.push({ from: unit.id, to: binding.specId, kind: binding.edge, claim: "anchored" }); + const ids = new Set([...selected, ...edges.flatMap((e) => [e.from, e.to])]); + const boxes: DiagramBox[] = [...ids].map((id) => { + const s = ctx.data.specs.find((s) => s.id === id); + const unit = ctx.data.search.records.find((r) => r.id === id); + const isExample = examples.has(id) || s?.specKind === "example"; + const test = edges.some((e) => e.to === id && e.kind === "verifies" && e.claim === "anchored"); + const satisfying = (s?.implementations.length ?? 0) > 0; + const satisfiable = !isExample && s?.specKind !== "decision"; + const claim = + s || id.startsWith("spec:") + ? "declared Spec" + : id.startsWith("oracle:") + ? "anchored oracle binding" + : id.startsWith("test:") + ? "anchored test binding · binding present" + : "anchored code"; + const absence = s + ? `${isExample && !test ? " · no test binding" : ""}${selected.has(id) && satisfiable && !satisfying ? " · no satisfying binding" : ""}` + : ""; + const title = plainTitle(s?.title ?? unit?.label ?? id); + return { + id, + title, + detail: `${claim}${absence}`, + words: `${title} · ${claim}${absence}`, + target: s ? (/href="([^"]+)"/u.exec(link(ctx, id))?.[1] ?? "#main") : "#bindings", + ...(s + ? {} + : { + source: { + ...(unit?.file === undefined ? {} : { file: unit.file }), + ...(unit?.line === undefined ? {} : { line: unit.line }), + }, + }), + band: + !selected.has(id) && (s || id.startsWith("spec:")) + ? "Outside the selected set" + : isExample + ? "Declared verifying examples" + : intended.has(id) + ? "Intended Specs" + : id.startsWith("impl:") || id.startsWith("api:") || id.startsWith("component:") + ? "Satisfying or referencing units" + : "Test or oracle bindings", + }; + }); + const empty = [ + ...(![...selected].some((id) => + ctx.data.specs.some((s) => s.id === id && s.specKind === "example"), + ) + ? ["Declared verifying examples: no declared verifying example."] + : []), + ...(!edges.some((e) => e.claim === "anchored" && ["satisfies", "references"].includes(e.kind)) + ? ["Satisfying or referencing units: no recorded binding."] + : []), + ...(!edges.some((e) => e.claim === "anchored" && ["verifies", "models"].includes(e.kind)) + ? ["Test or oracle bindings: no recorded binding."] + : []), + ]; + return `

    Requirement drawing

    ${empty.length ? list(empty) : ""}${renderDiagram(requirementDiagram({ subject: spec.id, boxes, edges }), `${spec.id}.requirement`)}${edges.length ? "" : `

    ${code(spec.id)} has no requirement edges recorded.

    `}`; +} diff --git a/src/projections/studio/pages/roadmap-page.ts b/src/projections/studio/pages/roadmap-page.ts new file mode 100644 index 000000000..618a1b732 --- /dev/null +++ b/src/projections/studio/pages/roadmap-page.ts @@ -0,0 +1,192 @@ +import { frame } from "./frame.js"; +import { badge, code, copy, escape, link, list, relative, rungLine, verb } from "./html.js"; +import type { PageContext } from "./html.js"; +import { answer } from "./register-answers.js"; +import type { + Roadmap, + RoadmapEntry, + RoadmapNeed, + RoadmapPart, + RoadmapQuestion, +} from "./register-answers.js"; +import { filtered, filterBar, summary, titleLink, total } from "./register-common.js"; +import { text, codeSpans } from "./text.js"; +import { pagePath } from "../paths.js"; + +const needs = ["to design", "to state", "to build", "built"] as const; +const labels = ["To design", "To state", "To build", "Built"] as const; +const slug = (need: RoadmapNeed) => need.replaceAll(" ", "-"); +const prefix = (pack: string) => `roadmap-${pagePath(pack)?.slice(6, -5) ?? ""}`; +const entryId = (pack: string, place: number) => `${prefix(pack)}-entry-${String(place)}`; +const entryLink = (pack: string, place: number, labelHtml = escape(`entry ${String(place)}`)) => + `${labelHtml}`; +const via: Readonly> = { + dependsOn: "depends on", + constrainedBy: "constrained by", + decidedBy: "decided by", + refines: "refines", +}; + +function holds(ctx: PageContext, rows: RoadmapPart["holdsNextRung"]): string { + return rows.length === 0 + ? "nothing blocks" + : `held by ${rows.map((row) => `${code(row.clause)}${row.targets.length ? ` (${row.targets.map((id) => link(ctx, id)).join(", ")})` : ""}`).join(", ")}`; +} +function questionLink(ctx: PageContext, q: RoadmapQuestion): string { + return q.address === null + ? `${link(ctx, q.spec)} question ${String(q.index + 1)}` + : `${link(ctx, q.address)}${copy(q.address)}`; +} +function entryRow(ctx: PageContext, pack: string, entry: RoadmapEntry): string { + const fields: string[] = []; + const field = (label: string, value: string) => { + if (value) fields.push(`
    ${label}: ${value}
    `); + }; + field("Id", link(ctx, entry.id)); + if (entry.statedNextRung !== null) + field( + "Next", + `${badge(entry.statedReadiness)} → ${badge(entry.statedNextRung)} · ${holds(ctx, entry.holdsNextRung)}`, + ); + const part = (row: RoadmapPart) => + `${link(ctx, row.id, row.id === entry.id ? "itself" : row.id)} · ${escape(row.need)} · ${rungLine(row.statedReadiness, row.floorReached ?? "none")}${row.statedNextRung === null ? "" : ` → ${badge(row.statedNextRung)} · ${holds(ctx, row.holdsNextRung)}`}${row.drift ? " · drift" : ""}`; + const setting = entry.workRows.filter((row) => row.need === entry.need); + const ordered = [ + ...setting.filter((row) => row.id === entry.id), + ...setting.filter((row) => row.id !== entry.id), + ]; + const rest = entry.workRows.filter((row) => row.need !== entry.need); + if (entry.workRows.length > 1 || setting.some((row) => row.id !== entry.id)) + field( + "Parts", + `${list(ordered.map(part))}${rest.length ? `
    ${total(rest.length, "other part")}${list(rest.map(part))}
    ` : ""}`, + ); + field("Build now", entry.buildNow.map((id) => titleLink(ctx, id)).join(", ")); + field( + "Blocked by", + listIf( + entry.blockingQuestions.map( + (q) => `${questionLink(ctx, q)} · ${escape(q.decider ?? "no decider")}`, + ), + ), + ); + field( + "Decided by", + listIf( + entry.decisions.map( + (d) => + `${link(ctx, d.id)} ${badge(d.statedReadiness)}${d.blockingQuestions.length ? ` · blocked by ${d.blockingQuestions.map((q) => questionLink(ctx, q)).join(", ")}` : ""}`, + ), + ), + ); + const restItem = (row: RoadmapEntry["restsOn"][number]) => + `${link(ctx, row.id)} · ${escape(via[row.via] ?? row.via)}${row.from.some((id) => id !== entry.id) ? ` from ${row.from.map((id) => link(ctx, id, id === entry.id ? "itself" : id)).join(", ")}` : ""} · ${escape(row.need)}${row.holdsReady ? " · holds ready" : ""} · ${row.onRoadmap === null ? "outside the roadmap" : entryLink(pack, row.onRoadmap)}`; + const prominent = entry.restsOn.filter( + (row) => row.need === "to design" || row.onRoadmap !== null || row.holdsReady, + ); + const folded = entry.restsOn.filter((row) => !prominent.includes(row)); + field( + "Rests on", + `${listIf(prominent.map(restItem))}${folded.length ? `
    ${total(folded.length, "more dependency", "more dependencies")}${list(folded.map(restItem))}
    ` : ""}`, + ); + field( + "Waited on by", + listIf( + entry.restedOnBy.map( + (row) => + `${link(ctx, row.id)} · ${escape(via[row.via] ?? row.via)} · ${escape(row.need)}${row.onRoadmap === null ? "" : ` · ${entryLink(pack, row.onRoadmap)}`}`, + ), + ), + ); + if (entry.need === "to state") + field( + "Earlier statements", + entry.earlierStatements.length + ? list( + entry.earlierStatements.map( + (row) => + `${link(ctx, row.id)} · ${row.where === "entry" ? "in this entry" : row.where === "roadmap" && row.onRoadmap !== null ? entryLink(pack, row.onRoadmap) : "outside the roadmap"}`, + ), + ) + : "", + ); + field("Also in", entry.packs.map((id) => link(ctx, id)).join(", ")); + field( + "Shares Specs with", + entry.sharesScopeWith.map((place) => entryLink(pack, place)).join(", "), + ); + const work = entry.work; + const counts = [work.toDesign, work.toState, work.toBuild, work.built].flatMap((n, at) => + n ? [`${String(n)} ${needs[at] ?? ""}`] : [], + ); + return `
  • ${String(entry.place)}. ${codeSpans(entry.title ?? entry.id)}${escape(entry.specKind)} · ${escape(entry.altitude)} · ${rungLine(entry.statedReadiness, entry.floorReached ?? "none")} · ${total(work.specs, "Spec")}: ${counts.join(", ")}${work.drift ? ` · ${String(work.drift)} drift` : ""}
    ${fields.join("")}
  • `; +} +const listIf = (rows: readonly string[]) => (rows.length ? list(rows) : ""); + +function roadmapSection(ctx: PageContext, r: Roadmap, memo: string): string { + const t = r.totals; + const question = (q: RoadmapQuestion & { readonly places: readonly number[] }) => + `

    ${q.blocking ? "blocking" : "non-blocking"} · ${questionLink(ctx, q)}

    ${text(ctx, q.question)}

    ${q.provisional === null ? "" : `

    Provisional: ${text(ctx, q.provisional)}

    `}

    Touches ${q.places.map((p) => entryLink(r.pack, p)).join(", ")}

    `; + const board = `
    ${needs + .map((need, at) => { + const rows = r.entries.filter((entry) => entry.need === need); + return `

    ${labels[at] ?? ""} ${String(rows.length)}

    ${rows.length ? `
      ${rows.map((entry) => `
    1. ${String(entry.place)}${entryLink(r.pack, entry.place, codeSpans(entry.title ?? entry.id))}
    2. `).join("")}
    ` : "

    none

    "}
    `; + }) + .join("")}
    `; + const lists = needs + .map((need, at) => { + const entries = r.entries.filter((entry) => entry.need === need); + return `

    ${labels[at] ?? ""}

    ${entries.length ? `
      ${entries.map((entry) => entryRow(ctx, r.pack, entry)).join("")}
    ` : `

    No entry has the need ${escape(need)}.

    `}`; + }) + .join(""); + const filters = filterBar({ need: { label: "Need", values: needs, ordered: true } }).replace( + /value="(to design|to state|to build)"/gu, + (_, need: RoadmapNeed) => `value="${slug(need)}"`, + ); + const waiting = [ + ...r.waiting.questions.map(question), + ...r.waiting.statements.map( + (s) => + `

    Statement · ${link(ctx, s.id)} states ${badge(s.statedReadiness)}, floor allows ${badge(s.floorReached)} · ${s.places.map((p) => entryLink(r.pack, p)).join(", ")}

    `, + ), + ]; + const memoId = `${prefix(r.pack)}-memo`; + const headingId = `${prefix(r.pack)}-heading`; + return `

    ${link(ctx, r.pack, r.title ?? r.pack, codeSpans(r.title ?? r.pack))}

    ${( + r.framing ?? "" + ) + .split(/\n\s*\n/u) + .filter((p) => p.trim()) + .map((p) => `

    ${text(ctx, p)}

    `) + .join("")}${summary([ + `${total(t.entries, "entry", "entries")} ${r.order === "priority" ? "in priority order" : "in authored reading order"}: ${String(t.toDesign)} to design, ${String(t.toState)} to state, ${String(t.toBuild)} to build, ${String(t.built)} built, ${String(t.apart)} listed apart${r.unresolved.length ? `, ${String(r.unresolved.length)} unresolved` : ""}.`, + `${String(t.blockingQuestions)} blocking and ${String(t.questions - t.blockingQuestions)} non-blocking questions wait on the owner; ${String(t.withoutDecider)} name no decider, ${String(t.blockingWithoutDecider)} of them blocking.`, + `${total(t.statements, "statement")} ${verb(t.statements, "wait")} for a person; the owner queue lists them all.`, + ])}${board}

    ${escape("To state means the floor allows ready and a person still judges the statement. Built means every Spec of the entry's work states ready and has an implementation binding; it reports no passing test and no deployment. The entry's own readiness is shown separately.")}

    ${filtered(lists, filters, r.entries.length)}${r.apart.length ? `

    Listed apart

    ${list(r.apart.map((row) => `${String(row.place)}. ${titleLink(ctx, row.id)} · ${escape(row.specKind)} · ${badge(row.statedReadiness)} · ${escape(row.need)}`))}` : ""}${r.unresolved.length ? `

    Unresolved members

    ${list(r.unresolved.map((row) => `${String(row.place)}. ${code(row.id)}`))}` : ""}

    Waiting on the owner

    ${waiting.length ? list(waiting) : "

    Nothing in these entries waits on the owner.

    "}${r.waiting.withoutDecider.length ? `

    Questions without a decider

    These questions have no named decider.

    ${list(r.waiting.withoutDecider.map(question))}` : ""}
    The forward section, as Markdown
    ${copy(memo)}
    ${escape(memo)}
    `; +} + +/** + * @sdpAnchor impl:protocol.studio-roadmap-page + * @sdpLabel renders roadmap needs, holding parts and the shared Markdown memo + * @sdpSatisfies spec:consumers.spec-studio.roadmap-page, spec:consumers.roadmap.memo + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function roadmapPage(ctx: PageContext): string { + const roadmaps = answer(ctx, 53).roadmaps; + return frame( + ctx, + "Roadmap", + `

    Roadmap

    What comes next, from the graph. A place is advisory priority: never a date, a deadline or permission to start.

    ${ + roadmaps.length + ? roadmaps + .map((r) => { + const memo = ctx.data.recipes.roadmapMemos.get(r.pack); + if (memo === undefined) throw new Error(`Roadmap memo is missing: ${r.pack}`); + return roadmapSection(ctx, r, memo); + }) + .join("") + : "

    This corpus states no roadmap. A Pack becomes one when its manifest states order: priority.

    " + }`, + ); +} diff --git a/src/projections/studio/pages/search-page.ts b/src/projections/studio/pages/search-page.ts index cb6cd6bb9..88bb667ed 100644 --- a/src/projections/studio/pages/search-page.ts +++ b/src/projections/studio/pages/search-page.ts @@ -8,6 +8,7 @@ import type { PageContext } from "./html.js"; * @sdpSatisfies spec:consumers.spec-studio.search * @sdpComponent component:protocol.projections * @sdpRole projection + * @sdpLabel publishes search records beside the browser results view */ export function searchPage(ctx: PageContext): string { return frame( diff --git a/src/projections/studio/pages/slice-page.ts b/src/projections/studio/pages/slice-page.ts new file mode 100644 index 000000000..975b02f79 --- /dev/null +++ b/src/projections/studio/pages/slice-page.ts @@ -0,0 +1,185 @@ +import { text } from "./text.js"; +import type { SliceAnswer, SliceMember, SliceDesign } from "../slice-answer.js"; +import type { PageContext } from "./html.js"; +import { code, copy, escape, link, list, relative, rungLine, componentLink } from "./html.js"; +import { frame } from "./frame.js"; +import { codeSpans } from "./text.js"; +import { pagePath, slicePath } from "../paths.js"; +import { dependencyDiagram, scopedArchitectureDiagrams } from "../diagrams/slice.js"; +import { renderDiagram } from "../diagrams/index.js"; +const value = (v: unknown): string => + v === null || v === undefined + ? "not stated" + : Array.isArray(v) + ? v.join(", ") || "none" + : typeof v === "string" + ? v + : JSON.stringify(v); +const lines = (rows: readonly string[]) => + rows.map((s) => `${s}`).join(""); +function table(label: string, heads: readonly string[], rows: readonly string[], cls = ""): string { + return !rows.length + ? "

    None.

    " + : `
    ${heads.map((h) => `${h}`).join("")}${rows.join("")}
    `; +} +/** + * @sdpAnchor impl:protocol.studio-slice-page + * @sdpLabel design slice summaries, member needs, architecture comparisons and boundary tables + * @sdpSatisfies spec:consumers.spec-studio.slice-page + * @sdpComponent component:protocol.projections + * @sdpRole projection + */ +export function slicePage(ctx: PageContext, a: SliceAnswer): string { + const subject = a.scope.pack; + const packPath = pagePath(subject); + if (packPath === null) throw new Error(`Slice has no Pack page: ${subject}`); + const members = new Map(a.members.map((m) => [m.id, m])); + const memberLink = (id: string) => + `${codeSpans(members.get(id)?.title ?? id)}`; + const ids = (rows: readonly string[], member = false) => + rows.length ? rows.map((id) => (member ? memberLink(id) : link(ctx, id))).join(", ") : "none"; + const target = (id: string) => + id.startsWith("component:") + ? `${relative(ctx, "architecture.html")}#${id}` + : members.has(id) + ? `${relative(ctx, packPath)}#${id}` + : relative(ctx, pagePath(id) ?? packPath); + const next = (m: SliceMember) => + m.statedNextRung === null + ? "" + : `next ${escape(m.statedNextRung)}: ${!m.holds.length ? "nothing blocks" : ""}${m.holds.length ? list(m.holds.map((h) => `${code(h.clauseId)} · ${escape(h.description)}${h.targets.length ? list(h.targets.map((t) => `${escape(t.type)} · ${link(ctx, t.id)} · stated ${escape(t.statedReadiness ?? "not recorded")}`)) : ""}`)) : ""}`; + const identity = (m: SliceMember) => + `${memberLink(m.id)}
    ${code(m.id)}
    ${escape(m.specKind)}`; + const rows = a.members + .filter((m) => m.exampleOf === null) + .sort((x, y) => x.depth - y.depth) + .map((m) => { + const carried = [ + `${String(m.rules.total)} rules${m.rules.keyed ? `, ${String(m.rules.keyed)} keyed` : ""}`, + `${String(m.design.entries)} entries`, + `${String(m.design.declarations)} declarations`, + `${String(m.design.openQuestions)} questions${m.design.blockingQuestions ? `, ${String(m.design.blockingQuestions)} blocking` : ""}`, + `${String(m.design.decisions)} decisions`, + `${String(m.examples.enabled)} of ${String(m.examples.total)} verifying examples bound`, + ]; + const examples = m.exampleMembers + .map((id) => members.get(id)) + .filter((e): e is SliceMember => e !== undefined), + held = examples.filter((e) => e.holds.length).length; + return `${identity(m)}${String(m.depth)}${rungLine(m.statedReadiness, m.floorReached ?? "none")}${next(m)}${lines(carried)}${lines([`${String(m.implementedBy.length)} implementing`, `${String(m.referencedBy.length)} referencing`])}${!examples.length ? "" : `
    ${String(examples.length)} example members folded in${held ? `, ${String(held)} held below the next rung` : ""}${list(examples.map((e) => `${identity(e)} · ${rungLine(e.statedReadiness, e.floorReached ?? "none")}${e.statedNextRung === null ? "" : ` · ${next(e)}`}`))}
    `}`; + }); + const arch = a.architecture; + const touched = table( + "Components the slice touches", + ["Component", "Layer and context", "Reached through"], + arch.components.map( + (c) => + `${componentLink(ctx, c.id)}${c.label ? `
    ${escape(c.label)}` : ""}${escape(c.layer ?? "not stated")} · ${escape(c.context ?? "not stated")}${lines([`Members its code satisfies: ${ids(c.members.satisfies, true)}`, `Members its code references: ${ids(c.members.references, true)}`, `What the slice rests on, realized here: ${ids(c.restsOn)}`, `Member designs naming it: ${ids(c.designs)}`])}`, + ), + ); + const comparison = (d: SliceDesign) => + lines([ + ...(!d.realized + ? ["not compared"] + : !d.differing.length && !d.unreadable.length + ? ["matches"] + : []), + ...d.differing.map((f) => + f.field === "uses" + ? `uses: only declared ${escape(value(f.planned))}; only anchored ${escape(value(f.realized))}` + : `${escape(f.field)}: declared ${escape(value(f.planned))}; anchored ${escape(value(f.realized))}`, + ), + ...d.unreadable.map((u) => `${escape(u.key)}: unreadable ${escape(u.value)}, not compared`), + ]); + const designs = table( + "Member component designs", + ["Design", "Component", "Declared", "Anchor", "Comparison"], + arch.designs.map( + (d) => + `${link(ctx, d.spec, members.get(d.spec)?.title ?? d.spec)}
    ${code(d.spec)}${code(d.componentId)}${lines( + (["layer", "context", "role", "uses"] as const).map((key) => { + const unreadable = d.unreadable.find((u) => u.key === key); + return `${key}: ${escape(unreadable ? `unreadable ${unreadable.value}` : value(d[key]))}`; + }), + )}${d.realized ? "satisfies the design" : d.anchorExists ? "present, does not satisfy the design" : "absent"}${comparison(d)}`, + ), + ); + const reachWords: Record = { + touchedUses: "a touched component uses it", + usesTouched: "it uses a touched component", + plannedUse: "a member design plans to use it", + }; + const architecture = + !arch.components.length && !arch.designs.length + ? `

    No component is touched and no member designs a component. ${String(a.boundary.restsOn.filter((b) => !b.implemented).length)} Specs the slice rests on have no implementation binding.

    ` + : scopedArchitectureDiagrams(subject, arch, target) + .map((d, i) => + renderDiagram( + d, + `${subject}.architecture${i ? `.${encodeURIComponent(d.occurrenceKey ?? String(i))}` : ""}`, + ), + ) + .join(""); + const decisions = table( + "Decisions in scope", + ["Decision", "Rung", "In the slice", "Shapes", "Questions"], + a.decisions.map( + (d) => + `${link(ctx, d.id, d.title ?? d.id)}
    ${code(d.id)}${d.refines.length ? `
    refines ${ids(d.refines)}` : ""}${d.supersedes.length ? `
    supersedes ${ids(d.supersedes)}` : ""}${rungLine(d.statedReadiness, d.floorReached ?? "none")}${d.member ? "member" : "outside"}${ids(d.shapes, true)}${String(d.openQuestions)} open, ${String(d.blockingQuestions)} blocking`, + ), + ); + const questions = list( + [...a.questions] + .sort((x, y) => Number(y.blocking) - Number(x.blocking)) + .map( + (q) => + `${q.blocking ? "blocking" : "non-blocking"} · ${q.address ? `${link(ctx, q.address, q.address.split("#")[1])}${copy(q.address)} · ` : ""}${link(ctx, q.spec)} · ${text(ctx, q.question)}${lines([...(q.decider ? [`Decider: ${escape(q.decider)}`] : []), ...(q.links?.length ? [`Links: ${ids(q.links)}`] : []), ...(q.provisional ? [`Provisional: ${text(ctx, q.provisional)}`] : []), ...(q.trigger ? [`Trigger: ${text(ctx, q.trigger)}`] : []), ...(q.holds.length ? [`Holds: ${ids(q.holds, true)}`] : [])])}`, + ), + "questions", + ); + const boundary = ( + [ + ["restsOn", "Rests on"], + ["restedOnBy", "Rested on by"], + ["verifiedBy", "Verified by"], + ["supersededBy", "Superseded by"], + ] as const + ) + .map( + ([key, label]) => + `
    ${label} (${String(a.boundary[key].length)})
    ${table( + label, + ["Spec", "Rung", "Implementation binding", "Joined members", "Holds"], + a.boundary[key].map( + (b) => + `${link(ctx, b.id, b.title ?? b.id)}
    ${code(b.id)}${rungLine(b.statedReadiness ?? undefined, b.floorReached ?? "none")}${b.implemented ? "present" : "none"}${lines(b.via.map((v) => `${escape(v.type)}: ${ids(v.members, true)}`))}${ids(b.holds, true)}`, + ), + )}
    `, + ) + .join(""); + const t = a.totals; + const count = (n: number, noun: string) => `${String(n)} ${noun}${n === 1 ? "" : "s"}`; + const summary = [ + `${count(t.members, "member")}; ${count(t.exampleMembers, "example member")} counted in their parents.`, + `${count(t.relations, "relation")} among members; deepest depth ${String(t.depth)}; ${count(t.cycles, "cycle")}.`, + `${String(t.heldBelowNextRung)} member${t.heldBelowNextRung === 1 ? "" : "s"} held below their stated next rung; ${String(t.waitingForAuthor)} waiting only for their author.`, + `${count(t.restsOn, "outside Spec")} the slice rests on; ${String(t.holdingOutside)} ${t.holdingOutside === 1 ? "holds" : "hold"} a member; ${count(t.restedOnBy, "outside Spec")} ${t.restedOnBy === 1 ? "rests" : "rest"} on it.`, + `${count(t.components.touched, "component")} touched: ${String(t.components.throughMembers)} through members' code, ${String(t.components.throughRestsOn)} through what the slice rests on, ${String(t.components.throughDesigns)} named by member designs; ${count(t.components.outside, "outside component")} shown for context.`, + `${count(t.designs.total, "member component design")}; ${String(t.designs.realized)} realized; ${String(t.designs.matching)} of those match.`, + `${count(t.decisions, "decision")} in scope; ${String(t.decisionsBelowReady)} ${t.decisionsBelowReady === 1 ? "states" : "state"} below ready.`, + `${count(t.openQuestions, "open question")}; ${String(t.blockingQuestions)} blocking.`, + ].map(escape); + const missing = a.scope.unresolved; + if (missing.length > 0) + summary.splice( + 1, + 0, + `${count(missing.length, "manifest member")} ${missing.length === 1 ? "resolves" : "resolve"} to no Spec: ${missing.map((id) => code(id)).join(", ")}.`, + ); + return frame( + ctx, + `Design slice: ${a.scope.title ?? subject}`, + `

    Design slice: ${codeSpans(a.scope.title ?? subject)}

    ${code(subject)}${copy(subject)}

    Read the Pack member by member

    The design as a whole

    ${list(summary, "summary")}

    Members read with the ones others rest on first. The columns count the design each member carries beside its rung; they never judge whether it is enough. This page records no review and approves nothing.

    How the members rest on each other

    ${renderDiagram(dependencyDiagram(subject, a, target), `${subject}.dependencies`)}

    What each member needs next

    ${table("What each member needs next", ["Member", "Depth", "Rung", "Next rung", "Design carried", "Code"], rows, "slice-members")}

    The architecture it touches

    ${architecture}

    Components the slice touches

    ${touched}

    Member component designs

    ${designs}

    Outside the slice, shown for context and not counted as touched

    ${list(arch.outside.map((c) => `${c.anchored ? componentLink(ctx, c.id) : code(c.id)}${c.label ? ` · ${escape(c.label)}` : ""} · ${c.anchored ? "anchored" : "not anchored"} · ${c.reachedBy.map((h) => escape(reachWords[h] ?? h)).join("; ")}`))}

    Units bound to a member and to no component

    ${list(arch.unplaced.map((u) => `${code(u.unit)} ${u.via === "satisfies" ? "implements" : "references"} ${memberLink(u.member)}`))}

    Decisions in scope

    ${decisions}

    Open questions in scope

    ${questions}

    Where the slice meets the rest

    ${boundary}
    `, + ); +} +export { slicePath }; diff --git a/src/projections/studio/pages/spec-page.ts b/src/projections/studio/pages/spec-page.ts index a16a226b9..1a69c99b8 100644 --- a/src/projections/studio/pages/spec-page.ts +++ b/src/projections/studio/pages/spec-page.ts @@ -204,6 +204,7 @@ function bindings(ctx: PageContext, spec: SpecContext): string { * @sdpSatisfies spec:consumers.spec-studio.spec-page.core, spec:consumers.spec-studio.spec-page * @sdpComponent component:protocol.projections * @sdpRole projection + * @sdpLabel renders Spec intent, relations and realization bindings */ export function specPage(ctx: PageContext, spec: SpecContext): string { const d = spec.designColumns; diff --git a/src/projections/studio/pages/spec-structure.ts b/src/projections/studio/pages/spec-structure.ts index bc066da3c..13e03dad1 100644 --- a/src/projections/studio/pages/spec-structure.ts +++ b/src/projections/studio/pages/spec-structure.ts @@ -1,19 +1,10 @@ +import { requirementView } from "./requirement-view.js"; import type { SpecContext } from "../../../reader/reader.js"; import type { Architecture } from "./architecture-page.js"; -import { realizationDiagram, renderDiagram, traceDiagram } from "../diagrams/index.js"; -import type { DiagramBox, DiagramEdge } from "../diagrams/index.js"; +import { renderDiagram, traceDiagram } from "../diagrams/index.js"; +import type { DiagramBox } from "../diagrams/index.js"; import type { PageContext } from "./html.js"; -import { - code, - sourceCode, - copy, - escape, - link, - list, - location, - relative, - componentLink, -} from "./html.js"; +import { code, sourceCode, copy, escape, link, list, location, componentLink } from "./html.js"; import { plainTitle } from "./code-spans.js"; import { codeSpans, prose } from "./text.js"; @@ -53,42 +44,12 @@ function structure(ctx: PageContext, spec: SpecContext) { /** * @sdpAnchor impl:protocol.studio-spec-structure * @sdpSatisfies spec:consumers.spec-studio.spec-page, spec:consumers.spec-studio.components - * @sdpLabel renders realization and trace diagrams and the recorded structural neighborhood + * @sdpLabel renders requirement and trace diagrams and the recorded structural neighborhood * @sdpComponent component:protocol.projections * @sdpRole projection */ export function designView(ctx: PageContext, spec: SpecContext): string { - const { units, own, components, uses } = structure(ctx, spec); - const selected = new Set([...own, ...components.map((e) => e.to), ...uses.map((e) => e.to)]); - const boxes: DiagramBox[] = [...selected].sort(compare).map((id) => { - const unit = units.get(id); - return { - id, - words: unit - ? `${plainTitle(unit.label ?? "")} ${location(unit.file ?? undefined, unit.line ?? undefined)}`.trim() - : "unresolved", - target: - id.startsWith("component:") && units.has(id) - ? `${relative(ctx, "architecture.html")}#${id}` - : "#panel.runtime", - band: own.has(id) - ? "Satisfying units" - : components.some((e) => e.to === id) - ? "Components" - : "Uses", - }; - }); - const edges: DiagramEdge[] = [...components, ...uses].map((e) => ({ - from: e.from, - to: e.to, - kind: e.type, - })); - const drawing = boxes.length - ? renderDiagram( - realizationDiagram({ subject: spec.id, sectionId: null, boxes, edges }), - `${spec.id}.realization`, - ) - : "

    No implementation binding recorded.

    "; + const drawing = requirementView(ctx, spec); const decisions = spec.relationsOut.filter((r) => r.type === "decidedBy"); return `${drawing}

    Decisions

    ${ decisions.length @@ -151,8 +112,20 @@ interface Impact { } export function impactView(ctx: PageContext, spec: SpecContext): string { const relations = [ - ...spec.relationsOut.map((r) => ({ from: spec.id, to: r.otherId, kind: r.type })), - ...spec.relationsIn.map((r) => ({ from: r.otherId, to: spec.id, kind: r.type })), + ...spec.relationsOut.map((r) => ({ + from: spec.id, + to: r.otherId, + kind: r.type, + claim: r.claim, + ...(r.reason === undefined ? {} : { reason: r.reason }), + })), + ...spec.relationsIn.map((r) => ({ + from: r.otherId, + to: spec.id, + kind: r.type, + claim: r.claim, + ...(r.reason === undefined ? {} : { reason: r.reason }), + })), ]; const ids = new Set([spec.id, ...relations.flatMap((r) => [r.from, r.to])]); const boxes: DiagramBox[] = [...ids].sort(compare).map((id) => ({ diff --git a/src/projections/studio/pages/text.ts b/src/projections/studio/pages/text.ts index ab125b899..73a4d7234 100644 --- a/src/projections/studio/pages/text.ts +++ b/src/projections/studio/pages/text.ts @@ -10,6 +10,7 @@ export { codeSpans } from "./code-spans.js"; * @sdpSatisfies spec:consumers.spec-studio.shell * @sdpComponent component:protocol.projections * @sdpRole codec + * @sdpLabel escapes prose and links graph-resolving mentions */ export function text(ctx: PageContext, value: string): string { const mentions = (part: string): string => { diff --git a/src/projections/studio/paths.ts b/src/projections/studio/paths.ts index 839c05e6e..b25fdb966 100644 --- a/src/projections/studio/paths.ts +++ b/src/projections/studio/paths.ts @@ -4,7 +4,7 @@ * @sdpLabel encodes uppercase id letters for collision-free Pack and Spec page paths * @sdpSatisfies spec:consumers.spec-studio.publisher * @sdpComponent component:protocol.projections - * @sdpRole utility + * @sdpRole codec */ export function pagePath(id: string): string | null { const namespace = id.startsWith("spec:") ? "specs" : id.startsWith("pack:") ? "packs" : null; @@ -14,3 +14,9 @@ export function pagePath(id: string): string | null { .replace(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`); return `${namespace}/${name}.html`; } + +export function slicePath(id: string): string { + const path = pagePath(id); + if (path === null || !id.startsWith("pack:")) throw new Error(`Slice has no Pack path: ${id}`); + return path.replace(/^packs\//u, "slices/"); +} diff --git a/src/projections/studio/recipes.ts b/src/projections/studio/recipes.ts index a94bf95bf..2b2107639 100644 --- a/src/projections/studio/recipes.ts +++ b/src/projections/studio/recipes.ts @@ -3,12 +3,15 @@ import { evaluateBodyWithReader } from "../../reader/evaluate-body.js"; import type { Reader } from "../../reader/reader.js"; import type { ValidationReport } from "../../validate/contracts.js"; import { graphReportId } from "../../validate/validators.js"; +import type { Roadmaps } from "./pages/register-answers.js"; export interface StudioRecipeAnswers { readonly all: ReadonlyMap; readonly packs: ReadonlyMap; + readonly slices: ReadonlyMap; readonly packDiffs: ReadonlyMap; readonly specs: ReadonlyMap>; + readonly roadmapMemos: ReadonlyMap; } type RecipeCaller = ( @@ -35,7 +38,7 @@ export async function evaluateStudioRecipe( * @sdpReferences spec:decisions.studio-build-renders * @sdpComponent component:protocol.projections * @sdpUses impl:protocol.reader, api:protocol.evaluate-body - * @sdpRole projection + * @sdpRole service */ export async function studioRecipeAnswers( reader: Reader, @@ -46,20 +49,32 @@ export async function studioRecipeAnswers( reader, graph: reader.graph, report, + memoizeParameterlessRecipes: true, }); if (typeof value !== "function") throw new Error("The body evaluator returned no recipe caller."); const evaluate = value as RecipeCaller; const all = new Map(); const packs = new Map(); + const slices = new Map(); const packDiffs = new Map(); const specs = new Map>(); + const roadmapMemos = new Map(); for (const number of [17, 20, 24, 31, 33, 43, 44, 46, 51]) all.set(number, await evaluate(number, {})); + all.set(52, await evaluate(52, { detail: true })); + const roadmaps = (await evaluate(53, {})) as Roadmaps; + all.set(53, roadmaps); + for (const roadmap of roadmaps.roadmaps) { + const result = (await evaluate(55, { pack: roadmap.pack })) as { memo: string | null }; + if (result.memo === null) throw new Error(`Roadmap memo could not render: ${roadmap.pack}`); + roadmapMemos.set(roadmap.pack, result.memo); + } if (inputs.runs.length > 0) all.set(45, await evaluate(45, { runs: inputs.runs })); if (inputs.base !== undefined) all.set(35, await evaluate(35, { base: inputs.base.graph, detail: true })); for (const pack of reader.packs()) { packs.set(pack.id, await evaluate(29, { pack: pack.id })); + slices.set(pack.id, await evaluate(54, { pack: pack.id, detail: true })); if (inputs.base !== undefined) packDiffs.set( pack.id, @@ -73,5 +88,5 @@ export async function studioRecipeAnswers( if (spec.specKind !== "example") answers.set(34, await evaluate(34, { spec: spec.id })); specs.set(spec.id, answers); } - return { all, packs, packDiffs, specs }; + return { all, packs, slices, packDiffs, specs, roadmapMemos }; } diff --git a/src/projections/studio/slice-answer.ts b/src/projections/studio/slice-answer.ts new file mode 100644 index 000000000..17a464c5e --- /dev/null +++ b/src/projections/studio/slice-answer.ts @@ -0,0 +1,199 @@ +/** The full answer of the shipped design-slice recipe, shared by its page and drawings. + * @sdpAnchor impl:protocol.studio-slice-answer + * @sdpLabel full design slice answer consumed by the page and drawings + * @sdpReferences spec:consumers.design-slice + * @sdpComponent component:protocol.projections + * @sdpRole contract + */ +export interface SliceMember { + readonly id: string; + readonly title: string | null; + readonly specKind: string; + readonly statedReadiness: string; + readonly floorReached: string | null; + readonly statedNextRung: string | null; + readonly depth: number; + readonly exampleOf: string | null; + readonly exampleMembers: readonly string[]; + readonly holds: readonly { + readonly clauseId: string; + readonly description: string; + readonly targets: readonly { + readonly id: string; + readonly type: string; + readonly statedReadiness: string | null; + }[]; + }[]; + readonly design: { + readonly entries: number; + readonly declarations: number; + readonly openQuestions: number; + readonly blockingQuestions: number; + readonly decisions: number; + }; + readonly rules: { readonly total: number; readonly keyed: number }; + readonly examples: { readonly total: number; readonly enabled: number }; + readonly implementedBy: readonly string[]; + readonly referencedBy: readonly string[]; +} +export interface SliceBoundary { + readonly id: string; + readonly title: string | null; + readonly statedReadiness: string | null; + readonly floorReached: string | null; + readonly implemented: boolean; + readonly via: readonly { + readonly type: string; + readonly members: readonly string[]; + readonly reasons?: readonly { readonly member: string; readonly reason: string }[]; + }[]; + readonly holds: readonly string[]; +} +export interface SliceEdge { + readonly from: string; + readonly to: string; + readonly type: string; + readonly orders: boolean; + readonly reason?: string; +} +export interface SliceDesign { + readonly spec: string; + readonly componentId: string; + readonly layer: string | null; + readonly context: string | null; + readonly role: string | null; + readonly uses: readonly string[] | null; + readonly anchorExists: boolean; + readonly realized: boolean; + readonly differing: readonly { + readonly field: string; + readonly planned: unknown; + readonly realized: unknown; + }[]; + readonly unreadable: readonly { readonly key: string; readonly value: string }[]; +} +export interface SliceArchitecture { + readonly components: readonly { + readonly id: string; + readonly label: string | null; + readonly layer: string | null; + readonly context: string | null; + readonly members: { + readonly satisfies: readonly string[]; + readonly references: readonly string[]; + }; + readonly restsOn: readonly string[]; + readonly designs: readonly string[]; + }[]; + readonly designs: readonly SliceDesign[]; + readonly outside: readonly { + readonly id: string; + readonly label: string | null; + readonly layer: string | null; + readonly context: string | null; + readonly anchored: boolean; + readonly reachedBy: readonly string[]; + }[]; + readonly uses: readonly { + readonly from: string; + readonly to: string; + readonly count: number; + readonly outside: boolean; + readonly units: readonly { + readonly from: string; + readonly to: string; + readonly claim: string; + }[]; + }[]; + readonly plannedUses: readonly { + readonly design: string; + readonly from: string; + readonly to: string; + readonly outside: boolean; + }[]; + readonly unplaced: readonly { + readonly unit: string; + readonly member: string; + readonly via: string; + }[]; +} +export interface SliceAnswer { + readonly scope: { + readonly pack: string; + readonly title: string | null; + readonly members: readonly string[]; + readonly unresolved: readonly string[]; + }; + readonly totals: { + readonly members: number; + readonly exampleMembers: number; + readonly relations: number; + readonly depth: number; + readonly cycles: number; + readonly heldBelowNextRung: number; + readonly waitingForAuthor: number; + readonly restsOn: number; + readonly holdingOutside: number; + readonly restedOnBy: number; + readonly components: { + readonly touched: number; + readonly throughMembers: number; + readonly throughRestsOn: number; + readonly throughDesigns: number; + readonly outside: number; + }; + readonly designs: { + readonly total: number; + readonly realized: number; + readonly matching: number; + }; + readonly decisions: number; + readonly decisionsBelowReady: number; + readonly openQuestions: number; + readonly blockingQuestions: number; + }; + readonly members: readonly SliceMember[]; + readonly structure: { + readonly edges: readonly SliceEdge[]; + readonly unresolved: readonly { + readonly from: string; + readonly to: string; + readonly type: string; + readonly reason?: string; + }[]; + readonly cycles: readonly { + readonly members: readonly string[]; + readonly cycle: readonly string[]; + }[]; + }; + readonly architecture: SliceArchitecture; + readonly decisions: readonly { + readonly id: string; + readonly title: string | null; + readonly statedReadiness: string; + readonly floorReached: string | null; + readonly member: boolean; + readonly shapes: readonly string[]; + readonly refines: readonly string[]; + readonly supersedes: readonly string[]; + readonly openQuestions: number; + readonly blockingQuestions: number; + }[]; + readonly questions: readonly { + readonly address: string | null; + readonly spec: string; + readonly question: string; + readonly blocking: boolean; + readonly decider?: string; + readonly links?: readonly string[]; + readonly provisional?: string; + readonly trigger?: string; + readonly holds: readonly string[]; + }[]; + readonly boundary: { + readonly restsOn: readonly SliceBoundary[]; + readonly restedOnBy: readonly SliceBoundary[]; + readonly verifiedBy: readonly SliceBoundary[]; + readonly supersededBy: readonly SliceBoundary[]; + }; +} diff --git a/src/projections/studio/studio.css b/src/projections/studio/studio.css index 070d9b8e5..a32f624c9 100644 --- a/src/projections/studio/studio.css +++ b/src/projections/studio/studio.css @@ -669,7 +669,7 @@ sdp-diagram figcaption { margin-bottom: 0.75rem; } sdp-diagram svg { - max-width: 100%; + max-width: none; height: auto; display: block; background: var(--surface); @@ -680,6 +680,22 @@ sdp-diagram svg text { 12px ui-monospace, monospace; } +sdp-diagram .box-title { + font-size: 14px; +} +sdp-diagram .box-id { + font-size: 12px; + fill: var(--muted); +} +sdp-diagram textarea { + display: block; + width: 100%; + box-sizing: border-box; +} +.diagram-scroll:focus-visible { + outline: 2px solid var(--link); + outline-offset: 2px; +} sdp-diagram .band > text, sdp-diagram .lane > text { fill: var(--muted); @@ -844,6 +860,9 @@ sdp-compose button { margin: 0.25rem; } @media (max-width: 719.98px) { + .scroll-help { + display: none; + } sdp-diagram svg { max-width: 100%; height: auto; @@ -928,3 +947,258 @@ sdp-compose button { border-color: #777; } } + +@media print { + sdp-diagram details[id$=".source"]:not([open]), + sdp-maturity-map details[id$=".source"]:not([open]) { + display: none !important; + } + sdp-diagram details[id$=".source"][open] textarea, + sdp-maturity-map details[id$=".source"][open] textarea { + display: block !important; + } +} + +.capability-overview { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); + gap: 1rem; +} +.capability-overview > .card { + padding: 1rem; +} +.capability-section { + margin-block: 2rem; +} +.capability-counts > div { + margin-block: 0.5rem; +} +.capability-counts dd { + margin-inline-start: 0; +} +.capability-walk { + list-style: none; + padding: 0; +} +.capability-walk > li { + margin-block: 1rem; + margin-inline-start: calc(var(--depth) * 0.6rem); + padding-inline-start: 0.6rem; + border-inline-start: 1px solid var(--border); +} +.capability-overview, +.capability-evidence, +.capability-complete, +.capability-counts { + overflow-wrap: anywhere; + min-width: 0; +} +/* Roadmap places stay visible across the board and the need lists. */ +.roadmap-board { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.75rem; + margin: 1rem 0; +} +.board-column { + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--surface); + padding: 0.75rem; + min-width: 0; +} +.board-column h3 { + margin: 0 0 0.5rem; +} +.board-column ol, +.roadmap-list { + list-style: none; + margin: 0; + padding: 0; +} +.board-column li { + display: grid; + grid-template-columns: 1.5rem minmax(0, 1fr); + margin-bottom: 0.5rem; + line-height: 1.4; +} +.board-column .place { + color: var(--muted); + font-weight: 600; + font-variant-numeric: tabular-nums; +} +.roadmap .floor { + white-space: normal; +} +@media (max-width: 719.98px) { + .roadmap-board { + grid-template-columns: 1fr; + } +} + +.reading-note { + max-width: var(--measure); + color: var(--muted); +} +.slice-members { + table-layout: fixed; +} +.slice-members th:nth-child(1) { + width: 27%; +} +.slice-members th:nth-child(2) { + width: 5%; +} +.slice-members th:nth-child(3) { + width: 9%; +} +.slice-members th:nth-child(4) { + width: 19%; +} +.slice-members th:nth-child(5) { + width: 26%; +} +.slice-members th:nth-child(6) { + width: 14%; +} +.slice-members .field { + display: block; + white-space: nowrap; +} +.slice-members td { + overflow-wrap: anywhere; +} +.slice-members td.member-cell { + min-width: 0; +} +.slice-members td.next { + min-width: 0; +} +.slice-members tr.folded > td { + padding-top: 0; +} +.slice-members tr.folded summary { + color: var(--muted); +} +@media (max-width: 719.98px) { + .slice-table-scroll { + overflow: visible; + } + .slice-members thead { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + } + .slice-members, + .slice-members tbody, + .slice-members td { + display: block; + width: auto; + min-width: 0; + } + .slice-members tr { + display: flex; + flex-direction: column; + } + .slice-members tr.member { + border-top: 1px solid var(--line); + padding-top: 0.75rem; + } + .slice-members td { + border: 0; + padding: 0.2rem 0; + } + .slice-members td.depth { + order: 4; + } + .slice-members td.carried { + order: 5; + } + .slice-members td.code-cell { + order: 6; + } + .slice-members td[data-label]::before { + content: attr(data-label) ": "; + color: var(--muted); + font-weight: 600; + } + .slice-members .field { + display: inline; + white-space: normal; + } + .slice-members .field + .field::before { + content: " · "; + color: var(--muted); + } + .slice-members tr.folded > td { + padding-bottom: 0.75rem; + } +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} +.component-uses td { + position: relative; + text-align: center; + min-width: 3rem; + font-variant-numeric: tabular-nums; +} +.component-uses .uses-self { + background: var(--surface-2); +} +[data-unit] { + overflow-wrap: anywhere; + margin-block: 1rem; +} +sdp-maturity-map svg { + display: block; + max-width: 100%; + height: auto; + fill: var(--text); + font-size: 14px; +} +sdp-maturity-map .maturity-cell rect { + fill: var(--surface); + stroke: var(--muted); + stroke-opacity: 0.3; +} +sdp-maturity-map .maturity-cell.matches rect { + stroke: var(--text); + stroke-opacity: 1; + stroke-width: 2; +} +sdp-maturity-map .maturity-cell.populated rect { + fill: var(--surface-2); + stroke-opacity: 1; +} +sdp-maturity-map .maturity-cell.solid rect { + fill: var(--text); +} +sdp-maturity-map .solid .cell-count { + fill: var(--bg); +} +sdp-maturity-map .cell-count { + font-weight: 650; +} +@media print { + .component-uses { + table-layout: fixed; + font-size: 0.7rem; + } + .component-uses th, + .component-uses td, + .component-uses code { + min-width: 0; + overflow-wrap: anywhere; + } +} diff --git a/src/projections/vault/notes.ts b/src/projections/vault/notes.ts index 5742ae46b..06c8a67cc 100644 --- a/src/projections/vault/notes.ts +++ b/src/projections/vault/notes.ts @@ -91,6 +91,7 @@ export function renderVault( aliases: [node.id, ...(node.title === undefined ? [] : [node.title])], title: node.title, manifest: node.file, + order: node.order, }), banner("Pack manifest", node.file), "", @@ -102,7 +103,13 @@ export function renderVault( "", `![[${notePath(node.id).slice(0, -3)}.base]]`, "", - "## Members, in reading order", + ...(node.order === "priority" + ? [ + "Priority confers no permission or sequencing requirement.", + "", + "## Members, in advisory priority order", + ] + : ["## Members, in reading order"]), "", ]; context.members.forEach((m, i) => diff --git a/src/reader/concept-match.ts b/src/reader/concept-match.ts index e6e60db27..615fe646b 100644 --- a/src/reader/concept-match.ts +++ b/src/reader/concept-match.ts @@ -46,7 +46,8 @@ function contains(value: unknown, needle: string, includeKeys: boolean): boolean * @sdpAnchor impl:protocol.studio-concept-match * @sdpSatisfies spec:consumers.spec-studio.search * @sdpComponent component:protocol.reader - * @sdpRole utility + * @sdpRole reader + * @sdpLabel matches concepts deterministically across graph and browser queries */ export function matchConcept( records: readonly ConceptRecord[], diff --git a/src/reader/evaluate-body.ts b/src/reader/evaluate-body.ts index 0d2a5beac..140008a3e 100644 --- a/src/reader/evaluate-body.ts +++ b/src/reader/evaluate-body.ts @@ -7,8 +7,6 @@ import { createReader } from "./reader.js"; import type { Reader } from "./reader.js"; import type { ValidationReport } from "../validate/contracts.js"; import { validateGraph } from "../validate/validators.js"; -import { codeAnchorId, componentAnchorId, ref } from "../ids.js"; -import { codeAnchor } from "../model/code-anchor.js"; function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); @@ -85,21 +83,19 @@ export class BodyParseError extends Error { } } -const queryAnswerShapeAnchor = codeAnchor({ - id: codeAnchorId("impl:protocol.query-answer-shapes"), - label: "reports parse positions in the caller's query body", - satisfies: ref("spec:consumers.agent-surface.evaluation-sink"), - component: componentAnchorId("component:protocol.reader"), - role: "service", -}); -void queryAnswerShapeAnchor; - function plainObject(value: unknown): value is Readonly> { if (typeof value !== "object" || value === null) return false; const prototype: unknown = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } +function freezeAnswer(value: unknown, seen = new Set()): unknown { + if (typeof value !== "object" || value === null || seen.has(value)) return value; + seen.add(value); + for (const child of Object.values(value)) freezeAnswer(child, seen); + return Object.freeze(value); +} + // Both source tests and bundled entries read this package's built bodies. Walking from the // module also works for the CommonJS main entry, whose bundle supplies __dirname. function recipeDirectory(): string { @@ -152,12 +148,14 @@ export async function evaluateBodyWithReader( graph: GraphSchema; report: ValidationReport; params?: Readonly>; + memoizeParameterlessRecipes?: boolean; }, ): Promise { const { reader: g, graph, report, params = {} } = input; if (!plainObject(params)) throw new Error("evaluateBody: params takes one parameters object"); const compiled = compileBody(body); const recipeBodies = new Map(); + const answers = new Map>(); const recipes: RecipeCaller = async (number, parameters = {}) => { if (!Number.isInteger(number)) throw new Error(`recipes: ${String(number)} is not a recipe number`); @@ -174,6 +172,15 @@ export async function evaluateBodyWithReader( recipeBody = compileBody(readFileSync(join(directory, file), "utf8")); recipeBodies.set(number, recipeBody); } + if (input.memoizeParameterlessRecipes && Object.keys(parameters).length === 0) { + const previous = answers.get(number); + if (previous !== undefined) return previous; + const answer = recipeBody(g, graph, report, parameters, recipes).then((value) => + freezeAnswer(value), + ); + answers.set(number, answer); + return answer; + } return recipeBody(g, graph, report, parameters, recipes); }; return compiled(g, graph, report, params, recipes); diff --git a/src/reader/reader.ts b/src/reader/reader.ts index 556a53a9c..e2bf61192 100644 --- a/src/reader/reader.ts +++ b/src/reader/reader.ts @@ -69,6 +69,7 @@ export interface PackSummary { readonly framing?: string; readonly file: string; readonly modelRefs: readonly string[]; + readonly order?: "priority"; } /** One decoded relation end: the edge, its `claim`, and the other endpoint's display data. */ @@ -668,26 +669,26 @@ function packBoundary( } /** + * Start with Reader and createReader here for public query contracts and joins. + * Read the Pack design helpers here for member and boundary assembly, then + * notation/example-space.ts in this component for shared graph-to-vocabulary + * decoding and example cards. Its slot parsing comes from notation/slots.ts. + * Read evaluate-body.ts for binding injection and shipped recipe evaluation. + * * @sdpAnchor component:protocol.reader - * @sdpLabel Protocol reader seam + * @sdpLabel the typed graph contract and body evaluator * @sdpSatisfies spec:consumers.reader - * @sdpUses component:protocol.graph, component:protocol.validate, component:protocol.model + * @sdpUses component:protocol.graph, component:protocol.validate, component:protocol.model, component:protocol.notation * @sdpLayer application * @sdpContext protocol - */ -/** - * @sdpAnchor impl:protocol.agent-surface - * @sdpLabel typed graph reader and agent entry adapters - * @sdpSatisfies spec:consumers.agent-surface - * @sdpComponent component:protocol.reader * @sdpRole reader */ /** * @sdpAnchor impl:protocol.reader - * @sdpLabel thin typed graph reader construction + * @sdpLabel typed graph queries, entry adapters and context joins * @sdpReferences spec:consumers.agent-surface.answer-shapes - * @sdpSatisfies spec:consumers.reader, spec:extraction.entry-locations, spec:validation.next-rung-floor + * @sdpSatisfies spec:consumers.reader, spec:extraction.entry-locations, spec:validation.next-rung-floor, spec:consumers.agent-surface, spec:extraction.pack-member-order * @sdpComponent component:protocol.reader * @sdpUses impl:protocol.delivery-facts, impl:protocol.next-rung-floor * @sdpRole reader @@ -868,6 +869,7 @@ export function createReader(graph: GraphSchema, report?: ValidationReport): Rea ...(node.framing === undefined ? {} : { framing: node.framing }), file: node.file, modelRefs: node.modelRefs ?? [], + ...(node.order === undefined ? {} : { order: node.order }), }); const primitiveNodes = (): readonly PrimitiveNode[] => diff --git a/src/runner/index.ts b/src/runner/index.ts index 3788498ba..36288b3ac 100644 --- a/src/runner/index.ts +++ b/src/runner/index.ts @@ -91,18 +91,28 @@ function substituteParams(skeleton: string, params: ParamShape): string { ); } +/** + * Start with ExampleContract and StepBindings, then planExample here. + * Follow the planned sequence into execution to inspect handler order, + * fresh-world lifecycle and authored-step failure rendering. + */ const runnerComponentAnchor = codeAnchor({ id: codeAnchorId("component:protocol.runner"), - label: "Protocol example runner seam", + label: "provider-neutral example execution", satisfies: ref("spec:extraction.example-runner"), uses: [componentAnchorId("component:protocol.notation")], layer: "application", context: "protocol", + role: "service", }); const exampleRunnerAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.example-runner"), label: "plans and executes a bound example against the caller's world", - satisfies: [ref("spec:extraction.example-runner"), ref("spec:carrier.step-keys")], + satisfies: [ + ref("spec:extraction.example-runner"), + ref("spec:carrier.step-keys"), + ref("spec:extraction.executable-examples"), + ], component: componentAnchorId("component:protocol.runner"), role: "service", }); diff --git a/src/studio-elements/compose.ts b/src/studio-elements/compose.ts index 6c6fb35a2..8a9d93fd7 100644 --- a/src/studio-elements/compose.ts +++ b/src/studio-elements/compose.ts @@ -15,7 +15,7 @@ const labels: Readonly> = { * @sdpLabel opens a native request dialog beside build-rendered scopes without changing their content * @sdpSatisfies spec:consumers.spec-studio.intent-panel, spec:consumers.spec-studio.components * @sdpComponent component:protocol.studio-elements - * @sdpRole utility + * @sdpRole interaction */ export function registerCompose(): void { class Compose extends globalThis.HTMLElement { diff --git a/src/studio-elements/copy.ts b/src/studio-elements/copy.ts index 3054bc0fa..ad389e364 100644 --- a/src/studio-elements/copy.ts +++ b/src/studio-elements/copy.ts @@ -25,18 +25,34 @@ export async function copyText( * @sdpAnchor impl:protocol.studio-copy * @sdpSatisfies spec:consumers.spec-studio.page-elements * @sdpComponent component:protocol.studio-elements - * @sdpRole utility + * @sdpRole interaction + * @sdpLabel copies text with selection fallback and failure feedback */ export function registerCopy(): void { class Copy extends globalThis.HTMLElement { private control?: HTMLButtonElement; private selected?: HTMLTextAreaElement; static get observedAttributes(): string[] { - return ["text"]; + return ["text", "for"]; } + private target(): HTMLTextAreaElement | undefined { + const id = this.getAttribute("for"); + if (!id) return undefined; + return Array.from(this.ownerDocument.querySelectorAll("textarea")).find( + (area) => area.id === id || area.getAttribute("id") === id, + ); + } attributeChangedCallback(): void { - this.control?.setAttribute("aria-label", `Copy ${this.getAttribute("text") ?? ""}`); + const target = this.target(); + const label = target + ? (Array.from(this.ownerDocument.querySelectorAll("label")).find( + (label) => label.getAttribute("for") === (target.id || target.getAttribute("id")), + )?.textContent ?? + target.getAttribute("aria-label") ?? + "text") + : (this.getAttribute("text") ?? ""); + this.control?.setAttribute("aria-label", `Copy ${label}`); } connectedCallback(): void { @@ -49,7 +65,14 @@ export function registerCopy(): void { clipboard === undefined ? undefined : (text: string): Promise => clipboard.writeText(text); - void copyText(this.getAttribute("text") ?? "", write, (text) => { + const target = this.target(); + if (this.getAttribute("for") && !target) return; + void copyText(target?.value ?? this.getAttribute("text") ?? "", write, (text) => { + if (target) { + target.focus(); + target.select(); + return; + } this.selected?.parentElement?.remove(); const label = this.ownerDocument.createElement("label"); label.textContent = "Text to copy "; diff --git a/src/studio-elements/diagram.ts b/src/studio-elements/diagram.ts index b3b9ab298..358fa3f99 100644 --- a/src/studio-elements/diagram.ts +++ b/src/studio-elements/diagram.ts @@ -23,10 +23,10 @@ export function zoomViewBox( /** * @sdpAnchor impl:protocol.studio-diagram-element * @sdpLabel adds view-box pan, zoom and pinch to build-rendered SVG without changing its layout - * @sdpSatisfies spec:consumers.spec-studio.diagrams, spec:consumers.spec-studio.responsive, spec:consumers.spec-studio.export, spec:consumers.spec-studio.components + * @sdpSatisfies spec:consumers.spec-studio.diagrams, spec:consumers.spec-studio.responsive, spec:consumers.spec-studio.export, spec:consumers.spec-studio.components, spec:consumers.spec-studio.diagram-kinds.legibility * @sdpReferences spec:decisions.studio-diagram-renderer, spec:decisions.studio-build-renders * @sdpComponent component:protocol.studio-elements - * @sdpRole utility + * @sdpRole interaction */ export function registerDiagram(): void { class Diagram extends globalThis.HTMLElement { @@ -87,6 +87,11 @@ export function registerDiagram(): void { "Reset", () => { this.view = this.original; + const viewport = this.querySelector(".diagram-scroll"); + if (viewport) { + viewport.scrollLeft = 0; + viewport.scrollTop = 0; + } this.draw(); }, ], diff --git a/src/studio-elements/expander.ts b/src/studio-elements/expander.ts index 435f4dbbc..f0d8f1e3d 100644 --- a/src/studio-elements/expander.ts +++ b/src/studio-elements/expander.ts @@ -1,5 +1,9 @@ import { button, liveOutput } from "./dom.js"; +// Mermaid source stays as the reader left it in print; the print style hides a closed one. +const sourceDisclosure = + 'sdp-diagram details[id$=".source"], sdp-maturity-map details[id$=".source"]'; + export function fragmentTarget(fragment: string): string | null { if (fragment.length < 2) return null; try { @@ -15,7 +19,8 @@ export function fragmentTarget(fragment: string): string | null { * @sdpAnchor impl:protocol.studio-expander * @sdpSatisfies spec:consumers.spec-studio.page-elements, spec:consumers.spec-studio.export * @sdpComponent component:protocol.studio-elements - * @sdpRole utility + * @sdpRole interaction + * @sdpLabel opens fragment ancestors and expands disclosures for print */ export function registerExpander(): void { class Expander extends globalThis.HTMLElement { @@ -23,10 +28,12 @@ export function registerExpander(): void { private printed: { details: HTMLDetailsElement; open: boolean }[] = []; private readonly beforePrint = (): void => { if (this.printed.length) return; - this.printed = Array.from(this.querySelectorAll("details")).map((details) => ({ - details, - open: details.open, - })); + this.printed = Array.from(this.querySelectorAll("details")) + .filter((details) => !details.matches(sourceDisclosure)) + .map((details) => ({ + details, + open: details.open, + })); for (const { details } of this.printed) details.open = true; }; private readonly afterPrint = (): void => { diff --git a/src/studio-elements/filter.ts b/src/studio-elements/filter.ts index 3242d4e3c..069fc741a 100644 --- a/src/studio-elements/filter.ts +++ b/src/studio-elements/filter.ts @@ -1,4 +1,5 @@ import { liveOutput } from "./dom.js"; +import { fragmentTarget } from "./expander.js"; export function rowMatches( row: Readonly>, @@ -9,13 +10,26 @@ export function rowMatches( ); } +export function epicFilter( + href: string, + epics: readonly string[], +): { value: string; unknown: boolean } { + const values = new URL(href).searchParams.getAll("epic"); + if (values.length === 0) return { value: "any", unknown: false }; + const value = values[0] ?? ""; + if (values.length !== 1 || (value !== "none" && !epics.includes(value))) + return { value: "any", unknown: true }; + return { value, unknown: false }; +} + /** * Reads each row's filter attributes and changes only its hidden state. * * @sdpAnchor impl:protocol.studio-filter - * @sdpSatisfies spec:consumers.spec-studio.page-elements, spec:consumers.spec-studio.components + * @sdpSatisfies spec:consumers.spec-studio.page-elements, spec:consumers.spec-studio.components, spec:consumers.spec-studio.capability-map-page * @sdpComponent component:protocol.studio-elements - * @sdpRole utility + * @sdpRole interaction + * @sdpLabel filters build-rendered rows through their recorded attributes */ export function registerFilter(): void { class Filter extends globalThis.HTMLElement { @@ -37,22 +51,87 @@ export function registerFilter(): void { `${String(rows.filter((row) => !row.hidden).length)} of ${String(rows.length)} shown`; }; + // A link or fragment that targets a row this filter hides clears every choice, so the + // linked row shows; the expander opens its disclosures. + private readonly showTarget = (id: string | null): void => { + const target = id === null ? null : this.ownerDocument.getElementById(id); + if (!target || !this.contains(target) || !target.closest("[data-row]")?.hidden) + return; + for (const select of Array.from( + this.querySelectorAll("select[data-filter]"), + )) + select.value = "any"; + this.update(); + const output = liveOutput(this); + output.textContent = `${output.textContent}; the filter was cleared to show ${id ?? "the linked entry"}`; + target.scrollIntoView(); + }; + + private readonly hashChange = (): void => { + this.showTarget(fragmentTarget(this.ownerDocument.location.hash)); + }; + + private readonly linkClick = (event: Event): void => { + const link = (event.target as Element | null)?.closest("a[href]"); + const href = link?.getAttribute("href") ?? ""; + if (href.startsWith("#")) this.showTarget(fragmentTarget(href)); + }; + connectedCallback(): void { const form = this.querySelector("form"); if (form) { form.hidden = false; form.addEventListener("submit", this.preventSubmit); } - this.addEventListener("change", this.update); + const epic = this.querySelector('select[data-filter="epic"]'); + if (epic) { + const requested = epicFilter( + this.ownerDocument.location.href, + Array.from(epic.options) + .map((o) => o.value) + .filter((v) => v !== "any" && v !== "none"), + ); + epic.value = requested.value; + const note = this.querySelector("[data-filter-note]"); + if (note) + note.textContent = requested.unknown ? "Unknown epic filter. All Specs are shown." : ""; + } + form?.addEventListener("reset", this.reset); + this.addEventListener("change", this.change); + this.ownerDocument.addEventListener("click", this.linkClick, true); + this.ownerDocument.defaultView?.addEventListener("hashchange", this.hashChange); this.update(); + this.hashChange(); } + private readonly change = (): void => { + const note = this.querySelector("[data-filter-note]"); + if (note) note.textContent = ""; + this.update(); + }; + + private readonly reset = (): void => { + // Native defaults apply after the event; let other form controls reset too. + queueMicrotask(() => { + for (const select of Array.from( + this.querySelectorAll("select[data-filter]"), + )) + select.value = "any"; + const note = this.querySelector("[data-filter-note]"); + if (note) note.textContent = ""; + this.update(); + }); + }; + private readonly preventSubmit = (event: Event): void => { event.preventDefault(); }; disconnectedCallback(): void { - this.removeEventListener("change", this.update); + this.removeEventListener("change", this.change); + this.ownerDocument.removeEventListener("click", this.linkClick, true); + this.ownerDocument.defaultView?.removeEventListener("hashchange", this.hashChange); + this.querySelector("form")?.removeEventListener("reset", this.reset); this.querySelector("form")?.removeEventListener("submit", this.preventSubmit); } } diff --git a/src/studio-elements/index.ts b/src/studio-elements/index.ts index 8157769b3..99c913eb7 100644 --- a/src/studio-elements/index.ts +++ b/src/studio-elements/index.ts @@ -11,6 +11,10 @@ import { registerSearchResults } from "./search-results.js"; import { registerStamp } from "./stamp.js"; /** + * Start with proposal.ts for the browser handoff shape and schema checks. + * Read intent-panel.ts for storage and request partition, then diagram.ts for + * view-box controls over build-rendered SVG and its text equivalent. + * * Registers the browser interactions from one bundled entry point. * * @sdpAnchor component:protocol.studio-elements @@ -18,6 +22,8 @@ import { registerStamp } from "./stamp.js"; * @sdpUses component:protocol.reader, component:protocol.projections * @sdpLayer edge * @sdpContext protocol + * @sdpRole interaction + * @sdpLabel optional browser behavior and proposal handoff */ registerSearch(); registerSearchResults(); diff --git a/src/studio-elements/intent-panel-element.ts b/src/studio-elements/intent-panel-element.ts index 0ddf6104f..c9c7afcd5 100644 --- a/src/studio-elements/intent-panel-element.ts +++ b/src/studio-elements/intent-panel-element.ts @@ -17,7 +17,7 @@ export function gatherInPage(host: HTMLElement, intent: GatheredIntent): void { * @sdpSatisfies spec:consumers.spec-studio.intent-panel, spec:consumers.spec-studio.components * @sdpReferences spec:consumers.edit-model, spec:consumers.intent-composition * @sdpComponent component:protocol.studio-elements - * @sdpRole utility + * @sdpRole interaction */ export function registerIntentPanel(): void { class Panel extends globalThis.HTMLElement implements IntentPanelElement { diff --git a/src/studio-elements/proposal.ts b/src/studio-elements/proposal.ts index c4e9414dd..1ea3314d3 100644 --- a/src/studio-elements/proposal.ts +++ b/src/studio-elements/proposal.ts @@ -1,3 +1,10 @@ +/** + * @sdpAnchor impl:protocol.studio-proposal-contract + * @sdpLabel browser proposal shape and schema checks + * @sdpSatisfies spec:consumers.intent-composition.proposal + * @sdpComponent component:protocol.studio-elements + * @sdpRole contract + */ export interface ProposalChange { scope: string[]; request: string; diff --git a/src/studio-elements/scenario-editor.ts b/src/studio-elements/scenario-editor.ts index 940c97270..bb3ba5d48 100644 --- a/src/studio-elements/scenario-editor.ts +++ b/src/studio-elements/scenario-editor.ts @@ -19,7 +19,7 @@ export function scenarioText(given: string, when: string, then: string): string * @sdpLabel gathers the edited Given, When and Then as exact text scoped to its subject * @sdpSatisfies spec:consumers.spec-studio.intent-panel, spec:consumers.spec-studio.components * @sdpComponent component:protocol.studio-elements - * @sdpRole utility + * @sdpRole interaction */ export function registerScenarioEditor(): void { class ScenarioEditor extends globalThis.HTMLElement { diff --git a/src/studio-elements/search-results.ts b/src/studio-elements/search-results.ts index 26c5f8970..0ff0e56ba 100644 --- a/src/studio-elements/search-results.ts +++ b/src/studio-elements/search-results.ts @@ -99,6 +99,7 @@ export function entryMatches(record: SearchRecord, entry: SearchEntry, term: str * @sdpSatisfies spec:consumers.spec-studio.search, spec:consumers.spec-studio.page-elements, spec:consumers.spec-studio.components * @sdpComponent component:protocol.studio-elements * @sdpRole projection + * @sdpLabel ranks and renders concept, entry and file search hits */ export function searchRecords(index: SearchIndex, term: string): readonly SearchHit[] { const needle = term.toLowerCase().trim(); diff --git a/src/studio-elements/search.ts b/src/studio-elements/search.ts index e9b5768c7..e5bd66dc5 100644 --- a/src/studio-elements/search.ts +++ b/src/studio-elements/search.ts @@ -21,7 +21,8 @@ export function searchUrl(root: string, term: string): string { * @sdpAnchor impl:protocol.studio-search-form * @sdpSatisfies spec:consumers.spec-studio.page-elements * @sdpComponent component:protocol.studio-elements - * @sdpRole utility + * @sdpRole interaction + * @sdpLabel routes search forms through page-root URLs and fragments */ export function registerSearch(): void { class Search extends globalThis.HTMLElement { diff --git a/src/studio-elements/stamp.ts b/src/studio-elements/stamp.ts index ed1a754cd..c7abc4f56 100644 --- a/src/studio-elements/stamp.ts +++ b/src/studio-elements/stamp.ts @@ -33,7 +33,8 @@ export function sourceUrl(template: string, file: string, line: string): string * @sdpAnchor impl:protocol.studio-stamp * @sdpSatisfies spec:consumers.spec-studio.page-elements, spec:consumers.spec-studio.sharing * @sdpComponent component:protocol.studio-elements - * @sdpRole utility + * @sdpRole interaction + * @sdpLabel resolves published stamps and source links in the browser */ export function registerStamp(): void { class Stamp extends globalThis.HTMLElement { diff --git a/src/studio-elements/views.ts b/src/studio-elements/views.ts index 45f699dd4..9a078d385 100644 --- a/src/studio-elements/views.ts +++ b/src/studio-elements/views.ts @@ -1,11 +1,3 @@ -/** - * @sdpAnchor impl:protocol.studio-view-elements - * @sdpSatisfies spec:consumers.spec-studio.components - * @sdpLabel registers build-rendered cards, maps, spaces, trace graphs, impact and findings - * @sdpReferences spec:decisions.studio-build-renders - * @sdpComponent component:protocol.studio-elements - * @sdpRole utility - */ export function registerViews(): void { for (const name of [ "sdp-spec-card", diff --git a/src/testing/index.ts b/src/testing/index.ts index 78fddf4dc..e457e5e0e 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -30,9 +30,14 @@ export interface RunnableExampleWorld { observed?: O; } +/** + * Start with RunnableExampleAdapters and createRunnableExample here. + * Follow the comparator through observed, expected and Spec-authored outcomes + * when changing generated registrar integration or failure rendering. + */ const testingComponentAnchor = codeAnchor({ id: codeAnchorId("component:protocol.testing"), - label: "Protocol testing seam", + label: "the generated registrar runtime", satisfies: ref("spec:extraction.runnable-modules"), uses: [ componentAnchorId("component:protocol.adapters"), @@ -40,13 +45,14 @@ const testingComponentAnchor = codeAnchor({ ], layer: "adapter", context: "protocol", + role: "adapter", }); const exampleTestingHelpersAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.example-testing-helpers"), label: "adapter-lifecycle and oracle-comparison helpers for bound examples", - satisfies: ref("spec:extraction.runnable-modules"), + satisfies: [ref("spec:extraction.runnable-modules"), ref("spec:extraction.executable-examples")], component: componentAnchorId("component:protocol.testing"), - role: "utility", + role: "adapter", }); void testingComponentAnchor; void exampleTestingHelpersAnchor; diff --git a/src/validate/graph-index.ts b/src/validate/graph-index.ts index 90228c601..8f2ac21a1 100644 --- a/src/validate/graph-index.ts +++ b/src/validate/graph-index.ts @@ -30,7 +30,7 @@ const graphIndexAnchor = codeAnchor({ label: "builds the one-graph index validators and the reader share", satisfies: ref("spec:validation.two-check-families"), component: componentAnchorId("component:protocol.validate"), - role: "utility", + role: "index", }); void graphIndexAnchor; diff --git a/src/validate/readiness-floor.ts b/src/validate/readiness-floor.ts index a25fb310f..f00dfb2e3 100644 --- a/src/validate/readiness-floor.ts +++ b/src/validate/readiness-floor.ts @@ -400,7 +400,7 @@ const kindEvidenceAnchor = codeAnchor({ label: "defines the code-level per-kind evidence rows for readiness floors", satisfies: ref("spec:validation.kind-evidence"), component: componentAnchorId("component:protocol.validate"), - role: "contract", + role: "decider", }); void kindEvidenceAnchor; diff --git a/src/validate/validators.ts b/src/validate/validators.ts index 04d32ae6b..4482d4c31 100644 --- a/src/validate/validators.ts +++ b/src/validate/validators.ts @@ -64,9 +64,14 @@ const validationFamiliesAnchor = codeAnchor({ component: componentAnchorId("component:protocol.validate"), role: "contract", }); +/** + * Start with contracts.ts for the shared finding and report shapes. + * Read graph-index.ts for disposable lookups and readiness-floor.ts for ordered + * floor clauses. The catalog here composes independently callable checks. + */ const validateComponentAnchor = codeAnchor({ id: codeAnchorId("component:protocol.validate"), - label: "Protocol validation seam", + label: "one finding currency and ordered checks", satisfies: ref("spec:validation.two-check-families"), uses: [ componentAnchorId("component:protocol.graph"), @@ -75,6 +80,7 @@ const validateComponentAnchor = codeAnchor({ ], layer: "domain", context: "protocol", + role: "validator", }); void validationFamiliesAnchor; @@ -1096,7 +1102,7 @@ function checkPackModelRefs(pack: PackNode, index: GraphIndex, findings: Finding const packCoherenceAnchor = codeAnchor({ id: codeAnchorId("impl:protocol.pack-coherence"), label: "checks Pack membership agreement and uniqueness and model-reference kinds", - satisfies: ref("spec:validation.pack-coherence"), + satisfies: [ref("spec:validation.pack-coherence"), ref("spec:extraction.pack-member-order")], component: componentAnchorId("component:protocol.validate"), role: "validator", }); diff --git a/test/builders.typecheck.ts b/test/builders.typecheck.ts index 8948c0559..bfbadfb58 100644 --- a/test/builders.typecheck.ts +++ b/test/builders.typecheck.ts @@ -28,6 +28,21 @@ const authoredPack = pack({ specs: [ref("spec:orders.create-order")], }); +pack({ + id: packId("pack:roadmap"), + title: "Upcoming work", + specs: [], + order: "priority", +}); + +pack({ + id: packId("pack:roadmap"), + title: "Upcoming work", + specs: [], + // @ts-expect-error only priority is authored; reading order is represented by omission + order: "reading", +}); + const implementationAnchor = codeAnchor({ id: codeAnchorId("impl:orders.create-order-use-case"), label: "CreateOrderUseCase", diff --git a/test/check-self-hosting-gates.test.ts b/test/check-self-hosting-gates.test.ts index c868eee7a..a94249c4f 100644 --- a/test/check-self-hosting-gates.test.ts +++ b/test/check-self-hosting-gates.test.ts @@ -1,13 +1,5 @@ import { spawnSync } from "node:child_process"; -import { - copyFileSync, - mkdirSync, - mkdtempSync, - readFileSync, - readdirSync, - rmSync, - writeFileSync, -} from "node:fs"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -17,31 +9,10 @@ import { afterEach, describe, expect, it } from "vitest"; const repoRoot = fileURLToPath(new URL("..", import.meta.url)); const guardPath = join(repoRoot, "check-self-hosting-gates.mjs"); const roots: string[] = []; -const primaryPlans = readdirSync(join(repoRoot, "plans"), { withFileTypes: true }) - .filter((entry) => entry.isFile()) - .map((entry) => { - const match = /^(?\d+)-[a-z0-9][a-z0-9-]*\.md$/u.exec(entry.name); - const number = match?.groups?.number; - return number === undefined ? null : { name: entry.name, number: Number(number) }; - }) - .filter((entry) => entry !== null) - .sort((left, right) => right.number - left.number); -const currentPlan = primaryPlans[0]; - -if (currentPlan === undefined) { - throw new Error("test fixture requires a current primary-numbered plan"); -} - -const currentPlanPath = ["plans", currentPlan.name].join("/"); -const currentPlanSource = readFileSync(join(repoRoot, currentPlanPath), "utf8"); -const currentStatus = /\b(DRAFTED|EXECUTING|RUN|EXECUTED)\b/u.exec( - currentPlanSource.split("\n").find((line) => line.startsWith("> **Status:**")) ?? "", -)?.[1]; - -if (currentStatus === undefined) { - throw new Error(`${currentPlanPath} has no readable status`); -} +import { codeAnchor, codeAnchorId, ref, specTest, testAnchorId } from "../src/anchors.js"; +import { extract } from "../src/extract/index.js"; +const roadmapPath = "specs/roadmap.pack.sdp.md"; const copiedPaths = [ "AGENTS.md", "CONTEXT.md", @@ -50,7 +21,7 @@ const copiedPaths = [ ["plans", "16-carrier-ruling.md"].join("/"), ["plans", "17-self-hosting-v1.md"].join("/"), ["plans", "18-self-hosting-phase-2.md"].join("/"), - currentPlanPath, + roadmapPath, ] as const; function copyRecordTree(): string { @@ -66,6 +37,8 @@ function copyRecordTree(): string { } function runGuard(root: string) { + mkdirSync(join(root, "generated"), { recursive: true }); + writeFileSync(join(root, "generated/graph.json"), JSON.stringify(extract({ root }).graph)); return spawnSync(process.execPath, [guardPath, root], { encoding: "utf8" }); } @@ -75,15 +48,21 @@ afterEach(() => { } }); -describe("the self-hosting records gate", () => { - it("discovers plan 46 as the current primary plan", () => { - // re-measured after plan 46's opening commit - expect(currentPlan).toMatchObject({ - number: 46, - name: "46-studio-phase-one-and-adopter-speed.md", - }); - }); +// The script is outside the TypeScript carrier; this suite asserts its realization with the handbook. +const roadmapPractice = codeAnchor({ + id: codeAnchorId("impl:protocol.roadmap-practice"), + label: "realizes the roadmap handbook and its current-record gate", + satisfies: ref("spec:protocol.roadmap-replaces-plans"), + role: "validator", +}); +void roadmapPractice; +const roadmapPracticeTest = specTest({ + id: testAnchorId("test:protocol.roadmap-practice"), + verifies: ref("spec:protocol.roadmap-replaces-plans"), +}); +void roadmapPracticeTest; +describe("the self-hosting records gate", () => { it("accepts the current record without requiring the historical ledger shape", () => { const result = runGuard(copyRecordTree()); @@ -108,55 +87,157 @@ describe("the self-hosting records gate", () => { expect(result.stderr).toContain("Gate 1 ledger SHA disagrees with the owner packet"); }); - it("selects a zero-padded primary plan over a lower unpadded number", () => { + it.each([ + ["pack:roadmap", "the handbook does not name the roadmap Pack"], + ["recipe 53", "the handbook does not name the roadmap recipe"], + ])("refuses a missing handbook pointer: %s", (token, message) => { const root = copyRecordTree(); - const probeNumber = currentPlan.number + 1; - const nextPlanPath = join(root, "plans", `${String(probeNumber).padStart(3, "0")}-next.md`); - writeFileSync(nextPlanPath, "> **Status:** ✅ EXECUTED — selector probe\n", "utf8"); - const handbook = join(root, "AGENTS.md"); - writeFileSync( - handbook, - readFileSync(handbook, "utf8").replaceAll( - `plan ${String(currentPlan.number)} is ${currentStatus}`, - `plan ${String(probeNumber)} is EXECUTED`, - ), - "utf8", - ); + const path = join(root, "AGENTS.md"); + writeFileSync(path, readFileSync(path, "utf8").replaceAll(token, "removed")); + const result = runGuard(root); + expect(result.status).toBe(1); + expect(result.stderr).toContain(`AGENTS.md — ${message}`); + }); + it("names a missing roadmap manifest", () => { + const root = copyRecordTree(); + rmSync(join(root, roadmapPath)); const result = runGuard(root); + expect(result.status).toBe(1); + expect(result.stderr).toContain( + "generated/graph.json — the roadmap Pack pack:roadmap is missing", + ); + }); - expect({ status: result.status, stderr: result.stderr }).toEqual({ status: 0, stderr: "" }); + it.each(["absent", "corrupt"])("names the generated graph when %s", (state) => { + const root = copyRecordTree(); + if (state === "corrupt") { + mkdirSync(join(root, "generated"), { recursive: true }); + writeFileSync(join(root, "generated/graph.json"), "{broken"); + } + const result = spawnSync(process.execPath, [guardPath, root], { encoding: "utf8" }); + expect(result.status).toBe(1); + expect(result.stderr).toContain( + "generated/graph.json — the generated graph is missing or unreadable", + ); }); - it("ignores a letter-suffixed plan when selecting the current primary", () => { + it("accepts quoted priority with a comment and rejects a renamed roadmap id", () => { const root = copyRecordTree(); + const path = join(root, roadmapPath); writeFileSync( - join(root, "plans", `${String(currentPlan.number + 1)}a-notes.md`), - "> **Status:** DRAFTED — letter-suffix probe\n", - "utf8", + path, + readFileSync(path, "utf8").replace("order: priority", 'order: "priority" # advisory'), ); - + expect(runGuard(root).status).toBe(0); + writeFileSync(path, readFileSync(path, "utf8").replace("id: pack:roadmap", "id: pack:renamed")); const result = runGuard(root); - - expect({ status: result.status, stderr: result.stderr }).toEqual({ status: 0, stderr: "" }); + expect(result.status).toBe(1); + expect(result.stderr).toContain( + "generated/graph.json — the roadmap Pack pack:roadmap is missing", + ); }); - it("fails when the handbook carries a stale current-plan status", () => { + it.each(["", "order: reading", "---\n# Roadmap\norder: priority"])( + "requires a priority roadmap node derived from the manifest: %s", + (order) => { + const root = copyRecordTree(); + writeFileSync( + join(root, roadmapPath), + `---\nid: pack:roadmap\nspecs: []\n${order}\n---\n# Roadmap\n`, + ); + const result = runGuard(root); + expect(result.status).toBe(1); + expect(result.stderr).toContain( + order === "" + ? "generated/graph.json — the roadmap Pack pack:roadmap does not state order: priority" + : "generated/graph.json — the roadmap Pack pack:roadmap is missing", + ); + }, + ); + + it.each(["[]", "[spec:probe.built]", "[spec:probe.dependent, spec:probe.dependency]"])( + "checks no content, dependency order or plan number: %s", + (members) => { + const root = copyRecordTree(); + writeFileSync( + join(root, roadmapPath), + `---\nid: pack:roadmap\norder: priority\nspecs: ${members}\n---\n# Roadmap\n`, + ); + writeFileSync(join(root, "plans", "999-next.md"), "No status header.\n"); + const result = runGuard(root); + expect({ status: result.status, stderr: result.stderr }).toEqual({ status: 0, stderr: "" }); + }, + ); + + it("keeps the documented green gate and semantic plan-status ban", () => { const root = copyRecordTree(); const path = join(root, "AGENTS.md"); - const source = readFileSync(path, "utf8"); + writeFileSync(path, readFileSync(path, "utf8").replace("`check:temporal` →", "")); writeFileSync( - path, - source.replace( - `plan ${String(currentPlan.number)} is ${currentStatus}`, - `plan ${String(currentPlan.number - 1)} is DRAFTED`, - ), + join(root, "CONTEXT.md"), + readFileSync(join(root, "CONTEXT.md"), "utf8") + "\nplan 999 is EXECUTED\n", ); - const result = runGuard(root); - expect(result.status).toBe(1); - expect(result.stderr).toContain("the handbook does not name the current primary plan"); - expect(result.stderr).toContain("the handbook status disagrees with the current primary plan"); + expect(result.stderr).toContain("documented green-gate legs disagree"); + expect(result.stderr).toContain("CONTEXT.md — carries plan-status wording"); + }); + + it("ships the roadmap, recovery and review convention in the handbook", () => { + const source = readFileSync(join(repoRoot, "AGENTS.md"), "utf8"); + const flat = (text: string) => text.replace(/\s+/gu, " "); + const bullet = (label: string) => { + const match = source.split(`- **${label}**`)[1]?.split(/^-(?= \*\*)|^## /mu)[0]; + expect(match, label).toBeDefined(); + return flat(match ?? ""); + }; + const next = flat(source.split("> **Next work**")[1]?.split(/^## /mu)[0] ?? ""); + for (const clause of ["pack:roadmap", "recipe 53", "frozen history"]) + expect(next).toContain(clause); + const commits = bullet("Commits are recovery boundaries, not workflow gates."); + for (const clause of [ + "first commit message", + "working rules", + "draft pull request", + "first coherent commit", + "Without such an instruction, ask before committing", + "never force one per todo", + "create empty commits", + "absorb unrelated or pre-existing changes", + "commit from an unsafe dirty baseline", + ]) + expect(commits).toContain(clause); + const reviews = bullet("Reviews travel as proposals."); + for (const clause of [ + "branch-head commit", + "a proposal composed in a preview names the preview's merge commit", + "recipe 38", + "recipe 49", + "decider", + "provisional", + "links", + "code defect", + ]) + expect(reviews).toContain(clause); + expect(reviews).not.toContain("never a preview's merge commit"); + const evidence = bullet("Tests are the evidence."); + for (const clause of [ + "removed before merge", + "run evidence", + "merge commits", + "The Protocol merges its pull requests with merge commits", + "head ref", + "squash or rebase merge", + "durable ref", + ]) + expect(evidence).toContain(clause); + const forward = bullet("Plan the next PR."); + for (const clause of ["recipe 55", "owner's reason"]) expect(forward).toContain(clause); + expect(bullet("DECISIONS.md admits sparingly, and reads by name.")).toContain( + "its paper trail is git and the pull request description", + ); + expect(flat(source)).not.toMatch(/plan \d+ is (?:EXECUTING|EXECUTED|DRAFTED)/u); + expect(source).not.toContain("highest primary-numbered plan"); }); }); diff --git a/test/checkout-v1.test.ts b/test/checkout-v1.test.ts index 94ad01daa..41a94e5c1 100644 --- a/test/checkout-v1.test.ts +++ b/test/checkout-v1.test.ts @@ -23,9 +23,21 @@ const goldenPath = fileURLToPath( const extraction = extract({ root: exampleRoot }); describe("checkout-v1 tracer bullet (extractor-fed)", () => { + it("teaches readers to derive roadmap needs each time", () => { + const readme = readFileSync( + new URL("../examples/checkout-v1/README.md", import.meta.url), + "utf8", + ); + const roadmap = readme.split("## Read the next work\n")[1]?.split(/^## /mu)[0] ?? ""; + expect(roadmap).toContain( + "Recipe 53 places each entry by what it needs, read from the graph each time", + ); + expect(roadmap).not.toContain("Order placement needs design"); + }); + it("extracts the example with zero findings", () => { expect(extraction.report.findings).toEqual([]); - expect(extraction.counts).toEqual({ specs: 11, packs: 1, anchors: 5 }); + expect(extraction.counts).toEqual({ specs: 11, packs: 2, anchors: 5 }); }); it("keeps the below-ready unbound verifier as data with no finding", () => { @@ -76,7 +88,9 @@ describe("checkout-v1 tracer bullet (extractor-fed)", () => { "spec:orders.order-model", "spec:decisions.order-lifecycle", ]; - const pack = extraction.graph.nodes.find((node) => node.nodeType === "Pack"); + const pack = extraction.graph.nodes + .filter((node) => node.nodeType === "Pack") + .find((node) => node.id === "pack:checkout-v1"); expect(pack?.members).toEqual(members); expect( createReader(extraction.graph) @@ -110,10 +124,10 @@ describe("checkout-v1 tracer bullet (extractor-fed)", () => { } }); - it("extracts the pack and the five anchors (the anchored layer)", () => { + it("extracts both Packs and the five anchors (the anchored layer)", () => { expect( extraction.graph.nodes.filter((node) => node.nodeType === "Pack").map((n) => n.id), - ).toEqual(["pack:checkout-v1"]); + ).toEqual(["pack:checkout-v1", "pack:checkout-roadmap"]); expect( extraction.graph.nodes .filter((node) => node.nodeType === "Anchor" || node.nodeType === "CodeNode") diff --git a/test/cli.test.ts b/test/cli.test.ts index a37507855..e18971304 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -221,7 +221,7 @@ describe("sdp cli", () => { expect(exitCode).toBe(0); expect(capture.readStderr()).toBe(""); expect(capture.readStdout()).toContain( - "11 specs · 1 packs · 5 anchors → 17 nodes · 32 edges", + "11 specs · 2 packs · 5 anchors → 18 nodes · 34 edges", ); expect(capture.readStdout()).toContain("(3 modules)"); expect(readdirSync(join(root, "generated")).sort()).toEqual([ @@ -304,13 +304,37 @@ describe("sdp cli", () => { expect(exitCode).toBe(0); const stderr = capture.readStderr(); for (const [file, specId] of [ + ["specs/extraction/executable-examples.sdp.md", "spec:extraction.executable-examples"], ["specs/model/spec-sections.sdp.md", "spec:model.spec-sections"], + ["specs/protocol/role-vocabulary.sdp.md", "spec:protocol.role-vocabulary"], ] as const) { expect(stderr).toContain(`${file} — [warning] honesty/gaps — Spec "${specId}"`); } expect(stderr).not.toContain("conformance/prose-mentions"); - // Re-measured from the refreshed carriers: no prose-mention pair is retained. - expect(capture.readStdout()).toContain("validate: 0 errors · 1 warnings"); + for (const specId of [ + "spec:consumers.spec-studio.diagram-kinds", + "spec:protocol.roadmap-replaces-plans", + "spec:consumers.spec-studio.capability-map-page", + "spec:consumers.spec-studio.diagram-kinds.capability-tree", + "spec:consumers.spec-studio.slice-page", + "spec:consumers.spec-studio.diagram-kinds.dependency", + "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + "spec:consumers.spec-studio.diagram-kinds.maturity-picture", + "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + "spec:consumers.spec-studio.lenses.architecture.unit-register", + "spec:protocol.significant-units-review", + "spec:consumers.capability-map", + "spec:protocol.capability-epics", + "spec:consumers.design-slice", + "spec:consumers.roadmap", + "spec:consumers.roadmap.memo", + "spec:consumers.spec-studio.roadmap-page", + ]) { + expect(stderr).not.toContain(`honesty/gaps — Spec "${specId}"`); + } + // Re-measured from the refreshed carriers: no prose-mention pair is retained, and the + // verifier gaps listed above are the whole warning set. + expect(capture.readStdout()).toContain("validate: 0 errors · 3 warnings"); expect(readFileSync(join(root, "generated", "graph.json"), "utf8")).toContain( '"id": "pack:self-hosting-v1"', ); @@ -1310,7 +1334,7 @@ export const probe = spec({ expect(exitCode).toBe(0); expect(capture.readStdout()).toContain( - "11 specs · 1 packs · 5 anchors → 17 nodes · 32 edges", + "11 specs · 2 packs · 5 anchors → 18 nodes · 34 edges", ); expect(capture.readStdout()).toContain( "validate: 0 errors · 0 warnings (conformance + honesty over the one graph)", @@ -1382,7 +1406,7 @@ export const probe = spec({ expect(capture.readStdout()).toContain( "validate: 0 errors · 0 warnings (conformance + honesty over the one graph)", ); - expect(capture.readStdout()).toContain("(13 pages)"); + expect(capture.readStdout()).toContain("(14 pages)"); const viewRoot = join(root, "generated", "design-review"); expect(readdirSync(viewRoot).sort()).toEqual(["index.md", "pack", "spec"]); diff --git a/test/consumers.design-slice.chain.test.generated.ts b/test/consumers.design-slice.chain.test.generated.ts new file mode 100644 index 000000000..b3e6d738f --- /dev/null +++ b/test/consumers.design-slice.chain.test.generated.ts @@ -0,0 +1,64 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.chain. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { chainContract } from "../generated/contracts/consumers.design-slice.chain.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.chain.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerChain( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the members read in depth order {order}": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the members read in depth order {order}", + text: "the members read in depth order {order}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + chainContract, + { + structure: "rest on each other in a chain", + outside: "that no code satisfies", + anchor: "an anchor satisfies, matching every declared value", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.component-absent.test.generated.ts b/test/consumers.design-slice.component-absent.test.generated.ts new file mode 100644 index 000000000..62947df15 --- /dev/null +++ b/test/consumers.design-slice.component-absent.test.generated.ts @@ -0,0 +1,65 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.component-absent. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { componentAbsentContract } from "../generated/contracts/consumers.design-slice.component-absent.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.component-absent.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerComponentAbsent( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows": + (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows", + text: "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + componentAbsentContract, + { + structure: "rest on each other in a chain", + outside: "that a component's unit satisfies", + anchor: "no anchor holds", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.component-differing.test.generated.ts b/test/consumers.design-slice.component-differing.test.generated.ts new file mode 100644 index 000000000..76923f4c7 --- /dev/null +++ b/test/consumers.design-slice.component-differing.test.generated.ts @@ -0,0 +1,65 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.component-differing. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { componentDifferingContract } from "../generated/contracts/consumers.design-slice.component-differing.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.component-differing.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerComponentDiffering( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows": + (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows", + text: "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + componentDifferingContract, + { + structure: "rest on each other in a chain", + outside: "that a component's unit satisfies", + anchor: "an anchor satisfies, with a differing layer and uses", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.component-matching.test.generated.ts b/test/consumers.design-slice.component-matching.test.generated.ts new file mode 100644 index 000000000..8f13d590f --- /dev/null +++ b/test/consumers.design-slice.component-matching.test.generated.ts @@ -0,0 +1,65 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.component-matching. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { componentMatchingContract } from "../generated/contracts/consumers.design-slice.component-matching.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.component-matching.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerComponentMatching( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows": + (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows", + text: "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + componentMatchingContract, + { + structure: "rest on each other in a chain", + outside: "that a component's unit satisfies", + anchor: "an anchor satisfies, matching every declared value", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.component-unreadable.test.generated.ts b/test/consumers.design-slice.component-unreadable.test.generated.ts new file mode 100644 index 000000000..e22b400ce --- /dev/null +++ b/test/consumers.design-slice.component-unreadable.test.generated.ts @@ -0,0 +1,65 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.component-unreadable. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { componentUnreadableContract } from "../generated/contracts/consumers.design-slice.component-unreadable.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.component-unreadable.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerComponentUnreadable( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows": + (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows", + text: "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + componentUnreadableContract, + { + structure: "rest on each other in a chain", + outside: "that a component's unit satisfies", + anchor: "an anchor satisfies, with an unreadable layer", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.component-unrelated.test.generated.ts b/test/consumers.design-slice.component-unrelated.test.generated.ts new file mode 100644 index 000000000..0c565f8c9 --- /dev/null +++ b/test/consumers.design-slice.component-unrelated.test.generated.ts @@ -0,0 +1,65 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.component-unrelated. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { componentUnrelatedContract } from "../generated/contracts/consumers.design-slice.component-unrelated.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.component-unrelated.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerComponentUnrelated( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows": + (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows", + text: "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + componentUnrelatedContract, + { + structure: "rest on each other in a chain", + outside: "that a component's unit satisfies", + anchor: "an anchor holds without satisfying the design", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.decision-backlink.test.generated.ts b/test/consumers.design-slice.decision-backlink.test.generated.ts new file mode 100644 index 000000000..8cdd75bb6 --- /dev/null +++ b/test/consumers.design-slice.decision-backlink.test.generated.ts @@ -0,0 +1,64 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.decision-backlink. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { decisionBacklinkContract } from "../generated/contracts/consumers.design-slice.decision-backlink.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.decision-backlink.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerDecisionBacklink( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the members read in depth order {order}": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the members read in depth order {order}", + text: "the members read in depth order {order}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + decisionBacklinkContract, + { + structure: "include a decision that refines the member it shapes", + outside: "that no code satisfies", + anchor: "an anchor satisfies, matching every declared value", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.dependency-cycle.test.generated.ts b/test/consumers.design-slice.dependency-cycle.test.generated.ts new file mode 100644 index 000000000..f8eb0aa2c --- /dev/null +++ b/test/consumers.design-slice.dependency-cycle.test.generated.ts @@ -0,0 +1,67 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.dependency-cycle. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { dependencyCycleContract } from "../generated/contracts/consumers.design-slice.dependency-cycle.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.dependency-cycle.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerDependencyCycle( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the slice reports {cycles} cycles and {exampleMembers} folded example members": ( + execution, + params, + ) => { + compareContractOutcome(execution, { + kind: "then", + key: "the slice reports {cycles} cycles and {exampleMembers} folded example members", + text: "the slice reports {cycles} cycles and {exampleMembers} folded example members", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + dependencyCycleContract, + { + structure: "include two members that depend on each other", + outside: "that no code satisfies", + anchor: "an anchor satisfies, matching every declared value", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.folded-example.test.generated.ts b/test/consumers.design-slice.folded-example.test.generated.ts new file mode 100644 index 000000000..5af268643 --- /dev/null +++ b/test/consumers.design-slice.folded-example.test.generated.ts @@ -0,0 +1,67 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.folded-example. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { foldedExampleContract } from "../generated/contracts/consumers.design-slice.folded-example.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.folded-example.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerFoldedExample( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the slice reports {cycles} cycles and {exampleMembers} folded example members": ( + execution, + params, + ) => { + compareContractOutcome(execution, { + kind: "then", + key: "the slice reports {cycles} cycles and {exampleMembers} folded example members", + text: "the slice reports {cycles} cycles and {exampleMembers} folded example members", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + foldedExampleContract, + { + structure: "include an example that refines a member", + outside: "that no code satisfies", + anchor: "an anchor satisfies, matching every declared value", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.independent-example.test.generated.ts b/test/consumers.design-slice.independent-example.test.generated.ts new file mode 100644 index 000000000..a1fb4eb15 --- /dev/null +++ b/test/consumers.design-slice.independent-example.test.generated.ts @@ -0,0 +1,67 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.independent-example. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { independentExampleContract } from "../generated/contracts/consumers.design-slice.independent-example.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.independent-example.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerIndependentExample( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the slice reports {cycles} cycles and {exampleMembers} folded example members": ( + execution, + params, + ) => { + compareContractOutcome(execution, { + kind: "then", + key: "the slice reports {cycles} cycles and {exampleMembers} folded example members", + text: "the slice reports {cycles} cycles and {exampleMembers} folded example members", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + independentExampleContract, + { + structure: "include an example that refines a member and depends on an outside Spec", + outside: "that no code satisfies", + anchor: "an anchor satisfies, matching every declared value", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.outside-components.test.generated.ts b/test/consumers.design-slice.outside-components.test.generated.ts new file mode 100644 index 000000000..02a43935a --- /dev/null +++ b/test/consumers.design-slice.outside-components.test.generated.ts @@ -0,0 +1,64 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.outside-components. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { outsideComponentsContract } from "../generated/contracts/consumers.design-slice.outside-components.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.outside-components.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerOutsideComponents( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the outside components are {outsideComponents}": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the outside components are {outsideComponents}", + text: "the outside components are {outsideComponents}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + outsideComponentsContract, + { + structure: "include a decision that refines the member it shapes", + outside: "that a component's unit satisfies", + anchor: "an anchor satisfies, matching every declared value", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.outside-holding.test.generated.ts b/test/consumers.design-slice.outside-holding.test.generated.ts new file mode 100644 index 000000000..e389ad9b7 --- /dev/null +++ b/test/consumers.design-slice.outside-holding.test.generated.ts @@ -0,0 +1,64 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.outside-holding. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { outsideHoldingContract } from "../generated/contracts/consumers.design-slice.outside-holding.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.outside-holding.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerOutsideHolding( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the outside Spec holds {held}": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the outside Spec holds {held}", + text: "the outside Spec holds {held}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + outsideHoldingContract, + { + structure: "include a decision that refines the member it shapes", + outside: "stated below defined", + anchor: "an anchor holds without satisfying the design", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.outside-satisfied.test.generated.ts b/test/consumers.design-slice.outside-satisfied.test.generated.ts new file mode 100644 index 000000000..5076a7cc8 --- /dev/null +++ b/test/consumers.design-slice.outside-satisfied.test.generated.ts @@ -0,0 +1,65 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.outside-satisfied. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { outsideSatisfiedContract } from "../generated/contracts/consumers.design-slice.outside-satisfied.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.outside-satisfied.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerOutsideSatisfied( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the touched components are {components}, {throughRestsOn} of them reached through what the slice rests on": + (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the touched components are {components}, {throughRestsOn} of them reached through what the slice rests on", + text: "the touched components are {components}, {throughRestsOn} of them reached through what the slice rests on", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + outsideSatisfiedContract, + { + structure: "include a decision that refines the member it shapes", + outside: "that a component's unit satisfies", + anchor: "an anchor holds without satisfying the design", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.design-slice.outside-unbound.test.generated.ts b/test/consumers.design-slice.outside-unbound.test.generated.ts new file mode 100644 index 000000000..7e7eb9dd8 --- /dev/null +++ b/test/consumers.design-slice.outside-unbound.test.generated.ts @@ -0,0 +1,65 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.design-slice.outside-unbound. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { outsideUnboundContract } from "../generated/contracts/consumers.design-slice.outside-unbound.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.design-slice.outside-unbound.contract.js"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; + +export function registerOutsideUnbound( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe Pack whose members {structure}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member depends on an outside Spec {outside}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "one member is a component design whose component {anchor}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the design slice runs over the probe Pack": async (execution) => { + await invokeRunnableExample(execution); + }, + "the touched components are {components}, {throughRestsOn} of them reached through what the slice rests on": + (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the touched components are {components}, {throughRestsOn} of them reached through what the slice rests on", + text: "the touched components are {components}, {throughRestsOn} of them reached through what the slice rests on", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + outsideUnboundContract, + { + structure: "include a decision that refines the member it shapes", + outside: "that no code satisfies", + anchor: "an anchor holds without satisfying the design", + }, + ["structure", "outside", "anchor"], + adapters, + bindings, + ); +} diff --git a/test/consumers.roadmap.blocking-part.test.generated.ts b/test/consumers.roadmap.blocking-part.test.generated.ts new file mode 100644 index 000000000..4450b9184 --- /dev/null +++ b/test/consumers.roadmap.blocking-part.test.generated.ts @@ -0,0 +1,63 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.roadmap.blocking-part. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { blockingPartContract } from "../generated/contracts/consumers.roadmap.blocking-part.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.roadmap.blocking-part.contract.js"; +import type { + RoadmapConditions, + RoadmapOutcome, +} from "../generated/contracts/consumers.roadmap.space.js"; + +export function registerBlockingPart( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe roadmap whose entry holds the work {work}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "that work {state}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the roadmap recipe reads the probe roadmap": async (execution) => { + await invokeRunnableExample(execution); + }, + "the entry names {part} among the parts with its need, held by {clause}": ( + execution, + params, + ) => { + compareContractOutcome(execution, { + kind: "then", + key: "the entry names {part} among the parts with its need, held by {clause}", + text: "the entry names {part} among the parts with its need, held by {clause}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + blockingPartContract, + { + work: "spec:probe.root, spec:probe.part", + state: "has a ready root and one descendant held by a blocking question", + }, + ["work", "state"], + adapters, + bindings, + ); +} diff --git a/test/consumers.roadmap.buildable-part.test.generated.ts b/test/consumers.roadmap.buildable-part.test.generated.ts new file mode 100644 index 000000000..2769f7605 --- /dev/null +++ b/test/consumers.roadmap.buildable-part.test.generated.ts @@ -0,0 +1,57 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.roadmap.buildable-part. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { buildablePartContract } from "../generated/contracts/consumers.roadmap.buildable-part.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.roadmap.buildable-part.contract.js"; +import type { + RoadmapConditions, + RoadmapOutcome, +} from "../generated/contracts/consumers.roadmap.space.js"; + +export function registerBuildablePart( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe roadmap whose entry holds the work {work}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "that work {state}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the roadmap recipe reads the probe roadmap": async (execution) => { + await invokeRunnableExample(execution); + }, + "the entry's need reads {need}": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the entry's need reads {need}", + text: "the entry's need reads {need}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + buildablePartContract, + { work: "spec:probe.root, spec:probe.part", state: "has one part ready and not implemented" }, + ["work", "state"], + adapters, + bindings, + ); +} diff --git a/test/consumers.roadmap.built-work.test.generated.ts b/test/consumers.roadmap.built-work.test.generated.ts new file mode 100644 index 000000000..3880ebb55 --- /dev/null +++ b/test/consumers.roadmap.built-work.test.generated.ts @@ -0,0 +1,57 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.roadmap.built-work. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { builtWorkContract } from "../generated/contracts/consumers.roadmap.built-work.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.roadmap.built-work.contract.js"; +import type { + RoadmapConditions, + RoadmapOutcome, +} from "../generated/contracts/consumers.roadmap.space.js"; + +export function registerBuiltWork( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe roadmap whose entry holds the work {work}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "that work {state}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the roadmap recipe reads the probe roadmap": async (execution) => { + await invokeRunnableExample(execution); + }, + "the entry's need reads {need}": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the entry's need reads {need}", + text: "the entry's need reads {need}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + builtWorkContract, + { work: "spec:probe.root, spec:probe.part", state: "is ready and implemented throughout" }, + ["work", "state"], + adapters, + bindings, + ); +} diff --git a/test/consumers.roadmap.design-part.test.generated.ts b/test/consumers.roadmap.design-part.test.generated.ts new file mode 100644 index 000000000..4466b1ff1 --- /dev/null +++ b/test/consumers.roadmap.design-part.test.generated.ts @@ -0,0 +1,60 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.roadmap.design-part. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { designPartContract } from "../generated/contracts/consumers.roadmap.design-part.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.roadmap.design-part.contract.js"; +import type { + RoadmapConditions, + RoadmapOutcome, +} from "../generated/contracts/consumers.roadmap.space.js"; + +export function registerDesignPart( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe roadmap whose entry holds the work {work}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "that work {state}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the roadmap recipe reads the probe roadmap": async (execution) => { + await invokeRunnableExample(execution); + }, + "the entry's need reads {need}": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the entry's need reads {need}", + text: "the entry's need reads {need}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + designPartContract, + { + work: "spec:probe.root, spec:probe.part", + state: "has one part below ready whose floor stops short", + }, + ["work", "state"], + adapters, + bindings, + ); +} diff --git a/test/consumers.roadmap.drifting-part.test.generated.ts b/test/consumers.roadmap.drifting-part.test.generated.ts new file mode 100644 index 000000000..2aa03b466 --- /dev/null +++ b/test/consumers.roadmap.drifting-part.test.generated.ts @@ -0,0 +1,57 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.roadmap.drifting-part. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { driftingPartContract } from "../generated/contracts/consumers.roadmap.drifting-part.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.roadmap.drifting-part.contract.js"; +import type { + RoadmapConditions, + RoadmapOutcome, +} from "../generated/contracts/consumers.roadmap.space.js"; + +export function registerDriftingPart( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe roadmap whose entry holds the work {work}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "that work {state}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the roadmap recipe reads the probe roadmap": async (execution) => { + await invokeRunnableExample(execution); + }, + "the entry counts {drift} parts as drift": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the entry counts {drift} parts as drift", + text: "the entry counts {drift} parts as drift", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + driftingPartContract, + { work: "spec:probe.root, spec:probe.part", state: "has one part implemented below ready" }, + ["work", "state"], + adapters, + bindings, + ); +} diff --git a/test/consumers.roadmap.missing-evidence.test.generated.ts b/test/consumers.roadmap.missing-evidence.test.generated.ts new file mode 100644 index 000000000..e91b92298 --- /dev/null +++ b/test/consumers.roadmap.missing-evidence.test.generated.ts @@ -0,0 +1,63 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.roadmap.missing-evidence. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { missingEvidenceContract } from "../generated/contracts/consumers.roadmap.missing-evidence.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.roadmap.missing-evidence.contract.js"; +import type { + RoadmapConditions, + RoadmapOutcome, +} from "../generated/contracts/consumers.roadmap.space.js"; + +export function registerMissingEvidence( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe roadmap whose entry holds the work {work}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "that work {state}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the roadmap recipe reads the probe roadmap": async (execution) => { + await invokeRunnableExample(execution); + }, + "the entry names {part} among the parts with its need, held by {clause}": ( + execution, + params, + ) => { + compareContractOutcome(execution, { + kind: "then", + key: "the entry names {part} among the parts with its need, held by {clause}", + text: "the entry names {part} among the parts with its need, held by {clause}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + missingEvidenceContract, + { + work: "spec:probe.root, spec:probe.part", + state: "has a ready root and one descendant without its kind's evidence", + }, + ["work", "state"], + adapters, + bindings, + ); +} diff --git a/test/consumers.roadmap.outside-parent.test.generated.ts b/test/consumers.roadmap.outside-parent.test.generated.ts new file mode 100644 index 000000000..bbb2a8c8a --- /dev/null +++ b/test/consumers.roadmap.outside-parent.test.generated.ts @@ -0,0 +1,60 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.roadmap.outside-parent. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { outsideParentContract } from "../generated/contracts/consumers.roadmap.outside-parent.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.roadmap.outside-parent.contract.js"; +import type { + RoadmapConditions, + RoadmapOutcome, +} from "../generated/contracts/consumers.roadmap.space.js"; + +export function registerOutsideParent( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe roadmap whose entry holds the work {work}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "that work {state}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the roadmap recipe reads the probe roadmap": async (execution) => { + await invokeRunnableExample(execution); + }, + "the entry rests on {target} through {origin}": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the entry rests on {target} through {origin}", + text: "the entry rests on {target} through {origin}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + outsideParentContract, + { + work: "spec:probe.root, spec:probe.part", + state: "has one descendant that also refines a Spec outside the roadmap", + }, + ["work", "state"], + adapters, + bindings, + ); +} diff --git a/test/consumers.roadmap.statement-order.test.generated.ts b/test/consumers.roadmap.statement-order.test.generated.ts new file mode 100644 index 000000000..5930df5d2 --- /dev/null +++ b/test/consumers.roadmap.statement-order.test.generated.ts @@ -0,0 +1,61 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.roadmap.statement-order. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { statementOrderContract } from "../generated/contracts/consumers.roadmap.statement-order.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.roadmap.statement-order.contract.js"; +import type { + RoadmapConditions, + RoadmapOutcome, +} from "../generated/contracts/consumers.roadmap.space.js"; + +export function registerStatementOrder( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe roadmap whose entry holds the work {work}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "that work {state}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "a probe roadmap whose first entry rests on a Spec of its second entry, both waiting for a statement": + () => { + // Given state is materialized by createWorld from the generated point. + }, + "the roadmap recipe reads the probe roadmap": async (execution) => { + await invokeRunnableExample(execution); + }, + "the waiting statements list {earlier} before {later}": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the waiting statements list {earlier} before {later}", + text: "the waiting statements list {earlier} before {later}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + statementOrderContract, + { work: "spec:probe.dependent", state: "has one part below ready whose floor reaches ready" }, + ["work", "state"], + adapters, + bindings, + ); +} diff --git a/test/consumers.roadmap.statement-part.test.generated.ts b/test/consumers.roadmap.statement-part.test.generated.ts new file mode 100644 index 000000000..5fe103bdb --- /dev/null +++ b/test/consumers.roadmap.statement-part.test.generated.ts @@ -0,0 +1,60 @@ +// GENERATED by `sdp build` — the runnable registrar of spec:consumers.roadmap.statement-part. +// Derived from the extracted graph (the statically-reified truth, MD-14), keyed by spec ID: +// regenerable, never edited, importable because it is a projection (JS-B2.6's pattern) — +// never the authored spec module (JS-B2.3: no spec↔code import edges). + +import type { StepBindings } from "@libar-dev/software-delivery-protocol/runner"; +import { + compareContractOutcome, + invokeRunnableExample, + registerRunnableExample, +} from "@libar-dev/software-delivery-protocol/testing"; +import type { + RunnableExampleAdapters, + RunnableExampleWorld, +} from "@libar-dev/software-delivery-protocol/testing"; +import { statementPartContract } from "../generated/contracts/consumers.roadmap.statement-part.contract.js"; +import type { + Step, + StepParams, +} from "../generated/contracts/consumers.roadmap.statement-part.contract.js"; +import type { + RoadmapConditions, + RoadmapOutcome, +} from "../generated/contracts/consumers.roadmap.space.js"; + +export function registerStatementPart( + adapters: RunnableExampleAdapters, +): void { + type Execution = RunnableExampleWorld; + const bindings = { + "a probe roadmap whose entry holds the work {work}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "that work {state}": () => { + // Given state is materialized by createWorld from the generated point. + }, + "the roadmap recipe reads the probe roadmap": async (execution) => { + await invokeRunnableExample(execution); + }, + "the entry's need reads {need}": (execution, params) => { + compareContractOutcome(execution, { + kind: "then", + key: "the entry's need reads {need}", + text: "the entry's need reads {need}", + params, + }); + }, + } satisfies StepBindings; + + registerRunnableExample( + statementPartContract, + { + work: "spec:probe.root, spec:probe.part", + state: "has one part below ready whose floor reaches ready", + }, + ["work", "state"], + adapters, + bindings, + ); +} diff --git a/test/fixtures/checkout-v1/expected-census/index.md b/test/fixtures/checkout-v1/expected-census/index.md index 5ebd70bfa..f2f7354b1 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.11.0`): 17 nodes · 32 edges · 11 Specs. +A disposable projection of the one graph (schema `0.12.0`): 18 nodes · 34 edges · 11 Specs. ## Spec kinds @@ -47,7 +47,7 @@ A disposable projection of the one graph (schema `0.11.0`): 17 nodes · 32 edges | Value | Count | | --- | ---: | | `Primitive` | 11 | -| `Pack` | 1 | +| `Pack` | 2 | | `Anchor` | 2 | | `CodeNode` | 3 | @@ -55,7 +55,7 @@ A disposable projection of the one graph (schema `0.11.0`): 17 nodes · 32 edges | Value | Count | | --- | ---: | -| `declared` | 12 | +| `declared` | 13 | | `anchored` | 5 | | `inferred` | 0 | @@ -77,7 +77,7 @@ A disposable projection of the one graph (schema `0.11.0`): 17 nodes · 32 edges | `decidedBy` | 2 | | `verifies` | 3 | | `supersedes` | 0 | -| `belongsTo` | 11 | +| `belongsTo` | 13 | | `satisfies` | 3 | | `models` | 1 | | `memberOf` | 0 | @@ -88,7 +88,7 @@ A disposable projection of the one graph (schema `0.11.0`): 17 nodes · 32 edges | Value | Count | | --- | ---: | -| `declared` | 27 | +| `declared` | 29 | | `anchored` | 5 | | `inferred` | 0 | diff --git a/test/fixtures/checkout-v1/expected-design-review/index.md b/test/fixtures/checkout-v1/expected-design-review/index.md index 7242ee126..e9966e10f 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 Design Review — a pure projection of the one graph (`graph.json`, schema `0.11.0`): 17 nodes · 32 edges. +The Design Review — a pure projection of the one graph (`graph.json`, schema `0.12.0`): 18 nodes · 34 edges. ## Specs @@ -20,6 +20,7 @@ The Design Review — a pure projection of the one graph (`graph.json`, schema ` ## Packs +- [`pack:checkout-roadmap`](pack/checkout-roadmap.md) Checkout roadmap — Clarify how a customer reaches order creation, then review the shared order vocabulary. The order is advisory priority; needs come from the graph and confer no permission. - [`pack:checkout-v1`](pack/checkout-v1.md) Checkout v1 — Let customers create orders from valid carts with honest authored traceability. ## Findings diff --git a/test/fixtures/checkout-v1/expected-design-review/pack/checkout-roadmap.md b/test/fixtures/checkout-v1/expected-design-review/pack/checkout-roadmap.md new file mode 100644 index 000000000..18ffe3644 --- /dev/null +++ b/test/fixtures/checkout-v1/expected-design-review/pack/checkout-roadmap.md @@ -0,0 +1,90 @@ +# Checkout roadmap + +`pack:checkout-roadmap` · Pack (the grouping / review aggregate — states no truth of its own) · authored in [specs/roadmap.pack.sdp.md](../../../specs/roadmap.pack.sdp.md) `[declared]` + +> Clarify how a customer reaches order creation, then review the shared order vocabulary. The order is advisory priority; needs come from the graph and confer no permission. + +## Members + +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. + +| Spec | Kind | Altitude | Stated | Floor reached | Stated next rung | Design entries | Declarations | Open questions | Decisions | Design reference | Implementation binding | Verifier binding | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| [`spec:orders.order-placement-flow`](../spec/orders.order-placement-flow.md) (1) Order placement flow | workflow | feature | scoped | scoped | defined · `kind-evidence-complete` | 0 | 0 | 0 | — | none | none | none | +| [`spec:orders.order-model`](../spec/orders.order-model.md) (2) Order-management domain vocabulary | model | story | defined | ready | ready · nothing blocks | 0 | 0 | 0 | — | none | none | none | + +## Stated 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). + +### Waiting for the author's statement + +The floor already holds these members at their stated 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-model`](../spec/orders.order-model.md) Order-management domain vocabulary + +## Boundary + +### Rests on + +The Specs outside the Pack that a member refines, depends on, is constrained by or is decided by. + +| Spec | Stated | Implementation binding | Members joined | +|---|---|---|---| +| [`spec:orders.create-order`](../spec/orders.create-order.md) Customer creates an order | defined | present | dependsOn: [`spec:orders.order-placement-flow`](../spec/orders.order-placement-flow.md) | +| [`spec:orders.order-management`](../spec/orders.order-management.md) Order management | defined | none | refines: [`spec:orders.order-placement-flow`](../spec/orders.order-placement-flow.md), [`spec:orders.order-model`](../spec/orders.order-model.md) | + +### 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 + +No member records an open question. + +## 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-placement-flow`](../spec/orders.order-placement-flow.md) Order placement flow + +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. + +## Verifier coverage gaps + +Members other than decisions with no verifier binding — a surfaced absence, informative, never a gate. `ready` members are the priority slice (designed, stated done, unverified): + +- [`spec:orders.order-placement-flow`](../spec/orders.order-placement-flow.md) — Order placement flow (stated `scoped`) +- [`spec:orders.order-model`](../spec/orders.order-model.md) — Order-management domain vocabulary (stated `defined`) + +## Findings + +None — conformance + honesty clean for this page's subject. + +--- + +*Generated from the one graph by `sdp view` — read-only; regenerate to update.* diff --git a/test/fixtures/checkout-v1/expected-design-review/spec/orders.order-model.md b/test/fixtures/checkout-v1/expected-design-review/spec/orders.order-model.md index 663a10ece..ed8a10f79 100644 --- a/test/fixtures/checkout-v1/expected-design-review/spec/orders.order-model.md +++ b/test/fixtures/checkout-v1/expected-design-review/spec/orders.order-model.md @@ -30,7 +30,7 @@ Every line is a one-hop neighbor over the curated graph: changing this spec touches this list plus the bindings above. Deeper reach is a script over the reader; symbol-level reach is the aspirational impact graph. -- Belongs to: [`pack:checkout-v1`](../pack/checkout-v1.md) `[declared]` +- Belongs to: [`pack:checkout-roadmap`](../pack/checkout-roadmap.md) · [`pack:checkout-v1`](../pack/checkout-v1.md) `[declared]` - refines → [`spec:orders.order-management`](orders.order-management.md) — Order management `[declared]` ## Findings diff --git a/test/fixtures/checkout-v1/expected-design-review/spec/orders.order-placement-flow.md b/test/fixtures/checkout-v1/expected-design-review/spec/orders.order-placement-flow.md index b7a06270b..5f5f2bad5 100644 --- a/test/fixtures/checkout-v1/expected-design-review/spec/orders.order-placement-flow.md +++ b/test/fixtures/checkout-v1/expected-design-review/spec/orders.order-placement-flow.md @@ -28,7 +28,7 @@ Every line is a one-hop neighbor over the curated graph: changing this spec touches this list plus the bindings above. Deeper reach is a script over the reader; symbol-level reach is the aspirational impact graph. -- Belongs to: [`pack:checkout-v1`](../pack/checkout-v1.md) `[declared]` +- Belongs to: [`pack:checkout-roadmap`](../pack/checkout-roadmap.md) · [`pack:checkout-v1`](../pack/checkout-v1.md) `[declared]` - dependsOn → [`spec:orders.create-order`](orders.create-order.md) — Customer creates an order `[declared]` - refines → [`spec:orders.order-management`](orders.order-management.md) — Order management `[declared]` diff --git a/test/fixtures/checkout-v1/expected-graph.json b/test/fixtures/checkout-v1/expected-graph.json index a3383f753..cf1f68921 100644 --- a/test/fixtures/checkout-v1/expected-graph.json +++ b/test/fixtures/checkout-v1/expected-graph.json @@ -1,5 +1,5 @@ { - "schemaVersion": "0.11.0", + "schemaVersion": "0.12.0", "nodes": [ { "id": "api:orders.post", @@ -33,6 +33,19 @@ "file": "test/orders/create-order.oracle.ts", "line": 12 }, + { + "id": "pack:checkout-roadmap", + "nodeType": "Pack", + "claim": "declared", + "title": "Checkout roadmap", + "framing": "Clarify how a customer reaches order creation, then review the shared order vocabulary. The order is advisory priority; needs come from the graph and confer no permission.", + "file": "specs/roadmap.pack.sdp.md", + "members": [ + "spec:orders.order-placement-flow", + "spec:orders.order-model" + ], + "order": "priority" + }, { "id": "pack:checkout-v1", "nodeType": "Pack", @@ -518,6 +531,12 @@ "to": "spec:decisions.order-lifecycle", "claim": "declared" }, + { + "from": "spec:orders.order-model", + "type": "belongsTo", + "to": "pack:checkout-roadmap", + "claim": "declared" + }, { "from": "spec:orders.order-model", "type": "belongsTo", @@ -530,6 +549,12 @@ "to": "spec:orders.order-management", "claim": "declared" }, + { + "from": "spec:orders.order-placement-flow", + "type": "belongsTo", + "to": "pack:checkout-roadmap", + "claim": "declared" + }, { "from": "spec:orders.order-placement-flow", "type": "belongsTo", diff --git a/test/fixtures/checkout-v1/expected-mermaid/index.md b/test/fixtures/checkout-v1/expected-mermaid/index.md index f86598ccd..1af952b32 100644 --- a/test/fixtures/checkout-v1/expected-mermaid/index.md +++ b/test/fixtures/checkout-v1/expected-mermaid/index.md @@ -16,6 +16,7 @@ ## Packs +- [`pack:checkout-roadmap`](pack/checkout-roadmap.md) - [`pack:checkout-v1`](pack/checkout-v1.md) _Generated from the one graph by `sdp mermaid` — read-only; regenerate to update._ diff --git a/test/fixtures/checkout-v1/expected-mermaid/pack/checkout-roadmap.md b/test/fixtures/checkout-v1/expected-mermaid/pack/checkout-roadmap.md new file mode 100644 index 000000000..736b06882 --- /dev/null +++ b/test/fixtures/checkout-v1/expected-mermaid/pack/checkout-roadmap.md @@ -0,0 +1,12 @@ +# Pack membership: pack:checkout-roadmap + +```mermaid +flowchart LR + n_007000610063006b003a0063006800650063006b006f00750074002d0072006f00610064006d00610070["Checkout roadmap pack:checkout-roadmap Pack"] + n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d006f00640065006c["Order-management domain vocabulary spec:orders.order-model Primitive"] + n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d0070006c006100630065006d0065006e0074002d0066006c006f0077["Order placement flow spec:orders.order-placement-flow Primitive"] + n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d006f00640065006c -->|"belongsTo [declared]"| n_007000610063006b003a0063006800650063006b006f00750074002d0072006f00610064006d00610070 + n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d0070006c006100630065006d0065006e0074002d0066006c006f0077 -->|"belongsTo [declared]"| n_007000610063006b003a0063006800650063006b006f00750074002d0072006f00610064006d00610070 +``` + +_Generated from the one graph by `sdp mermaid` — read-only; regenerate to update._ diff --git a/test/fixtures/checkout-v1/expected-mermaid/spec/orders.order-model.md b/test/fixtures/checkout-v1/expected-mermaid/spec/orders.order-model.md index ba55b21fd..dda94b8ea 100644 --- a/test/fixtures/checkout-v1/expected-mermaid/spec/orders.order-model.md +++ b/test/fixtures/checkout-v1/expected-mermaid/spec/orders.order-model.md @@ -2,9 +2,11 @@ ```mermaid flowchart LR + n_007000610063006b003a0063006800650063006b006f00750074002d0072006f00610064006d00610070["Checkout roadmap pack:checkout-roadmap Pack"] n_007000610063006b003a0063006800650063006b006f00750074002d00760031["Checkout v1 pack:checkout-v1 Pack"] n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d0061006e006100670065006d0065006e0074["Order management spec:orders.order-management Primitive"] n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d006f00640065006c["Order-management domain vocabulary spec:orders.order-model Primitive"] + n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d006f00640065006c -->|"belongsTo [declared]"| n_007000610063006b003a0063006800650063006b006f00750074002d0072006f00610064006d00610070 n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d006f00640065006c -->|"belongsTo [declared]"| n_007000610063006b003a0063006800650063006b006f00750074002d00760031 n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d006f00640065006c -->|"refines [declared]"| n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d0061006e006100670065006d0065006e0074 ``` diff --git a/test/fixtures/checkout-v1/expected-mermaid/spec/orders.order-placement-flow.md b/test/fixtures/checkout-v1/expected-mermaid/spec/orders.order-placement-flow.md index 6191cb559..4bd35abdc 100644 --- a/test/fixtures/checkout-v1/expected-mermaid/spec/orders.order-placement-flow.md +++ b/test/fixtures/checkout-v1/expected-mermaid/spec/orders.order-placement-flow.md @@ -2,10 +2,12 @@ ```mermaid flowchart LR + n_007000610063006b003a0063006800650063006b006f00750074002d0072006f00610064006d00610070["Checkout roadmap pack:checkout-roadmap Pack"] n_007000610063006b003a0063006800650063006b006f00750074002d00760031["Checkout v1 pack:checkout-v1 Pack"] n_0073007000650063003a006f00720064006500720073002e006300720065006100740065002d006f0072006400650072["Customer creates an order spec:orders.create-order Primitive"] n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d0061006e006100670065006d0065006e0074["Order management spec:orders.order-management Primitive"] n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d0070006c006100630065006d0065006e0074002d0066006c006f0077["Order placement flow spec:orders.order-placement-flow Primitive"] + n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d0070006c006100630065006d0065006e0074002d0066006c006f0077 -->|"belongsTo [declared]"| n_007000610063006b003a0063006800650063006b006f00750074002d0072006f00610064006d00610070 n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d0070006c006100630065006d0065006e0074002d0066006c006f0077 -->|"belongsTo [declared]"| n_007000610063006b003a0063006800650063006b006f00750074002d00760031 n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d0070006c006100630065006d0065006e0074002d0066006c006f0077 -->|"dependsOn [declared]"| n_0073007000650063003a006f00720064006500720073002e006300720065006100740065002d006f0072006400650072 n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d0070006c006100630065006d0065006e0074002d0066006c006f0077 -->|"refines [declared]"| n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d0061006e006100670065006d0065006e0074 diff --git a/test/fixtures/extract/annotation-forms/expected-graph.json b/test/fixtures/extract/annotation-forms/expected-graph.json index e9a32c9d1..5f673fb8f 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.11.0", + "schemaVersion": "0.12.0", "nodes": [ { "id": "component:platform.gateway", diff --git a/test/fixtures/studio/platform-architecture.json b/test/fixtures/studio/platform-architecture.json new file mode 100644 index 000000000..36b9ae3f5 --- /dev/null +++ b/test/fixtures/studio/platform-architecture.json @@ -0,0 +1,366 @@ +{ + "boxes": [ + { + "id": "component:example.inventory-context", + "title": "the Inventory context component", + "words": "the Inventory context component, 4 members", + "detail": "4 members", + "context": "inventory", + "layer": "adapter", + "target": "#component:example.inventory-context" + }, + { + "id": "component:example.inventory-domain", + "title": "the Inventory context's pure domain code", + "words": "the Inventory context's pure domain code, 1 members", + "detail": "1 members", + "context": "inventory", + "layer": "domain", + "target": "#component:example.inventory-domain" + }, + { + "id": "component:example.orders-context", + "title": "the Orders context component", + "words": "the Orders context component, 4 members", + "detail": "4 members", + "context": "orders", + "layer": "adapter", + "target": "#component:example.orders-context" + }, + { + "id": "component:example.orders-domain", + "title": "the Orders context's pure domain code", + "words": "the Orders context's pure domain code, 1 members", + "detail": "1 members", + "context": "orders", + "layer": "domain", + "target": "#component:example.orders-domain" + }, + { + "id": "component:example.parent", + "title": "the production composition's parent deployment over Orders and Inventory", + "words": "the production composition's parent deployment over Orders and Inventory, 4 members", + "detail": "4 members", + "context": "orders-inventory", + "layer": "application", + "target": "#component:example.parent" + }, + { + "id": "component:platform.audit", + "title": "the audit library: the audit record of a command and of a gate change", + "words": "the audit library: the audit record of a command and of a gate change, 0 members", + "detail": "0 members", + "context": "platform", + "layer": "infrastructure", + "target": "#component:platform.audit" + }, + { + "id": "component:platform.command", + "title": "the command library that runs in the parent deployment", + "words": "the command library that runs in the parent deployment, 7 members", + "detail": "7 members", + "context": "platform", + "layer": "application", + "target": "#component:platform.command" + }, + { + "id": "component:platform.context", + "title": "the context library that runs inside every context component", + "words": "the context library that runs inside every context component, 8 members", + "detail": "8 members", + "context": "platform", + "layer": "adapter", + "target": "#component:platform.context" + }, + { + "id": "component:platform.gate", + "title": "the maintenance gate: its table, the writer check, the two writers and the operator entries", + "words": "the maintenance gate: its table, the writer check, the two writers and the operator entries, 3 members", + "detail": "3 members", + "context": "platform", + "layer": "infrastructure", + "target": "#component:platform.gate" + }, + { + "id": "component:platform.kernel", + "title": "the pure domain kernel: the decider contract and the outcome types", + "words": "the pure domain kernel: the decider contract and the outcome types, 2 members", + "detail": "2 members", + "context": "platform", + "layer": "domain", + "target": "#component:platform.kernel" + }, + { + "id": "component:platform.operations", + "title": "the operations library: the diagnostic record a command writes", + "words": "the operations library: the diagnostic record a command writes, 0 members", + "detail": "0 members", + "context": "platform", + "layer": "infrastructure", + "target": "#component:platform.operations" + }, + { + "id": "component:platform.read-model", + "title": "the read-model library: declare, rebuild, write and read a read model", + "words": "the read-model library: declare, rebuild, write and read a read model, 6 members", + "detail": "6 members", + "context": "platform", + "layer": "application", + "target": "#component:platform.read-model" + } + ], + "edges": [ + { + "from": "component:example.inventory-context", + "to": "component:example.inventory-domain", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.inventory-context", + "to": "component:platform.context", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.inventory-domain", + "to": "component:platform.kernel", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.orders-context", + "to": "component:example.orders-domain", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.orders-context", + "to": "component:platform.context", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.orders-domain", + "to": "component:platform.kernel", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.parent", + "to": "component:example.inventory-context", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.parent", + "to": "component:example.inventory-domain", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.parent", + "to": "component:example.orders-context", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.parent", + "to": "component:example.orders-domain", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.parent", + "to": "component:platform.audit", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.parent", + "to": "component:platform.command", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.parent", + "to": "component:platform.context", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.parent", + "to": "component:platform.gate", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:example.parent", + "to": "component:platform.read-model", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.audit", + "to": "component:platform.command", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.audit", + "to": "component:platform.context", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.command", + "to": "component:platform.audit", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.command", + "to": "component:platform.context", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.command", + "to": "component:platform.gate", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.command", + "to": "component:platform.kernel", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.command", + "to": "component:platform.operations", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.command", + "to": "component:platform.read-model", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.context", + "to": "component:platform.command", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.context", + "to": "component:platform.kernel", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.gate", + "to": "component:platform.audit", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.gate", + "to": "component:platform.command", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.gate", + "to": "component:platform.context", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.gate", + "to": "component:platform.read-model", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.operations", + "to": "component:platform.command", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.operations", + "to": "component:platform.context", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.operations", + "to": "component:platform.kernel", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.read-model", + "to": "component:platform.command", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.read-model", + "to": "component:platform.context", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.read-model", + "to": "component:platform.gate", + "kind": "uses", + "claim": "anchored", + "count": 1 + }, + { + "from": "component:platform.read-model", + "to": "component:platform.kernel", + "kind": "uses", + "claim": "anchored", + "count": 1 + } + ] +} diff --git a/test/fixtures/studio/platform-traces.json b/test/fixtures/studio/platform-traces.json new file mode 100644 index 000000000..471a3c1b2 --- /dev/null +++ b/test/fixtures/studio/platform-traces.json @@ -0,0 +1,589 @@ +[ + { + "subject": "spec:laws.law05-authorization-before-execution-and-disclosure", + "boxes": [ + { + "id": "spec:agents.agent-runs", + "words": "Agent runs", + "target": "#relations" + }, + { + "id": "spec:application.read-models", + "words": "Read models", + "target": "#relations" + }, + { + "id": "spec:command.actor-and-scope", + "words": "Actor, tenant scope, namespace and grants", + "target": "#relations" + }, + { + "id": "spec:command.command-pipeline", + "words": "The command pipeline", + "target": "#relations" + }, + { + "id": "spec:command.idempotency-and-receipts", + "words": "Idempotency and receipts", + "target": "#relations" + }, + { + "id": "spec:command.outcome-boundary", + "words": "The outcome and rejection boundary", + "target": "#relations" + }, + { + "id": "spec:command.tenancy-and-authority", + "words": "Tenancy and authority", + "target": "#relations" + }, + { + "id": "spec:context.queries", + "words": "The context queries", + "target": "#relations" + }, + { + "id": "spec:effects.claim-call-settle", + "words": "Claim, call and settle", + "target": "#relations" + }, + { + "id": "spec:laws.law05-authorization-before-execution-and-disclosure", + "words": "Authorization comes before execution and disclosure", + "target": "#relations" + }, + { + "id": "spec:obligations.local-reaction-wrapper", + "words": "Local reaction wrapper", + "target": "#relations" + }, + { + "id": "spec:obligations.obligation-module", + "words": "Obligation module", + "target": "#relations" + }, + { + "id": "spec:obligations.operator-operations", + "words": "Operator operations on obligations", + "target": "#relations" + }, + { + "id": "spec:operations.baseline-operations", + "words": "Baseline operations", + "target": "#relations" + }, + { + "id": "spec:platform.transactional-domain-platform", + "words": "Convex transactional domain platform", + "target": "#relations" + }, + { + "id": "spec:processes.approvals", + "words": "Approvals bind to the exact proposal", + "target": "#relations" + } + ], + "edges": [ + { + "from": "spec:laws.law05-authorization-before-execution-and-disclosure", + "to": "spec:platform.transactional-domain-platform", + "kind": "refines", + "claim": "declared" + }, + { + "from": "spec:agents.agent-runs", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:application.read-models", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:command.actor-and-scope", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:command.command-pipeline", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:command.idempotency-and-receipts", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:command.outcome-boundary", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:command.tenancy-and-authority", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:context.queries", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:effects.claim-call-settle", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:obligations.local-reaction-wrapper", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:obligations.obligation-module", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:obligations.operator-operations", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:operations.baseline-operations", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:platform.transactional-domain-platform", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:processes.approvals", + "to": "spec:laws.law05-authorization-before-execution-and-disclosure", + "kind": "constrainedBy", + "claim": "declared" + } + ] + }, + { + "subject": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "boxes": [ + { + "id": "spec:application.rebuild", + "words": "Rebuild: generations, backfill and the write pause", + "target": "#relations" + }, + { + "id": "spec:application.write-pause", + "words": "Write pause", + "target": "#relations" + }, + { + "id": "spec:context.journal", + "words": "The journal", + "target": "#relations" + }, + { + "id": "spec:decisions.d13-deferred-work-is-an-obligation", + "words": "Deferred work is an obligation", + "target": "#relations" + }, + { + "id": "spec:decisions.d14-external-effects-declare-safe-repetition", + "words": "External effects declare how repetition is safe", + "target": "#relations" + }, + { + "id": "spec:effects.claim-call-settle", + "words": "Claim, call and settle", + "target": "#relations" + }, + { + "id": "spec:effects.external-effects", + "words": "External effects", + "target": "#relations" + }, + { + "id": "spec:effects.retry-ownership", + "words": "One retry owner per obligation", + "target": "#relations" + }, + { + "id": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "words": "Scheduled mutations run once, scheduled actions are not retried", + "target": "#relations" + }, + { + "id": "spec:facts.fact-ledger", + "words": "The fact ledger", + "target": "#relations" + }, + { + "id": "spec:obligations.do-nothing-check", + "words": "Do-nothing check at obligation activation", + "target": "#relations" + }, + { + "id": "spec:obligations.local-reaction-wrapper", + "words": "Local reaction wrapper", + "target": "#relations" + }, + { + "id": "spec:obligations.obligation-module", + "words": "Obligation module", + "target": "#relations" + }, + { + "id": "spec:obligations.sweeper", + "words": "Obligation sweeper", + "target": "#relations" + } + ], + "edges": [ + { + "from": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "to": "spec:facts.fact-ledger", + "kind": "refines", + "claim": "declared" + }, + { + "from": "spec:application.rebuild", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:application.write-pause", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:context.journal", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:decisions.d13-deferred-work-is-an-obligation", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:decisions.d14-external-effects-declare-safe-repetition", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:effects.claim-call-settle", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:effects.external-effects", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:effects.retry-ownership", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:obligations.do-nothing-check", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:obligations.local-reaction-wrapper", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:obligations.obligation-module", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:obligations.sweeper", + "to": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "kind": "constrainedBy", + "claim": "declared" + } + ] + }, + { + "subject": "spec:laws.law11-tenant-scope-named", + "boxes": [ + { + "id": "spec:application.aggregate-projection", + "words": "Aggregate projection", + "target": "#relations" + }, + { + "id": "spec:application.generation-registry", + "words": "Generation registry", + "target": "#relations" + }, + { + "id": "spec:application.history-rebuild", + "words": "Rebuild of a history view under a write pause", + "target": "#relations" + }, + { + "id": "spec:application.parent-use-cases", + "words": "Parent use cases", + "target": "#relations" + }, + { + "id": "spec:application.projection-contract", + "words": "Projection contract", + "target": "#relations" + }, + { + "id": "spec:application.read-models", + "words": "Read models", + "target": "#relations" + }, + { + "id": "spec:application.rebuild", + "words": "Rebuild: generations, backfill and the write pause", + "target": "#relations" + }, + { + "id": "spec:application.restore", + "words": "Restore", + "target": "#relations" + }, + { + "id": "spec:command.actor-and-scope", + "words": "Actor, tenant scope, namespace and grants", + "target": "#relations" + }, + { + "id": "spec:command.command-pipeline", + "words": "The command pipeline", + "target": "#relations" + }, + { + "id": "spec:command.receipt-table", + "words": "The receipts table", + "target": "#relations" + }, + { + "id": "spec:command.tenancy-and-authority", + "words": "Tenancy and authority", + "target": "#relations" + }, + { + "id": "spec:context.context-component", + "words": "The context component", + "target": "#relations" + }, + { + "id": "spec:context.journal", + "words": "The journal", + "target": "#relations" + }, + { + "id": "spec:context.persistence-adapter", + "words": "The persistence adapter", + "target": "#relations" + }, + { + "id": "spec:context.queries", + "words": "The context queries", + "target": "#relations" + }, + { + "id": "spec:context.tables", + "words": "The context tables", + "target": "#relations" + }, + { + "id": "spec:laws.law11-tenant-scope-named", + "words": "Every command and query names its tenant scope", + "target": "#relations" + }, + { + "id": "spec:obligations.operator-operations", + "words": "Operator operations on obligations", + "target": "#relations" + }, + { + "id": "spec:obligations.record-contract", + "words": "Obligation record contract", + "target": "#relations" + }, + { + "id": "spec:operations.baseline-operations", + "words": "Baseline operations", + "target": "#relations" + }, + { + "id": "spec:platform.transactional-domain-platform", + "words": "Convex transactional domain platform", + "target": "#relations" + } + ], + "edges": [ + { + "from": "spec:laws.law11-tenant-scope-named", + "to": "spec:platform.transactional-domain-platform", + "kind": "refines", + "claim": "declared" + }, + { + "from": "spec:application.aggregate-projection", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:application.generation-registry", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:application.history-rebuild", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:application.parent-use-cases", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:application.projection-contract", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:application.read-models", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:application.rebuild", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:application.restore", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:command.actor-and-scope", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:command.command-pipeline", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:command.receipt-table", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:command.tenancy-and-authority", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:context.context-component", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:context.journal", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:context.persistence-adapter", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:context.queries", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:context.tables", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:obligations.operator-operations", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:obligations.record-contract", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:operations.baseline-operations", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + }, + { + "from": "spec:platform.transactional-domain-platform", + "to": "spec:laws.law11-tenant-scope-named", + "kind": "constrainedBy", + "claim": "declared" + } + ] + } +] diff --git a/test/graph-schema.test.ts b/test/graph-schema.test.ts index 399f5be0b..858be04bf 100644 --- a/test/graph-schema.test.ts +++ b/test/graph-schema.test.ts @@ -39,7 +39,7 @@ const claimTaxonomyTestAnchor = specTest({ void claimTaxonomyTestAnchor; describe("graph schema", () => { - it("serializes authored entry order with description first and schema 0.11.0", () => { + it("serializes authored entry order with description first and schema 0.12.0", () => { const graph = deriveFixtureGraph({ specs: [ spec({ @@ -62,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.11.0"); + expect(serialized.schemaVersion).toBe("0.12.0"); expect(Object.keys(node?.sections?.design ?? {})).toEqual([ "description", "zeta", @@ -209,12 +209,19 @@ The Model prose. 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 packs = graph.nodes.filter((node) => node.nodeType === "Pack"); + expect(packs.map((pack) => pack.id).sort()).toEqual([ + "pack:checkout-roadmap", + "pack:checkout-v1", + ]); + for (const pack of packs) { + expect(pack.claim).toBe("declared"); + const membership = graph.edges.filter( + (edge) => edge.type === "belongsTo" && edge.to === pack.id, + ); + expect(membership.map((edge) => edge.from).sort()).toEqual([...pack.members].sort()); + expect(membership.every((edge) => 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( @@ -240,7 +247,7 @@ The Model prose. }); it("exports the graph schema contracts", () => { - expect(schemaVersion).toBe("0.11.0"); + expect(schemaVersion).toBe("0.12.0"); expect(graphClaims).toEqual(["declared", "anchored", "inferred"]); expect(graphNodeTypes).toEqual(["Primitive", "Pack", "Anchor", "CodeNode"]); expect(deliveryFactNames).toEqual(["implemented", "has-verifier", "observed"]); diff --git a/test/helpers/design-slice-probe.ts b/test/helpers/design-slice-probe.ts new file mode 100644 index 000000000..619e91535 --- /dev/null +++ b/test/helpers/design-slice-probe.ts @@ -0,0 +1,112 @@ +import { schemaVersion } from "../../src/index.js"; +import type { GraphEdge, GraphNode, GraphSchema, PrimitiveNode } from "../../src/index.js"; + +export type SliceProbe = Omit & { + nodes: GraphNode[]; + edges: GraphEdge[]; +}; +export const sid = (name: string) => `spec:probe.${name}`; +export const cid = (name: string) => `component:probe.${name}`; +export function primitive(name: string, extra: Partial = {}): PrimitiveNode { + return { + id: sid(name), + nodeType: "Primitive", + claim: "declared", + specKind: "behavior", + altitude: "story", + readiness: "defined", + title: name, + file: `${name}.sdp.md`, + sections: { + intent: { outcome: "Read the probe." }, + behavior: { rules: ["Read the probe."], ruleKeys: { read: "rules[0]" } }, + }, + ...extra, + }; +} +export function link( + from: string, + type: GraphEdge["type"], + to: string, + claim: GraphEdge["claim"] = "declared", + reason?: string, +): GraphEdge { + return { from, type, to, claim, ...(reason === undefined ? {} : { reason }) }; +} +export function component(name: string): GraphNode { + return { + id: cid(name), + nodeType: "CodeNode", + claim: "anchored", + file: `${name}.ts`, + line: 1, + label: name, + layer: "domain", + context: "probe", + role: "service", + }; +} +export function sliceProbe(): SliceProbe { + return { + schemaVersion, + nodes: [ + primitive("base"), + primitive("top"), + { + id: "pack:probe.slice", + nodeType: "Pack", + claim: "declared", + title: "Probe slice", + framing: "Read these members together.", + file: "probe.pack.sdp.md", + members: [sid("top"), sid("base")], + }, + ], + edges: [ + link(sid("top"), "dependsOn", sid("base"), "declared", "the top needs its base"), + link(sid("top"), "belongsTo", "pack:probe.slice"), + link(sid("base"), "belongsTo", "pack:probe.slice"), + ], + }; +} +export function setMembers(graph: SliceProbe, names: string[]): void { + const pack = graph.nodes.findIndex((node) => node.id === "pack:probe.slice"); + graph.nodes[pack] = { + id: "pack:probe.slice", + nodeType: "Pack", + claim: "declared", + title: "Probe slice", + file: "probe.pack.sdp.md", + members: names.map(sid), + }; + graph.edges = graph.edges.filter((edge) => edge.type !== "belongsTo"); + graph.edges.push(...names.map((name) => link(sid(name), "belongsTo", "pack:probe.slice"))); +} +export function designProbe(anchor: string): SliceProbe { + const graph = sliceProbe(); + const absent = anchor === "no anchor holds"; + const unrelated = anchor === "an anchor holds without satisfying the design"; + const differs = anchor === "an anchor satisfies, with a differing layer and uses"; + const unreadable = anchor === "an anchor satisfies, with an unreadable layer"; + graph.nodes.push( + primitive("design", { + specKind: "model", + sections: { + intent: { outcome: "Design the probe component." }, + model: { terms: { probe: "The component under review." } }, + design: { + id: "`component:probe.designed` with explanatory prose", + layer: unreadable ? "middleware" : differs ? "application" : "domain", + context: "probe", + role: "service", + uses: differs ? "component:probe.missing" : "none", + }, + }, + }), + ); + if (!absent) graph.nodes.push(component("designed")); + if (!absent && !unrelated) + graph.edges.push(link(cid("designed"), "satisfies", sid("design"), "anchored")); + setMembers(graph, ["design"]); + return graph; +} diff --git a/test/helpers/roadmap-probe.ts b/test/helpers/roadmap-probe.ts new file mode 100644 index 000000000..55fbdb141 --- /dev/null +++ b/test/helpers/roadmap-probe.ts @@ -0,0 +1,183 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { evaluateBody, extract } from "../../src/index.js"; +import type { GraphSchema } from "../../src/index.js"; + +export function probeSpec( + name: string, + readiness = "ready", + relations = "\n dependsOn: spec:probe.foundation", + body = "## Behavior\n- rule: The probe has a rule.", + kind = "behavior", +): string { + return `---\nid: spec:probe.${name}\nkind: ${kind}\naltitude: story\nreadiness: ${readiness}\nrelations: ${relations}\n---\n# ${name}\n\n## Intent\n- outcome: Read the probe.\n\n${body}\n`; +} + +export function probePack(name: string, members: readonly string[], framing = ""): string { + return `---\nid: pack:${name}\nspecs:\n${members.map((id) => ` - spec:probe.${id}`).join("\n")}\n---\n# ${name}\n\n${framing}\n`; +} + +export function implementation(name: string): string { + return `/**\n * @sdpAnchor impl:probe.${name}\n * @sdpSatisfies spec:probe.${name}\n */\n`; +} + +/** Parse real carriers; add the Pack field carried by the parallel schema change. */ +export function probeGraph( + files: Record, + priority: readonly string[] = ["probe"], +): GraphSchema { + const root = mkdtempSync(join(tmpdir(), "sdp-roadmap-")); + try { + files = { + "foundation.sdp.md": probeSpec( + "foundation", + "ready", + "\n refines: spec:probe.foundation-rule", + "## Decision\n- context: The probe has settled footing.\n- decision: Keep the footing.", + "decision", + ), + "foundation-rule.sdp.md": probeSpec( + "foundation-rule", + "ready", + "\n decidedBy: spec:probe.foundation", + ), + ...files, + }; + for (const [name, content] of Object.entries(files)) writeFileSync(join(root, name), content); + const { graph, report } = extract({ root }); + const findings = report.findings; + if (findings.some((finding) => finding.severity === "error")) { + throw new Error(JSON.stringify(findings)); + } + return { + ...graph, + nodes: graph.nodes.map((node) => + node.nodeType === "Pack" && priority.includes(node.id.slice(5)) + ? { ...node, order: "priority" as const } + : node, + ), + }; + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +export interface QuestionRow { + address: string | null; + spec: string; + index: number; + question: string; + blocking: boolean; + decider: string | null; + provisional: string | null; + places?: number[]; +} +export interface WorkRow { + id: string; + need: string; + statedReadiness: string; + floorReached: string; + statedNextRung: string | null; + holdsNextRung: { clause: string; targets: string[] }[]; + drift: boolean; +} +export interface EntryRow extends Omit { + place: number; + title: string; + work: Record; + workRows: WorkRow[]; + buildNow: string[]; + blockingQuestions: QuestionRow[]; + decisions: { id: string; blockingQuestions: QuestionRow[] }[]; + restsOn: { + id: string; + via: string; + from: string[]; + need: string; + holdsReady: boolean; + onRoadmap: number | null; + }[]; + restedOnBy: { id: string; via: string; need: string; onRoadmap: number | null }[]; + earlierStatements: { id: string; where: string; onRoadmap: number | null }[]; + packs: string[]; + sharesScopeWith: number[]; +} +export interface Roadmap { + pack: string; + order: string; + totals: Record; + entries: EntryRow[]; + apart: { place: number; id: string; need: string }[]; + unresolved: { place: number; id: string }[]; + waiting: { + questions: QuestionRow[]; + withoutDecider: QuestionRow[]; + statements: { id: string; places: number[] }[]; + }; +} +export interface RoadmapAnswer { + decider: string; + roadmaps: Roadmap[]; + diagnostics?: { parameter: string; problem: string }[]; +} +export async function roadmap( + graph: GraphSchema, + params: Record = {}, +): Promise { + return (await evaluateBody("return recipes(53, params)", { graph, params })) as RoadmapAnswer; +} +export async function memo( + graph: GraphSchema, + params: Record = {}, +): Promise<{ diagnostics: { parameter: string; problem: string }[]; memo: string | null }> { + return (await evaluateBody("return recipes(55, params)", { graph, params })) as { + diagnostics: { parameter: string; problem: string }[]; + memo: string | null; + }; +} + +export const workStates = [ + "is ready and implemented throughout", + "has one part ready and not implemented", + "has one part below ready whose floor reaches ready", + "has one part below ready whose floor stops short", + "has one part implemented below ready", + "has a ready root and one descendant without its kind's evidence", + "has a ready root and one descendant held by a blocking question", + "has one descendant that also refines a Spec outside the roadmap", +] as const; + +export function workGraph(state: string): GraphSchema { + const files: Record = { + "pack.sdp.md": probePack("probe", ["root"]), + "root.sdp.md": probeSpec("root"), + "root.ts": implementation("root"), + }; + let readiness = "ready"; + let relations = "\n refines: spec:probe.root"; + let body: string | undefined; + if (state === workStates[2]) readiness = "defined"; + if (state === workStates[3] || state === workStates[4] || state === workStates[6]) { + readiness = "scoped"; + body = "## Behavior\n- rule: The probe has a rule."; + } + if (state === workStates[5]) { + readiness = "scoped"; + body = ""; + } + files["part.sdp.md"] = probeSpec("part", readiness, relations, body); + if (state === workStates[3] || state === workStates[4] || state === workStates[6]) { + files["part.sdp.md"] = files["part.sdp.md"].replace( + "\n\n## Behavior", + "\n\n### Open questions\n- [blocking #choice] Settle the probe.\n\n## Behavior", + ); + } + if (state === workStates[0] || state === workStates[4]) files["part.ts"] = implementation("part"); + if (state === workStates[7]) { + relations = "\n refines: [spec:probe.root, spec:probe.outside]"; + files["part.sdp.md"] = probeSpec("part", "defined", relations); + files["outside.sdp.md"] = probeSpec("outside", "scoped"); + } + return probeGraph(files); +} diff --git a/test/helpers/structural-coverage.ts b/test/helpers/structural-coverage.ts index 31e6f82f7..6910436e4 100644 --- a/test/helpers/structural-coverage.ts +++ b/test/helpers/structural-coverage.ts @@ -7,8 +7,73 @@ import { createRuntimeExportResolver, referencedModuleSource } from "./runtime-e export type StructuralCoverageResult = | "ok" | "exported unit missing" + | "covering source does not type-consume unit" | "covering source does not value-consume unit"; +function auditTypeCoverage( + checker: ts.TypeChecker, + source: ts.SourceFile, + name: string, + coveringSource: ts.SourceFile | undefined, +): StructuralCoverageResult { + const module = checker.getSymbolAtLocation(source); + const exported = + module && checker.getExportsOfModule(module).find((entry) => entry.name === name); + const resolve = (symbol: ts.Symbol): ts.Symbol => + symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol; + const target = exported && resolve(exported); + if (target === undefined || !(target.flags & ts.SymbolFlags.Type)) return "exported unit missing"; + if (coveringSource === undefined) return "covering source does not type-consume unit"; + + const imported = new Set(); + const namespaces = new Set(); + for (const statement of coveringSource.statements) { + if (!ts.isImportDeclaration(statement)) continue; + const bindings = statement.importClause?.namedBindings; + if (bindings === undefined) continue; + if (ts.isNamespaceImport(bindings)) { + const symbol = checker.getSymbolAtLocation(bindings.name); + if (symbol !== undefined) namespaces.add(symbol); + continue; + } + for (const entry of bindings.elements) { + const symbol = checker.getSymbolAtLocation(entry.name); + if (symbol !== undefined && resolve(symbol) === target) imported.add(symbol); + } + } + // Audit explicit type references, heritage clauses and type queries. Resolve both + // ends of qualified names so an unused, shadowed or wrong-source import stays uncovered. + const consumes = (name: ts.Node): boolean => { + let root = name; + const leaf = ts.isQualifiedName(name) + ? name.right + : ts.isPropertyAccessExpression(name) + ? name.name + : name; + while (ts.isQualifiedName(root) || ts.isPropertyAccessExpression(root)) { + root = ts.isQualifiedName(root) ? root.left : root.expression; + } + const local = checker.getSymbolAtLocation(root); + if (local === undefined) return false; + if (ts.isIdentifier(name)) return imported.has(local); + const member = checker.getSymbolAtLocation(leaf); + return namespaces.has(local) && member !== undefined && resolve(member) === target; + }; + const consumed = new Set(); + const visit = (node: ts.Node): void => { + if ( + (ts.isTypeReferenceNode(node) && consumes(node.typeName)) || + (ts.isExpressionWithTypeArguments(node) && consumes(node.expression)) || + (ts.isTypeQueryNode(node) && consumes(node.exprName)) + ) { + consumed.add(node); + } + ts.forEachChild(node, visit); + }; + visit(coveringSource); + return consumed.size > 0 ? "ok" : "covering source does not type-consume unit"; +} + function valueImportedLocals( checker: ts.TypeChecker, source: ts.SourceFile, @@ -53,6 +118,7 @@ export function auditStructuralCoverage( program: ts.Program, unit: string, coveringSourcePath: string, + consumption: "value" | "type" = "value", ): StructuralCoverageResult { const separator = unit.indexOf("#"); if (separator <= 0 || separator !== unit.lastIndexOf("#") || separator === unit.length - 1) { @@ -67,6 +133,12 @@ export function auditStructuralCoverage( } const checker = program.getTypeChecker(); + const coveringSource = program.getSourceFile( + resolve(program.getCurrentDirectory(), coveringSourcePath), + ); + if (consumption === "type") { + return auditTypeCoverage(checker, source, unit.slice(separator + 1), coveringSource); + } const resolveRuntimeExport = createRuntimeExportResolver(checker); const resolution = resolveRuntimeExport(source, unit.slice(separator + 1)); if (resolution.cyclic || resolution.candidates.size !== 1) { @@ -83,9 +155,6 @@ export function auditStructuralCoverage( return "exported unit missing"; } - const coveringSource = program.getSourceFile( - resolve(program.getCurrentDirectory(), coveringSourcePath), - ); if (coveringSource === undefined) { return "covering source does not value-consume unit"; } diff --git a/test/helpers/studio-dom.ts b/test/helpers/studio-dom.ts index 9a79a45a5..159690ccf 100644 --- a/test/helpers/studio-dom.ts +++ b/test/helpers/studio-dom.ts @@ -11,6 +11,7 @@ export function studioDom( ) { const definitions = new Map Element>(); const events = new Map>(); + const documentEvents = new Map>(); const observers = new Map(); const copied: string[] = []; const downloads = new Map(); @@ -152,6 +153,17 @@ export function studioDom( querySelectorAll(selector: string) { return body.querySelectorAll(selector); }, + getElementById(id: string) { + return ( + body.querySelectorAll("[id]").find((element) => element.getAttribute("id") === id) ?? null + ); + }, + addEventListener(type: string, listener: Listener) { + documentEvents.set(type, new Set([...(documentEvents.get(type) ?? []), listener])); + }, + removeEventListener(type: string, listener: Listener) { + documentEvents.get(type)?.delete(listener); + }, defaultView: { localStorage: storage, navigator: { diff --git a/test/helpers/validator-audit.ts b/test/helpers/validator-audit.ts index 39a24e8be..1c7d46b8f 100644 --- a/test/helpers/validator-audit.ts +++ b/test/helpers/validator-audit.ts @@ -87,7 +87,7 @@ export function scanFindingIds(srcRoot: string): SourceScan { * @sdpAnchor impl:protocol.validator-self-testing * @sdpLabel audits the finding registries against source literals and paired probes * @sdpSatisfies spec:validation.validator-self-testing - * @sdpRole utility + * @sdpRole validator */ export function auditValidatorEvidence( table: readonly ProbeEntry[], diff --git a/test/helpers/validator-probes.ts b/test/helpers/validator-probes.ts index 905b01d75..5d5f5b28a 100644 --- a/test/helpers/validator-probes.ts +++ b/test/helpers/validator-probes.ts @@ -373,6 +373,6 @@ const extractionPairs: ProbeEntry[] = [ * @sdpAnchor impl:protocol.validator-probe-table * @sdpLabel declares the refusing and accepting probes for every registered finding id * @sdpSatisfies spec:validation.validator-self-testing - * @sdpRole utility + * @sdpRole contract */ export const validatorProbeTable: readonly ProbeEntry[] = [...graphPairs, ...extractionPairs]; diff --git a/test/iteration-recipes.test.ts b/test/iteration-recipes.test.ts index 2ce705f6c..504e86f59 100644 --- a/test/iteration-recipes.test.ts +++ b/test/iteration-recipes.test.ts @@ -154,6 +154,9 @@ interface Diff { membersAdded: string[]; membersRemoved: string[]; reordered: boolean; + orderChanged: boolean; + orderBefore: "reading" | "priority" | null; + orderAfter: "reading" | "priority" | null; }[]; bindings: { id: string; @@ -318,6 +321,9 @@ describe("iteration loop: design diff", () => { membersAdded: ["spec:probe.new"], membersRemoved: ["spec:probe.b"], reordered: false, + orderChanged: false, + orderBefore: "reading", + orderAfter: "reading", }, ]); const all = await runRecipe(35, { base: base.graph }, head); @@ -2009,6 +2015,9 @@ describe("iteration loop: comparison and budget edges", () => { membersAdded: [], membersRemoved: ["spec:probe.a", "spec:probe.b", "spec:probe.model"], reordered: false, + orderChanged: false, + orderBefore: "reading", + orderAfter: null, }, ]); }); diff --git a/test/reach-examples.test.ts b/test/reach-examples.test.ts index e522374c0..e0d8b3e8f 100644 --- a/test/reach-examples.test.ts +++ b/test/reach-examples.test.ts @@ -28,7 +28,13 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { isDeepStrictEqual } from "node:util"; -import { expect, it, onTestFinished } from "vitest"; +import { expect, it, onTestFinished, vi } from "vitest"; + +// Every example here drives the CLI through full validation runs, and each watch example runs three +// cycles of a probe corpus; under CI load that outruns Vitest's default five seconds, so the file's +// tests, registered by the generated registrars, carry the heavy-test timeout. +const REACH_EXAMPLE_TIMEOUT_MS = 30_000; +vi.setConfig({ testTimeout: REACH_EXAMPLE_TIMEOUT_MS }); import { evaluateBody, extract } from "../src/index.js"; import { unspecified } from "@libar-dev/software-delivery-protocol/runner"; import type { @@ -355,6 +361,23 @@ const watchAdapters = (contract: Contract) => ({ assertThens(contract, world.answer, expected); }, }); +const WATCH_EXAMPLE_SPECS = [ + "spec:validation.corpus-rules.rules-edit-starts-a-cycle", + "spec:validation.corpus-rules.params-edit-starts-a-cycle", + "spec:validation.corpus-rules.unreadable-params-fail-one-file", +]; +it("gives every watch example the heavy-test timeout", ({ task }) => { + const suites = task.file.tasks.filter( + (entry) => + entry.type === "suite" && WATCH_EXAMPLE_SPECS.some((id) => entry.name.endsWith(`(${id})`)), + ); + expect( + suites.map((suite) => suite.name.slice(suite.name.lastIndexOf("(") + 1, -1)).sort(), + ).toEqual([...WATCH_EXAMPLE_SPECS].sort()); + for (const suite of suites) + for (const example of suite.type === "suite" ? suite.tasks : []) + expect(example.type === "test" ? example.timeout : 0).toBe(REACH_EXAMPLE_TIMEOUT_MS); +}); it("leaves non-watch corpus-rule points and incomplete points unspecified", () => { for (const first of [ "returns one error finding", diff --git a/test/reader.test.ts b/test/reader.test.ts index ce7177b9b..955c2f8e4 100644 --- a/test/reader.test.ts +++ b/test/reader.test.ts @@ -197,6 +197,15 @@ describe("the reader — the thin typed loader behind the agent surface", () => const reader = exampleReader(); expect(reader.packs()).toEqual([ + { + id: "pack:checkout-roadmap", + title: "Checkout roadmap", + framing: + "Clarify how a customer reaches order creation, then review the shared order vocabulary. The order is advisory priority; needs come from the graph and confer no permission.", + file: "specs/roadmap.pack.sdp.md", + modelRefs: [], + order: "priority", + }, { id: "pack:checkout-v1", title: "Checkout v1", diff --git a/test/recipes.test.ts b/test/recipes.test.ts index 3127baa28..6bf329b03 100644 --- a/test/recipes.test.ts +++ b/test/recipes.test.ts @@ -348,10 +348,12 @@ 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 headings = recipes + .map((recipe) => ({ + ordinal: recipe.ordinal, + heading: recipe.title.toLowerCase(), + })) + .sort((a, b) => b.heading.length - a.heading.length); const citations: { spec: string; cited: number; named: number }[] = []; for (const node of derived.graph.nodes) { @@ -1151,9 +1153,10 @@ describe("the agent-surface recipe corpus", () => { const result = asRecord(await runRecipe(recipeByOrdinal(13))); const rows = asArray(result.components).map(asRecord); const owners = new Map(expectedMemberOfEdges); - const expectedUses = expectedUsesEdges - .map(([from, to]) => ({ from: owners.get(from) ?? from, to: owners.get(to) ?? to })) - .filter((edge) => edge.from !== edge.to); + const expectedUses = expectedUsesEdges.map(([from, to]) => ({ + from: owners.get(from) ?? from, + to: owners.get(to) ?? to, + })); expect(rows.map((row) => stringAt(row, "id")).sort()).toEqual([...expectedComponentIds].sort()); @@ -1498,13 +1501,13 @@ describe("the agent-surface recipe corpus", () => { }); }); - // Given: a clone of the live ExtractionResult with impl:protocol.agent-surface removed + // Given: a clone of the live ExtractionResult with impl:protocol.reader removed // and its memberOf edge retained — invalid, but still a graph validateGraph can report. // When: catalog recipe 17 runs through real runSdpCli on that override. // Then: validateGraph reports conformance/referential-integrity, the sink exits 0, and // the unresolved member row keeps null label/file/line under component:protocol.reader. it("preserves an unresolved architecture-map member when memberOf outlives the node", async () => { - const missingMemberId = "impl:protocol.agent-surface"; + const missingMemberId = "impl:protocol.reader"; const ownerComponentId = "component:protocol.reader"; const dirty: ExtractionResult = { counts: derived.counts, @@ -1754,11 +1757,11 @@ describe("the agent-surface recipe corpus", () => { // memberOf edges retained — invalid, but still a graph validateGraph can report. // When: catalog recipe 19 runs through real runSdpCli on that override. // Then: validateGraph reports conformance/referential-integrity, the sink exits 0, the - // unresolved component row keeps null label/file/line, impl:protocol.agent-surface is retained, + // unresolved component row keeps null label/file/line, impl:protocol.reader is retained, // and that component contributes no entry point. it("preserves an unresolved component row when memberOf outlives the node", async () => { const missingComponentId = "component:protocol.reader"; - const retainedImplId = "impl:protocol.agent-surface"; + const retainedImplId = "impl:protocol.reader"; const dirty: ExtractionResult = { counts: derived.counts, report: derived.report, @@ -4548,14 +4551,17 @@ describe("recipe parameters and the shipped recipe files", () => { !(recipe.ordinal === 41 && name !== "prefixes") && !(recipe.ordinal === 42 && name !== "severity") && !(recipe.ordinal === 51 && name !== "severity") && - !(recipe.ordinal === 50 && ["register", "pattern"].includes(name)); + !(recipe.ordinal === 52 && name === "epic") && + !(recipe.ordinal === 54 && name !== "detail") && + !(recipe.ordinal === 50 && ["register", "pattern"].includes(name)) && + !([53, 55].includes(recipe.ordinal) && name === "pack"); expect({ recipe: recipe.ordinal, fallback: recipe.body.includes(`params.${name} ?? `) || recipe.body.includes(`Object.hasOwn(params, "${name}") ? params.${name} : `) || - (recipe.ordinal === 51 && - name === "severity" && + ([51, 54].includes(recipe.ordinal) && + ["severity", "detail"].includes(name) && recipe.body.includes(`params.${name} === undefined ? `)), }).toEqual({ recipe: recipe.ordinal, fallback: hasSample }); expect({ @@ -5817,6 +5823,55 @@ describe("design recipes", () => { ]); }); + it("keeps the decoder in reader without adding crossings against the lawful notation alternative", async () => { + // spec:protocol.significant-units-review#rule.decoderBoundary + const decoder = "impl:protocol.example-space-decoding"; + expect( + derived.graph.edges.filter((edge) => edge.from === decoder && edge.type === "memberOf"), + ).toEqual([ + { from: decoder, to: "component:protocol.reader", type: "memberOf", claim: "anchored" }, + ]); + const fallback = { + ...derived, + graph: { + ...derived.graph, + edges: [ + ...derived.graph.edges + .filter( + (edge) => + !( + edge.type === "uses" && + edge.from === "component:protocol.codegen" && + edge.to === "component:protocol.reader" + ), + ) + .map((edge) => + edge.type === "memberOf" && edge.from === decoder + ? { ...edge, to: "component:protocol.notation" } + : edge, + ), + { + from: "component:protocol.notation", + to: "component:protocol.graph", + type: "uses" as const, + claim: "anchored" as const, + }, + { + from: "component:protocol.notation", + to: "component:protocol.validate", + type: "uses" as const, + claim: "anchored" as const, + }, + ], + }, + }; + // Both alternatives retain the real reader → notation slot-parser import. + const moved = asRecord(await runRecipe(recipeByOrdinal(32))); + const stayed = asRecord(await runRecipe(recipeByOrdinal(32), undefined, fallback)); + expect(asRecord(moved.totals).layerCrossings).toBe(asRecord(stayed.totals).layerCrossings); + expect(asRecord(moved.totals).contextCrossings).toBe(asRecord(stayed.totals).contextCrossings); + }); + 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); @@ -7491,3 +7546,875 @@ it.each(["rule", "flow"])( } }, ); + +function capabilitySpec( + name: string, + altitude: "epic" | "feature" | "story" = "feature", + kind: ProbeSpecKind = "behavior", + sections?: ProbeSections, +): Extract { + return { + id: `spec:probe.${name}`, + nodeType: "Primitive", + claim: "declared", + specKind: kind, + readiness: "idea", + altitude, + title: name, + file: `specs/${name}.sdp.md`, + sections: sections ?? { intent: { outcome: "Probe." } }, + }; +} + +function refinement(child: string, parent: string, reason?: string): GraphEdge { + return { + ...probeEdge(`spec:probe.${child}`, "refines", `spec:probe.${parent}`), + ...(reason === undefined ? {} : { reason }), + }; +} + +async function capabilityMap( + nodes: readonly GraphNode[], + edges: readonly GraphEdge[] = [], + params: Readonly> = { detail: true }, +): Promise> { + const extraction = syntheticExtraction(nodes, edges); + return asRecord( + await compileBody(recipeByOrdinal(52).body)( + createReader(extraction.graph), + extraction.graph, + extraction.report, + params, + ), + ); +} + +function mapRows(answer: Record, key: string): Record[] { + return asArray(answer[key]).map(asRecord); +} + +function mapRow(answer: Record, key: string, id: string): Record { + const row = mapRows(answer, key).find((row) => row.id === id); + if (row === undefined) throw new Error(`missing ${key} row ${id}`); + return row; +} + +const capabilityMapRecipeImplementationAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.capability-map-recipe"), + label: "asserts realization of The capability map in docs/agent-surface/recipes.md", + satisfies: ref("spec:consumers.capability-map"), + references: [ref("spec:decisions.epics-of-any-kind")], + role: "projection", +}); +void capabilityMapRecipeImplementationAnchor; +const capabilityMapRecipeTestAnchor = specTest({ + id: testAnchorId("test:protocol.capability-map"), + verifies: ref("spec:consumers.capability-map"), +}); +void capabilityMapRecipeTestAnchor; + +describe("the capability map recipe", () => { + const diamondNodes = [ + capabilitySpec("epic", "epic"), + capabilitySpec("left"), + capabilitySpec("right"), + capabilitySpec("child", "story"), + ]; + const diamondEdges = [ + refinement("left", "epic", "narrows the capability"), + refinement("right", "epic"), + refinement("child", "left"), + refinement("child", "right"), + ]; + + it("retains every diamond edge and intermediate node while counting each descendant once", async () => { + const answer = await capabilityMap(diamondNodes, diamondEdges); + expect(mapRows(answer, "nodes").map((row) => row.id)).toEqual([ + "spec:probe.child", + "spec:probe.epic", + "spec:probe.left", + "spec:probe.right", + ]); + expect(mapRow(answer, "nodes", "spec:probe.child").parents).toEqual([ + "spec:probe.left", + "spec:probe.right", + ]); + expect(answer.edges).toEqual([ + { from: "spec:probe.child", to: "spec:probe.left", claim: "declared", reason: null }, + { from: "spec:probe.child", to: "spec:probe.right", claim: "declared", reason: null }, + { + from: "spec:probe.left", + to: "spec:probe.epic", + claim: "declared", + reason: "narrows the capability", + }, + { from: "spec:probe.right", to: "spec:probe.epic", claim: "declared", reason: null }, + ]); + const epic = mapRow(answer, "epics", "spec:probe.epic"); + expect(epic.descendantIds).toEqual(["spec:probe.child", "spec:probe.left", "spec:probe.right"]); + expect(epic.rollup).toMatchObject({ specs: 3, byAltitude: { epic: 0, feature: 2, story: 1 } }); + expect(answer.totals).toMatchObject({ specs: 4, reached: 4, unreached: 0 }); + }); + + it.each([{}, { epic: "spec:probe.epic" }])( + "returns summary/full parity with explicit omissions for scope %j", + async (scope) => { + const nodes = [...diamondNodes, capabilitySpec("unreached")]; + const edges = [...diamondEdges, refinement("left", "missing"), refinement("child", "child")]; + const full = await capabilityMap(nodes, edges, { ...scope, detail: true }); + const summary = await capabilityMap(nodes, edges, scope); + expect(summary).toEqual(await capabilityMap(nodes, edges, { ...scope, detail: false })); + const { nodes: omittedNodes, edges: omittedEdges, epics, unreached, ...kept } = full; + expect(omittedNodes).toBeDefined(); + expect(omittedEdges).toBeDefined(); + expect(full).not.toHaveProperty("leftOut"); + expect(summary).toEqual({ + ...kept, + epics: asArray(epics).map((row) => { + const { descendantIds, ...summaryEpic } = asRecord(row); + expect(descendantIds).toBeDefined(); + return summaryEpic; + }), + unreached: { ids: asRecord(unreached).ids }, + leftOut: [ + "nodes: full Spec rows and memberships", + "edges: selected refinement edges and reasons", + "epics.descendantIds: distinct descendant ids", + "unreached.roots: roots of the unreached refinement subgraph", + ], + }); + }, + ); + + it("normalizes prose and unflagged questions while preserving authored indices", async () => { + const answer = await capabilityMap([ + capabilitySpec("questions", "feature", "behavior", { + intent: { + outcome: "Read lawful question forms.", + openQuestions: [ + "A plain string question?", + { question: "No flag?" }, + "", + { key: "keyed", question: "A keyed question?", blocking: true }, + ], + }, + }), + ]); + expect(mapRow(answer, "nodes", "spec:probe.questions").questions).toEqual([ + { index: 0, address: null, question: "A plain string question?", blocking: false }, + { index: 1, address: null, question: "No flag?", blocking: false }, + { + index: 3, + address: "spec:probe.questions#question.keyed", + question: "A keyed question?", + blocking: true, + }, + ]); + expect(answer.totals).toMatchObject({ openQuestions: 3, blockingQuestions: 1 }); + }); + + it("retains null titles in serialized node and epic rows when a title degrades", async () => { + const { title: omittedTitle, ...epic } = capabilitySpec("untitled", "epic"); + expect(omittedTitle).toBeDefined(); + const full = JSON.parse(JSON.stringify(await capabilityMap([epic]))) as Record; + const summary = JSON.parse(JSON.stringify(await capabilityMap([epic], [], {}))) as Record< + string, + unknown + >; + expect(mapRow(full, "nodes", epic.id)).toHaveProperty("title", null); + expect(mapRow(full, "epics", epic.id)).toHaveProperty("title", null); + expect(mapRow(summary, "epics", epic.id)).toHaveProperty("title", null); + }); + + it("reports a self-refining epic as a cycle without making it its own parent epic", async () => { + const answer = await capabilityMap( + [capabilitySpec("epic", "epic")], + [refinement("epic", "epic")], + ); + expect(mapRow(answer, "epics", "spec:probe.epic").parentEpicIds).toEqual([]); + expect(mapRow(answer, "nodes", "spec:probe.epic").parents).toEqual(["spec:probe.epic"]); + expect(answer.cycles).toEqual([{ members: ["spec:probe.epic"] }]); + }); + + it("uses the outside unreached parent as root rather than its child cycle group", async () => { + const answer = await capabilityMap( + [capabilitySpec("p"), capabilitySpec("a"), capabilitySpec("b")], + [refinement("a", "p"), refinement("a", "b"), refinement("b", "a")], + ); + expect(answer.unreached).toEqual({ + ids: ["spec:probe.a", "spec:probe.b", "spec:probe.p"], + roots: ["spec:probe.p"], + }); + expect(answer.cycles).toEqual([{ members: ["spec:probe.a", "spec:probe.b"] }]); + }); + + it.each([ + "behavior", + "workflow", + "example", + "rule", + "constraint", + "model", + "decision", + "contract", + ] as const)("selects an epic of kind %s without changing its kind", async (kind) => { + const answer = await capabilityMap([capabilitySpec("epic", "epic", kind)], [], { + epic: "spec:probe.epic", + detail: true, + }); + expect(answer.input).toEqual({ epic: "spec:probe.epic" }); + expect(mapRow(answer, "nodes", "spec:probe.epic")).toMatchObject({ + specKind: kind, + epicIds: ["spec:probe.epic"], + }); + expect(answer.totals).toMatchObject({ specs: 1, epics: 1, reached: 1 }); + expect(mapRow(answer, "epics", "spec:probe.epic").rollup).toMatchObject({ specs: 0 }); + }); + + it("reads node evidence and ordered questions directly, including examples and decisions as parents", async () => { + const example = capabilitySpec("example", "story", "example", { + intent: { + outcome: "Probe questions.", + openQuestions: [ + { key: "first", question: "What is missing?", blocking: true }, + { question: "What else?", blocking: false }, + ], + }, + }); + const nodes = [ + capabilitySpec("epic", "epic"), + example, + capabilitySpec("decision", "story", "decision"), + capabilitySpec("underExample"), + capabilitySpec("underDecision"), + { + id: "test:probe.bound", + nodeType: "Anchor" as const, + claim: "anchored" as const, + file: "test.ts", + line: 1, + }, + probeUnit("impl:probe.bound", 2), + ]; + const edges = [ + refinement("example", "epic"), + refinement("decision", "epic"), + refinement("underExample", "example"), + refinement("underDecision", "decision"), + probeEdge("test:probe.bound", "verifies", "spec:probe.example", "anchored"), + probeEdge("impl:probe.bound", "satisfies", "spec:probe.decision", "anchored"), + ]; + const answer = await capabilityMap(nodes, edges); + expect(mapRow(answer, "nodes", "spec:probe.example")).toMatchObject({ + statedReadiness: "idea", + floorReached: "idea", + deliveryFacts: ["has-verifier"], + children: ["spec:probe.underExample"], + questions: [ + { + index: 0, + address: "spec:probe.example#question.first", + question: "What is missing?", + blocking: true, + }, + { index: 1, address: null, question: "What else?", blocking: false }, + ], + }); + expect(mapRow(answer, "nodes", "spec:probe.epic").deliveryFacts).toEqual([]); + expect(mapRow(answer, "epics", "spec:probe.epic").rollup).toEqual({ + specs: 4, + byStatedReadiness: { idea: 4, scoped: 0, defined: 0, ready: 0 }, + byFloorReached: { none: 0, idea: 4, scoped: 0, defined: 0, ready: 0 }, + byKind: { + behavior: 2, + workflow: 0, + example: 1, + rule: 0, + constraint: 0, + model: 0, + decision: 1, + contract: 0, + }, + byAltitude: { epic: 0, feature: 2, story: 2 }, + withImplementationBinding: 1, + withVerifierBinding: 1, + openQuestions: 2, + blockingQuestions: 1, + }); + }); + + it("represents a missing floor as none and keeps the epic's evidence outside its rollup", async () => { + const answer = await capabilityMap( + [ + capabilitySpec("epic", "epic", "behavior", {}), + { ...capabilitySpec("empty", "story"), title: "" }, + probeUnit("impl:probe.epic", 1), + ], + [ + refinement("empty", "epic"), + probeEdge("impl:probe.epic", "satisfies", "spec:probe.epic", "anchored"), + ], + ); + expect(mapRow(answer, "nodes", "spec:probe.empty").floorReached).toBe("none"); + expect(answer.totals).toMatchObject({ + withImplementationBinding: 1, + byFloorReached: { none: 2 }, + }); + expect(mapRow(answer, "epics", "spec:probe.epic").rollup).toMatchObject({ + specs: 1, + withImplementationBinding: 0, + }); + }); + + it("counts stated rungs separately from reached floors without lending a bound example's facts to its epic", async () => { + const complete = { intent: { outcome: "Probe." }, behavior: { rules: ["A rule."] } }; + const nodes = [ + { ...capabilitySpec("epic", "epic", "behavior", complete), readiness: "ready" as const }, + { + ...capabilitySpec("decision", "story", "decision", { + intent: { outcome: "Settle the probe." }, + decision: { context: "A choice.", decision: "Choose the intended outcome." }, + }), + readiness: "ready" as const, + }, + { + ...capabilitySpec("example", "story", "example", { + intent: { outcome: "Exercise the probe." }, + behavior: { + examples: [{ given: ["A probe."], when: ["It runs."], then: ["It answers."] }], + }, + }), + readiness: "ready" as const, + }, + capabilitySpec("idea"), + { + ...capabilitySpec("scoped", "feature", "behavior", { + ...complete, + intent: { + outcome: "Probe.", + openQuestions: [{ question: "Which answer?", blocking: true }], + }, + }), + readiness: "scoped" as const, + }, + { + ...capabilitySpec("defined", "feature", "behavior", complete), + readiness: "defined" as const, + }, + { + id: "test:probe.example", + nodeType: "Anchor" as const, + claim: "anchored" as const, + file: "test.ts", + line: 1, + }, + ]; + const edges = ["decision", "example", "idea", "scoped", "defined"].map((id) => + refinement(id, "epic"), + ); + const answer = await capabilityMap(nodes, [ + ...edges, + probeEdge("spec:probe.epic", "dependsOn", "spec:probe.decision"), + probeEdge("spec:probe.defined", "dependsOn", "spec:probe.idea"), + probeEdge("test:probe.example", "verifies", "spec:probe.example", "anchored"), + ]); + expect(mapRow(answer, "nodes", "spec:probe.epic")).toMatchObject({ + floorReached: "ready", + deliveryFacts: [], + }); + expect(mapRow(answer, "nodes", "spec:probe.decision")).toMatchObject({ + statedReadiness: "ready", + floorReached: "ready", + }); + expect(mapRow(answer, "epics", "spec:probe.epic").rollup).toMatchObject({ + specs: 5, + byStatedReadiness: { idea: 1, scoped: 1, defined: 1, ready: 2 }, + byFloorReached: { none: 0, idea: 1, scoped: 1, defined: 1, ready: 2 }, + withImplementationBinding: 0, + withVerifierBinding: 1, + openQuestions: 1, + blockingQuestions: 1, + }); + expect(mapRow(answer, "nodes", "spec:probe.example").deliveryFacts).toEqual(["has-verifier"]); + }); + + it("keeps nested and overlapping memberships and outside parents when selecting one epic", async () => { + const nodes = [ + capabilitySpec("outer", "epic"), + capabilitySpec("nested", "epic", "model"), + capabilitySpec("other", "epic"), + capabilitySpec("child"), + capabilitySpec("outside"), + ]; + const edges = [ + refinement("nested", "outer"), + refinement("child", "nested"), + refinement("child", "other"), + refinement("child", "outside"), + ]; + const full = await capabilityMap(nodes, edges); + const selected = await capabilityMap(nodes, edges, { epic: "spec:probe.nested", detail: true }); + expect(mapRows(selected, "nodes").map((row) => row.id)).toEqual([ + "spec:probe.child", + "spec:probe.nested", + ]); + expect(mapRow(selected, "nodes", "spec:probe.child")).toEqual( + mapRow(full, "nodes", "spec:probe.child"), + ); + expect(mapRow(selected, "nodes", "spec:probe.child").epicIds).toEqual([ + "spec:probe.nested", + "spec:probe.other", + "spec:probe.outer", + ]); + expect(mapRow(selected, "epics", "spec:probe.nested")).toEqual( + mapRow(full, "epics", "spec:probe.nested"), + ); + expect(mapRow(selected, "epics", "spec:probe.nested").parentEpicIds).toEqual([ + "spec:probe.outer", + ]); + expect(mapRow(full, "epics", "spec:probe.outer").descendantIds).toEqual([ + "spec:probe.child", + "spec:probe.nested", + ]); + expect(selected.totals).toMatchObject({ specs: 2, epics: 1, reached: 2, unreached: 0 }); + expect(selected.unreached).toEqual({ ids: [], roots: [] }); + expect(full.unreached).toEqual({ ids: ["spec:probe.outside"], roots: ["spec:probe.outside"] }); + expect(mapRows(full, "edges")).toContainEqual({ + from: "spec:probe.child", + to: "spec:probe.outside", + claim: "declared", + reason: null, + }); + const outer = await capabilityMap(nodes, edges, { epic: "spec:probe.outer", detail: true }); + expect(mapRow(outer, "epics", "spec:probe.nested")).toEqual( + mapRow(full, "epics", "spec:probe.nested"), + ); + }); + + it("traverses only declared refinement and never infers ancestry from other graph structure", async () => { + const nodes = [ + capabilitySpec("epic", "epic"), + capabilitySpec("child"), + capabilitySpec("inferred"), + capabilitySpec("dependent"), + { + id: "pack:probe.map", + nodeType: "Pack" as const, + claim: "declared" as const, + file: "map.pack.sdp.md", + members: ["spec:probe.epic", "spec:probe.dependent"], + }, + ]; + const answer = await capabilityMap(nodes, [ + refinement("child", "epic"), + { ...refinement("inferred", "epic"), claim: "inferred" }, + probeEdge("spec:probe.dependent", "dependsOn", "spec:probe.epic"), + probeEdge("spec:probe.dependent", "belongsTo", "pack:probe.map"), + ]); + expect(answer.totals).toMatchObject({ specs: 4, reached: 2, unreached: 2 }); + expect(mapRows(answer, "edges")).toHaveLength(1); + expect(answer.unreached).toEqual({ + ids: ["spec:probe.dependent", "spec:probe.inferred"], + roots: ["spec:probe.dependent", "spec:probe.inferred"], + }); + }); + + it("reports cycles, self-links and unresolved or non-Spec endpoints separately from parameter failures", async () => { + const nodes = [ + capabilitySpec("a"), + capabilitySpec("b"), + capabilitySpec("c"), + capabilitySpec("self"), + probeUnit("impl:probe.non-spec", 1), + ]; + const edges = [ + refinement("a", "b"), + refinement("b", "a"), + refinement("c", "a"), + refinement("self", "self"), + refinement("c", "missing"), + refinement("missing", "b"), + probeEdge("spec:probe.a", "refines", "impl:probe.non-spec"), + probeEdge("impl:probe.non-spec", "refines", "spec:probe.b"), + ]; + const answer = await capabilityMap(nodes, edges); + expect(answer.totals).toMatchObject({ specs: 4, epics: 0, reached: 0, unreached: 4 }); + expect(answer.cycles).toEqual([ + { members: ["spec:probe.a", "spec:probe.b"] }, + { members: ["spec:probe.self"] }, + ]); + expect(answer.unreached).toEqual({ + ids: ["spec:probe.a", "spec:probe.b", "spec:probe.c", "spec:probe.self"], + roots: ["spec:probe.a", "spec:probe.self"], + }); + expect(answer.damage).toEqual([ + { code: "unresolved-refinement", from: "impl:probe.non-spec", to: "spec:probe.b" }, + { code: "unresolved-refinement", from: "spec:probe.a", to: "impl:probe.non-spec" }, + { code: "unresolved-refinement", from: "spec:probe.c", to: "spec:probe.missing" }, + { code: "unresolved-refinement", from: "spec:probe.missing", to: "spec:probe.b" }, + ]); + expect(answer).not.toHaveProperty("diagnostics"); + }); + + it("limits damage to edges touching a selected epic scope and terminates reached cycles", async () => { + const answer = await capabilityMap( + [capabilitySpec("epic", "epic"), capabilitySpec("child"), capabilitySpec("outside")], + [ + refinement("child", "epic"), + refinement("epic", "child"), + refinement("child", "missing"), + refinement("absent", "epic"), + refinement("outside", "otherMissing"), + ], + { epic: "spec:probe.epic", detail: true }, + ); + expect(answer.totals).toMatchObject({ specs: 2, reached: 2 }); + expect(answer.cycles).toEqual([{ members: ["spec:probe.child", "spec:probe.epic"] }]); + expect(mapRow(answer, "epics", "spec:probe.epic").descendantIds).toEqual(["spec:probe.child"]); + expect(answer.damage).toEqual([ + { code: "unresolved-refinement", from: "spec:probe.absent", to: "spec:probe.epic" }, + { code: "unresolved-refinement", from: "spec:probe.child", to: "spec:probe.missing" }, + ]); + }); + + it("keeps overlapping unreached root scopes unique and exposes an empty corpus", async () => { + const answer = await capabilityMap( + [capabilitySpec("a"), capabilitySpec("b"), capabilitySpec("shared")], + [refinement("shared", "a"), refinement("shared", "b")], + ); + expect(answer.unreached).toEqual({ + ids: ["spec:probe.a", "spec:probe.b", "spec:probe.shared"], + roots: ["spec:probe.a", "spec:probe.b"], + }); + const empty = await capabilityMap([]); + expect(empty).toMatchObject({ + input: { epic: null }, + nodes: [], + edges: [], + epics: [], + cycles: [], + damage: [], + unreached: { ids: [], roots: [] }, + totals: { specs: 0, epics: 0, reached: 0, unreached: 0 }, + }); + expect(asRecord(asRecord(empty.totals).byKind)).toEqual({ + behavior: 0, + workflow: 0, + example: 0, + rule: 0, + constraint: 0, + model: 0, + decision: 0, + contract: 0, + }); + }); + + it.each([ + { params: { epic: null }, problems: [["epic", "Expected a Spec id string"]] }, + { params: { epic: 1 }, problems: [["epic", "Expected a Spec id string"]] }, + { params: { epic: [] }, problems: [["epic", "Expected a Spec id string"]] }, + { + params: { epic: "spec:probe.absent" }, + problems: [["epic", "No resolving Spec for spec:probe.absent"]], + }, + { + params: { epic: "spec:probe.plain" }, + problems: [["epic", "spec:probe.plain is a feature Spec, not an epic"]], + }, + { + params: { epic: "impl:probe.non-spec" }, + problems: [["epic", "No resolving Spec for impl:probe.non-spec"]], + }, + { params: { detail: "true" }, problems: [["detail", "Expected a boolean"]] }, + { params: { detail: 1 }, problems: [["detail", "Expected a boolean"]] }, + { params: { detail: null }, problems: [["detail", "Expected a boolean"]] }, + { + params: { epic: 1, detail: "x" }, + problems: [ + ["detail", "Expected a boolean"], + ["epic", "Expected a Spec id string"], + ], + }, + ])("refuses invalid parameters without a map: $params", async ({ params, problems }) => { + const answer = await capabilityMap( + [capabilitySpec("plain"), probeUnit("impl:probe.non-spec", 1)], + [], + params, + ); + expect(answer).toEqual({ + diagnostics: problems.map(([parameter, problem]) => ({ parameter, problem })), + }); + }); + + it("checks unknown names before known values", async () => { + expect(await capabilityMap([], [], { unknown: true, epic: 1 })).toEqual({ + unknownParams: ["unknown"], + accepts: ["epic", "detail"], + }); + }); + + it("answers deterministically in code-unit id order even when graph arrays are reversed", async () => { + const nodes = [ + ...diamondNodes, + capabilitySpec("Zebra"), + capabilitySpec("apple"), + capabilitySpec("Banana"), + ]; + const edges = [...diamondEdges, refinement("apple", "Zebra"), refinement("Zebra", "apple")]; + const answer = await capabilityMap(nodes, edges); + expect(await capabilityMap([...nodes].reverse(), [...edges].reverse())).toEqual(answer); + expect(await capabilityMap(nodes, edges)).toEqual(answer); + expect(mapRows(answer, "nodes").map((row) => row.id)).toEqual( + nodes.map((node) => node.id).sort(), + ); + }); +}); + +const capabilityEpicsImplementationAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.capability-epics"), + label: "asserts realization of the capability epics in the shipped corpus's refinement", + satisfies: ref("spec:protocol.capability-epics"), +}); +void capabilityEpicsImplementationAnchor; +const capabilityEpicsTestAnchor = specTest({ + id: testAnchorId("test:protocol.capability-epics"), + verifies: ref("spec:protocol.capability-epics"), +}); +void capabilityEpicsTestAnchor; + +describe("the Protocol's capability epics", () => { + it("reaches every Spec from the intended epic owners while preserving kinds and overlapping refinement", async () => { + const answer = asRecord(await runRecipe(recipeByOrdinal(52), { detail: true })); + const epics = mapRows(answer, "epics"); + expect(epics.map((row) => row.id)).toEqual([ + "spec:carrier.markdown-authoring", + "spec:consumers.agent-surface", + "spec:consumers.authoring-on-ramp", + "spec:consumers.projections-model", + "spec:extraction.derive-graph", + "spec:extraction.executable-examples", + "spec:model.core-model", + "spec:protocol.self-hosting", + "spec:validation.two-check-families", + ]); + expect(answer.unreached).toEqual({ ids: [], roots: [] }); + expect(answer.totals).toMatchObject({ + specs: reader.specs().length, + reached: reader.specs().length, + unreached: 0, + }); + expect(mapRow(answer, "nodes", "spec:model.core-model").specKind).toBe("model"); + expect(mapRow(answer, "nodes", "spec:consumers.projections-model").specKind).toBe("model"); + expect(mapRow(answer, "nodes", "spec:validation.two-check-families").specKind).toBe("rule"); + for (const epic of epics) { + expect(mapRow(answer, "nodes", String(epic.id)).statedReadiness, String(epic.id)).toBe( + "ready", + ); + } + expect(mapRow(answer, "epics", "spec:consumers.agent-surface").parentEpicIds).toEqual([ + "spec:consumers.projections-model", + ]); + expect(mapRow(answer, "nodes", "spec:extraction.executable-contracts").parents).toEqual([ + "spec:extraction.build-pipeline", + "spec:extraction.executable-examples", + ]); + for (const id of ["spec:extraction.example-runner", "spec:extraction.executable-contracts"]) { + expect(mapRow(answer, "nodes", id).epicIds).toContain("spec:extraction.executable-examples"); + } + for (const id of [ + "spec:protocol.capability-epics", + "spec:protocol.structural-self-binding", + "spec:protocol.roadmap-replaces-plans", + ]) { + expect(mapRow(answer, "nodes", id).epicIds).toContain("spec:protocol.self-hosting"); + } + for (const id of [ + "spec:model.core-model", + "spec:extraction.derive-graph", + "spec:validation.two-check-families", + "spec:consumers.projections-model", + ]) { + expect(mapRow(answer, "nodes", id).epicIds).not.toContain("spec:protocol.self-hosting"); + } + expect(answer.cycles).toEqual([]); + expect(answer.damage).toEqual([]); + }); + + it.each([ + ["spec:extraction.build-pipeline", "spec:extraction.derive-graph"], + ["spec:validation.corpus-rules", "spec:consumers.agent-surface"], + ["spec:validation.architecture-constraints", "spec:validation.corpus-rules"], + ["spec:validation.external-registers", "spec:validation.corpus-rules"], + ])("keeps refinement and dependency from %s to %s", (from, to) => { + for (const type of ["refines", "dependsOn"]) { + expect(derived.graph.edges).toContainEqual( + expect.objectContaining({ from, to, type, claim: "declared" }), + ); + } + }); + + it("keeps every ready statement at the ready floor and leaves the drift alarm empty", async () => { + const ready = reader.specs().filter((spec) => spec.statedReadiness === "ready"); + expect(ready.length).toBeGreaterThan(0); + expect( + ready.filter((spec) => spec.derivedReadiness !== "ready").map((spec) => spec.id), + ).toEqual([]); + expect(await runRecipe(recipeByOrdinal(2))).toMatchObject({ alarms: [] }); + }); +}); + +describe("the shipped design slice", () => { + it("runs the catalog body over the arc Pack through the CLI and keeps its summary totals", async () => { + const recipe = recipeByOrdinal(54); + const full = asRecord( + await runRecipe(recipe, { pack: "pack:roadmaps-and-slices-v1", detail: true }), + ); + const summary = asRecord(await runRecipe(recipe, { pack: "pack:roadmaps-and-slices-v1" })); + expect(summary.totals).toEqual(full.totals); + expect(asArray(full.members).length).toBeGreaterThan(0); + expect(asArray(summary.leftOut).length).toBeGreaterThan(0); + expect(full).not.toHaveProperty("diagnostics"); + const pack = asRecord( + await runRecipe(recipeByOrdinal(29), { pack: "pack:roadmaps-and-slices-v1", detail: true }), + ); + for (const [direction, rows] of Object.entries(asRecord(pack.boundary))) { + const boundary = asRecord(full.boundary); + for (const row of asArray(boundary[direction])) { + const boundaryRow = asRecord(row); + for (const entry of asArray(boundaryRow.via)) { + const via = asRecord(entry); + const reasons = asArray(via.members).flatMap((member) => + derived.graph.edges + .filter( + (edge) => + edge.claim === "declared" && + edge.type === via.type && + edge.reason !== undefined && + (direction === "restsOn" + ? edge.from === member && edge.to === boundaryRow.id + : edge.to === member && edge.from === boundaryRow.id), + ) + .map((edge) => ({ member, reason: edge.reason })), + ); + expect(via.reasons ?? []).toEqual(reasons); + } + } + expect( + asArray(boundary[direction]).map((row) => { + const { id, title, statedReadiness, implemented, via } = asRecord(row); + return { + id, + title, + statedReadiness, + implemented, + via: asArray(via).map((entry) => { + const { type, members } = asRecord(entry); + return { type, members }; + }), + }; + }), + ).toEqual(rows); + } + }); +}); + +it("runs the roadmap and its memo from the catalog over a named Pack", async () => { + const answer = asRecord(await runRecipe(recipeByOrdinal(53), { pack: "pack:roadmap" })); + const roadmaps = asArray(answer.roadmaps); + expect(roadmaps).toHaveLength(1); + const roadmap = asRecord(roadmaps[0]); + expect(roadmap.pack).toBe("pack:roadmap"); + expect(asArray(roadmap.entries).length).toBeGreaterThan(0); + const rendered = asRecord(await runRecipe(recipeByOrdinal(55), { pack: "pack:roadmap" })); + expect(rendered.diagnostics).toEqual([]); + expect(stringAt(rendered, "memo")).toContain("## The next work, from the roadmap\n"); +}); + +it("pins the adopter roadmap and its forward memo over the worked example", async () => { + const example = extract({ root: join(repoRoot, "examples/checkout-v1") }); + expect(example.report.findings).toEqual([]); + const params = { pack: "pack:checkout-roadmap" }; + const answer = asRecord(await runRecipe(recipeByOrdinal(53), params, example)); + const roadmaps = asArray(answer.roadmaps); + expect(roadmaps).toHaveLength(1); + const roadmap = asRecord(roadmaps[0]); + expect(roadmap.order).toBe("priority"); + expect(roadmap.totals).toEqual({ + entries: 2, + toDesign: 1, + toState: 1, + toBuild: 0, + built: 0, + apart: 0, + unresolved: 0, + questions: 0, + blockingQuestions: 0, + withoutDecider: 0, + blockingWithoutDecider: 0, + statements: 1, + }); + expect( + asArray(roadmap.entries).map((entry) => { + const row = asRecord(entry); + return { + id: row.id, + need: row.need, + statedReadiness: row.statedReadiness, + floorReached: row.floorReached, + holdsNextRung: row.holdsNextRung, + }; + }), + ).toEqual([ + { + id: "spec:orders.order-placement-flow", + need: "to design", + statedReadiness: "scoped", + floorReached: "scoped", + holdsNextRung: [{ clause: "kind-evidence-complete", targets: [] }], + }, + { + id: "spec:orders.order-model", + need: "to state", + statedReadiness: "defined", + floorReached: "ready", + holdsNextRung: [], + }, + ]); + // Transcribed from the memo's head, design, state, build and waiting contracts. + // spec:consumers.roadmap.memo#design.memoHead + // spec:consumers.roadmap.memo#design.memoDesign + // spec:consumers.roadmap.memo#design.memoState + // spec:consumers.roadmap.memo#design.memoBuild + // spec:consumers.roadmap.memo#design.memoWaiting + expect(await runRecipe(recipeByOrdinal(55), params, example)).toEqual({ + diagnostics: [], + memo: `## The next work, from the roadmap + +Roadmap: Checkout roadmap \`pack:checkout-roadmap\` + +To state means the floor allows ready and a person still judges the statement. Built means every Spec of the entry's work states ready and has an implementation binding; it reports no passing test and no deployment. + +Clarify how a customer reaches order creation, then review the shared order vocabulary. The order is advisory priority; needs come from the graph and confer no permission. + +### To design + +| Place | Entry | Next | Purpose and reach | +| --- | --- | --- | --- | +| 1 | Order placement flow \`spec:orders.order-placement-flow\` | itself \`scoped\` → \`defined\`, held by kind-evidence-complete | Sequence cart submission through validation to a created order. | + +### To state + +| Place | Entry | Earlier waiting statements | Purpose and reach | +| --- | --- | --- | --- | +| 2 | Order-management domain vocabulary \`spec:orders.order-model\` | 1 outside the roadmap | Define the core terms used by the checkout-v1 order-management slice. | + +### To build + +No entry is ready to build. + +### Waiting on the owner + +- State \`spec:orders.order-model\`: it states \`defined\` and its floor allows \`ready\`; place 2 +`, + }); +}); diff --git a/test/roadmap-examples.test.ts b/test/roadmap-examples.test.ts new file mode 100644 index 000000000..6a561c3ba --- /dev/null +++ b/test/roadmap-examples.test.ts @@ -0,0 +1,228 @@ +import { expect, it } from "vitest"; +import { + oracleAnchorId, + ref, + specOracle, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { unspecified } from "@libar-dev/software-delivery-protocol/runner"; +import type { + RoadmapConditions, + RoadmapOutcome, +} from "../generated/contracts/consumers.roadmap.space.js"; +import type { GraphSchema } from "../src/index.js"; +import { probeGraph, probePack, probeSpec, roadmap, workGraph } from "./helpers/roadmap-probe.js"; +import type { Roadmap } from "./helpers/roadmap-probe.js"; +import { registerBuiltWork } from "./consumers.roadmap.built-work.test.generated.js"; +import { registerBuildablePart } from "./consumers.roadmap.buildable-part.test.generated.js"; +import { registerStatementPart } from "./consumers.roadmap.statement-part.test.generated.js"; +import { registerDesignPart } from "./consumers.roadmap.design-part.test.generated.js"; +import { registerDriftingPart } from "./consumers.roadmap.drifting-part.test.generated.js"; +import { registerMissingEvidence } from "./consumers.roadmap.missing-evidence.test.generated.js"; +import { registerBlockingPart } from "./consumers.roadmap.blocking-part.test.generated.js"; +import { registerOutsideParent } from "./consumers.roadmap.outside-parent.test.generated.js"; +import { registerStatementOrder } from "./consumers.roadmap.statement-order.test.generated.js"; + +const oracleAnchor = specOracle({ + id: oracleAnchorId("oracle:protocol.roadmap"), + models: ref("spec:consumers.roadmap"), +}); +void oracleAnchor; + +const NEED = "the entry's need reads {need}"; +const DRIFT = "the entry counts {drift} parts as drift"; +const PART = "the entry names {part} among the parts with its need, held by {clause}"; +const OUTSIDE = "the entry rests on {target} through {origin}"; +const ORDER = "the waiting statements list {earlier} before {later}"; + +function isStatementOrder(point: Partial): boolean { + return ( + point.work === "spec:probe.dependent" && + point.state === "has one part below ready whose floor reaches ready" + ); +} + +function expected(point: Partial): RoadmapOutcome { + if (isStatementOrder(point)) + return { kind: ORDER, earlier: "spec:probe.base", later: "spec:probe.dependent" }; + if (point.work === "spec:probe.dependent") return unspecified; + switch (point.state) { + case "is ready and implemented throughout": + return { kind: NEED, need: "built" }; + case "has one part ready and not implemented": + return { kind: NEED, need: "to build" }; + case "has one part below ready whose floor reaches ready": + return { kind: NEED, need: "to state" }; + case "has one part below ready whose floor stops short": + return { kind: NEED, need: "to design" }; + case "has one part implemented below ready": + return { kind: DRIFT, drift: 1 }; + case "has a ready root and one descendant without its kind's evidence": + return { kind: PART, part: "spec:probe.part", clause: "kind-evidence-present" }; + case "has a ready root and one descendant held by a blocking question": + return { kind: PART, part: "spec:probe.part", clause: "no-blocking-open-questions" }; + case "has one descendant that also refines a Spec outside the roadmap": + return { kind: OUTSIDE, target: "spec:probe.outside", origin: "spec:probe.part" }; + + default: + return unspecified; + } +} + +interface World { + point: Partial; + graph: GraphSchema; + answer?: Roadmap; +} +function createWorld(point: Partial): World { + if (point.work === "spec:probe.dependent" && !isStatementOrder(point)) { + throw new Error("The statement-order work requires its waiting state."); + } + return { + point, + graph: isStatementOrder(point) + ? probeGraph({ + "pack.sdp.md": probePack("probe", ["dependent", "base"]), + "dependent.sdp.md": probeSpec("dependent", "defined", "\n dependsOn: spec:probe.base"), + "base.sdp.md": probeSpec("base", "defined"), + }) + : workGraph(point.state ?? ""), + }; +} +async function invoke(world: World): Promise { + world.answer = (await roadmap(world.graph)).roadmaps[0]; +} +function observe(world: World): RoadmapOutcome { + const entry = world.answer?.entries[0]; + if (entry === undefined) return unspecified; + if (isStatementOrder(world.point)) { + const statements = world.answer?.waiting.statements ?? []; + return { kind: ORDER, earlier: statements[0]?.id ?? "", later: statements[1]?.id ?? "" }; + } + switch (world.point.state) { + case "has one part implemented below ready": + return { kind: DRIFT, drift: entry.work.drift ?? -1 }; + case "has a ready root and one descendant without its kind's evidence": + case "has a ready root and one descendant held by a blocking question": { + const part = entry.workRows.find((row) => row.need === entry.need); + return { kind: PART, part: part?.id ?? "", clause: part?.holdsNextRung[0]?.clause ?? "" }; + } + case "has one descendant that also refines a Spec outside the roadmap": { + const row = entry.restsOn[0]; + return { kind: OUTSIDE, target: row?.id ?? "", origin: row?.from.join(", ") ?? "" }; + } + case undefined: { + const statements = world.answer?.waiting.statements ?? []; + return { kind: ORDER, earlier: statements[0]?.id ?? "", later: statements[1]?.id ?? "" }; + } + default: { + if ( + entry.need !== "to design" && + entry.need !== "to state" && + entry.need !== "to build" && + entry.need !== "built" + ) + return unspecified; + return { kind: NEED, need: entry.need }; + } + } +} +function assertions(world: World): void { + expect(world.answer?.pack).toBe("pack:probe"); + expect(world.answer?.order).toBe("priority"); + expect(world.answer?.entries[0]?.workRows.map((row) => row.id)).toEqual( + world.point.work?.split(", ").sort(), + ); + if (isStatementOrder(world.point)) { + expect(world.answer?.entries[0]?.earlierStatements).toEqual([ + { id: "spec:probe.base", where: "roadmap", onRoadmap: 2 }, + ]); + } else { + expect(world.answer?.entries[0]?.statedReadiness).toBe("ready"); + expect(world.answer?.entries[0]?.workRows).toHaveLength(2); + } +} + +it("rejects a statement-order point with a different bound state", () => { + const point = { + work: "spec:probe.dependent", + state: "is ready and implemented throughout", + } as const; + expect(expected(point)).toEqual(unspecified); + expect(() => createWorld(point)).toThrow("The statement-order work requires its waiting state."); +}); + +it("checks the bound work against the observed scope", async () => { + const world = createWorld({ + work: "spec:probe.wrong", + state: "is ready and implemented throughout", + }); + await invoke(world); + expect(() => { + assertions(world); + }).toThrow(); +}); + +const builtWorkAnchor = specTest({ + id: testAnchorId("test:protocol.roadmap.built-work"), + verifies: ref("spec:consumers.roadmap.built-work"), +}); +void builtWorkAnchor; +registerBuiltWork({ createWorld, invoke, observe, expected, assertions }); + +const buildablePartAnchor = specTest({ + id: testAnchorId("test:protocol.roadmap.buildable-part"), + verifies: ref("spec:consumers.roadmap.buildable-part"), +}); +void buildablePartAnchor; +registerBuildablePart({ createWorld, invoke, observe, expected, assertions }); + +const statementPartAnchor = specTest({ + id: testAnchorId("test:protocol.roadmap.statement-part"), + verifies: ref("spec:consumers.roadmap.statement-part"), +}); +void statementPartAnchor; +registerStatementPart({ createWorld, invoke, observe, expected, assertions }); + +const designPartAnchor = specTest({ + id: testAnchorId("test:protocol.roadmap.design-part"), + verifies: ref("spec:consumers.roadmap.design-part"), +}); +void designPartAnchor; +registerDesignPart({ createWorld, invoke, observe, expected, assertions }); + +const driftingPartAnchor = specTest({ + id: testAnchorId("test:protocol.roadmap.drifting-part"), + verifies: ref("spec:consumers.roadmap.drifting-part"), +}); +void driftingPartAnchor; +registerDriftingPart({ createWorld, invoke, observe, expected, assertions }); + +const missingEvidenceAnchor = specTest({ + id: testAnchorId("test:protocol.roadmap.missing-evidence"), + verifies: ref("spec:consumers.roadmap.missing-evidence"), +}); +void missingEvidenceAnchor; +registerMissingEvidence({ createWorld, invoke, observe, expected, assertions }); + +const blockingPartAnchor = specTest({ + id: testAnchorId("test:protocol.roadmap.blocking-part"), + verifies: ref("spec:consumers.roadmap.blocking-part"), +}); +void blockingPartAnchor; +registerBlockingPart({ createWorld, invoke, observe, expected, assertions }); + +const outsideParentAnchor = specTest({ + id: testAnchorId("test:protocol.roadmap.outside-parent"), + verifies: ref("spec:consumers.roadmap.outside-parent"), +}); +void outsideParentAnchor; +registerOutsideParent({ createWorld, invoke, observe, expected, assertions }); + +const statementOrderAnchor = specTest({ + id: testAnchorId("test:protocol.roadmap.statement-order"), + verifies: ref("spec:consumers.roadmap.statement-order"), +}); +void statementOrderAnchor; +registerStatementOrder({ createWorld, invoke, observe, expected, assertions }); diff --git a/test/roadmap-priority.test.ts b/test/roadmap-priority.test.ts new file mode 100644 index 000000000..4973bad74 --- /dev/null +++ b/test/roadmap-priority.test.ts @@ -0,0 +1,424 @@ +import { mkdtempSync, readFileSync, 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 { createReader, evaluateBody, extract, serializeGraph } from "../src/index.js"; +import type { GraphSchema } from "../src/index.js"; +import { createStudioData } from "../src/projections/studio/data.js"; +import type { Diff } from "../src/projections/studio/pages/register-answers.js"; +import { renderStudio } from "./helpers/studio-render.js"; + +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); +const markdown = (order = "") => + `---\nid: pack:probe.review\nspecs: []\nmodelRefs: []\n${order}---\n# Review\n\nRead the next work.\n`; +const typescript = (order = "") => + `import { pack, packId } from "@libar-dev/software-delivery-protocol";\nexport const review = pack({ id: packId("pack:probe.review"), specs: [], modelRefs: [], title: "Review", framing: "Read the next work.", ${order} });\n`; +function corpus(source: string, extension = "md"): string { + const root = mkdtempSync(join(tmpdir(), "sdp-priority-")); + roots.push(root); + writeFileSync(join(root, `review.pack.sdp.${extension}`), source); + return root; +} +function graphOf(priority: boolean): GraphSchema { + return extract({ root: corpus(markdown(priority ? "order: priority\n" : "")) }).graph; +} +async function diff(base: unknown, graph: GraphSchema, pack?: string): Promise { + // Execute the catalog body itself; the build also ships these exact bytes. + const catalog = readFileSync( + new URL("../docs/agent-surface/recipes.md", import.meta.url), + "utf8", + ); + const section = catalog.split(/^## 35\. .+$/mu)[1]?.split(/^## /mu)[0]; + const body = /```js\n([\s\S]*?)\n```/u.exec(section ?? "")?.[1]; + if (body === undefined) throw new Error("Missing design diff recipe"); + return (await evaluateBody(body, { + graph, + params: { base, detail: true, ...(pack === undefined ? {} : { pack }) }, + })) as Diff; +} + +const priorityAnchor = specTest({ + id: testAnchorId("test:protocol.roadmap-priority"), + label: "checks Pack priority extraction, comparison and Studio change notes", + verifies: [ + ref("spec:carrier.markdown-pack-authoring"), + ref("spec:model.pack-aggregate"), + ref("spec:extraction.pack-member-order"), + ref("spec:consumers.design-diff"), + ref("spec:consumers.spec-studio.diff-page"), + ref("spec:consumers.spec-studio.pack-page"), + ], +}); +void priorityAnchor; + +describe("roadmap priority manifest", () => { + it("derives equal Markdown and TypeScript Pack nodes and edges, serializes order last, and exposes it through the reader", () => { + const mdRoot = corpus( + markdown("order: priority\n").replace("specs: []", "specs: [spec:probe.member]"), + ); + const tsRoot = corpus( + typescript('order: "priority"').replace("specs: []", 'specs: ["spec:probe.member"]'), + "ts", + ); + for (const root of [mdRoot, tsRoot]) + writeFileSync( + join(root, "member.sdp.md"), + "---\nid: spec:probe.member\nkind: behavior\naltitude: story\nreadiness: idea\nrelations: {}\n---\n# Member\n\n## Intent\n- outcome: Read this member.\n", + ); + const md = extract({ root: mdRoot }); + const ts = extract({ root: tsRoot }); + expect(md.report.findings).toEqual([]); + expect(ts.report.findings).toEqual([]); + const node = md.graph.nodes.find((node) => node.nodeType === "Pack"); + expect(node).toMatchObject({ nodeType: "Pack", order: "priority" }); + expect(ts.graph.nodes.find((node) => node.nodeType === "Pack")).toEqual({ + ...node, + file: "review.pack.sdp.ts", + }); + expect(ts.graph.edges).toEqual(md.graph.edges); + expect(md.graph.edges).toContainEqual({ + from: "spec:probe.member", + type: "belongsTo", + to: "pack:probe.review", + claim: "declared", + }); + const serialized = JSON.parse(serializeGraph(md.graph)) as { nodes: Record[] }; + expect(Object.keys(serialized.nodes[0] ?? {})).toEqual([ + "id", + "nodeType", + "claim", + "title", + "framing", + "file", + "members", + "modelRefs", + "order", + ]); + const reader = createReader(md.graph); + expect(reader.packs()[0]).toHaveProperty("order", "priority"); + expect(reader.packContext("pack:probe.review")).toHaveProperty("order", "priority"); + expect(createReader(graphOf(false)).packs()[0]).not.toHaveProperty("order"); + expect(serializeGraph(graphOf(false))).not.toContain('"order"'); + }); + + it.each(["reading", "arbitrary", "true", "null", "[]", "{}"])( + "refuses the invalid Markdown value %s", + (value) => { + const result = extract({ root: corpus(markdown(`order: ${value}\n`)) }); + expect(result.graph.nodes).toEqual([]); + expect(result.report.findings).toEqual([ + expect.objectContaining({ + validatorId: "extract/invalid-frontmatter", + severity: "error", + file: "review.pack.sdp.md", + line: 5, + }), + ]); + }, + ); + + it.each(['"reading"', '"arbitrary"', "true", "null", "[]", "{}", "undefined", "chosenOrder"])( + "refuses the invalid TypeScript value %s", + (value) => { + const result = extract({ root: corpus(typescript(`order: ${value}`), "ts") }); + expect(result.graph.nodes).toEqual([]); + expect(result.report.findings).toEqual([ + expect.objectContaining({ + validatorId: "extract/invalid-frontmatter", + severity: "error", + file: "review.pack.sdp.ts", + line: 2, + }), + ]); + }, + ); +}); + +describe("roadmap priority design diff", () => { + it.each([true, false])( + "names a removed Pack by its order meaning (priority %s)", + async (priority) => { + const base = graphOf(priority); + const head = { ...base, nodes: [], edges: [] }; + const data = await createStudioData(createReader(head), undefined, { + base: { graph: base, label: null }, + runs: [], + }); + const html = renderStudio(data).find((page) => page.path === "diff.html")?.content ?? ""; + expect(html).toContain("removed ·"); + if (priority) expect(html).toContain("removed as a roadmap"); + else expect(html).not.toContain("removed as a roadmap"); + }, + ); + + it("names an added roadmap and preserves manifest order in its member delta", async () => { + const source = markdown("order: priority\n").replace( + "specs: []", + "specs: [spec:probe.z, spec:probe.a]", + ); + const head = extract({ root: corpus(source) }).graph; + const base = { ...head, nodes: [], edges: [] }; + expect((await diff(base, head)).packs?.[0]?.membersAdded).toEqual([ + "spec:probe.z", + "spec:probe.a", + ]); + const data = await createStudioData(createReader(head), undefined, { + base: { graph: base, label: null }, + runs: [], + }); + for (const path of ["diff.html", "packs/probe.review.html"]) { + const html = renderStudio(data).find((page) => page.path === path)?.content ?? ""; + expect(html).toContain("added as a roadmap"); + const added = html.split("Members added:")[1]?.split("

    ")[0] ?? ""; + expect(added.indexOf("spec:probe.z")).toBeLessThan(added.indexOf("spec:probe.a")); + } + }); + + it.each([true, false])( + "labels Pack membership by its order meaning (priority %s)", + async (priority) => { + const data = await createStudioData(createReader(graphOf(priority))); + const html = renderStudio(data).find( + (page) => page.path === "packs/probe.review.html", + )?.content; + expect(html).toContain(priority ? "in priority order" : "in authored reading order"); + }, + ); + + it("states order values when a change is not a known roadmap transition", async () => { + const head = graphOf(false); + const data = await createStudioData(createReader(head), undefined, { + base: { graph: head, label: null }, + runs: [], + }); + const delta: NonNullable[number] = { + id: "pack:probe.review", + change: "changed", + membersAdded: [], + membersRemoved: [], + reordered: false, + orderChanged: false, + orderBefore: null, + orderAfter: "reading", + }; + const all = new Map(data.recipes.all).set(35, { diagnostics: [], packs: [delta] }); + const html = renderStudio({ ...data, recipes: { ...data.recipes, all } }).find( + (page) => page.path === "diff.html", + )?.content; + expect(html).not.toContain("no longer a roadmap"); + expect(html).toContain("Order meaning: unknown → reading"); + }); + it("documents the order comparison fields, default and diagnostic in the recipe contract", () => { + const catalog = readFileSync( + new URL("../docs/agent-surface/recipes.md", import.meta.url), + "utf8", + ); + const contract = catalog.split("`packs` reports")[1]?.split("`bindings`")[0]; + for (const term of [ + "`orderChanged`", + "`orderBefore`", + "`orderAfter`", + '`"reading"`', + '{ part: "order", id, side, value, problem }', + ]) { + expect(contract).toContain(term); + } + }); + + it.each([true, false])( + "compares a priority transition to %s without a membership edit", + async (priority) => { + const result = await diff(graphOf(!priority), graphOf(priority)); + expect(result.packs).toEqual([ + { + id: "pack:probe.review", + change: "changed", + membersAdded: [], + membersRemoved: [], + reordered: false, + orderChanged: true, + orderBefore: priority ? "reading" : "priority", + orderAfter: priority ? "priority" : "reading", + }, + ]); + expect(result.totals?.packs).toBe(1); + }, + ); + + it("reads an absent order as reading order in a base schema that predates the field", async () => { + const result = await diff({ ...graphOf(false), schemaVersion: "0.11.0" }, graphOf(true)); + expect(result.packs?.[0]).toMatchObject({ + orderChanged: true, + orderBefore: "reading", + orderAfter: "priority", + }); + expect(result.diagnostics).toEqual([{ part: "schemaVersion", base: "0.11.0", head: "0.12.0" }]); + expect((await diff(graphOf(false), graphOf(false))).packs).toEqual([]); + expect((await diff(graphOf(true), graphOf(true))).packs).toEqual([]); + }); + + it.each([true, false])( + "marks the absent side of an added or removed roadmap as null (%s)", + async (added) => { + const empty = { ...graphOf(false), nodes: [] }; + const priority = graphOf(true); + expect((await diff(added ? empty : priority, added ? priority : empty)).packs).toEqual([ + { + id: "pack:probe.review", + change: added ? "added" : "removed", + membersAdded: [], + membersRemoved: [], + reordered: false, + orderChanged: false, + orderBefore: added ? null : "priority", + orderAfter: added ? "priority" : null, + }, + ]); + }, + ); + + it.each(["base", "head"])( + "diagnoses an unknown order at the %s without reporting an order change", + async (side) => { + const known = graphOf(false); + const unknown = { + ...known, + nodes: known.nodes.map((node) => ({ ...node, order: "schedule" })), + }; + const result = await diff( + side === "base" ? unknown : known, + (side === "head" ? unknown : known) as GraphSchema, + ); + expect(result.packs).toEqual([]); + expect(result.diagnostics).toEqual([ + { + part: "order", + id: "pack:probe.review", + side, + value: "schedule", + problem: "Part is outside the common schema.", + }, + ]); + }, + ); + + it.each(["base", "head"])( + "still compares membership and member order when the %s order is unknown", + async (side) => { + const original = graphOf(true); + const withMembers = (members: string[], unknown: boolean) => ({ + ...original, + nodes: original.nodes.map((node) => ({ + ...node, + members, + ...(unknown ? { order: "schedule" } : {}), + })), + }); + const result = await diff( + withMembers(["spec:probe.a", "spec:probe.c"], side === "base"), + withMembers( + ["spec:probe.c", "spec:probe.a", "spec:probe.b"], + side === "head", + ) as GraphSchema, + ); + expect(result.packs).toEqual([ + { + id: "pack:probe.review", + change: "changed", + membersAdded: ["spec:probe.b"], + membersRemoved: [], + reordered: true, + orderChanged: false, + orderBefore: side === "base" ? null : "priority", + orderAfter: side === "head" ? null : "priority", + }, + ]); + expect(result.totals?.packs).toBe(1); + expect(result.diagnostics).toEqual([ + { + part: "order", + id: "pack:probe.review", + side, + value: "schedule", + problem: "Part is outside the common schema.", + }, + ]); + }, + ); + + it("keeps an order-only change in the selected Pack's comparison scope", async () => { + const withOther = (graph: GraphSchema): GraphSchema => { + const pack = graph.nodes.find((node) => node.nodeType === "Pack"); + if (pack === undefined) throw new Error("Missing Pack"); + return { ...graph, nodes: [...graph.nodes, { ...pack, id: "pack:probe.other" }] }; + }; + const base = withOther(graphOf(false)), + head = withOther(graphOf(true)); + const result = await diff(base, head, "pack:probe.review"); + expect(result.packs).toEqual([ + { + id: "pack:probe.review", + change: "changed", + membersAdded: [], + membersRemoved: [], + reordered: false, + orderChanged: true, + orderBefore: "reading", + orderAfter: "priority", + }, + ]); + expect(result.totals?.packs).toBe(1); + expect(result.diagnostics).toEqual([]); + }); + + it.each([true, false])( + "shows no roadmap transition for a membership-only change (priority %s)", + async (priority) => { + const base = graphOf(priority); + const head = { + ...base, + nodes: base.nodes.map((node) => + node.nodeType === "Pack" + ? { ...node, members: [...node.members, "spec:probe.added"] } + : node, + ), + }; + const data = await createStudioData(createReader(head), undefined, { + base: { graph: base, label: "Earlier design" }, + runs: [], + }); + const pages = renderStudio(data); + for (const path of ["diff.html", "packs/probe.review.html"]) { + const content = pages.find((page) => page.path === path)?.content; + expect(content).toContain("spec:probe.added"); + expect(content).not.toContain("now a roadmap"); + expect(content).not.toContain("no longer a roadmap"); + } + }, + ); + + it.each([true, false])( + "shows the roadmap transition to %s on the diff and Pack pages", + async (priority) => { + const data = await createStudioData(createReader(graphOf(priority)), undefined, { + base: { graph: graphOf(!priority), label: "Earlier design" }, + runs: [], + }); + const pages = renderStudio(data); + const diffPage = pages.find((page) => page.path === "diff.html")?.content; + expect(diffPage).toContain("1 Pack with membership, member order or order meaning changes."); + expect(diffPage).toContain( + "

    Packs with membership, member order or order meaning changes

    ", + ); + for (const path of ["diff.html", "packs/probe.review.html"]) { + const content = pages.find((page) => page.path === path)?.content; + expect(content).toContain("Member order unchanged."); + expect(content).toContain(priority ? "now a roadmap" : "no longer a roadmap"); + } + }, + ); +}); diff --git a/test/roadmap.test.ts b/test/roadmap.test.ts new file mode 100644 index 000000000..49c76bcd6 --- /dev/null +++ b/test/roadmap.test.ts @@ -0,0 +1,680 @@ +import { describe, expect, it } from "vitest"; +import { + codeAnchor, + codeAnchorId, + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { + implementation, + memo, + probeGraph, + probePack, + probeSpec, + roadmap, + workGraph, + workStates, +} from "./helpers/roadmap-probe.js"; + +const roadmapImplementation = codeAnchor({ + id: codeAnchorId("impl:protocol.roadmap-recipe"), + label: "realizes the roadmap in the shipped recipe catalog", + satisfies: ref("spec:consumers.roadmap"), + role: "projection", +}); +void roadmapImplementation; +const memoImplementation = codeAnchor({ + id: codeAnchorId("impl:protocol.roadmap-memo-recipe"), + label: "realizes the roadmap memo in the shipped recipe catalog", + satisfies: ref("spec:consumers.roadmap.memo"), + role: "projection", +}); +void memoImplementation; +const roadmapTests = specTest({ + id: testAnchorId("test:protocol.roadmap-recipe"), + verifies: ref("spec:consumers.roadmap"), +}); +void roadmapTests; +const memoTests = specTest({ + id: testAnchorId("test:protocol.roadmap-memo-recipe"), + verifies: ref("spec:consumers.roadmap.memo"), +}); +void memoTests; + +describe("the roadmap recipes", () => { + it("keeps manifest places and reports an unresolved middle member", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["first", "missing", "last"]), + "first.sdp.md": probeSpec("first", "defined", "\n dependsOn: spec:probe.last"), + "missing.sdp.md": probeSpec("missing"), + "last.sdp.md": probeSpec("last", "defined"), + "part.sdp.md": probeSpec( + "part", + "defined", + "\n refines: [spec:probe.first, spec:probe.last]", + ), + }); + const damaged = { + ...graph, + nodes: graph.nodes.filter((node) => node.id !== "spec:probe.missing"), + }; + const result = (await roadmap(damaged)).roadmaps[0]; + expect(result?.unresolved).toEqual([{ place: 2, id: "spec:probe.missing" }]); + expect(result?.totals.entries).toBe(3); + expect(result?.totals.unresolved).toBe(1); + expect(result?.totals.entries).toBe( + ["toDesign", "toState", "toBuild", "built", "apart", "unresolved"].reduce( + (sum, key) => sum + (result?.totals[key] ?? 0), + 0, + ), + ); + expect((await roadmap(graph)).roadmaps[0]?.totals.unresolved).toBe(0); + expect(result?.entries.map(({ place, id }) => ({ place, id }))).toEqual([ + { place: 1, id: "spec:probe.first" }, + { place: 3, id: "spec:probe.last" }, + ]); + expect(result?.entries[0]?.restsOn).toContainEqual( + expect.objectContaining({ id: "spec:probe.last", onRoadmap: 3 }), + ); + expect(result?.entries[0]?.sharesScopeWith).toEqual([3]); + expect(result?.waiting.statements.find((row) => row.id === "spec:probe.part")?.places).toEqual([ + 1, 3, + ]); + expect((await memo(damaged)).memo).toContain( + "### Unresolved members\n\n- Place 2: `spec:probe.missing`", + ); + }); + + it("escapes HTML outside code spans in memo titles and framing", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["root"], "Read & inspect `a < b & c > d`."), + "root.sdp.md": probeSpec("root").replace( + "# root", + "# Epic
    & > `a < b & c > d` ``a ` < b & > c``", + ), + }); + const result = (await memo(graph)).memo ?? ""; + expect(result).toContain("Epic <details> & > `a < b & c > d` ``a ` < b & > c``"); + expect(result).toContain("Read & inspect `a < b & c > d`."); + expect(result).not.toContain("Epic
    "); + }); + it.each( + workStates.map((state, index) => ({ + state, + need: [ + "built", + "to build", + "to state", + "to design", + "to design", + "to design", + "to design", + "to design", + ][index], + })), + )("reads the work's need: $state", async ({ state, need }) => { + const answer = await roadmap(workGraph(state)); + const entry = answer.roadmaps[0]?.entries[0]; + expect(entry?.need).toBe(need); + expect(entry?.statedReadiness).toBe("ready"); + expect(entry?.work.drift).toBe(state === workStates[4] ? 1 : 0); + expect(entry?.workRows.map((row) => row.id)).toEqual(["spec:probe.part", "spec:probe.root"]); + expect(JSON.stringify(answer)).not.toContain('"lane"'); + }); + + it("selects priority Packs in id order and reads an explicitly named ordinary Pack", async () => { + const graph = probeGraph( + { + "a.sdp.md": probePack("a", ["root"]), + "z.sdp.md": probePack("z", ["root"]), + "r.sdp.md": probePack("reading", ["root"]), + "root.sdp.md": probeSpec("root"), + }, + ["z", "a"], + ); + expect((await roadmap(graph)).roadmaps.map((row) => row.pack)).toEqual(["pack:a", "pack:z"]); + expect((await roadmap(graph, { pack: "pack:reading" })).roadmaps[0]?.order).toBe("reading"); + for (const params of [ + { pack: "pack:missing" }, + { pack: null }, + { decider: "Bad owner" }, + { decider: null }, + ]) + expect(await roadmap(graph, params)).toMatchObject({ + diagnostics: [{ parameter: Object.keys(params)[0] }], + }); + expect((await roadmap({ ...graph, nodes: [] })).roadmaps).toEqual([]); + expect((await memo(graph)).memo).toBeNull(); + expect((await memo({ ...graph, nodes: [] })).memo).toBeNull(); + expect((await memo(graph, { pack: "pack:missing" })).memo).toBeNull(); + }); + + it("names holding parts and a descendant's outside parent", async () => { + for (const [state, clause] of [ + [workStates[5], "kind-evidence-present"], + [workStates[6], "no-blocking-open-questions"], + [workStates[7], "typed-dependency-targets-are-defined"], + ] as const) { + const entry = (await roadmap(workGraph(state))).roadmaps[0]?.entries[0]; + expect( + entry?.workRows.find((row) => row.id === "spec:probe.part")?.holdsNextRung, + ).toContainEqual({ clause, targets: state === workStates[7] ? ["spec:probe.outside"] : [] }); + if (state === workStates[7]) + expect(entry?.restsOn).toEqual([ + { + id: "spec:probe.outside", + via: "refines", + from: ["spec:probe.part"], + need: "to state", + statedReadiness: "scoped", + holdsReady: true, + onRoadmap: null, + }, + ]); + } + }); + + it("renders each unbuilt dependency once when two relations reach it", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["root"]), + "root.sdp.md": probeSpec("root", "ready", "\n dependsOn: spec:probe.o"), + "part.sdp.md": probeSpec("part", "ready", "\n refines: [spec:probe.root, spec:probe.o]"), + "o.sdp.md": probeSpec("o"), + }); + expect((await roadmap(graph)).roadmaps[0]?.entries[0]?.restsOn.map((row) => row.via)).toEqual([ + "dependsOn", + "refines", + ]); + expect((await memo(graph)).memo).toContain( + "| 1 | root `spec:probe.root` | Read the probe. | `spec:probe.o` (to build) |\n", + ); + }); + + it("lists sorted incoming relations, Pack memberships and dependency places", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["root", "z", "a", "part"]), + "z-pack.sdp.md": probePack("z", ["root"]), + "a-pack.sdp.md": probePack("a", ["root"]), + "root.sdp.md": probeSpec("root"), + "part.sdp.md": probeSpec("part", "ready", "\n refines: spec:probe.root"), + "a.sdp.md": probeSpec( + "a", + "ready", + "\n dependsOn: [spec:probe.root, spec:probe.part]\n constrainedBy: spec:probe.root", + ), + "z.sdp.md": probeSpec("z", "ready", "\n decidedBy: spec:probe.root"), + "outside.sdp.md": probeSpec("outside", "ready", "\n dependsOn: spec:probe.part"), + }); + const entries = (await roadmap(graph, { pack: "pack:probe" })).roadmaps[0]?.entries; + expect(entries?.[0]?.packs).toEqual(["pack:a", "pack:z"]); + expect(entries?.[0]?.restedOnBy).toEqual([ + { id: "spec:probe.a", via: "constrainedBy", need: "to build", onRoadmap: 3 }, + { id: "spec:probe.a", via: "dependsOn", need: "to build", onRoadmap: 3 }, + { id: "spec:probe.outside", via: "dependsOn", need: "to build", onRoadmap: null }, + { id: "spec:probe.z", via: "decidedBy", need: "to build", onRoadmap: 2 }, + ]); + expect(entries?.[2]?.restsOn).toEqual([ + { + id: "spec:probe.part", + via: "dependsOn", + from: ["spec:probe.a"], + need: "to build", + statedReadiness: "ready", + holdsReady: false, + onRoadmap: 1, + }, + { + id: "spec:probe.root", + via: "constrainedBy", + from: ["spec:probe.a"], + need: "to build", + statedReadiness: "ready", + holdsReady: false, + onRoadmap: 1, + }, + { + id: "spec:probe.root", + via: "dependsOn", + from: ["spec:probe.a"], + need: "to build", + statedReadiness: "ready", + holdsReady: false, + onRoadmap: 1, + }, + ]); + }); + + it("includes keyed and unkeyed blocking questions on shaping decisions", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["root"]), + "root.sdp.md": probeSpec("root", "defined", "\n decidedBy: spec:probe.choice"), + "choice.sdp.md": probeSpec( + "choice", + "scoped", + "\n refines: spec:probe.foundation-rule", + "## Decision\n- context: Choose a shape.\n- decision: Keep it.", + "decision", + ).replace( + "\n\n## Decision", + "\n\n### Open questions\n- [blocking #shape] Which shape?\n - decider: owner\n - provisional: Keep it simple.\n- [non-blocking #note] A note.\n- [blocking] Which boundary?\n\n## Decision", + ), + }); + expect((await roadmap(graph)).roadmaps[0]?.entries[0]?.decisions).toEqual([ + { + id: "spec:probe.choice", + title: "choice", + statedReadiness: "scoped", + blockingQuestions: [ + { + address: "spec:probe.choice#question.shape", + spec: "spec:probe.choice", + index: 0, + question: "Which shape?", + blocking: true, + decider: "owner", + provisional: "Keep it simple.", + }, + { + address: null, + spec: "spec:probe.choice", + index: 2, + question: "Which boundary?", + blocking: true, + decider: null, + provisional: null, + }, + ], + }, + ]); + }); + + it("finds earlier statements through a shaping decision's own waiting statement", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["root", "base"]), + "root.sdp.md": probeSpec("root", "ready", "\n decidedBy: spec:probe.choice"), + "choice.sdp.md": probeSpec( + "choice", + "defined", + "\n refines: spec:probe.foundation-rule\n dependsOn: spec:probe.base", + "## Decision\n- context: Choose a shape.\n- decision: Keep it.", + "decision", + ), + "base.sdp.md": probeSpec("base", "defined"), + }); + const result = (await roadmap(graph)).roadmaps[0]; + expect(result?.waiting.statements.map((row) => row.id)).toEqual([ + "spec:probe.base", + "spec:probe.choice", + ]); + expect(result?.entries[0]?.earlierStatements).toEqual([ + { id: "spec:probe.base", where: "roadmap", onRoadmap: 2 }, + ]); + }); + + it.each(["A purpose?", "A purpose!", "A purpose", "A purpose."])( + "ends the intent line with a full stop: %s", + async (purpose) => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["root"]), + "root.sdp.md": probeSpec("root").replace("Read the probe.", purpose), + }); + expect((await memo(graph)).memo).toContain( + `| ${purpose.endsWith(".") ? purpose : `${purpose}.`} | nothing unbuilt |`, + ); + }, + ); + + it("keeps queue order across places, including earlier statements inside, elsewhere and outside", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["dependent", "base", "dependent.part"]), + "dependent.sdp.md": probeSpec( + "dependent", + "defined", + "\n dependsOn: [spec:probe.base, spec:probe.outside]", + ), + "base.sdp.md": probeSpec("base", "defined"), + "part.sdp.md": probeSpec( + "dependent.part", + "defined", + "\n refines: spec:probe.dependent\n dependsOn: spec:probe.base", + ), + "outside.sdp.md": probeSpec("outside", "defined"), + }); + const result = (await roadmap(graph)).roadmaps[0]; + expect(result?.waiting.statements.map((row) => row.id)).toEqual([ + "spec:probe.base", + "spec:probe.dependent", + "spec:probe.dependent.part", + ]); + expect(result?.waiting.statements[0]?.places).toEqual([2]); + expect(result?.entries[0]?.earlierStatements).toEqual([ + { id: "spec:probe.base", where: "roadmap", onRoadmap: 2 }, + { id: "spec:probe.outside", where: "outside", onRoadmap: null }, + { id: "spec:probe.dependent", where: "entry", onRoadmap: null }, + ]); + expect(result?.entries[0]?.sharesScopeWith).toEqual([3]); + expect(result?.entries[1]?.earlierStatements).toEqual([]); + expect((await memo(graph)).memo).toContain( + "in this entry `spec:probe.dependent`; on the roadmap `spec:probe.base` (place 2); 1 outside the roadmap", + ); + }); + + it("deduplicates shared descendants, preserves places, and lists decision entries apart", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["root", "other", "choice"]), + "root.sdp.md": probeSpec("root"), + "other.sdp.md": probeSpec("other"), + "part.sdp.md": probeSpec("part", "ready", "\n refines: [spec:probe.root, spec:probe.other]"), + "choice.sdp.md": probeSpec( + "choice", + "ready", + "{}", + "## Decision\n- context: The probe chooses.\n- decision: Keep the choice.", + "decision", + ), + "impl.ts": implementation("root") + implementation("other") + implementation("part"), + }); + const result = (await roadmap(graph)).roadmaps[0]; + expect( + result?.entries.map((row) => [row.place, row.need, row.work.specs, row.sharesScopeWith]), + ).toEqual([ + [1, "built", 2, [2]], + [2, "built", 2, [1]], + ]); + expect(result?.apart).toEqual([ + { + place: 3, + id: "spec:probe.choice", + specKind: "decision", + statedReadiness: "ready", + need: "settled", + }, + ]); + }); + + it("lists every unassigned question apart, including unkeyed and shared shaping decisions", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["first", "second"]), + "first.sdp.md": probeSpec("first", "scoped", "\n decidedBy: spec:probe.choice").replace( + "\n\n## Behavior", + "\n\n### Open questions\n- [non-blocking #note] An unassigned note.\n- [blocking] An unassigned hold.\n- [non-blocking #selected] A selected question.\n - decider: team-lead\n - provisional: Keep the current design.\n- [blocking #owner] An owner hold.\n - decider: owner\n\n## Behavior", + ), + "second.sdp.md": probeSpec("second", "ready", "\n decidedBy: spec:probe.choice"), + "choice.sdp.md": probeSpec( + "choice", + "defined", + "\n refines: spec:probe.foundation", + "## Decision\n- context: Choose a shape.\n- decision: Keep it.", + "decision", + ).replace( + "\n\n## Decision", + "\n\n### Open questions\n- [non-blocking #choice] Ratify this choice.\n - decider: owner\n- [non-blocking] An unassigned decision question.\n\n## Decision", + ), + }); + const result = (await roadmap(graph)).roadmaps[0]; + expect(result?.totals.withoutDecider).toBe(3); + expect(result?.totals.blockingWithoutDecider).toBe(1); + expect(result?.waiting.withoutDecider.map((row) => [row.spec, row.index, row.places])).toEqual([ + ["spec:probe.first", 1, [1]], + ["spec:probe.choice", 1, [1, 2]], + ["spec:probe.first", 0, [1]], + ]); + expect(result?.waiting.questions.map((row) => row.address)).toEqual([ + "spec:probe.first#question.owner", + "spec:probe.choice#question.choice", + ]); + expect(result?.entries[0]?.decisions).toMatchObject([{ id: "spec:probe.choice" }]); + const selected = (await roadmap(graph, { decider: "team-lead" })).roadmaps[0]?.waiting + .questions; + expect(selected).toMatchObject([ + { address: "spec:probe.first#question.selected", provisional: "Keep the current design." }, + ]); + expect(result?.waiting.statements).toContainEqual({ + id: "spec:probe.choice", + statedReadiness: "defined", + floorReached: "ready", + places: [1, 2], + }); + const rendered = (await memo(graph)).memo; + expect(rendered).toContain( + "These questions have no named decider.\n\n- `spec:probe.first` question 2 (blocking), place 1", + ); + expect(rendered).not.toContain("An unassigned hold."); + }); + + it("pins memo bytes, intent fallback, line breaks and table escaping", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack( + "probe", + ["root"], + "First paragraph.\nA second line.\n\nAnother paragraph.", + ), + "root.sdp.md": probeSpec("root") + .replace("# root", "# Root | `quoted`") + .replace("Read the probe.", "Read | `the probe`."), + }); + const expected = [ + "## The next work, from the roadmap", + "", + "Roadmap: probe `pack:probe`", + "", + "To state means the floor allows ready and a person still judges the statement. Built means every Spec of the entry's work states ready and has an implementation binding; it reports no passing test and no deployment.", + "", + "First paragraph. A second line.", + "", + "Another paragraph.", + "", + "### To design", + "", + "No entry needs design.", + "", + "### To build", + "", + "| Place | Entry | Purpose and reach | Rests on |", + "| --- | --- | --- | --- |", + "| 1 | Root \\| `quoted` `spec:probe.root` | Read \\| `the probe`. | nothing unbuilt |", + "", + ].join("\n"); + expect(await memo(graph)).toEqual({ diagnostics: [], memo: expected }); + expect(await memo(graph)).toEqual({ diagnostics: [], memo: expected }); + const reading = (await memo(graph, { pack: "pack:probe" })).memo; + expect(reading?.endsWith("\n")).toBe(true); + expect(reading?.endsWith("\n\n")).toBe(false); + expect( + ( + await memo( + probeGraph( + { "pack.sdp.md": probePack("probe", ["root"]), "root.sdp.md": probeSpec("root") }, + [], + ), + { pack: "pack:probe" }, + ) + ).memo, + ).toContain("This Pack's order is reading order, not a stated priority."); + }); + + it("pins every populated memo section byte for byte", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", ["root", "build", "built", "state", "dependent", "base"]), + "root.sdp.md": probeSpec("root", "scoped", "\n decidedBy: spec:probe.choice").replace( + "\n\n## Behavior", + "\n\n### Open questions\n- [blocking #root] Settle the root.\n - decider: owner\n- [non-blocking] An unassigned note.\n\n## Behavior", + ), + "choice.sdp.md": probeSpec( + "choice", + "scoped", + "\n refines: spec:probe.foundation", + "## Decision\n- context: A choice remains.", + "decision", + ), + ...Object.fromEntries( + ["a", "b", "c", "d"].map((name) => [ + `${name}.sdp.md`, + probeSpec(name, "scoped", "\n refines: spec:probe.root", ""), + ]), + ), + "build.sdp.md": probeSpec("build", "ready", "\n dependsOn: spec:probe.external"), + "external.sdp.md": probeSpec("external"), + "built.sdp.md": probeSpec("built"), + "built.ts": implementation("built"), + "state.sdp.md": probeSpec("state", "defined"), + "dependent.sdp.md": probeSpec( + "dependent", + "defined", + "\n dependsOn: [spec:probe.base, spec:probe.outside]", + ), + "dependent-part.sdp.md": probeSpec( + "dependent.part", + "defined", + "\n refines: spec:probe.dependent\n dependsOn: spec:probe.base", + ), + "base.sdp.md": probeSpec("base", "defined"), + "outside.sdp.md": probeSpec("outside", "defined"), + "deferred.sdp.md": probeSpec("deferred", "scoped").replace( + "\n\n## Behavior", + "\n\n### Open questions\n- [blocking #later] A deferred | choice.\n - trigger: A reader asks.\n\n## Behavior", + ), + }); + const expected = [ + "## The next work, from the roadmap", + "", + "Roadmap: probe `pack:probe`", + "", + "To state means the floor allows ready and a person still judges the statement. Built means every Spec of the entry's work states ready and has an implementation binding; it reports no passing test and no deployment.", + "", + "### To design", + "", + "| Place | Entry | Next | Purpose and reach |", + "| --- | --- | --- | --- |", + "| 1 | root `spec:probe.root` | itself `scoped` → `defined`, held by no-blocking-open-questions; `spec:probe.a` `scoped` → `defined`, held by kind-evidence-present, kind-evidence-complete; `spec:probe.b` `scoped` → `defined`, held by kind-evidence-present, kind-evidence-complete; and 2 more parts to design; blocked by `spec:probe.root#question.root`; decided by `spec:probe.choice` (scoped) | Read the probe. |", + "", + "### To state", + "", + "| Place | Entry | Earlier waiting statements | Purpose and reach |", + "| --- | --- | --- | --- |", + "| 4 | state `spec:probe.state` | No preceding waiting statement in the statement queue. | Read the probe. |", + "| 5 | dependent `spec:probe.dependent` | in this entry `spec:probe.dependent`; on the roadmap `spec:probe.base` (place 6); 1 outside the roadmap | Read the probe. |", + "| 6 | base `spec:probe.base` | No preceding waiting statement in the statement queue. | Read the probe. Waited on by 2. |", + "", + "### To build", + "", + "| Place | Entry | Purpose and reach | Rests on |", + "| --- | --- | --- | --- |", + "| 2 | build `spec:probe.build` | Read the probe. | `spec:probe.external` (to build) |", + "", + "### Waiting on the owner", + "", + "- `spec:probe.root#question.root` (blocking), place 1: Settle the root.", + "- State `spec:probe.base`: it states `defined` and its floor allows `ready`; place 6", + "- State `spec:probe.state`: it states `defined` and its floor allows `ready`; place 4", + "- State `spec:probe.dependent`: it states `defined` and its floor allows `ready`; place 5", + "- State `spec:probe.dependent.part`: it states `defined` and its floor allows `ready`; place 5", + "", + "### Questions without a decider", + "", + "These questions have no named decider.", + "", + "- `spec:probe.root` question 2, place 1", + "", + "### Built and still on the roadmap", + "", + "- Place 3: built `spec:probe.built`", + "", + "### Deferred, with their triggers", + "", + "- `spec:probe.deferred#question.later`: A deferred | choice. Trigger: A reader asks.", + "", + ].join("\n"); + expect(await memo(graph)).toEqual({ diagnostics: [], memo: expected }); + expect( + await memo({ + ...graph, + nodes: [...graph.nodes].reverse(), + edges: [...graph.edges].reverse(), + }), + ).toEqual({ diagnostics: [], memo: expected }); + }); + + it("pins empty design and build sections byte for byte", async () => { + const graph = probeGraph({ + "pack.sdp.md": probePack("probe", []).replace("specs:\n", "specs: []\n"), + }); + expect(await memo(graph)).toEqual({ + diagnostics: [], + memo: [ + "## The next work, from the roadmap", + "", + "Roadmap: probe `pack:probe`", + "", + "To state means the floor allows ready and a person still judges the statement. Built means every Spec of the entry's work states ready and has an implementation binding; it reports no passing test and no deployment.", + "", + "### To design", + "", + "No entry needs design.", + "", + "### To build", + "", + "No entry is ready to build.", + "", + ].join("\n"), + }); + }); + + it("reads purpose from intent and counts distinct waiting Specs and buildable parts", async () => { + const graph = workGraph(workStates[6]); + const extra = probeGraph({ + "waiter.sdp.md": probeSpec( + "waiter", + "defined", + "\n dependsOn: spec:probe.root\n refines: spec:probe.beyond", + ), + "beyond.sdp.md": probeSpec("beyond"), + }); + const waiter = extra.nodes.find((node) => node.id === "spec:probe.waiter"); + if (waiter === undefined) throw new Error("The waiting Spec must be present."); + const changed = { + ...graph, + nodes: [ + ...graph.nodes.map((node) => + node.id === "spec:probe.root" && node.nodeType === "Primitive" + ? { + ...node, + sections: { + ...node.sections, + intent: { ...node.sections?.intent, problem: "Read the purpose | clearly" }, + }, + } + : node, + ), + waiter, + ], + edges: [ + ...graph.edges, + { + from: waiter.id, + to: "spec:probe.root", + type: "dependsOn" as const, + claim: "declared" as const, + }, + { + from: waiter.id, + to: "spec:probe.root", + type: "constrainedBy" as const, + claim: "declared" as const, + }, + ], + }; + const text = (await memo(changed)).memo ?? ""; + expect(text).toContain("Read the purpose \\| clearly. Waited on by 1."); + const mixed = workGraph(workStates[6]); + const noRootBinding = { + ...mixed, + edges: mixed.edges.filter((edge) => edge.type !== "satisfies"), + }; + expect((await memo(noRootBinding)).memo).toContain("1 of its parts ready to build now."); + expect((await memo(workGraph(workStates[7]))).memo).toContain( + "typed-dependency-targets-are-defined (`spec:probe.outside`)", + ); + }); +}); diff --git a/test/self-hosting-design-slice.test.ts b/test/self-hosting-design-slice.test.ts new file mode 100644 index 000000000..17e12ec08 --- /dev/null +++ b/test/self-hosting-design-slice.test.ts @@ -0,0 +1,1383 @@ +import { registerOutsideComponents } from "./consumers.design-slice.outside-components.test.generated.js"; +import { registerOutsideHolding } from "./consumers.design-slice.outside-holding.test.generated.js"; +import { registerOutsideUnbound } from "./consumers.design-slice.outside-unbound.test.generated.js"; +import { registerOutsideSatisfied } from "./consumers.design-slice.outside-satisfied.test.generated.js"; +import { registerComponentUnreadable } from "./consumers.design-slice.component-unreadable.test.generated.js"; +import { registerComponentDiffering } from "./consumers.design-slice.component-differing.test.generated.js"; +import { registerComponentMatching } from "./consumers.design-slice.component-matching.test.generated.js"; +import { registerComponentUnrelated } from "./consumers.design-slice.component-unrelated.test.generated.js"; +import { registerComponentAbsent } from "./consumers.design-slice.component-absent.test.generated.js"; +import { registerIndependentExample } from "./consumers.design-slice.independent-example.test.generated.js"; +import { registerFoldedExample } from "./consumers.design-slice.folded-example.test.generated.js"; +import { registerDependencyCycle } from "./consumers.design-slice.dependency-cycle.test.generated.js"; +import { registerDecisionBacklink } from "./consumers.design-slice.decision-backlink.test.generated.js"; +import { registerChain } from "./consumers.design-slice.chain.test.generated.js"; +import { unspecified } from "@libar-dev/software-delivery-protocol/runner"; +import type { + DesignSliceConditions, + DesignSliceOutcome, +} from "../generated/contracts/consumers.design-slice.space.js"; +import { readFileSync } from "node:fs"; +import { recipeFiles } from "../src/cli/build-artifacts.js"; +import { evaluateBody } from "../src/index.js"; +import { + codeAnchor, + codeAnchorId, + oracleAnchorId, + ref, + specOracle, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { describe, expect, it } from "vitest"; +import { catalogRecipe } from "./helpers/recipe-probe.js"; +import { + cid, + component, + designProbe, + link, + primitive, + setMembers, + sid, + sliceProbe, +} from "./helpers/design-slice-probe.js"; +import type { GraphSchema } from "../src/index.js"; + +interface Member { + id: string; + depth: number; + restsOn: string[]; + exampleOf: string | null; + exampleMembers: string[]; + holds: unknown[]; + design: Record; + rules: { total: number; keyed: number }; + implementedBy: string[] | number; + referencedBy: string[] | number; + title?: string; + examples: { total: number; enabled: number }; + verifiers: { total: number; enabled: number }; +} +interface Design { + spec: string; + componentId: string; + anchorExists: boolean; + realized: boolean; + layer: string | null; + uses: string[] | null; + differing: unknown[]; + unreadable: unknown[]; +} +interface Slice { + scope: Record; + totals: { + depth: number; + cycles: number; + exampleMembers: number; + designs: Record; + components: Record; + componentUses: Record; + }; + members: Member[]; + structure: { + edges: { from: string; to: string; type: string; orders: boolean; reason?: string }[]; + cycles: { members: string[]; cycle: string[] }[]; + unresolved: { from: string; to: string; type: string; reason?: string }[]; + }; + boundary: Record< + string, + { + id: string; + holds: string[]; + inPack?: boolean; + via: { type: string; members: string[]; reasons?: { member: string; reason: string }[] }[]; + }[] + >; + architecture: { + components: { + id: string; + members: { satisfies: string[]; references: string[] }; + restsOn: string[]; + designs: string[]; + }[]; + designs: Design[]; + uses: { from: string; to: string; count: number; outside: boolean; units: unknown[] }[]; + plannedUses: { design: string; from: string; to: string; outside: boolean }[]; + outside: { id: string; anchored: boolean; reachedBy: string[] }[]; + unplaced: unknown[]; + }; + decisions: unknown[]; + questions: { + spec: string; + blocking: boolean; + holds: string[]; + origin: string; + address: string | null; + }[]; + leftOut?: string[]; + diagnostics?: unknown[]; +} +function assertKeys(value: unknown, names: string): void { + expect(Object.keys(value as object).sort()).toEqual(names.split(" ").sort()); +} +function assertMemberKeys(value: unknown, full: boolean): void { + assertKeys( + value, + full + ? "id title specKind altitude statedReadiness floorReached statedNextRung holds depth restsOn restedOnBy exampleOf exampleMembers design rules examples verifiers implementedBy referencedBy components" + : "id specKind statedReadiness floorReached statedNextRung holds depth restsOn exampleOf design rules implementedBy referencedBy", + ); +} +function required(value: T | undefined): T { + if (value === undefined) throw new Error("The probe did not produce the expected value."); + return value; +} +const slice = ( + graph: GraphSchema = sliceProbe(), + params: Record = { pack: "pack:probe.slice", detail: true }, +) => + evaluateBody( + required( + recipeFiles( + readFileSync(new URL("../docs/agent-surface/recipes.md", import.meta.url), "utf8"), + ).find((row) => row.ordinal === 54), + ).content, + { graph, params }, + ) as Promise; + +const sliceDocumentAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.design-slice-recipe"), + label: "realizes the design slice in the shipped recipe catalog", + satisfies: ref("spec:consumers.design-slice"), + role: "projection", +}); +void sliceDocumentAnchor; +const sliceTestAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice"), + verifies: ref("spec:consumers.design-slice"), +}); +void sliceTestAnchor; + +describe("design slice", () => { + it("keeps authored order while assigning depth and composing whole-Pack boundary", async () => { + const graph = sliceProbe(); + graph.nodes.push( + primitive("outside"), + primitive("downstream"), + primitive("verifier", { specKind: "example" }), + ); + graph.edges.push( + link(sid("base"), "dependsOn", sid("outside")), + link(sid("downstream"), "constrainedBy", sid("base")), + link(sid("verifier"), "verifies", sid("top")), + ); + const answer = await slice(graph); + expect(answer.members.map(({ id, depth }) => ({ id, depth }))).toEqual([ + { id: sid("top"), depth: 2 }, + { id: sid("base"), depth: 1 }, + ]); + const pack = await catalogRecipe<{ boundary: Record }>(29, graph, { + pack: "pack:probe.slice", + detail: true, + }); + for (const [direction, rows] of Object.entries(pack.boundary)) + expect(answer.boundary[direction]).toEqual( + rows.map((row) => expect.objectContaining(row as object)), + ); + expect(answer.structure.edges).toEqual([ + { + from: sid("top"), + to: sid("base"), + type: "dependsOn", + orders: true, + reason: "the top needs its base", + }, + ]); + }); + it("keeps summary totals, selected full members and explicit omissions", async () => { + const full = await slice(); + const summary = await slice(sliceProbe(), { pack: "pack:probe.slice" }); + expect(summary.totals).toEqual(full.totals); + expect(summary.members[0]).not.toHaveProperty("title"); + expect(summary.structure.edges[0]).not.toHaveProperty("reason"); + expect(summary.leftOut?.length).toBeGreaterThan(0); + expect(await slice(sliceProbe(), { pack: "pack:probe.slice", detail: false })).toEqual(summary); + const mixed = await slice(sliceProbe(), { + pack: "pack:probe.slice", + detail: [sid("top"), sid("other")], + }); + expect(mixed.members[0]).toEqual(full.members[0]); + expect(mixed.members[1]).toEqual(summary.members[1]); + expect(mixed.diagnostics).toEqual([{ id: sid("other"), reason: "not-member" }]); + }); + it("reads an ordered Spec list and a narrowed Pack, marking excluded Pack members", async () => { + const narrowed = await slice(sliceProbe(), { + pack: "pack:probe.slice", + members: [sid("top")], + detail: true, + }); + expect(narrowed.scope).toMatchObject({ subset: true, members: [sid("top")] }); + expect(narrowed.boundary.restsOn).toMatchObject([{ id: sid("base"), inPack: true }]); + const list = await slice(sliceProbe(), { specs: [sid("base"), sid("top")], detail: true }); + expect(list.scope).toEqual({ specs: [sid("base"), sid("top")] }); + expect(list.members.map((row) => row.id)).toEqual([sid("base"), sid("top")]); + }); + it.each(["dependsOn", "constrainedBy", "decidedBy"] as const)( + "only suppresses a refinement the target answers with %s", + async (type) => { + const graph = sliceProbe(); + graph.edges = [link(sid("base"), "refines", sid("top")), link(sid("top"), type, sid("base"))]; + const answer = await slice(graph); + expect(answer.structure.edges.filter((edge) => !edge.orders)).toEqual([ + { from: sid("base"), to: sid("top"), type: "refines", orders: false }, + ]); + expect(answer.members.map((row) => row.depth)).toEqual([2, 1]); + }, + ); + it("lists cycles at the depth after their deepest outside prerequisite", async () => { + const graph = sliceProbe(); + graph.nodes.push(primitive("root")); + graph.edges.push( + link(sid("base"), "dependsOn", sid("top")), + link(sid("base"), "dependsOn", sid("root")), + ); + setMembers(graph, ["top", "base", "root"]); + const answer = await slice(graph); + expect(answer.structure.cycles).toEqual([ + { members: [sid("base"), sid("top")], cycle: [sid("base"), sid("top"), sid("base")] }, + ]); + expect(answer.members.map((row) => row.depth)).toEqual([2, 2, 1]); + }); + it.each(["refines", "dependsOn", "constrainedBy", "decidedBy"] as const)( + "does not fold an example with another %s in either direction or across the boundary", + async (type) => { + for (const incoming of [false, true]) { + const graph = sliceProbe(); + graph.nodes.push(primitive("example", { specKind: "example" }), primitive("outside")); + graph.edges.push( + link(sid("example"), "refines", sid("base")), + incoming + ? link(sid("outside"), type, sid("example")) + : link(sid("example"), type, sid("outside")), + ); + setMembers(graph, ["base", "example"]); + expect((await slice(graph)).members[1]?.exampleOf).toBeNull(); + } + }, + ); + it("accumulates diagnostics in contract order and skips dependent checks", async () => { + const graph = sliceProbe(); + const run = (params: Record) => catalogRecipe(54, graph, params); + expect(await run({ detail: null })).toEqual({ + diagnostics: [ + { scope: "detail", problem: "Expected true, false or a list of member ids." }, + { scope: "scope", problem: "Supply exactly one of pack or specs." }, + ], + }); + expect(await run({ pack: false, members: false, detail: 1 })).toEqual({ + diagnostics: [ + { scope: "detail", problem: "Expected true, false or a list of member ids." }, + { scope: "pack", problem: "Pack does not resolve." }, + ], + }); + expect(await run({ specs: [sid("missing"), sid("absent")], members: [], detail: {} })).toEqual({ + diagnostics: [ + { scope: "detail", problem: "Expected true, false or a list of member ids." }, + { scope: sid("missing"), problem: "Spec does not resolve." }, + { scope: sid("absent"), problem: "Spec does not resolve." }, + { scope: "members", problem: "members narrows only a Pack scope." }, + ], + }); + expect( + await run({ pack: "pack:probe.slice", members: [sid("absent"), sid("missing")] }), + ).toEqual({ + diagnostics: [ + { scope: sid("absent"), problem: "Not a resolved member of the Pack." }, + { scope: sid("missing"), problem: "Not a resolved member of the Pack." }, + ], + }); + expect(await run({ pack: "pack:probe.slice", specs: [sid("base")] })).toEqual({ + diagnostics: [{ scope: "scope", problem: "Supply exactly one of pack or specs." }], + }); + for (const value of [null, [], [1], [sid("base"), sid("base")], "bad"]) { + expect(await run({ pack: "pack:probe.slice", members: value })).toEqual({ + diagnostics: [ + { scope: "members", problem: "Expected a non-empty list of distinct member ids." }, + ], + }); + expect(await run({ specs: value })).toEqual({ + diagnostics: [ + { scope: "specs", problem: "Expected a non-empty list of distinct Spec ids." }, + ], + }); + } + expect(await run({ wrong: true, detail: null })).toEqual({ + unknownParams: ["wrong"], + accepts: ["pack", "specs", "members", "detail"], + }); + }); + it("pins the component Design grammar and totals to recipe 43", async () => { + for (const anchor of [ + "no anchor holds", + "an anchor holds without satisfying the design", + "an anchor satisfies, matching every declared value", + "an anchor satisfies, with a differing layer and uses", + "an anchor satisfies, with an unreadable layer", + ]) { + const graph = designProbe(anchor); + const answer = await slice(graph); + const comparison = await catalogRecipe<{ + totals: Record; + differing: { + spec: string; + componentId: string; + field: string; + planned: unknown; + realized: unknown; + }[]; + unreadable: { spec: string; key: string; value: unknown }[]; + }>(43, graph); + const { designs, ...totals } = comparison.totals; + expect(answer.totals.designs).toEqual({ total: designs, ...totals }); + expect(answer.architecture.designs[0]?.differing).toEqual( + comparison.differing.map(({ field, planned, realized }) => ({ field, planned, realized })), + ); + expect(answer.architecture.designs[0]?.unreadable).toEqual( + comparison.unreadable.map(({ key, value }) => ({ key, value })), + ); + } + }); +}); + +const ORDER = "the members read in depth order {order}"; +const COUNTS = "the slice reports {cycles} cycles and {exampleMembers} folded example members"; +const DESIGN = + "the member design reads {realized} with {differing} differing and {unreadable} unreadable rows"; +const COMPONENTS = + "the touched components are {components}, {throughRestsOn} of them reached through what the slice rests on"; +const OUTSIDE = "the outside components are {outsideComponents}"; +const HELD = "the outside Spec holds {held}"; +interface World { + point: Partial; + graph: GraphSchema; + outcomeKind: DesignSliceOutcome["kind"]; + answer?: Slice; +} +function createWorld(point: Partial): World { + const graph = sliceProbe(); + const designed = designProbe(point.anchor ?? "no anchor holds"); + graph.nodes.push( + ...designed.nodes.filter((node) => node.id === sid("design") || node.nodeType === "CodeNode"), + ); + graph.edges.push(...designed.edges.filter((edge) => edge.type === "satisfies")); + if (point.structure?.includes("decision")) { + const at = graph.nodes.findIndex((node) => node.id === sid("base")); + graph.nodes[at] = primitive("base", { + specKind: "decision", + sections: { + intent: { outcome: "Shape the top." }, + decision: { context: "A probe choice.", decision: "Choose the probe." }, + }, + }); + graph.edges = [ + link(sid("top"), "decidedBy", sid("base")), + link(sid("base"), "refines", sid("top")), + ...graph.edges.filter((edge) => edge.type === "satisfies"), + ]; + } + if (point.structure?.includes("two members")) + graph.edges.push(link(sid("base"), "dependsOn", sid("top"))); + if (point.structure?.includes("example")) { + const at = graph.nodes.findIndex((node) => node.id === sid("top")); + graph.nodes[at] = primitive("top", { + specKind: "example", + readiness: "scoped", + sections: { + intent: { + outcome: "Exercise the base.", + openQuestions: [ + { question: "What holds this example?", blocking: true, key: "hold", decider: "owner" }, + ], + }, + behavior: { examples: [{ given: ["a probe"], when: ["it runs"], then: ["it holds"] }] }, + }, + }); + graph.edges = [ + link(sid("top"), "refines", sid("base")), + ...graph.edges.filter((edge) => edge.type === "satisfies"), + ]; + if (point.structure.includes("outside")) + graph.edges.push(link(sid("top"), "dependsOn", sid("outside"))); + } + graph.nodes.push( + primitive( + "outside", + point.outside === "stated below defined" + ? { + readiness: "scoped", + sections: { + intent: { + outcome: "Hold a member.", + openQuestions: [ + { + question: "Which contract holds?", + blocking: true, + key: "contract", + decider: "owner", + provisional: "Read the probe.", + links: [sid("base")], + }, + { question: "Which label reads best?", blocking: false }, + ], + }, + behavior: { rules: ["Hold the base."] }, + }, + } + : {}, + ), + ); + graph.edges.push(link(sid("base"), "dependsOn", sid("outside"))); + if (point.outside === "that a component's unit satisfies") { + graph.nodes.push( + component("dependency"), + component("neighbor"), + component("far"), + primitive("downstream"), + component("downstream"), + { + id: "impl:probe.dependency", + nodeType: "CodeNode", + claim: "anchored", + file: "dependency.ts", + line: 2, + }, + ); + graph.edges.push( + link("impl:probe.dependency", "memberOf", cid("dependency"), "anchored"), + link("impl:probe.dependency", "satisfies", sid("outside"), "anchored"), + link(cid("dependency"), "uses", cid("neighbor"), "anchored"), + link(cid("neighbor"), "uses", cid("dependency"), "anchored"), + link(cid("neighbor"), "uses", cid("far"), "anchored"), + link(sid("downstream"), "dependsOn", sid("base")), + link(cid("downstream"), "satisfies", sid("downstream"), "anchored"), + ); + } + setMembers(graph, ["top", "base", "design"]); + const outcomeKind = point.structure?.includes("decision") + ? point.anchor === "an anchor satisfies, matching every declared value" && + point.outside === "that a component's unit satisfies" + ? OUTSIDE + : point.outside === "stated below defined" + ? HELD + : point.anchor === "an anchor holds without satisfying the design" + ? COMPONENTS + : ORDER + : point.structure?.includes("two members") || point.structure?.includes("example") + ? COUNTS + : point.outside === "that a component's unit satisfies" + ? DESIGN + : ORDER; + return { point, graph, outcomeKind }; +} +function columnProbe() { + const world = createWorld({ + structure: "rest on each other in a chain", + outside: "stated below defined", + anchor: "an anchor satisfies, with a differing layer and uses", + }); + const graph = world.graph as ReturnType; + graph.nodes.push( + primitive("choice", { + specKind: "decision", + readiness: "scoped", + sections: { + intent: { + outcome: "Choose the contract.", + openQuestions: [ + { + question: "Which contract?", + blocking: true, + key: "contract", + decider: "owner", + links: [sid("base")], + provisional: "Use the base.", + trigger: "The contract is chosen.", + }, + { question: "Which spelling?", blocking: false }, + ], + }, + decision: { context: "Choose a contract.", decision: "Use the base." }, + }, + }), + primitive("old", { specKind: "decision" }), + primitive("new", { specKind: "decision" }), + primitive("waiting"), + primitive("downstream"), + primitive("enabled", { specKind: "example" }), + primitive("disabled", { specKind: "example" }), + { + id: "test:probe.enabled", + nodeType: "Anchor", + claim: "anchored", + file: "enabled.test.ts", + line: 1, + }, + { + id: "test:probe.direct", + nodeType: "Anchor", + claim: "anchored", + file: "direct.test.ts", + line: 1, + }, + { id: "impl:probe.bare", nodeType: "CodeNode", claim: "anchored", file: "bare.ts", line: 1 }, + ); + graph.edges.push( + link(sid("top"), "decidedBy", sid("choice")), + link(sid("waiting"), "dependsOn", sid("base")), + link(sid("choice"), "supersedes", sid("old")), + link(sid("new"), "supersedes", sid("choice")), + link(sid("downstream"), "dependsOn", sid("base")), + link(sid("enabled"), "verifies", sid("base")), + link(sid("disabled"), "verifies", sid("base")), + link("test:probe.enabled", "verifies", sid("enabled"), "anchored"), + link("test:probe.direct", "verifies", sid("base"), "anchored"), + link("impl:probe.bare", "references", sid("top"), "anchored"), + ); + setMembers(graph, ["top", "base", "design", "choice", "waiting", "missing"]); + return graph; +} +const sliceOracleAnchor = specOracle({ + id: oracleAnchorId("oracle:protocol.design-slice"), + models: ref("spec:consumers.design-slice"), +}); +void sliceOracleAnchor; + +function expected(point: Partial): DesignSliceOutcome { + if ( + point.structure === "include a decision that refines the member it shapes" && + point.outside === "that a component's unit satisfies" && + point.anchor === "an anchor satisfies, matching every declared value" + ) + return { kind: OUTSIDE, outsideComponents: "component:probe.neighbor" }; + if ( + point.outside === "that no code satisfies" && + point.anchor === "an anchor satisfies, matching every declared value" + ) { + if (point.structure?.includes("two members")) + return { kind: COUNTS, cycles: 1, exampleMembers: 0 }; + if (point.structure?.includes("example")) + return { + kind: COUNTS, + cycles: 0, + exampleMembers: point.structure.includes("outside") ? 0 : 1, + }; + return { kind: ORDER, order: "spec:probe.base, spec:probe.design, spec:probe.top" }; + } + if ( + point.structure === "rest on each other in a chain" && + point.outside === "that a component's unit satisfies" + ) + return { + kind: DESIGN, + realized: point.anchor?.startsWith("an anchor satisfies") ? "realized" : "unrealized", + differing: point.anchor === "an anchor satisfies, with a differing layer and uses" ? 2 : 0, + unreadable: point.anchor === "an anchor satisfies, with an unreadable layer" ? 1 : 0, + }; + if ( + point.structure?.includes("decision") && + point.anchor === "an anchor holds without satisfying the design" + ) { + if (point.outside === "stated below defined") return { kind: HELD, held: "spec:probe.base" }; + return { + kind: COMPONENTS, + components: + point.outside === "that a component's unit satisfies" + ? "component:probe.dependency, component:probe.designed" + : "component:probe.designed", + throughRestsOn: point.outside === "that a component's unit satisfies" ? 1 : 0, + }; + } + return unspecified; +} +function observe(world: World): DesignSliceOutcome { + const answer = required(world.answer); + switch (world.outcomeKind) { + case ORDER: + return { + kind: ORDER, + order: [...answer.members] + .sort((a, b) => a.depth - b.depth) + .map((row) => row.id) + .join(", "), + }; + case COUNTS: + return { + kind: COUNTS, + cycles: answer.totals.cycles, + exampleMembers: answer.totals.exampleMembers, + }; + case DESIGN: { + const design = required(answer.architecture.designs[0]); + return { + kind: DESIGN, + realized: design.realized ? "realized" : "unrealized", + differing: design.differing.length, + unreadable: design.unreadable.length, + }; + } + case COMPONENTS: + return { + kind: COMPONENTS, + components: answer.architecture.components.map((row) => row.id).join(", "), + throughRestsOn: required(answer.totals.components.throughRestsOn), + }; + case OUTSIDE: + return { + kind: OUTSIDE, + outsideComponents: answer.architecture.outside.map((row) => row.id).join(", "), + }; + case HELD: + return { + kind: HELD, + held: required( + required(answer.boundary.restsOn).find((row) => row.id === sid("outside")), + ).holds.join(", "), + }; + default: + return unspecified; + } +} +const adapters = { + createWorld, + async invoke(world: World) { + world.answer = await slice(world.graph); + }, + observe, + expected, + assertions(world: World) { + const answer = required(world.answer); + if (world.point.structure === "rest on each other in a chain") { + expect(answer.scope).toMatchObject({ + pack: "pack:probe.slice", + subset: false, + members: [sid("top"), sid("base"), sid("design")], + unresolved: [], + }); + for (const member of answer.members) assertMemberKeys(member, true); + expect(answer.members[0]).toMatchObject({ + statedReadiness: "defined", + floorReached: "ready", + statedNextRung: "ready", + rules: { total: 1, keyed: 1 }, + examples: { total: 0, enabled: 0 }, + verifiers: { total: 0, enabled: 0 }, + }); + } + if (world.point.structure?.includes("decision")) { + expect(answer.decisions).toEqual([ + expect.objectContaining({ + id: sid("base"), + member: true, + shapes: [sid("top")], + refines: [], + supersedes: [], + supersededBy: [], + openQuestions: 0, + blockingQuestions: 0, + }), + ]); + } + if (world.point.structure?.includes("two members")) { + expect(answer.members.map((row) => row.depth)).toEqual([1, 1, 1]); + expect(answer.totals.depth).toBe(1); + } + if ( + world.point.structure === + "include an example that refines a member and depends on an outside Spec" + ) { + expect(answer.boundary.restsOn).toContainEqual( + expect.objectContaining({ + id: sid("outside"), + via: [{ type: "dependsOn", members: [sid("top"), sid("base")] }], + }), + ); + } + if (world.point.outside === "that a component's unit satisfies") { + expect(answer.architecture.outside).toContainEqual( + expect.objectContaining({ + id: cid("neighbor"), + anchored: true, + reachedBy: ["touchedUses", "usesTouched"], + }), + ); + expect(answer.architecture.outside.map((row) => row.id)).not.toContain(cid("far")); + expect(answer.architecture.components.map((row) => row.id)).not.toContain(cid("downstream")); + expect(answer.architecture.uses).toEqual([ + expect.objectContaining({ + from: cid("dependency"), + to: cid("neighbor"), + count: 1, + outside: true, + units: [{ from: cid("dependency"), to: cid("neighbor"), claim: "anchored" }], + }), + expect.objectContaining({ + from: cid("neighbor"), + to: cid("dependency"), + count: 1, + outside: true, + }), + ]); + } + if (world.point.anchor === "an anchor satisfies, with a differing layer and uses") + expect(answer.architecture.outside).toContainEqual({ + id: cid("missing"), + label: null, + layer: null, + context: null, + anchored: false, + reachedBy: ["plannedUse"], + }); + if (world.point.structure === "include an example that refines a member") { + expect(answer.members.find((row) => row.id === sid("top"))).toMatchObject({ + exampleOf: sid("base"), + holds: [expect.anything()], + }); + expect(answer.members.find((row) => row.id === sid("base"))).toMatchObject({ + exampleMembers: [sid("top")], + holds: [], + }); + } + if (world.point.outside === "stated below defined") + expect(answer.questions).toContainEqual( + expect.objectContaining({ + spec: sid("outside"), + origin: "restsOn", + blocking: true, + address: `${sid("outside")}#question.contract`, + holds: [sid("base")], + }), + ); + }, +}; +const outsideComponentsAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.outside-components"), + verifies: ref("spec:consumers.design-slice.outside-components"), +}); +void outsideComponentsAnchor; +registerOutsideComponents(adapters); +const chainAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.chain"), + verifies: ref("spec:consumers.design-slice.chain"), +}); +void chainAnchor; +registerChain(adapters); +const decisionBacklinkAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.decision-backlink"), + verifies: ref("spec:consumers.design-slice.decision-backlink"), +}); +void decisionBacklinkAnchor; +registerDecisionBacklink(adapters); +const dependencyCycleAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.dependency-cycle"), + verifies: ref("spec:consumers.design-slice.dependency-cycle"), +}); +void dependencyCycleAnchor; +registerDependencyCycle(adapters); +const foldedExampleAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.folded-example"), + verifies: ref("spec:consumers.design-slice.folded-example"), +}); +void foldedExampleAnchor; +registerFoldedExample(adapters); +const independentExampleAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.independent-example"), + verifies: ref("spec:consumers.design-slice.independent-example"), +}); +void independentExampleAnchor; +registerIndependentExample(adapters); +const componentAbsentAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.component-absent"), + verifies: ref("spec:consumers.design-slice.component-absent"), +}); +void componentAbsentAnchor; +registerComponentAbsent(adapters); +const componentUnrelatedAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.component-unrelated"), + verifies: ref("spec:consumers.design-slice.component-unrelated"), +}); +void componentUnrelatedAnchor; +registerComponentUnrelated(adapters); +const componentMatchingAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.component-matching"), + verifies: ref("spec:consumers.design-slice.component-matching"), +}); +void componentMatchingAnchor; +registerComponentMatching(adapters); +const componentDifferingAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.component-differing"), + verifies: ref("spec:consumers.design-slice.component-differing"), +}); +void componentDifferingAnchor; +registerComponentDiffering(adapters); +const componentUnreadableAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.component-unreadable"), + verifies: ref("spec:consumers.design-slice.component-unreadable"), +}); +void componentUnreadableAnchor; +registerComponentUnreadable(adapters); +const outsideSatisfiedAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.outside-satisfied"), + verifies: ref("spec:consumers.design-slice.outside-satisfied"), +}); +void outsideSatisfiedAnchor; +registerOutsideSatisfied(adapters); +const outsideUnboundAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.outside-unbound"), + verifies: ref("spec:consumers.design-slice.outside-unbound"), +}); +void outsideUnboundAnchor; +registerOutsideUnbound(adapters); +const outsideHoldingAnchor = specTest({ + id: testAnchorId("test:protocol.design-slice.outside-holding"), + verifies: ref("spec:consumers.design-slice.outside-holding"), +}); +void outsideHoldingAnchor; +registerOutsideHolding(adapters); + +describe("design slice composition", () => { + it("keeps unresolved targets and boundary relation reasons in the recipe answer", async () => { + const graph = sliceProbe(); + graph.nodes.push(primitive("outside")); + graph.edges.push( + link(sid("top"), "dependsOn", sid("outside"), "declared", "outside footing"), + link(sid("base"), "dependsOn", sid("outside")), + link(sid("top"), "constrainedBy", sid("missing"), "declared", "unresolved footing"), + ); + const full = await slice(graph); + expect(full.structure.unresolved).toEqual([ + { from: sid("top"), to: sid("missing"), type: "constrainedBy", reason: "unresolved footing" }, + ]); + expect(required(full.boundary.restsOn).find((row) => row.id === sid("outside"))?.via).toEqual([ + { + type: "dependsOn", + members: [sid("top"), sid("base")], + reasons: [{ member: sid("top"), reason: "outside footing" }], + }, + ]); + expect(required(full.boundary.restsOn).map((row) => row.id)).not.toContain(sid("missing")); + }); + it("keeps bindings to unresolved components unplaced rather than touching an invented component", async () => { + const graph = sliceProbe(); + graph.nodes.push({ + id: "impl:probe.unplaced", + nodeType: "CodeNode", + claim: "anchored", + file: "unplaced.ts", + line: 1, + }); + graph.edges.push( + link("impl:probe.unplaced", "satisfies", sid("top"), "anchored"), + link("impl:probe.unplaced", "references", sid("base"), "anchored"), + link("impl:probe.unplaced", "memberOf", cid("missing"), "anchored"), + ); + const answer = await slice(graph); + expect(answer.architecture.components).toEqual([]); + expect(answer.totals.components).toMatchObject({ touched: 0, throughMembers: 0 }); + expect(answer.architecture.unplaced).toEqual([ + { unit: "impl:probe.unplaced", member: sid("top"), via: "satisfies" }, + { unit: "impl:probe.unplaced", member: sid("base"), via: "references" }, + ]); + }); + it("pins every full and summary row shape to the carrying Design entries", async () => { + const graph = columnProbe(); + const full = await slice(graph); + const summary = await slice(graph, { pack: "pack:probe.slice" }); + expect(full.scope).toEqual({ + pack: "pack:probe.slice", + title: "Probe slice", + framing: null, + subset: false, + members: [sid("top"), sid("base"), sid("design"), sid("choice"), sid("waiting")], + unresolved: [sid("missing")], + }); + expect(summary.scope).toEqual(full.scope); + expect(summary.totals).toEqual(full.totals); + for (const [answer, detailed] of [ + [full, true], + [summary, false], + ] as const) { + assertKeys( + answer, + `scope totals members structure boundary architecture decisions questions${detailed ? "" : " leftOut"}`, + ); + assertKeys( + answer.totals, + "members exampleMembers relations depth cycles heldBelowNextRung waitingForAuthor restsOn holdingOutside restedOnBy verifiedBy supersededBy components designs componentUses decisions decisionsBelowReady openQuestions blockingQuestions", + ); + assertKeys( + answer.totals.components, + "touched throughMembers throughRestsOn throughDesigns outside", + ); + assertKeys(answer.totals.designs, "total realized matching unrealized differing unreadable"); + assertKeys(answer.totals.componentUses, "inside outside planned"); + for (const row of answer.members) { + assertMemberKeys(row, detailed); + assertKeys(row.design, "entries declarations openQuestions blockingQuestions decisions"); + assertKeys(row.rules, "total keyed"); + for (const hold of row.holds as { targets: unknown[] }[]) { + assertKeys(hold, detailed ? "clauseId description targets" : "clauseId targets"); + for (const target of hold.targets) + if (detailed) assertKeys(target, "type id statedReadiness"); + else expect(typeof target).toBe("string"); + } + if (detailed) { + assertKeys(row.examples, "total enabled"); + assertKeys(row.verifiers, "total enabled"); + } + } + for (const rows of Object.values(answer.boundary)) { + expect(rows.length).toBeGreaterThan(0); + for (const row of rows) + assertKeys( + row, + detailed + ? "id title specKind statedReadiness floorReached implemented via holds" + : "id statedReadiness holds", + ); + } + for (const row of answer.decisions) + assertKeys( + row, + detailed + ? "id title statedReadiness floorReached member shapes refines supersedes supersededBy openQuestions blockingQuestions" + : "id statedReadiness member shapes", + ); + for (const row of answer.questions) + assertKeys( + row, + detailed || row.blocking + ? "address spec origin index question blocking deferral decider links provisional trigger holds" + : "address spec index", + ); + assertKeys(answer.architecture, "components designs uses plannedUses outside unplaced"); + for (const row of answer.architecture.components) + assertKeys( + row, + detailed + ? "id label layer context members restsOn designs" + : "id layer context members restsOn designs", + ); + for (const row of answer.architecture.designs) + assertKeys( + row, + detailed + ? "spec componentId layer context role uses anchorExists realized differing unreadable" + : "spec componentId anchorExists realized differing unreadable", + ); + for (const row of answer.architecture.plannedUses) assertKeys(row, "design from to outside"); + for (const row of answer.architecture.outside) + assertKeys( + row, + detailed ? "id label layer context anchored reachedBy" : "id anchored reachedBy", + ); + for (const row of answer.architecture.unplaced) assertKeys(row, "unit member via"); + for (const row of answer.structure.edges) + assertKeys( + row, + detailed && row.reason !== undefined + ? "from to type orders reason" + : "from to type orders", + ); + } + const touched = createWorld({ + structure: "rest on each other in a chain", + outside: "that a component's unit satisfies", + anchor: "an anchor satisfies, matching every declared value", + }); + for (const detail of [true, false]) { + const answer = await slice(touched.graph, { pack: "pack:probe.slice", detail }); + expect(answer.architecture.uses.length).toBeGreaterThan(0); + for (const row of answer.architecture.uses) { + assertKeys(row, detail ? "from to count outside units" : "from to count outside"); + if (detail) for (const unit of row.units) assertKeys(unit, "from to claim"); + } + const narrowed = await slice(graph, { + pack: "pack:probe.slice", + members: [sid("top")], + detail, + }); + for (const row of required(narrowed.boundary.restsOn)) + assertKeys( + row, + detail + ? "id title specKind statedReadiness floorReached implemented via holds inPack" + : "id statedReadiness holds inPack", + ); + } + }); + it("reports outside holds, waiting statements, decision replacements and own-Spec question holds", async () => { + const graph = columnProbe(); + const full = await slice(graph); + const summary = await slice(graph, { pack: "pack:probe.slice" }); + expect(full.totals).toMatchObject({ + holdingOutside: 1, + decisionsBelowReady: 1, + heldBelowNextRung: 4, + waitingForAuthor: 1, + supersededBy: 1, + }); + const pack = await catalogRecipe<{ + totals: { heldBelowNextRung: number; waitingForAuthor: number }; + }>(29, graph, { pack: "pack:probe.slice", detail: true }); + expect(full.totals).toMatchObject({ + heldBelowNextRung: pack.totals.heldBelowNextRung, + waitingForAuthor: pack.totals.waitingForAuthor, + }); + expect(full.decisions).toEqual([ + expect.objectContaining({ + id: sid("choice"), + supersedes: [sid("old")], + supersededBy: [sid("new")], + shapes: [sid("top")], + }), + ]); + expect(summary.decisions).toEqual([ + { id: sid("choice"), statedReadiness: "scoped", member: true, shapes: 1 }, + ]); + expect(full.questions.find((row) => row.spec === sid("choice") && row.blocking)).toEqual({ + address: `${sid("choice")}#question.contract`, + spec: sid("choice"), + origin: "member", + index: 0, + question: "Which contract?", + blocking: true, + deferral: true, + decider: "owner", + links: [sid("base")], + provisional: "Use the base.", + trigger: "The contract is chosen.", + holds: [sid("choice"), sid("top")], + }); + expect(summary.questions.find((row) => row.spec === sid("choice") && !row.blocking)).toEqual({ + address: null, + spec: sid("choice"), + index: 1, + }); + expect(full.boundary.supersededBy).toEqual([ + expect.objectContaining({ + id: sid("new"), + holds: [], + via: [{ type: "supersedes", members: [sid("choice")] }], + }), + ]); + expect(full.members.find((row) => row.id === sid("base"))).toMatchObject({ + examples: { total: 2, enabled: 1 }, + verifiers: { total: 3, enabled: 2 }, + }); + }); + it.each([ + ["rest on each other in a chain", "setDepth[index] = 1;"], + ["include a decision that refines the member it shapes", "setDepth[index] = 1;"], + ["include a decision that refines the member it shapes", "edge.orders = true;"], + ] as const)( + "the executable order observation rejects %s with %s", + async (structure, mutation) => { + const world = createWorld({ + structure, + outside: "that no code satisfies", + anchor: "an anchor satisfies, matching every declared value", + }); + const body = required( + recipeFiles( + readFileSync(new URL("../docs/agent-surface/recipes.md", import.meta.url), "utf8"), + ).find((row) => row.ordinal === 54), + ).content; + const mutated = mutation.startsWith("setDepth") + ? body.replace("setDepth[index] = deepest + 1;", mutation) + : body.replace( + 'edge.orders = !(edge.type === "refines" && names(edge.to, edge.from, typed.slice(1)));', + mutation, + ); + expect(mutated).not.toBe(body); + world.answer = (await evaluateBody(mutated, { + graph: world.graph, + params: { pack: "pack:probe.slice", detail: true }, + })) as Slice; + expect(observe(world)).not.toEqual(expected(world.point)); + }, + ); + it.each([ + ["rest on each other in a chain", "scope"], + ["rest on each other in a chain", "memberColumns"], + ["include a decision that refines the member it shapes", "decisionsInScope"], + ["include an example that refines a member and depends on an outside Spec", "boundary"], + ["include two members that depend on each other", "depth"], + ] as const)( + "the executable assertions reject missing %s evidence for %s", + async (structure, rule) => { + const world = createWorld({ + structure, + outside: "that no code satisfies", + anchor: "an anchor satisfies, matching every declared value", + }); + world.answer = await slice(world.graph); + if (rule === "scope") world.answer.scope.members = []; + if (rule === "memberColumns") delete (world.answer.members[0] as Partial).rules; + if (rule === "decisionsInScope") world.answer.decisions = []; + if (rule === "boundary") world.answer.boundary.restsOn = []; + if (rule === "depth") for (const row of world.answer.members) row.depth = 7; + expect(() => { + adapters.assertions(world); + }).toThrow(); + }, + ); + it("counts only example-kind declared verifiers as examples, keeping all verifiers", async () => { + const graph = sliceProbe(); + graph.nodes.push( + primitive("enabled", { specKind: "example" }), + primitive("disabled", { specKind: "example" }), + primitive("behavior-verifier"), + { + id: "test:probe.enabled", + nodeType: "Anchor", + claim: "anchored", + file: "enabled.test.ts", + line: 1, + }, + { + id: "test:probe.direct", + nodeType: "Anchor", + claim: "anchored", + file: "direct.test.ts", + line: 1, + }, + ); + for (const name of ["enabled", "disabled", "behavior-verifier"]) + graph.edges.push(link(sid(name), "verifies", sid("base"))); + graph.edges.push( + link("test:probe.enabled", "verifies", sid("enabled"), "anchored"), + link("test:probe.direct", "verifies", sid("base"), "anchored"), + ); + const answer = await slice(graph); + const pack = await catalogRecipe<{ + members: { id: string; examples: unknown; verifiers: unknown }[]; + }>(29, graph, { pack: "pack:probe.slice", detail: true }); + const member = required(answer.members.find((row) => row.id === sid("base"))); + expect(member.examples).toEqual({ total: 2, enabled: 1 }); + expect(member.verifiers).toEqual({ total: 4, enabled: 2 }); + expect(member.examples).toEqual( + required(pack.members.find((row) => row.id === sid("base"))).examples, + ); + }); + it("names the hold detail and decision shape lists the summary omits", async () => { + const summary = await slice(sliceProbe(), { pack: "pack:probe.slice" }); + expect(summary.leftOut?.[0]).toMatch(/hold descriptions.*target types.*rungs/u); + expect(summary.leftOut?.[7]).toMatch(/shaped member ids.*count kept/u); + }); + it("summarizes component and design rows without losing totals", async () => { + const world = createWorld({ + structure: "rest on each other in a chain", + outside: "that a component's unit satisfies", + anchor: "an anchor satisfies, matching every declared value", + }); + const full = await slice(world.graph); + const summary = await slice(world.graph, { pack: "pack:probe.slice" }); + expect(summary.totals).toEqual(full.totals); + expect(summary.architecture.components).toEqual([ + { + id: cid("dependency"), + layer: "domain", + context: "probe", + members: { satisfies: 0, references: 0 }, + restsOn: 1, + designs: 0, + }, + { + id: cid("designed"), + layer: "domain", + context: "probe", + members: { satisfies: 1, references: 0 }, + restsOn: 0, + designs: 1, + }, + ]); + expect(summary.architecture.designs).toEqual([ + { + spec: sid("design"), + componentId: cid("designed"), + anchorExists: true, + realized: true, + differing: 0, + unreadable: 0, + }, + ]); + expect(summary.architecture.outside).toEqual([ + { id: cid("neighbor"), anchored: true, reachedBy: ["touchedUses", "usesTouched"] }, + ]); + for (const pair of summary.architecture.uses) expect(pair).not.toHaveProperty("units"); + }); + it("keeps all binding paths, unplaced units, pair counts and planned uses apart", async () => { + const graph = sliceProbe(); + graph.nodes.push( + component("local"), + component("outside"), + primitive("design", { + specKind: "model", + sections: { + design: { + id: cid("local"), + uses: `${cid("local")}, ${cid("future")}, ${cid("missing")}`, + }, + }, + }), + primitive("future", { specKind: "model", sections: { design: { id: cid("future") } } }), + ); + for (const name of ["one", "two", "bare"]) + graph.nodes.push({ + id: `impl:probe.${name}`, + nodeType: "CodeNode", + claim: "anchored", + file: `${name}.ts`, + line: 1, + }); + graph.edges.push( + link("impl:probe.one", "memberOf", cid("local"), "anchored"), + link("impl:probe.two", "memberOf", cid("local"), "anchored"), + link("impl:probe.one", "satisfies", sid("top"), "anchored"), + link("impl:probe.two", "references", sid("base"), "anchored"), + link("impl:probe.bare", "references", sid("top"), "anchored"), + link(cid("local"), "satisfies", sid("design"), "anchored"), + link("impl:probe.one", "uses", cid("outside"), "anchored"), + link("impl:probe.two", "uses", cid("outside"), "anchored"), + link("impl:probe.one", "uses", "impl:probe.two", "anchored"), + ); + setMembers(graph, ["top", "base", "design", "future"]); + const answer = await slice(graph); + expect(answer.architecture.components).toEqual([ + expect.objectContaining({ + id: cid("local"), + members: { satisfies: [sid("top"), sid("design")], references: [sid("base")] }, + restsOn: [], + designs: [sid("design")], + }), + ]); + expect(answer.architecture.unplaced).toEqual([ + { unit: "impl:probe.bare", member: sid("top"), via: "references" }, + ]); + expect(answer.architecture.uses).toEqual([ + { + from: cid("local"), + to: cid("outside"), + count: 2, + outside: true, + units: [ + { from: "impl:probe.one", to: cid("outside"), claim: "anchored" }, + { from: "impl:probe.two", to: cid("outside"), claim: "anchored" }, + ], + }, + ]); + expect(answer.architecture.plannedUses).toEqual([ + { design: sid("design"), from: cid("local"), to: cid("future"), outside: false }, + { design: sid("design"), from: cid("local"), to: cid("missing"), outside: true }, + ]); + expect(answer.totals.componentUses).toEqual({ inside: 0, outside: 1, planned: 2 }); + expect(answer.members[0]).toMatchObject({ + implementedBy: ["impl:probe.one"], + referencedBy: ["impl:probe.bare"], + components: [cid("local")], + }); + }); + it("reads outside decisions and orders their questions before non-blocking member questions", async () => { + const graph = sliceProbe(); + graph.nodes.push( + primitive("choice", { + specKind: "decision", + readiness: "scoped", + sections: { + intent: { + outcome: "Choose the shared contract.", + openQuestions: [ + { key: "choice", question: "Which contract?", blocking: true, decider: "owner" }, + { question: "Which label?", blocking: false }, + ], + }, + decision: { context: "A contract is unsettled.", decision: "Choose it together." }, + }, + }), + ); + graph.nodes[0] = primitive("base", { + sections: { + intent: { + outcome: "Read the base.", + openQuestions: [{ question: "Which spelling?", blocking: false }], + }, + behavior: { + rules: ["Read the base."], + flows: ["Read before returning."], + ruleKeys: { read: "rules[0]" }, + }, + }, + }); + graph.edges.push( + link(sid("top"), "decidedBy", sid("choice")), + link(sid("choice"), "refines", sid("top")), + ); + const answer = await slice(graph); + expect(answer.decisions).toEqual([ + expect.objectContaining({ + id: sid("choice"), + member: false, + shapes: [sid("top")], + refines: [], + openQuestions: 2, + blockingQuestions: 1, + }), + ]); + expect(answer.questions[0]).toMatchObject({ + spec: sid("choice"), + origin: "decision", + holds: [sid("top")], + address: `${sid("choice")}#question.choice`, + }); + const coverage = await catalogRecipe<{ totals: { keyedRules: number; unkeyedRules: number } }>( + 34, + graph, + { spec: sid("base") }, + ); + expect(answer.members[1]?.rules).toEqual({ + total: coverage.totals.keyedRules + coverage.totals.unkeyedRules, + keyed: coverage.totals.keyedRules, + }); + }); + it.each([ + { + layer: "`domain` explanation", + context: "``probe`` explanation", + role: "service", + uses: "`none` explanation", + }, + { layer: "Domain", context: "Bad", role: "bad_role", uses: "component:probe.bad, invalid" }, + { uses: "component:probe.z, component:probe.a, component:probe.z" }, + {}, + ])("reads declared values with the realization comparison grammar: %j", async (entries) => { + const graph = designProbe("an anchor holds without satisfying the design"); + graph.nodes = graph.nodes.map((node) => + node.id === sid("design") && node.nodeType === "Primitive" + ? { + ...node, + sections: { design: { id: "``component:probe.designed`` rationale", ...entries } }, + } + : node, + ); + const answer = await slice(graph); + const comparison = await catalogRecipe<{ + unrealized: { layer: unknown; context: unknown; role: unknown; uses: unknown }[]; + unreadable: { key: string; value: unknown }[]; + }>(43, graph); + const { layer, context, role, uses } = required(comparison.unrealized[0]); + expect(answer.architecture.designs[0]).toMatchObject({ layer, context, role, uses }); + expect(answer.architecture.designs[0]?.unreadable).toEqual( + comparison.unreadable.map(({ key, value }) => ({ key, value })), + ); + }); +}); diff --git a/test/self-hosting-graph.test.ts b/test/self-hosting-graph.test.ts index 50f646d6c..df40b81ce 100644 --- a/test/self-hosting-graph.test.ts +++ b/test/self-hosting-graph.test.ts @@ -44,6 +44,8 @@ import { expectedDesignManagementPackMembers, expectedMemberOfEdges, expectedPackMembers, + expectedRoadmapPackMembers, + expectedRoadmapsAndSlicesPackMembers, expectedStudioPackMembers, expectedPacks, expectedSpecs, @@ -87,7 +89,11 @@ const structuralSelfBindingTestAnchor = specTest({ id: testAnchorId("test:protocol.structural-self-binding"), label: "checks the accepted membership and helper coverage, authored uses, and pinned anchor roles, layers and contexts", - verifies: [ref("spec:protocol.structural-self-binding"), ref("spec:model.structural-patterns")], + verifies: [ + ref("spec:protocol.structural-self-binding"), + ref("spec:model.structural-patterns"), + ref("spec:protocol.significant-units-review"), + ], }); void structuralSelfBindingTestAnchor; @@ -134,6 +140,13 @@ const selfHostingImplementationAnchor = codeAnchor({ }); void selfHostingImplementationAnchor; +const roleVocabularyImplementationAnchor = codeAnchor({ + id: codeAnchorId("impl:protocol.role-vocabulary"), + label: "asserts realization of the role vocabulary in the engine anchors' pinned roles", + satisfies: ref("spec:protocol.role-vocabulary"), +}); +void roleVocabularyImplementationAnchor; + // Given: the repository root with evidence and the worked example excluded from the authored model. // The corpus walk is this suite's one expensive step, so it runs exactly once and every assertion // below reads the same derived graph: splitting the oracle by law never multiplies extraction. @@ -212,12 +225,97 @@ function compareCoverageMismatch( ); } +function structuralRosterMismatches(graph: typeof result.graph) { + const acceptedIds = new Set(acceptedArchitecturalUnits.map((row) => row.anchorId)); + const exceptionIds = new Set(structuralMembershipExceptions); + const componentIds = new Set(expectedComponentIds); + const mismatches: { + unit: string; + anchorId: string; + target: string; + reason: string; + }[] = []; + + for (const row of acceptedArchitecturalUnits) { + const path = row.unit.slice(0, row.unit.indexOf("#")); + const node = graph.nodes.find((entry) => entry.id === row.anchorId); + const memberships = graph.edges.filter( + (edge) => edge.from === row.anchorId && edge.type === "memberOf", + ); + const target = memberships[0]?.to ?? ""; + + if (node?.nodeType !== "CodeNode") { + mismatches.push({ + unit: row.unit, + anchorId: row.anchorId, + target, + reason: "anchor is not a CodeNode", + }); + continue; + } + + if (node.file !== path) { + mismatches.push({ + unit: row.unit, + anchorId: row.anchorId, + target, + reason: `file ${node.file} !== ${path}`, + }); + } + + if (memberships.length !== 1) { + mismatches.push({ + unit: row.unit, + anchorId: row.anchorId, + target, + reason: `memberOf count ${String(memberships.length)}`, + }); + } else { + if (target !== row.componentId) { + mismatches.push({ + unit: row.unit, + anchorId: row.anchorId, + target, + reason: `memberOf target ${target} !== ${row.componentId}`, + }); + } + + if (!componentIds.has(target)) { + mismatches.push({ + unit: row.unit, + anchorId: row.anchorId, + target, + reason: "target not in expectedComponentIds", + }); + } + } + } + + for (const edge of graph.edges.filter((entry) => entry.type === "memberOf")) { + if (!acceptedIds.has(edge.from) && !exceptionIds.has(edge.from)) { + const node = graph.nodes.find((entry) => entry.id === edge.from); + mismatches.push({ + unit: node && "file" in node ? node.file : edge.from, + anchorId: edge.from, + target: edge.to, + reason: "unrostered memberOf", + }); + } + } + + return mismatches; +} + describe("the self-hosting corpus", () => { it.each([ { file: "test/self-hosting-oracle/structural-edges.ts", anchor: "impl:protocol.structural-self-binding", - specs: ["spec:protocol.structural-self-binding", "spec:model.structural-patterns"], + specs: [ + "spec:protocol.structural-self-binding", + "spec:model.structural-patterns", + "spec:protocol.significant-units-review", + ], }, { file: "test/helpers/validator-probes.ts", @@ -260,16 +358,13 @@ 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 from the refreshed tree: 401 Markdown Specs and four Gherkin Specs, - // 405 in the self-hosting Pack; seven Pack manifests and 543 source anchors. - // The excluded-root validate run corroborates 955 nodes and 2558 edges. - expect(result.counts).toEqual({ specs: 405, packs: 7, anchors: 543 }); - expect(expectedSpecs).toHaveLength(405); - expect(expectedPackMembers).toHaveLength(405); - expect(expectedAnchors).toHaveLength(543); - expect(expectedBindingEdges).toHaveLength(681); - expect(result.graph.nodes).toHaveLength(955); - expect(result.graph.edges).toHaveLength(2558); + expect(result.counts).toEqual({ specs: 456, packs: 9, anchors: 607 }); + expect(expectedSpecs).toHaveLength(456); + expect(expectedPackMembers).toHaveLength(456); + expect(expectedAnchors).toHaveLength(607); + expect(expectedBindingEdges).toHaveLength(810); + expect(result.graph.nodes).toHaveLength(1072); + expect(result.graph.edges).toHaveLength(2978); }); it("permits a named second diagram view in the authored element-id contract", () => { @@ -353,7 +448,7 @@ describe("the self-hosting corpus", () => { }), {}, ), - ).toEqual({ idea: 5, ready: 399, scoped: 1 }); + ).toEqual({ idea: 5, ready: 447, scoped: 4 }); }); it("derives the Pack membership edges from the manifest, in manifest order", () => { @@ -376,6 +471,12 @@ describe("the self-hosting corpus", () => { "declared", ]), ...expectedObsidianVaultPackMembers.map((id) => [id, "pack:obsidian-vault-v1", "declared"]), + ...expectedRoadmapPackMembers.map((id) => [id, "pack:roadmap", "declared"]), + ...expectedRoadmapsAndSlicesPackMembers.map((id) => [ + id, + "pack:roadmaps-and-slices-v1", + "declared", + ]), ...expectedPackMembers.map((id) => [id, "pack:self-hosting-v1", "declared"]), ]); }); @@ -406,7 +507,7 @@ describe("the self-hosting corpus", () => { it("gives every owned impl/api CodeNode exactly one component", () => { // Re-measured from the refreshed tree. - expect(expectedMemberOfEdges).toHaveLength(167); + expect(expectedMemberOfEdges).toHaveLength(182); const exceptions = new Set(structuralMembershipExceptions); const codeUnits = result.graph.nodes.filter( (node) => @@ -431,7 +532,7 @@ describe("the self-hosting corpus", () => { it("derives exactly the sparse authored component uses edges", () => { // Re-measured from the refreshed tree. - expect(expectedUsesEdges).toHaveLength(48); + expect(expectedUsesEdges).toHaveLength(57); expect( result.graph.edges .filter((edge) => edge.type === "uses") @@ -441,82 +542,7 @@ describe("the self-hosting corpus", () => { }); it("covers every accepted architecturally significant unit", () => { - const acceptedIds = new Set(acceptedArchitecturalUnits.map((row) => row.anchorId)); - const exceptionIds = new Set(structuralMembershipExceptions); - const componentIds = new Set(expectedComponentIds); - const mismatches: { - unit: string; - anchorId: string; - target: string; - reason: string; - }[] = []; - - for (const row of acceptedArchitecturalUnits) { - const path = row.unit.slice(0, row.unit.indexOf("#")); - const node = result.graph.nodes.find((entry) => entry.id === row.anchorId); - const memberships = result.graph.edges.filter( - (edge) => edge.from === row.anchorId && edge.type === "memberOf", - ); - const target = memberships[0]?.to ?? ""; - - if (node?.nodeType !== "CodeNode") { - mismatches.push({ - unit: row.unit, - anchorId: row.anchorId, - target, - reason: "anchor is not a CodeNode", - }); - continue; - } - - if (node.file !== path) { - mismatches.push({ - unit: row.unit, - anchorId: row.anchorId, - target, - reason: `file ${node.file} !== ${path}`, - }); - } - - if (memberships.length !== 1) { - mismatches.push({ - unit: row.unit, - anchorId: row.anchorId, - target, - reason: `memberOf count ${String(memberships.length)}`, - }); - } else { - if (target !== row.componentId) { - mismatches.push({ - unit: row.unit, - anchorId: row.anchorId, - target, - reason: `memberOf target ${target} !== ${row.componentId}`, - }); - } - - if (!componentIds.has(target)) { - mismatches.push({ - unit: row.unit, - anchorId: row.anchorId, - target, - reason: "target not in expectedComponentIds", - }); - } - } - } - - for (const edge of result.graph.edges.filter((entry) => entry.type === "memberOf")) { - if (!acceptedIds.has(edge.from) && !exceptionIds.has(edge.from)) { - const node = result.graph.nodes.find((entry) => entry.id === edge.from); - mismatches.push({ - unit: node && "file" in node ? node.file : edge.from, - anchorId: edge.from, - target: edge.to, - reason: "unrostered memberOf", - }); - } - } + const mismatches = structuralRosterMismatches(result.graph); for (const row of coarseGrainCoverage) { const memberships = result.graph.edges.filter( @@ -544,7 +570,12 @@ describe("the self-hosting corpus", () => { }); } - const coverage = auditStructuralCoverage(structuralCoverageProgram, row.unit, covering.file); + const coverage = auditStructuralCoverage( + structuralCoverageProgram, + row.unit, + covering.file, + "consumption" in row ? row.consumption : "value", + ); if (coverage !== "ok") { mismatches.push({ unit: row.unit, @@ -564,9 +595,73 @@ describe("the self-hosting corpus", () => { expect(mismatches, message).toEqual([]); }); + it("gives every accepted unit a readable responsibility label", () => { + for (const anchorId of [ + ...acceptedArchitecturalUnits.map((row) => row.anchorId), + ...expectedComponentIds, + ]) { + const node = result.graph.nodes.find((entry) => entry.id === anchorId); + const label = node && "label" in node ? node.label?.trim() : undefined; + expect(label, anchorId).toBeTruthy(); + if (anchorId.startsWith("component:")) { + expect(label, anchorId).not.toMatch(/^Protocol .* seam$/u); + } + } + }); + + it("detects a missing accepted member independently of the source", () => { + const id = "impl:protocol.parameter-codec"; + const graph = { + ...result.graph, + nodes: result.graph.nodes.filter((node) => node.id !== id), + edges: result.graph.edges.filter((edge) => edge.from !== id && edge.to !== id), + }; + expect(structuralRosterMismatches(graph)).toContainEqual({ + unit: "src/cli/params.ts#mergeParams", + anchorId: id, + target: "", + reason: "anchor is not a CodeNode", + }); + }); + + it("detects extra unrostered membership independently of the source", () => { + const graph: typeof result.graph = { + ...result.graph, + nodes: [ + ...result.graph.nodes, + { + id: "impl:protocol.unreviewed-helper", + nodeType: "CodeNode", + claim: "anchored", + file: "src/cli/params.ts", + line: 1, + role: "codec", + }, + ], + edges: [ + ...result.graph.edges, + { + from: "impl:protocol.unreviewed-helper", + to: "component:protocol.cli", + type: "memberOf", + claim: "anchored", + }, + ], + }; + expect(validateGraph(graph).findings.filter((finding) => finding.severity === "error")).toEqual( + [], + ); + expect(structuralRosterMismatches(graph)).toContainEqual({ + unit: "src/cli/params.ts", + anchorId: "impl:protocol.unreviewed-helper", + target: "component:protocol.cli", + reason: "unrostered memberOf", + }); + }); + it("finds every converted comment anchor at its block's first line", () => { // Re-measured from the refreshed tree. - expect(expectedCommentAnchorIds).toHaveLength(93); + expect(expectedCommentAnchorIds).toHaveLength(118); expect( expectedAnchors .filter((anchor) => anchor.constant === undefined) @@ -651,9 +746,7 @@ describe("the self-hosting corpus", () => { expect(anchorLine).toBeGreaterThan(0); expect(siteLine, anchor.id).toBeGreaterThan(0); - // The build-command constants precede the Studio invalidation block beside runBuild. - // Preserve the tighter bound for every other declaration group. - const maxDistance = anchor.file === "src/cli/build-command.ts" ? 36 : 29; + const maxDistance = 29; expect(Math.abs(anchorLine - siteLine), anchor.id).toBeLessThanOrEqual(maxDistance); expect(node).toMatchObject({ file: anchor.file, line: anchorLine, claim: "anchored" }); } diff --git a/test/self-hosting-oracle/anchors.ts b/test/self-hosting-oracle/anchors.ts index cc96bccd0..9d3446b75 100644 --- a/test/self-hosting-oracle/anchors.ts +++ b/test/self-hosting-oracle/anchors.ts @@ -20,6 +20,472 @@ export interface ExpectedAnchor { // re-measured under plan 41 export const expectedAnchors: readonly ExpectedAnchor[] = [ + { + id: "test:protocol.vault-priority", + nodeType: "Anchor", + file: "test/vault.test.ts", + constant: "priorityNoteTest", + site: 'it("changes the Pack note\'s meaning when only advisory priority changes",', + }, + { + id: "test:protocol.studio-diagram-catalog", + nodeType: "Anchor", + file: "test/studio-diagram-catalog.test.ts", + constant: "catalog", + site: 'describe("the shipped diagram catalog",', + }, + + { + id: "impl:protocol.roadmap-practice", + nodeType: "CodeNode", + label: "realizes the roadmap handbook and its current-record gate", + file: "test/check-self-hosting-gates.test.ts", + constant: "roadmapPractice", + site: 'describe("the self-hosting records gate",', + role: "validator", + }, + { + id: "test:protocol.roadmap-practice", + nodeType: "Anchor", + file: "test/check-self-hosting-gates.test.ts", + constant: "roadmapPracticeTest", + site: 'describe("the self-hosting records gate",', + }, + + { + id: "impl:protocol.studio-capabilities-page", + nodeType: "CodeNode", + label: "renders epic evidence, refinement and unreached Specs from the capability map", + file: "src/projections/studio/pages/capabilities-page.ts", + site: "export function capabilitiesPage(", + role: "projection", + }, + { + id: "impl:protocol.studio-capability-diagrams", + nodeType: "CodeNode", + label: "places refinement occurrences and bounded branches from the capability map", + file: "src/projections/studio/diagrams/capability.ts", + site: "export function capabilityDiagrams(", + role: "projection", + }, + { + id: "test:protocol.studio-capabilities", + nodeType: "Anchor", + file: "test/studio-capabilities.test.ts", + constant: "capabilitiesAnchor", + site: 'describe("Studio capabilities",', + }, + + { + id: "impl:protocol.studio-roadmap-page", + nodeType: "CodeNode", + label: "renders roadmap needs, holding parts and the shared Markdown memo", + file: "src/projections/studio/pages/roadmap-page.ts", + site: "export function roadmapPage(", + role: "projection", + }, + { + id: "test:protocol.studio-roadmap-page", + nodeType: "Anchor", + file: "test/studio-roadmap.test.ts", + site: "async function page(", + }, + { + id: "impl:protocol.studio-slice-answer", + nodeType: "CodeNode", + file: "src/projections/studio/slice-answer.ts", + site: "export interface SliceMember", + role: "contract", + label: "full design slice answer consumed by the page and drawings", + }, + { + id: "impl:protocol.studio-slice-dependencies", + nodeType: "CodeNode", + file: "src/projections/studio/diagrams/slice.ts", + site: "export function dependencyDiagram(", + role: "projection", + label: "supplied depth bands with protected boundary boxes and lossless text", + }, + { + id: "impl:protocol.studio-slice-architecture", + nodeType: "CodeNode", + file: "src/projections/studio/diagrams/slice.ts", + site: "export function scopedArchitectureDiagrams(", + role: "projection", + label: "selected component grid with distinct planned identities and complete unit edges", + }, + { + id: "impl:protocol.studio-slice-page", + nodeType: "CodeNode", + file: "src/projections/studio/pages/slice-page.ts", + site: "export function slicePage(", + role: "projection", + label: "design slice summaries, member needs, architecture comparisons and boundary tables", + }, + { + id: "test:protocol.studio-slice-page", + nodeType: "Anchor", + file: "test/studio-slice.test.ts", + constant: "sliceAnchor", + site: 'describe("slice page",', + }, + { + id: "test:protocol.studio-slice-dependency", + nodeType: "Anchor", + file: "test/studio-slice.test.ts", + constant: "dependencyAnchor", + site: 'describe("slice dependency drawing",', + }, + { + id: "test:protocol.studio-slice-architecture", + nodeType: "Anchor", + file: "test/studio-slice.test.ts", + constant: "architectureAnchor", + site: 'describe("scoped architecture drawing",', + }, + + { + id: "test:protocol.studio-architecture-maturity-requirement", + nodeType: "Anchor", + file: "test/studio-panels.test.ts", + constant: "panels", + site: 'describe("architecture tables and register",', + }, + { + id: "impl:protocol.studio-requirement-view", + nodeType: "CodeNode", + file: "src/projections/studio/pages/requirement-view.ts", + site: "export function requirementView(", + label: "selects Spec refiners, verifying examples and their recorded bindings", + role: "projection", + }, + { + id: "impl:protocol.studio-requirement-diagram", + nodeType: "CodeNode", + file: "src/projections/studio/diagrams/requirement.ts", + site: "export function requirementDiagram(", + label: + "folds requirement bindings by relation direction and claim and serializes exact relations", + role: "projection", + }, + { + id: "impl:protocol.studio-unit-register", + nodeType: "CodeNode", + file: "src/projections/studio/pages/architecture-register.ts", + site: "export function unitRegister(", + label: "renders component source bindings and role-grouped recorded units", + role: "projection", + }, + { + id: "test:protocol.roadmap-priority", + nodeType: "Anchor", + label: "checks Pack priority extraction, comparison and Studio change notes", + file: "test/roadmap-priority.test.ts", + constant: "priorityAnchor", + site: 'describe("roadmap priority manifest",', + }, + { + id: "impl:protocol.build-args", + nodeType: "CodeNode", + file: "src/cli/build-args.ts", + site: "export interface BuildArgs", + role: "codec", + label: "shared build arguments and corpus-rule inputs", + }, + { + id: "impl:protocol.parameter-codec", + nodeType: "CodeNode", + file: "src/cli/params.ts", + site: "export function mergeParams(", + role: "codec", + label: "decodes recipe parameters and refuses duplicate keys", + }, + { + id: "impl:protocol.corpus-rule-runner", + nodeType: "CodeNode", + file: "src/cli/corpus-rules.ts", + site: "export async function runCorpusRules(", + role: "service", + label: "runs selected corpus rules and separates their failures", + }, + { + id: "impl:protocol.markdown-yaml-policy", + nodeType: "CodeNode", + file: "src/extract/markdown-yaml-policy.ts", + site: "export function isReservedEnvelopeKey(", + role: "codec", + label: "bounded YAML scalars and relation-target forms", + }, + { + id: "impl:protocol.markdown-content", + nodeType: "CodeNode", + file: "src/extract/markdown-body-content.ts", + site: "export interface MarkdownLine", + role: "codec", + label: "decodes located Markdown entries and typed fences", + }, + { + id: "impl:protocol.gherkin-kind-policy", + nodeType: "CodeNode", + file: "src/extract/gherkin-kind-honesty.ts", + site: "export const GHERKIN_KIND_LIE_REASONS", + role: "decider", + label: "shared Gherkin kind-refusal and loss explanations", + }, + { + id: "impl:protocol.import-fidelity", + nodeType: "CodeNode", + file: "src/import/markdown-fidelity.ts", + site: "export function assertMarkdownEmissionFidelity(", + role: "validator", + label: "refuses Markdown emission that changes authored meaning", + }, + { + id: "impl:protocol.studio-diagram-model", + nodeType: "CodeNode", + file: "src/projections/studio/diagrams/model.ts", + site: "export interface DiagramBox", + role: "contract", + label: "shared diagram boxes, edges and drawing bounds", + }, + { + id: "impl:protocol.studio-html", + nodeType: "CodeNode", + file: "src/projections/studio/pages/html.ts", + site: "export interface PageContext", + role: "codec", + label: "shared Studio HTML, source and link encoding", + }, + { + id: "impl:protocol.studio-inputs", + nodeType: "CodeNode", + file: "src/projections/studio/inputs.ts", + site: "export interface StudioInputs", + role: "contract", + label: "named base graph and run input contract", + }, + { + id: "impl:protocol.studio-proposal-contract", + nodeType: "CodeNode", + file: "src/studio-elements/proposal.ts", + site: "export interface ProposalChange", + role: "contract", + label: "browser proposal shape and schema checks", + }, + { + id: "impl:protocol.design-review-codec", + nodeType: "CodeNode", + file: "src/projections/design-review-markdown.ts", + site: "export function pagePathOf(", + role: "codec", + label: "shared Design Review Markdown paths and links", + }, + { + id: "impl:protocol.studio-register-contracts", + nodeType: "CodeNode", + file: "src/projections/studio/pages/register-answers.ts", + site: "interface RegisterAnswers", + role: "contract", + label: "shared recipe answer shapes for Studio registers", + }, + { + id: "impl:protocol.studio-page-catalog", + nodeType: "CodeNode", + file: "src/projections/studio/pages/index.ts", + site: "export interface StudioPage", + role: "projection", + label: "selects Studio pages, optional inputs and Pack exports", + }, + { + id: "impl:protocol.studio-diagram-layout", + nodeType: "CodeNode", + label: "title-first box placement, wrapping and band sizing for desktop and phone", + file: "src/projections/studio/diagrams/layout.ts", + site: "export function layoutDiagram(", + role: "projection", + }, + { + id: "impl:protocol.studio-diagram-routing", + nodeType: "CodeNode", + label: "port, channel and overflow-rail edge routes around boxes and labels", + file: "src/projections/studio/diagrams/routing.ts", + site: "export function routeDiagram(", + role: "projection", + }, + { + id: "impl:protocol.studio-diagram-source", + nodeType: "CodeNode", + label: "escaped Mermaid flowchart and mindmap source in placed box order", + file: "src/projections/studio/diagrams/mermaid.ts", + site: "export function mermaidSource(", + role: "projection", + }, + { + id: "test:protocol.studio-diagram-core", + nodeType: "Anchor", + file: "test/studio-diagram-core.test.ts", + constant: "core", + site: 'describe("shared diagram core",', + }, + { + id: "test:protocol.studio-copy-source", + nodeType: "Anchor", + file: "test/studio-diagram-core.test.ts", + constant: "copyAnchor", + site: 'it("copies the current value of the labelled textarea and selects that same area on clipboard failure",', + }, + { + id: "impl:protocol.capability-map-recipe", + nodeType: "CodeNode", + file: "test/recipes.test.ts", + constant: "capabilityMapRecipeImplementationAnchor", + site: 'describe("the capability map recipe",', + role: "projection", + label: "asserts realization of The capability map in docs/agent-surface/recipes.md", + }, + { + id: "test:protocol.capability-map", + nodeType: "Anchor", + file: "test/recipes.test.ts", + constant: "capabilityMapRecipeTestAnchor", + site: 'describe("the capability map recipe",', + }, + { + id: "impl:protocol.capability-epics", + nodeType: "CodeNode", + file: "test/recipes.test.ts", + constant: "capabilityEpicsImplementationAnchor", + site: 'describe("the Protocol\'s capability epics",', + label: "asserts realization of the capability epics in the shipped corpus's refinement", + }, + { + id: "test:protocol.capability-epics", + nodeType: "Anchor", + file: "test/recipes.test.ts", + constant: "capabilityEpicsTestAnchor", + site: 'describe("the Protocol\'s capability epics",', + }, + { + id: "test:protocol.design-slice.outside-components", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "outsideComponentsAnchor", + site: "registerOutsideComponents(", + }, + { + id: "impl:protocol.design-slice-recipe", + nodeType: "CodeNode", + file: "test/self-hosting-design-slice.test.ts", + constant: "sliceDocumentAnchor", + site: 'describe("design slice",', + role: "projection", + label: "realizes the design slice in the shipped recipe catalog", + }, + { + id: "test:protocol.design-slice", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "sliceTestAnchor", + site: 'describe("design slice",', + }, + { + id: "oracle:protocol.design-slice", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "sliceOracleAnchor", + site: "function expected(", + }, + { + id: "test:protocol.design-slice.chain", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "chainAnchor", + site: "registerChain(", + }, + { + id: "test:protocol.design-slice.decision-backlink", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "decisionBacklinkAnchor", + site: "registerDecisionBacklink(", + }, + { + id: "test:protocol.design-slice.dependency-cycle", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "dependencyCycleAnchor", + site: "registerDependencyCycle(", + }, + { + id: "test:protocol.design-slice.folded-example", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "foldedExampleAnchor", + site: "registerFoldedExample(", + }, + { + id: "test:protocol.design-slice.independent-example", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "independentExampleAnchor", + site: "registerIndependentExample(", + }, + { + id: "test:protocol.design-slice.component-absent", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "componentAbsentAnchor", + site: "registerComponentAbsent(", + }, + { + id: "test:protocol.design-slice.component-unrelated", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "componentUnrelatedAnchor", + site: "registerComponentUnrelated(", + }, + { + id: "test:protocol.design-slice.component-matching", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "componentMatchingAnchor", + site: "registerComponentMatching(", + }, + { + id: "test:protocol.design-slice.component-differing", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "componentDifferingAnchor", + site: "registerComponentDiffering(", + }, + { + id: "test:protocol.design-slice.component-unreadable", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "componentUnreadableAnchor", + site: "registerComponentUnreadable(", + }, + { + id: "test:protocol.design-slice.outside-satisfied", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "outsideSatisfiedAnchor", + site: "registerOutsideSatisfied(", + }, + { + id: "test:protocol.design-slice.outside-unbound", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "outsideUnboundAnchor", + site: "registerOutsideUnbound(", + }, + { + id: "test:protocol.design-slice.outside-holding", + nodeType: "Anchor", + file: "test/self-hosting-design-slice.test.ts", + constant: "outsideHoldingAnchor", + site: "registerOutsideHolding(", + }, { id: "test:protocol.studio-navigation", nodeType: "Anchor", @@ -48,7 +514,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ nodeType: "CodeNode", file: "src/studio-elements/intent-panel-element.ts", site: "export function registerIntentPanel(", - role: "utility", + role: "interaction", label: "displays gathered requests and writes copy selections or local download links", }, { @@ -56,7 +522,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ nodeType: "CodeNode", file: "src/studio-elements/compose.ts", site: "export function registerCompose(", - role: "utility", + role: "interaction", label: "opens a native request dialog beside build-rendered scopes without changing their content", }, @@ -65,7 +531,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ nodeType: "CodeNode", file: "src/studio-elements/scenario-editor.ts", site: "export function registerScenarioEditor(", - role: "utility", + role: "interaction", label: "gathers the edited Given, When and Then as exact text scoped to its subject", }, { @@ -170,7 +636,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ nodeType: "CodeNode", file: "src/projections/studio/pages/spec-structure.ts", site: "export function designView(", - label: "renders realization and trace diagrams and the recorded structural neighborhood", + label: "renders requirement and trace diagrams and the recorded structural neighborhood", role: "projection", }, { @@ -181,14 +647,6 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ label: "places resolved Pack members by their stated rung and floor in authored order", role: "projection", }, - { - id: "impl:protocol.studio-view-elements", - nodeType: "CodeNode", - file: "src/studio-elements/views.ts", - site: "export function registerViews(", - label: "registers build-rendered cards, maps, spaces, trace graphs, impact and findings", - role: "utility", - }, { id: "test:protocol.studio-cited-declaration", nodeType: "Anchor", @@ -284,6 +742,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-registers", nodeType: "CodeNode", + label: "renders question, decision, declaration and backlog registers", file: "src/projections/studio/pages/registers-page.ts", site: "export function questionsPage(", role: "projection", @@ -291,6 +750,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-owner-queue", nodeType: "CodeNode", + label: "renders waiting readiness statements and decider questions", file: "src/projections/studio/pages/queue-page.ts", site: "export function queuePage(", role: "projection", @@ -298,6 +758,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-diff", nodeType: "CodeNode", + label: "renders declared design changes and binding changes separately", file: "src/projections/studio/pages/diff-page.ts", site: "export function diffPage(", role: "projection", @@ -305,6 +766,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-glossary", nodeType: "CodeNode", + label: "renders Model terms with their entry-address links", file: "src/projections/studio/pages/glossary-page.ts", site: "export function glossaryPage(", role: "projection", @@ -312,6 +774,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-base-input", nodeType: "CodeNode", + label: "reads named Studio base graphs and run inputs", file: "src/cli/studio-inputs.ts", site: "export function readStudioBase(", role: "codec", @@ -741,7 +1204,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "test/helpers/validator-probes.ts", site: "export const validatorProbeTable:", label: "declares the refusing and accepting probes for every registered finding id", - role: "utility", + role: "contract", }, { id: "impl:protocol.self-hosting", @@ -751,6 +1214,14 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ constant: "selfHostingImplementationAnchor", label: "realizes the self-hosted corpus through the shared extraction and validation path", }, + { + id: "impl:protocol.role-vocabulary", + nodeType: "CodeNode", + file: "test/self-hosting-graph.test.ts", + site: "const result = extract(", + constant: "roleVocabularyImplementationAnchor", + label: "asserts realization of the role vocabulary in the engine anchors' pinned roles", + }, { id: "test:protocol.graph-first-planning", nodeType: "Anchor", @@ -836,7 +1307,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "test/helpers/validator-audit.ts", site: "export function auditValidatorEvidence(", label: "audits the finding registries against source literals and paired probes", - role: "utility", + role: "validator", }, { id: "test:protocol.superseded-decision-signal.end-and-loop-named", @@ -1130,7 +1601,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "src/graph/entry-address.ts", constant: "entryAddressAnchor", site: "export function resolveEntryAddress(", - role: "utility", + role: "reader", }, { id: "impl:protocol.entry-citation-scan", @@ -1338,7 +1809,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "src/projections/studio/recipes.ts", site: "export async function studioRecipeAnswers(", label: "evaluates shipped recipe bodies once and shares answers between Studio pages", - role: "projection", + role: "service", }, { id: "impl:protocol.studio-architecture-diagrams", @@ -1353,7 +1824,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ nodeType: "CodeNode", file: "src/projections/studio/diagrams/bands.ts", site: "export function traceDiagram(", - label: "relation, member and realization bands and bounded relation folds", + label: "relation and member bands and bounded relation folds", role: "projection", }, { @@ -1370,7 +1841,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "src/studio-elements/diagram.ts", site: "export function registerDiagram(", label: "adds view-box pan, zoom and pinch to build-rendered SVG without changing its layout", - role: "utility", + role: "interaction", }, { id: "impl:protocol.studio-architecture-lens", @@ -1589,7 +2060,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "src/projections/studio/data.ts", site: "export async function createStudioData(", label: "assembles reader contexts, Pack totals, rails and search records for static pages", - role: "projection", + role: "contract", }, { id: "impl:protocol.studio-paths", @@ -1597,7 +2068,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "src/projections/studio/paths.ts", site: "export function pagePath(", label: "encodes uppercase id letters for collision-free Pack and Spec page paths", - role: "utility", + role: "codec", }, { id: "impl:protocol.studio-publish", @@ -1616,52 +2087,50 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ "validates and renders the Studio with independent byte checks and diagnostic exit codes", role: "publisher", }, - { - id: "impl:protocol.studio-build-invalidation", - nodeType: "CodeNode", - file: "src/cli/build-command.ts", - site: "export function runBuild(", - label: "removes the Studio root and temporary sibling before shared extraction", - role: "service", - }, { id: "impl:protocol.studio-concept-match", nodeType: "CodeNode", + label: "matches concepts deterministically across graph and browser queries", file: "src/reader/concept-match.ts", site: "export function matchConcept(", - role: "utility", + role: "reader", }, { id: "impl:protocol.studio-copy", nodeType: "CodeNode", + label: "copies text with selection fallback and failure feedback", file: "src/studio-elements/copy.ts", site: "export function registerCopy(", - role: "utility", + role: "interaction", }, { id: "impl:protocol.studio-expander", nodeType: "CodeNode", + label: "opens fragment ancestors and expands disclosures for print", file: "src/studio-elements/expander.ts", site: "export function registerExpander(", - role: "utility", + role: "interaction", }, { id: "impl:protocol.studio-filter", nodeType: "CodeNode", + label: "filters build-rendered rows through their recorded attributes", file: "src/studio-elements/filter.ts", site: "export function registerFilter(", - role: "utility", + role: "interaction", }, { id: "impl:protocol.studio-search-form", nodeType: "CodeNode", + label: "routes search forms through page-root URLs and fragments", file: "src/studio-elements/search.ts", site: "export function registerSearch(", - role: "utility", + role: "interaction", }, { id: "impl:protocol.studio-search-results", nodeType: "CodeNode", + label: "ranks and renders concept, entry and file search hits", file: "src/studio-elements/search-results.ts", site: "export function searchRecords(", role: "projection", @@ -1669,13 +2138,15 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-stamp", nodeType: "CodeNode", + label: "resolves published stamps and source links in the browser", file: "src/studio-elements/stamp.ts", site: "export function registerStamp(", - role: "utility", + role: "interaction", }, { id: "impl:protocol.studio-frame", nodeType: "CodeNode", + label: "renders Studio navigation, page rails and composing controls", file: "src/projections/studio/pages/frame.ts", site: "export function frame(", role: "projection", @@ -1683,6 +2154,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-text", nodeType: "CodeNode", + label: "escapes prose and links graph-resolving mentions", file: "src/projections/studio/pages/text.ts", site: "export function text(", role: "codec", @@ -1690,6 +2162,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-index-page", nodeType: "CodeNode", + label: "renders Pack disclosures and filterable Spec rows", file: "src/projections/studio/pages/index-page.ts", site: "export function indexPage(", role: "projection", @@ -1697,6 +2170,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-pack-page", nodeType: "CodeNode", + label: "renders Pack members, maturity and boundary joins", file: "src/projections/studio/pages/pack-page.ts", site: "export function packPage(", role: "projection", @@ -1704,6 +2178,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-spec-page", nodeType: "CodeNode", + label: "renders Spec intent, relations and realization bindings", file: "src/projections/studio/pages/spec-page.ts", site: "export function specPage(", role: "projection", @@ -1711,6 +2186,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-search-page", nodeType: "CodeNode", + label: "publishes search records beside the browser results view", file: "src/projections/studio/pages/search-page.ts", site: "export function searchPage(", role: "projection", @@ -1718,6 +2194,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.studio-code-spans", nodeType: "CodeNode", + label: "escapes and maps code spans across Studio text", file: "src/projections/studio/pages/code-spans.ts", site: "export function mapCodeSpans(", role: "codec", @@ -1725,6 +2202,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.studio-elements", nodeType: "CodeNode", + label: "optional browser behavior and proposal handoff", + role: "interaction", file: "src/studio-elements/index.ts", site: "registerSearch();", layer: "edge", @@ -1831,7 +2310,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.open-section-order", nodeType: "CodeNode", - label: "preserves authored open-section and model-term order", + label: "preserves authored open-section and model-term order and Pack field order", file: "src/extract/serialize.ts", constant: "openSectionOrderAnchor", site: "function canonicalDynamicSection", @@ -1840,7 +2319,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.model", nodeType: "CodeNode", - label: "Protocol model seam", + role: "contract", + label: "the authored contracts and stable ids", file: "src/model/anchors.ts", constant: "modelComponentAnchor", site: "const anchorModelAnchor", @@ -1850,7 +2330,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.extract", nodeType: "CodeNode", - label: "Protocol extraction seam", + role: "extractor", + label: "the carrier and source pipeline", file: "src/extract/index.ts", constant: "extractComponentAnchor", site: "export const extractAnchor", @@ -1860,17 +2341,19 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.graph", nodeType: "CodeNode", - label: "Protocol graph seam", + role: "contract", + label: "the one graph and its semantic decisions", file: "src/graph/schema.ts", constant: "graphComponentAnchor", - site: "const graphClaimsAnchor", + site: "export const deliveryFactNames", layer: "domain", context: "protocol", }, { id: "component:protocol.validate", nodeType: "CodeNode", - label: "Protocol validation seam", + role: "validator", + label: "one finding currency and ordered checks", file: "src/validate/validators.ts", constant: "validateComponentAnchor", site: "const validationFamiliesAnchor", @@ -1880,7 +2363,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.reader", nodeType: "CodeNode", - label: "Protocol reader seam", + role: "reader", + label: "the typed graph contract and body evaluator", file: "src/reader/reader.ts", site: "export function createReader", layer: "application", @@ -1889,7 +2373,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.projections", nodeType: "CodeNode", - label: "Protocol projections seam", + role: "projection", + label: "read-only output, layout and owned publication", file: "src/projections/design-review.ts", site: "export function renderDesignReview", layer: "edge", @@ -1898,7 +2383,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.cli", nodeType: "CodeNode", - label: "Protocol CLI seam", + role: "service", + label: "entry points, shared build and publication", file: "src/cli/build-command.ts", constant: "cliComponentAnchor", site: "export interface BuildHooks", @@ -1908,7 +2394,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.runner", nodeType: "CodeNode", - label: "Protocol example runner seam", + role: "service", + label: "provider-neutral example execution", file: "src/runner/index.ts", constant: "runnerComponentAnchor", site: "const exampleRunnerAnchor", @@ -1918,7 +2405,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.codegen", nodeType: "CodeNode", - label: "Protocol runnable code generation seam", + role: "projection", + label: "contracts and registrar output", file: "src/codegen/contracts.ts", constant: "codegenComponentAnchor", site: "const runnableModulesAnchor", @@ -1928,7 +2416,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.notation", nodeType: "CodeNode", - label: "Protocol slot notation seam", + role: "codec", + label: "the leaf step codec", file: "src/notation/slots.ts", constant: "notationComponentAnchor", site: "const slotNotationAnchor", @@ -1938,7 +2427,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.adapters", nodeType: "CodeNode", - label: "Protocol adapters: Vitest examples and TypeScript file imports", + role: "adapter", + label: "external runtime and source adapters", file: "src/adapters/vitest.ts", constant: "adaptersComponentAnchor", site: "const exampleRunnerAdapterAnchor", @@ -1948,7 +2438,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.import", nodeType: "CodeNode", - label: "Protocol import seam", + role: "service", + label: "canonical conversion with a fidelity boundary", file: "src/import/import.ts", constant: "importComponentAnchor", site: "const sdpImportCoreAnchor", @@ -1958,7 +2449,8 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "component:protocol.testing", nodeType: "CodeNode", - label: "Protocol testing seam", + role: "adapter", + label: "the generated registrar runtime", file: "src/testing/index.ts", constant: "testingComponentAnchor", site: "const exampleTestingHelpersAnchor", @@ -2089,15 +2581,6 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ constant: "readinessFloorTestAnchor", site: 'describe("readiness and validation contracts",', }, - { - id: "impl:protocol.markdown-authoring", - nodeType: "CodeNode", - label: "reifies Markdown authoring into the one carrier path", - file: "src/extract/markdown.ts", - constant: "markdownAuthoringAnchor", - site: "export function reifyMarkdownCarrier", - role: "extractor", - }, { id: "impl:protocol.gherkin-authoring", nodeType: "CodeNode", @@ -2110,7 +2593,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.markdown-parser", nodeType: "CodeNode", - label: "reifies the ruled Markdown parser input", + label: "reifies Markdown Specs and Packs through one carrier boundary", file: "src/extract/markdown.ts", constant: "markdownParserAnchor", site: "export function reifyMarkdownCarrier", @@ -2128,8 +2611,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ id: "impl:protocol.envelope-contract", nodeType: "CodeNode", label: "parses the bounded Markdown frontmatter envelope", - file: "src/extract/markdown.ts", - constant: "envelopeContractAnchor", + file: "src/extract/markdown-frontmatter.ts", site: "export function parseMarkdownFrontmatter", role: "codec", }, @@ -2141,15 +2623,6 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ constant: "envelopeContractTestAnchor", site: 'describe("Markdown frontmatter reifier",', }, - { - id: "impl:protocol.prose-ownership", - nodeType: "CodeNode", - label: "reads Markdown body content through its prose owners", - file: "src/extract/markdown.ts", - constant: "proseOwnershipAnchor", - site: "export function readMarkdownBody", - role: "codec", - }, { id: "test:protocol.prose-ownership", nodeType: "Anchor", @@ -2577,18 +3050,10 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ site: "export interface DesignReviewPage", role: "contract", }, - { - id: "impl:protocol.agent-surface", - nodeType: "CodeNode", - label: "typed graph reader and agent entry adapters", - file: "src/reader/reader.ts", - site: "export function createReader", - role: "reader", - }, { id: "impl:protocol.reader", nodeType: "CodeNode", - label: "thin typed graph reader construction", + label: "typed graph queries, entry adapters and context joins", file: "src/reader/reader.ts", site: "export function createReader", role: "reader", @@ -2642,37 +3107,10 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ site: "export interface ExtractOptions", role: "contract", }, - { - id: "impl:protocol.graph-claims", - nodeType: "CodeNode", - label: "declares the graph claim taxonomy", - file: "src/graph/schema.ts", - constant: "graphClaimsAnchor", - site: "export const graphClaims", - role: "contract", - }, - { - id: "impl:protocol.graph-edge-types", - nodeType: "CodeNode", - label: "declares the closed list of graph edge types", - file: "src/graph/schema.ts", - constant: "graphEdgeTypesAnchor", - site: "export const graphEdgeTypes", - role: "contract", - }, - { - id: "impl:protocol.regenerability", - nodeType: "CodeNode", - label: "repeats graph and contract producers for deterministic regeneration", - file: "src/cli/build-command.ts", - constant: "regenerabilityAnchor", - site: "export function runBuild", - role: "service", - }, { id: "impl:protocol.schema-version", nodeType: "CodeNode", - label: "declares the graph schema version", + label: "versioned graph nodes, claims, edges and located evidence", file: "src/graph/schema.ts", constant: "schemaVersionAnchor", site: "export const schemaVersion", @@ -2916,28 +3354,10 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.sdp-import", nodeType: "CodeNode", - label: "plans, refuses, and publishes TypeScript-to-Markdown Spec imports", - file: "src/cli/import-command.ts", - constant: "sdpImportAnchor", - site: "export function runImport", - role: "service", - }, - { - id: "impl:protocol.extraction-determinism", - nodeType: "CodeNode", - label: "repeats and byte-compares graph and contract generation under --check-clean", - file: "src/cli/build-command.ts", - constant: "determinismAnchor", - site: "export function runBuild", - role: "service", - }, - { - id: "impl:protocol.wholesale-view-build-invalidation", - nodeType: "CodeNode", - label: "invalidates the prior Design Review before every build attempt", - file: "src/cli/build-command.ts", - constant: "wholesaleViewBuildInvalidationAnchor", - site: "export function runBuild", + label: "plans, refuses, and publishes TypeScript-to-Markdown Spec imports", + file: "src/cli/import-command.ts", + constant: "sdpImportAnchor", + site: "export function runImport", role: "service", }, { @@ -2955,7 +3375,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ label: "renders stated readiness beside the structural floor and dishonest divergence", file: "src/projections/design-review-context.ts", site: "export function renderReadiness", - role: "renderer", + role: "projection", }, { id: "impl:protocol.binding-language-spec-page", @@ -2963,7 +3383,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ label: "renders implementation, verifier, oracle, and observation binding language", file: "src/projections/design-review-context.ts", site: "export function renderBindings", - role: "renderer", + role: "projection", }, { id: "impl:protocol.diagnostic-rendering-design-review", @@ -2971,23 +3391,15 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ label: "renders structured finding locations in the Design Review table", file: "src/projections/design-review-context.ts", site: "export function renderFindings", - role: "renderer", - }, - { - id: "impl:protocol.binding-language-pack-table", - nodeType: "CodeNode", - label: "renders Pack member implementation and verifier bindings as present or none", - file: "src/projections/design-review-pages.ts", - site: "export function renderPackPage", - role: "renderer", + role: "projection", }, { id: "impl:protocol.binding-language-index-table", nodeType: "CodeNode", - label: "renders index implementation and verifier bindings as present or none", + label: "renders binding presence in index and Pack tables", file: "src/projections/design-review-pages.ts", site: "export function renderIndexPage", - role: "renderer", + role: "projection", }, { id: "impl:protocol.diagnostic-rendering-cli", @@ -2996,7 +3408,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "src/cli/output.ts", constant: "diagnosticCliAnchor", site: "export function formatFinding", - role: "renderer", + role: "codec", }, { id: "impl:protocol.kind-evidence", @@ -3005,7 +3417,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "src/validate/readiness-floor.ts", constant: "kindEvidenceAnchor", site: "export const kindEvidence", - role: "contract", + role: "decider", }, { id: "impl:protocol.referential-integrity", @@ -3175,8 +3587,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.build-pipeline-emit", nodeType: "CodeNode", - label: - "runBuild stages graph, contracts and registrar outputs in temporary paths before publishing them, repeating derivation under --check-clean", + label: "stages build outputs, checks determinism and invalidates projections", file: "src/cli/build-command.ts", constant: "buildPipelineEmitAnchor", site: "export function runBuild", @@ -3241,7 +3652,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "src/testing/index.ts", constant: "exampleTestingHelpersAnchor", site: "export function createRunnableExample", - role: "utility", + role: "adapter", }, { id: "impl:protocol.discover-files", @@ -3313,7 +3724,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "src/validate/graph-index.ts", constant: "graphIndexAnchor", site: "export function buildGraphIndex", - role: "utility", + role: "index", }, { id: "impl:protocol.validation-contracts", @@ -3474,7 +3885,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ { id: "impl:protocol.markdown-body-grammar", nodeType: "CodeNode", - label: "enforces the closed Markdown body grammar", + label: "maps Markdown body content to typed prose owners", file: "src/extract/markdown-body.ts", constant: "markdownBodyGrammarAnchor", site: "export function parseMarkdownBody", @@ -3829,7 +4240,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ label: "renders the Pack page from the Pack design", file: "src/projections/design-review-pages.ts", site: "export function renderPackPage", - role: "renderer", + role: "projection", }, { id: "impl:protocol.pack-design-sections", @@ -3837,7 +4248,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ 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", + role: "projection", }, { id: "test:protocol.pack-design-page", @@ -3920,7 +4331,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ "prints sdp version and the commit parsed from the build-info file beside the compiled CLI", file: "src/cli/sdp.ts", site: "export function runSdpCli", - role: "service", + role: "projection", }, { id: "test:protocol.engine-provenance", @@ -3990,18 +4401,9 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ nodeType: "CodeNode", label: "opt-in imports publication in the build command", file: "src/cli/build-command.ts", - site: "const wholesaleViewBuildInvalidationAnchor", + site: "const buildPipelineEmitAnchor", role: "publisher", }, - { - id: "impl:protocol.query-answer-shapes", - nodeType: "CodeNode", - label: "reports parse positions in the caller's query body", - file: "src/reader/evaluate-body.ts", - constant: "queryAnswerShapeAnchor", - site: "function plainObject", - role: "service", - }, { id: "impl:protocol.authoring-normalization", nodeType: "CodeNode", @@ -4174,7 +4576,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ "SDP_HELP_TEXT indexes the shipped skills, recipe catalog, schema, walkthrough, glossary and specs/ by package-relative path", file: "src/cli/help.ts", site: "export const SDP_HELP_TEXT =", - role: "service", + role: "projection", }, { id: "impl:protocol.sdp-import-scan", @@ -4190,7 +4592,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ label: "stages Markdown siblings and publishes them with atomic hard links", file: "src/cli/import-publish.ts", site: "export function publishImports(", - role: "service", + role: "publisher", }, { id: "impl:protocol.structural-anchors", @@ -4217,7 +4619,7 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ file: "src/cli/validate-view-command.ts", constant: "validateSummaryAnchor", site: "function writeProtocolValidation(", - role: "renderer", + role: "codec", }, { id: "test:protocol.evaluation-sink", @@ -4308,10 +4710,399 @@ export const expectedAnchors: readonly ExpectedAnchor[] = [ constant: "ideaFloorUnmetTestAnchor", site: 'it("renders a Spec whose idea floor is unmet with no rung and a below-idea banner",', }, + { + id: "impl:protocol.roadmap-recipe", + nodeType: "CodeNode", + file: "test/roadmap.test.ts", + constant: "roadmapImplementation", + site: 'describe("the roadmap recipes",', + label: "realizes the roadmap in the shipped recipe catalog", + role: "projection", + }, + { + id: "impl:protocol.roadmap-memo-recipe", + nodeType: "CodeNode", + file: "test/roadmap.test.ts", + constant: "memoImplementation", + site: 'describe("the roadmap recipes",', + label: "realizes the roadmap memo in the shipped recipe catalog", + role: "projection", + }, + { + id: "test:protocol.roadmap-recipe", + nodeType: "Anchor", + file: "test/roadmap.test.ts", + constant: "roadmapTests", + site: 'describe("the roadmap recipes",', + }, + { + id: "test:protocol.roadmap-memo-recipe", + nodeType: "Anchor", + file: "test/roadmap.test.ts", + constant: "memoTests", + site: 'describe("the roadmap recipes",', + }, + { + id: "oracle:protocol.roadmap", + nodeType: "Anchor", + file: "test/roadmap-examples.test.ts", + constant: "oracleAnchor", + site: "function expected(", + }, + { + id: "test:protocol.roadmap.built-work", + nodeType: "Anchor", + file: "test/roadmap-examples.test.ts", + constant: "builtWorkAnchor", + site: "registerBuiltWork(", + }, + { + id: "test:protocol.roadmap.buildable-part", + nodeType: "Anchor", + file: "test/roadmap-examples.test.ts", + constant: "buildablePartAnchor", + site: "registerBuildablePart(", + }, + { + id: "test:protocol.roadmap.statement-part", + nodeType: "Anchor", + file: "test/roadmap-examples.test.ts", + constant: "statementPartAnchor", + site: "registerStatementPart(", + }, + { + id: "test:protocol.roadmap.design-part", + nodeType: "Anchor", + file: "test/roadmap-examples.test.ts", + constant: "designPartAnchor", + site: "registerDesignPart(", + }, + { + id: "test:protocol.roadmap.drifting-part", + nodeType: "Anchor", + file: "test/roadmap-examples.test.ts", + constant: "driftingPartAnchor", + site: "registerDriftingPart(", + }, + { + id: "test:protocol.roadmap.missing-evidence", + nodeType: "Anchor", + file: "test/roadmap-examples.test.ts", + constant: "missingEvidenceAnchor", + site: "registerMissingEvidence(", + }, + { + id: "test:protocol.roadmap.blocking-part", + nodeType: "Anchor", + file: "test/roadmap-examples.test.ts", + constant: "blockingPartAnchor", + site: "registerBlockingPart(", + }, + { + id: "test:protocol.roadmap.outside-parent", + nodeType: "Anchor", + file: "test/roadmap-examples.test.ts", + constant: "outsideParentAnchor", + site: "registerOutsideParent(", + }, + { + id: "test:protocol.roadmap.statement-order", + nodeType: "Anchor", + file: "test/roadmap-examples.test.ts", + constant: "statementOrderAnchor", + site: "registerStatementOrder(", + }, ] as const; // re-measured under plan 41 export const expectedBindingEdges = [ + [ + "impl:protocol.studio-capability-diagrams", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds", + ], + [ + "impl:protocol.studio-slice-dependencies", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds", + ], + [ + "impl:protocol.studio-slice-architecture", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds", + ], + ["impl:protocol.studio-maturity-map", "satisfies", "spec:consumers.spec-studio.diagram-kinds"], + [ + "impl:protocol.studio-requirement-diagram", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds", + ], + [ + "impl:protocol.studio-architecture-lens", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds", + ], + ["test:protocol.studio-diagram-catalog", "verifies", "spec:consumers.spec-studio.diagram-kinds"], + + ["impl:protocol.roadmap-practice", "satisfies", "spec:protocol.roadmap-replaces-plans"], + ["test:protocol.roadmap-practice", "verifies", "spec:protocol.roadmap-replaces-plans"], + + ["impl:protocol.studio-frame", "satisfies", "spec:consumers.spec-studio.capability-map-page"], + [ + "impl:protocol.studio-index-page", + "satisfies", + "spec:consumers.spec-studio.capability-map-page", + ], + ["impl:protocol.studio-filter", "satisfies", "spec:consumers.spec-studio.capability-map-page"], + [ + "impl:protocol.studio-capabilities-page", + "satisfies", + "spec:consumers.spec-studio.capability-map-page", + ], + [ + "impl:protocol.studio-capability-diagrams", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.capability-tree", + ], + [ + "test:protocol.studio-capabilities", + "verifies", + "spec:consumers.spec-studio.capability-map-page", + ], + [ + "test:protocol.studio-capabilities", + "verifies", + "spec:consumers.spec-studio.diagram-kinds.capability-tree", + ], + + ["impl:protocol.studio-roadmap-page", "satisfies", "spec:consumers.spec-studio.roadmap-page"], + ["impl:protocol.studio-roadmap-page", "satisfies", "spec:consumers.roadmap.memo"], + ["test:protocol.studio-roadmap-page", "verifies", "spec:consumers.spec-studio.roadmap-page"], + ["test:protocol.studio-roadmap-page", "verifies", "spec:consumers.roadmap.memo"], + ["test:protocol.studio-roadmap-page", "verifies", "spec:consumers.spec-studio.navigation"], + ["impl:protocol.studio-slice-answer", "references", "spec:consumers.design-slice"], + ["impl:protocol.studio-slice-page", "satisfies", "spec:consumers.spec-studio.slice-page"], + ["test:protocol.studio-slice-page", "verifies", "spec:consumers.spec-studio.slice-page"], + [ + "impl:protocol.studio-slice-dependencies", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.dependency", + ], + [ + "test:protocol.studio-slice-dependency", + "verifies", + "spec:consumers.spec-studio.diagram-kinds.dependency", + ], + [ + "impl:protocol.studio-slice-architecture", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + ], + [ + "test:protocol.studio-slice-architecture", + "verifies", + "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + ], + + [ + "test:protocol.studio-architecture-maturity-requirement", + "verifies", + "spec:consumers.spec-studio.lenses.architecture", + ], + [ + "test:protocol.studio-architecture-maturity-requirement", + "verifies", + "spec:consumers.spec-studio.lenses.architecture.unit-register", + ], + [ + "test:protocol.studio-architecture-maturity-requirement", + "verifies", + "spec:consumers.spec-studio.diagram-kinds.maturity-picture", + ], + [ + "test:protocol.studio-architecture-maturity-requirement", + "verifies", + "spec:consumers.spec-studio.pack-page.maturity-map", + ], + [ + "test:protocol.studio-architecture-maturity-requirement", + "verifies", + "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + ], + [ + "test:protocol.studio-architecture-maturity-requirement", + "verifies", + "spec:consumers.spec-studio.spec-page", + ], + [ + "impl:protocol.studio-maturity-map", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.maturity-picture", + ], + + [ + "impl:protocol.studio-requirement-view", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + ], + ["impl:protocol.studio-requirement-view", "satisfies", "spec:consumers.spec-studio.spec-page"], + + [ + "impl:protocol.studio-requirement-diagram", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + ], + + [ + "impl:protocol.studio-unit-register", + "satisfies", + "spec:consumers.spec-studio.lenses.architecture.unit-register", + ], + + [ + "impl:protocol.studio-diagram-layout", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.legibility", + ], + ["impl:protocol.studio-diagram-layout", "satisfies", "spec:consumers.spec-studio.diagram-kinds"], + [ + "impl:protocol.studio-diagram-routing", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.edge-routing", + ], + [ + "impl:protocol.studio-diagram-source", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.mermaid-source", + ], + [ + "impl:protocol.studio-diagram-source", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.capability-tree", + ], + ["impl:protocol.studio-diagram-source", "satisfies", "spec:consumers.spec-studio.diagram-kinds"], + ["impl:protocol.studio-diagrams", "satisfies", "spec:consumers.spec-studio.diagram-kinds"], + [ + "impl:protocol.studio-diagram-element", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.legibility", + ], + ["test:protocol.studio-copy-source", "verifies", "spec:consumers.spec-studio.page-elements"], + ["test:protocol.studio-diagram-core", "verifies", "spec:consumers.spec-studio.diagrams"], + [ + "test:protocol.studio-diagram-core", + "verifies", + "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + ], + [ + "test:protocol.studio-diagram-core", + "verifies", + "spec:consumers.spec-studio.diagram-kinds.legibility", + ], + [ + "test:protocol.studio-diagram-core", + "verifies", + "spec:consumers.spec-studio.diagram-kinds.edge-routing", + ], + [ + "test:protocol.studio-diagram-core", + "verifies", + "spec:consumers.spec-studio.diagram-kinds.mermaid-source", + ], + [ + "impl:protocol.studio-diagrams", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.legibility", + ], + [ + "impl:protocol.studio-diagrams", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.mermaid-source", + ], + ["impl:protocol.capability-map-recipe", "satisfies", "spec:consumers.capability-map"], + ["impl:protocol.capability-map-recipe", "references", "spec:decisions.epics-of-any-kind"], + ["test:protocol.capability-map", "verifies", "spec:consumers.capability-map"], + ["test:protocol.capability-epics", "verifies", "spec:protocol.capability-epics"], + ["impl:protocol.capability-epics", "satisfies", "spec:protocol.capability-epics"], + ["impl:protocol.role-vocabulary", "satisfies", "spec:protocol.role-vocabulary"], + ["impl:protocol.pack-design-page", "satisfies", "spec:extraction.pack-member-order"], + ["impl:protocol.pack-coherence", "satisfies", "spec:extraction.pack-member-order"], + ["impl:protocol.markdown-yaml-policy", "satisfies", "spec:extraction.pack-member-order"], + ["impl:protocol.example-runner", "satisfies", "spec:extraction.executable-examples"], + ["impl:protocol.example-runner-adapter", "satisfies", "spec:extraction.executable-examples"], + ["impl:protocol.executable-contracts", "satisfies", "spec:extraction.executable-examples"], + ["impl:protocol.runnable-modules", "satisfies", "spec:extraction.executable-examples"], + ["impl:protocol.example-testing-helpers", "satisfies", "spec:extraction.executable-examples"], + ["impl:protocol.delivery-facts", "satisfies", "spec:extraction.executable-examples"], + [ + "test:protocol.design-slice.outside-components", + "verifies", + "spec:consumers.design-slice.outside-components", + ], + ["impl:protocol.design-slice-recipe", "satisfies", "spec:consumers.design-slice"], + ["test:protocol.design-slice", "verifies", "spec:consumers.design-slice"], + ["oracle:protocol.design-slice", "models", "spec:consumers.design-slice"], + ["test:protocol.design-slice.chain", "verifies", "spec:consumers.design-slice.chain"], + [ + "test:protocol.design-slice.decision-backlink", + "verifies", + "spec:consumers.design-slice.decision-backlink", + ], + [ + "test:protocol.design-slice.dependency-cycle", + "verifies", + "spec:consumers.design-slice.dependency-cycle", + ], + [ + "test:protocol.design-slice.folded-example", + "verifies", + "spec:consumers.design-slice.folded-example", + ], + [ + "test:protocol.design-slice.independent-example", + "verifies", + "spec:consumers.design-slice.independent-example", + ], + [ + "test:protocol.design-slice.component-absent", + "verifies", + "spec:consumers.design-slice.component-absent", + ], + [ + "test:protocol.design-slice.component-unrelated", + "verifies", + "spec:consumers.design-slice.component-unrelated", + ], + [ + "test:protocol.design-slice.component-matching", + "verifies", + "spec:consumers.design-slice.component-matching", + ], + [ + "test:protocol.design-slice.component-differing", + "verifies", + "spec:consumers.design-slice.component-differing", + ], + [ + "test:protocol.design-slice.component-unreadable", + "verifies", + "spec:consumers.design-slice.component-unreadable", + ], + [ + "test:protocol.design-slice.outside-satisfied", + "verifies", + "spec:consumers.design-slice.outside-satisfied", + ], + [ + "test:protocol.design-slice.outside-unbound", + "verifies", + "spec:consumers.design-slice.outside-unbound", + ], + [ + "test:protocol.design-slice.outside-holding", + "verifies", + "spec:consumers.design-slice.outside-holding", + ], ["impl:protocol.studio-pack-page", "satisfies", "spec:consumers.spec-studio.components"], ["impl:protocol.studio-diagram-element", "satisfies", "spec:consumers.spec-studio.components"], [ @@ -4325,7 +5116,6 @@ export const expectedBindingEdges = [ ["test:protocol.studio-intent-handoff", "verifies", "spec:consumers.spec-studio.components"], ["test:protocol.studio-publisher", "verifies", "spec:consumers.spec-studio"], ["test:protocol.studio-navigation", "verifies", "spec:consumers.spec-studio.navigation"], - [ "impl:protocol.studio-proposal-memo", "satisfies", @@ -4402,8 +5192,6 @@ export const expectedBindingEdges = [ "spec:consumers.spec-studio.pack-page.maturity-map", ], ["impl:protocol.studio-maturity-map", "satisfies", "spec:consumers.spec-studio.components"], - ["impl:protocol.studio-view-elements", "satisfies", "spec:consumers.spec-studio.components"], - ["impl:protocol.studio-view-elements", "references", "spec:decisions.studio-build-renders"], [ "test:protocol.studio-cited-declaration", "verifies", @@ -4591,7 +5379,6 @@ export const expectedBindingEdges = [ ["oracle:protocol.vault-notes", "models", "spec:consumers.obsidian-vault.notes"], ["oracle:protocol.vault-bases", "models", "spec:consumers.obsidian-vault.bases"], ["oracle:protocol.vault-iteration", "models", "spec:consumers.obsidian-vault.iteration"], - ["impl:protocol.upgrade-guide", "satisfies", "spec:consumers.upgrade-kit"], [ "impl:protocol.capability-adoption-recipe", @@ -4919,7 +5706,6 @@ export const expectedBindingEdges = [ "verifies", "spec:extraction.entry-citations.unresolved-citation", ], - ["impl:protocol.generate-command", "satisfies", "spec:consumers.command-line"], ["impl:protocol.adopter-help", "satisfies", "spec:consumers.command-line"], ["impl:protocol.external-registers-recipe", "satisfies", "spec:validation.external-registers"], @@ -4997,6 +5783,11 @@ export const expectedBindingEdges = [ "spec:consumers.spec-studio.diagrams", ], ["impl:protocol.studio-relation-diagrams", "satisfies", "spec:consumers.spec-studio.diagrams"], + [ + "impl:protocol.studio-diagrams", + "satisfies", + "spec:consumers.spec-studio.diagram-kinds.edge-routing", + ], ["impl:protocol.studio-diagrams", "satisfies", "spec:consumers.spec-studio.diagrams"], ["impl:protocol.studio-diagrams", "references", "spec:decisions.studio-diagram-renderer"], ["impl:protocol.studio-diagrams", "references", "spec:decisions.studio-build-renders"], @@ -5134,7 +5925,7 @@ export const expectedBindingEdges = [ ["impl:protocol.studio-paths", "satisfies", "spec:consumers.spec-studio.publisher"], ["impl:protocol.studio-publish", "satisfies", "spec:consumers.spec-studio.publisher"], ["impl:protocol.studio-command", "satisfies", "spec:consumers.spec-studio.publisher"], - ["impl:protocol.studio-build-invalidation", "satisfies", "spec:consumers.spec-studio.publisher"], + ["impl:protocol.build-pipeline-emit", "satisfies", "spec:consumers.spec-studio.publisher"], ["impl:protocol.studio-concept-match", "satisfies", "spec:consumers.spec-studio.search"], ["impl:protocol.studio-copy", "satisfies", "spec:consumers.spec-studio.page-elements"], ["impl:protocol.studio-expander", "satisfies", "spec:consumers.spec-studio.page-elements"], @@ -5220,13 +6011,13 @@ export const expectedBindingEdges = [ ["test:protocol.authoring-recipes", "verifies", "spec:consumers.agent-surface.authoring-recipes"], ["impl:protocol.readiness-floor", "satisfies", "spec:validation.readiness-floor"], ["test:protocol.readiness-floor", "verifies", "spec:validation.readiness-floor"], - ["impl:protocol.markdown-authoring", "satisfies", "spec:carrier.markdown-authoring"], + ["impl:protocol.markdown-parser", "satisfies", "spec:carrier.markdown-authoring"], ["impl:protocol.gherkin-authoring", "satisfies", "spec:carrier.gherkin-authoring"], ["impl:protocol.markdown-parser", "satisfies", "spec:carrier.markdown-parser"], ["test:protocol.markdown-parser", "verifies", "spec:carrier.markdown-parser"], ["impl:protocol.envelope-contract", "satisfies", "spec:carrier.envelope-contract"], ["test:protocol.envelope-contract", "verifies", "spec:carrier.envelope-contract"], - ["impl:protocol.prose-ownership", "satisfies", "spec:carrier.prose-ownership-rule"], + ["impl:protocol.markdown-body-grammar", "satisfies", "spec:carrier.prose-ownership-rule"], ["test:protocol.prose-ownership", "verifies", "spec:carrier.prose-ownership-rule"], ["impl:protocol.duplicate-id-exclusion", "satisfies", "spec:validation.duplicate-ids"], [ @@ -5423,7 +6214,7 @@ export const expectedBindingEdges = [ ["impl:protocol.verifier-semantics", "satisfies", "spec:model.spec-sections"], ["impl:protocol.validation-families", "satisfies", "spec:validation.two-check-families"], ["impl:protocol.projections-model", "satisfies", "spec:consumers.projections-model"], - ["impl:protocol.agent-surface", "satisfies", "spec:consumers.agent-surface"], + ["impl:protocol.reader", "satisfies", "spec:consumers.agent-surface"], ["impl:protocol.reader", "satisfies", "spec:consumers.reader"], ["impl:protocol.reader", "satisfies", "spec:extraction.entry-locations"], ["impl:protocol.design-review", "satisfies", "spec:consumers.design-review"], @@ -5432,8 +6223,8 @@ export const expectedBindingEdges = [ ["impl:protocol.gherkin-view", "satisfies", "spec:consumers.gherkin-view"], ["test:protocol.gherkin-view", "verifies", "spec:consumers.gherkin-view"], ["impl:protocol.exclusion-surface", "satisfies", "spec:extraction.excludes"], - ["impl:protocol.graph-claims", "satisfies", "spec:extraction.claim-taxonomy"], - ["impl:protocol.regenerability", "satisfies", "spec:extraction.regenerability"], + ["impl:protocol.schema-version", "satisfies", "spec:extraction.claim-taxonomy"], + ["impl:protocol.build-pipeline-emit", "satisfies", "spec:extraction.regenerability"], ["impl:protocol.schema-version", "satisfies", "spec:extraction.schema-versioning"], ["impl:protocol.executable-contracts", "satisfies", "spec:extraction.executable-contracts"], ["impl:protocol.example-runner", "satisfies", "spec:extraction.example-runner"], @@ -5549,12 +6340,8 @@ export const expectedBindingEdges = [ ["test:protocol.reader.file-entry", "verifies", "spec:consumers.reader.file-entry"], ["test:protocol.reader.changeset-entry", "verifies", "spec:consumers.reader.changeset-entry"], ["impl:protocol.sdp-import", "satisfies", "spec:carrier.sdp-import"], - ["impl:protocol.extraction-determinism", "satisfies", "spec:extraction.determinism"], - [ - "impl:protocol.wholesale-view-build-invalidation", - "satisfies", - "spec:consumers.wholesale-view-rewrite", - ], + ["impl:protocol.build-pipeline-emit", "satisfies", "spec:extraction.determinism"], + ["impl:protocol.build-pipeline-emit", "satisfies", "spec:consumers.wholesale-view-rewrite"], ["impl:protocol.wholesale-view-rewrite", "satisfies", "spec:consumers.wholesale-view-rewrite"], [ "impl:protocol.derived-readiness-banner", @@ -5571,11 +6358,6 @@ export const expectedBindingEdges = [ "satisfies", "spec:validation.diagnostic-rendering", ], - [ - "impl:protocol.binding-language-pack-table", - "satisfies", - "spec:consumers.binding-language-views", - ], [ "impl:protocol.binding-language-index-table", "satisfies", @@ -5835,8 +6617,8 @@ export const expectedBindingEdges = [ ["impl:protocol.anchors-subpath", "references", "spec:decisions.anchor-comment-form"], ["impl:protocol.anchor-extraction", "references", "spec:decisions.anchor-comment-form"], ["impl:protocol.derive-graph", "references", "spec:decisions.anchor-binding-grain"], - ["impl:protocol.graph-edge-types", "satisfies", "spec:extraction.derive-graph"], - ["impl:protocol.graph-edge-types", "references", "spec:decisions.anchor-binding-grain"], + ["impl:protocol.schema-version", "satisfies", "spec:extraction.derive-graph"], + ["impl:protocol.schema-version", "references", "spec:decisions.anchor-binding-grain"], ["impl:protocol.anchor-model", "references", "spec:decisions.architectural-annotation"], ["impl:protocol.census-page", "references", "spec:decisions.architectural-annotation"], ["impl:protocol.entry-locations", "satisfies", "spec:extraction.entry-locations"], @@ -5917,14 +6699,9 @@ export const expectedBindingEdges = [ ], ["impl:protocol.reader", "references", "spec:consumers.agent-surface.answer-shapes"], ["impl:protocol.adopter-help", "satisfies", "spec:consumers.adopter-on-ramp"], - ["impl:protocol.sdp-import-scan", "references", "spec:carrier.sdp-import"], - ["impl:protocol.sdp-import-publish", "references", "spec:carrier.sdp-import"], + ["impl:protocol.sdp-import-scan", "satisfies", "spec:carrier.sdp-import"], + ["impl:protocol.sdp-import-publish", "satisfies", "spec:carrier.sdp-import"], ["test:protocol.shipped-protocol-corpus", "verifies", "spec:consumers.adopter-on-ramp"], - [ - "impl:protocol.query-answer-shapes", - "satisfies", - "spec:consumers.agent-surface.evaluation-sink", - ], ["test:protocol.query-answer-shapes", "verifies", "spec:consumers.agent-surface.evaluation-sink"], [ "impl:protocol.build-pipeline-query", @@ -5960,6 +6737,7 @@ export const expectedBindingEdges = [ ["test:protocol.studio-on-ramp", "verifies", "spec:consumers.studio-on-ramp"], ["impl:protocol.vault-on-ramp", "satisfies", "spec:consumers.vault-on-ramp"], ["test:protocol.vault-on-ramp", "verifies", "spec:consumers.vault-on-ramp"], + ["test:protocol.vault-priority", "verifies", "spec:consumers.obsidian-vault.notes"], ["test:protocol.self-hosting", "verifies", "spec:protocol.self-hosting"], [ "test:protocol.derived-readiness-banner.idea-floor-unmet", @@ -5970,10 +6748,76 @@ export const expectedBindingEdges = [ ["impl:protocol.authoring-grammar-recipes", "satisfies", "spec:model.open-question-fields"], ["impl:protocol.authoring-grammar-recipes", "satisfies", "spec:model.relation-reasons"], ["impl:protocol.curation-recipe", "references", "spec:model.planned-architecture"], + ["test:protocol.roadmap-priority", "verifies", "spec:carrier.markdown-pack-authoring"], + ["test:protocol.roadmap-priority", "verifies", "spec:model.pack-aggregate"], + ["test:protocol.roadmap-priority", "verifies", "spec:extraction.pack-member-order"], + ["test:protocol.roadmap-priority", "verifies", "spec:consumers.design-diff"], + ["test:protocol.roadmap-priority", "verifies", "spec:consumers.spec-studio.diff-page"], + ["test:protocol.roadmap-priority", "verifies", "spec:consumers.spec-studio.pack-page"], + ["impl:protocol.pack-aggregate", "references", "spec:decisions.roadmap-priority"], + ["impl:protocol.pack-member-order", "references", "spec:decisions.roadmap-priority"], + ["impl:protocol.static-reification", "satisfies", "spec:carrier.markdown-pack-authoring"], + ["impl:protocol.open-section-order", "satisfies", "spec:extraction.pack-member-order"], + ["impl:protocol.reader", "satisfies", "spec:extraction.pack-member-order"], + ["impl:protocol.build-args", "satisfies", "spec:consumers.command-line"], + ["impl:protocol.parameter-codec", "satisfies", "spec:consumers.agent-surface.recipe-parameters"], + ["impl:protocol.corpus-rule-runner", "satisfies", "spec:validation.corpus-rules"], + ["impl:protocol.markdown-yaml-policy", "satisfies", "spec:carrier.envelope-contract"], + ["impl:protocol.markdown-yaml-policy", "satisfies", "spec:carrier.markdown-pack-authoring"], + ["impl:protocol.markdown-content", "satisfies", "spec:carrier.markdown-body-grammar"], + ["impl:protocol.gherkin-kind-policy", "satisfies", "spec:carrier.gherkin-authoring"], + ["impl:protocol.gherkin-kind-policy", "satisfies", "spec:consumers.gherkin-view"], + ["impl:protocol.gherkin-kind-policy", "references", "spec:decisions.carrier-universality"], + ["impl:protocol.import-fidelity", "satisfies", "spec:carrier.sdp-import"], + ["impl:protocol.studio-diagram-model", "satisfies", "spec:consumers.spec-studio.diagrams"], + ["impl:protocol.studio-html", "satisfies", "spec:consumers.spec-studio.shell"], + ["impl:protocol.studio-inputs", "satisfies", "spec:consumers.spec-studio.data.inputs"], + [ + "impl:protocol.studio-proposal-contract", + "satisfies", + "spec:consumers.intent-composition.proposal", + ], + ["impl:protocol.design-review-codec", "satisfies", "spec:consumers.design-review"], + [ + "impl:protocol.studio-register-contracts", + "references", + "spec:consumers.spec-studio.data.recipe-answers", + ], + ["impl:protocol.studio-page-catalog", "satisfies", "spec:consumers.spec-studio.publisher"], + ["impl:protocol.structural-self-binding", "satisfies", "spec:protocol.significant-units-review"], + ["test:protocol.structural-self-binding", "verifies", "spec:protocol.significant-units-review"], + ["impl:protocol.roadmap-recipe", "satisfies", "spec:consumers.roadmap"], + ["impl:protocol.roadmap-memo-recipe", "satisfies", "spec:consumers.roadmap.memo"], + ["test:protocol.roadmap-recipe", "verifies", "spec:consumers.roadmap"], + ["test:protocol.roadmap-memo-recipe", "verifies", "spec:consumers.roadmap.memo"], + ["oracle:protocol.roadmap", "models", "spec:consumers.roadmap"], + ["test:protocol.roadmap.built-work", "verifies", "spec:consumers.roadmap.built-work"], + ["test:protocol.roadmap.buildable-part", "verifies", "spec:consumers.roadmap.buildable-part"], + ["test:protocol.roadmap.statement-part", "verifies", "spec:consumers.roadmap.statement-part"], + ["test:protocol.roadmap.design-part", "verifies", "spec:consumers.roadmap.design-part"], + ["test:protocol.roadmap.drifting-part", "verifies", "spec:consumers.roadmap.drifting-part"], + ["test:protocol.roadmap.missing-evidence", "verifies", "spec:consumers.roadmap.missing-evidence"], + ["test:protocol.roadmap.blocking-part", "verifies", "spec:consumers.roadmap.blocking-part"], + ["test:protocol.roadmap.outside-parent", "verifies", "spec:consumers.roadmap.outside-parent"], + ["test:protocol.roadmap.statement-order", "verifies", "spec:consumers.roadmap.statement-order"], ] as const; // re-measured under plan 41 export const expectedCommentAnchorIds = [ + "impl:protocol.studio-capabilities-page", + "impl:protocol.studio-capability-diagrams", + "impl:protocol.studio-roadmap-page", + "test:protocol.studio-roadmap-page", + "impl:protocol.studio-slice-answer", + "impl:protocol.studio-slice-dependencies", + "impl:protocol.studio-slice-architecture", + "impl:protocol.studio-slice-page", + "impl:protocol.studio-requirement-view", + "impl:protocol.studio-requirement-diagram", + "impl:protocol.studio-unit-register", + "impl:protocol.studio-diagram-layout", + "impl:protocol.studio-diagram-routing", + "impl:protocol.studio-diagram-source", "impl:protocol.studio-proposal-memo", "impl:protocol.studio-intent-panel", "impl:protocol.studio-intent-panel-element", @@ -5984,7 +6828,6 @@ export const expectedCommentAnchorIds = [ "impl:protocol.studio-verification-panels", "impl:protocol.studio-spec-structure", "impl:protocol.studio-maturity-map", - "impl:protocol.studio-view-elements", "impl:protocol.studio-registers", "impl:protocol.studio-owner-queue", "impl:protocol.studio-diff", @@ -6000,7 +6843,6 @@ export const expectedCommentAnchorIds = [ "impl:protocol.vault-sections", "impl:protocol.vault-command", "impl:protocol.vault-watch", - "impl:protocol.structural-self-binding", "impl:protocol.validator-probe-table", "impl:protocol.validator-self-testing", @@ -6021,7 +6863,6 @@ export const expectedCommentAnchorIds = [ "impl:protocol.studio-paths", "impl:protocol.studio-publish", "impl:protocol.studio-command", - "impl:protocol.studio-build-invalidation", "impl:protocol.studio-concept-match", "impl:protocol.studio-copy", "impl:protocol.studio-expander", @@ -6039,13 +6880,11 @@ export const expectedCommentAnchorIds = [ "component:protocol.studio-elements", "test:protocol.example-runner-cleanup", "component:protocol.reader", - "impl:protocol.agent-surface", "impl:protocol.reader", "impl:protocol.census-page", "impl:protocol.derived-readiness-banner", "impl:protocol.binding-language-spec-page", "impl:protocol.diagnostic-rendering-design-review", - "impl:protocol.binding-language-pack-table", "impl:protocol.binding-language-index-table", "component:protocol.projections", "impl:protocol.projections-model", @@ -6068,4 +6907,19 @@ export const expectedCommentAnchorIds = [ "impl:protocol.adopter-help", "impl:protocol.sdp-import-scan", "impl:protocol.sdp-import-publish", + "impl:protocol.build-args", + "impl:protocol.parameter-codec", + "impl:protocol.corpus-rule-runner", + "impl:protocol.markdown-yaml-policy", + "impl:protocol.markdown-content", + "impl:protocol.gherkin-kind-policy", + "impl:protocol.import-fidelity", + "impl:protocol.studio-diagram-model", + "impl:protocol.studio-html", + "impl:protocol.studio-inputs", + "impl:protocol.studio-proposal-contract", + "impl:protocol.design-review-codec", + "impl:protocol.studio-register-contracts", + "impl:protocol.studio-page-catalog", + "impl:protocol.envelope-contract", ] as const; diff --git a/test/self-hosting-oracle/carrier.ts b/test/self-hosting-oracle/carrier.ts index 135b96c4c..6cfed0395 100644 --- a/test/self-hosting-oracle/carrier.ts +++ b/test/self-hosting-oracle/carrier.ts @@ -6,7 +6,7 @@ export const carrierSpecs = [ { id: "spec:carrier.markdown-authoring", specKind: "behavior", - altitude: "feature", + altitude: "epic", readiness: "ready", file: "specs/carrier/markdown-authoring.sdp.md", title: "Markdown authoring enters the one graph", @@ -526,7 +526,7 @@ export const carrierSpecs = [ behavior: { rules: [ "Routing is by envelope id namespace: `pack:` reifies as a Pack manifest; `spec:` keeps the Spec path; any other namespace is refused as `extract/invalid-id`.", - "The pack envelope is closed to exactly `id` (required, `pack:` namespace), `specs` (required YAML list of `spec:` ids, manifest order; may be empty), and `modelRefs` (optional YAML list of `spec:` ids). A derived-name key is refused as `extract/reserved-property`. Any other key — including the Spec envelope keys `kind`, `altitude`, `readiness` and `relations`, and `title` — refuses with `extract/unrecognized-property`. Symmetrically, `specs` or `modelRefs` on a `spec:` carrier refuse the same way.", + "The pack envelope is closed to exactly `id` (required, `pack:` namespace), `specs` (required YAML list of `spec:` ids, manifest order; may be empty), `modelRefs` (optional YAML list of `spec:` ids), and `order` (optional; its one lawful value is `priority`, and any other value refuses with `extract/invalid-frontmatter`). A derived-name key is refused as `extract/reserved-property`. Any other key — including the Spec envelope keys `kind`, `altitude`, `readiness` and `relations`, and `title` — refuses with `extract/unrecognized-property`. Symmetrically, `specs` or `modelRefs` on a `spec:` carrier refuse the same way.", "The body H1 is the Pack title. All remaining body prose is the framing, owned by the Pack node. Any `##` section heading refuses with `extract/unrecognized-heading` — a Pack has no section tier.", "A Markdown manifest and a TypeScript manifest of the same Pack derive the identical Pack node (the `file` field aside) and identical `belongsTo` edges. The same id authored in both carriers is the standing duplicate-id refusal.", "The TypeScript `pack()` builder stays a lawful per-ID option and the import source; `sdp import` remains Spec-only and does not convert pack manifests.", diff --git a/test/self-hosting-oracle/consumers.ts b/test/self-hosting-oracle/consumers.ts index 1101d19ff..1f3712b76 100644 --- a/test/self-hosting-oracle/consumers.ts +++ b/test/self-hosting-oracle/consumers.ts @@ -22,7 +22,7 @@ export const consumersSpecs = [ }, behavior: { rules: [ - "Changed-file impact, Pack design and design diff, recipes 4, 29 and 35, accept `detail: true` and default to summaries preserving every total and explicitly naming omitted detail.", + "Changed-file impact, Pack design, design diff and the design slice, recipes 4, 29, 35 and 54, accept `detail: true` and default to summaries preserving every total and explicitly naming omitted detail.", "The guarantees recipe (recipe 3) returns each section's entries as the Spec states them and the binding fields the Spec context carries, not only the names of the sections.", "The orphans and gaps recipe (recipe 8) reports each kind the verifier-gap signal exempts and how many ready Specs it exempts, as the build backlog reports its exclusions.", "A design-backlog recipe lists the Specs stated `defined` that no code realizes, with their design columns, excluding examples and decisions as the build backlog does.", @@ -31,7 +31,7 @@ export const consumersSpecs = [ }, design: { detailParameter: - "recipes 4, 29 and 35 read `params.detail`, absent or `false` for the summary and `true` for every row in full; recipe 29's `detail` may also be a list of member ids, which returns full rows for those members and summary rows for the rest of the Pack, and an id that is not a member is a diagnostic in the result. A summary keeps `totals` as the full answer counts them and adds `leftOut`, one line for each kind of detail it omits.", + "recipes 4, 29, 35 and 54 read `params.detail`, absent or `false` for the summary and `true` for every row in full; recipe 29's and recipe 54's `detail` may also be a list of member ids, which returns full rows for those members and summary rows for the rest of the Pack, and an id that is not a member is a diagnostic in the result. A summary keeps `totals` as the full answer counts them and adds `leftOut`, one line for each kind of detail it omits.", impactSummary: "`{ changedFiles, totals, impactedSpecs: [id], impactedPacks: [id], atRisk: { incomingSpecs: [id], outgoingSpecs: [id], other: [id] }, coverageUnknownFiles, unlinkedUnits: [id], leftOut }` is recipe 4's summary. Incoming and outgoing name the connecting edge's direction relative to the impacted node and assert no dependency, since the edge may be any type; a Spec reached both ways is listed in both, and `other` holds the at-risk nodes that are not Specs.", impactDetail: @@ -211,7 +211,7 @@ export const consumersSpecs = [ { id: "spec:consumers.projections-model", specKind: "model", - altitude: "feature", + altitude: "epic", readiness: "ready", file: "specs/consumers/projections-model.sdp.md", title: "Projections fan out from one graph without becoming truth stores", @@ -243,7 +243,7 @@ export const consumersSpecs = [ { id: "spec:consumers.agent-surface", specKind: "behavior", - altitude: "feature", + altitude: "epic", readiness: "ready", file: "specs/consumers/agent-surface.sdp.md", title: "Agents script a visible typed graph", @@ -1076,7 +1076,7 @@ export const consumersSpecs = [ { id: "spec:consumers.authoring-on-ramp", specKind: "behavior", - altitude: "feature", + altitude: "epic", readiness: "ready", file: "specs/consumers/authoring-on-ramp.sdp.md", title: "Authors move one Spec from intent to reviewed evidence", @@ -1092,6 +1092,7 @@ export const consumersSpecs = [ "Cheap capture starts with `sdp new spec` (or the equivalent hand-authored idea carrier) in the family found through concept search; the scaffolder emits envelope, Intent outcome, and the kind's bare typed heading, never invented content, and for `constraint` emits envelope, title, and Intent only with no twin section. Every later readiness edit is preceded by the promotion-preflight recipe and remains a human statement.", "The skill teaches question keys and their optional fields through `spec:model.open-question-keys` and `spec:model.open-question-fields`, rule keys and an example's `exercises` through `spec:model.rule-keys`, and optional relation reasons through `spec:model.relation-reasons`.", "The skill teaches the least design two parties need to agree: keyed Design entries for shared names and shapes, code spans for pinned declarations, rule keys for outside citations, one prose owner for why, and no derived counts, delivery state or implementation sequence authored as design.", + "The skill teaches design in rounds over a design slice: what each rung's design typically settles, that a detail only the builder reads and a question only the builder answers belong to the build, and how a reviewer reads the design columns beside each rung; no check reads this guidance.", "The skill points behavior and example authors to `spec:carrier.gherkin-authoring` for the lawful Gherkin alternative and Pack authors to `spec:carrier.markdown-pack-authoring` for namespace routing, framing and ordered membership.", "Binding is taught for code, test and oracle anchors in the constant form and the comment form, following `spec:decisions.anchor-comment-form` and `spec:decisions.anchor-binding-grain`. The constant builders are `codeAnchor`, `specTest` and `specOracle`; a code anchor's `satisfies` is optional and plural, a test anchor's `verifies` is non-empty and plural, and an oracle anchor has one `models` target. The skill teaches `references` as the code-to-design binding that claims no realization, and names the two silent non-binding hazards, a constant builder without a Protocol builder binding and a comment that does not open with `/**`.", "The skill teaches the collective promise test for `satisfies` and the one-way meaning of `references`, following `spec:decisions.anchor-binding-grain`; every promise is realized by the satisfying units together, and each satisfying unit realizes at least one promise.", @@ -1102,6 +1103,7 @@ export const consumersSpecs = [ "An anchor carries identity, an optional label, its flavor's targets and optional structure, never intent; a code anchor with neither `satisfies` nor `references` is identity-only and lawful. A Markdown deliverable binds through the document-realization convention: the suite asserting the shipped document carries the code anchor, and blast radius stays coverage-unknown for the Markdown file.", "Design Review supplies context for the human readiness statement without becoming a workflow gate.", ], + ruleKeys: { designRounds: "rules[4]" }, }, }, deliveryFacts: ["implemented", "has-verifier"], @@ -1126,7 +1128,7 @@ export const consumersSpecs = [ "Capture or refinement uses concept search, the lower ladder, and promotion preflight; design uses promotion preflight and readiness divergence. The listed recipes are starting points, not exhaustive recipe sets.", "The skill teaches an advisory Pack design loop: read the design diff from the last pass, compose a context bundle within a stated budget, narrow by perspective when needed, and return a change proposal for a base check before an agent applies it.", "Implementation uses the build backlog and the target Spec context; review uses the Pack backbone and warn-level signals, or the target Spec context and warn-level signals when no Pack exists. The listed recipes are starting points, not exhaustive recipe sets.", - "The skill teaches a review report as a numbered list ordered by severity, with each item naming one entry address or Spec section, stating the gap in one sentence, and proposing exact text with the address it replaces or follows.", + "The skill teaches a Spec proposal report as a numbered list ordered by severity, with each item naming one entry address or Spec section, stating the gap in one sentence, and proposing exact text with the address it replaces or follows. Findings against code or other files stay review comments on their lines; a review proposing no Spec edit has no proposal.", "Close uses the drift alarm and changed-file blast radius; slimming is optional and preserves durable law and one prose owner. For a Design entry, the skill teaches the distillation boundary stated by `spec:model.enrichment-lifecycle`, compares the entry with its bound artifact before removal, and keeps every explanation of why.", "A handoff names targets, changed files, current readiness, findings or open questions, and commands or evidence locations to re-run; it never carries an inherited verification verdict.", "An arc-scale handoff names the Spec ids that carry the arc's standing refusals, guarantees and holds in the homes ruled by `spec:decisions.planning-truths-placement`: tradeoff refusals in decision Specs, already-homed guarantees on their carrying Specs, and deferrals or holds in their blocking open questions.", @@ -1184,7 +1186,7 @@ export const consumersSpecs = [ "A body of work is grouped in a Pack named for the capability it designs, never for a plan number, listing its Specs in reading order; membership confers no readiness, delivery fact or gate, and the Pack remains a review aggregate after the work ends.", "Heuristics for choosing work remain advisory behavior rules here or 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.", - "A plan file points to the Pack and carrying Specs for intended truth and derived state; it may keep an advisory execution and recovery record, while plan numbering, recorded work order and staleness never become graph structure or workflow authority.", + "A corpus that keeps plan documents lets each point to its Packs and carrying Specs for intended truth and derived state; a plan document confers nothing, and plan numbering, recorded work order and staleness never become graph structure or workflow authority. Advisory priority over the next work lives in a roadmap Pack.", ], }, }, @@ -1598,7 +1600,7 @@ export const consumersSpecs = [ traceGraph: "`sdp-trace-graph` with `spec-id`: the Spec's relations one hop each way, drawn in relation bands inside an `sdp-diagram`, then its text equivalent; no behavior of its own.", maturityMap: - "`sdp-maturity-map` with `pack-id`: the Pack's members placed by stated rung and floor reached, as a table of short cards with its axis explanation in the caption; an empty cell shows `—` alone and a filled cell names its count and Specs; no behavior.", + "`sdp-maturity-map` with `pack-id`: the Pack's members placed by stated rung and floor reached, drawn as a static SVG grid of counts, then its count table and each populated cell's member list; no behavior.", exampleSpace: "`sdp-example-space` with `spec-id`, naming the Spec that owns the example space: the space's slots with their types and the step that declares each, its children's bound points as a table with one row per example and one column per slot, an unbound slot written `unbound`, its expected-outcome oracle binding as present or none, and recipe 44's coverage; its coverage is a report, never a verifier run or a delivery fact; no behavior beyond the composing actions inside it.", diagramView: @@ -1633,10 +1635,10 @@ export const consumersSpecs = [ }, behavior: { rules: [ - "The build renders the index from `specs()`, `packs()`, `findings()` and each Spec's design columns, a Pack page from `packContext` and the Pack totals, a Spec page from `specContext`, and the search page from the search index; every page's rail renders the rail values. A page states no value the reader did not return or a named recipe did not answer, apart from counts the build takes of those values.", + "The build renders the index from `specs()`, `packs()`, `findings()`, each Spec's design columns and the epic memberships recipe 52 answers, a Pack page from `packContext` and the Pack totals, a Spec page from `specContext`, and the search page from the search index; every page's rail renders the rail values. A page states no value the reader did not return or a named recipe did not answer, apart from counts the build takes of those values.", "A page whose script reads data embeds it in one `script` element of type `application/json` with the id `sdp-data`, serialized as JSON with every `<` then written as the six characters `\\u003c`, so no text can close the element. The search page embeds the search index there; every other page embeds no data, because its values are in its markup.", "The page set writes no data file of its own: an agent reads the graph through `sdp q`, and `sdp build` writes the serialized graph.", - "The Studio's data holds no context bundle or other AI slice: the context bundle is composed on each invocation and never saved, and a design pass composes one with its recipe when it needs it.", + "The Studio's data holds no context bundle or other AI slice: the context bundle is composed on each invocation and never saved, and a design pass composes one with its recipe when it needs it. A slice page renders the design slice's answer into its markup, as every page renders its values, and embeds no slice data.", ], ruleKeys: { readerValues: "rules[0]", @@ -1667,7 +1669,7 @@ export const consumersSpecs = [ readiness: "ready", title: "A page takes each recipe answer from the recipe's shipped body", narrative: - "The lenses, the registers, the owner queue, the diff and runs pages and the panels of a Spec page show recipe answers. Each takes its answer from the recipe's shipped body, evaluated over the graph the build derived, so a page and `sdp q` give one answer. The two Design entries name every evaluation a build runs and which page reads each answer, so a builder and a test author read one list.", + "The lenses, the registers, the owner queue, the slice and roadmap pages, the diff and runs pages and the panels of a Spec page show recipe answers. Each takes its answer from the recipe's shipped body, evaluated over the graph the build derived, so a page and `sdp q` give one answer. The two Design entries name every evaluation a build runs and which page reads each answer, so a builder and a test author read one list.", sections: { intent: { outcome: @@ -1685,9 +1687,9 @@ export const consumersSpecs = [ }, design: { recipeEvaluations: - "a build evaluates each of these once, with no parameter unless one is named: recipes 17, 20, 24, 31, 33, 43, 44, 46 and 51; recipe 29 with `{ pack: id }` for every Pack; recipe 21 with `{ spec: id }` and recipe 30 with `{ specs: [id] }` for every Spec; recipe 34 with `{ spec: id }` for every Spec that is not an example, Specs in no Pack included; given a base graph, recipe 35 with `{ base, detail: true }`, and with `{ base, pack: id, detail: true }` for every Pack; given run records, recipe 45 with `{ runs }`.", + "a build evaluates each of these once, with no parameter unless one is named: recipes 17, 20, 24, 31, 33, 43, 44, 46, 51 and 53; recipe 52 with `{ detail: true }`; recipe 29 with `{ pack: id }` and recipe 54 with `{ pack: id, detail: true }` for every Pack; recipe 55 with `{ pack: id }` for every Pack whose manifest states `order: priority`; recipe 21 with `{ spec: id }` and recipe 30 with `{ specs: [id] }` for every Spec; recipe 34 with `{ spec: id }` for every Spec that is not an example, Specs in no Pack included; given a base graph, recipe 35 with `{ base, detail: true }`, and with `{ base, pack: id, detail: true }` for every Pack; given run records, recipe 45 with `{ runs }`.", pageRecipes: - "the index and the glossary render reader values only; the architecture lens renders recipes 17 and 43; the tests lens renders every recipe 34 answer, recipe 44, and recipe 45 given run records; a Spec page renders its recipe 21, 30 and 34 answers, its own space from recipe 44, the recipe 51 rows that cite its entries, the recipe 45 files whose `specs` hold its id, and its own row of recipe 35's whole-corpus answer; a Pack page renders the totals of its recipe 29 answer and, given a base graph, its own recipe 35 answer; the registers render recipes 20, 31, 24 beside recipe 51's rows, and 33; the owner queue renders recipe 46; the diff page renders recipe 35's whole-corpus answer; the runs page renders recipe 45; an export renders the values of the pages it holds.", + "the index renders reader values and each Spec's `epicIds` from recipe 52; the glossary renders reader values only; the capabilities lens renders recipe 52; the architecture lens renders recipes 17 and 43; the tests lens renders every recipe 34 answer, recipe 44, and recipe 45 given run records; a Spec page renders its recipe 21, 30 and 34 answers, its own space from recipe 44, the recipe 51 rows that cite its entries, the recipe 45 files whose `specs` hold its id, and its own row of recipe 35's whole-corpus answer; a Pack page renders the totals of its recipe 29 answer and, given a base graph, its own recipe 35 answer; a slice page renders its Pack's recipe 54 answer; the roadmap page renders recipe 53 and each roadmap's recipe 55 memo; the registers render recipes 20, 31, 24 beside recipe 51's rows, and 33; the owner queue renders recipe 46; the diff page renders recipe 35's whole-corpus answer; the runs page renders recipe 45; an export renders the values of the pages it holds.", }, }, deliveryFacts: ["implemented", "has-verifier"], @@ -1898,17 +1900,17 @@ export const consumersSpecs = [ specKind: "behavior", altitude: "story", readiness: "ready", - title: "Four lenses read the graph as Packs, architecture, tests and evidence", + title: "Five lenses read the graph as Packs, capabilities, architecture, tests and evidence", narrative: - "Each lens is a page with a Spec of its own, so each builds on the graph it reads today: the Packs, architecture and tests lenses read what the graph records, and the evidence lens reads not tracked until the graph records an observation. This Spec keeps what the four share.", + "Each lens is a page with a Spec of its own, so each builds on the graph it reads today: the Packs, capabilities, architecture and tests lenses read what the graph records, and the evidence lens reads not tracked until the graph records an observation. This Spec keeps what the five share.", sections: { intent: { outcome: - "Let a person see the graph four ways, by Pack, by architecture, by verification and by evidence, from one top navigation.", + "Let a person see the graph five ways, by Pack, by capability, by architecture, by verification and by evidence, from one top navigation.", }, behavior: { rules: [ - "The lenses are pages of the page set, reached from the top navigation in the order Packs, architecture, tests, evidence; the index page is the Packs lens and the default landing page.", + "The lenses are pages of the page set, reached from the top navigation in the order Packs, capabilities, architecture, tests, evidence; the index page is the Packs lens and the default landing page.", "A lens renders reader values and the recipe answers its own Spec names, and computes none of them in the page.", "A lens writes nothing and states nothing: it lists, groups, draws and links, and a change a person wants from it leaves through the composing panel as scoped intent.", ], @@ -1920,7 +1922,7 @@ export const consumersSpecs = [ }, ui: { lensPages: - "`index.html` for the Packs lens, `architecture.html`, `tests.html` and `evidence.html`, each opening with its name and one sentence saying what it reads.", + "`index.html` for the Packs lens, `capabilities.html` for the capabilities lens, `architecture.html`, `tests.html` and `evidence.html`, each opening with its name and one sentence saying what it reads.", }, }, deliveryFacts: ["implemented", "has-verifier"], @@ -1982,7 +1984,7 @@ export const consumersSpecs = [ rules: [ "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 static page set: the index page, which is the Packs lens; a search page; a page for each other lens, each register, the owner queue and the glossary; one page per Pack and per Spec; one export file per Pack; and, when the build is given their inputs, a diff page and a runs page; with their local assets. Its build renders each page whole, so a page needs no server, and every page but the search page reads without its script, which adds filtering, search, copying, pan and zoom, and the composing panel.", + "The Studio is a static page set: the index page, which is the Packs lens; a search page; a page for each other lens, each register, the owner queue, the roadmap and the glossary; one page and one slice page per Pack, one page per Spec; one export file per Pack; and, when the build is given their inputs, a diff page and a runs page; with their local assets. Its build renders each page whole, so a page needs no server, and every page but the search page reads without its script, which adds filtering, search, copying, pan and zoom, and the composing panel.", "The Studio works offline: the same files open from the file system through `file://` for local use and from any static host for sharing.", "The Studio is reproducible: one graph and the same inputs render byte-identical files, and an asset whose filename embeds a hash of its content gets the same name from the same graph.", "The Studio is read-rich: everything the graph holds about a Spec or a Pack is explorable in it.", @@ -2153,9 +2155,9 @@ export const consumersSpecs = [ }, behavior: { rules: [ - "After the lens entries, the top navigation lists Queue, Questions, Decisions, Declarations, Backlog and Glossary, then Diff when the build was given a base graph and Runs when it was given run records, each linking its page; the search field follows them.", + "After the lens entries, the top navigation lists Roadmap, Queue, Questions, Decisions, Declarations, Backlog and Glossary, then Diff when the build was given a base graph and Runs when it was given run records, each linking its page; the search field follows them.", 'The top-navigation link to the page being read carries `aria-current="page"` and an underline, so it shows by more than color.', - "After its four routes, the index's `Start here` list adds See what the owner can state first, linking the owner queue; See the components by context and layer, linking the architecture lens; and, given a base graph, See what changed since the base, linking the diff page.", + "After its four routes, the index's `Start here` list adds See what comes next, linking the roadmap page; See what the owner can state first, linking the owner queue; See the components by context and layer, linking the architecture lens; and, given a base graph, See what changed since the base, linking the diff page.", ], ruleKeys: { topEntries: "rules[0]", @@ -2189,7 +2191,7 @@ export const consumersSpecs = [ "Inside the core's sections the page adds a composing action beside each keyed entry that proposes new text for it, the composing actions beside each open question, the rule coverage beside each keyed rule or flow and the example cards after the rules, each constraint's target, the citations beside each cited entry, and an element id on each keyed term, as the rules below state.", "Each keyed Model term in the core's Model section carries the element id `term.`, its address after the `#`, and shows its key and its recorded line, as the core's entry ids do.", "Beside each keyed entry an anchored source file cites, its section lists every citing file and line recipe 51 records, as text with a copy action; a pinned declaration no file cites reads `no citation recorded`.", - "The Design view panel draws the realization diagram of the units that satisfy the Spec, their components and what they use, and then lists the decision records the Spec names through `decidedBy`, each with its title linked and its decision text inline.", + "The Design view panel draws the requirement drawing defined by spec:consumers.spec-studio.diagram-kinds.requirement-trace in place of realization, then lists the decision records the Spec names through decidedBy with linked titles and decision text inline. The Runtime panel retains the implementation anchors' components and uses.", "The Runtime panel shows the structural neighborhood of the Spec's implementation anchors: each anchor's component with its layer and context, and the units it uses, each with its file and line; runtime observation reads not tracked.", "The Impact panel draws the Spec's trace graph, one hop each way, and then lists what it rests on with each target's stated and derived rung, from recipe 21, and every Spec that rests on it to any depth, by distance, with the units, enabled verifiers and Packs each brings, from recipe 30.", "Given a base graph, a line under the header names the base and lists the Spec's changed parts, as `spec:consumers.spec-studio.diff-page` states.", @@ -2231,7 +2233,7 @@ export const consumersSpecs = [ verificationPanel: "the verifier list, with run evidence given run records, then the coverage grid when the Spec owns an example space, laid out as `spec:consumers.spec-studio.verification-panels` states; the panel's element id is `panel.verification`.", designViewPanel: - "the realization diagram, drawn as SVG whose nodes show their id, source file and line as the source-text rule states, then the decision records with their decision text; the panel's element id is `panel.design-view`.", + "The requirement drawing, with the text equivalent naming each binding's id, source file and line under the source-text rule, then the decision records and their decision text; its element id remains `panel.design-view`.", runtimePanel: "the structural neighborhood of the Spec's implementation anchors, each anchor's component and the code units it uses, with file and line, then `Runtime observation: not tracked`; the panel's element id is `panel.runtime`.", evidencePanel: @@ -2633,7 +2635,7 @@ export const consumersSpecs = [ 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 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 Protocol takes the member filter and the order and leaves each member's content whole; emphasis inside a member stays the reader's.\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.", + "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 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 Protocol takes the member filter and the order and leaves each member's content whole; emphasis inside a member stays the reader's.\n\nThe owner ruled the word: perspective. The Spec Studio keeps lens for its graph views, its Packs, capabilities, 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: @@ -2864,7 +2866,7 @@ export const consumersSpecs = [ "Inside the Design and UI sections, the open questions and other keyed sections apart from rules and flows, it compares keyed entries by key and reports each as added, removed, changed in value or moved in order.", "Rule and flow entries match first by unchanged text, preferring the same key for repeated wording, then by retained key; remaining unkeyed base entries pair in list order, including an entry reworded while adding a key. An unmatched keyed base entry is removed, and an unmatched head entry is added. Other unkeyed entries have no identity across the two graphs and compare as part of their whole section.", "A rule or flow with unchanged text whose key was added, removed or changed is one `keyed` change naming its old and new address, or none; only a text change is `changed`. An unchanged entry moving between rules and flows is `moved`; when one graph keeps no order across rules and flows, keyed entries compare by order within each list and a `ruleOrder` diagnostic names the Spec.", - "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.", + "It reports changes to Pack membership, member order and what a Pack's order means, 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 the two graphs and confers nothing. It grants no approval, raises no staleness flag, moves no readiness and never records that a pass happened.", ], }, @@ -2880,7 +2882,7 @@ export const consumersSpecs = [ movedEntry: "a keyed entry whose value is unchanged and whose position among the keyed entries of its section changed is `moved`; an entry whose value changed is `changed` whatever its position. For rules and flows, unchanged text with a different key takes `keyed` precedence, then a list change or relative order change among retained entries is `moved`; anonymous entries compare order within their list. Adding or removing a key alone moves no other entry.", packRows: - "`packs` rows are `{ id, change, membersAdded, membersRemoved, reordered }` for each Pack added, removed or changed in membership or order.", + '`packs` rows are `{ id, change, membersAdded, membersRemoved, reordered, orderChanged, orderBefore, orderAfter }` for each Pack added, removed or changed in membership, member order or order meaning; `orderBefore` and `orderAfter` are `"reading"` or `"priority"`, null on a side where the Pack is absent, and `orderChanged` is true when both sides hold the Pack and the two differ. An absent `order` reads as `"reading"` in every schema version; a value the head\'s engine does not know is outside the common schema, listed in `diagnostics` and not compared; that Pack\'s membership and member order still compare, its `orderChanged` is false, and the side holding the unknown value reads null. `membersAdded` and `membersRemoved` keep their respective manifest order.', bindingRows: "`bindings` rows are `{ id, implementedBy, referencedBy, verifiers, deliveryFacts }`, each an `{ added, removed }` pair of ids or fact names, kept apart from `specs` because they derive from anchors.", baseRecipe: @@ -3094,11 +3096,11 @@ export const consumersSpecs = [ id: "spec:consumers.projections-model.roadmap-views", specKind: "model", altitude: "story", - readiness: "idea", + readiness: "scoped", file: "specs/consumers/projections-model.roadmap-views.sdp.md", title: "Roadmap views read releases, baselines and phases from git and the graph", narrative: - "Release, baseline and roadmap vocabulary belongs to optional projections that wait until a reader needs them.", + "Release, baseline and roadmap vocabulary belongs to optional projections that wait until a reader needs them. The owner, choosing what the next pull requests take up, is the reader the roadmap view waited for, and the roadmap reading answers it: a Pack whose manifest states a priority order, read with what each entry still needs, shown on the Studio's roadmap page and rendered as a pull request's forward section. Release and baseline views still wait for a reader of their own.", sections: { intent: { outcome: @@ -3106,12 +3108,13 @@ export const consumersSpecs = [ openQuestions: [ { question: - "Does a reader need release, baseline or roadmap views that the shipped projections, the recipes and git do not serve?", + "Does a reader need release or baseline views that the roadmap, the recipes and git tags do not serve?", blocking: true, - key: "namedRoadmapReader", + key: "releaseReader", decider: "owner", - trigger: - "a named reader with that need, and a way to measure it inside the arc that builds the views", + provisional: + "Keep release and baseline views deferred; the trigger opens this question and does not authorize either view's build.", + trigger: "the first published release", }, ], }, @@ -3120,8 +3123,10 @@ export const consumersSpecs = [ 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.", + roadmap: + "A Pack whose manifest states a priority order, read with what each entry still needs next; its order is an advisory preference, never a schedule or a gate.", "phase / iteration / milestone": - "Descriptive vocabulary for optional roadmap projections, never gates or enforced sequences.", + "Descriptive words a roadmap's framing may use, never an entry field, a date, a gate or an enforced sequence.", }, }, }, @@ -3629,7 +3634,7 @@ export const consumersSpecs = [ specProperties: "`aliases` (the id, then the title), `title`, `specKind`, `altitude`, `statedReadiness`, `derivedReadiness` (`none` when no rung derives), `statedNextRung` (left out when the Spec states `ready`), `statedNextRungFailures` (the failing clause ids), `implementationBinding`, `verifierBinding`, `designEntries`, `declarations`, `openQuestions` (a count), `blockingQuestions`, `decisions` (links to the notes of the resolving decisions the design columns name), `packs` (links to the Pack notes), `carrier` (the root-relative carrier path).", packProperties: - "`aliases` (the id, then the title), `title`, `manifest` (the root-relative manifest path).", + '`aliases` (the id, then the title), `title`, `manifest` (the root-relative manifest path), and optional `order`, present as `"priority"` only when the Pack states it.', codeProperties: "`aliases` (the id), `label`, `role`, `layer`, `context`, `component` (a link to the component's note), `sourceFile` (root-relative), `sourceLine`.", termProperties: @@ -3643,7 +3648,7 @@ export const consumersSpecs = [ bindingLines: "under `## Bindings`, `- Implementation binding: present` or `none` with each realizing unit, the units that reference the Spec, `- Verifier binding: present` or `none` with each verifier, `- Expected-outcome oracle: present` or `none`, and `- Runtime observation: not tracked`.", packNoteBody: - "the banner, `# `, the framing, `## Members, filterable` with the Pack's Base embedded, `## Members, in reading order` as a numbered list in manifest order, each a link with its stated rung, floor and stated next rung or `nothing blocks`, then `## Boundary` with the reader's four boundary lists and `## Findings`.", + "the banner, `# <title>`, the framing, `## Members, filterable` with the Pack's Base embedded, `## Members, in advisory priority order` when `order` is `\"priority\"`, otherwise `## Members, in reading order`, as a numbered list in manifest order, each a link with its stated rung, floor and stated next rung or `nothing blocks`, then `## Boundary` with the reader's four boundary lists and `## Findings`. A roadmap states `Priority confers no permission or sequencing requirement.` before its ordered list heading.", codeNoteBody: "the banner, `# <id>`, the label, `` `<file>:<line>` ``, then link lists under `## Member of`, `## Members`, `## Uses`, `## Used by`, `## Satisfies`, `## References`, `## Verifies` and `## Models`, each left out when empty.", termNoteBody: @@ -4286,7 +4291,7 @@ export const consumersSpecs = [ "A short list then routes by task, in fixed words that quote nothing from the corpus: read a Pack's design, find a Spec, entry or file by a word, narrow the Specs, and read the findings; each route links to its place in the page set.", "The Packs section lists every Pack in id order with its title and its members in authored order, each member with its stated rung beside its floor; it draws no capability tree.", "The Specs section lists every Spec in id order, one row each: its title and id, kind and altitude, stated rung beside the floor reached, implementation binding and verifier binding as present or none, open and blocking questions, and its Packs.", - "Filters narrow the Specs section by kind, altitude, stated rung, floor reached, implementation binding, verifier binding and Pack; each filter takes one value or any, the filters combine, and the section says how many of its rows it shows. Without script every row shows.", + "Filters narrow the Specs section by kind, altitude, stated rung, floor reached, implementation binding, verifier binding, Pack and epic; each filter takes one value or any, the epic filter also takes no epic, the filters combine, and the section says how many of its rows it shows. Without script every row shows.", "The findings section lists the validation report's findings in its order, each with its severity in words, its validator id, its subject when it has one, linked to its page when it is a Spec or a Pack and as text otherwise, its message, and its recorded file and line as text.", ], ruleKeys: { @@ -4331,8 +4336,8 @@ export const consumersSpecs = [ }, behavior: { rules: [ - "The page opens with the Pack's title, its id and its carrier file as text with a copy action, then the summary; the framing follows as prose under the heading Framing, with the vocabulary references, linked.", - "The summary states each Pack total in one sentence that opens with its count, never as a table: the members and the unresolved ones, the members held below their stated next rung and those waiting only for their author, the design entries and pinned declarations, the open and blocking questions, the members with an implementation binding and with a design reference, and the boundary in each direction.", + "The page opens with the Pack's title, its id and its carrier file as text with a copy action, and a link to the Pack's slice page, then the summary; the framing follows as prose under the heading Framing, with the vocabulary references, linked.", + "The summary states each Pack total in one sentence that opens with its count, never as a table: the members and the unresolved ones, the members held below their stated next rung and those waiting only for their author, the design entries and pinned declarations, the open and blocking questions, the members with an implementation binding and with a design reference, and the boundary in each direction. The member sentence says `in priority order` when the Pack states `order: priority`, otherwise `in authored reading order`.", "The summary's held sentence links each held member to its card with its count of unmet clauses, and its waiting sentence names the members whose stated next rung waits only for their author's statement unless every member does; the page never presents a rung as approved.", "The members follow in authored order, one closed card each. A card's summary line shows the member's position, title and kind, its stated rung beside the floor reached, its stated next rung with `nothing blocks` or how many of its clauses are unmet, its open and blocking questions, and its implementation and verifier bindings as present or none.", "An open card shows the member's id, linked to its Spec page, then the parts the next five rules state, in their order.", @@ -4364,7 +4369,7 @@ export const consumersSpecs = [ }, ui: { headerBlock: - "the title as the page heading; below it the id and the carrier file, each with a copy action; then the summary; then the framing under a `Framing` heading, and the vocabulary references.", + "the title as the page heading; below it the id and the carrier file, each with a copy action, and the link `Read this Pack as one design` to its slice page; then the summary; then the framing under a `Framing` heading, and the vocabulary references.", summaryList: "the totals as a short list of sentences under a `Where the design stands` heading, each opening with its count, a zero written as No.", memberCard: @@ -4388,7 +4393,7 @@ export const consumersSpecs = [ readiness: "ready", title: "A Pack page's maturity map places every member by its stated rung and its floor", narrative: - "A Pack of many members takes a reviewer a long scroll through its cards before they know where the design stands. The maturity map answers that first: one table, a row per stated rung and a column per floor reached, each member a short card in its cell. A member right of its row's own rung waits only for a statement the floor already allows; a member left of it states more than its floor holds.", + "A Pack's maturity map shows its distribution before its member lists. Equal-sized cells place each resolved member by stated readiness and floor reached. Populated cells show exact counts and member marks; faint empty cells show no number. The count table and ordered lists retain the reading in text.", sections: { intent: { outcome: @@ -4396,7 +4401,7 @@ export const consumersSpecs = [ }, behavior: { rules: [ - "After the summary, the maturity map places each resolved member by its stated rung and the floor it reaches: one row per stated rung, idea to ready, and one column per floor, none to ready, each filled cell listing its members in authored order as short cards linked to their member cards, with its count, and an empty cell showing `—` alone; the table caption explains in words where the floor sits above the statement and where the statement sits above the floor.", + "After the summary, show the categorical grid and full member lists defined by spec:consumers.spec-studio.diagram-kinds.maturity-picture. Every resolved member belongs to one stated-rung and floor cell; unresolved members are named apart. Populated cells link to their built lists, whose members stay in authored order.", ], ruleKeys: { maturityMap: "rules[0]", @@ -4404,7 +4409,7 @@ export const consumersSpecs = [ }, ui: { maturityMap: - "an `sdp-maturity-map` holding a table under a `Maturity map` heading, rows headed `stated idea` to `stated ready`, columns headed `floor none` to `floor ready`, each filled cell its count and its members as short cards, an empty cell reading `—` alone, and the axis explanation in the table's caption, never inside a header cell: right of the matching rung the floor is above the statement and already allows it; left of it the statement is above the floor.", + "An `sdp-maturity-map` holds the static SVG grid, caption and accessible count table, followed by populated cells' full member lists in disclosures that print open. Equal-sized cells have explicit axes; the legend names matches, floor above statement and statement above floor once. The diagonal has an outline, empty cells are faint, and populated cells carry exact counts and member marks, up to `100` marks and solid fill above that. Every member links to its card.", }, }, deliveryFacts: ["implemented", "has-verifier"], @@ -4537,7 +4542,7 @@ export const consumersSpecs = [ "Every command that enters the shared build, `sdp studio` among them, removes `generated/spec-studio/` and its temporary sibling before extraction, beside the four roots `spec:consumers.wholesale-view-rewrite#rule.buildInvalidates` names; a removal failure refuses the command before extraction.", "`sdp studio --check-clean` renders an independent twin and refuses divergent renders, then compares the current root with the new render; missing or drifted output returns nonzero and is removed, and clean output is replaced wholesale with byte-identical content.", "Findings never withhold the page set: a run over a graph with validation findings still publishes and returns the validation exit code, and each finding shows on the index, and also on its subject's page when its subject is a Spec or a Pack. A run whose extraction fails publishes nothing and returns nonzero.", - "The page set holds `index.html`, `search.html`, one `packs/<name>.html` per Pack, one `specs/<name>.html` per Spec, and `assets/studio.css` and `assets/studio.js`. `<name>` is the id after its namespace, with each uppercase ASCII letter written as `_` and its lowercase, so no two pages collide on a file system that ignores case.", + "The page set holds `index.html`, `search.html`, one `packs/<name>.html` and one `slices/<name>.html` per Pack, one `specs/<name>.html` per Spec, and `assets/studio.css` and `assets/studio.js`. `<name>` is the id after its namespace, with each uppercase ASCII letter written as `_` and its lowercase, so no two pages collide on a file system that ignores case.", "Two runs over one graph and the same inputs write byte-identical files, and no file holds a time, a commit, an absolute path or a host name of the run's own.", "Every link a page writes stays inside the page set: it resolves to a page the run wrote and, with a fragment, to an element id that page carries, apart from the search page's term fragment `search.html#q=<term>`, which names no element. No page links a carrier or source file, an editor URI or an external address; the only link that leaves the page set is one the stamp element adds in the browser from a present stamp's source template.", ], @@ -4688,10 +4693,10 @@ export const consumersSpecs = [ searchResults: "`sdp-search-results`: on the search page, reads the index from the `sdp-data` element, matches the fragment's term and each term the person types, writes the results `spec:consumers.spec-studio.search` designs, and keeps the term in the fragment.", filter: - '`sdp-filter`: wraps a hidden form of labelled `select` elements, each with `data-filter="<name>"` and an `any` option, and the rows marked `data-row`. On upgrade it shows the form, hides each row whose `data-<name>` differs from a chosen value, and writes `<shown> of <total> shown` in its `output` element; a row whose attribute holds several space-separated values, such as its Packs, matches when one of them equals the choice.', + '`sdp-filter`: wraps a hidden form of labelled `select` elements, each with `data-filter="<name>"` and an `any` option, and the rows marked `data-row`. On upgrade it shows the form, hides each row whose `data-<name>` differs from a chosen value, and writes `<shown> of <total> shown` in its `output` element; a row whose attribute holds several space-separated values, such as its Packs, matches when one of them equals the choice. When a followed link or a fragment change targets an element inside a row it hides, it returns every control to `any`, adds to its output that the filter was cleared to show that target, and scrolls the target into view.', expander: "`sdp-expander`: adds `Open all` and `Close all` buttons for the `details` elements inside it, and on load and on each fragment change opens every `details` that holds the fragment's target and scrolls the target into view.", - copy: "`sdp-copy` with `text`: a `Copy` button whose accessible name includes the text; it copies the text to the clipboard and says `Copied`, and where the clipboard is unavailable it selects the text so the person can copy it.", + copy: "`sdp-copy` with `text`, or with `for` naming a text area on its page: a `Copy` button whose accessible name includes the text or the text area's label; it copies that text and says `Copied`, and where the clipboard is unavailable it selects the text so the person can copy it.", stamp: "`sdp-stamp` with `page-root`: served over http or https, it requests `<page-root>stamp.json` and, when it loads, shows its corpus commit, engine and build time as text; otherwise it shows nothing.", }, @@ -4934,7 +4939,7 @@ export const consumersSpecs = [ guideFile: "`docs/agent-surface/upgrade-guide.md`, shipped in the package beside the recipe catalog, listed in the package's files and named by `sdp --help` among the shipped on-ramps.", guideSections: - "in this order: re-pin the package and check `sdp --version`; measure the corpus with capability adoption (recipe 47); key the rules with the rule-key proposal (recipe 48), check the proposal with the proposal check (recipe 38) and apply it through an agent's edit; for every capability in recipe 47's answer, one subsection citing its `carriedBy` Spec and recipe 47 as the measurement, naming a further recipe only when the subsection teaches a distinct next action with it and otherwise sending the reader to the carrying Spec; move rules files onto shipped recipes, recipes 25, 26, 42, 50 and 51 and the `recipes` binding; read large Packs in parts and order the waiting statements, recipes 36 and 46; publish every root with `sdp generate`; and pass a recipe's subject as `--params` data, the recipe-caller guidance the README carries.", + "in this order: re-pin the package and check `sdp --version`; measure the corpus with capability adoption (recipe 47); organize features in epics, reading the capability map (recipe 52) and citing `spec:consumers.capability-map`; `Model upcoming work in the graph; keep review history in git and pull requests`, a roadmap Pack read with recipes 53 and 55 and intended design proposed as a change proposal checked by recipe 38, citing `spec:consumers.roadmap`, with the guidance `Unresolved intended design lives in open questions. Review disposition and acceptance remain in git and the pull request; neither becomes a graph field.`; key the rules with the rule-key proposal (recipe 48), check the proposal with the proposal check (recipe 38) and apply it through an agent's edit; for every capability in recipe 47's answer, one subsection citing its `carriedBy` Spec and recipe 47 as the measurement, naming a further recipe only when the subsection teaches a distinct next action with it and otherwise sending the reader to the carrying Spec; move rules files onto shipped recipes, recipes 25, 26, 42, 50 and 51 and the `recipes` binding; read large Packs in parts and order the waiting statements, recipes 36 and 46; publish every root with `sdp generate`; and pass a recipe's subject as `--params` data, the recipe-caller guidance the README carries.", }, }, deliveryFacts: ["implemented", "has-verifier"], @@ -5352,7 +5357,7 @@ export const consumersSpecs = [ readiness: "ready", title: "A diagram is laid out from graph values into SVG with a text equivalent", narrative: - "The Studio draws four diagrams: the architecture lens's components by context and layer, one component's members and what they use, one Spec's relations, and on a Spec page the code that realizes it. Each is laid out in the engine when the page set is built, so a page draws it with no script and the same graph draws the same bytes. Words carry every reading the drawing shows: bands and columns are labelled, a planned or folded box says so, and a text equivalent after the drawing states every box and edge.", + "Every Studio diagram is laid out in the engine when the page set is built, so a page draws it with no script and the same inputs draw the same bytes. The architecture grid, component members and one-hop trace retain their readings; the Spec page's requirement drawing replaces realization, and other pages name their supplied drawing answers. Words carry every reading the picture shows, and its text equivalent states every box and edge.", sections: { intent: { outcome: @@ -5361,22 +5366,21 @@ export const consumersSpecs = [ behavior: { rules: [ "The build lays out each diagram and writes it into its page as inline SVG; no diagram is laid out in the browser, and a page draws every diagram with no script.", - "A layout reads only the diagram's boxes, each with its id and its words, and its edges, each with its two ends and its kind, taken from the reader values and recipe answers its page names; it reads no font, file, clock or random value, measures text by character count, and writes whole-pixel coordinates.", + "A layout reads only the boxes, each with its id, words and supplied grouping, and edges, each with its two ends, exact kind and claim, optional reason and aggregation count, taken from the reader values and recipe answers its page names; it reads no font, file, clock or random value, measures text by character count and writes whole-pixel coordinates. Depth, reach and component resolution belong to those answers, never layout.", "One graph and the same inputs draw byte-identical SVG on every build and every machine.", "The architecture diagram places each component in the column of its recorded context, contexts in code-unit order and then a column for components that state none, and in the band of its layer, in the order edge, application, domain, adapter, infrastructure, then a band for components that state none; a cell holds its components in id order.", "An architecture edge joins two components and carries the count of unit `uses` edges between them, as the component rows of `spec:consumers.spec-studio.lenses.architecture#design.componentRows` count them: each end of a unit `uses` edge resolves to its component, directly or through `memberOf`, and a use within one component draws nothing.", "A trace graph draws the Spec in a band of its own, the Specs its declared relations name above it in one band per relation type, in the order refines, dependsOn, constrainedBy, decidedBy, verifies, supersedes, and the Specs whose declared relations name it below in bands of the same order; a Spec related more than one way sits in the band of its first relation, and each relation is its own edge.", "A member diagram draws a component's members in bands by how deep `uses` among members reaches, a member that uses another sitting above it, and the units and components outside the component that members use in a last band of their own.", - "A realization diagram draws the units that satisfy the Spec, then their components, then what those units use, in three bands.", "A diagram draws at most the boxes and edges the bound names.", "An architecture diagram over the bound with more than one context draws one box per context, with the uses between contexts aggregated and counted, and then each context's own diagram in a section of its own, where every other context it uses or is used by is one folded box in a band of its own.", "A diagram of one context still over the bound draws one box per layer of that context, and then each layer's components in a section of their own.", "A drawing still over the bound after these folds, the context overview, one context's diagram or one layer's, writes no SVG; its text equivalent lists every box and edge the drawing would hold and then says the drawing was omitted because it exceeds the bound.", "A trace graph over the bound folds its largest relation group first, ties broken by direction and then by relation type, into one box that names the count and the relation and links to the Spec page's relations, until the drawing fits.", - "A member or realization diagram over the bound writes no SVG; its text equivalent lists every member or unit and every use or realization edge, and then says the drawing was omitted because it exceeds the bound.", + "A member diagram over the bound writes no SVG; its text equivalent lists every member and every use edge, and then says the drawing was omitted because it exceeds the bound.", "Every reading a fill, outline or line style marks is also in words: bands and columns carry labels, a planned component reads `planned`, a folded box reads `folded`, and an aggregated edge's count is in its text equivalent. A drawing whose edges all have one relation names it once in its legend and draws no edge label; solid `uses` and dashed `plans to use` share the use relation, with the planned wording also in the legend. A drawing with several relations keeps its labelled bands and edge labels.", "Each diagram is followed by its text equivalent, which states every box with its words and every edge as a sentence naming its two ends and its kind, and the SVG names it as its description.", - "The diagram element adds only pan, zoom and pinch, by rewriting the SVG's view box; it never moves a box or an edge. With or without script the SVG is never wider than its container or larger than its own size on first view at every viewport width; pan and zoom enlarge it for reading.", + "The diagram element adds only pan, zoom and pinch by changing the SVG's view box; it never moves a box or edge. At viewport widths of at least 720 CSS pixels the SVG starts at natural size inside a keyboard-accessible scroll region, with overflow inside the figure. Below that width the responsive contract fits the same drawing to the screen. Reset restores the initial view and scroll position. Without script the drawing, scrolling, links and text equivalent remain usable. Print fits the drawing to paper and exposes its text equivalent.", ], ruleKeys: { buildTime: "rules[0]", @@ -5386,16 +5390,15 @@ export const consumersSpecs = [ aggregatedUses: "rules[4]", relationBands: "rules[5]", memberBands: "rules[6]", - realizationBands: "rules[7]", - bound: "rules[8]", - foldByContext: "rules[9]", - foldByLayer: "rules[10]", - boundFallback: "rules[11]", - foldGroups: "rules[12]", - noFoldBelowComponent: "rules[13]", - wordsBesideStyle: "rules[14]", - textEquivalent: "rules[15]", - panZoomOnly: "rules[16]", + bound: "rules[7]", + foldByContext: "rules[8]", + foldByLayer: "rules[9]", + boundFallback: "rules[10]", + foldGroups: "rules[11]", + noFoldBelowComponent: "rules[12]", + wordsBesideStyle: "rules[13]", + textEquivalent: "rules[14]", + panZoomOnly: "rules[15]", }, exampleSpace: { given: [ @@ -5411,26 +5414,34 @@ export const consumersSpecs = [ }, }, design: { + edgeRouting: + "Reserve boxes and labels as obstacles, route orthogonally with distinct ports and lanes eight pixels apart, and never share a collinear segment or overlap labels. Crossings are permitted only at free points. Keep exact relations and claims through the words-beside-style rule. A deterministic finite routing failure omits the SVG, opens its text equivalent and explains the failure; the build owns search mechanics.", + shortId: + "Drop the namespace and leading dotted segments every id in the figure shares, marking elision with `…`; keep more segments on collisions. A displayed id carries a namespace only when shown in full. Full ids remain in link titles, the text equivalent and source.", + widthBudget: + "`880` logical pixels for a band or grid, band labels included above their band; `264` per context cell when several contexts share a grid, the full band budget when only one context is shown, and `24` between boxes. Routing gutters may widen the SVG to preserve distinct lanes. A drawing wider than `880` scrolls inside its figure and receives the build-written instruction `Scroll horizontally to read the whole drawing.`", bound: "`40` boxes and `100` edges per diagram, counting every box a drawing holds, folded and planned boxes included, and every edge it draws.", + density: + "For requirement, trace, capability and dependency drawings, trigger the existing folds before the hard bound when deterministic layout height exceeds `2000` logical pixels. Measure height from the box and lane layout before routing, using the same label metrics and width budget as the drawing. A fold triggered by these limits must strictly lower that height and hold at least two boxes, keeping the same scope, claims and complete text equivalent. If no lawful fold fits the height budget, omit the SVG and open the text equivalent.", labelMetrics: - "a label line holds at most `30` characters, an id longer than that wraps after a dot or a colon and then after a hyphen, a character advances `7.5` pixels at the 12 pixel monospace label size, and a box is as wide as its longest line plus padding.", + "A title or recorded label leads at fourteen pixels, followed by the displayed id at twelve pixels and remaining words such as a component's member count. Title lines hold at most `28` characters and id lines `30`, with an eight-pixel character advance. Wrap at spaces or id punctuation, then by code point, and calculate box height from every line.", cellWrap: - "a grid cell or a band places at most `6` boxes on one line and wraps onto the next.", + "Wrap a cell's boxes within the fixed logical width budget instead of a fixed box count; retain context columns with local scrolling when they exceed it. Layout reads no browser or font measurement.", edgeKinds: "`uses` for a realized use, drawn solid; `plans to use` for a use a component design states, drawn dashed; on a trace graph the relation's type; an arrow points from the unit or Spec that uses or declares to the one it names.", svgMarkup: - 'the `svg` carries `role="img"`, a `title`, `aria-describedby` naming its text equivalent and a `viewBox` of its own size; a box is a `g` with class `node` and `data-id`, plus `data-planned="true"` or `data-folded="true"` when it is planned or folded, holding a link to the box\'s target; an edge is a `g` with class `edge`, `data-from`, `data-to` and `data-kind`; a band is a `g` with class `band` and `data-label`, a column a `g` with class `lane` and `data-label`, each also drawing its label as text.', + 'the `svg` carries `role="img"`, a `title`, `aria-describedby` naming its text equivalent and a `viewBox` of its own size; a box is a `g` with class `node` and `data-id`, plus `data-planned="true"` or `data-folded="true"` when it is planned or folded, holding a link to the box\'s target; an edge is a `g` with class `edge`, `data-from`, `data-to` and `data-kind`; a band is a `g` with class `band` and `data-label`, a column a `g` with class `lane` and `data-label`, each also drawing its label as text. An edge also carries `data-claim` and a stable `data-edge-key` when its label uses a legend key; route and label coordinates are presentation values and mint no graph edge.', boxTargets: "a component's box links to its detail on the architecture lens, a planned component's to its component design's Spec page, a context's or layer's folded box to its section on the same page, a Spec's to its page, and a folded relation group's to the Spec page's relations.", textEquivalent: - "a `details` element closed by default, summary `The diagram as text`, holding one `li` per box, `<id>: <words>`, and then one `li` per edge, `<from> <kind> <to>`, followed by `(<n> unit edges)` when the edge aggregates more than one.", + "a `details` element closed by default, summary `The diagram as text`, holding one `li` per box, `<id>: <words>`, followed by `· folded members: <ids>` when folded identities are present, and then one `li` per edge, `<from> <kind> <to>`, followed by `· <claim>` when present, `(<n> unit edges)` when the edge aggregates more than one, and `— <reason>` when present.", diagramElement: - "`sdp-diagram` with `subject`, the Spec, Pack, component or context id the diagram is rooted at, or `architecture`, wrapping one `figure` that holds the caption, a legend `Arrows: <relation>.` when all edges share one relation, followed by `Dashed arrows: plans to use.` when that use relation includes planned uses, the SVG and its text equivalent; on upgrade it adds `Zoom in`, `Zoom out` and `Reset` buttons and handles drag, a wheel turned with Control or Command held, and a two-finger pinch.", + "`sdp-diagram` with `subject`, the rooted Spec, Pack, component or context id, or `architecture`, wraps one figure containing its caption and legend, any build-written scroll instruction, a keyboard-focusable named `.diagram-scroll` region containing the SVG, its text equivalent and, when offered, its closed Mermaid-source disclosure with labelled read-only textarea and `sdp-copy`. Caption, legend and source stay outside the scroll region. On upgrade it adds Zoom in, Zoom out and Reset buttons and handles drag, a wheel with Control or Command held, and pinch; it adds no layout or copy behavior.", elementIds: "a diagram's text equivalent has the id `diagram.<subject>.text` and its arrow has the id `diagram.<subject>.arrow`. A page that draws a second view of one subject inserts that view's name before `text` or `arrow`, so the Spec page's realization drawing uses `diagram.<subject>.realization.text` and `diagram.<subject>.realization.arrow`; its trace graph keeps the unqualified ids. Every id is unique on its page.", omittedDrawing: - "a drawing over the bound keeps its `figure` and caption, holds the sentence `Shown as text: this drawing exceeds the bound of <b> boxes and <e> edges.`, with the bound's two numbers, where the SVG would be, and writes its text equivalent open.", + "A drawing over the bound keeps its figure and caption, writes `Shown as text: this drawing exceeds the bound of <b> boxes and <e> edges.`, and opens its text equivalent. A routing failure instead writes `Shown as text: a distinct route or label could not be placed within the routing budget.` and opens the same complete box-and-edge reading. A drawing still above the height budget after lawful folds instead writes `Shown as text: this drawing exceeds the height budget of <h> pixels after folding.` and opens its text equivalent. No omission offers partial Mermaid source.", }, }, deliveryFacts: ["implemented", "has-verifier"], @@ -5548,7 +5559,7 @@ export const consumersSpecs = [ readiness: "ready", title: "The Packs lens groups every Spec by the Packs that hold it", narrative: - "A capability is read as a Pack, or as a projection over high-altitude behavior Specs that no view draws yet, so the lens groups by Pack and draws no capability tree. The index page holds it, because the Packs are where a reader of a design starts.", + "Packs collect Specs for review, while the separate Capabilities page reads authored refinement. The index remains the landing page and keeps each Pack's members in authored order.", sections: { intent: { outcome: @@ -5557,7 +5568,7 @@ export const consumersSpecs = [ behavior: { rules: [ "The Packs lens is the index page, the page a person lands on.", - "The Packs lens groups Specs by Pack, each Pack's members in authored order, and draws no capability tree.", + "The Packs lens groups Specs by Pack, each Pack's members in authored order. The separate Capabilities page reads refinement; selecting an epic narrows the index's Specs section without changing any Pack.", "A Spec in no Pack is still listed, in the index's Specs section, so the lens hides no Spec.", ], ruleKeys: { @@ -5592,6 +5603,7 @@ export const consumersSpecs = [ rules: [ "The lens draws every component the anchors realize, from recipe 17's component rows and the attributes the `componentRows` entry adds to them, grouped by its recorded context and layer as the architecture grid of `spec:consumers.spec-studio.diagrams` places it, and folds above the bound as that Spec states.", "The lens draws component membership, as the member count in each box, and `uses`, as edges between components, and nothing else: no port, external system, runtime layer or import the impact graph infers.", + "Beside the architecture drawing comes the component uses table, read from recipe 17's component rows and their counts of unit `uses` edges: one row per realized component as the user and one column per realized component as the used. A nonzero cell holds the count of anchored unit `uses` edges behind the pair and links to that use in the row component's detail; a zero cell between two components shows `·`, with `no anchored use recorded` as hidden text and the legend `· means no anchored use recorded`, and the cell where a component meets itself stays empty. Planned uses, unresolved endpoints and uses within one component stay in their own readings and never enter the table. The table is not a drawing, and it reads in full without script and in print.", "Each component design no anchor realizes, a row of recipe 43's `unrealized` list, draws in the same grid by its planned context and layer as a planned box, with the uses it plans drawn dashed to the components it names, and is listed again in a `Designed, not built` section apart from the realized components; a planned use naming a component the lens draws no box for draws no edge and stays listed in its design's row.", "A realized component whose design differs from it lists each differing field, as recipe 43's `differing` rows give it, with its planned and realized values in its detail, and a design value recipe 43 cannot read lists in the `Designed, not built` section as unreadable.", "Each realized component has a detail section on the lens page whose element id is its id, which its box and every link to it from another page open: its id, label, layer, context and role; its recorded file and line as text with a copy action; its members, each with its id, label, role and recorded file and line; the components it uses and is used by, each with the count of unit edges; the Specs it and its members satisfy and reference, linked, a component design its anchor satisfies among them; and its member diagram, every value read from its recipe 17 row.", @@ -5602,12 +5614,13 @@ export const consumersSpecs = [ ruleKeys: { contextAndLayer: "rules[0]", membershipAndUses: "rules[1]", - plannedApart: "rules[2]", - differences: "rules[3]", - componentDetail: "rules[4]", - codeRootedHere: "rules[5]", - unresolvedStructure: "rules[6]", - lensSummary: "rules[7]", + usesTable: "rules[2]", + plannedApart: "rules[3]", + differences: "rules[4]", + componentDetail: "rules[5]", + codeRootedHere: "rules[6]", + unresolvedStructure: "rules[7]", + lensSummary: "rules[8]", }, exampleSpace: { given: [ @@ -5628,16 +5641,57 @@ export const consumersSpecs = [ }, ui: { architecturePage: - "in order, the page title `Architecture`, the summary sentences, the architecture diagram or its fold with one section per context, the `Designed, not built` section, the component details in id order, and the `Unresolved structure` section when it has rows.", + "in order, the page title `Architecture`, the summary sentences, the architecture diagram or its fold with one section per context, the component uses table, the `Designed, not built` section, the component details in id order, and the `Unresolved structure` section when it has rows.", + usesTable: + "a table captioned `Which components use which` in a named, keyboard-focusable scroll region, one row header and one column header per realized component, both in one order: by layer from `edge` to `infrastructure`, unlayered components last, then by id; each header names the component's label with its id as code.", designedNotBuilt: "a table with one row per unrealized design in Spec id order: the component design's Spec, linked; the planned component's id as code; its planned context, layer and role, or `not stated`; the components it plans to use; and whether its component anchor exists without satisfying it; a design whose value recipe 43 cannot read adds a row naming the design, the key and the unreadable value.", componentDetail: - "a heading with the component's id as code and its label, a line with its layer, context and role in words, its file and line with a copy action, then lists headed Members, Uses, Used by, Satisfies and References, an empty list reading None, then the member diagram.", + "a heading with the component's id as code and its label, a line with its layer, context and role in words, its source binding apart from its members, then Members grouped by recorded role, Uses with supporting unit edges at the table's fragment targets, Used by, any design differences, Satisfies and References, an empty list reading None, then the member diagram.", }, }, deliveryFacts: ["implemented", "has-verifier"], file: "specs/consumers/spec-studio.lenses.architecture.sdp.md", }, + { + id: "spec:consumers.spec-studio.lenses.architecture.unit-register", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "The architecture page lists each component's significant units by role", + narrative: + "A contributor about to change the engine needs the curated register of its significant units: the ports, contracts, pipeline stages and catalogs each component owns, and the designs each unit realizes or answers to. The graph already records them on the anchors. This panel reads them there, grouped by component and role, and shows what no anchor states rather than guessing it.", + sections: { + intent: { + outcome: + "Let a contributor find recorded units and their design bindings through their component and role, with omitted structure visible.", + }, + behavior: { + rules: [ + "The architecture page groups recorded members by role within each recorded component, and shows a component's own source binding apart from its members. A row shows the recorded id, label, file and line and its satisfies and references targets separately. The page reads graph bindings only, never an annotation review or oracle source file.", + "Keep a not-stated role group and a no-component section. A binding confers no review approval or execution verdict, and the page shows no inferred accepted badge.", + "Show each recorded role token without interpreting it through a Protocol-corpus Spec id. An adopter's unfamiliar roles remain visible and produce no finding.", + "Sort components by id, role groups by token and members by id in code-unit order, with not-stated roles last and the no-component section after the components.", + "Every row is available without script through native disclosures. The component index links to the existing component section ids, and narrow screens retain readable source paths and separate realization and reference lists.", + ], + ruleKeys: { + recordedOnly: "rules[0]", + omissions: "rules[1]", + roleTokens: "rules[2]", + stableOrder: "rules[3]", + staticAccess: "rules[4]", + }, + }, + verification: { + mode: "reviewed", + criteria: [ + "Render the self-hosted corpus and an adopter with unfamiliar roles, omitted roles and omitted membership. Check ordering, component source rows, target separation, keyboard disclosures and narrow-screen overflow, with no inferred approval badge.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.lenses.architecture.unit-register.sdp.md", + }, { id: "spec:consumers.spec-studio.lenses.architecture.planned-apart", specKind: "example", @@ -5979,9 +6033,9 @@ export const consumersSpecs = [ rules: [ "The page set holds `diff.html` only when the build was given a base graph; without one, no page, link or change note names a diff.", "The page opens by naming its input: the base graph, its label when the build was given one, its schema version and its Spec count beside the built graph's, and a sentence that the comparison reads the two graphs only.", - "The page renders recipe 35's answer with `detail: true` over the whole corpus, in sections in this order: the totals as sentences; the Specs added, linked; the Specs removed, as their ids in text, since no page holds them; the Specs changed, each linked, with each changed part, its change in words, and for a changed value the base value and the built value as readable text blocks or lists, one above the other, never a raw JSON array; keyed changes list in the head's rule and flow order, each as one line with the entry's text rendered as on its Spec page beside its old and new address or `none`; the Packs whose membership or order changed; the bindings changed, kept apart and headed as derived from anchors; and the diagnostics.", + "The page renders recipe 35's answer with `detail: true` over the whole corpus, in sections in this order: the totals as sentences; the Specs added, linked; the Specs removed, as their ids in text, since no page holds them; the Specs changed, each linked, with each changed part, its change in words, and for a changed value the base value and the built value as readable text blocks or lists, one above the other, never a raw JSON array; keyed changes list in the head's rule and flow order, each as one line with the entry's text rendered as on its Spec page beside its old and new address or `none`; the Packs whose membership, order or order meaning changed, an added Pack whose order is priority read as `added as a roadmap`, its added members in manifest order, a removed Pack whose order was priority read as `removed as a roadmap`, and a known change to priority read as `now a roadmap` or from priority as `no longer a roadmap`, stating the before and after order values for other changes of meaning; the bindings changed, kept apart and headed as derived from anchors; and the diagnostics.", "Given a base graph, each changed Spec's page adds a line under its header that names the base input, counts keyed parts apart as `<n> rules keyed, wording unchanged` beside any other changed parts it lists, and links to its row on the diff page; each added Spec's page says it is new since the base.", - "Given a base graph, each Pack page adds a `Changed since the base` section after its maturity map, from recipe 35's answer for that Pack, listing its changed and added members, linked, and its own membership change; a member whose only changes are keying is marked `keyed only`.", + "Given a base graph, each Pack page adds a `Changed since the base` section after its maturity map, from recipe 35's answer for that Pack, listing its changed and added members, linked, and its own membership change, with the same roadmap and order-meaning wording as the diff page; a member whose only changes are keying is marked `keyed only`.", "The page and the change notes state differences between two graphs and confer nothing: no approval, staleness or readiness, and no record that a design pass happened.", ], ruleKeys: { @@ -6243,7 +6297,7 @@ export const consumersSpecs = [ "Inside an export, a link to the Pack or to a member points at its section in the file, a link to an element of the Pack page or of a member's page points at that element in the file, and a link to anything else becomes its id as text, so no link leaves the file.", "Within an export, every element id copied from the Pack page takes the prefix `pack/` and every id copied from a member's page the prefix `<Spec id>/`, while each member's section keeps its Spec id as its own id; every fragment link and every attribute that names an element id, such as `aria-describedby` or a label's `for`, is rewritten to the prefixed id, so no two elements in the file share an id.", "Each Pack page links its export under the header, as `Download this Pack as one file`.", - "Every page prints as a document: the print style hides the navigation, the rail, the search field, the composing panel and every control, gives the canvas the page's width, prints a diagram whole at that width, and prints every disclosure open, the expander opening each before printing and restoring it after.", + "Every page prints as a document: the print style hides the navigation, the rail, the search field, the composing panel and every control, gives the canvas the page's width, prints a diagram whole at that width, and prints every disclosure open except Mermaid source, which prints only when the reader opened it, the expander opening each before printing and restoring it after.", "An export is a function of the graph like every page: the same graph writes the same bytes, and an export holds no time, commit or stamp.", ], ruleKeys: { @@ -6274,7 +6328,7 @@ export const consumersSpecs = [ exportIds: "a copied id is written `pack/<id>` for the Pack page and `<Spec id>/<id>` for a member's page, so a member's `section.behavior` becomes its Spec id, a slash and `section.behavior`; the slash appears in no graph id, so a prefixed id never equals a member's section id or another prefixed id.", expanderPrint: - "`sdp-expander`, before the page prints, opens every `details` inside it, and after printing restores each one's state.", + "`sdp-expander`, before the page prints, opens every `details` inside it except a drawing's or the maturity map's Mermaid source, which keeps the state the reader left, and after printing restores each one's state.", }, ui: { exportTitle: @@ -6455,4 +6509,1860 @@ export const consumersSpecs = [ deliveryFacts: ["has-verifier"], file: "specs/consumers/agent-surface.example-space-coverage.number-not-enumerable.sdp.md", }, + { + id: "spec:consumers.capability-map", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + title: "The capability map reads every epic and its refinement descendants", + narrative: + "The map answers what a corpus intends to do before a reader chooses a Pack. Refinement can skip an altitude, repeat an altitude, or give a Spec several parents. The map keeps that structure. An epic's descendants are a set of Specs, not a measure of completion.", + sections: { + intent: { + actor: "A person or agent choosing which capability's design to read.", + problem: + "An altitude filter gives a flat list and Pack membership does not explain refinement.", + outcome: + "Read every epic with its refinement descendants, its design maturity and direct binding evidence, and expose every Spec no epic reaches.", + }, + behavior: { + rules: [ + "The map follows only declared `refines` edges between resolving Specs, from parent to child for reading. It reads every kind at every altitude, preserves intermediate Specs, and derives no parent from a title, path, Pack, dependency or binding.", + "Every Spec at epic altitude is an entry, whatever its kind. An epic may itself refine another epic; both remain selectable, and a parent's descendant set includes the nested epic and its descendants.", + "Each Spec carries its own stated readiness, structural floor reached, delivery facts, and open questions with their blocking flags. A missing floor reads `none`. No value propagates from a child to its parent.", + "Every refining parent remains in the answer; a Spec appears once in nodes with all its parents, and a page decides where it expands.", + "An epic's rollup counts distinct descendants, excluding the epic itself, by stated rung, floor reached, kind and altitude; it counts those with implementation bindings and those with verifier bindings separately. Open and blocking question totals count the questions on those distinct descendants. Rollups for overlapping epics are never added to form a corpus total.", + "Examples and decisions participate in reach, counts and edges like every other kind. The page may fold them into labelled disclosures, but exposes their rows and any descendants they parent. A verifier count means a binding exists, never that a test passed; the map states no percent complete, inherited fact or work order.", + "A Spec is unreached when no epic reaches it, counting an epic as reaching itself. The answer lists every unreached id, including examples and decisions, and groups the unreached subgraph by its roots. A branch that is also reached remains outside that subgraph, with its cross-edge retained in the full answer.", + "Unresolved or non-Spec refinement endpoints are reported in damage and excluded from traversal. Cyclic refinement is reported as sorted groups whose members all reach one another, including self-links. Traversal visits each id once per root. The map displays the damage without inventing ancestry or withholding the rest.", + "With an epic selected, the answer contains that epic and its distinct descendants. Node parent and epic memberships still name their full-corpus memberships so narrowing never claims exclusive ownership. Without an epic the answer includes every Spec, including an empty corpus or a corpus with no epics.", + ], + ruleKeys: { + graphOnly: "rules[0]", + epics: "rules[1]", + nodeEvidence: "rules[2]", + multipleParents: "rules[3]", + rollup: "rules[4]", + examplesAndDecisions: "rules[5]", + unreached: "rules[6]", + damagedGraph: "rules[7]", + scope: "rules[8]", + }, + }, + design: { + mapRecipe: + 'recipe 52 takes optional `params.epic`, a resolving epic Spec id, and `params.detail`, a boolean. Omitted epic means the whole corpus. An unknown parameter returns `{ unknownParams, accepts: ["epic", "detail"] }`; a wrong type, absent id or non-epic returns `{ diagnostics: [{ parameter, problem }] }` without a map, naming `epic` or `detail` as appropriate. There is no sample epic and no filtering by kind.', + mapAnswer: + "the full answer is `{ input: { epic }, totals, nodes, edges, epics, unreached: { ids, roots }, cycles, damage }`; `epic` is the selected id or null. The selected node set defines all totals; unselected nodes are never counted. Full-corpus memberships on a selected node are context, not extra selected nodes.", + mapDetail: + "recipe 52 reads `params.detail`, absent or `false` for the summary and `true` for the full answer; the summary keeps `input`, `totals`, `epics` as `{ id, title, parentEpicIds, rollup }`, `unreached.ids`, `cycles` and `damage`, and adds `leftOut`, one line for each omitted field. `detail: true` adds `nodes`, `edges`, each epic's `descendantIds` and `unreached.roots`, and omits `leftOut`.", + nodeRow: + "`{ id, title, specKind, altitude, statedReadiness, floorReached, deliveryFacts, questions, parents, children, epicIds }`; parents and children are all resolving direct refinement neighbors in the corpus, and epicIds lists all epics that reach this node, including itself when epic. Questions preserve authored order as `{ index, address, question, blocking }`, with a keyed question's entry address or null.", + edgeRow: + '`{ from, to, claim: "declared", reason }`, child to parent, for each resolving refinement whose ends are selected. An absent reason is null. Other relation types never enter this list.', + epicRow: + "`{ id, title, parentEpicIds, descendantIds, rollup }` for every selected epic, with its distinct descendants in the selected set. `parentEpicIds` lists resolving direct refinement parents at epic altitude in the full corpus, in id order. Selection by one epic includes every descendant of a nested epic, so its rollup stays complete.", + countShape: + "totals and each rollup carry `{ specs, byStatedReadiness, byFloorReached, byKind, byAltitude, withImplementationBinding, withVerifierBinding, openQuestions, blockingQuestions }`; every descriptor value has a key including zero, floor counts add `none`, and totals additionally carry `{ epics, reached, unreached }`. Reached and unreached partition selected nodes, using their full-corpus epic membership. The epic's own evidence stays on its node row, outside its rollup.", + unreachedRoots: + "a root is an unreached Spec with no unreached parent; a group of unreached Specs that refine one another in a cycle, with no unreached parent outside the group, uses its smallest id as its root. Reach may overlap between roots; unreached.ids stays unique and complete. In an epic selection both lists are empty.", + graphDamage: + '`damage` lists `{ code: "unresolved-refinement", from, to }` for a missing or non-Spec endpoint touching the selected scope, or every such edge in whole-corpus scope. Cycles are separate `{ members }` rows for cyclic groups in the selected graph, members sorted by id; they are graph damage, not parameter failures or a new validator.', + stableOrder: + "ids, nodes, epic rows, descendant ids, memberships and unreached roots use code-unit id order; edges and damage sort by from then to; cycles sort by first member. Question order stays authored. The answer has no clock, recorded work order or mutable state.", + }, + verification: { + mode: "reviewed", + criteria: [ + "A diamond retains both parent edges, lists the shared child once in nodes, and counts it once in that epic's descendants.", + "A corpus with no epics still exposes every Spec; a cyclic unreached component has a root and finite traversal.", + "An epic with a bound example and a ratified decision counts both descendants while retaining its own empty delivery facts.", + "Summary and full answers have identical totals, epic rollups, cycles and damage; the summary names every omitted field.", + "An epic selection includes its whole reachable set and no unrelated roots; invalid parameters return diagnostics without a plausible empty map.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/capability-map.sdp.md", + }, + { + id: "spec:consumers.spec-studio.capability-map-page", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "The capability map page makes refinement readable by epic", + narrative: null, + sections: { + intent: { + actor: "A reviewer looking for a capability before choosing a Spec or Pack.", + problem: + "A flat altitude filter hides how a capability is decomposed, and a large refinement drawing becomes unreadable when shrunk to the page width.", + outcome: + "Read an epic overview, inspect its refinement structure and evidence, and narrow the index's Specs to its descendants.", + }, + behavior: { + rules: [ + "The Studio publishes `capabilities.html` on every build and links it as `Capabilities` immediately after `Packs` in the top navigation. The Packs lens keeps its membership order and the index remains the landing page.", + "The page reads recipe 52 with `{ detail: true }` and no epic parameter. It renders every epic's own evidence beside its descendant rollup and names the rollup's scope. Examples and decisions have separate counts and expandable rows. There is no combined total obtained by adding epic rows.", + "Each epic has a refinement drawing supplied by the shared diagram implementation. It keeps parent-to-child reading order while arrows, if drawn, point child to parent and the legend says `refines`. Repeated nodes link to their first occurrence within that epic section. A nested epic remains a labelled epic, not a new altitude.", + "A large branch folds at the shared diagram bound and links to its complete refinement text. At desktop width it never scales labels below the shared legibility size to fit an entire tree; below the responsive breakpoint it follows the shared legibility rule's narrow fit. The text includes every node and edge, repeats and cyclic links labelled in words, and stays readable without script.", + "Every node title links to that Spec's Studio page. A separate `Show these Specs` link goes to the index's Specs section with the epic selected. Keyed questions link to their entry on the Spec page; unkeyed questions link to the Spec page's `section.intent`. Unresolved endpoints are text with their diagnostics, never plausible Spec links.", + "The index's Specs filter gains `Epic`, with `Any`, `No epic` and every epic by title and id. An epic selects its own Spec and every Spec whose full-corpus epicIds contains it. It intersects with the existing filters, resets with them, and never changes Pack membership or hides Pack cards.", + "Native links, disclosures, complete refinement text and all Spec rows work without script. Index filtering is an enhancement: before script applies a requested epic, the index says filtering needs script and shows all rows. A supplied id that is absent or not epic reports `Unknown epic filter` and leaves all rows visible.", + "The page always has `No epic reaches these Specs`, including an explicit empty message. With no epics it explains that authors may organize Specs through refinement and still lists the whole corpus. Cycles and unresolved edges are visible diagnostics beside the complete text, without a new validation rule.", + ], + ruleKeys: { + page: "rules[0]", + recipeData: "rules[1]", + drawing: "rules[2]", + bounded: "rules[3]", + links: "rules[4]", + filter: "rules[5]", + withoutScript: "rules[6]", + emptyAndDamage: "rules[7]", + }, + }, + design: { + route: + "`capabilities.html`; an epic's section id is its Spec id. The filtered-index route is `index.html?epic=<percent-encoded Spec id>#specs`; `epic=none` selects unreached Specs, absence selects Any. An unknown or repeated epic parameter reports the unknown-filter message without applying it.", + diagramInput: + "recipe 52's node, edge, epic and unreached rows supply the drawing and its text; layout derives no reach, floor or delivery fact. The shared drawing implementation owns folding, label metrics and SVG or Mermaid representation. A capability drawing must support same-altitude edges, skipped altitudes, multiple parents, nested epics, and cycles without pretending to be a strict three-level tree.", + disclosureOrder: + "epic overview cards and sections follow recipe order. Each section opens with the epic's own rung, floor, binding evidence and questions, then the rollup labelled `Distinct descendants, excluding this epic`. Refinement text expands a depth-first walk with children in id order and renders later visits as references; it follows the recipe's root order and uses per-section visited ids. Examples and decisions fold through native disclosures without pruning their descendants.", + rowEvidence: + "each row names kind and altitude, `stated <rung>`, `floor <rung>`, the implementation binding and the verifier binding as `present` or `none`, and open and blocking question counts with their text available. Facts that are absent stay absent; a folded branch's counts never become the parent's badges.", + nestedEpics: + "a card for an epic that refines another epic says `Inside <title>`, using `parentEpicIds`, and a card whose descendants include an epic names it, so overlapping counts read as overlap.", + responsive: + "overview cards reflow to one column on a narrow viewport; refinement text wraps ids and titles, caps visual indentation, and names the parent in words so deep trees remain readable. At desktop width diagram labels use the shared readable size and bounded branches. Below the responsive breakpoint the drawing follows the legibility rule's narrow fit; the complete refinement text is the readable form. Print opens the disclosures and retains the complete text.", + }, + verification: { + mode: "reviewed", + criteria: [ + "Read the same page on a corpus with one broad epic, several epics, and no epics, with script disabled and at a narrow viewport.", + "Follow a shared descendant from both parents and verify that the rollup counts it once and the index filter lists it once.", + "Select an epic on the index, combine it with kind and readiness filters, reset it, and open the filtered URL directly; invalid and repeated epic parameters never show a false empty result.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.capability-map-page.sdp.md", + }, + { + id: "spec:consumers.spec-studio.diagram-kinds", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + title: "The Studio chooses diagram kinds from graph values", + narrative: + "Dependency chains across a Pack, capability refinement, the components a design touches, and differences between stated readiness and the floor need different pictures. A diagram kind earns its place through the question it answers and the graph values that answer it. Mermaid supplies a useful catalog of visual forms, not the Studio's layout runtime or an instruction to draw every form.", + sections: { + intent: { + outcome: + "Choose useful drawings from graph values, keep their labels readable on first view, and offer source a reader can copy without changing the shipped Mermaid projection.", + }, + behavior: { + rules: [ + "A drawing reads the graph through the page's reader values and recipe answers. A visual form never supplies missing intent, order, cardinality, scores, dates or runtime evidence from prose, paths or imports.", + "The catalog names each visual family, whether the graph holds its data, its subject, the question it answers, and a refusal or deferral's re-entry trigger. Renderer aliases share their family's row; diagnostic renderers are named apart.", + "The Studio adds dependency depth, capability refinement, scoped architecture and the maturity map as a grid of counts, and a component uses table beside the architecture lens's drawing; requirement traceability replaces the realization drawing on a Spec page. Equivalent Mermaid forms share one Studio drawing.", + "A Pack, slice or roadmap passes an explicit ordered set of Spec ids to a drawing. A grouping or a diagram never authors a delivery fact, ranks work, grants permission or sets a workflow gate.", + "Every new drawing keeps the diagram contract's bound, deterministic static SVG, links, words beside style, text equivalent and unique element ids. Layout and copying add no browser layout dependency.", + ], + ruleKeys: { + graphData: "rules[0]", + kindCatalog: "rules[1]", + drawnKinds: "rules[2]", + honestScope: "rules[3]", + sharedContract: "rules[4]", + }, + }, + design: { + kindCatalog: + "`draw`, `equivalent`, `defer` and `refuse` describe catalog choices only. They are not graph fields or delivery states. A deferred form has honest inputs but adds no needed reading beyond the selected views; a refused form lacks typed inputs.", + legibleLabels: + "The figure keeps the Studio's navigation rail. Title-first labels and width-based cell wrapping reduce width; natural-size SVG with local horizontal scrolling preserves readable labels on desktop. Below the responsive breakpoint the same drawing fits the screen. The legibility child pins these measurements and behavior.", + flowchart: + "Draw for a Spec set, Pack, slice or roadmap from declared relations; dependency depth reveals chains and cycles the one-hop trace cannot show. Arrows retain their exact relation and direction, never execution order. The existing trace and members remain equivalent flowchart subjects.", + mindmap: + "Draw capability refinement from declared refines for an epic or capability map. The tree shows nesting absent from a flat list; repeated parents and cycles stay explicit rather than becoming an invented hierarchy.", + treeView: + "Equivalent to the capability tree for a strict refinement tree. Folder icons and file paths would invent meaning; use plain labels. Repeated parents use the capability view's cross-links or its flowchart source.", + block: + "Draw scoped architecture and the discrete maturity matrix. Components and anchored uses answer which seams a Spec set touches; stated and reached rungs answer where statements lag or overreach. Cell position is categorical, never a score.", + architecture: + "Equivalent to the scoped architecture drawing for a component or Spec set, from component, memberOf and uses plus planned-component declarations. Generic boxes keep optional context and layer; cloud, service, network and deployment icons need explicit typed declarations before they can mean anything.", + usesTable: + "Draw a component uses table, a design structure matrix, beside the architecture lens's drawing: one row per realized component as the user and one column per realized component as the used, in one order, a cell for each pair. A table stays legible at any number of uses, where routed edges between a dozen components run in parallel bundles. It is an HTML table read from anchored uses, needs no layout, bound or fold, and offers no Mermaid source because Mermaid has no matrix form.", + requirement: + "Draw a Spec's or bounded Spec set's refinement, satisfying units and declared versus anchored verifiers together. This shows the distinction between intent and binding in one picture; it supplies no risk, test method or passing result.", + class: + "Defer a role view for a component or slice: units as classes stereotyped by their recorded role, with anchored uses as dependency arrows and no fields, methods or inheritance. Role, memberOf and uses are honest inputs, but the role vocabulary is under review and the architecture page lists roles as text. Re-enter when the role vocabulary settles and a reader needs pattern relationships drawn. Refuse fields, methods, inheritance and multiplicities; Model prose is not a schema.", + entityRelationship: + "Refuse for a Spec, component or corpus. Terms and relations are not entity attributes, keys or cardinalities. Re-enter with an explicit typed entity and cardinality contract.", + sequence: + "Refuse for a Spec or slice. Uses and dependsOn hold no messages, participants or temporal order. Re-enter with typed ordered interactions or a separately admitted runtime input.", + zenUml: + "Refuse for the same missing interaction data as sequence; the optional external renderer adds no graph evidence. Re-enter when ordered interactions have a lawful graph home.", + state: + "Refuse for a Spec or roadmap. Readiness is a position, not a transition machine, and the graph holds no domain states, events or guards. Re-enter with a typed domain state contract unrelated to delivery gating.", + journey: + "Refuse for a Spec, Pack or corpus. Intent actors do not define ordered journey tasks or satisfaction scores. Re-enter with typed journey steps and a reader who needs them.", + gantt: + "Refuse for a roadmap or corpus. No durations, start dates or schedules are held, and dependency depth is not time. Re-enter only with a separately ruled temporal input that never gates work.", + pie: "Defer for a Pack or corpus. Counts by kind or readiness are honest, but slices repeat the census while hiding exact comparisons. Re-enter when a reader needs part-to-whole comparison that the census cannot answer.", + quadrant: + "Refuse normalized quadrant coordinates for a Pack or roadmap. Readiness rungs have no numeric distances and priority is not a score. The maturity map uses a discrete matrix instead. Re-enter with explicitly typed independent numeric measures.", + gitGraph: + "Refuse for a Spec, Pack or corpus. Git is the event log, but commits and branch ancestry are not nodes in the current graph. Re-enter with a ruled history input and a history reader.", + c4: "Refuse C4 semantics for a component or slice. Component anchors do not state people, software systems, containers, trust boundaries or deployment. Re-enter with typed C4 meanings; generic component structure remains in the architecture drawing.", + timeline: + "Refuse for a roadmap or corpus. A dependency chain and authored Pack order are neither dated events nor time periods. Re-enter with a lawful history input.", + sankey: + "Refuse for a component, slice or corpus. Uses counts are discrete bindings, not a conserved flow or its magnitude. Re-enter with typed flow units and weights.", + xyChart: + "Defer categorical count bars for a Pack or corpus because the census already gives the exact counts; refuse a trend without time-series input. Re-enter when a reader needs a count comparison or a separately ruled time series.", + packet: + "Refuse for a Spec or component. Contract prose contains no typed bit offsets, field widths or packet layout. Re-enter with a typed wire-format layout.", + kanban: + "Defer a separate readiness board because the maturity map shows both stated and reached rungs; refuse workflow states or inferred work order. The roadmap page's HTML lists by need already provide the categorical board over explicit graph values. Re-enter for Mermaid kanban export when a reader needs to copy that board.", + radar: + "Refuse for a Spec, Pack or corpus. There are no comparable numeric quality scores or justified common scales; cardinalities cannot manufacture them. Re-enter with typed independent measures and their units.", + treemap: + "Defer for a Pack or corpus. Spec counts and membership are honest, but area repeats the census and a multi-parent refinement graph cannot be partitioned without duplication. Re-enter with a named size-comparison reader and an explicit partition and measure.", + swimlanes: + "Refuse process swimlanes for a slice or component. Context and layer do not define actors, tasks or control flow. The architecture grid remains an equivalent grouping only. Re-enter with typed process participants and flow.", + useCase: + "Refuse UML use-case associations for an epic or Spec. Intent.actor is prose, not an actor identity or an include, extend or association edge. Re-enter with explicit typed actor associations; behavior altitude alone supplies none.", + eventModeling: + "Refuse for a component or slice. The graph holds no ordered commands, events, screens or event streams. Re-enter with typed event-modeling declarations.", + venn: "Defer Pack-overlap sets for the corpus. Membership allows honest intersections, but no current task needs a bounded overlap picture. Re-enter with named Packs and an overlap question whose intersections remain legible.", + ishikawa: + "Refuse for a Spec or corpus. A dependency is not a causal explanation and the graph holds no typed cause categories. Re-enter with authored causal relations admitted by the Protocol.", + wardley: + "Refuse for a capability map or roadmap. Refinement and dependencies hold no value-chain visibility or evolution coordinates. Re-enter with a ruled typed strategy input; never map readiness to evolution.", + cynefin: + "Refuse for a Spec or roadmap. Kind and readiness are not complexity classifications. Re-enter with explicitly authored complexity assessments in an admitted contract.", + agentFlow: + "Refuse agent-flow semantics for a component or slice. Anchored uses do not identify agents, tools, prompts or routing decisions. Re-enter with typed agent and routing declarations.", + railroad: + "Refuse railroad, EBNF, ABNF and PEG forms for a Spec or corpus. A vocabulary's step strings and parameter slots do not state a complete grammar production graph. Re-enter with typed productions and a grammar reader; never parse prose into a grammar.", + diagnostics: + "Mermaid's info and error renderers are diagnostics, not corpus diagram kinds. Keep them outside the Studio catalog of graph drawings; renderer variants such as flowchart ELK, class and state versions share their semantic family's row.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.diagram-kinds.sdp.md", + }, + { + id: "spec:consumers.spec-studio.diagram-kinds.legibility", + specKind: "contract", + altitude: "story", + readiness: "ready", + title: "A diagram preserves readable labels before zoom", + narrative: null, + sections: { + intent: { + outcome: + "Read a drawing's labels on first view while allowing a wide graph to scroll inside its figure.", + }, + behavior: { + rules: [ + "At 720 CSS pixels of viewport width and wider, a diagram's SVG starts at its natural size inside its figure's scroll region and is never shrunk to fit. Overflow scrolls inside the figure, never the page. The build writes `Scroll horizontally to read the whole drawing.` above every drawing wider than the width budget. Below that viewport width the responsive contract's narrow drawing applies.", + "A box starts with its title or recorded label at fourteen pixels, then a shortened id at twelve pixels, with full identity in its link title and text equivalent. Shortening changes no identity, target or element id; collisions extend the displayed suffix until it is unique within the figure. The box's remaining words, such as a component's member count, follow the id.", + ], + ruleKeys: { + firstView: "rules[0]", + titleFirst: "rules[1]", + }, + }, + design: { + widthBudget: + "`880` logical pixels for a band or grid, band labels included, which stay above their band; `264` per context cell when several contexts share a grid, the full band budget when only one context is shown, and `24` between boxes. A drawing wider than `880` scrolls inside its figure. Layout reads no browser or font measurement.", + labelMetrics: + "Title lines hold at most `28` characters at fourteen pixels with an eight-pixel character advance; ids hold at most `30` characters at twelve pixels with an eight-pixel advance. Wrap at spaces or id punctuation, then by code point, with whole-pixel coordinates and height calculated from all lines.", + minimumScale: + "`1` on first desktop view at viewport widths of at least `720` CSS pixels; below that width the responsive contract fits the same drawing to the screen. Zooming out on desktop remains an explicit reader choice.", + shortId: + "Drop the namespace and the leading dotted segments every id in the figure shares, and mark the elision with a leading `…`, so a label may show `…markdown-authoring`; keep more segments on collisions. A displayed id never carries a namespace unless it is shown in full. Full ids remain in the link title, the text equivalent and the source.", + viewport: + "`.diagram-scroll` is a keyboard-focusable named region with overflow auto and max-width one hundred percent. At desktop widths its SVG has natural width and height with no max-width fit; below `720` CSS pixels the SVG fits the region. The caption and legend stay outside. The build emits the scroll instruction only when SVG width exceeds the fixed width budget; CSS leaves the region discoverable on narrower columns.", + elementIds: + "Keep each kind's text and arrow ids; the viewport uses `diagram.<subject>[.<view>].viewport` and its visible instruction uses `.scroll-help`, inserting the view name exactly where the diagram's own ids do.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.diagram-kinds.legibility.sdp.md", + }, + { + id: "spec:consumers.spec-studio.diagram-kinds.edge-routing", + specKind: "contract", + altitude: "story", + readiness: "ready", + title: "Edges route around boxes with deterministic orthogonal paths", + narrative: null, + sections: { + intent: { + outcome: + "Follow an arrow without it crossing a box or covering a label, using the engine's own static layout.", + }, + behavior: { + rules: [ + "After box placement and label wrapping, reserve each box and band label rectangle as an obstacle with clearance. An edge may touch only its own source and destination ports; no other segment may intersect a reserved rectangle's interior.", + "Route edges deterministically in code-unit order by from, to, kind and claim with orthogonal whole-pixel paths around the reserved obstacles. Add no layout dependency and read no browser measurement. The build owns the search representation, weights and tie-break implementation.", + "Choose source and destination sides deterministically for forward, same-band and backward edges. Every incident edge has its own port. A cycle and a backward edge remain directed and route around intervening boxes.", + "Reserve an edge label's rectangle beside a free segment. If the exact relation and claim words do not fit, use a stable edge key and name its full wording in the adjacent legend and text equivalent. Every placed label rectangle is an obstacle for later labels and routes; no two labels overlap. An edge carries a label only where spec:consumers.spec-studio.diagrams#rule.wordsBesideStyle asks for one; a drawing whose edges share one relation and one claim names them once in its legend.", + "If any required route, distinct lane or label cannot be placed within the deterministic routing budget, omit that SVG and open its text equivalent with an explicit routing explanation. Never draw a straight line through a box as fallback.", + ], + ruleKeys: { + obstacles: "rules[0]", + routes: "rules[1]", + ports: "rules[2]", + labels: "rules[3]", + noPath: "rules[4]", + }, + }, + design: { + clearance: + "`12` logical pixels around boxes and text, with ports connected through their own clearance by a stub. Lane spacing is `8` pixels and gutter margin is `24` pixels; all coordinates and costs are integers.", + cost: "Crossings remain permitted at free points, never through nodes or labels. Two edges never share a collinear segment: they take distinct ports and lanes `8` pixels apart, and an edge that cannot get a lane is a routing failure under the no-path rule.", + searchBound: + "The build chooses a finite deterministic search budget and tests its routing-failure explanation; tuning the search changes no graph meaning. The shared box and edge bound applies first.", + overflow: + "Routing gutters may increase SVG width beyond the box layout budget to preserve distinct lanes; the desktop scroll instruction follows that final width. No label is shrunk to fit.", + routeModel: + "Each route retains from, to, kind, claim, optional aggregation count and relation reason, plus its integer point list; ports and routes are presentation values, never graph edges.", + elementIds: + "Preserve `.edge` data-from, data-to and data-kind, add data-claim when present and a stable data-edge-key for legend lookup; arrow marker ids remain scoped to subject and view.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.diagram-kinds.edge-routing.sdp.md", + }, + { + id: "spec:consumers.spec-studio.diagram-kinds.mermaid-source", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "A diagram offers Mermaid source as copyable text", + narrative: + "Mermaid source is a second serialization of the drawing's supplied values. It changes neither the static SVG renderer nor the separately shipped Mermaid root, so it requires no decision refining either ruling. A future Mermaid runtime, layout library or change to that root would need its own decision.", + sections: { + intent: { + outcome: + "Copy a drawing into another document while keeping the Studio usable without script or a Mermaid runtime.", + }, + behavior: { + rules: [ + "Build escaped Mermaid text from the same selected and folded diagram values as the SVG. Source is an optional export, never a browser rendering path, a network request, a new command or a write to the shipped Mermaid root.", + "Each kind names its preferred syntax. Use a labelled flowchart fallback whenever that syntax cannot state the drawing's exact relations, claims or folds. State the chosen syntax and fallback reason beside the source. Preserve full ids in labels or comments and every aggregation count and folded member identity in comments; the text equivalent remains authoritative for the diagram's reading.", + "A closed source disclosure contains a read-only text area with a visible label. Without script a reader selects and copies it. The page elements' copy control enhances this same text with its Copy button, Copied feedback and select-the-text fallback. The diagram element keeps only its viewport controls.", + "Treat every title, label, id and reason as data. Escape HTML at the page boundary and Mermaid syntax at the source boundary; never emit source directives, executable links, raw HTML or configuration supplied by corpus text.", + "A drawing omitted for bounds or routing offers its text equivalent and a reason; it offers no partial Mermaid source that appears complete. Source follows the same explicit scope and folds as its figure.", + ], + ruleKeys: { + sourceOnly: "rules[0]", + losslessMeaning: "rules[1]", + copy: "rules[2]", + escaping: "rules[3]", + omission: "rules[4]", + }, + }, + design: { + disclosure: + "Summary `Mermaid source`, a paragraph `Copyable source; the Studio draws static SVG.`, a syntax label and optional fallback explanation, then a labelled read-only textarea and the script-added copy button.", + sourceElement: + "The disclosure's copy button is the page elements' `sdp-copy`, given a `for` attribute that names the source text area so a long source is not repeated in an attribute; no new element is added, and `sdp-diagram` still adds only pan, zoom and pinch.", + sourceEscaping: + 'Inside a quoted label, `"`, `#`, `%`, `<` and `>` become Mermaid entity codes (`#quot;`, `#35;`, `#37;`, `#lt;`, `#gt;`); a backtick becomes `#96;`, a line break becomes a space, and every other character is kept.', + sourceIds: + "Stable safe aliases assigned in displayed box order, with full graph ids in quoted labels or escaped comments. Aliases are local serialization names, never graph identities. No click directive or initialization directive is emitted.", + elementIds: + "Use `diagram.<subject>[.<view>].source` and `.source-text`, inserting the view name exactly where the diagram's own ids do, so a trace graph's source is `diagram.<subject>.source`.", + export: + "The self-contained Pack export retains source as selectable text and omits the copy script; print expands the text equivalent and leaves source closed unless the reader opened it.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.diagram-kinds.mermaid-source.sdp.md", + }, + { + id: "spec:consumers.spec-studio.diagram-kinds.dependency", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "A dependency drawing shows the declared footing of a Spec set", + narrative: null, + sections: { + intent: { + outcome: + "See multi-hop dependency chains and cycles within a Pack, slice or roadmap without reading them as an execution schedule.", + }, + behavior: { + rules: [ + "Draw the design slice answer's structure edges and rests-on boundary, preserving each declared refines, dependsOn, constrainedBy and decidedBy relation's exact direction, reason and claim. The design slice owns its rests-on exception for a refines back to a member that names it by another typed relation: such an edge carries `orders: false`, is drawn dashed and says in its legend and text that it orders nothing. Membership is scope, never a dependency.", + "Band each member by the depth its supplied answer gives, the design slice's `depth` for a Pack, slice or roadmap scope; the layout never recomputes it. A cycle's members share one band and are named as a cycle in words.", + "Show immediate dependencies outside the supplied scope in a separate boundary band, without traversing their dependencies. An unresolved target is named unresolved and has no invented title or link.", + "The boundary band always folds into one box per relation type that names its count, keeping as its own box each outside Spec that holds a member's stated next rung or states below defined, and every folded Spec stays a row of the text equivalent; above the shared bound, whole depth bands fold in descending box count, ties by ascending depth; if the drawing still exceeds the bound, the SVG is omitted and the text equivalent opens.", + "An example member the design slice folds into its parent is counted on that parent's box and is never drawn as its own box; it keeps its row in the text equivalent. An example the slice does not fold is a box like any other member's.", + ], + ruleKeys: { + declaredOnly: "rules[0]", + depth: "rules[1]", + boundary: "rules[2]", + fold: "rules[3]", + examplesInParent: "rules[4]", + }, + }, + design: { + inputs: + "`subject` and the design slice answer supply ordered members, exampleOf, exampleMembers, depth, cycles, structure edges and the rests-on boundary with its holds and relation reasons; a roadmap passes its explicitly selected ordered Spec set to that same reader. Duplicate or unknown scope ids are that reader's diagnostics, never repaired by layout.", + bands: + "Label bands `Depth <n>` from the deepest at the top down to depth 1, then `Outside this scope` at the bottom, so a member sits above what it rests on and boundary edges stay short. Within a band keep supplied order. Depths are structural, never phases, dates or priority.", + edgeWords: + "Show refines, dependsOn, constrainedBy and decidedBy beside their arrow or as a keyed legend where several kinds occur, and in the text equivalent with claim and optional reason. Mixed relations cannot become an unlabeled dependency arrow. Arrows with the same ends, claim and ordering may combine into one arrow naming every relation; the text equivalent retains each original relation and reason.", + textEquivalent: + "List every scoped and boundary Spec with its full id and title, then every original edge and every cycle's members; folding removes no text row.", + elementIds: + "Use `diagram.<subject>.dependencies.text`, `.dependencies.arrow` and `.dependencies.fold.<depth>`; a second scope on one page uses a distinct subject occurrence key supplied by the page.", + mermaidKind: + "`flowchart TB` with depth subgraphs and exact relation labels; export the displayed folds and name the full text equivalent beside the source.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.diagram-kinds.dependency.sdp.md", + }, + { + id: "spec:consumers.spec-studio.diagram-kinds.capability-tree", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "A refinement drawing follows the capability map", + narrative: null, + sections: { + intent: { + outcome: + "Read the capability map as a hierarchy without inventing parentage from names or hiding multiple parents.", + }, + behavior: { + rules: [ + "Draw the roots, refinement edges, unreached Specs and repeated reach supplied by the capability map. Root selection and reach belong to that reader, never to label prefixes, files or the drawing.", + "Place a Spec at its first visit in the capability map page's traversal of the epic and draw each later visit as a repeat box that links to the first occurrence and keeps its own refines arrow. A cycle is a labelled group whose internal relations stay in the text equivalent; the drawing never cuts an edge silently.", + "Above the shared bound, retain each root and fold the largest descendant branch below a non-root parent first, ties by root order then id, linking to its full branch list. If siblings still exceed the bound, keep their first entries in supplied order and replace the remaining siblings of one parent together with their descendant boxes with a labelled folded remainder, keeping that parent's relation explicit and counting every hidden unique Spec. Count unique Specs and rendered boxes separately where a cycle or repeated reach needs an occurrence. If no lawful fold fits, omit the SVG and open the text equivalent.", + ], + ruleKeys: { + mapInput: "rules[0]", + multipleParents: "rules[1]", + fold: "rules[2]", + }, + }, + design: { + layout: + "Roots lead, then depth bands of children; label each box with its title, altitude and shortened id. A refines arrow still points child to parent. Unreached Specs occupy a labelled band apart. Within a depth band, children sit together under their placement parent as one labelled sibling group, in parent order; a band never interleaves siblings of different parents.", + wordsBesideStyle: + "The legend says `Arrows: refines (declared). Later visits repeat a Spec and link to its first occurrence.` A group says `cycle` or `folded` in words and names its unique member count.", + textEquivalent: + "List every root and reachable Spec, all declared refines edges, cycle membership and unreached ids using full identities. Grouping confers no delivery fact or readiness.", + elementIds: + "Use `diagram.<subject>.capability.text`, `.capability.arrow` and `.capability.branch.<encoded-root-id>`; encoded identities are injective and stable, not truncated labels.", + mermaidKind: + "`mindmap` for a strict tree, with title and full id per node. Use `flowchart BT` with exact refines edges whenever there is repeated parentage, a cycle, an unreached band or a fold that mindmap cannot state losslessly. TreeView is an optional equivalent export of a strict tree with icons disabled.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.diagram-kinds.capability-tree.sdp.md", + }, + { + id: "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "A Spec set draws the architecture it touches", + narrative: null, + sections: { + intent: { + outcome: + "Review a Pack, slice or roadmap's realized and planned seams together while seeing exactly why each component is present.", + }, + behavior: { + rules: [ + "Read the design slice's `architecture` answer: `components` with how the slice reaches each, in words, members' satisfying code, members' referencing code or what the slice rests on; `designs` with their declared values, realization and comparison rows, each drawn as a planned box; `uses` with the unit edges behind each count and its outside mark; `plannedUses`; `outside`; and `unplaced`. The drawing resolves no unit to a component itself and recomputes none of them; the unplaced units remain listed apart.", + "Draw anchored components and component designs as separate identities, even when the realization comparison joins them. Planned layer, context and uses stay declared; realized structure stays anchored; a difference is labelled rather than merged away.", + "Draw only the uses boundary the supplied design-slice architecture answer includes; never traverse it in layout. Its `uses` includes anchored pairs between touched components and pairs with one immediate outside neighbour, marked `outside: true`, as `spec:consumers.design-slice#rule.outsideNeighbors` states. Fold supplied outside neighbours into one `outside scope` box per context and layer while keeping their identities and original unit edges in the text equivalent; never add pairs between two outside components. Planned uses remain separate and never increase an anchored count.", + "Apply the shared architecture folds by context then layer to this selected graph. If no component or planned design resolves, show the empty explanation and unassigned units instead of a blank or invented architecture.", + ], + ruleKeys: { + scopeInput: "rules[0]", + separatePlans: "rules[1]", + boundary: "rules[2]", + bound: "rules[3]", + }, + }, + design: { + layout: + "Use the architecture grid's context columns and layer bands, with the legibility child's title-first boxes, cell wrapping and local scroll. Prefix planned boxes with `planned`; outside-scope boxes say `outside scope`.", + edgeWords: + "The legend distinguishes `uses (anchored)`, `plans to use (declared)` and `realizes (anchored)` when present. A drawing containing uses and planned uses alone carries no per-edge label. When a realization correspondence is present, label the distinct relation families beside each link or through a keyed legend; realization stays apart from uses and confers no delivery fact.", + textEquivalent: + "List each component's full id, label, layer, context and the scoped Spec and binding kind that brought it in; list unassigned units, unreadable declarations, planned-to-realized comparisons and every original uses edge.", + elementIds: + "Use `diagram.<subject>.architecture.text` and `.architecture.arrow`; context and layer folds append their encoded full key before the suffix.", + mermaidKind: + "`block` with generic component boxes and uses arrows for a purely anchored grid; use `flowchart TB` with explicit declared and anchored labels when planned, comparison or folded edges cannot be preserved by block syntax. Architecture icons never infer deployment meaning.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.diagram-kinds.scoped-architecture.sdp.md", + }, + { + id: "spec:consumers.spec-studio.diagram-kinds.maturity-picture", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "The maturity map is drawn as a grid of counts", + narrative: null, + sections: { + intent: { + outcome: + "See whether a Pack's statements match, lag or exceed its floors without a wall of member cards inside the picture.", + }, + behavior: { + rules: [ + "Draw one equal-sized cell per stated-rung and floor pair. A populated cell shows its exact count and one mark per member, up to `100` marks, beyond which it is filled solid, and links to that cell's full ordered member list; an empty cell is a faint outline with no number. `matches`, `floor above statement` and `statement above floor` are named once each in the legend, and the diagonal carries an outline. Floor none is distinct from idea, and unresolved members are listed apart.", + "Use categorical cell position, never normalized coordinates, a quality score, area proportional to maturity, completion percentage or a recommendation to implement. Marks count members and score nothing.", + "Every resolved member appears in exactly one cell list in authored order. The picture holds cell counts, not per-member cards. Without script every populated cell links to its already-built list and every member links to its card.", + "When every resolved member falls in one cell, one sentence before the grid names that cell's stated rung and floor with its count and links to the cell's member list, and the grid sits in a closed disclosure; when two or more cells hold members, the grid shows with no disclosure. The sentence scores nothing, and print opens the disclosure.", + ], + ruleKeys: { + discreteCells: "rules[0]", + noScore: "rules[1]", + fullMembership: "rules[2]", + oneCell: "rules[3]", + }, + }, + design: { + matrix: + "Rows run stated idea, scoped, defined, ready from top to bottom; columns run floor none, idea, scoped, defined, ready from left to right. The grid including axes fits the shared width budget. Empty cells show only a faint outline; the matching diagonal has a stronger outline. Relation phrases appear once in the legend.", + bound: + "The fixed categorical cells fit the shared box bound and have no edges. Membership volume does not grow the picture; complete lists follow in closed disclosures and print open. Overlong lists are never truncated.", + oneCellDisclosure: + "the disclosure that holds a one-cell grid reads `Stated readiness by floor reached` as its summary; the grid inside it keeps its element ids.", + textEquivalent: + "Keep the accessible table of cell counts and complete member lists as the diagram's text equivalent; identify every member by its full id, title, stated rung and floor. The axis explanation belongs to the caption.", + elementIds: + "Use `diagram.<subject>.maturity.text` and `diagram.<subject>.maturity.cell.<stated>.<floor>`; cell links target the built list, with unique member-card targets supplied by the Pack page.", + mermaidKind: + "`block` with a fixed categorical grid, cell labels and counts. This is not a quadrant chart; source includes each cell's full membership as comments and names its accessible table.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.diagram-kinds.maturity-picture.sdp.md", + }, + { + id: "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "A requirement drawing separates intent from realization and verification", + narrative: null, + sections: { + intent: { + outcome: + "Read a Spec's refinement, satisfying code and verifier bindings in one picture without mistaking a declared example for a running test.", + }, + behavior: { + rules: [ + "For the Spec page's selected set, draw declared refines, declared and anchored verifies, and anchored satisfies edges incident on the set, plus anchored verifies on its declared verifying examples. Draw code references and oracle models only as their own named kinds, never as realization or successful verification.", + "Every box and edge names its claim. A declared verifying example with no resolving test says `no test binding`; an anchored verifier says `binding present`, never passed. Missing satisfies says `no satisfying binding`, never unimplemented by inference through a child. Examples and decision records, which code never satisfies, carry no satisfying-binding label.", + "Combine arrows with the same ends, direction and claim into one labelled arrow naming every relation. Fold verifying examples together with their exclusive test bindings beyond a small readable count, naming both claims and counts. Above the shared bound or readable density, fold the largest remaining group within each band by edge kind, direction and claim, ties in that lexical order and then by band order, until it fits. A fold names its relation and claim and hides its internal identity. Keep every original edge and box in the text equivalent. If the fold still cannot fit, omit the drawing and open its text.", + ], + ruleKeys: { + exactEdges: "rules[0]", + claimsApart: "rules[1]", + fold: "rules[2]", + }, + }, + design: { + placement: + "On a Spec page the requirement drawing replaces the realization drawing under its heading; its set is the Spec, the Specs that refine it, and the examples that verify any of them.", + bands: + "Place selected non-example Specs, declared verifying examples, satisfying or referencing units, and test or oracle bindings in labelled bands. An example in the selected set always sits in the examples band, including when it also refines a selected Spec. Place Specs outside the selected set in a separate labelled band. Each node has one placement and keeps every edge. Derive each absence statement from the selected examples and recorded edge kinds, never from which bands contain boxes.", + exampleFoldCount: + "Fold more than `4` selected verifying examples, excluding the subject, with their exclusive test bindings; retain the fold only when it lowers layout height. A generic examples-band fold is named `verifying examples`, never `refining Specs`.", + foldTitles: + "Name incoming and outgoing refinement folds `refining Specs` and `refined Specs`; incoming satisfies, references, anchored verifies and models folds read `satisfying units`, `referencing units`, `test bindings` and `oracle bindings`. Every fold includes its claim and hides its internal identity.", + wordsBesideStyle: + "Box subtitles say `declared Spec`, `anchored code`, `anchored test binding` or `anchored oracle binding`; edge labels carry both the exact relation and claim. Verification modes, risk levels and assertion results are not invented.", + textEquivalent: + "List every full id and title or binding label, each binding's file and line, every exact edge with its claim and reason, and examples with no test binding. This drawing replaces the realization drawing and complements the detailed verification panels.", + elementIds: + "Use `diagram.<subject>.requirement.text`, `.requirement.arrow` and `.requirement.fold.<kind>.<direction>.<claim>`; when the same group occurs in multiple bands, append `.band.<band-index>` in the stated band order.", + mermaidKind: + "`requirementDiagram` for the exact refines, satisfies and verifies subset, generic requirement nodes and element nodes for code and bindings; omit risk and verifyMethod. Serialize the original exact relations separately even when the SVG combines their arrow. Put claim words in node text and per-edge comments. Use `flowchart TB` with exact labels when references, models or folds require relations requirement syntax cannot express; never rename them traces.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.diagram-kinds.requirement-trace.sdp.md", + }, + { + id: "spec:consumers.design-slice", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + title: + "A slice reads a Pack as one design, with the architecture it touches and what each member needs next", + narrative: + "Features that must be reviewed together are already grouped as a Pack, and the Pack page shows each member in turn. A design pass also needs the whole at once: how the members rest on each other, which components they touch, realized or planned, which decisions and open questions bound them, and what each member needs for its next rung. Rounds of design add detail a little at a time; the floor sets how little a rung needs, and how much is enough stays a judgment the authoring guidance supports and no check makes.\n\nThe first generation kept a slice as an authored file: a list of members and the one question they answer together, a saved view for design and review that delivered nothing itself (`@libar-dev/architect`, `formal-spec/04-tag-registry.md`, the hierarchy level `slice`). The original design saved AI context slices per Pack, per capability and per changed Spec as files (`docs/lineage/v0-design/07-spec-studio-and-projections.md` §5.5). The Protocol holds the first as a Pack, its members in reading order and its framing saying what they answer together, and it refused the second: a context bundle is composed on each invocation and never saved. A design slice is therefore a reading and not an artifact. It is a Pack read whole, narrowed to one perspective's members when a pass reads for one concern, or an ordered list of Specs, composed from the graph each time, and it adds no grouping, kind, stage or gate. A Spec's planning slice reads one Spec's neighborhood; widening it hop by hop would pick a reach no one authored, so a design slice starts from the set a person chose.\n\nThe reading answers what a card-by-card page leaves a reviewer to assemble. A design that builds on existing code says so through its own `dependsOn` or `refines` to the Spec that code satisfies, so the architecture a design ahead of its code will touch is read from those relations and the anchors of the Specs they name. The first adopter asked for the same separation: the design under review apart from the contracts and decisions it rests on, and the outside Spec whose rung holds a member shown at the boundary with what it holds (`libar-platform/docs/feedback/sdp-feedback-02.md`, items 11 and 12).", + sections: { + intent: { + actor: + "A person or an agent running a design pass over features that must be reviewed together.", + problem: + "The Pack page reads member by member, so a reviewer assembles the dependency order, the touched architecture and the open decisions by hand before judging the design as a whole.", + outcome: + "Read a Pack as one design slice: its members and their relations, the components they touch, realized and planned, the decisions and open questions that bound them, and what each member needs for its next rung.", + value: + "A design pass judges the whole before its parts, starting from the members others rest on, and sees how much design each member carries beside its rung without a score.", + }, + behavior: { + rules: [ + "A design slice is a reading of a set of Specs that a catalog recipe composes from the graph on each invocation; it is never authored, saved or committed, it adds no grouping, namespace, kind, stage or gate, and reading it records nothing.", + "A slice takes one scope: a Pack, its resolved members read in authored order, or an ordered list of Spec ids; a Pack scope may be narrowed to an ordered subset of its members, such as the members the perspective filter selects, and then says that it is a subset.", + "Inside the slice a member rests on each other member it names by `refines`, `dependsOn`, `constrainedBy` or `decidedBy`, except a `refines` to a member that names it back by one of the other three, as the statement queue reads them; every relation of those four types between two members is listed with its type, its reason and whether it orders, so a reading tells an ordering link from a relation that orders nothing.", + "A member that rests on no other member has depth 1, as the statement queue's rounds start at 1, and any other member has the depth after the deepest member it rests on; members that reach each other through those links form a cycle, share the depth after the deepest member outside the cycle that any of them rests on, and are listed with one closed path, and the slice refuses nothing.", + "An example member folds into the member it refines when that `refines` is its only relation of the four types, in either direction, inside the slice or across its boundary: it is listed among that member's example members and keeps its own row, with its own stated rung, floor reached, stated next rung and holds, and folding lends the member none of them; an example with any other such relation is a member like any other.", + "Each member carries its stated rung, the floor reached, its stated next rung with the clauses that hold it, its design columns, its rule count as rule coverage counts it, its verifiers and verifying examples with how many are enabled, the units that implement and reference it, and the components those units belong to; the columns are counts and lists beside the rung, never a score, a verdict or a rung.", + "The slice's boundary is built over its members as Pack design builds a Pack's, so a whole Pack's slice has the Pack's boundary; each Spec the slice rests on also carries the floor it reaches and the members whose stated next rung it holds, and a Pack member left out of a narrowed slice is marked as in the Pack.", + "The components a slice touches are the anchored components it reaches three ways, each named on the component's row and never merged: a unit that satisfies or references a member belongs to it; a unit that satisfies a Spec the slice rests on belongs to it; or a member component design names it.", + "Every member that is a component design, as the planned-architecture convention selects it, is its own row whether or not code realizes it: the component it names, the layer, context, role and uses it declares, whether an anchor holds that component, whether that anchor satisfies the design, and the realization comparison's differing and unreadable rows for it; a declared value never stands in for an anchored one, nor an anchored value for a declared one.", + "Between touched components the slice lists the anchored `uses` aggregated to component pairs, as the architecture map counts them, a use within one component left out, each pair with the unit edges behind its count; each component a member design declares in `uses` is a planned use of that design, listed apart from the anchored pairs and never added to their counts.", + "A component one `uses` step from a touched component, in either direction, or named in a member design's `uses`, that the slice does not touch is listed as outside the slice with how it is reached, and its anchored pairs with touched components are listed and marked outside; the slice reaches no further, and outside components and pairs are counted apart from the touched ones.", + "A unit that implements or references a member and belongs to no component is listed apart, so no binding drops out of the reading.", + "The slice does not count the code of Specs that rest on it: that code follows a change to the slice, and design-change impact reads it; a component that uses a touched component appears only as an outside neighbor.", + "The decisions in scope are the decision members and every decision a member names through `decidedBy`, each with its stated rung, the floor reached, whether it is a member, the members it shapes, the decisions it refines and supersedes, and its open and blocking question counts.", + "The open questions in scope are those of every member and of every decision in scope, and the blocking questions of every Spec outside the slice that holds a member's stated next rung, blocking questions first; each carries its address, its fields and the members whose stated next rung its Spec holds.", + "The slice shows how much design each member carries and never whether it is enough: it reports no over- or under-designed member, no ratio of design to rung and no finding, because how much to write is the authoring guidance's judgment and checks police conformance and honesty, never content quality.", + "The slice reads declared relations, anchored bindings and component designs only; it infers no dependency, component or decision from prose, paths, imports or names.", + "Depth gives a reading order, the members others rest on first; it records, schedules and gates nothing, and a member read or designed out of that order is never a finding.", + ], + ruleKeys: { + readingNotArtifact: "rules[0]", + scope: "rules[1]", + restsOn: "rules[2]", + depth: "rules[3]", + examplesFold: "rules[4]", + memberColumns: "rules[5]", + boundary: "rules[6]", + touchedComponents: "rules[7]", + componentDesigns: "rules[8]", + touchedUses: "rules[9]", + outsideNeighbors: "rules[10]", + unplaced: "rules[11]", + notDownstream: "rules[12]", + decisionsInScope: "rules[13]", + questionsInScope: "rules[14]", + noDesignVerdict: "rules[15]", + graphOnly: "rules[16]", + orderAdvisory: "rules[17]", + }, + exampleSpace: { + given: [ + 'a probe Pack whose members {structure:"rest on each other in a chain"|"include a decision that refines the member it shapes"|"include two members that depend on each other"|"include an example that refines a member"|"include an example that refines a member and depends on an outside Spec"}', + 'one member depends on an outside Spec {outside:"that a component\'s unit satisfies"|"that no code satisfies"|"stated below defined"}', + 'one member is a component design whose component {anchor:"no anchor holds"|"an anchor holds without satisfying the design"|"an anchor satisfies, matching every declared value"|"an anchor satisfies, with a differing layer and uses"|"an anchor satisfies, with an unreadable layer"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + "the members read in depth order {order:string}", + "the slice reports {cycles:number} cycles and {exampleMembers:number} folded example members", + "the touched components are {components:string}, {throughRestsOn:number} of them reached through what the slice rests on", + 'the member design reads {realized:"realized"|"unrealized"} with {differing:number} differing and {unreadable:number} unreadable rows', + "the outside components are {outsideComponents:string}", + "the outside Spec holds {held:string}", + ], + }, + }, + design: { + sliceRecipe: + "recipe 54, the design slice, takes exactly one scope, `params.pack` or `params.specs`, an optional `params.members` beside `pack`, and an optional `params.detail`; an unknown parameter name returns `{ unknownParams, accepts }` alone, and any input `#design.sliceDiagnostics` refuses returns `{ diagnostics }` and no slice.", + sliceDiagnostics: + 'the checks run in this order and every failure is kept, a check that reads an earlier value being skipped when that value failed: a `detail` that is present and neither a boolean nor a list of strings, null included, gives `{ scope: "detail", problem: "Expected true, false or a list of member ids." }`; neither or both of `pack` and `specs` gives `{ scope: "scope", problem: "Supply exactly one of pack or specs." }`; a `pack` that is not a string naming a Pack gives `{ scope: "pack", problem: "Pack does not resolve." }`; beside a resolved Pack, a `members` that is not a non-empty list of distinct strings gives `{ scope: "members", problem: "Expected a non-empty list of distinct member ids." }`, and otherwise each id that is no resolved member gives `{ scope: <the id>, problem: "Not a resolved member of the Pack." }` in list order; a `specs` that is not a non-empty list of distinct strings gives `{ scope: "specs", problem: "Expected a non-empty list of distinct Spec ids." }`, and otherwise each id the graph does not hold gives `{ scope: <the id>, problem: "Spec does not resolve." }` in list order, as the context bundle names an unresolved id; and a `members` beside `specs` gives `{ scope: "members", problem: "members narrows only a Pack scope." }`.', + sliceDetail: + '`params.detail` is absent or `false` for the summary, `true` for every row in full, or a list of member ids for full rows of those members beside summary rows of the rest, an id that is no member adding `{ id, reason: "not-member" }` to the slice\'s `diagnostics`, as Pack design reads it; a summary keeps `totals` whole and adds `leftOut`, one line for each kind of detail it omits.', + sliceAnswer: + "`{ scope, totals, members, structure, boundary, architecture, decisions, questions }`, where `scope` is `{ pack, title, framing, subset, members, unresolved }` for a Pack, `unresolved` naming the manifest ids the graph does not hold, and `{ specs }` for a Spec list.", + sliceTotals: + "`{ members, exampleMembers, relations, depth, cycles, heldBelowNextRung, waitingForAuthor, restsOn, holdingOutside, restedOnBy, verifiedBy, supersededBy, components: { touched, throughMembers, throughRestsOn, throughDesigns, outside }, designs: { total, realized, matching, unrealized, differing, unreadable }, componentUses: { inside, outside, planned }, decisions, decisionsBelowReady, openQuestions, blockingQuestions }`, where `exampleMembers` counts folded example members, `depth` is the deepest member's depth, `holdingOutside` counts the outside Specs that hold a member, `heldBelowNextRung` and `waitingForAuthor` count as Pack design counts them, `components.touched` counts distinct anchored components however reached and `components.outside` the outside components, `designs` counts member designs as the realization comparison's totals count designs, and `componentUses` counts the anchored pairs inside, the anchored pairs marked outside, and the planned uses.", + memberRow: + "`{ id, title, specKind, altitude, statedReadiness, floorReached, statedNextRung, holds, depth, restsOn, restedOnBy, exampleOf, exampleMembers, design, rules: { total, keyed }, examples: { total, enabled }, verifiers: { total, enabled }, implementedBy, referencedBy, components }` in the scope's order, where `holds` lists the stated-next-rung failures as `[{ clauseId, description, targets: [{ type, id, statedReadiness }] }]`, `restsOn` and `restedOnBy` are member ids, `exampleOf` is the member an example member refines or null, `exampleMembers` the folded example members, `design` is the design columns as `{ entries, declarations, openQuestions, blockingQuestions, decisions }` counts, `examples` the verifying examples bound to the member wherever they sit, apart from `exampleMembers`, `rules.total` is the keyed and unkeyed rule and flow entries of the primary behavior owner, `implementedBy` and `referencedBy` are unit ids, and `components` the ids of the components those units belong to.", + memberSummary: + "`{ id, specKind, statedReadiness, floorReached, statedNextRung, holds, depth, restsOn, exampleOf, design, rules, implementedBy, referencedBy }` is a member's summary row, with `holds` as `[{ clauseId, targets: [id] }]`, as Pack design's summary reads it, and `implementedBy` and `referencedBy` as counts.", + structureShape: + "`{ edges: [{ from, to, type, orders, reason }], cycles: [{ members, cycle }], unresolved: [{ from, to, type, reason }] }`, one edge per declared typed relation between two members, sorted by `from`, `type` and `to`, `orders` false only for a `refines` the target names back, and `reason` present only when the relation states one; `cycles` in the statement queue's cycle row; a summary drops each edge's `reason`; `unresolved` keeps unresolved outgoing typed targets apart from the resolved boundary, with no invented title or rung.", + boundaryRow: + "Pack design's boundary row `{ id, title, statedReadiness, implemented, via }` with a `via` that has relation reasons also carrying `reasons: [{ member, reason }]` for those relations, and with `specKind`, `floorReached`, `holds`, the member ids whose stated-next-rung failures name it as a target, empty outside `restsOn`, and `inPack: true` on a Pack member left out of a narrowed slice, in `{ restsOn, restedOnBy, verifiedBy, supersededBy }` sorted by id; a summary keeps `{ id, statedReadiness, holds }` and `inPack`.", + componentRow: + "`{ id, label, layer, context, members: { satisfies, references }, restsOn, designs }` sorted by id, one per touched anchored component, where `members.satisfies` and `members.references` are the members whose units belong to it by each binding, `restsOn` the outside Specs the slice rests on whose satisfying units belong to it, `designs` the member component designs naming it, and `label`, `layer` and `context` come from the architecture map's component row.", + componentSummary: + "`{ id, layer, context, members: { satisfies, references }, restsOn, designs }`, each list replaced by its count and `label` left out.", + designRow: + "`{ spec, componentId, layer, context, role, uses, anchorExists, realized, differing, unreadable }` sorted by `spec`, one per member component design, where the declared values are read as spec:model.planned-architecture#design.designKeys reads them, null when unstated or unreadable and `uses` an empty list for `none`, `realized` holds as spec:model.planned-architecture#design.realizedWhen says, `differing` lists the comparison's `{ field, planned, realized }` rows for the design and `unreadable` its `{ key, value }` rows; a summary keeps `{ spec, componentId, anchorExists, realized }` and the two lists as counts.", + componentUse: + "`{ from, to, count, outside, units }` sorted by `from` and `to`, one per pair of components with an anchored `uses` between them and at least one end touched, where `count` is the architecture map's count of unit `uses` edges for the pair, `units` lists those edges as `{ from, to, claim }` from the architecture map's `usesEdges`, and `outside` is true when one end is not touched; a summary drops `units`.", + plannedUse: + "`{ design, from, to, outside }` sorted by `from`, `to` and `design`, one per component a member design declares in `uses` other than its own, where `from` is the component the design names and `outside` is true when `to` is neither touched nor named by a member design.", + outsideRow: + "`{ id, label, layer, context, anchored, reachedBy }` sorted by id, where `reachedBy` lists, in this order, `touchedUses` when a touched component uses it, `usesTouched` when it uses a touched component, and `plannedUse` when a member design declares it, and `anchored` says whether an anchor holds it; a summary keeps `{ id, anchored, reachedBy }`.", + architectureShape: + "`{ components, designs, uses, plannedUses, outside, unplaced }`, where `unplaced` lists `{ unit, member, via }`, `via` being `satisfies` or `references`, for each unit of a member that belongs to no component.", + decisionRow: + "`{ id, title, statedReadiness, floorReached, member, shapes, refines, supersedes, supersededBy, openQuestions, blockingQuestions }` sorted by id, where `shapes` lists the members that name the decision through `decidedBy` and the next three lists come from its decision register row; a summary keeps `{ id, statedReadiness, member, shapes }` with `shapes` as a count.", + questionRow: + "`{ address, spec, origin, index, question, blocking, deferral, decider, links, provisional, trigger, holds }` with the open-question register's fields, where `origin` is `member`, `decision` or `restsOn`, `index` the question's authored position from 0, `address` null without a key, and `holds` the members whose stated next rung its Spec holds when the question blocks, its own Spec first when that Spec is a member held by the floor's blocking-question clause, empty for a non-blocking question; rows sort blocking first, then by how many members they hold, then by Spec id and position; a summary keeps a non-blocking question as `{ address, spec, index }`.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/design-slice.sdp.md", + }, + { + id: "spec:consumers.spec-studio.slice-page", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "A slice page shows a Pack's design as one whole", + narrative: + "The Pack page reads a Pack card by card, in the order its author wrote. A reviewer judging the design needs the other reading first: which members the rest stand on, what the design will touch in the code, and what decisions and questions bound it. The slice page gives that reading on a page of its own beside the Pack page, one link apart. It keeps the Pack page whole, because a Pack of a hundred members already makes a long page of cards, and the two readings answer different questions in different orders: the author's narrative there, the members others rest on first here.", + sections: { + intent: { + outcome: + "Give the Studio a page per Pack that shows its design slice: the members' dependency drawing, the components they touch, the decisions and open questions in scope, and each member's next rung.", + openQuestions: [ + { + question: "Does a Pack export also carry the Pack's slice page?", + blocking: false, + key: "exportCarriesSlice", + decider: "delegated", + provisional: + "no; the export keeps the Pack page and its members, and the slice page stays one link away in the page set", + }, + ], + }, + behavior: { + rules: [ + "The page set holds one slice page per Pack, `slices/<name>.html`, its name formed from the Pack's id as the Pack page's name is; it renders the build's design slice answer for that Pack in full and states no value the answer does not hold, apart from counts the build takes of it.", + "The page opens with the heading `Design slice` and the Pack's title, the Pack's id as text with a copy action, and a link back to the Pack page, then the summary and a reading note.", + "The summary states each slice total in one sentence that opens with its count: the members and the examples counted in their parents; when the manifest names members that resolve to no Spec, how many and each one's id, so an incomplete Pack never reads as complete; the relations among members, the deepest depth and any cycle; the members held below their stated next rung and those waiting only for their author; the outside Specs the slice rests on, how many of them hold a member, and the outside Specs that rest on it; the components touched, through the members' code, through what the slice rests on, and named by member designs, with the outside components shown beside them; the member component designs, how many are realized and how many of those match; the decisions in scope and how many state below `ready`; and the open and blocking questions.", + "The reading note says in words that members read with the ones others rest on first, that the design columns count what each member carries beside its rung and never judge whether it is enough, and that the page records no review and approves nothing.", + "Under `How the members rest on each other` comes the slice's dependency drawing, `spec:consumers.spec-studio.diagram-kinds.dependency` given the slice's order: it consumes the answer's members, depths, rests-on links, structure edges, cycles and boundary rows and recomputes no scope, depth or order. Each member that is not a folded example is a box with its title, id, stated rung and floor, and its count of folded example members; every supplied relation keeps its type and direction, and a relation that orders nothing reads apart from one that orders. The drawing kind owns layout, bounded folding and the complete text equivalent, which lists every folded example with its `refines`; a fold changes no value of the answer.", + "Under `What each member needs next` comes one row per member that is not a folded example, by depth and then in the scope's order: the member's title linked to its card on the Pack page with its id, its depth, its stated rung beside the floor reached, its stated next rung with `nothing blocks` or each unmet clause and the Specs it names with their stated rungs, then the design it carries, its rules with how many are keyed, its design entries, pinned declarations, open questions with how many block, decisions, and verifying examples with how many are bound, each count on its own line, and its implementation and reference bindings as counts; the table has no total row, and no count is combined with another.", + "A folded example member has no row of its own and stays readable in a disclosure within its parent's row, whose summary counts the folded examples and how many are held below their stated next rung; inside, each example shows its title linked to its card on the Pack page, its id, its own stated rung, floor reached and stated next rung with each unmet clause and the Specs it names. Folding lends the parent no rung, hold or binding, and the folded count stays apart from the count of examples that verify the parent.", + "Under `The architecture it touches` comes the scoped architecture drawing of the diagram kinds, given the answer's components, designs, uses, planned uses and outside components, then one row per touched component: its id linked to its detail on the architecture lens; its label, layer and context; and how the slice reaches it, the members whose code satisfies it, the members whose code references it, the Specs the slice rests on whose code it holds, and the member designs naming it, each on its own line in words.", + "After the component rows comes one row per member component design: the design linked to its Spec page, the component it names, the layer, context, role and uses it declares, whether an anchor holds that component and whether that anchor satisfies the design, in words, and its differing rows with the declared and anchored values side by side and its unreadable rows; declared entries stay in the Declared cell, and every anchored value in the Comparison cell is labelled anchored.", + "Then come the components outside the slice, each with how it is reached and whether an anchor holds it, under a heading that says they are shown for context and not counted as touched.", + "After them come the units that implement or reference a member and belong to no component, reading None when empty.", + "A slice that touches no component and holds no component design says so in one sentence and names, by count, the Specs it rests on that no code satisfies, so an empty architecture never reads as an architecture nobody needs.", + "Under `Decisions in scope` comes one row per decision: its title linked to its Spec page, its stated rung beside the floor reached, whether it is a member, the members it shapes, the decisions it refines or supersedes, and its open and blocking question counts.", + "Under `Open questions in scope` comes each question, blocking ones first, with its blocking flag in words, its address linked to the question on its Spec page when it is keyed, its Spec, its text, its decider, links, provisional reading and trigger when present, and the members it holds.", + "Under `Where the slice meets the rest` come four closed sections, Rests on, Rested on by, Verified by and Superseded by, each headed with its count; a Rests on row shows the outside Spec linked, its stated rung beside the floor reached, its implementation binding as present or none, the members it joins under each relation type, and the members it holds.", + "The page speaks binding language: a unit implements or references a member, a verifier is bound, and no cell reads built, done, verified or approved.", + "`Open all` and `Close all` open or close every closed section and text equivalent at once, and the page reads in full without script.", + ], + ruleKeys: { + slicePerPack: "rules[0]", + header: "rules[1]", + summary: "rules[2]", + readingNote: "rules[3]", + dependencyDrawing: "rules[4]", + memberTable: "rules[5]", + exampleRows: "rules[6]", + architecture: "rules[7]", + designs: "rules[8]", + outside: "rules[9]", + unplaced: "rules[10]", + noArchitecture: "rules[11]", + decisions: "rules[12]", + questions: "rules[13]", + boundary: "rules[14]", + bindingWords: "rules[15]", + openAll: "rules[16]", + }, + }, + ui: { + pagePath: + "`slices/<name>.html`, `<name>` formed from the Pack id as `packs/<name>.html` forms it, so the Pack page and its slice page share a name.", + headerBlock: + "the page heading `Design slice: <Pack title>`; below it the Pack id with a copy action and the link `Read the Pack member by member` to its Pack page; then the summary under `The design as a whole`, then the reading note.", + readingNoteText: + "`Members read with the ones others rest on first. The columns count the design each member carries beside its rung; they never judge whether it is enough. This page records no review and approves nothing.`", + drawingIds: + "Both drawings take the Pack id as their subject and use their drawing kind's element-id contract. The dependency text and arrow ids are `diagram.<pack id>.dependencies.text` and `diagram.<pack id>.dependencies.arrow`; scoped architecture uses `diagram.<pack id>.architecture.text` and `diagram.<pack id>.architecture.arrow`. The kinds also own viewport, source, fold and occurrence suffixes; the page invents no singular dependency or scoped-architecture alias.", + memberTable: + "a table in a scroll region with the columns `Member`, `Depth`, `Rung`, `Next rung`, `Design carried` and `Code`, so it fits a desktop column without scrolling; `Member` holds the title link, the id and the kind; `Rung` the rung badge and `floor <rung>`; `Design carried` six lines, `<n> rules, <k> keyed`, `<n> entries`, `<n> declarations`, `<n> questions, <b> blocking`, `<n> decisions` and `<bound> of <n> verifying examples bound`, each count a plain number, a zero written `0`, and a zero part such as `, 0 keyed` left out; `Code` two lines, `<n> implementing` and `<n> referencing`. Below the phone breakpoint each member reads as a stacked block: its title, id and kind, its rung badge and floor, and its next rung with each unmet clause stay together at the top, and its depth, design counts and binding counts follow as labelled runs that wrap; no part of a member sits beyond a horizontal scroll.", + foldedExamples: + "a row under the parent's row spanning the table, holding a closed disclosure whose summary reads `<n> example members folded in`, followed by `, <h> held below the next rung` when any is held; inside, one line per example with its title link, its id, its rung badge, `floor <rung>` and `next <rung>:` with `nothing blocks` or each unmet clause.", + componentTable: + "a table with the columns `Component`, `Layer and context` and `Reached through`, the last listing each way the slice reaches the component on its own labelled line.", + designTable: + "a table with the columns `Design`, `Component`, `Declared`, `Anchor` and `Comparison`; `Declared` one labelled line per key, `not stated` for a key the design leaves out; `Anchor` reads `absent`, `present, does not satisfy the design` or `satisfies the design`; `Comparison` reads `not compared` for a design no anchor satisfies, `matches` for a realized design with no differing or unreadable row, and otherwise one line per differing field as `<field>: declared <value>; anchored <value>`, a `uses` line reading `uses: only declared <components>; only anchored <components>` as the comparison names them, and one line per unreadable key as `<key>: unreadable <value>, not compared`.", + outsideList: + "a list headed `Outside the slice, shown for context and not counted as touched`, each component with its id, linked when anchored, its label, `anchored` or `not anchored`, and how it is reached in words.", + decisionTable: + "a table with the columns `Decision`, `Rung`, `In the slice`, `Shapes` and `Questions`.", + questionItem: + "the blocking flag in words; for a keyed question, `question.<key>` as a link to its address with a copy action for the full address; its Spec; the text; each present field on its own labelled line; then `Holds: ` and the held members, or nothing for a question that holds none.", + boundaryTable: + "five columns, Spec, rung, implementation binding, joined members and holds, the joined members listing each relation type with its members.", + }, + verification: { + mode: "manual", + criteria: [ + "A person given only the slice page of the first adopter's history-view Pack names the member the most others rest on, the components the Pack's code touches and the outside Specs whose code the Pack builds on, from the page alone.", + "The same person, given the slice page of a Pack whose members no code realizes yet, names the components the design will touch through what it rests on, and the members carrying pinned declarations while they state below `defined`.", + "The same person, given the slice page of a Pack holding member component designs, says for each design whether code realizes it and where its declared values differ from the anchored ones, and reads no declared value as anchored.", + "The same person, given the same page on a phone, names each member's stated next rung and what holds it without scrolling sideways, including an example member folded into its parent.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.slice-page.sdp.md", + }, + { + id: "spec:consumers.roadmap", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + title: "A roadmap orders the next bodies of work, and the graph says what each still needs", + narrative: + "The Protocol's planning reads the graph: the build backlog lists ready work no code realizes, the design backlog lists defined work, and the statement queue orders what a person can state. None of them says which capability comes first. A roadmap is that one authored choice, the order of the next few bodies of work. Everything else about an entry, whether it still needs design, waits only for a person's statement, is ready to build, or is built, is read from the graph each time, so a roadmap never goes stale in what it says about state. It goes stale only in its order, which its owner edits.\n\nAn entry stands for its whole refinement subtree: an epic for its features and stories, a feature for its stories. The reading counts the subtree's work by what each part needs, so a large entry shows how much of it is designed and built, and the parts that could be built now.", + sections: { + intent: { + actor: + "The owner choosing what the next pull requests take up, and an agent opening the next body of work.", + problem: + "Priority lives in prose, in each pull request's forward section and in the plan files, which the graph cannot read and which go stale as work lands.", + outcome: + "Read a roadmap as an ordered list of upcoming capabilities, each placed by the graph in what it needs next, to design, to state, to build or built, with the design it lacks and the work it rests on.", + }, + behavior: { + rules: [ + "A roadmap's entries are its Pack's members in authored order, each numbered by its place from 1; an entry may be an epic, a feature or a story, and its place states its owner's preference and nothing else.", + "An entry's scope is the entry and every Spec that reaches it through one or more `refines` edges, each counted once whichever parent it reaches through; its work is the Specs of its scope whose kind is neither `example` nor `decision`, the build backlog's exclusions.", + "Each Spec in a scope has a need read from the graph: to design when it states a rung below `ready` and its floor does not reach `ready`; to state when it states a rung below `ready` and its floor reaches `ready`, so only a person's statement stands before it; to build when it states `ready`, is neither an example nor a decision, and is not `implemented`; built when it states `ready` and is `implemented`; settled when it is an example or a decision that states `ready`.", + "A Spec of an entry's work that is `implemented` while it states below `ready` keeps its need and counts as drift, as the drift alarm reads it.", + "An entry needs the first of to design, to state and to build that any Spec of its work needs, and is built when every Spec of its work is built; an entry reads built only when every Spec of its work states `ready` and is `implemented`.", + "An entry that is itself an example or a decision, whose scope holds no work, takes no need: it is listed apart with its place and its own need, and it is read through the entries it verifies or shapes.", + "Each entry shows its stated rung, the rung its floor reaches, its stated next rung with the floor clauses that hold it, its work counted by need with the parts to build now, every blocking open question in its scope, and every decision below `ready` that its scope holds or names by `decidedBy`, with that decision's blocking questions, keyed or not.", + "Each entry lists every Spec of its work once, with its need, its stated rung, the rung its floor reaches, its stated next rung with the floor clauses and the dependency targets that hold it, and whether it is drift; the parts with the entry's need are the ones that set it, so a descendant that holds an entry back is named with what holds it, and the entry's own readiness stays labelled apart.", + "An entry rests on each resolving Spec outside its scope that any Spec of its scope names by `dependsOn`, `constrainedBy` or `refines`, while that Spec is to design, to state or to build, so a descendant's second parent outside the entry counts as the entry's own parent does; shaping decisions stay in the decision rows. Each row names the relation, the Specs of the scope that name it, the Spec's need and stated rung, whether it holds a `ready` statement in the scope because it states below `defined`, and the place of the first entry whose scope holds it, or none when it lies outside the roadmap.", + "An entry is rested on by each Spec outside its scope, to design, to state or to build, that names a Spec of its scope by `dependsOn`, `constrainedBy` or `decidedBy`, so a reader sees what waits on it.", + "Apart from the entries, the reading lists what waits on the person the caller names, the owner by default: each open question whose `decider` names that person, on a Spec of any entry's scope or on a decision a scope names by `decidedBy`; and each such Spec or decision whose floor reaches a rung above the rung it states, a statement only a person makes, which for a decision is its ratification. Each row carries the places of the entries it touches. Questions follow their first place; statements keep their relative order in the statement queue across the whole roadmap, so a Spec others rest on comes first whatever its place, and their places never reorder them.", + "The open questions over the same Specs that name no `decider` are listed apart and counted, blocking and not, so a question nobody is named to settle never reads as an empty workload; the reading never infers a decider from a question's prose.", + "Each entry lists the waiting statements the statement queue puts before its own: every Spec that the queue says one of the entry's waiting statements rests on, its work's and its shaping decisions' alike, each once, in the queue's order, marked as inside the entry, at the first place of another entry whose scope or shaping decisions it is among, or outside the roadmap. The list advises the order of statements and is never a readiness requirement.", + "A built entry keeps its place and reads built until someone edits the roadmap; nothing removes it, and no check reports it.", + "Two entries may share Specs, as an epic and a feature beneath it do; each reads its own scope and names the places of the entries it shares a Spec with.", + "A Pack whose manifest does not state `order: priority` reads the same way when it is named, and the answer says its order is reading order, so no view presents it as a priority.", + "The reading states, records and gates nothing: it moves no rung, authors no fact and reports no finding, and work taken up out of its place is never a finding.", + ], + ruleKeys: { + entries: "rules[0]", + scope: "rules[1]", + specNeed: "rules[2]", + drift: "rules[3]", + entryNeed: "rules[4]", + apart: "rules[5]", + entryShows: "rules[6]", + workRows: "rules[7]", + restsOn: "rules[8]", + restedOnBy: "rules[9]", + waiting: "rules[10]", + withoutDecider: "rules[11]", + earlier: "rules[12]", + builtStays: "rules[13]", + overlap: "rules[14]", + readingOrder: "rules[15]", + statesNothing: "rules[16]", + }, + exampleSpace: { + given: [ + "a probe roadmap whose entry holds the work {work:string}", + 'that work {state:"is ready and implemented throughout"|"has one part ready and not implemented"|"has one part below ready whose floor reaches ready"|"has one part below ready whose floor stops short"|"has one part implemented below ready"|"has a ready root and one descendant without its kind\'s evidence"|"has a ready root and one descendant held by a blocking question"|"has one descendant that also refines a Spec outside the roadmap"}', + "a probe roadmap whose first entry rests on a Spec of its second entry, both waiting for a statement", + ], + when: ["the roadmap recipe reads the probe roadmap"], + then: [ + 'the entry\'s need reads {need:"to design"|"to state"|"to build"|"built"}', + "the entry counts {drift:number} parts as drift", + "the entry names {part:string} among the parts with its need, held by {clause:string}", + "the entry rests on {target:string} through {origin:string}", + "the waiting statements list {earlier:string} before {later:string}", + ], + }, + }, + design: { + roadmapRecipe: + 'recipe 53, the roadmap, takes `params.pack`, one Pack id, and `params.decider`, a token on the `decider` field\'s grammar, `"owner"` by default; without `pack` it reads every Pack whose manifest states `order: priority`, in id order; a `pack` that names no Pack, or a `decider` off the grammar, returns `{ diagnostics: [{ parameter, problem }] }` and no roadmaps.', + roadmapAnswer: + '`{ decider, roadmaps }`, one roadmap per Pack read: `{ pack, title, framing, order, totals, entries, apart, unresolved, waiting }`, where `order` is `"priority"` or `"reading"` and `totals` is `{ entries, toDesign, toState, toBuild, built, apart, unresolved, questions, blockingQuestions, withoutDecider, blockingWithoutDecider, statements }`, the entries with each need and the rows waiting. `entries` in `totals` counts every manifest member, including unresolved ones, and `totals.unresolved` counts the unresolved members; `unresolved` lists `{ place, id }` in manifest order for members that do not resolve, and every place is the original manifest position, never renumbered.', + needValues: + 'an entry\'s `need` is `"to design"`, `"to state"`, `"to build"` or `"built"`; a Spec\'s need is one of those or `"settled"`.', + entryRow: + "`{ place, id, title, specKind, altitude, need, statedReadiness, floorReached, statedNextRung, holdsNextRung, work, workRows, buildNow, blockingQuestions, decisions, restsOn, restedOnBy, earlierStatements, packs, sharesScopeWith }` in place order, where `statedNextRung` is null at `ready`, `holdsNextRung` lists the holding rows that keep the floor from the stated next rung, `work` is `{ specs, toDesign, toState, toBuild, built, drift }` counted over the entry's work, `buildNow` lists the ids of its work to build, `packs` lists the entry's Packs other than the roadmap, and `sharesScopeWith` lists places.", + workRow: + "`workRows` lists every Spec counted in `work` once, in Spec-id order, as `{ id, need, statedReadiness, floorReached, statedNextRung, holdsNextRung, drift }`, with `statedNextRung` and `holdsNextRung` read as on the entry row and `drift` a boolean; the rows whose `need` equals the entry's are the ones that set it.", + holdsRow: + "a holding row is `{ clause, targets }`, `clause` the id of a floor clause that fails at the stated next rung, in the floor evaluator's order, and `targets` the ids that the typed-dependency clause names, in its order, empty for any other clause.", + questionRow: + "`{ address, spec, index, question, blocking, decider, provisional }` for each blocking question in the entry's scope, `address` null when the question carries no key, `index` its authored position from 0, and `decider` and `provisional` null when absent, sorted by Spec id and authored position.", + decisionRow: + "`{ id, title, statedReadiness, blockingQuestions }` for each decision below `ready` the entry's scope holds or names by `decidedBy`, sorted by id, `blockingQuestions` listing a question row for each of its blocking questions, keyed or not.", + restsOnRow: + '`{ id, via, from, need, statedReadiness, holdsReady, onRoadmap }`, where `via` is `"dependsOn"`, `"constrainedBy"` or `"refines"`, `from` lists in id order the Specs of the scope that name it by that relation, `holdsReady` is true when the Spec states below `defined`, and `onRoadmap` is the place of the first entry whose scope holds it or null; rows sort by id, then `via`.', + restedOnByRow: + "`{ id, via, need, onRoadmap }`, where `via` is the relation it names the scope by; rows sort by id, then `via`.", + earlierRow: + '`earlierStatements` lists `{ id, where, onRoadmap }` in the statement queue\'s order, where `where` is `"entry"` for a Spec of the entry\'s scope or one of its shaping decisions, `"roadmap"` for a waiting statement another entry touches through its scope or its shaping decisions, and `"outside"` otherwise, and `onRoadmap` is the first place of the entries that statement touches, null unless `where` is `"roadmap"`.', + apartRow: "`{ place, id, specKind, statedReadiness, need }` in place order.", + waitingRows: + "`waiting` is `{ questions, withoutDecider, statements }`: a question in either list is a question row with `places`; a statement is `{ id, statedReadiness, floorReached, places }`. Questions sort by their first place, then blocking ones first, then by Spec id and authored position. Statements keep their relative order in the statement queue, recipe 46, across the whole roadmap; their `places` name the entries they touch and never reorder them.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/roadmap.sdp.md", + }, + { + id: "spec:consumers.spec-studio.roadmap-page", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "The roadmap page shows the upcoming work in priority order, split by what it needs", + narrative: + "The owner reads a roadmap to choose what the next pull requests take up, and a reviewer reads it to see where an arc stands. The page shows each roadmap at a glance as a board, its entries in the column of what each needs next, and then each entry with what holds it. It ends with the forward section a pull request description carries, ready to copy. The page orders by the owner's places and links; it states nothing.", + sections: { + intent: { + actor: "The owner choosing the next work, and a reviewer with only a link.", + problem: + "No page shows what comes next in priority order, so the owner reads it from the last pull request description, and a reviewer cannot tell which upcoming work needs design, a statement or a build.", + outcome: + "Give the Studio a page that shows each roadmap in its priority order, with what to design, what to state, what to build and what is built, and for each entry what it lacks.", + }, + behavior: { + rules: [ + "The page set holds `roadmap.html`, which renders recipe 53 over every Pack whose manifest states `order: priority`, one section per roadmap in id order, each ending with recipe 55's memo for that Pack.", + "With no such Pack, the page says the corpus states no roadmap and that a Pack becomes one when its manifest states `order: priority`.", + "Each roadmap's section opens with its title linked to its Pack page, its framing, and sentences the build counts from the answer: the entries with each need, those listed apart and any unresolved members, said to be in priority order only when the answer's `order` is `priority` and in authored reading order otherwise; the questions waiting on the owner, blocking and not, and those that name no decider; and the statements waiting, with a link to the owner queue.", + "A board follows: four columns, to design, to state, to build and built, each headed by its need and its count and listing its entries in place order, each as its place and its title linked to the entry's row below; an empty column reads `none`.", + "Beside the board, a legend reads `To state means the floor allows ready and a person still judges the statement. Built means every Spec of the entry's work states ready and has an implementation binding; it reports no passing test and no deployment. The entry's own readiness is shown separately.`", + "Four lists follow in the same order, each keeping the roadmap's order and numbering each entry by its place, so a gap in the numbers shows an entry with another need; an empty list says no entry has that need.", + "Each entry shows its place and title, its kind and altitude, its stated rung and the rung its floor reaches, and its work by need with any drift; opened, it shows its Spec linked, its stated next rung with the clauses that hold it or `nothing blocks`, the parts with its need, each linked with its rungs and what holds its next rung, its own labelled `itself`, with its other parts folded under one line that counts them, the parts to build now linked, its blocking questions linked to their Spec pages, its decisions below `ready` with their blocking questions, what it rests on with the parts that reach it, what waits on it, for an entry to state the waiting statements the statement queue puts before its own, its other Packs and the entries it shares Specs with.", + "Among what an entry rests on, the Specs to design, the Specs on the roadmap and the Specs that hold a `ready` statement show first, each with its relation, its need and its place on the roadmap or `outside the roadmap`; the rest fold under one line that counts them.", + "After the lists, the page lists what waits on the owner, recipe 53's `waiting`: each question with its blocking flag, its address linked to the question on its Spec page with a copy action, its text and its provisional reading when present, and each waiting statement with its stated rung and the rung its floor allows, in the order the answer gives, each row linking the entries it touches. The questions that name no decider follow under the sentence `These questions have no named decider.`, apart from the owner's.", + "The entries listed apart follow the lists, each with its place, kind, stated rung and need. Unresolved members follow under `Unresolved members`, each with its original place and its id as code, with no invented title, rung or need.", + "Each roadmap ends with a disclosure holding recipe 55's memo as preformatted text with a copy action, so a description's author pastes the forward section the graph holds.", + "A filter narrows the lists by need; without script every row shows.", + "The page orders by the roadmap's places and links: it states no rung, approves nothing, and never presents a place as a date, a deadline or permission to start.", + ], + ruleKeys: { + roadmapPage: "rules[0]", + noRoadmap: "rules[1]", + roadmapSummary: "rules[2]", + board: "rules[3]", + readingLegend: "rules[4]", + needLists: "rules[5]", + entryRow: "rules[6]", + restsOnShown: "rules[7]", + waitingList: "rules[8]", + apartList: "rules[9]", + forwardSection: "rules[10]", + needFilter: "rules[11]", + statesNothing: "rules[12]", + }, + }, + ui: { + boardColumn: + "a column headed `To design`, `To state`, `To build` or `Built` with its count, listing `<place>` and the title as a link to the entry's element id; the four columns sit side by side at desktop width and stack at phone width.", + entryRow: + "a disclosure whose summary reads `<place>. <title>` on its first line and, on its second, the kind and altitude, the stated rung as a badge, `floor <rung>`, and the work as `<n> Specs:` followed by the nonzero counts of `to design`, `to state`, `to build` and `built`, then `<d> drift` when present; its element id is `roadmap-<name>-entry-<place>`, `<name>` the name the page set gives the roadmap's Pack page, and every link to an entry, from the board, a rests-on row, the waiting list or a shared-Specs line, uses the owning roadmap's prefix, so two roadmaps on the page never share an id.", + entryFields: + "labelled lines Id, Next, Parts, Build now, Blocked by, Decided by, Rests on, Waited on by, Earlier statements, Also in and Shares Specs with, an empty line left out; Parts appears when the entry has more than one part or a part other than itself sets its need.", + restsOnItem: + "the Spec linked, its relation in words, followed by `from` and the parts that reach it when any part other than the entry itself does, its need, `holds ready` when it states below `defined`, and `entry <place>` linked to that row or `outside the roadmap`.", + memoDisclosure: + "a disclosure whose summary reads `The forward section, as Markdown`, holding a copy action and the memo in a `pre` element.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/spec-studio.roadmap-page.sdp.md", + }, + { + id: "spec:consumers.roadmap.memo", + specKind: "behavior", + altitude: "story", + readiness: "ready", + title: "A roadmap renders as the forward section of a pull request description", + narrative: + "A pull request description ends with a forward section: the next work in priority order, what waits on the owner, and what is deferred until its trigger. Written by hand, it drifts from the Specs it names and is rewritten each time. The roadmap memo renders it from the roadmap reading at the head of the branch, so the forward section a description carries is the roadmap the graph holds. The memo states what the graph holds and nothing more: it copies each entry's purpose from its own intent and derives its dependency reach, and the owner's reason for an entry's place, the re-entry condition of the next pull request and the durable refused list stay the description author's words around it.", + sections: { + intent: { + actor: "The author of a pull request description, and the owner reading it.", + problem: + "A description's forward section is written by hand from plan files and recipe answers, so it drifts from the roadmap and the Specs it names.", + outcome: + "Render a roadmap's entries, what waits on the owner and the deferred questions as Markdown a pull request description carries as its forward section, so the plan a description proposes is the roadmap the graph holds.", + }, + behavior: { + rules: [ + "Recipe 55 renders one roadmap's reading, recipe 53's answer for that Pack, as one Markdown memo a pull request description carries as its forward section; the roadmap page shows the same bytes.", + "The memo holds, in order: the roadmap's title, a line saying what to state and built mean, and its framing; the entries to design, to state and to build, each need in priority order with every entry's place; what waits on the owner; the questions that name no decider; the entries built and still listed; the unresolved members with their original places and ids; and the deferred open questions with their triggers. The to design and to build sections always appear and say when they hold no entry; every other section is left out when it holds no row.", + "An entry's purpose and reach is read, never authored: its intent's problem, or its outcome when it states no problem, then how many Specs outside its scope wait on it and how many of its parts are ready to build now. These values state its purpose and its place in the graph's structure, never why the owner selected the entry or gave it its place; the description's author may state that reason around the memo, and the memo adds no status, date, estimate or assignment to a pull request.", + "An entry to design names the parts that set its need, its own first, each with its stated next rung and the clauses and dependency targets that hold it, then its blocking questions and its decisions below `ready`; an entry to state names the waiting statements the statement queue puts before its own, inside the entry, elsewhere on the roadmap and outside it, as advice on the order of statements and never as a readiness requirement; an entry to build names each Spec it rests on that is not built.", + "The memo derives no reason for the priority and carries no re-entry condition or refused list; the description's author writes those around it.", + "A memo of a Pack whose order is reading order says so under the framing, so no description presents it as a priority.", + "One graph and one set of parameters render one memo, byte for byte.", + ], + ruleKeys: { + memoRecipe: "rules[0]", + sections: "rules[1]", + purposeAndReach: "rules[2]", + holds: "rules[3]", + authorsOwn: "rules[4]", + readingOrderNoted: "rules[5]", + deterministic: "rules[6]", + }, + }, + design: { + memoRecipe: + "recipe 55, the roadmap memo, takes `params.pack` and `params.decider` as recipe 53 does; without `pack` it reads the one Pack whose manifest states `order: priority`, and with none or several it returns `{ diagnostics: [{ parameter, problem }], memo: null }`; otherwise it answers `{ diagnostics: [], memo }`, the memo one string.", + memoHead: + "the memo opens with the line `## The next work, from the roadmap`, a blank line, the line `Roadmap: ` followed by the Pack's title and its id as a code span, a blank line, the line `To state means the floor allows ready and a person still judges the statement. Built means every Spec of the entry's work states ready and has an implementation binding; it reports no passing test and no deployment.` and a blank line, each paragraph of the framing as authored followed by a blank line, and for a reading-order Pack the line `This Pack's order is reading order, not a stated priority.` and a blank line.", + memoDesign: + "the section `### To design` holds a table with the columns `Place`, `Entry`, `Next` and `Purpose and reach`; `Next` reads, for at most three of the work rows with the entry's need, the entry's own first and the rest in Spec-id order, the row's id as a code span or `itself` for the entry's own, its stated rung and stated next rung as code spans joined by ` → `, then `held by` and each clause id followed by its targets as code spans in parentheses, or `nothing holds it`; then `and <n> more parts to design` when rows are left out; then `blocked by` and each blocking question's address as a code span, or its Spec id when unkeyed; then `decided by` and each decision below `ready` as a code span with its stated rung; the parts joined by `; `.", + memoState: + "the section `### To state` holds a table with the columns `Place`, `Entry`, `Earlier waiting statements` and `Purpose and reach`; the third column reads `No preceding waiting statement in the statement queue.` when the entry's `earlierStatements` is empty, and otherwise `in this entry` followed by those inside it as code spans, `on the roadmap` followed by those at another entry as code spans each with `(place <n>)`, and `<n> outside the roadmap`, each part present only when it holds a row and the parts joined by `; `.", + memoBuild: + "the section `### To build` holds a table with the columns `Place`, `Entry`, `Purpose and reach` and `Rests on`; `Rests on` lists each Spec the entry rests on as a code span with its need, or reads `nothing unbuilt`.", + memoEntry: + "an `Entry` cell is the entry's title followed by its id as a code span; the `Purpose and reach` cell is the intent line, ending with a full stop when it ends with none, then `Waited on by <n>.` when Specs wait on it and `<n> of its parts ready to build now.` for an entry not to build that has such parts.", + memoWaiting: + "the section `### Waiting on the <decider>` lists one line per row of recipe 53's `waiting.questions` and `waiting.statements`, questions first and statements in the order the answer gives: a question as its address as a code span, or its Spec id when unkeyed, `(blocking)` when it blocks, `place` and the places, and its text; a statement as `State `, the Spec id as a code span, its stated rung and the rung its floor allows, and the places.", + memoUndecided: + "the section `### Questions without a decider` opens with the line `These questions have no named decider.` and a blank line, then lists one line per row of recipe 53's `waiting.withoutDecider`: its address as a code span, or its Spec id as a code span followed by `question` and its authored position from 1 when unkeyed, then `(blocking)` when it blocks, `place` and the places; the text stays on the roadmap page, so a corpus with many such questions keeps a short memo.", + memoBuilt: + "the section `### Built and still on the roadmap` lists each built entry as its place, its title and its id as a code span. The section `### Unresolved members` follows when recipe 53 reports unresolved members, listing each as `- Place <place>: <id>`, the id as a code span.", + memoDeferred: + "the section `### Deferred, with their triggers` lists each open question recipe 20 marks as a deferral, in the register's order, as its address as a code span, or its Spec id when unkeyed, its text, and `Trigger:` with its trigger.", + memoMarkup: + "code spans follow the proposal memo's rule; outside code spans, authored `<`, `>` and `&` are written as `<`, `>` and `&`, respectively; other authored text is written verbatim, with each line break written as a space and, inside a table cell, each `|` written `\\|`; every line ends with a line feed and the memo with one.", + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + file: "specs/consumers/roadmap.memo.sdp.md", + }, + { + id: "spec:consumers.spec-studio.cited-line-text", + specKind: "behavior", + altitude: "story", + readiness: "scoped", + title: "A pinned declaration shows the words of each test line that cites it", + narrative: + "A reader checking a pinned declaration reaches the test line that cites it and then follows a source link to read the assertion. The words of that line would close the task on the page. The graph records where a line cites an entry and never what the line says, and the Studio reads only the graph and its named inputs, so one of them has to hold the words. The extractor already reads the line when it records the citation, which makes the citation row the cheapest honest home; the choice is the owner's, because it puts source text in the graph for the first time.", + sections: { + intent: { + actor: + "A reviewer with only a link, checking that a pinned declaration is held to its code.", + problem: + "The declarations page names each citing line by file and line only, so the reader leaves the page to read it, and has no route at all when the page set carries no source link.", + outcome: + "Show beside each pinned declaration the text of every test line that cites it, so a reviewer reads the check without leaving the page.", + openQuestions: [ + { + question: + "May the Studio show source text the graph does not hold, and if so is it recorded in the graph beside the citation or read as a named input?", + blocking: true, + key: "lineText", + decider: "owner", + provisional: + "the extractor records the citing line's text, trimmed and bounded, on the citation row it already writes, as inferred data that confers nothing, and the Studio reads no source", + }, + { + question: + "How long may a recorded line be, and how does a page show a line that was cut?", + blocking: false, + key: "lineBound", + decider: "delegated", + provisional: + "a bound of a few hundred code units, cut at a word boundary and marked as cut, the whole line one source link away", + }, + ], + }, + behavior: { + rules: [ + "Beside each citing line, the declarations page and the Spec page show the line's words as code after its file and line, and the reader's citation row carries them.", + "The words are what the citing line said at the commit the graph was derived from; no page says the line asserts, checks or passes anything because of them.", + "A citation whose words are not recorded shows its file and line as it does now.", + ], + ruleKeys: { + lineShown: "rules[0]", + wordsNotVerdict: "rules[1]", + withoutWords: "rules[2]", + }, + }, + }, + deliveryFacts: [], + file: "specs/consumers/spec-studio.cited-line-text.sdp.md", + }, + { + id: "spec:consumers.spec-studio.register-input", + specKind: "behavior", + altitude: "story", + readiness: "scoped", + title: "An outside register is a named Studio input, so a cited token links to its entry", + narrative: + "A corpus that cites an outside register, an open-decisions list or an earlier system's references, writes its tokens in prose. The external-registers recipe checks them against a register file the corpus passes; a reader on the page still sees only the token. On the first adopter's pages a reader met a cited open decision and found no route to its entry. The register is the corpus's own data, so it reaches the Studio the way the base graph and the run records do: named on the command line, named on every page that shows it, and never graph data.", + sections: { + intent: { + actor: + "A reviewer with only a link who meets a cited token, such as an open decision's number, in a Spec's text.", + problem: + "A cited token reads as bare text on every page, and the register that defines it is neither linked nor shown.", + outcome: + "Let the Studio take an outside register as a named input, so each token a corpus cites from it links to its entry, named as outside evidence and never as graph data.", + openQuestions: [ + { + question: + "What does a register file give the Studio beyond what the external-registers recipe reads, so a token's entry can be shown?", + blocking: true, + key: "registerEntry", + decider: "delegated", + provisional: + "an optional `title` and an optional `href` on a token object; the Studio writes one page per register listing each token with its title, retired tokens marked, each linking to its `href` when present", + }, + { + question: + "How does the studio verb name a register, its pattern and the words that name it on a page?", + blocking: true, + key: "inputForm", + decider: "delegated", + provisional: + "a repeatable `--register NAME=PATH` with a matching `--register-pattern NAME=SOURCE`, the name naming the register in words wherever it shows", + }, + { + question: + "Does a cited token become a link inside the rendered text of a Spec, or only in a list beside it?", + blocking: false, + key: "textLinks", + decider: "delegated", + provisional: + "inside the text, matched by the register's pattern exactly as the recipe matches it, so the page and the check agree on what is cited", + }, + ], + }, + behavior: { + rules: [ + "A register is a named input like the base graph and the run records: a page that shows it names it in words, and it never becomes graph data, a finding, a fact or a rung.", + "Each token the register's pattern finds in a Spec's text routes to that token's entry on the register's page, and a token the register does not list, or retires, reads so beside it.", + "Without the input every page is what the graph alone writes.", + ], + ruleKeys: { + registerNamed: "rules[0]", + tokenRoute: "rules[1]", + withoutRegister: "rules[2]", + }, + }, + }, + deliveryFacts: [], + file: "specs/consumers/spec-studio.register-input.sdp.md", + }, + { + id: "spec:consumers.design-slice.chain", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.chain.sdp.md", + title: "Members rest on a chain", + narrative: null, + sections: { + intent: { + outcome: "Members rest on a chain.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "rest on each other in a chain"}', + 'one member depends on an outside Spec {outside: "that no code satisfies"}', + 'one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + 'the members read in depth order {order: "spec:probe.base, spec:probe.design, spec:probe.top"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.decision-backlink", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.decision-backlink.sdp.md", + title: "A shaping decision comes before its member", + narrative: null, + sections: { + intent: { + outcome: "A shaping decision comes before its member.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "include a decision that refines the member it shapes"}', + 'one member depends on an outside Spec {outside: "that no code satisfies"}', + 'one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + 'the members read in depth order {order: "spec:probe.base, spec:probe.design, spec:probe.top"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.dependency-cycle", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.dependency-cycle.sdp.md", + title: "A cycle shares a depth", + narrative: null, + sections: { + intent: { + outcome: "A cycle shares a depth.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "include two members that depend on each other"}', + 'one member depends on an outside Spec {outside: "that no code satisfies"}', + 'one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + "the slice reports {cycles: 1} cycles and {exampleMembers: 0} folded example members", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.folded-example", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.folded-example.sdp.md", + title: "An example folds without lending its readiness", + narrative: null, + sections: { + intent: { + outcome: "An example folds without lending its readiness.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "include an example that refines a member"}', + 'one member depends on an outside Spec {outside: "that no code satisfies"}', + 'one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + "the slice reports {cycles: 0} cycles and {exampleMembers: 1} folded example members", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.independent-example", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.independent-example.sdp.md", + title: "An example with its own dependency stays unfolded", + narrative: null, + sections: { + intent: { + outcome: "An example with its own dependency stays unfolded.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "include an example that refines a member and depends on an outside Spec"}', + 'one member depends on an outside Spec {outside: "that no code satisfies"}', + 'one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + "the slice reports {cycles: 0} cycles and {exampleMembers: 0} folded example members", + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.component-absent", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.component-absent.sdp.md", + title: "An unanchored component design stays visible", + narrative: null, + sections: { + intent: { + outcome: "An unanchored component design stays visible.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "rest on each other in a chain"}', + 'one member depends on an outside Spec {outside: "that a component\'s unit satisfies"}', + 'one member is a component design whose component {anchor: "no anchor holds"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + 'the member design reads {realized: "unrealized"} with {differing: 0} differing and {unreadable: 0} unreadable rows', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.component-unrelated", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.component-unrelated.sdp.md", + title: "An anchor alone does not realize a component design", + narrative: null, + sections: { + intent: { + outcome: "An anchor alone does not realize a component design.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "rest on each other in a chain"}', + 'one member depends on an outside Spec {outside: "that a component\'s unit satisfies"}', + 'one member is a component design whose component {anchor: "an anchor holds without satisfying the design"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + 'the member design reads {realized: "unrealized"} with {differing: 0} differing and {unreadable: 0} unreadable rows', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.component-matching", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.component-matching.sdp.md", + title: "A realized component matches its declared values", + narrative: null, + sections: { + intent: { + outcome: "A realized component matches its declared values.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "rest on each other in a chain"}', + 'one member depends on an outside Spec {outside: "that a component\'s unit satisfies"}', + 'one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + 'the member design reads {realized: "realized"} with {differing: 0} differing and {unreadable: 0} unreadable rows', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.component-differing", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.component-differing.sdp.md", + title: "A realized component keeps its differing layer and uses apart", + narrative: null, + sections: { + intent: { + outcome: "A realized component keeps its differing layer and uses apart.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "rest on each other in a chain"}', + 'one member depends on an outside Spec {outside: "that a component\'s unit satisfies"}', + 'one member is a component design whose component {anchor: "an anchor satisfies, with a differing layer and uses"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + 'the member design reads {realized: "realized"} with {differing: 2} differing and {unreadable: 0} unreadable rows', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.component-unreadable", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.component-unreadable.sdp.md", + title: "An unreadable declared layer stays visible", + narrative: null, + sections: { + intent: { + outcome: "An unreadable declared layer stays visible.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "rest on each other in a chain"}', + 'one member depends on an outside Spec {outside: "that a component\'s unit satisfies"}', + 'one member is a component design whose component {anchor: "an anchor satisfies, with an unreadable layer"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + 'the member design reads {realized: "realized"} with {differing: 0} differing and {unreadable: 1} unreadable rows', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.outside-satisfied", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.outside-satisfied.sdp.md", + title: "A prerequisite reaches its satisfying component", + narrative: null, + sections: { + intent: { + outcome: "A prerequisite reaches its satisfying component.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "include a decision that refines the member it shapes"}', + 'one member depends on an outside Spec {outside: "that a component\'s unit satisfies"}', + 'one member is a component design whose component {anchor: "an anchor holds without satisfying the design"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + 'the touched components are {components: "component:probe.dependency, component:probe.designed"}, {throughRestsOn: 1} of them reached through what the slice rests on', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.outside-unbound", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.outside-unbound.sdp.md", + title: "An unbound prerequisite invents no component", + narrative: null, + sections: { + intent: { + outcome: "An unbound prerequisite invents no component.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "include a decision that refines the member it shapes"}', + 'one member depends on an outside Spec {outside: "that no code satisfies"}', + 'one member is a component design whose component {anchor: "an anchor holds without satisfying the design"}', + ], + when: ["the design slice runs over the probe Pack"], + then: [ + 'the touched components are {components: "component:probe.designed"}, {throughRestsOn: 0} of them reached through what the slice rests on', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.outside-holding", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.outside-holding.sdp.md", + title: "An outside prerequisite names the member it holds", + narrative: null, + sections: { + intent: { + outcome: "An outside prerequisite names the member it holds.", + }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "include a decision that refines the member it shapes"}', + 'one member depends on an outside Spec {outside: "stated below defined"}', + 'one member is a component design whose component {anchor: "an anchor holds without satisfying the design"}', + ], + when: ["the design slice runs over the probe Pack"], + then: ['the outside Spec holds {held: "spec:probe.base"}'], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.design-slice.outside-components", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/design-slice.outside-components.sdp.md", + title: "Outside components stop at the touched components' neighbors", + narrative: null, + sections: { + intent: { outcome: "Outside components stop at the touched components' neighbors." }, + behavior: { + examples: [ + { + given: [ + 'a probe Pack whose members {structure: "include a decision that refines the member it shapes"}', + 'one member depends on an outside Spec {outside: "that a component\'s unit satisfies"}', + 'one member is a component design whose component {anchor: "an anchor satisfies, matching every declared value"}', + ], + when: ["the design slice runs over the probe Pack"], + then: ['the outside components are {outsideComponents: "component:probe.neighbor"}'], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.roadmap.built-work", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/roadmap.built-work.sdp.md", + title: "Built work stays on the roadmap", + narrative: null, + sections: { + intent: { + outcome: "Execute one roadmap reading over a probe corpus.", + assumptions: [ + "The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. Every part states ready and has a direct implementation binding. The roadmap reads the shipped body over the extracted probe corpus.", + ], + }, + behavior: { + examples: [ + { + given: [ + 'a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"}', + 'that work {state: "is ready and implemented throughout"}', + ], + when: ["the roadmap recipe reads the probe roadmap"], + then: ['the entry\'s need reads {need: "built"}'], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.roadmap.buildable-part", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/roadmap.buildable-part.sdp.md", + title: "A ready unbound part needs building", + narrative: null, + sections: { + intent: { + outcome: "Execute one roadmap reading over a probe corpus.", + assumptions: [ + "The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The root has a binding and its ready descendant has none. The roadmap reads the shipped body over the extracted probe corpus.", + ], + }, + behavior: { + examples: [ + { + given: [ + 'a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"}', + 'that work {state: "has one part ready and not implemented"}', + ], + when: ["the roadmap recipe reads the probe roadmap"], + then: ['the entry\'s need reads {need: "to build"}'], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.roadmap.statement-part", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/roadmap.statement-part.sdp.md", + title: "A complete part waits for a statement", + narrative: null, + sections: { + intent: { + outcome: "Execute one roadmap reading over a probe corpus.", + assumptions: [ + "The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The descendant states defined with complete behavior evidence and a ready parent. The roadmap reads the shipped body over the extracted probe corpus.", + ], + }, + behavior: { + examples: [ + { + given: [ + 'a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"}', + 'that work {state: "has one part below ready whose floor reaches ready"}', + ], + when: ["the roadmap recipe reads the probe roadmap"], + then: ['the entry\'s need reads {need: "to state"}'], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.roadmap.design-part", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/roadmap.design-part.sdp.md", + title: "An unfinished part needs design", + narrative: null, + sections: { + intent: { + outcome: "Execute one roadmap reading over a probe corpus.", + assumptions: [ + "The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The descendant states scoped and has a blocking question. The roadmap reads the shipped body over the extracted probe corpus.", + ], + }, + behavior: { + examples: [ + { + given: [ + 'a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"}', + 'that work {state: "has one part below ready whose floor stops short"}', + ], + when: ["the roadmap recipe reads the probe roadmap"], + then: ['the entry\'s need reads {need: "to design"}'], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.roadmap.drifting-part", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/roadmap.drifting-part.sdp.md", + title: "A binding below ready remains drift", + narrative: null, + sections: { + intent: { + outcome: "Execute one roadmap reading over a probe corpus.", + assumptions: [ + "The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The descendant states scoped, has a blocking question and an implementation binding. The roadmap reads the shipped body over the extracted probe corpus.", + ], + }, + behavior: { + examples: [ + { + given: [ + 'a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"}', + 'that work {state: "has one part implemented below ready"}', + ], + when: ["the roadmap recipe reads the probe roadmap"], + then: ["the entry counts {drift: 1} parts as drift"], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.roadmap.missing-evidence", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/roadmap.missing-evidence.sdp.md", + title: "A descendant without evidence names its holding clause", + narrative: null, + sections: { + intent: { + outcome: "Execute one roadmap reading over a probe corpus.", + assumptions: [ + "The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The descendant states scoped, refines the ready root and has no behavior evidence. The roadmap reads the shipped body over the extracted probe corpus.", + ], + }, + behavior: { + examples: [ + { + given: [ + 'a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"}', + 'that work {state: "has a ready root and one descendant without its kind\'s evidence"}', + ], + when: ["the roadmap recipe reads the probe roadmap"], + then: [ + 'the entry names {part: "spec:probe.part"} among the parts with its need, held by {clause: "kind-evidence-present"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.roadmap.blocking-part", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/roadmap.blocking-part.sdp.md", + title: "A descendant question holds the entry for design", + narrative: null, + sections: { + intent: { + outcome: "Execute one roadmap reading over a probe corpus.", + assumptions: [ + "The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The descendant states scoped, refines the ready root, has one rule and a blocking question. The roadmap reads the shipped body over the extracted probe corpus.", + ], + }, + behavior: { + examples: [ + { + given: [ + 'a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"}', + 'that work {state: "has a ready root and one descendant held by a blocking question"}', + ], + when: ["the roadmap recipe reads the probe roadmap"], + then: [ + 'the entry names {part: "spec:probe.part"} among the parts with its need, held by {clause: "no-blocking-open-questions"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.roadmap.outside-parent", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/roadmap.outside-parent.sdp.md", + title: "A descendant names its outside parent", + narrative: null, + sections: { + intent: { + outcome: "Execute one roadmap reading over a probe corpus.", + assumptions: [ + "The world writes a roadmap Pack with one ready behavior root and one behavior descendant, each with an Intent outcome. The root has one rule and a direct implementation binding; both Specs have resolving relations. The defined descendant refines both the ready root and an outside scoped Spec. The roadmap reads the shipped body over the extracted probe corpus.", + ], + }, + behavior: { + examples: [ + { + given: [ + 'a probe roadmap whose entry holds the work {work: "spec:probe.root, spec:probe.part"}', + 'that work {state: "has one descendant that also refines a Spec outside the roadmap"}', + ], + when: ["the roadmap recipe reads the probe roadmap"], + then: [ + 'the entry rests on {target: "spec:probe.outside"} through {origin: "spec:probe.part"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, + { + id: "spec:consumers.roadmap.statement-order", + specKind: "example", + altitude: "story", + readiness: "ready", + file: "specs/consumers/roadmap.statement-order.sdp.md", + title: "Waiting statements keep dependency order against roadmap places", + narrative: null, + sections: { + intent: { + outcome: "Execute one roadmap reading over a probe corpus.", + assumptions: [ + "The world writes two defined behavior Specs with complete evidence and resolving relations. The first roadmap entry depends on the second; both floors reach ready. The roadmap reads the shipped body over the extracted probe corpus.", + ], + }, + behavior: { + examples: [ + { + given: [ + 'a probe roadmap whose entry holds the work {work: "spec:probe.dependent"}', + 'that work {state: "has one part below ready whose floor reaches ready"}', + "a probe roadmap whose first entry rests on a Spec of its second entry, both waiting for a statement", + ], + when: ["the roadmap recipe reads the probe roadmap"], + then: [ + 'the waiting statements list {earlier: "spec:probe.base"} before {later: "spec:probe.dependent"}', + ], + }, + ], + }, + }, + deliveryFacts: ["has-verifier"], + }, ] as const; diff --git a/test/self-hosting-oracle/decisions.ts b/test/self-hosting-oracle/decisions.ts index c2ae93681..8be0a6355 100644 --- a/test/self-hosting-oracle/decisions.ts +++ b/test/self-hosting-oracle/decisions.ts @@ -1237,7 +1237,7 @@ export const decisionsSpecs = [ "Architectural significance never selects a Spec kind; the Spec states the kind of truth it carries, and relationships among such Specs are the existing relations, with `dependsOn` reserved for genuine semantic need, `supersedes` for actual replacement, and scheduling-flavored edges refused (planning truths live in ruled graph homes, MD-33).", "Negative architecture constraints, a layer that must not use another, stay declared intent in a Spec, never a finding of the Protocol's validators; a corpus that wants one enforced checks its declared `uses`, `layer`, and `context` in its own gate.", "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 (the binding-grain ruling, MD-37), and an anchor is never pointed at an unfinished Spec to manufacture coverage.", + "The engine significance criterion requires exported public surface or cross-component reach whose contract or responsibility a designer or builder must understand, as `spec:model.structural-patterns` defines it; export status or import rank alone is insufficient, so the criterion asks for no anchor on every export and changes no validator. Code never satisfies a decision Spec directly (the binding-grain ruling, MD-37), 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 a decision that shapes it through the `satisfies` to `decidedBy` join, and a unit may name a decision it follows directly through `references`.", @@ -1399,4 +1399,126 @@ export const decisionsSpecs = [ deliveryFacts: [], file: "specs/decisions/studio-build-renders.sdp.md", }, + { + id: "spec:decisions.roadmap-priority", + specKind: "decision", + altitude: "feature", + readiness: "ready", + title: "A roadmap states advisory priority in the graph", + narrative: + "Dependency order already has a graph home: `dependsOn`, which the statement queue reads to order the statements a person can make. Preference has none: which body of work the owner wants next when nothing forces one before another. It lives in prose, in each pull request's forward section and in the plan files, and the graph reads neither. The roadmap views waited for a named reader; the owner, choosing what the next pull requests take up, is that reader. A Pack already keeps its members in authored order and states no truth of its own, so a Pack whose manifest says its order is a priority holds the preference with one optional field and no new relation.", + sections: { + intent: { + outcome: + "Decide whether and how the graph may hold an advisory priority over upcoming work, a roadmap, without dates, without a new relation, and without gating or sequencing any work.", + }, + decision: { + context: + "The planning truths decision gives each planning truth a ruled graph home and keeps recorded work order out of the graph, so dependency rides `dependsOn` and everything else about order stays in plan files and pull request descriptions. The owner now reads and edits that order and asks for it in the graph: what to design next and a prioritized list of what to build, tied to the pull requests that carry it out. A Pack keeps its members in authored order and states no system truth; the Pack manifest's envelope is closed to `id`, `specs` and `modelRefs`.", + decision: + "A roadmap is a Pack whose manifest states `order: priority`; its members' authored order is the corpus's advisory priority, first entry first. `order` is an optional manifest field whose one lawful value is `priority`, and a Pack without it keeps the reading order every Pack has. Priority is preference among bodies of work and nothing more: it carries no date, no dependency, no status and no gate, and no check, recipe or page treats a place on a roadmap as permission, obligation or sequence. Everything a roadmap shows about an entry, what it still needs and what holds it, is read from the graph each time. A corpus may keep several roadmaps, and a Spec may be listed on any number of them.", + rationale: [ + "Hard to reverse: the manifest envelope gains a field that both manifest carriers, the graph's Pack node and the reader carry, and corpora author roadmaps against it. Surprising without context: the planning truths decision kept recorded work order out of the graph, so a reader needs to know why one order is now graph data and why it still sequences nothing. A real trade-off: a priority held in the graph is checked where membership is checked, so every entry resolves; it shows in the design diff, as an added, removed or reordered member and as a Pack that becomes or stops being a roadmap; and it reaches the Studio, the vault and a pull request's forward section with no extra input. It costs one optional field and the risk that an order reads as a schedule, which this decision bounds by giving the order no consequence.", + "A Pack is the right home because a roadmap says nothing about the system. It is an authored grouping, as a Pack is, whose members already carry an authored order; the field states what that order means and leaves the rest of the Pack law unchanged.", + ], + alternatives: [ + "A Pack read as a roadmap by a naming convention, such as a reserved id: the Protocol reads no meaning from an id anywhere else, a reserved name would collide with a corpus's own Packs, and nothing in the manifest would tell its reader that the order means priority.", + "Any Pack's order read as a priority, the roadmap named to each recipe and page as a parameter: the Studio takes no corpus configuration, so it could not tell a roadmap from a Pack in reading order, and a reading order would show as a priority nobody stated.", + "Priority outside the graph, as corpus data a recipe reads, like a perspective's `order`: an entry that no longer resolves is caught only by a rules file the corpus writes, the design diff, the Studio, the vault and the preview workflow would each need a new input to see it, and planning would gain a second store beside the graph. A perspective suits a reader's own concern; a roadmap is one authored choice for the whole corpus.", + "A new relation such as `precedes`, or a rank on each Spec: the planning truths decision refused `precedes` for its engine surface across the model, the carriers, extraction, validators and the agent surface, and a rank on the truth primitive is a status that goes stale with every edit and invites scoring.", + "A boolean field such as `roadmap: true`: it names the noun but not what differs, which is the meaning of the order; `order: priority` says that directly.", + "Prose, as today: the graph cannot read it, and each forward section is rewritten by hand and drifts from the Specs it names.", + ], + consequences: [ + "The planning truths decision's refusal of recorded work order narrows for this one case: a roadmap's priority is graph structure, carried as its Pack's member order, and never workflow authority. Plan numbering, staleness and every other recorded work order stay out of the graph.", + "The Pack manifest admits `order`, in the Markdown and the TypeScript manifest alike; the Pack node carries `order` when the manifest states it, after `modelRefs` in the serialized node; any other value refuses at extraction as a malformed envelope field does; and the Pack model names the priority order. These touch the Pack carrier, the Pack model and the Pack node's member-order law, each enriched in place when the field is built.", + "No check reads a roadmap's content. An entry already built, an order that disagrees with dependency, an empty roadmap and several roadmaps are never findings, because a place carries no authority and staleness is never a finding.", + "Where priority and dependency disagree, a roadmap view shows what an entry rests on and never reorders it; readiness statements keep the statement queue's dependency order, in a roadmap's list of what waits on a person as everywhere else.", + "The design diff reports a change in what a Pack's order means apart from a change in its members' order, so a Pack that becomes or stops being a roadmap shows in a comparison even when its members stay. A base graph whose schema predates the field carries no `order` and reads as reading order, as an absent field does in every schema version; a value the head's engine does not know lies outside the common schema and is reported as a diagnostic, never compared. This touches the design diff and its Studio page when the field is built.", + "A place on a roadmap confers what membership of any Pack confers, which is nothing: no readiness, no delivery fact and no gate.", + "The roadmap reading, its page and its forward-section memo read a roadmap this way, under `spec:consumers.roadmap`.", + ], + }, + }, + deliveryFacts: [], + file: "specs/decisions/roadmap-priority.sdp.md", + }, + { + id: "spec:decisions.roadmap-over-plans", + specKind: "decision", + altitude: "feature", + readiness: "ready", + title: + "Plan and review files stop carrying the delivery record once the roadmap and the pull request do", + narrative: + "Each arc's intent already lives in its capture Specs and its Pack, its rulings in their carrying Specs, its open items in keyed questions, and its story in its pull request description. A plan file still carries the status line the handbook copies and agents read first, the arc's lanes and waves, copies of its ruling tables, and its acceptance as measured. Beside it, committed review reports and their assets carry a review's findings, what became of each, and the screenshots and prototypes the review cites, while a change proposal already gives a finding against a Spec its reviewable shape. Once a roadmap holds priority in the graph and the pull request holds the arc's story and its reviews, the forward role and the record each have one home; what remains of a plan or a review file is history, or a second copy of what the graph, git and the pull request description already hold. Retiring the files removes competing records, never durable evidence.", + sections: { + intent: { + outcome: + "Decide whether the Protocol retires its numbered plan files and its committed review files, reading its next work from the roadmap and keeping each arc's record in git and its pull request, and what replaces the gate that requires a current plan.", + }, + decision: { + context: + "Plan files grew as the Protocol's working record: every arc adds one, the handbook keeps a status line for each, and the self-hosting gate requires the highest-numbered plan to carry a status header the handbook names. Review reports grew beside them under `reviews/`, with their prompts and assets. Since the graph-first planning arc, a plan is a thin pointer to its Pack and capture Specs, and the pull request description standard already asks each description for the arc's story, its rulings as tables, its acceptance as measured and a forward section. The owner asked whether plans can be replaced by Specs: no prose in the plan, but designed, grouped and prioritized roadmaps and sets of features or epics. The owner then retired both folders as carriers of the delivery record, leaving their removal or archival to a separate cleanup change.", + decision: + "The Protocol reads its forward planning from its roadmap and keeps its delivery record in git and its pull requests; it adds no plan file and no review file, and `plans/` and `reviews/` stand as frozen history until a later arc removes or archives them. An agent opening the next work runs the roadmap recipe over the roadmap Pack the handbook names and follows each entry to its Packs and carrying Specs. Before an arc's first push its record is its branch, git being the event log: the arc's first commit message states the owner's working rules for the arc and the arc's Pack, each later message says what its unit did, and outstanding work is read from the arc's Pack, the backlogs and the open-question register; no record file is kept under `reviews/` or anywhere else. The arc pushes and opens its draft pull request at its first coherent commit, normally the scaffold, so the window in which only local git holds the record stays short and a session restarted on another machine sees the work. The draft pull request description then holds the arc's narrative: its story, lanes, rulings with who made them, review findings as change proposals and their memos, acceptance as measured, deferrals and refusals. A review's findings against Specs travel as a change proposal whose base is the commit the reviewer read: the branch head, or, for a proposal composed in a preview, the merge commit its stamp names, checked against that base and posted to the pull request as its memo; a finding against code or another file is a review comment on its line; a finding left open becomes an open question with its decider, provisional reading and links. Evidence no test or recipe carries is committed on the arc's branch with the commands that made it, linked from the description at that commit, and removed before the merge; the Protocol merges its pull requests with merge commits, and GitHub keeps each pull request's head ref, so links at the evidence commit keep resolving. A squash or rebase merge would need the evidence published to a durable ref first. Briefs and prompts stay in the session. The handbook names the roadmap Pack and the roadmap recipe where it named the current plan, and the self-hosting gate checks that instead of a current plan.", + rationale: [ + "Hard to reverse: the handbook, the skills, the description standard and a gate point every session at the newest plan and at committed review files, and every session's first read changes. Surprising without context: a reader finds two folders that stopped. A real trade-off: one forward-planning carrier in the graph and one record per arc in git and its pull request, with nothing to keep in step, against review findings and pre-push state living in commit messages and pull request threads rather than in files a person can search in the tree.", + ], + alternatives: [ + "Keep plan files for forward planning beside the roadmap: priority would live in two places, and the plan's copy would drift.", + "Keep a thin plan file per arc for its execution record alone, with no status gate: the pull request description already carries the same record by the description standard, so the file would be a second copy that the next session must reconcile.", + "Record an arc's execution in the graph, as an arc Spec or Pack fields: who ruled what and when is history that git and the description keep, and that a review happened is never a graph fact.", + "A gate check that refuses a numbered plan past the last frozen one: the cleanup arc removes the folder, so the check would guard a short window, and refusing a kind of file polices the workflow, not conformance; the handbook saying the plans are frozen history is enough.", + "Keep an arc's record in a file under `reviews/` until its first push, then only its link: the file repeats what the commits already say, outlives the arc as a stub, and keeps `reviews/` growing after its retirement.", + "Keep review reports as committed files beside the change proposal: the proposal is the reviewable shape, an applied edit is git history and an open finding is an open question, so a report file repeats all three and goes stale with the next edit.", + "Keep evidence no test or recipe carries in a committed evidence folder on the main line: the folder becomes a new store that grows with every arc, while a link pinned to the arc's commit already lasts after the merge.", + "Link only the latest preview or an expiring CI artifact as evidence: a later run replaces what a historical description cites.", + ], + consequences: [ + "The handbook drops its stack of plan statuses for a short pointer to the roadmap and its recipe. The self-hosting gate's current-record check refuses a handbook that does not name the roadmap Pack and recipe, and a roadmap Pack manifest that is missing or does not state priority order; its frozen historical checks, its green-gate legs and its ban on plan status in the registry and the glossary stay until the cleanup arc removes the folders they read. It adds no check of plan numbers, roadmap content, work order, freshness or review disposition.", + "The graph-first planning Spec's plan-file rule narrows to a plan document a corpus chooses to keep, which points to its Packs and carrying Specs and confers nothing; an adopter keeps whatever planning documents it wants, and a roadmap is open to it as to the Protocol.", + "The description standard renders its forward section with the roadmap memo; the pull request that completes an entry's selected work removes it from the roadmap in the same change, and entries join or move only on the owner's word.", + "The owner's instruction that opens an arc authorizes commits on its branch, as a plan's commit strategy did, and its push; this record convention creates no authority of its own. The arc's first commit message records the owner's working rules and the draft description repeats them. A session that must stop with no clean boundary commits its work in progress on the arc's branch with a message saying what is incomplete, rather than accounting for it in a file; a session resuming without its predecessor reads the branch, its commits, the draft pull request and the graph, and never claims git kept what was never committed.", + "The proposal schema still scopes Specs only: a finding that changes no Spec takes no invented Spec scope, and a review that proposes no Spec edit needs no empty proposal. An accepted change is an ordinary carrier edit, and whether a change was accepted stays in the pull request's discussion and git, never in the proposal or the graph.", + "Tests and recipes carry repeatable evidence. A preview is the current human view of a branch, never an immutable acceptance record, so the description names the commit it measured and the commands that re-derive each number.", + "The plan and review files stay as they are until a dedicated cleanup arc removes or archives them and updates the historical checks and links that read them; until then nothing is added to either, and a file in either changes only to correct the record it keeps. This decision sets no record convention for an adopter.", + ], + }, + }, + deliveryFacts: [], + file: "specs/decisions/roadmap-over-plans.sdp.md", + }, + { + id: "spec:decisions.epics-of-any-kind", + specKind: "decision", + altitude: "feature", + readiness: "ready", + title: "Capability epics keep kind independent of altitude", + narrative: null, + sections: { + intent: { + outcome: + "Decide whether a capability map admits epics of every kind or only behavior epics.", + }, + decision: { + context: + "The glossary row for capability and domain names a Capability Map over high-altitude behavior Specs, while kind and altitude are independent descriptors on the one primitive, and the hubs that already state the Protocol's capabilities include a domain model, a projection model and the rule that separates the two check families.", + decision: + "Any Spec stated at epic altitude is an epic, whatever its kind, and keeps its kind: the capability map reads every epic and its refinement descendants, and the glossary row broadens from high-altitude behavior Specs to epic Specs of every kind.", + rationale: [ + "Hard to reverse: corpora organize their capabilities around which Specs may be epics, and the capability map, its page and the upgrade guide read that choice. Surprising without context: the ratified glossary row limited the map to behavior Specs, so a reader needs to know why a model or a rule now heads a capability. A real trade-off: an epic of any kind keeps the Spec that already owns a capability's outcome as its head, as the one-primitive decision's named coordinates already imply, against a map whose roots differ in kind and a glossary row that changes.", + ], + alternatives: [ + "Keep behavior-only capability epics and introduce behavior wrappers over the model and rule hubs: each wrapper would restate an outcome another Spec already owns, a second owner that drifts, only so that a view can read it.", + ], + consequences: [ + "The capability map draws epics of every kind, each labelled with its kind, and a Spec's kind never decides whether it heads a capability.", + "The glossary row for capability and domain names a Capability Map over epic Specs of every kind and their refinement descendants.", + ], + }, + }, + deliveryFacts: [], + file: "specs/decisions/epics-of-any-kind.sdp.md", + }, ] as const; diff --git a/test/self-hosting-oracle/declared-relations.ts b/test/self-hosting-oracle/declared-relations.ts index b7f2fbf32..2a6a8564b 100644 --- a/test/self-hosting-oracle/declared-relations.ts +++ b/test/self-hosting-oracle/declared-relations.ts @@ -192,13 +192,22 @@ export const expectedDeclaredRelations: readonly (readonly [ ["spec:protocol.self-hosting", "decidedBy", "spec:decisions.plain-language-references"], ["spec:protocol.structural-self-binding", "refines", "spec:protocol.self-hosting"], ["spec:model.structural-patterns", "refines", "spec:model.anchors"], - ["spec:extraction.derive-graph", "refines", "spec:protocol.self-hosting"], ["spec:extraction.derive-graph", "constrainedBy", "spec:extraction.determinism"], ["spec:extraction.derive-graph", "decidedBy", "spec:decisions.one-validation-path"], ["spec:extraction.delivery-facts", "refines", "spec:extraction.derive-graph"], ["spec:extraction.delivery-facts", "decidedBy", "spec:decisions.binding-not-liveness"], - ["spec:extraction.determinism", "refines", "spec:protocol.self-hosting"], - ["spec:extraction.build-pipeline", "refines", "spec:protocol.self-hosting"], + [ + "spec:extraction.determinism", + "refines", + "spec:extraction.derive-graph", + "deterministic output constrains the derivation capability", + ], + [ + "spec:extraction.build-pipeline", + "refines", + "spec:extraction.derive-graph", + "the build orchestrates derivation and publication of the one graph", + ], ["spec:extraction.build-pipeline", "dependsOn", "spec:extraction.derive-graph"], ["spec:extraction.build-pipeline.same-invocation", "refines", "spec:extraction.build-pipeline"], [ @@ -242,6 +251,12 @@ export const expectedDeclaredRelations: readonly (readonly [ ["declaredVersion"], ], ["spec:extraction.executable-contracts", "refines", "spec:extraction.build-pipeline"], + [ + "spec:extraction.executable-contracts", + "refines", + "spec:extraction.executable-examples", + "typed contracts carry the authored steps and example space into execution", + ], ["spec:extraction.executable-contracts", "decidedBy", "spec:decisions.point-per-example"], [ "spec:extraction.executable-contracts.concreteness-refusal", @@ -322,7 +337,12 @@ export const expectedDeclaredRelations: readonly (readonly [ null, ["slotName", "unusableSlot", "localDegradation"], ], - ["spec:validation.readiness-floor", "refines", "spec:protocol.self-hosting"], + [ + "spec:validation.readiness-floor", + "refines", + "spec:validation.two-check-families", + "the readiness floor checks honesty of the stated rung", + ], ["spec:validation.readiness-floor", "decidedBy", "spec:decisions.kind-conditional-floor"], ["spec:validation.readiness-floor", "decidedBy", "spec:decisions.carried-evidence"], [ @@ -390,7 +410,12 @@ export const expectedDeclaredRelations: readonly (readonly [ "verifies", "spec:validation.kind-evidence", ], - ["spec:validation.duplicate-ids", "refines", "spec:protocol.self-hosting"], + [ + "spec:validation.duplicate-ids", + "refines", + "spec:validation.two-check-families", + "duplicate identity is a conformance refusal", + ], [ "spec:validation.duplicate-ids", "dependsOn", @@ -399,7 +424,6 @@ export const expectedDeclaredRelations: readonly (readonly [ ], ["spec:validation.duplicate-ids.dual-carrier", "refines", "spec:validation.duplicate-ids"], ["spec:validation.duplicate-ids.dual-carrier", "verifies", "spec:validation.duplicate-ids"], - ["spec:validation.two-check-families", "refines", "spec:protocol.self-hosting"], ["spec:validation.two-check-families", "decidedBy", "spec:decisions.one-validation-path"], ["spec:validation.referential-integrity", "refines", "spec:validation.two-check-families"], [ @@ -519,7 +543,6 @@ export const expectedDeclaredRelations: readonly (readonly [ "verifies", "spec:validation.warn-level-signals", ], - ["spec:consumers.projections-model", "refines", "spec:protocol.self-hosting"], ["spec:consumers.mermaid-view", "refines", "spec:consumers.projections-model"], [ "spec:consumers.projections-model", @@ -550,12 +573,6 @@ export const expectedDeclaredRelations: readonly (readonly [ ], ["spec:consumers.agent-surface.authoring-recipes", "refines", "spec:consumers.agent-surface"], ["spec:consumers.intent-composition", "refines", "spec:consumers.edit-model"], - [ - "spec:model.core-model", - "refines", - "spec:protocol.self-hosting", - "the self-hosted corpus models itself with this primitive", - ], ["spec:model.enrichment-lifecycle", "refines", "spec:model.core-model"], ["spec:model.core-model", "decidedBy", "spec:decisions.one-primitive"], ["spec:model.core-model", "decidedBy", "spec:decisions.example-realization-posture"], @@ -980,7 +997,12 @@ export const expectedDeclaredRelations: readonly (readonly [ "dependsOn", "spec:decisions.binding-not-liveness", ], - ["spec:observation.runtime-overlay", "refines", "spec:protocol.self-hosting"], + [ + "spec:observation.runtime-overlay", + "refines", + "spec:extraction.derive-graph", + "an observed fact would be derived beside implemented and has-verifier", + ], ["spec:observation.runtime-overlay", "dependsOn", "spec:model.core-model"], ["spec:consumers.impact-graph", "refines", "spec:consumers.projections-model"], ["spec:consumers.impact-graph", "dependsOn", "spec:extraction.derive-graph"], @@ -1615,11 +1637,23 @@ export const expectedDeclaredRelations: readonly (readonly [ "dependsOn", "spec:carrier.slot-notation", ], + [ + "spec:consumers.engine-provenance", + "refines", + "spec:consumers.command-line", + "the version flag is one more answer the command line gives an adopter", + ], ["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", + "refines", + "spec:model.core-model", + "planned components enrich Specs before code binds them", + ], [ "spec:model.planned-architecture", "dependsOn", @@ -1685,14 +1719,32 @@ export const expectedDeclaredRelations: readonly (readonly [ "spec:model.relations", "its supersedes term keeps a replaced record only while a reader still needs its text, so the edges the signal reads are records a corpus chose to keep", ], + [ + "spec:validation.architecture-constraints", + "refines", + "spec:validation.corpus-rules", + "the architecture walk is a corpus-selected rule over anchored structure", + ], ["spec:validation.architecture-constraints", "dependsOn", "spec:model.anchors"], [ "spec:validation.architecture-constraints", "decidedBy", "spec:decisions.architectural-annotation", ], + [ + "spec:validation.corpus-rules", + "refines", + "spec:consumers.agent-surface", + "a corpus scripts the graph through the evaluation sink to report its own findings", + ], ["spec:validation.corpus-rules", "dependsOn", "spec:consumers.agent-surface"], ["spec:validation.corpus-rules", "constrainedBy", "spec:validation.two-check-families"], + [ + "spec:observation.run-evidence", + "refines", + "spec:consumers.projections-model", + "run records inform a view without becoming graph facts", + ], [ "spec:observation.run-evidence", "dependsOn", @@ -1701,6 +1753,12 @@ export const expectedDeclaredRelations: readonly (readonly [ ], ["spec:observation.run-evidence", "decidedBy", "spec:decisions.binding-not-liveness"], ["spec:consumers.projections-model.roadmap-views", "refines", "spec:consumers.projections-model"], + [ + "spec:consumers.projections-model.roadmap-views", + "dependsOn", + "spec:consumers.roadmap", + "the roadmap view the owner asked for is the roadmap reading, its page and its memo", + ], ["spec:consumers.intent-composition.proposal", "dependsOn", "spec:consumers.design-diff"], ["spec:consumers.spec-studio.sharing", "dependsOn", "spec:consumers.engine-provenance"], ["spec:validation.architecture-constraints", "dependsOn", "spec:validation.corpus-rules"], @@ -2349,6 +2407,12 @@ export const expectedDeclaredRelations: readonly (readonly [ "the anchors Spec owns binding eligibility and structural endpoint restrictions", ], ["spec:extraction.derive-graph", "decidedBy", "spec:decisions.anchor-binding-grain"], + [ + "spec:extraction.example-runner", + "refines", + "spec:extraction.executable-examples", + "step dispatch and failure rendering execute a bound example", + ], [ "spec:extraction.example-runner", "dependsOn", @@ -3061,6 +3125,12 @@ export const expectedDeclaredRelations: readonly (readonly [ null, ["cycleFindings", "answersUnchanged"], ], + [ + "spec:validation.external-registers", + "refines", + "spec:validation.corpus-rules", + "external-register checks run as corpus-selected rules", + ], [ "spec:validation.external-registers", "dependsOn", @@ -3297,6 +3367,18 @@ export const expectedDeclaredRelations: readonly (readonly [ "spec:consumers.edit-model", "a carrier changes only through an agent's or a person's edit, recorded by git", ], + [ + "spec:consumers.upgrade-kit", + "dependsOn", + "spec:consumers.capability-map", + "the guide teaches epic organization through the capability map", + ], + [ + "spec:consumers.upgrade-kit", + "dependsOn", + "spec:consumers.roadmap", + "the guide teaches advisory priority and the roadmap memo as planning records", + ], ["spec:consumers.upgrade-kit.capability-adoption", "refines", "spec:consumers.upgrade-kit"], [ "spec:consumers.upgrade-kit.capability-adoption", @@ -3486,7 +3568,7 @@ export const expectedDeclaredRelations: readonly (readonly [ "spec:consumers.spec-studio.spec-page", "dependsOn", "spec:consumers.spec-studio.diagrams", - "the design view and the impact panel draw the realization diagram and the trace graph", + "the design view and impact panel draw requirement traceability and the one-hop trace graph", ], [ "spec:consumers.spec-studio.spec-page", @@ -4102,4 +4184,692 @@ export const expectedDeclaredRelations: readonly (readonly [ null, ["terms", "sameWordTwice"], ], + [ + "spec:protocol.significant-units-review", + "refines", + "spec:protocol.structural-self-binding", + "states what the independently reviewed accepted set must contain and exclude", + ], + ["spec:protocol.significant-units-review", "dependsOn", "spec:model.structural-patterns"], + ["spec:protocol.significant-units-review", "dependsOn", "spec:protocol.role-vocabulary"], + ["spec:protocol.significant-units-review", "decidedBy", "spec:decisions.anchor-binding-grain"], + [ + "spec:protocol.significant-units-review", + "decidedBy", + "spec:decisions.architectural-annotation", + ], + [ + "spec:protocol.role-vocabulary", + "refines", + "spec:protocol.structural-self-binding", + "names what each role value the engine's significant units carry means", + ], + ["spec:protocol.role-vocabulary", "decidedBy", "spec:decisions.architectural-annotation"], + [ + "spec:consumers.spec-studio.lenses.architecture.unit-register", + "refines", + "spec:consumers.spec-studio.lenses.architecture", + ], + [ + "spec:consumers.capability-map", + "refines", + "spec:consumers.projections-model", + "the capability map projects authored refinement without introducing a grouping", + ], + ["spec:consumers.capability-map", "decidedBy", "spec:decisions.epics-of-any-kind"], + [ + "spec:consumers.capability-map", + "dependsOn", + "spec:validation.readiness-floor", + "each node reads the existing floor beside the author's statement", + ], + [ + "spec:consumers.capability-map", + "dependsOn", + "spec:extraction.delivery-facts", + "rollups count direct bindings without conferring facts on ancestors", + ], + [ + "spec:decisions.epics-of-any-kind", + "refines", + "spec:consumers.capability-map", + "settles which Specs the capability map reads as epics", + ], + [ + "spec:decisions.epics-of-any-kind", + "dependsOn", + "spec:decisions.one-primitive", + "kind and altitude are independent named coordinates on the one primitive", + ], + ["spec:protocol.capability-epics", "refines", "spec:protocol.self-hosting"], + ["spec:protocol.capability-epics", "decidedBy", "spec:decisions.epics-of-any-kind"], + [ + "spec:protocol.capability-epics", + "dependsOn", + "spec:consumers.capability-map", + "the capability map exposes the corpus's authored refinement", + ], + [ + "spec:protocol.capability-epics", + "dependsOn", + "spec:validation.readiness-floor", + "reparenting preserves the floor of each existing ready child", + ], + [ + "spec:extraction.executable-examples", + "dependsOn", + "spec:extraction.derive-graph", + "executable contracts read the one graph rather than another carrier path", + ], + [ + "spec:consumers.spec-studio.capability-map-page", + "refines", + "spec:consumers.spec-studio.lenses", + "the Capabilities page is a lens that reads refinement", + ], + [ + "spec:consumers.spec-studio.capability-map-page", + "refines", + "spec:consumers.capability-map", + "the page is the capability map's reading for a person", + ], + [ + "spec:consumers.spec-studio.capability-map-page", + "dependsOn", + "spec:consumers.spec-studio.index-page", + "the Epic filter extends the index's Specs filters", + ], + [ + "spec:consumers.spec-studio.capability-map-page", + "dependsOn", + "spec:consumers.binding-language-views", + "aggregate rows use the same binding labels as Spec pages", + ], + [ + "spec:consumers.spec-studio.capability-map-page", + "dependsOn", + "spec:consumers.capability-map", + "the page and index filter render the capability map's answer", + ], + [ + "spec:consumers.spec-studio.capability-map-page", + "dependsOn", + "spec:consumers.spec-studio.diagram-kinds", + "the refinement drawing shares the Studio's diagram layout and accessible text contract", + ], + ["spec:consumers.spec-studio.diagram-kinds", "refines", "spec:consumers.spec-studio.diagrams"], + [ + "spec:consumers.spec-studio.diagram-kinds", + "decidedBy", + "spec:decisions.studio-diagram-renderer", + "every new kind is laid out by the engine at build time as the renderer decision rules", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.legibility", + "refines", + "spec:consumers.spec-studio.diagrams", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.legibility", + "refines", + "spec:consumers.spec-studio.diagram-kinds", + "readable labels are part of the outcome the diagram kinds state", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.legibility", + "dependsOn", + "spec:consumers.spec-studio.responsive", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.edge-routing", + "refines", + "spec:consumers.spec-studio.diagrams", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.edge-routing", + "refines", + "spec:consumers.spec-studio.diagram-kinds", + "arrows that cover no label serve the readable drawings the diagram kinds state", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.mermaid-source", + "refines", + "spec:consumers.spec-studio.diagram-kinds", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.mermaid-source", + "dependsOn", + "spec:consumers.spec-studio.page-elements", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.mermaid-source", + "decidedBy", + "spec:decisions.studio-diagram-renderer", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.mermaid-source", + "decidedBy", + "spec:decisions.shipped-projections-frozen", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.dependency", + "refines", + "spec:consumers.spec-studio.diagram-kinds", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.dependency", + "dependsOn", + "spec:consumers.design-slice", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.capability-tree", + "refines", + "spec:consumers.spec-studio.diagram-kinds", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.capability-tree", + "dependsOn", + "spec:consumers.capability-map", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + "refines", + "spec:consumers.spec-studio.diagram-kinds", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + "dependsOn", + "spec:consumers.design-slice", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + "dependsOn", + "spec:model.planned-architecture", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + "dependsOn", + "spec:consumers.spec-studio.lenses.architecture", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.maturity-picture", + "refines", + "spec:consumers.spec-studio.diagram-kinds", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + "refines", + "spec:consumers.spec-studio.diagram-kinds", + ], + [ + "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + "dependsOn", + "spec:consumers.spec-studio.data", + ], + [ + "spec:consumers.design-slice", + "refines", + "spec:consumers.pack-design", + "a slice reads a Pack's design as one whole, with its architecture and open decisions beside it", + ], + [ + "spec:consumers.design-slice", + "dependsOn", + "spec:consumers.review-perspectives", + "a slice read for one concern takes the members a perspective selects", + ], + [ + "spec:consumers.design-slice", + "dependsOn", + "spec:consumers.context-bundle", + "a slice takes its scope as the bundle does, and builds a Spec list's boundary the same way", + ], + [ + "spec:consumers.design-slice", + "dependsOn", + "spec:consumers.readiness-planner", + "members rest on each other as the statement queue reads the four typed relations, and a cycle reports in its shape", + ], + [ + "spec:consumers.design-slice", + "dependsOn", + "spec:model.planned-architecture", + "planned components and differing designs come from the realization comparison", + ], + [ + "spec:consumers.design-slice", + "dependsOn", + "spec:model.rule-keys", + "each member's rule count is the count rule coverage reports", + ], + [ + "spec:consumers.design-slice", + "dependsOn", + "spec:consumers.agent-surface.design-recipes", + "decisions in scope read the decision register's rows", + ], + [ + "spec:consumers.design-slice", + "dependsOn", + "spec:model.open-question-fields", + "each question in scope carries its fields", + ], + [ + "spec:consumers.design-slice", + "decidedBy", + "spec:decisions.anchor-binding-grain", + "the code a design builds on is reached through the design's own relations, never through a reference from that code", + ], + [ + "spec:consumers.design-slice", + "decidedBy", + "spec:decisions.architectural-annotation", + "a touched component carries the layer and context its anchor states", + ], + ["spec:consumers.spec-studio.slice-page", "refines", "spec:consumers.spec-studio"], + [ + "spec:consumers.spec-studio.slice-page", + "refines", + "spec:consumers.design-slice", + "the page is the design slice's reading for a person", + ], + [ + "spec:consumers.spec-studio.slice-page", + "dependsOn", + "spec:consumers.design-slice", + "the page renders the slice's answer", + ], + [ + "spec:consumers.spec-studio.slice-page", + "dependsOn", + "spec:consumers.spec-studio.data.recipe-answers", + "the build evaluates the slice recipe once for each Pack, and the page reads that answer", + ], + [ + "spec:consumers.spec-studio.slice-page", + "dependsOn", + "spec:consumers.spec-studio.pack-page", + "each member links to its card on the Pack page, whose element id is the member's id", + ], + [ + "spec:consumers.spec-studio.slice-page", + "dependsOn", + "spec:consumers.spec-studio.diagram-kinds.dependency", + "the dependency drawing given the slice's order is a drawing kind, which owns layout, folding and the text equivalent", + ], + [ + "spec:consumers.spec-studio.slice-page", + "dependsOn", + "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + "the scoped architecture drawing is a drawing kind, which owns layout, folding and the text equivalent", + ], + [ + "spec:consumers.spec-studio.slice-page", + "dependsOn", + "spec:consumers.spec-studio.diagrams", + "both drawings keep the diagram contract, bound and text equivalent included", + ], + ["spec:consumers.spec-studio.slice-page", "dependsOn", "spec:consumers.derived-readiness-banner"], + ["spec:consumers.spec-studio.slice-page", "dependsOn", "spec:consumers.binding-language-views"], + ["spec:consumers.spec-studio.slice-page", "dependsOn", "spec:model.open-question-fields"], + [ + "spec:decisions.roadmap-priority", + "refines", + "spec:decisions.planning-truths-placement", + "narrows the refusal of recorded work order for one case, the advisory priority of a roadmap", + ], + [ + "spec:decisions.roadmap-priority", + "refines", + "spec:decisions.pack-markdown-carrier", + "opens the closed Pack manifest envelope to one optional field, `order`", + ], + [ + "spec:consumers.roadmap", + "refines", + "spec:consumers.graph-first-planning", + "a roadmap is advisory reading of the graph for choosing the next work", + ], + [ + "spec:consumers.roadmap", + "dependsOn", + "spec:validation.next-rung-floor", + "an entry's stated next rung and the clauses that hold it come from the one floor evaluator", + ], + [ + "spec:consumers.roadmap", + "dependsOn", + "spec:validation.typed-dependency-floor", + "what an entry rests on is read through the relations the ready floor reads", + ], + [ + "spec:consumers.roadmap", + "dependsOn", + "spec:model.open-question-fields", + "the questions waiting on a person are selected by their decider field", + ], + [ + "spec:consumers.roadmap", + "dependsOn", + "spec:consumers.readiness-planner", + "the statements waiting on a person follow the statement queue's order", + ], + ["spec:consumers.roadmap", "decidedBy", "spec:decisions.roadmap-priority"], + ["spec:consumers.spec-studio.roadmap-page", "refines", "spec:consumers.spec-studio"], + [ + "spec:consumers.spec-studio.roadmap-page", + "refines", + "spec:consumers.roadmap", + "the page is the roadmap's reading for a person", + ], + ["spec:consumers.spec-studio.roadmap-page", "dependsOn", "spec:consumers.spec-studio.data"], + ["spec:consumers.spec-studio.roadmap-page", "dependsOn", "spec:consumers.spec-studio.shell"], + [ + "spec:consumers.spec-studio.roadmap-page", + "dependsOn", + "spec:consumers.spec-studio.publisher", + "an entry's element id carries the name the page set gives its roadmap's Pack page", + ], + [ + "spec:consumers.spec-studio.roadmap-page", + "dependsOn", + "spec:consumers.roadmap", + "the page renders the roadmap reading", + ], + [ + "spec:consumers.spec-studio.roadmap-page", + "dependsOn", + "spec:consumers.roadmap.memo", + "the page carries the roadmap memo for a description's author to copy", + ], + ["spec:consumers.roadmap.memo", "refines", "spec:consumers.roadmap"], + [ + "spec:consumers.roadmap.memo", + "dependsOn", + "spec:consumers.intent-composition.proposal.memo", + "the memo writes its code spans by the proposal memo's markup rules", + ], + [ + "spec:consumers.roadmap.memo", + "dependsOn", + "spec:consumers.agent-surface.register-recipes", + "the deferred items come from the open-question register", + ], + [ + "spec:decisions.roadmap-over-plans", + "refines", + "spec:decisions.planning-truths-placement", + "narrows what a plan file may still carry once a roadmap holds priority", + ], + [ + "spec:decisions.roadmap-over-plans", + "dependsOn", + "spec:decisions.roadmap-priority", + "plan files give up forward planning only once a roadmap holds priority in the graph", + ], + ["spec:protocol.roadmap-replaces-plans", "refines", "spec:protocol.self-hosting"], + [ + "spec:protocol.roadmap-replaces-plans", + "dependsOn", + "spec:consumers.roadmap", + "the Protocol reads its own next work from its roadmap", + ], + [ + "spec:protocol.roadmap-replaces-plans", + "dependsOn", + "spec:consumers.roadmap.memo", + "each description's forward section is the roadmap memo", + ], + [ + "spec:protocol.roadmap-replaces-plans", + "dependsOn", + "spec:consumers.intent-composition.proposal", + "a review's findings against Specs travel as a checked change proposal", + ], + [ + "spec:protocol.roadmap-replaces-plans", + "dependsOn", + "spec:consumers.intent-composition.proposal.memo", + "the pull request carries a review's proposal as its memo", + ], + [ + "spec:protocol.roadmap-replaces-plans", + "dependsOn", + "spec:observation.run-evidence", + "an arc's acceptance reads run records as evidence from test files at their recorded commit", + ], + ["spec:protocol.roadmap-replaces-plans", "decidedBy", "spec:decisions.roadmap-over-plans"], + [ + "spec:protocol.delivery-record-cleanup", + "refines", + "spec:protocol.roadmap-replaces-plans", + "carries out the removal the roadmap and the pull request record made possible for frozen history", + ], + ["spec:protocol.delivery-record-cleanup", "decidedBy", "spec:decisions.roadmap-over-plans"], + ["spec:consumers.spec-studio.cited-line-text", "refines", "spec:consumers.spec-studio"], + [ + "spec:consumers.spec-studio.cited-line-text", + "dependsOn", + "spec:extraction.entry-citations", + "the words belong to a citation the graph already records by file and line", + ], + [ + "spec:consumers.spec-studio.cited-line-text", + "dependsOn", + "spec:consumers.spec-studio.registers", + "the declarations page lists each citing line beside its declaration", + ], + ["spec:consumers.spec-studio.register-input", "refines", "spec:consumers.spec-studio"], + [ + "spec:consumers.spec-studio.register-input", + "dependsOn", + "spec:validation.external-registers", + "the input is the register file and pattern the external-registers recipe already reads", + ], + [ + "spec:consumers.spec-studio.register-input", + "dependsOn", + "spec:consumers.spec-studio.data.inputs", + "the register joins the base graph and the run records as a named input", + ], + ["spec:carrier.markdown-pack-authoring", "decidedBy", "spec:decisions.roadmap-priority"], + ["spec:model.pack-aggregate", "decidedBy", "spec:decisions.roadmap-priority"], + ["spec:extraction.pack-member-order", "decidedBy", "spec:decisions.roadmap-priority"], + ["spec:consumers.design-diff", "decidedBy", "spec:decisions.roadmap-priority"], + ["spec:consumers.design-slice.outside-components", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.outside-components", + "verifies", + "spec:consumers.design-slice", + null, + ["outsideNeighbors"], + ], + ["spec:consumers.design-slice.chain", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.chain", + "verifies", + "spec:consumers.design-slice", + null, + ["depth", "scope", "memberColumns"], + ], + ["spec:consumers.design-slice.decision-backlink", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.decision-backlink", + "verifies", + "spec:consumers.design-slice", + null, + ["restsOn", "depth", "decisionsInScope"], + ], + ["spec:consumers.design-slice.dependency-cycle", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.dependency-cycle", + "verifies", + "spec:consumers.design-slice", + null, + ["depth", "orderAdvisory"], + ], + ["spec:consumers.design-slice.folded-example", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.folded-example", + "verifies", + "spec:consumers.design-slice", + null, + ["examplesFold", "memberColumns"], + ], + ["spec:consumers.design-slice.independent-example", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.independent-example", + "verifies", + "spec:consumers.design-slice", + null, + ["examplesFold", "boundary"], + ], + ["spec:consumers.design-slice.component-absent", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.component-absent", + "verifies", + "spec:consumers.design-slice", + null, + ["componentDesigns", "touchedComponents", "touchedUses", "outsideNeighbors"], + ], + ["spec:consumers.design-slice.component-unrelated", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.component-unrelated", + "verifies", + "spec:consumers.design-slice", + null, + ["componentDesigns", "touchedComponents", "touchedUses", "outsideNeighbors"], + ], + ["spec:consumers.design-slice.component-matching", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.component-matching", + "verifies", + "spec:consumers.design-slice", + null, + ["componentDesigns", "touchedComponents", "touchedUses", "outsideNeighbors"], + ], + ["spec:consumers.design-slice.component-differing", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.component-differing", + "verifies", + "spec:consumers.design-slice", + null, + ["componentDesigns", "touchedComponents", "touchedUses", "outsideNeighbors"], + ], + ["spec:consumers.design-slice.component-unreadable", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.component-unreadable", + "verifies", + "spec:consumers.design-slice", + null, + ["componentDesigns", "touchedComponents", "touchedUses", "outsideNeighbors"], + ], + ["spec:consumers.design-slice.outside-satisfied", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.outside-satisfied", + "verifies", + "spec:consumers.design-slice", + null, + ["touchedComponents", "outsideNeighbors", "notDownstream"], + ], + ["spec:consumers.design-slice.outside-unbound", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.outside-unbound", + "verifies", + "spec:consumers.design-slice", + null, + ["touchedComponents", "graphOnly"], + ], + ["spec:consumers.design-slice.outside-holding", "refines", "spec:consumers.design-slice"], + [ + "spec:consumers.design-slice.outside-holding", + "verifies", + "spec:consumers.design-slice", + null, + ["boundary", "questionsInScope"], + ], + ["spec:consumers.roadmap.built-work", "refines", "spec:consumers.roadmap"], + [ + "spec:consumers.roadmap.built-work", + "verifies", + "spec:consumers.roadmap", + null, + ["scope", "specNeed", "entryNeed", "workRows"], + ], + ["spec:consumers.roadmap.buildable-part", "refines", "spec:consumers.roadmap"], + [ + "spec:consumers.roadmap.buildable-part", + "verifies", + "spec:consumers.roadmap", + null, + ["scope", "specNeed", "entryNeed", "workRows"], + ], + ["spec:consumers.roadmap.statement-part", "refines", "spec:consumers.roadmap"], + [ + "spec:consumers.roadmap.statement-part", + "verifies", + "spec:consumers.roadmap", + null, + ["scope", "specNeed", "entryNeed", "workRows"], + ], + ["spec:consumers.roadmap.design-part", "refines", "spec:consumers.roadmap"], + [ + "spec:consumers.roadmap.design-part", + "verifies", + "spec:consumers.roadmap", + null, + ["scope", "specNeed", "entryNeed", "workRows"], + ], + ["spec:consumers.roadmap.drifting-part", "refines", "spec:consumers.roadmap"], + [ + "spec:consumers.roadmap.drifting-part", + "verifies", + "spec:consumers.roadmap", + null, + ["drift", "workRows"], + ], + ["spec:consumers.roadmap.missing-evidence", "refines", "spec:consumers.roadmap"], + [ + "spec:consumers.roadmap.missing-evidence", + "verifies", + "spec:consumers.roadmap", + null, + ["entryNeed", "entryShows", "workRows"], + ], + ["spec:consumers.roadmap.blocking-part", "refines", "spec:consumers.roadmap"], + [ + "spec:consumers.roadmap.blocking-part", + "verifies", + "spec:consumers.roadmap", + null, + ["entryNeed", "entryShows", "workRows"], + ], + ["spec:consumers.roadmap.outside-parent", "refines", "spec:consumers.roadmap"], + [ + "spec:consumers.roadmap.outside-parent", + "verifies", + "spec:consumers.roadmap", + null, + ["scope", "restsOn", "workRows"], + ], + ["spec:consumers.roadmap.statement-order", "refines", "spec:consumers.roadmap"], + [ + "spec:consumers.roadmap.statement-order", + "verifies", + "spec:consumers.roadmap", + null, + ["waiting", "earlier"], + ], + [ + "spec:consumers.spec-studio.pack-page.maturity-map", + "dependsOn", + "spec:consumers.spec-studio.diagram-kinds.maturity-picture", + "the map is drawn as the picture defines", + ], + [ + "spec:consumers.spec-studio.spec-page", + "dependsOn", + "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + "the Design view reads the selected set and claims the requirement drawing defines", + ], ] as const; diff --git a/test/self-hosting-oracle/extraction.ts b/test/self-hosting-oracle/extraction.ts index f900f9c89..8deba17df 100644 --- a/test/self-hosting-oracle/extraction.ts +++ b/test/self-hosting-oracle/extraction.ts @@ -6,7 +6,7 @@ export const extractionSpecs = [ { id: "spec:extraction.derive-graph", specKind: "behavior", - altitude: "feature", + altitude: "epic", readiness: "ready", file: "specs/extraction/derive-graph.sdp.md", title: "Carrier reification derives the one graph", @@ -312,7 +312,7 @@ export const extractionSpecs = [ '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.11.0"}'], + then: ['the payload declares the schema version {schemaVersion: "0.12.0"}'], }, ], }, @@ -322,6 +322,47 @@ export const extractionSpecs = [ }, deliveryFacts: ["has-verifier"], }, + { + id: "spec:extraction.executable-examples", + specKind: "behavior", + altitude: "epic", + readiness: "ready", + file: "specs/extraction/executable-examples.sdp.md", + title: "Bound examples execute their authored steps", + narrative: + "The capability connects authored intent to an example's execution. Contract generation and the runner own narrower parts of that outcome; neither alone owns it. A change to the authored example must reach its test through the generated contract.", + sections: { + intent: { + actor: "An author binding an example to a test of the intended behavior.", + problem: + "A separately handwritten test can drift from the Spec's own steps and expected outcome.", + outcome: + "Run an example against its authored intent through graph-derived contracts and report failures in the words of its steps.", + }, + behavior: { + rules: [ + "Execution reads the steps and bound values from contracts derived from the graph; it adds no second authored description of the example.", + "A bound test supplies the world and step handlers, and the runner executes the contract's steps in authored order. Contract generation and execution keep their separate responsibilities.", + "A failing step names the authored intent it exercised. An unspecified expected outcome remains explicit and cannot become a passing comparison by assumption.", + "A verifier binding says the example has a verifier. Execution results stay outside the graph and confer no readiness or delivery fact on this epic.", + ], + ruleKeys: { + authoredSteps: "rules[0]", + binding: "rules[1]", + failure: "rules[2]", + facts: "rules[3]", + }, + }, + verification: { + mode: "reviewed", + criteria: [ + "Trace a bound example from its graph steps through generated contracts to runner dispatch, then change an expected outcome and check that its test fails in the example's words.", + "Read the epic's own binding facts separately from the bindings on its contract-generation and runner descendants.", + ], + }, + }, + deliveryFacts: ["implemented"], + }, { id: "spec:extraction.executable-contracts", specKind: "behavior", @@ -849,7 +890,7 @@ export const extractionSpecs = [ behavior: { rules: [ "A Pack node carries `members`, the manifest's `specs` list in authored order, a repeat included where the carrier admits one. The `belongsTo` edges re-express the same list, one edge per entry, so the members of a Pack node and the sources of its `belongsTo` edges are one list read twice and never disagree in a derived graph.", - "`members` is always present on a Pack node, and an empty manifest list derives `members: []`. The serialized Pack node writes its fields in the order `id`, `nodeType`, `claim`, `title`, `framing`, `file`, `members`, `modelRefs`, omitting an absent optional field.", + "`members` is always present on a Pack node, and an empty manifest list derives `members: []`. The serialized Pack node writes its fields in the order `id`, `nodeType`, `claim`, `title`, `framing`, `file`, `members`, `modelRefs`, `order`, omitting an absent optional field.", "Nodes keep their sort by id and edges keep their sort by from, type, and to. Authored member order is content of the Pack node, as authored entry order is content of a Spec's open sections, so two derivations of one commit stay byte-identical and two manifests that list the same members in different orders derive different Pack nodes.", "The reader's Pack context lists one member row per entry of `members`, in that order, and its verifier gaps follow the same order. The Design Review's Pack page renders its member table and its verifier gap list in that order.", "A repeated member is kept only where its carrier admits it. TypeScript keeps repetitions in `members` and reader rows, and pack-coherence reports the duplicate `belongsTo` edge. Markdown refuses a repeated manifest member with `extract/invalid-frontmatter` and the message `duplicate entry in specs`.", diff --git a/test/self-hosting-oracle/index.ts b/test/self-hosting-oracle/index.ts index c2c559022..27bc053ac 100644 --- a/test/self-hosting-oracle/index.ts +++ b/test/self-hosting-oracle/index.ts @@ -50,14 +50,22 @@ export const expectedSpecs: readonly ExpectedSpec[] = specFamilies.flatMap( ); // No intended prose-mention pair is left without a declared relation. The owner retains the -// honesty gap on spec:model.spec-sections. The Studio gaps now have genuine bound verifiers. -export const expectedWarnings = [ - { +// honesty gap on spec:model.spec-sections. The owner's ready statements on plan 47's +// roadmaps-and-slices Pack add one gap for each newly ready non-decision Spec that no verifier +// binds yet; decisions take no gap. The validator sorts findings by carrier path, so the rows +// follow the carriers' paths in code-unit order. +const gap = (subjectId: string) => + ({ validatorId: "honesty/gaps", family: "honesty", severity: "warning", - subjectId: "spec:model.spec-sections", - }, + subjectId, + }) as const; + +export const expectedWarnings = [ + gap("spec:extraction.executable-examples"), + gap("spec:model.spec-sections"), + gap("spec:protocol.role-vocabulary"), ] as const; export type { ExpectedAnchor } from "./anchors.js"; @@ -70,6 +78,8 @@ export { expectedObsidianVaultPackMembers, expectedDesignManagementPackMembers, expectedPackMembers, + expectedRoadmapPackMembers, + expectedRoadmapsAndSlicesPackMembers, expectedStudioPackMembers, expectedPacks, } from "./pack-members.js"; diff --git a/test/self-hosting-oracle/model.ts b/test/self-hosting-oracle/model.ts index dbe81bcde..fbfb1a76d 100644 --- a/test/self-hosting-oracle/model.ts +++ b/test/self-hosting-oracle/model.ts @@ -6,7 +6,7 @@ export const modelSpecs = [ { id: "spec:model.core-model", specKind: "model", - altitude: "feature", + altitude: "epic", readiness: "ready", file: "specs/model/core-model.sdp.md", title: "The Protocol models delivery with one enrichable Spec", @@ -279,6 +279,8 @@ export const modelSpecs = [ "A declared manifest reference that derives a belongsTo edge; a Spec may belong to many Packs.", modelRefs: "References from a Pack to standalone model Specs that carry shared vocabulary.", + "priority order": + "The authored member order of a Pack whose manifest states `order: priority`, read as its owner's advisory preference among its members; it confers nothing and sequences nothing.", refinement: "A truth-bearing parent-child relation, distinct from the cross-cutting Pack aggregate.", }, @@ -439,7 +441,7 @@ export const modelSpecs = [ model: { terms: { "architecturally significant unit": - "A code unit with exported public surface or cross-component reach that warrants graph-visible structural binding: component membership, a role, and `references` to the designs it answers to. An architectural dependency is declared as `uses`, normally between components; a unit's own `uses` is optional.", + "A code unit with exported public surface or cross-component reach whose contract or responsibility a designer or builder must understand; export status or import rank alone is insufficient. Ports, shared data shapes, pipeline stages, decision tables, catalogs, layout policies and adapters may qualify. Its anchor records component membership, a role and `references` to the designs it answers to. An architectural dependency is declared as `uses`, normally between components; a unit's own `uses` is optional.", "structural attributes": "`role` on any code anchor; `layer` and `context` on a `component:` anchor; they describe the unit's architecture and carry no readiness or delivery fact, and the census renders their taxonomy from the graph.", "accepted set": diff --git a/test/self-hosting-oracle/pack-members.ts b/test/self-hosting-oracle/pack-members.ts index b228eab69..45206d6c1 100644 --- a/test/self-hosting-oracle/pack-members.ts +++ b/test/self-hosting-oracle/pack-members.ts @@ -106,6 +106,7 @@ export const expectedPackMembers = [ "spec:extraction.regenerability", "spec:extraction.schema-versioning", "spec:extraction.schema-versioning.declared-version", + "spec:extraction.executable-examples", "spec:extraction.executable-contracts", "spec:extraction.executable-contracts.concreteness-refusal", "spec:extraction.executable-contracts.multi-entry-example", @@ -277,6 +278,7 @@ export const expectedPackMembers = [ "spec:consumers.spec-studio.navigation", "spec:consumers.spec-studio.lenses.packs", "spec:consumers.spec-studio.lenses.architecture", + "spec:consumers.spec-studio.lenses.architecture.unit-register", "spec:consumers.spec-studio.lenses.architecture.planned-apart", "spec:consumers.spec-studio.lenses.tests", "spec:consumers.spec-studio.lenses.tests.coverage-row", @@ -357,12 +359,58 @@ export const expectedPackMembers = [ "spec:consumers.command-line.generate-check-failure", "spec:consumers.command-line.generate-writes-every-root", "spec:consumers.command-line.verb-help-touches-nothing", + "spec:consumers.capability-map", + "spec:consumers.spec-studio.capability-map-page", + "spec:consumers.spec-studio.diagram-kinds", + "spec:consumers.spec-studio.diagram-kinds.legibility", + "spec:consumers.spec-studio.diagram-kinds.edge-routing", + "spec:consumers.spec-studio.diagram-kinds.mermaid-source", + "spec:consumers.spec-studio.diagram-kinds.dependency", + "spec:consumers.spec-studio.diagram-kinds.capability-tree", + "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + "spec:consumers.spec-studio.diagram-kinds.maturity-picture", + "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + "spec:consumers.design-slice", + "spec:consumers.design-slice.outside-components", + "spec:consumers.design-slice.chain", + "spec:consumers.design-slice.decision-backlink", + "spec:consumers.design-slice.dependency-cycle", + "spec:consumers.design-slice.folded-example", + "spec:consumers.design-slice.independent-example", + "spec:consumers.design-slice.component-absent", + "spec:consumers.design-slice.component-unrelated", + "spec:consumers.design-slice.component-matching", + "spec:consumers.design-slice.component-differing", + "spec:consumers.design-slice.component-unreadable", + "spec:consumers.design-slice.outside-satisfied", + "spec:consumers.design-slice.outside-unbound", + "spec:consumers.design-slice.outside-holding", + "spec:consumers.spec-studio.slice-page", + "spec:consumers.roadmap", + "spec:consumers.roadmap.built-work", + "spec:consumers.roadmap.buildable-part", + "spec:consumers.roadmap.statement-part", + "spec:consumers.roadmap.design-part", + "spec:consumers.roadmap.drifting-part", + "spec:consumers.roadmap.missing-evidence", + "spec:consumers.roadmap.blocking-part", + "spec:consumers.roadmap.outside-parent", + "spec:consumers.roadmap.statement-order", + "spec:consumers.spec-studio.roadmap-page", + "spec:consumers.roadmap.memo", + "spec:consumers.spec-studio.cited-line-text", + "spec:consumers.spec-studio.register-input", "spec:observation.runtime-overlay", "spec:observation.run-evidence", "spec:observation.run-evidence.absolute-path-joins", "spec:observation.run-evidence.unanchored-file-unjoined", "spec:protocol.self-hosting", "spec:protocol.structural-self-binding", + "spec:protocol.significant-units-review", + "spec:protocol.role-vocabulary", + "spec:protocol.capability-epics", + "spec:protocol.roadmap-replaces-plans", + "spec:protocol.delivery-record-cleanup", "spec:decisions.plain-language-references", "spec:decisions.concept-docs-dissolve", "spec:decisions.one-validation-path", @@ -407,6 +455,9 @@ export const expectedPackMembers = [ "spec:decisions.architectural-annotation", "spec:decisions.pack-design-page", "spec:decisions.vault-owned-folder", + "spec:decisions.roadmap-priority", + "spec:decisions.roadmap-over-plans", + "spec:decisions.epics-of-any-kind", ] as const; export const expectedStudioPackMembers = [ @@ -427,6 +478,7 @@ export const expectedStudioPackMembers = [ "spec:consumers.spec-studio.lenses.packs", "spec:consumers.spec-studio.index-page", "spec:consumers.spec-studio.lenses.architecture", + "spec:consumers.spec-studio.lenses.architecture.unit-register", "spec:consumers.spec-studio.lenses.architecture.planned-apart", "spec:consumers.spec-studio.lenses.tests", "spec:consumers.spec-studio.lenses.tests.coverage-row", @@ -545,6 +597,67 @@ export const expectedObsidianVaultPackMembers = [ "spec:model.term-addresses", ] as const; +// The authored membership of `pack:roadmaps-and-slices-v1`, in manifest order. +export const expectedRoadmapsAndSlicesPackMembers = [ + "spec:protocol.significant-units-review", + "spec:protocol.role-vocabulary", + "spec:consumers.spec-studio.lenses.architecture.unit-register", + "spec:consumers.capability-map", + "spec:decisions.epics-of-any-kind", + "spec:protocol.capability-epics", + "spec:extraction.executable-examples", + "spec:consumers.spec-studio.capability-map-page", + "spec:consumers.spec-studio.diagram-kinds", + "spec:consumers.spec-studio.diagram-kinds.legibility", + "spec:consumers.spec-studio.diagram-kinds.edge-routing", + "spec:consumers.spec-studio.diagram-kinds.mermaid-source", + "spec:consumers.spec-studio.diagram-kinds.dependency", + "spec:consumers.spec-studio.diagram-kinds.capability-tree", + "spec:consumers.spec-studio.diagram-kinds.scoped-architecture", + "spec:consumers.spec-studio.diagram-kinds.maturity-picture", + "spec:consumers.spec-studio.diagram-kinds.requirement-trace", + "spec:consumers.design-slice", + "spec:consumers.design-slice.outside-components", + "spec:consumers.design-slice.chain", + "spec:consumers.design-slice.decision-backlink", + "spec:consumers.design-slice.dependency-cycle", + "spec:consumers.design-slice.folded-example", + "spec:consumers.design-slice.independent-example", + "spec:consumers.design-slice.component-absent", + "spec:consumers.design-slice.component-unrelated", + "spec:consumers.design-slice.component-matching", + "spec:consumers.design-slice.component-differing", + "spec:consumers.design-slice.component-unreadable", + "spec:consumers.design-slice.outside-satisfied", + "spec:consumers.design-slice.outside-unbound", + "spec:consumers.design-slice.outside-holding", + "spec:consumers.spec-studio.slice-page", + "spec:decisions.roadmap-priority", + "spec:consumers.roadmap", + "spec:consumers.roadmap.built-work", + "spec:consumers.roadmap.buildable-part", + "spec:consumers.roadmap.statement-part", + "spec:consumers.roadmap.design-part", + "spec:consumers.roadmap.drifting-part", + "spec:consumers.roadmap.missing-evidence", + "spec:consumers.roadmap.blocking-part", + "spec:consumers.roadmap.outside-parent", + "spec:consumers.roadmap.statement-order", + "spec:consumers.spec-studio.roadmap-page", + "spec:consumers.roadmap.memo", + "spec:decisions.roadmap-over-plans", + "spec:protocol.roadmap-replaces-plans", + "spec:consumers.projections-model.roadmap-views", +] as const; + +// The authored membership of `pack:roadmap`, in manifest order: the Protocol's proposed priority. +export const expectedRoadmapPackMembers = [ + "spec:protocol.delivery-record-cleanup", + "spec:consumers.spec-studio.cited-line-text", + "spec:consumers.spec-studio.register-input", + "spec:consumers.edit-model", +] as const; + export const expectedCorpusChecksPackMembers = [ "spec:validation.corpus-rules", "spec:validation.corpus-rules.error-finding-fails", @@ -690,6 +803,27 @@ export const expectedPacks = [ file: "specs/obsidian-vault.pack.sdp.md", members: expectedObsidianVaultPackMembers, }, + { + id: "pack:roadmap", + nodeType: "Pack", + claim: "declared", + title: "The Protocol's roadmap", + framing: + "The Protocol's next work, first entry first. The order is a preference: it dates, gates and sequences nothing, and what each entry still needs is read from the graph each time.\n\nFirst the cleanup that removes the frozen plans, review reports and superseded concept material, as the roadmap and the pull request record make possible. Then the two steps a reader of the Studio's pages could not take, the words of a test line that checks a declaration and an outside register's entries, and then the composed-intent model the build backlog holds. What the first adopter's mass update asks for joins when the owner selects it; a capture landing adds nothing here. An entry leaves by an edit to this manifest once its selected work is complete or no longer belongs among the next work.", + file: "specs/roadmap.pack.sdp.md", + members: expectedRoadmapPackMembers, + order: "priority", + }, + { + id: "pack:roadmaps-and-slices-v1", + nodeType: "Pack", + claim: "declared", + title: "Roadmaps, capability maps and design slices", + framing: + "How a corpus reads where it stands and what comes next, drawn from the graph. First the architecture the views stand on: a review of which engine units are significant enough to anchor, and the meaning of each role they carry. Then the capability map, epics with their features and stories, and the Protocol's own capabilities stated as epics. Then the diagrams the graph has data for, and the design slice, a Pack read as one design with the architecture it touches. Last the roadmap: the decision that lets the graph hold an advisory priority, the roadmap reading and its page and memo, and the decision that lets the roadmap replace the Protocol's numbered plans.", + file: "specs/roadmaps-and-slices.pack.sdp.md", + members: expectedRoadmapsAndSlicesPackMembers, + }, { id: "pack:self-hosting-v1", nodeType: "Pack", diff --git a/test/self-hosting-oracle/protocol.ts b/test/self-hosting-oracle/protocol.ts index c9e1153e2..c2e59f75b 100644 --- a/test/self-hosting-oracle/protocol.ts +++ b/test/self-hosting-oracle/protocol.ts @@ -41,7 +41,7 @@ export const protocolSpecs = [ }, behavior: { rules: [ - "The engine applies the structural-patterns model's significance criterion: exported public surface or cross-component reach.", + "The engine applies the structural-patterns model's significance criterion: exported public surface or cross-component reach whose contract or responsibility a designer or builder must understand; export status or import rank alone is insufficient.", "Every architecturally significant engine unit carries component membership through a realizing anchor; a helper with no realization target of its own is covered through the nearest realization anchor that consumes it.", "An architectural dependency of the engine is declared as `uses`, normally from one engine component to another; a unit's own `uses` is optional.", "A component-level `uses` declaration tracks real value or type imports from another component's source files; imports made only to author anchors through the stable-id or anchor-builder modules confer no edge.", @@ -64,4 +64,277 @@ export const protocolSpecs = [ }, deliveryFacts: ["implemented", "has-verifier"], }, + { + id: "spec:protocol.significant-units-review", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + file: "specs/protocol/significant-units-review.sdp.md", + title: "The engine's accepted set covers its significant abstractions and only those", + narrative: + "A designer needs to find the contracts, decisions and stages a change must preserve. The accepted set records those units on the code that owns them. Exporting a helper, appearing in an import ranking or giving a file an impact answer does not by itself earn an identity.", + sections: { + intent: { + outcome: + "Keep the accepted set focused on the engine's significant contracts, decisions and stages, with meaningful roles and architectural uses, and exclude units anchored only for coverage.", + }, + behavior: { + rules: [ + "Apply the structural-patterns criterion to the public contract or cross-component responsibility a unit owns. Ports, shared data shapes, pipeline stages, decision tables, catalogs, layout policies and adapters qualify when changing them requires a designer or builder to understand that responsibility; being exported alone is insufficient.", + "Leave forwarding barrels, empty element registrations, mechanical wrappers, formatting helpers and generated files unanchored unless they own an independent contract or architectural responsibility. A coverage target, test expectation, file count or desired impact answer never establishes significance.", + "Declare a component uses for a value or type import the importing file itself makes from another component's file. An import made only to author anchors, and a pair the imports adapter reports only through a re-export the importer does not use, confer no edge. Before calling a declared edge stale, inspect input omissions and every file the comparison leaves unassigned. Import evidence proposes an edit and never authors an edge.", + "The self-hosting suite pins the reviewed component identities, unit ownership, roles, uses and deliberate membership exceptions independently of extraction. It compares the graph with those declarations and rejects unrostered membership. It never derives its expected roster from paths, exports, imports or the graph under test.", + "The accepted set is the annotation review's change table as the owner accepted it: its additions, merges, drop, role changes and component uses, each realization claim it names kept.", + "The example-space decoder joins the reader component, leaving notation a leaf codec, only when the architecture crossings recipe and the self-binding tests show that the move adds no layer crossing; otherwise it stays in notation, its real imports are declared, and the uses cycle they form stays lawful data.", + "Read role meanings from the engine's role-vocabulary Model when reviewing its anchors. The vocabulary guides authors and reviewers; the Protocol never validates membership in it.", + ], + ruleKeys: { + significantAbstractions: "rules[0]", + curatedBoundary: "rules[1]", + dependencyEvidence: "rules[2]", + rosterOracle: "rules[3]", + acceptedChanges: "rules[4]", + decoderBoundary: "rules[5]", + roleMeaning: "rules[6]", + }, + }, + design: { + rosterSource: + "The self-hosting oracle holds the accepted roster: each row names an accepted unit's anchor, its component and its file, and its expectations for components, membership, uses, exceptions, coarse-grain coverage and roles sit beside it. A symbol suffix is a review locator, never extracted symbol identity.", + }, + verification: { + mode: "reviewed", + criteria: [ + "Compare the independently reviewed accepted roster with the source responsibilities it names, including dependencies that an imports comparison cannot assign to components.", + "Check that every proposed merge or drop preserves the whole-Spec realization set it claims to preserve and that every architectural uses pair has supporting value or type imports beyond anchor authoring.", + "Compare the extracted graph with the independently reviewed oracle roster after anchor changes; exercise a missing accepted member and an extra unrostered membership to show that the roster comparison detects both.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:protocol.role-vocabulary", + specKind: "model", + altitude: "feature", + readiness: "ready", + file: "specs/protocol/role-vocabulary.sdp.md", + title: "The engine's architectural roles each name one pattern", + narrative: + "Authors choose a role by the architectural responsibility a unit owns. The same responsibility uses the same word across the engine. These definitions are this corpus's design vocabulary, not a Protocol enum or a registry a validator checks. An adopter may use other roles for its own architecture.", + sections: { + intent: { + outcome: + "Define each architectural role the engine's anchors use, so the accepted set of significant units reads as a curated register of the engine's patterns and abstractions.", + }, + model: { + terms: { + adapter: + "Connects an external tool or runtime to an engine contract, such as a test runner or a TypeScript import reader.", + codec: + "Parses, normalizes or encodes a representation while preserving the meaning owned by its contract, such as ids, carrier text, parameter input or source links.", + contract: + "Owns a shared interface, data shape, closed vocabulary or public boundary that independently developed callers and providers must agree on.", + decider: + "Computes a semantic answer from explicit inputs without reading or writing external state, such as delivery facts or example eligibility.", + extractor: + "Reads authored carriers or source bindings, or composes their reified results, into the graph and its located evidence.", + interaction: + "Adds browser controls and feedback to build-rendered content without owning its graph joins or layout. This is a corpus role for optional element behavior.", + projection: + "Derives read-only presentation or generated contract content from the graph, the reader or an explicitly supplied answer. A page fragment and a complete page use the same role when both own presentation.", + publisher: + "Writes or synchronizes an owned artifact set, including its path checks, replacement, recovery and clean comparison.", + index: + "Builds a disposable lookup structure over the graph for the units that query it; it holds no authored state and is never a second read model.", + reader: + "Answers queries over graph content, resolving or joining recorded identities and bindings without reading source files.", + service: + "Coordinates a use case or pipeline across collaborators, including execution and failure handling, while each collaborator owns its own contract.", + validator: + "Evaluates a stated contract and reports its failures, including readiness clauses, import fidelity and corpus-selected checks. A role does not determine a Protocol validator family.", + }, + termKeys: { + adapter: "adapter", + codec: "codec", + contract: "contract", + decider: "decider", + extractor: "extractor", + interaction: "interaction", + projection: "projection", + publisher: "publisher", + index: "index", + reader: "reader", + service: "service", + validator: "validator", + }, + }, + design: { + roleChoice: + "Use the dominant responsibility of the anchored unit. A catalog of shared shapes is a contract, a table computing a semantic answer is a decider, and a failure-producing check is a validator. File size, caller count and naming style never select a role.", + componentRole: + "A component anchor's role names the dominant architectural responsibility its members serve together, read from their reviewed responsibilities rather than a count of role tokens. Omit the role when no responsibility dominates.", + presentationWord: + "Use projection for pure page, fragment and text-equivalent rendering. Use publisher for filesystem publication, codec for representation encoding and interaction for optional DOM behavior; a unit that presents uses projection, never renderer.", + helperWord: + "Choose a specific responsibility after reading the code. Graph indexes use index, concept queries use reader, parameter and path encoders use codec, browser controls use interaction and test-runtime integration uses adapter.", + freeVocabulary: + "These Model terms explain the engine's role values. Token grammar remains the only Protocol check on role, omission remains lawful, and this Model is not an adopter-wide allowlist; the self-hosting suite pins each accepted engine unit's role.", + }, + verification: { + mode: "reviewed", + criteria: [ + "Compare the graph's role taxonomy with these meanings and review every renamed unit against its implementation; keep an unfamiliar adopter role visible without reporting a Protocol finding.", + "Check that pure presentation uses projection across the Markdown and Studio implementations, and that publication and optional browser interaction retain their distinct responsibilities.", + ], + }, + }, + deliveryFacts: ["implemented"], + }, + { + id: "spec:protocol.capability-epics", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + file: "specs/protocol/capability-epics.sdp.md", + title: "The Protocol organizes its own features in capability epics", + narrative: + "An epic states an enduring intended outcome. Where an existing hub owns an outcome, enlarging its altitude preserves its identity, kind, evidence and bindings. Self-hosting describes the Protocol using itself; it is not the parent of every capability offered to an adopter.", + sections: { + intent: { + outcome: + "Organize the Protocol's capabilities through explicit refinement so a reader can choose a capability and inspect its design and bindings.", + }, + behavior: { + rules: [ + "Enrich an existing hub to epic altitude when its intended outcome already names the capability. Keep its id, kind and truth; author a new epic only when no existing Spec owns that outcome. A model or rule can be an epic without becoming behavior merely to appear in the map.", + "Each feature belongs under the epic whose intended outcome it makes more precise, directly or through intermediate Specs. Existing genuine refinement remains, including a nested epic and multiple parents. A Pack, path or dependency alone does not establish that relationship.", + "The self-hosting epic keeps the Protocol's own use of the carrier, structural binding, roadmap and capability organization, and the decisions about that practice and the Protocol's identity. General model, derivation, validation and projection capabilities no longer refine self-hosting merely because the corpus exercises them.", + "A re-altituded hub keeps its readiness statement when its content still supports it; reparenting confers no delivery fact and adds no anchor.", + ], + ruleKeys: { + epicHome: "rules[0]", + refinement: "rules[1]", + selfHosting: "rules[2]", + preserveEvidence: "rules[3]", + }, + }, + design: { + epicHome: + "reuse an existing outcome owner so altitude changes do not duplicate intent; add an epic only for an outcome no hub already states. Design iteration has no epic: no Spec owns that outcome, its features are views and reader recipes that refine the projection and agent capabilities, and its Pack already groups them for review.", + carrierHome: + "the Markdown hub names the default authoring entry, while its rule puts Markdown and TypeScript on one graph path and its carrier ruling admits Gherkin. Its existing descendants refine that shared authoring outcome, so it keeps its identity and title.", + executableHome: + "the runner owns step dispatch and failure rendering, and contract generation owns typed contracts. Neither owns the whole outcome of executing an example against its authored intent, so a separate executable-examples epic joins those refinements. Their other genuine refinement and dependency relations remain.", + nestedEpic: + "the agent capability remains a refinement of the projection capability because scripted graph access is a consumer-specific view. Its descendants belong to both epic scopes, and the map reports that overlap explicitly.", + corpusChecks: + "corpus-selected rules refine the agent capability because their intended operation is scripting the graph; the validation command is their host, while their findings remain outside the Protocol's conformance and honesty families. A refinement states the narrower outcome and a dependency states what must hold: both relations intentionally connect the build pipeline to derivation, corpus rules to the agent capability, and architecture and external-register checks to corpus rules.", + }, + verification: { + mode: "reviewed", + criteria: [ + "Re-measure the corpus's refinement transitions, roots, overlapping parents and epic reach from the graph.", + "Every previously ready Spec still states ready and reaches the ready floor after the edits; the drift alarm stays empty.", + "Compare the capability map before and after authoring the epics, and read the same projection on an adopter with one broad epic without changing its intent.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:protocol.roadmap-replaces-plans", + specKind: "behavior", + altitude: "feature", + readiness: "ready", + file: "specs/protocol/roadmap-replaces-plans.sdp.md", + title: + "The Protocol reads its next work from its roadmap and keeps its delivery record in git and its pull requests", + narrative: + "Each arc's intent already lives in its capture Specs and Pack, its rulings in their carrying Specs, and its story in its pull request description. What a plan file still carries is the status line agents read first, the lanes and waves of the arc, and copies of ruling tables; what a committed review file carries, a change proposal, its memo and git already hold. Once a roadmap holds priority, the Protocol reads its next work from the graph, keeps each arc's record in git and its pull request, with review findings and evidence beside the story, and lets the plan and review files stand as the record of how earlier arcs were run. The Protocol keeps one living roadmap: its owner selects what it lists, and the pull requests that complete that work remove it.", + sections: { + intent: { + actor: + "An agent or a person opening, reviewing or resuming the Protocol's work, and the owner keeping its order.", + problem: + "The handbook points every session at the newest plan file and keeps a status line for each, and a gate requires both, so forward planning lives in prose beside the graph that already holds the arc's intent, and committed review files repeat what change proposals, git and the pull request already carry.", + outcome: + "Let an agent opening the Protocol's next work read it from the roadmap recipe, review and recover an arc from git and its pull request, and keep the plan and review files as frozen history no gate requires to grow.", + }, + behavior: { + rules: [ + "An agent opening the Protocol's next work runs the roadmap recipe over the roadmap Pack the handbook names and follows each entry to its Packs and carrying Specs; no plan file answers for forward work.", + "The Protocol keeps one living roadmap Pack, edited in place. Its owner selects captured work for the upcoming pull requests; landing a capture does not add it. Entries leave by an ordinary manifest edit when the selected work is complete or no longer belongs among the next work, and move on the owner's word, with git as history.", + "`plans/` and `reviews/` keep the plans and review records of the arcs that had them as history until a cleanup arc removes or archives them; no file is added to either, and a file in either changes only to correct the record it keeps.", + "Before an arc's first push its record is its branch, git being the event log: the first commit message states the owner's working rules for the arc and the arc's Pack, each later message says what its unit did, and outstanding work is read from the arc's Pack, the backlogs and the open-question register; no record file is kept under `reviews/` or anywhere else. The arc pushes and opens its draft pull request at its first coherent commit, normally the scaffold, so the window in which only local git holds the record stays short and a session restarted on another machine sees the work. The draft pull request description then holds the record: its branch and base, working rules, lanes, the rulings with who made them, review findings as change proposals and their memos, acceptance as measured, deferrals and refusals. A session resuming without its predecessor's context reads the branch, its commits, the draft pull request and the graph, and never claims git kept what was never committed. The authority to commit and push comes from the owner's instruction for the arc, never from this rule, and no check reads the record.", + "A review's findings against Specs travel as one change proposal in the shape `spec:consumers.intent-composition.proposal` states, whose base is the branch-head commit the reviewer read; a proposal composed in a preview names the preview's merge commit from its stamp; the proposal check runs it against the graph at that base, a moved, unresolved or incomparable scope is read again before its change applies, and its memo, as `spec:consumers.intent-composition.proposal.memo` renders it, is posted to the pull request. A finding against code or another file is a review comment on its line, and a review that proposes no Spec edit has no proposal. The commit that applies a proposal carries its reason line and base, and whether a change was accepted stays in the pull request's discussion and git, never in the proposal or the graph. No review file is committed, and a brief or prompt stays in the session that used it.", + "A finding left open against intended design becomes an open question on its carrying Spec with its `decider`, `provisional` reading and `links`, and a deferral is a blocking question with its re-entry `trigger`; a code defect against settled intent stays a code finding until it is repaired and never becomes an invented design question.", + "Evidence no test or recipe carries, such as a prototype of a view not yet built, an adopter measurement's raw output or a screenshot for the description, is committed on the arc's branch with the commands that made it, linked from the description at that commit, and removed before the merge; the Protocol merges its pull requests with merge commits, and GitHub keeps each pull request's head ref, so links at the evidence commit keep resolving. A squash or rebase merge would need the evidence published to a durable ref first.", + "An arc's acceptance links its bound examples and their run evidence, and re-derives each recipe measurement at the commit the description names, with its commands, never inheriting an earlier result. A run record is evidence from its test file as `spec:observation.run-evidence` reads it, never a verdict for one verifier, and it counts as evidence for a preview only when its recorded commit is the commit the preview's stamp names; a preview stays a current view, never an acceptance record.", + "Each description's forward section is the roadmap memo rendered at the branch's head, with the re-entry condition, the owner's reason for each entry's place and the refused list in the author's words around it; the pull request that completes an entry's selected work removes it from the roadmap in the same change, entries join or move only on the owner's word, and nothing checks either.", + "The handbook names the roadmap Pack and the roadmap recipe where it named the current plan, and keeps no status line for any plan.", + "The self-hosting gate's current-record check refuses a handbook that does not name the roadmap Pack or the roadmap recipe, and a generated graph without a Pack node `pack:roadmap` whose `order` is `priority`; it keeps the frozen historical checks, the documented green-gate legs, and the ban on plan status in the decision registry and the glossary until the cleanup arc removes the folders they read. It checks no plan number, roadmap content, freshness, work order or review disposition.", + ], + ruleKeys: { + readsRoadmap: "rules[0]", + oneRoadmap: "rules[1]", + plansFrozen: "rules[2]", + executionRecord: "rules[3]", + reviewFindings: "rules[4]", + openFindings: "rules[5]", + evidence: "rules[6]", + acceptance: "rules[7]", + forwardSection: "rules[8]", + handbookPointer: "rules[9]", + gateRecord: "rules[10]", + }, + }, + design: { + roadmapPack: "`pack:roadmap`, carried by `specs/roadmap.pack.sdp.md`.", + handbookTokens: + 'the gate reads the handbook for the strings `pack:roadmap` and `recipe 53`, and the generated graph at `generated/graph.json` for a Pack node `pack:roadmap` whose `order` is `"priority"`.', + gateMessages: + "`AGENTS.md — the handbook does not name the roadmap Pack`, `AGENTS.md — the handbook does not name the roadmap recipe`, `generated/graph.json — the roadmap Pack pack:roadmap is missing`, `generated/graph.json — the roadmap Pack pack:roadmap does not state order: priority`, and `generated/graph.json — the generated graph is missing or unreadable`.", + }, + verification: { + mode: "reviewed", + criteria: [ + "Resume an arc from its branch and draft pull request alone, reading the tree, its commits and the graph, and name any context that lived only in the earlier session.", + "Run the gate with a handbook that does not name the roadmap Pack and with a roadmap manifest that does not state `order: priority`, and see each refused; an empty roadmap, a built entry and an order contrary to dependency add no finding.", + ], + }, + }, + deliveryFacts: ["implemented", "has-verifier"], + }, + { + id: "spec:protocol.delivery-record-cleanup", + specKind: "behavior", + altitude: "feature", + readiness: "idea", + file: "specs/protocol/delivery-record-cleanup.sdp.md", + title: "The Protocol removes its frozen plans, review reports and superseded concept material", + narrative: + "Since the roadmap and each pull request carry the Protocol's delivery record, `plans/` and `reviews/` stand as frozen history, and the concept documents keep material the Specs already carry. Before they go, the durable content they still hold that no Spec, test, decision or recipe carries moves into the graph; git keeps the rest.", + sections: { + intent: { + outcome: + "Remove `plans/`, `reviews/` and the superseded concept material from the tree, after moving into the graph the durable content they still hold, so the repository keeps one delivery record: the graph, git and the pull requests.", + risks: [ + "The self-hosting gate's frozen historical checks read three plan files, and the temporal guard exempts both folders, so the removal changes those checks in the same change.", + ], + openQuestions: [ + { + question: + "Which content in `plans/`, `reviews/` and the concept documents does no Spec, test, decision or recipe carry yet, and where does each piece belong?", + blocking: true, + key: "durableContent", + decider: "owner", + provisional: + "a measured inventory by the arc that removes them, reviewed before anything is deleted", + }, + ], + }, + }, + deliveryFacts: [], + }, ] as const; diff --git a/test/self-hosting-oracle/structural-edges.ts b/test/self-hosting-oracle/structural-edges.ts index 6b455dd5e..897f4f859 100644 --- a/test/self-hosting-oracle/structural-edges.ts +++ b/test/self-hosting-oracle/structural-edges.ts @@ -25,6 +25,10 @@ export const expectedComponentIds = [ // outside the engine source seams and therefore have no truthful owner among the accepted // component set. export const structuralMembershipExceptions = [ + "impl:protocol.roadmap-practice", + "impl:protocol.design-slice-recipe", + "impl:protocol.roadmap-recipe", + "impl:protocol.roadmap-memo-recipe", "impl:protocol.proposal-memo-recipe", "impl:protocol.run-evidence-recipe", "impl:protocol.studio-preview-workflow", @@ -37,6 +41,9 @@ export const structuralMembershipExceptions = [ "impl:protocol.validator-self-testing", "impl:protocol.validator-probe-table", "impl:protocol.entry-citation-recipe", + "impl:protocol.capability-map-recipe", + "impl:protocol.capability-epics", + "impl:protocol.role-vocabulary", "impl:protocol.external-registers-recipe", "impl:protocol.example-space-coverage-recipe", "impl:protocol.architecture-walk-recipe", @@ -69,9 +76,132 @@ export const structuralMembershipExceptions = [ /** * @sdpAnchor impl:protocol.structural-self-binding * @sdpLabel realizes the declared significant-unit set and its structural self-binding - * @sdpSatisfies spec:protocol.structural-self-binding, spec:model.structural-patterns + * @sdpSatisfies spec:protocol.structural-self-binding, spec:model.structural-patterns, spec:protocol.significant-units-review */ export const acceptedArchitecturalUnits = [ + { + unit: "src/projections/studio/pages/capabilities-page.ts#capabilitiesPage", + anchorId: "impl:protocol.studio-capabilities-page", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/diagrams/capability.ts#capabilityDiagrams", + anchorId: "impl:protocol.studio-capability-diagrams", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/slice-answer.ts#SliceMember", + anchorId: "impl:protocol.studio-slice-answer", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/diagrams/slice.ts#dependencyDiagram", + anchorId: "impl:protocol.studio-slice-dependencies", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/diagrams/slice.ts#scopedArchitectureDiagrams", + anchorId: "impl:protocol.studio-slice-architecture", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/pages/slice-page.ts#slicePage", + anchorId: "impl:protocol.studio-slice-page", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/pages/architecture-register.ts#unitRegister", + anchorId: "impl:protocol.studio-unit-register", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/diagrams/requirement.ts#requirementDiagram", + anchorId: "impl:protocol.studio-requirement-diagram", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/pages/requirement-view.ts#requirementView", + anchorId: "impl:protocol.studio-requirement-view", + componentId: "component:protocol.projections", + }, + + { + unit: "src/projections/studio/pages/roadmap-page.ts#roadmapPage", + anchorId: "impl:protocol.studio-roadmap-page", + componentId: "component:protocol.projections", + }, + { + unit: "src/cli/params.ts#mergeParams", + anchorId: "impl:protocol.parameter-codec", + componentId: "component:protocol.cli", + }, + { + unit: "src/import/markdown-fidelity.ts#assertMarkdownEmissionFidelity", + anchorId: "impl:protocol.import-fidelity", + componentId: "component:protocol.import", + }, + { + unit: "src/extract/gherkin-kind-honesty.ts#gherkinKindLieReason", + anchorId: "impl:protocol.gherkin-kind-policy", + componentId: "component:protocol.extract", + }, + + { + unit: "src/cli/build-args.ts#parseBuildArgs", + anchorId: "impl:protocol.build-args", + componentId: "component:protocol.cli", + }, + { + unit: "src/cli/corpus-rules.ts#runCorpusRules", + anchorId: "impl:protocol.corpus-rule-runner", + componentId: "component:protocol.cli", + }, + { + unit: "src/extract/markdown-yaml-policy.ts#inspectMarkdownNodes", + anchorId: "impl:protocol.markdown-yaml-policy", + componentId: "component:protocol.extract", + }, + { + unit: "src/extract/markdown-body-content.ts#parseSectionContent", + anchorId: "impl:protocol.markdown-content", + componentId: "component:protocol.extract", + }, + { + unit: "src/projections/studio/diagrams/model.ts#DiagramInput", + anchorId: "impl:protocol.studio-diagram-model", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/pages/html.ts#PageContext", + anchorId: "impl:protocol.studio-html", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/inputs.ts#StudioInputs", + anchorId: "impl:protocol.studio-inputs", + componentId: "component:protocol.projections", + }, + { + unit: "src/studio-elements/proposal.ts#Proposal", + anchorId: "impl:protocol.studio-proposal-contract", + componentId: "component:protocol.studio-elements", + }, + { + unit: "src/projections/design-review-markdown.ts#pagePathOf", + anchorId: "impl:protocol.design-review-codec", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/pages/register-answers.ts#RegisterAnswers", + anchorId: "impl:protocol.studio-register-contracts", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/pages/index.ts#renderStudio", + anchorId: "impl:protocol.studio-page-catalog", + componentId: "component:protocol.projections", + }, + { unit: "src/studio-elements/memo.ts#renderMemo", anchorId: "impl:protocol.studio-proposal-memo", @@ -122,11 +252,6 @@ export const acceptedArchitecturalUnits = [ anchorId: "impl:protocol.studio-maturity-map", componentId: "component:protocol.projections", }, - { - unit: "src/studio-elements/views.ts#registerViews", - anchorId: "impl:protocol.studio-view-elements", - componentId: "component:protocol.studio-elements", - }, { unit: "src/projections/studio/pages/registers-page.ts#questionsPage", @@ -248,13 +373,28 @@ export const acceptedArchitecturalUnits = [ { unit: "src/notation/example-space.ts#decodeExampleSpaces", anchorId: "impl:protocol.example-space-decoding", - componentId: "component:protocol.notation", + componentId: "component:protocol.reader", }, { unit: "src/projections/studio/recipes.ts#studioRecipeAnswers", anchorId: "impl:protocol.studio-recipes", componentId: "component:protocol.projections", }, + { + unit: "src/projections/studio/diagrams/layout.ts#layoutDiagram", + anchorId: "impl:protocol.studio-diagram-layout", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/diagrams/routing.ts#routeDiagram", + anchorId: "impl:protocol.studio-diagram-routing", + componentId: "component:protocol.projections", + }, + { + unit: "src/projections/studio/diagrams/mermaid.ts#mermaidSource", + anchorId: "impl:protocol.studio-diagram-source", + componentId: "component:protocol.projections", + }, { unit: "src/projections/studio/diagrams/svg.ts#renderDiagram", anchorId: "impl:protocol.studio-diagrams", @@ -305,11 +445,6 @@ export const acceptedArchitecturalUnits = [ anchorId: "impl:protocol.studio-command", componentId: "component:protocol.cli", }, - { - unit: "src/cli/build-command.ts#runBuild", - anchorId: "impl:protocol.studio-build-invalidation", - componentId: "component:protocol.cli", - }, { unit: "src/reader/concept-match.ts#matchConcept", anchorId: "impl:protocol.studio-concept-match", @@ -405,26 +540,11 @@ export const acceptedArchitecturalUnits = [ anchorId: "impl:protocol.example-runner-adapter", componentId: "component:protocol.adapters", }, - { - unit: "src/cli/build-command.ts#runBuild", - anchorId: "impl:protocol.regenerability", - componentId: "component:protocol.cli", - }, - { - unit: "src/cli/build-command.ts#runBuild", - anchorId: "impl:protocol.wholesale-view-build-invalidation", - componentId: "component:protocol.cli", - }, { unit: "src/cli/build-command.ts#runBuild", anchorId: "impl:protocol.build-pipeline-emit", componentId: "component:protocol.cli", }, - { - unit: "src/cli/build-command.ts#runBuild", - anchorId: "impl:protocol.extraction-determinism", - componentId: "component:protocol.cli", - }, { unit: "src/cli/output.ts#formatFinding", anchorId: "impl:protocol.diagnostic-rendering-cli", @@ -516,20 +636,10 @@ export const acceptedArchitecturalUnits = [ componentId: "component:protocol.extract", }, { - unit: "src/extract/markdown.ts#parseMarkdownFrontmatter", + unit: "src/extract/markdown-frontmatter.ts#parseMarkdownFrontmatter", anchorId: "impl:protocol.envelope-contract", componentId: "component:protocol.extract", }, - { - unit: "src/extract/markdown.ts#readMarkdownBody", - anchorId: "impl:protocol.prose-ownership", - componentId: "component:protocol.extract", - }, - { - unit: "src/extract/markdown.ts#reifyMarkdownCarrier", - anchorId: "impl:protocol.markdown-authoring", - componentId: "component:protocol.extract", - }, { unit: "src/extract/markdown.ts#reifyMarkdownCarrier", anchorId: "impl:protocol.markdown-parser", @@ -565,16 +675,6 @@ export const acceptedArchitecturalUnits = [ anchorId: "impl:protocol.schema-version", componentId: "component:protocol.graph", }, - { - unit: "src/graph/schema.ts#graphClaims", - anchorId: "impl:protocol.graph-claims", - componentId: "component:protocol.graph", - }, - { - unit: "src/graph/schema.ts#graphEdgeTypes", - anchorId: "impl:protocol.graph-edge-types", - componentId: "component:protocol.graph", - }, { unit: "src/graph/delivery-facts.ts#computeDeliveryFacts", anchorId: "impl:protocol.delivery-facts", @@ -660,11 +760,6 @@ export const acceptedArchitecturalUnits = [ anchorId: "impl:protocol.diagnostic-rendering-design-review", componentId: "component:protocol.projections", }, - { - unit: "src/projections/design-review-pages.ts#renderPackPage", - anchorId: "impl:protocol.binding-language-pack-table", - componentId: "component:protocol.projections", - }, { unit: "src/projections/design-review-pages.ts#renderIndexPage", anchorId: "impl:protocol.binding-language-index-table", @@ -695,11 +790,6 @@ export const acceptedArchitecturalUnits = [ anchorId: "impl:protocol.reader", componentId: "component:protocol.reader", }, - { - unit: "src/reader/reader.ts#createReader", - anchorId: "impl:protocol.agent-surface", - componentId: "component:protocol.reader", - }, { unit: "src/reader/reader.ts#createReader", anchorId: "impl:protocol.reader", @@ -865,11 +955,6 @@ export const acceptedArchitecturalUnits = [ anchorId: "impl:protocol.build-imports", componentId: "component:protocol.cli", }, - { - unit: "src/reader/evaluate-body.ts#compileBody", - anchorId: "impl:protocol.query-answer-shapes", - componentId: "component:protocol.reader", - }, { unit: "src/extract/key-markers.ts#normalizeKeyedContent", anchorId: "impl:protocol.key-markers", @@ -917,7 +1002,6 @@ export const acceptedArchitecturalUnits = [ }, ] as const; -// re-measured under plan 41 export const expectedMemberOfEdges = acceptedArchitecturalUnits .map((row) => [row.anchorId, row.componentId] as const) .filter( @@ -928,16 +1012,24 @@ export const expectedMemberOfEdges = acceptedArchitecturalUnits // 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. Plan 43 removed the -// unused targetsForRelationType helper after the emitter switched to full relation objects. export const coarseGrainCoverage = [ { - unit: "src/cli/params.ts#mergeParams", - coveredBy: "impl:protocol.query-params", - componentId: "component:protocol.cli", - rationale: "src/cli/q-command.ts#parseQueryArgs value-calls mergeParams", + unit: "src/extract/markdown-types.ts#MarkdownFrontmatterResult", + coveredBy: "impl:protocol.markdown-parser", + componentId: "component:protocol.extract", + consumption: "type", + rationale: + "src/extract/markdown.ts uses the imported MarkdownFrontmatterResult as a parser return type", + }, + { + unit: "src/extract/markdown-types.ts#MarkdownBodyResult", + coveredBy: "impl:protocol.markdown-parser", + componentId: "component:protocol.extract", + consumption: "type", + rationale: + "src/extract/markdown.ts uses the imported MarkdownBodyResult as a parser return type", }, + { unit: "src/studio-elements/dom.ts#button", coveredBy: "impl:protocol.studio-search-results", @@ -950,13 +1042,6 @@ export const coarseGrainCoverage = [ componentId: "component:protocol.studio-elements", rationale: "src/studio-elements/search-results.ts#registerSearchResults value-calls liveOutput", }, - { - unit: "src/import/markdown-fidelity.ts#assertMarkdownEmissionFidelity", - coveredBy: "impl:protocol.sdp-import-markdown-emit", - componentId: "component:protocol.import", - rationale: - "src/import/emit-markdown.ts#emitMarkdownSpec value-calls assertMarkdownEmissionFidelity", - }, { unit: "src/import/data-access.ts#importData", coveredBy: "impl:protocol.sdp-import-markdown-emit", @@ -975,19 +1060,21 @@ export const coarseGrainCoverage = [ componentId: "component:protocol.import", rationale: "src/import/emit-markdown.ts#emitMarkdownSpec value-calls importTexts", }, - { - unit: "src/extract/gherkin-kind-honesty.ts#gherkinKindLieReason", - coveredBy: "impl:protocol.gherkin-authoring", - componentId: "component:protocol.extract", - rationale: "src/extract/gherkin.ts#decorationTagFinding value-calls gherkinKindLieReason", - }, ] as const; // 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 41 export const expectedUsesEdges = [ + ["component:protocol.import", "component:protocol.notation"], + ["component:protocol.reader", "component:protocol.notation"], + ["impl:protocol.extract", "impl:protocol.discover-files"], + ["impl:protocol.extract", "impl:protocol.carrier-reification"], + ["impl:protocol.extract", "impl:protocol.derive-graph"], + ["impl:protocol.studio-diagrams", "impl:protocol.studio-diagram-model"], + ["impl:protocol.gherkin-authoring", "impl:protocol.gherkin-kind-policy"], + ["impl:protocol.gherkin-view", "impl:protocol.gherkin-kind-policy"], + ["impl:protocol.stamp-command", "impl:protocol.schema-version"], ["impl:protocol.vault-command", "component:protocol.projections"], ["impl:protocol.vault-notes", "impl:protocol.reader"], @@ -1012,6 +1099,7 @@ export const expectedUsesEdges = [ ["component:protocol.cli", "component:protocol.validate"], ["component:protocol.codegen", "component:protocol.graph"], ["component:protocol.codegen", "component:protocol.notation"], + ["component:protocol.codegen", "component:protocol.reader"], ["component:protocol.codegen", "component:protocol.validate"], ["component:protocol.extract", "component:protocol.graph"], ["component:protocol.extract", "component:protocol.model"], diff --git a/test/self-hosting-oracle/validation.ts b/test/self-hosting-oracle/validation.ts index eae454770..c8694af45 100644 --- a/test/self-hosting-oracle/validation.ts +++ b/test/self-hosting-oracle/validation.ts @@ -353,7 +353,7 @@ export const validationSpecs = [ { id: "spec:validation.two-check-families", specKind: "rule", - altitude: "feature", + altitude: "epic", readiness: "ready", file: "specs/validation/two-check-families.sdp.md", title: "Validation separates well-formedness from non-pretending", diff --git a/test/skills.test.ts b/test/skills.test.ts index fdca85f08..ebefa0174 100644 --- a/test/skills.test.ts +++ b/test/skills.test.ts @@ -61,6 +61,15 @@ const skillPaths = [ ] as const; const studioSkillPath = ".agents/skills/sdp-studio/SKILL.md"; const vaultSkillPath = ".agents/skills/sdp-vault/SKILL.md"; + +it("discovers the design slice's summary and detail parameter", () => { + const { body } = readSkill(".agents/skills/sdp-agent-surface/SKILL.md"); + const detail = body.slice( + body.indexOf("A recipe that takes `params.detail`"), + body.indexOf("To filter any shipped answer"), + ); + expect(detail).toContain("the design slice, recipe 54"); +}); /** Every shipped skill; the generic checks run over all of them. */ const allSkillPaths = [...skillPaths, studioSkillPath, vaultSkillPath] as const; @@ -109,6 +118,82 @@ it("teaches the named-file exception to both carrier-watch descriptions", () => } }); +it("teaches the optional priority field in the Pack manifest envelope", () => { + const { body } = readSkill(".agents/skills/sdp-authoring/SKILL.md"); + const pack = body.split("## Author a Pack")[1]?.split("## Bind code")[0]; + expect(pack).toContain("optional `order`, whose one lawful value is `priority`"); +}); + +it("teaches design rounds and curates bindings by responsibility", () => { + const rounds = flat(skillSection(skillPaths[1], "### Design in rounds")); + for (const clause of [ + "recipe 54", + "`idea`", + "`scoped`", + "`defined`", + "`ready`", + "only the builder", + "second party", + "design columns", + "No check reads this guidance", + ]) + expect(rounds).toContain(clause); + const binding = flat(skillSection(skillPaths[1], "### Curate the anchors")); + for (const clause of [ + "contract or responsibility", + "export status or import rank alone", + "Mechanical wrappers", + "unless they own an independent contract or architectural responsibility", + "Several Specs realized by one responsibility normally share an anchor with plural targets", + "independent contracts, entry points and semantic policies", + "Before merging or dropping an identity, list every promise", + "name the surviving units", + "explicitly record a false claim being removed", + "value or type import", + "responsibility in its label", + "component's reading guidance", + ]) + expect(binding).toContain(clause); +}); + +it("presents the Protocol engine roles and reviewed boundary as worked examples for adopters", () => { + const architecture = flat( + skillSection(skillPaths[1], "### Declare architecture where it is realized"), + ); + expect(architecture).toContain( + "the Protocol's own engine roles (`spec:protocol.role-vocabulary`) are a worked example, not a list", + ); + const curation = flat(skillSection(skillPaths[1], "### Curate the anchors")); + expect(curation).toContain( + "the Protocol's own reviewed boundary, `spec:protocol.significant-units-review`, is a worked example", + ); +}); + +it("routes the next work and reviews through roadmap and proposal recipes", () => { + const sessions = flat(readSkill(skillPaths[2]).body); + for (const clause of [ + "roadmap (recipe 53)", + "design slice (recipe 54)", + "branch-head commit", + "stamp's merge commit", + "proposal memo (recipe 49)", + "code or other files", + "open question", + ]) + expect(sessions).toContain(clause); + expect(sessions).not.toContain("plan pointers"); + expect(skillSection(skillPaths[2], "### Design").match(/recipe 53/gu)).toHaveLength(1); + const tasks = skillSection(skillPaths[0], "### Which recipe answers a task"); + for (const number of [52, 53, 54, 55]) expect(tasks).toContain(`recipe ${String(number)}`); + expect(sessions).toContain("a proposal composed in a preview names the stamp's merge commit"); + expect(sessions).toContain("Recipe 38 always reads the graph of the commit that was read"); + expect(sessions).toContain("Report a Spec proposal as a numbered list"); + expect(sessions).not.toContain("Report a review as a numbered list"); + const shape = flat(skillSection(skillPaths[0], "## The shape of the graph")); + expect(shape).toContain("optional `order`"); + expect(shape).toContain("advisory priority"); +}); + // A fence inside a numbered step is indented, so fences are matched after trimming. function shellFenceLines(body: string): readonly string[] { const lines: string[] = []; @@ -182,8 +267,8 @@ function assertPlanningHomes(authoring: string): void { "A ruling that awaits its owner is a `decision` Spec below `ready`, with `decidedBy` from each Spec it shapes.", "A statement a test checks lives in the Spec that states it, plus an `example` Spec that `verifies` it.", "Derive a count, a register, or a review scope each time you need it:", - "Group a body of work in a Pack named for its capability, never its plan number; keep its Specs in reading order. Membership confers no readiness, delivery fact or gate; the Pack remains a review aggregate after the work ends.", - "A plan file points to the Pack and carrying Specs for intended truth and derived state. It may keep an advisory execution and recovery record; plan numbering, recorded work order and staleness confer no graph structure or workflow authority.", + "Group a body of work in a Pack named for its capability, never a number or a date; keep its Specs in reading order. Membership confers no readiness, delivery fact or gate; the Pack remains a review aggregate after the work ends.", + "Hold advisory priority over the next work in a roadmap, a Pack whose manifest states `order: priority`, and read it with recipe 53; its order confers nothing. Keep an arc's execution record in git and its pull request description. A corpus that keeps plan documents of its own lets them point to Packs and carrying Specs; they confer nothing.", ]) expect(placement).toContain(home); } @@ -232,7 +317,7 @@ describe("Protocol skill assets", () => { "A statement a test checks", "Derive a count", "Group a body of work", - "A plan file points", + "Hold advisory priority", ])("rejects missing planning guidance: %s", (opening) => { const authoring = readSkill(skillPaths[1]).body; const withoutHome = authoring.replace(new RegExp(`^- ${opening}[^]*?(?=^\\n|^- )`, "mu"), ""); diff --git a/test/structural-coverage.test.ts b/test/structural-coverage.test.ts index 795caa00c..b6c6c85ae 100644 --- a/test/structural-coverage.test.ts +++ b/test/structural-coverage.test.ts @@ -86,6 +86,79 @@ function fixtureProgram(files: Readonly<Record<string, string>>): ts.Program { } describe("coarse structural coverage", () => { + it.each([ + [ + 'import type { Result } from "./unit.js";\nexport function parse(): Result { return { ok: true }; }', + "ok", + ], + [ + 'import { type Result as Parsed } from "./unit.js";\nexport const result: Parsed = { ok: true };', + "ok", + ], + ['import type * as T from "./unit.js";\nexport const result: T.Result = { ok: true };', "ok"], + ['import type { Result } from "./unit.js";\nexport interface Parsed extends Result {}', "ok"], + ['import type * as T from "./unit.js";\nexport interface Parsed extends T.Result {}', "ok"], + [ + 'import type * as T from "./unit.js";\nexport const result = true;', + "covering source does not type-consume unit", + ], + [ + 'import type * as T from "./other.js";\nexport const result: T.Result = { ok: true };', + "covering source does not type-consume unit", + ], + [ + 'import type { Result } from "./unit.js";\nexport const result = true;', + "covering source does not type-consume unit", + ], + [ + "type Result = { ok: boolean };\nexport const result: Result = { ok: true };", + "covering source does not type-consume unit", + ], + [ + 'import type { Result } from "./other.js";\nexport const result: Result = { ok: true };', + "covering source does not type-consume unit", + ], + ])("checks actual consumption of a shared type: %s", (cover, expected) => { + const program = fixtureProgram({ + "unit.ts": "export type Result = { ok: boolean };\n", + "other.ts": "export type Result = { ok: boolean };\n", + "cover.ts": cover, + }); + expect(auditStructuralCoverage(program, "unit.ts#Result", "cover.ts", "type")).toBe(expected); + }); + + it.each([ + ['import { Result } from "./unit.js";\nexport type Constructor = typeof Result;', "ok"], + ['import * as T from "./unit.js";\nexport type Constructor = typeof T.Result;', "ok"], + ['import { Result } from "./unit.js";\nexport class Parsed extends Result {}', "ok"], + ['import * as T from "./unit.js";\nexport class Parsed extends T.Result {}', "ok"], + [ + 'import * as T from "./other.js";\nexport type Constructor = typeof T.Result;', + "covering source does not type-consume unit", + ], + [ + 'import { Result } from "./unit.js";\nexport function local() { class Result {} type Constructor = typeof Result; }', + "covering source does not type-consume unit", + ], + ])("checks shared class consumption in heritage and type queries: %s", (cover, expected) => { + const program = fixtureProgram({ + "unit.ts": "export class Result {}\n", + "other.ts": "export class Result {}\n", + "cover.ts": cover, + }); + expect(auditStructuralCoverage(program, "unit.ts#Result", "cover.ts", "type")).toBe(expected); + }); + + it("reports a missing shared type independently of its consumer", () => { + const program = fixtureProgram({ + "unit.ts": "export interface Other { ok: boolean }\n", + "cover.ts": "export const result = true;\n", + }); + expect(auditStructuralCoverage(program, "unit.ts#Result", "cover.ts", "type")).toBe( + "exported unit missing", + ); + }); + it("reports a missing runtime export when the named unit is absent", () => { // Given: a source module that does not export the rostered function. const program = fixtureProgram({ diff --git a/test/studio-capabilities.test.ts b/test/studio-capabilities.test.ts new file mode 100644 index 000000000..b0c6ac115 --- /dev/null +++ b/test/studio-capabilities.test.ts @@ -0,0 +1,587 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { runInNewContext } from "node:vm"; +import postcss from "postcss"; +import { evaluateBody } from "../src/reader/evaluate-body.js"; +import { specTest, testAnchorId, ref } from "../src/anchors.js"; +import { schemaVersion } from "../src/graph/schema.js"; +import type { GraphSchema, PrimitiveNode } from "../src/graph/schema.js"; +import { createReader } from "../src/reader/reader.js"; +import { createStudioData } from "../src/projections/studio/data.js"; +import { renderStudio } from "./helpers/studio-render.js"; +import { + capabilityDiagrams, + capabilityWalk, +} from "../src/projections/studio/diagrams/capability.js"; +import type { CapabilityMap } from "../src/projections/studio/diagrams/capability.js"; +import { renderDiagram } from "../src/projections/studio/diagrams/svg.js"; +import { mermaidSource } from "../src/projections/studio/diagrams/mermaid.js"; +import { rowMatches, epicFilter } from "../src/studio-elements/filter.js"; + +function required<T>(value: T | undefined): T { + if (value === undefined) throw new Error("Missing test value"); + return value; +} + +const spec = (id: string, changes: Partial<PrimitiveNode> = {}): PrimitiveNode => ({ + id: `spec:probe.${id}`, + file: "design/probe.sdp.md", + nodeType: "Primitive", + claim: "declared", + title: `Title ${id}`, + specKind: "behavior", + altitude: "story", + readiness: "scoped", + deliveryFacts: [], + sections: { + intent: { + outcome: "Read a capability", + openQuestions: [ + { key: "choice", question: "Which choice?", blocking: false }, + { question: "Which fallback?", blocking: true }, + ], + }, + behavior: { rules: ["A rule"] }, + }, + ...changes, +}); +const refines = (from: string, to: string) => ({ + from: `spec:probe.${from}`, + to: `spec:probe.${to}`, + type: "refines" as const, + claim: "declared" as const, +}); +function graph(nodes: PrimitiveNode[], edges: GraphSchema["edges"] = []): GraphSchema { + return { schemaVersion, nodes, edges, locations: [] }; +} +const probe = graph( + [ + spec("root", { altitude: "epic", specKind: "model" }), + spec("a", { altitude: "epic" }), + spec("b", { altitude: "feature" }), + spec("shared", { specKind: "example" }), + spec("decision", { specKind: "decision" }), + spec("outside"), + ], + [ + refines("a", "root"), + refines("b", "root"), + refines("shared", "a"), + refines("shared", "b"), + refines("decision", "shared"), + refines("missing", "root"), + ], +); +let data: Awaited<ReturnType<typeof createStudioData>>; +let map: CapabilityMap; +let pages: ReturnType<typeof renderStudio>; +beforeAll(async () => { + data = await createStudioData(createReader(probe)); + map = data.recipes.all.get(52) as CapabilityMap; + pages = renderStudio(data); +}, 30_000); +const html = (path: string) => { + const page = pages.find((p) => p.path === path); + if (!page) throw new Error(`Missing ${path}`); + return page.content; +}; + +const capabilitiesAnchor = specTest({ + id: testAnchorId("test:protocol.studio-capabilities"), + verifies: [ + ref("spec:consumers.spec-studio.capability-map-page"), + ref("spec:consumers.spec-studio.diagram-kinds.capability-tree"), + ], +}); +void capabilitiesAnchor; + +describe("Studio capabilities", () => { + it.each([ + [30, 1], + [45, 5], + [40, 40], + ])("folds whole sibling branches for %i features with %i stories", async (features, stories) => { + const root = spec("root", { altitude: "epic" }); + const nodes = [root]; + const edges: GraphSchema["edges"][number][] = []; + for (let i = 0; i < features; i++) { + const feature = `feature${String(i).padStart(2, "0")}`; + nodes.push(spec(feature, { altitude: "feature" })); + edges.push(refines(feature, "root")); + for (let j = 0; j < stories; j++) { + const story = `${feature}.story${String(j).padStart(2, "0")}`; + nodes.push(spec(story)); + edges.push(refines(story, feature)); + } + } + const answer = (await evaluateBody("return recipes(52, {detail: true})", { + graph: graph(nodes, edges), + })) as CapabilityMap; + const drawing = capabilityDiagrams(answer, [root.id], root.id)[0]; + const drawn = new Set(drawing.boxes.map((b) => b.id)); + expect(drawing.boxes.length).toBeLessThanOrEqual(40); + for (const box of drawing.boxes) + if (box.parent) expect(drawn.has(box.parent), box.id).toBe(true); + expect(drawing.edges).toHaveLength(drawing.boxes.length - 1); + const visible = new Set(drawing.boxes.filter((b) => !b.folded).map((b) => b.identity)); + const hidden = nodes + .filter((n) => !visible.has(n.id)) + .map((n) => n.id) + .sort(); + const folded = [...new Set(drawing.boxes.flatMap((b) => b.foldedIds ?? []))].sort(); + expect(folded).toEqual(hidden); + const output = renderDiagram(drawing); + expect(output).toContain("<svg"); + for (const match of output.matchAll(/<tspan class="box-id"[^>]*>([^<]*)<\/tspan>/gu)) { + expect(match[1]).not.toMatch(/%3A|\.capability\.branch\./u); + } + expect(drawing).toEqual(capabilityDiagrams(answer, [root.id], root.id)[0]); + }); + + it("keeps reached children out of the unreached drawing and complete rows", async () => { + const data = await createStudioData( + createReader( + graph( + [spec("e", { altitude: "epic" }), spec("u"), spec("s")], + [refines("s", "e"), refines("s", "u")], + ), + ), + ); + const output = required(renderStudio(data).find((p) => p.path === "capabilities.html")).content; + const unreached = output.slice(output.indexOf('<section id="unreached">')); + expect(unreached).toContain("spec:probe.u"); + expect(unreached).not.toContain("spec:probe.s"); + expect(unreached).toContain("an unreached band"); + }); + + it("shows cycle diagnostics outside closed disclosures", async () => { + const data = await createStudioData( + createReader( + graph( + [spec("root", { altitude: "epic" }), spec("a"), spec("b"), spec("c")], + [refines("a", "root"), refines("b", "a"), refines("a", "b"), refines("c", "b")], + ), + ), + ); + const output = required(renderStudio(data).find((p) => p.path === "capabilities.html")).content; + const diagnostics = required( + /<p>Refinement cycles<\/p><ul>([\s\S]*?)<\/ul>/u.exec(output)?.[1], + ); + expect(diagnostics).toContain("spec:probe.a"); + expect(diagnostics).toContain("spec:probe.b"); + expect(output).toContain("</details><p>Refinement cycles</p>"); + }); + + it("exports every cycle member and exact edge with the Mermaid fallback reason", async () => { + const answer = (await evaluateBody("return recipes(52, {detail: true})", { + graph: graph( + [spec("root", { altitude: "epic" }), spec("a"), spec("b"), spec("c")], + [refines("a", "root"), refines("b", "a"), refines("a", "b"), refines("c", "b")], + ), + })) as CapabilityMap; + const drawing = capabilityDiagrams(answer, ["spec:probe.root"], "cycle")[0]; + expect(mermaidSource(drawing)).toContain("%% cycle"); + expect(mermaidSource(drawing)).toContain("spec:probe.b"); + expect(mermaidSource(drawing)).toContain("spec:probe.c refines spec:probe.b"); + expect(mermaidSource(drawing)).toContain("spec:probe.a refines spec:probe.b"); + expect(mermaidSource(drawing)).toContain("spec:probe.b refines spec:probe.a"); + const rendered = renderDiagram(drawing); + expect(rendered).toContain("Preferred syntax: mindmap"); + expect(rendered).toContain("Chosen syntax: flowchart BT"); + expect(rendered).toContain("a cycle"); + }); + + it("renders code-span titles as plain drawing words and questions as linked prose", async () => { + const node = spec("root", { + altitude: "epic", + title: "`Root` title", + sections: { + intent: { + outcome: "Read", + openQuestions: [{ question: "Read `code` and spec:probe.root", blocking: false }], + }, + }, + }); + const d = await createStudioData(createReader(graph([node]))); + const page = required(renderStudio(d).find((p) => p.path === "capabilities.html")).content; + expect(page).toContain( + 'non-blocking</a>: Read <code>code</code> and <a href="specs/probe.root.html">', + ); + const drawing = required( + capabilityDiagrams(d.recipes.all.get(52) as CapabilityMap, [node.id], node.id)[0], + ); + expect(drawing.boxes[0]?.title).toBe("Root title"); + expect(mermaidSource(drawing)).not.toContain("`Root`"); + }); + it("initializes epic filtering from the URL, intersects every control and resets native rows", async () => { + const row = { + epic: "spec:probe.root", + kind: "example", + altitude: "story", + stated: "ready", + floor: "defined", + implementation: "present", + verifier: "none", + pack: "pack:a", + }; + for (const query of ["?epic=spec%3Aprobe.root", "?epic=bad", "?epic=none&epic=none"]) { + const defined = new Map< + string, + new () => FakeNode & { connectedCallback(): void; disconnectedCallback(): void } + >(); + class FakeNode { + value = "any"; + hidden = false; + textContent = ""; + parentNode?: FakeNode; + attributes = new Map<string, string>(); + listeners = new Map<string, (event?: Event) => void>(); + options = [{ value: "any" }, { value: "none" }, { value: "spec:probe.root" }]; + ownerDocument = { + location: { href: `file:///index.html${query}`, hash: "" }, + createElement: () => new FakeNode(), + getElementById: () => null, + addEventListener: () => undefined, + removeEventListener: () => undefined, + }; + setAttribute(name: string, value: string) { + this.attributes.set(name, value); + } + getAttribute(name: string) { + return this.attributes.get(name) ?? null; + } + addEventListener(name: string, callback: (event?: Event) => void) { + this.listeners.set(name, callback); + } + removeEventListener(name: string) { + this.listeners.delete(name); + } + append(node: FakeNode) { + node.parentNode = this; + } + querySelector(selector: string) { + return selector === "form" ? form : selector === "[data-filter-note]" ? note : selects[0]; + } + querySelectorAll(selector: string) { + return selector === "[data-row]" ? rows : selector === "output" ? [] : selects; + } + } + const form = new FakeNode(), + note = new FakeNode(); + form.hidden = true; + const selects = Object.keys(row).map((name) => { + const n = new FakeNode(); + n.setAttribute("data-filter", name); + return n; + }); + const rows = [row, { ...row, epic: "none" }].map((values) => { + const n = new FakeNode(); + for (const [key, value] of Object.entries(values)) n.setAttribute(`data-${key}`, value); + return n; + }); + runInNewContext(readFileSync(new URL("../dist/studio/studio.js", import.meta.url), "utf8"), { + HTMLElement: FakeNode, + Event, + queueMicrotask, + URL, + customElements: { + get: () => undefined, + define: ( + name: string, + ctor: new () => FakeNode & { connectedCallback(): void; disconnectedCallback(): void }, + ) => defined.set(name, ctor), + }, + }); + const host = new (required(defined.get("sdp-filter")))(); + host.connectedCallback(); + const valid = query === "?epic=spec%3Aprobe.root"; + expect(form.hidden).toBe(false); + expect(selects[0]?.value).toBe(valid ? "spec:probe.root" : "any"); + expect(rows.map((r) => r.hidden)).toEqual(valid ? [false, true] : [false, false]); + expect(note.textContent).toBe(valid ? "" : "Unknown epic filter. All Specs are shown."); + if (valid) + for (const select of selects.slice(1)) { + select.value = "missing"; + host.listeners.get("change")?.(); + expect(rows.map((r) => r.hidden)).toEqual([true, true]); + select.value = + row[required(select.getAttribute("data-filter") ?? undefined) as keyof typeof row]; + host.listeners.get("change")?.(); + expect(rows.map((r) => r.hidden)).toEqual([false, true]); + select.value = "any"; + } + const event = new Event("reset", { cancelable: true }); + form.listeners.get("reset")?.(event); + expect(event.defaultPrevented).toBe(false); + // The browser applies option defaults only after an uncancelled reset event. + if (!event.defaultPrevented) for (const select of selects) select.value = "native-default"; + await new Promise<void>((resolve) => { + queueMicrotask(resolve); + }); + expect(rows.map((r) => r.hidden)).toEqual([false, false]); + expect(note.textContent).toBe(""); + expect(selects.every((s) => s.value === "any")).toBe(true); + host.disconnectedCallback(); + expect(form.listeners.size).toBe(0); + expect(host.listeners.size).toBe(0); + } + }); + + it("reflows capability cards, caps deep indentation and opens complete text for print", async () => { + const css = postcss.parse( + readFileSync(new URL("../src/projections/studio/studio.css", import.meta.url), "utf8"), + ); + const declarations: string[] = []; + css.walkRules((rule) => { + if (rule.selector === ".capability-overview") + rule.walkDecls((d) => { + declarations.push(`${d.prop}:${d.value}`); + }); + }); + expect(declarations).toContain( + "grid-template-columns:repeat(auto-fit, minmax(min(100%, 17rem), 1fr))", + ); + let printOpensText = false; + css.walkAtRules("media", (rule) => { + if (rule.params === "print") + rule.walkRules("details::details-content", (r) => { + r.walkDecls("content-visibility", (d) => { + if (d.value === "visible" && d.important) printOpensText = true; + }); + }); + }); + expect(printOpensText).toBe(true); + const nodes = Array.from({ length: 7 }, (_, i) => + spec(`deep${String(i)}`, i === 0 ? { altitude: "epic" } : {}), + ); + const data = await createStudioData( + createReader( + graph( + nodes, + nodes.slice(1).map((_, i) => refines(`deep${String(i + 1)}`, `deep${String(i)}`)), + ), + ), + ); + const output = required(renderStudio(data).find((p) => p.path === "capabilities.html")).content; + expect(output).toContain('style="--depth:4"'); + expect(output).not.toMatch(/style="--depth:[5-9]/u); + expect(output).toContain('Parent: <a href="specs/probe.deep5.html"'); + }); + + it("folds only a leaf remainder at the root, preserves first siblings and falls back for too many roots", async () => { + const root = spec("root", { altitude: "epic" }); + const leaves = Array.from({ length: 50 }, (_, i) => spec(`leaf${String(i).padStart(2, "0")}`)); + const answer = ( + await createStudioData( + createReader( + graph( + [root, ...leaves], + leaves.map((n) => refines(n.id.slice(11), "root")), + ), + ), + ) + ).recipes.all.get(52) as CapabilityMap; + const d = capabilityDiagrams(answer, [root.id], root.id)[0]; + expect(d.boxes).toHaveLength(31); + expect(d.boxes[1]?.identity).toBe("spec:probe.leaf00"); + expect(d.boxes.at(-1)?.foldedIds).toHaveLength(21); + expect(d.edges.at(-1)?.to).toBe(d.boxes[0]?.id); + const isolated = leaves.map((n) => ({ ...n, altitude: "epic" as const })); + const many = (await createStudioData(createReader(graph(isolated)))).recipes.all.get( + 52, + ) as CapabilityMap; + const fallback = renderDiagram( + capabilityDiagrams( + many, + isolated.map((n) => n.id), + "many", + )[0], + "many.capability", + ); + expect(fallback).not.toContain("<svg"); + expect(fallback).toContain('<details open id="diagram.many.capability.text"'); + for (const node of isolated) expect(fallback).toContain(node.id); + }); + it("condenses a cycle into a labelled group and retains its internal relations in words", async () => { + const g = graph( + [spec("root", { altitude: "epic" }), spec("a"), spec("b")], + [refines("a", "root"), refines("b", "a"), refines("a", "b")], + ); + const m = (await createStudioData(createReader(g))).recipes.all.get(52) as CapabilityMap; + const d = capabilityDiagrams(m, ["spec:probe.root"], "cycle")[0]; + expect(d.boxes).toHaveLength(2); + expect(d.edges).toHaveLength(1); + expect(d.boxes[1]?.detail).toContain("cycle · 2 unique Specs"); + const text = renderDiagram(d, "cycle.capability"); + expect(text).toContain("spec:probe.b</code> refines <code>spec:probe.a"); + expect(text).toContain("spec:probe.a</code> refines <code>spec:probe.b"); + }); + + it("shares the detailed recipe answer and publishes a lens immediately after Packs", () => { + expect(map.nodes).toHaveLength(6); + expect(map.epics.find((e) => e.id === "spec:probe.root")?.rollup.specs).toBe(4); + for (const page of pages.filter((p) => !p.path.startsWith("exports/"))) { + expect(page.content).toMatch( + /href="(?:\.\.\/)?index.html"[^>]*>Packs<\/a><a href="(?:\.\.\/)?capabilities.html"/u, + ); + } + expect(html("capabilities.html")).toContain('href="capabilities.html" aria-current="page"'); + expect(html("capabilities.html")).not.toContain('id="sdp-data"'); + }); + it("labels nested epics, scopes distinct rollups and separates examples and decisions", () => { + const page = html("capabilities.html"); + expect(page).toContain('id="spec:probe.root"'); + expect(page).toContain('Inside <a href="specs/probe.root.html"'); + expect(page).toContain('Contains epic <a href="specs/probe.a.html"'); + expect(page).toContain("Distinct descendants, excluding this epic"); + expect(page).toContain("<p>4 distinct descendants · 1 example · 1 decision.</p></article>"); + expect(page).toContain("<summary>Example · Title shared</summary>"); + expect(page).toContain("<summary>Decision · Title decision</summary>"); + expect(page).toContain("implementation binding none"); + expect(page).toContain("verifier binding none"); + expect(page).toContain("stated scoped"); + expect(page).toContain("floor scoped"); + expect(page).toContain("index.html?epic=spec%3Aprobe.root#specs"); + expect(page).toContain("unresolved-refinement"); + expect(page).not.toContain('href="specs/probe.missing.html"'); + }); + it("keeps depth-first repeats, every relation and question links to real ids", () => { + const walk = capabilityWalk(map, ["spec:probe.root"]); + expect(walk.map((v) => [v.node.id, v.repeat])).toEqual([ + ["spec:probe.root", false], + ["spec:probe.a", false], + ["spec:probe.shared", false], + ["spec:probe.decision", false], + ["spec:probe.b", false], + ["spec:probe.shared", true], + ]); + const page = html("capabilities.html"); + expect(page).toContain("Repeat of"); + expect(page).toContain("Parent:"); + expect(page).toContain("specs/probe.root.html#question.choice"); + expect(page).toContain("specs/probe.root.html#section.intent"); + expect(html("specs/probe.root.html")).toContain('id="question.choice"'); + expect(html("specs/probe.root.html")).toContain('id="section.intent"'); + for (const edge of map.edges) + expect(page).toContain(`${edge.from}</code> refines <code>${edge.to}`); + }); + it("renders a repeat box with its own child-to-parent arrow and groups siblings", () => { + const d = capabilityDiagrams(map, ["spec:probe.root"], "spec:probe.root")[0]; + expect(d.boxes).toHaveLength(6); + expect(d.edges).toHaveLength(5); + const repeats = d.boxes.filter((b) => b.detail?.includes("repeat")); + expect(repeats).toHaveLength(1); + expect(repeats[0]?.target).toContain(".capability.branch.spec%253Aprobe.shared"); + expect(new Set(d.boxes.map((b) => b.band)).size).toBeGreaterThan(2); + expect(mermaidSource(d)).toMatch(/^flowchart BT/u); + const drawing = renderDiagram(d, "spec:probe.root.capability"); + expect(drawing).toContain("Later visits repeat a Spec and link to its first occurrence."); + expect(drawing).toContain("repeated parentage"); + expect(drawing).toContain('id="diagram.spec:probe.root.capability.arrow"'); + }); + it("uses mindmap only for a strict tree and folds large branches without losing text", async () => { + const nodes = [ + spec("root", { altitude: "epic" }), + spec("a", { altitude: "feature" }), + ...Array.from({ length: 65 }, (_, i) => spec(`leaf${String(i).padStart(2, "0")}`)), + ]; + const edges = [ + refines("a", "root"), + ...nodes.slice(2).map((n) => refines(n.id.slice(11), "a")), + ]; + const answer = (await createStudioData(createReader(graph(nodes, edges)))).recipes.all.get( + 52, + ) as CapabilityMap; + const drawing = capabilityDiagrams(answer, ["spec:probe.root"], "large")[0]; + expect(drawing.boxes.length).toBeLessThanOrEqual(40); + expect(drawing.boxes.some((b) => b.folded && b.foldedIds?.length === 65)).toBe(true); + const output = renderDiagram(drawing, "large.capability"); + expect( + [...output.matchAll(/<tspan class="box-id"[^>]*>([^<]*)<\/tspan>/gu)] + .map((m) => m[1]) + .join(" "), + ).not.toMatch(/%3A|\.capability\.branch\./u); + expect(output).toContain("65 unique Specs"); + expect(output).toContain("a fold"); + for (const n of nodes) expect(output).toContain(n.id); + const small = ( + await createStudioData(createReader(graph(nodes.slice(0, 2), edges.slice(0, 1)))) + ).recipes.all.get(52) as CapabilityMap; + expect(mermaidSource(capabilityDiagrams(small, ["spec:probe.root"], "small")[0])).toMatch( + /^mindmap/u, + ); + expect(output).toEqual( + renderDiagram( + capabilityDiagrams(answer, ["spec:probe.root"], "large")[0], + "large.capability", + ), + ); + }); + it("keeps cycles finite, labelled and complete, and handles no epics or an empty corpus", async () => { + for (const g of [ + graph([], []), + graph([spec("a"), spec("b")], [refines("a", "b"), refines("b", "a")]), + ]) { + const d = await createStudioData(createReader(g)); + const output = required(renderStudio(d).find((p) => p.path === "capabilities.html")).content; + expect(output).toContain("No epic reaches these Specs"); + expect(output).toContain("No epics"); + if (g.nodes.length) { + expect(output).toContain("cycle"); + expect(output).toContain("spec:probe.a</code> refines <code>spec:probe.b"); + } else expect(output).toContain("No unreached Specs."); + } + }); + it("records all epic memberships on index rows and preserves Pack disclosures", () => { + const index = html("index.html"); + expect(index).toContain('data-filter="epic"'); + expect(index).toContain('<option value="none">No epic</option>'); + expect(index).toContain('data-epic="spec:probe.a spec:probe.root"'); + expect(index).toContain('data-epic="none"'); + expect(index).toContain("Filtering needs script"); + expect(index).toContain('type="reset"'); + expect(index).not.toMatch(/data-row[^>]* hidden/u); + }); + it("intersects epic selection with every existing filter and treats no epic explicitly", () => { + const row = { + epic: "spec:probe.a spec:probe.root", + kind: "example", + altitude: "story", + stated: "ready", + floor: "defined", + implementation: "present", + verifier: "none", + pack: "pack:a pack:b", + }; + for (const name of Object.keys(row).filter((k) => k !== "epic")) { + expect( + rowMatches(row, { + epic: "spec:probe.root", + [name]: required(row[name as keyof typeof row].split(" ")[0]), + }), + ).toBe(true); + expect(rowMatches(row, { epic: "spec:probe.root", [name]: "missing" })).toBe(false); + } + expect(rowMatches({ epic: "none" }, { epic: "none" })).toBe(true); + for (const query of [ + "?epic=missing", + "?epic=spec%3Aprobe.shared", + "?epic=none&epic=none", + "?epic=", + "?epic=any", + ]) + expect(epicFilter(`file:///index.html${query}`, ["spec:probe.root"])).toEqual({ + value: "any", + unknown: true, + }); + expect(epicFilter("file:///index.html?epic=spec%3Aprobe.root", ["spec:probe.root"])).toEqual({ + value: "spec:probe.root", + unknown: false, + }); + expect(epicFilter("file:///index.html?epic=none", [])).toEqual({ + value: "none", + unknown: false, + }); + expect(epicFilter("file:///index.html", [])).toEqual({ value: "any", unknown: false }); + }); +}); diff --git a/test/studio-data.test.ts b/test/studio-data.test.ts index 4fe1f0b2c..21ea7e647 100644 --- a/test/studio-data.test.ts +++ b/test/studio-data.test.ts @@ -1,10 +1,11 @@ +import { graphReportId } from "../src/validate/validators.js"; import { reifyMarkdownCarrier } from "../src/extract/markdown.js"; import { deriveGraph } from "../src/extract/derive.js"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it } from "vitest"; import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; import { extract } from "../src/extract/index.js"; @@ -32,7 +33,8 @@ const root = fileURLToPath(new URL("..", import.meta.url)); const corpora = [ { name: "Protocol", root, exclude: ["explorations", "examples", "test/fixtures/import/parity"] }, { name: "checkout", root: join(root, "examples/checkout-v1"), exclude: [] }, -].map((corpus) => ({ ...corpus, extracted: extract(corpus) })); +]; +type Corpus = (typeof corpora)[number] & { extracted: ReturnType<typeof extract> }; function required<T>(value: T | null | undefined): T { if (value === null || value === undefined) throw new Error("Expected a value"); @@ -49,7 +51,7 @@ function recipeBody(ordinal: number): string { async function recipe<T>( ordinal: number, - corpus: (typeof corpora)[number], + corpus: Corpus, params: Record<string, unknown>, ): Promise<T> { const capture = createCaptureOutput(); @@ -71,12 +73,18 @@ async function recipe<T>( return JSON.parse(capture.readStdout()) as T; } -describe.each(corpora)("Studio data over $name", (corpus) => { - const reader = createReader(corpus.extracted.graph); +describe.each(corpora)("Studio data over $name", (subject) => { + let corpus: Corpus; + let reader: ReturnType<typeof createReader>; + let data: Awaited<ReturnType<typeof createStudioData>>; + beforeAll(async () => { + corpus = { ...subject, extracted: extract(subject) }; + reader = createReader(corpus.extracted.graph); + data = await createStudioData(reader); + }, 30_000); it("counts every Pack exactly as recipe 29", async () => { expect(reader.packs().length).toBeGreaterThan(0); - const data = await createStudioData(reader); for (const pack of reader.packs()) { const answer = await recipe<{ totals: unknown }>(29, corpus, { pack: pack.id }); expect(data.packs.find((p) => p.context.id === pack.id)?.totals).toEqual(answer.totals); @@ -150,7 +158,7 @@ describe.each(corpora)("Studio data over $name", (corpus) => { }, ); - it("keeps rails in Pack-id and authored-member order, with severity and space ownership", async () => { + it("keeps rails in Pack-id and authored-member order, with severity and space ownership", () => { const rails = railValues(reader); expect(rails.packs.map((pack) => pack.id)).toEqual( reader @@ -171,7 +179,6 @@ describe.each(corpora)("Studio data over $name", (corpus) => { errors: reader.findings().filter((finding) => finding.severity === "error").length, warnings: reader.findings().filter((finding) => finding.severity === "warning").length, }); - const data = await createStudioData(reader); expect(data.specs).toEqual(reader.specs().map((spec) => reader.specContext(spec.id))); expect(data.findings).toEqual(reader.findings()); expect(data.packs.map((pack) => pack.context)).toEqual( @@ -346,3 +353,24 @@ relations: {} ); } }); + +it("shares only parameterless recipe answers within one Studio evaluation", async () => { + const { evaluateBodyWithReader } = await import("../src/reader/evaluate-body.js"); + const reader = createReader({ schemaVersion, nodes: [], edges: [], locations: [] }); + const input = { + reader, + graph: reader.graph, + report: { validatorId: graphReportId, findings: [] }, + memoizeParameterlessRecipes: true, + }; + const body = "return [await recipes(20), await recipes(20, {}), await recipes(20, {scope:[]})];"; + const first = (await evaluateBodyWithReader(body, input)) as unknown[]; + const second = (await evaluateBodyWithReader(body, input)) as unknown[]; + expect(first[0]).toBe(first[1]); + expect(first[0]).not.toBe(first[2]); + expect(first[0]).not.toBe(second[0]); + expect(Object.isFrozen(first[0])).toBe(true); + const answer = first[0] as { questions: unknown[] }; + expect(Object.isFrozen(answer.questions)).toBe(true); + expect(() => answer.questions.push("changed")).toThrow(TypeError); +}); diff --git a/test/studio-diagram-catalog.test.ts b/test/studio-diagram-catalog.test.ts new file mode 100644 index 000000000..14f814e06 --- /dev/null +++ b/test/studio-diagram-catalog.test.ts @@ -0,0 +1,150 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { extract } from "../src/extract/index.js"; +import { createReader } from "../src/reader/reader.js"; +import { ref, specTest, testAnchorId } from "../src/anchors.js"; +import { createStudioData } from "../src/projections/studio/data.js"; +import { renderStudio } from "./helpers/studio-render.js"; + +const catalog = specTest({ + id: testAnchorId("test:protocol.studio-diagram-catalog"), + verifies: ref("spec:consumers.spec-studio.diagram-kinds"), +}); +void catalog; + +describe("the shipped diagram catalog", () => { + let context: ReturnType<ReturnType<typeof createReader>["specContext"]>; + beforeAll(() => { + const graph = extract({ + root: fileURLToPath(new URL("..", import.meta.url)), + exclude: ["explorations", "examples", "test/fixtures/import/parity"], + }).graph; + context = createReader(graph).specContext("spec:consumers.spec-studio.diagram-kinds"); + }, 30_000); + it("keeps the complete drawn catalog and every other family deferred or refused", () => { + const design = context?.sections?.design ?? {}; + const choices = Object.entries(design) + .filter(([, value]) => /^(Draw|Equivalent|Defer|Refuse)\b/u.test(String(value))) + .map( + ([key, value]) => + [key, /^(Draw|Equivalent|Defer|Refuse)\b/u.exec(String(value))?.[1]] as const, + ); + expect(Object.fromEntries(choices)).toEqual({ + flowchart: "Draw", + mindmap: "Draw", + treeView: "Equivalent", + block: "Draw", + architecture: "Equivalent", + usesTable: "Draw", + requirement: "Draw", + class: "Defer", + entityRelationship: "Refuse", + sequence: "Refuse", + zenUml: "Refuse", + state: "Refuse", + journey: "Refuse", + gantt: "Refuse", + pie: "Defer", + quadrant: "Refuse", + gitGraph: "Refuse", + c4: "Refuse", + timeline: "Refuse", + sankey: "Refuse", + xyChart: "Defer", + packet: "Refuse", + kanban: "Defer", + radar: "Refuse", + treemap: "Defer", + swimlanes: "Refuse", + useCase: "Refuse", + eventModeling: "Refuse", + venn: "Defer", + ishikawa: "Refuse", + wardley: "Refuse", + cynefin: "Refuse", + agentFlow: "Refuse", + railroad: "Refuse", + }); + for (const [key, choice] of choices) { + if (choice === "Defer" || choice === "Refuse") expect(design[key]).toMatch(/Re-enter/u); + } + expect(design.diagnostics).toContain("not corpus diagram kinds"); + }); + + let root: string; + let before: string; + let after: string; + let pages: ReturnType<typeof renderStudio>; + let repeated: ReturnType<typeof renderStudio>; + beforeAll(async () => { + root = mkdtempSync(join(tmpdir(), "sdp-catalog-")); + + const spec = (id: string, altitude: string, relations: string) => + `---\nid: spec:probe.${id}\nkind: behavior\naltitude: ${altitude}\nreadiness: defined\nrelations: ${relations}\n---\n# ${id}\n\n## Intent\n- outcome: Read ${id}.\n\n## Behavior\n- rule: Keep its graph identity.\n`; + writeFileSync(join(root, "root.sdp.md"), spec("root", "epic", "{}")); + writeFileSync( + join(root, "part.sdp.md"), + spec("part", "story", "{refines: spec:probe.root, dependsOn: spec:probe.root}"), + ); + writeFileSync( + join(root, "pack.pack.sdp.md"), + "---\nid: pack:probe\norder: priority\nspecs: [spec:probe.root, spec:probe.part]\n---\n# Probe\n", + ); + writeFileSync( + join(root, "bindings.ts"), + `/**\n * @sdpAnchor component:probe.one\n * @sdpSatisfies spec:probe.root\n * @sdpUses component:probe.two\n */\nexport const one=1;\n/**\n * @sdpAnchor component:probe.two\n * @sdpSatisfies spec:probe.part\n */\nexport const two=2;\n`, + ); + const graph = extract({ root }).graph; + before = JSON.stringify(graph); + const data = await createStudioData(createReader(graph)); + pages = renderStudio(data); + repeated = renderStudio(data); + after = JSON.stringify(graph); + }, 30_000); + afterAll(() => { + if (root) rmSync(root, { recursive: true, force: true }); + }); + it("renders the six promised readings from explicit graph values without adding other forms", () => { + const page = (path: string) => pages.find((row) => row.path === path)?.content ?? ""; + const slice = page("slices/probe.html"); + for (const kind of ["dependencies", "architecture"]) + expect(slice).toContain(`id="diagram.pack:probe.${kind}.text"`); + expect(page("capabilities.html")).toContain(".capability.text"); + expect(page("packs/probe.html")).toContain('id="diagram.pack:probe.maturity.text"'); + // spec:consumers.spec-studio.diagram-kinds#design.usesTable + const uses = /<section><h2>Component uses<\/h2>[\s\S]*?<\/section>/u.exec( + page("architecture.html"), + )?.[0]; + expect(uses).toBeDefined(); + expect(uses).toContain('class="component-uses"'); + expect(uses).not.toContain("<textarea"); + expect(page("specs/probe.root.html")).toContain( + 'id="diagram.spec:probe.root.requirement.text"', + ); + // spec:consumers.spec-studio.diagram-kinds#rule.drawnKinds + const specPage = page("specs/probe.root.html"); + const designView = + specPage.split('id="panel.design-view"')[1]?.split('id="panel.runtime"')[0] ?? ""; + const specDrawingIds = [...designView.matchAll(/\bid="(diagram\.[^"]+)"/gu)].map( + (match) => match[1], + ); + expect(specDrawingIds.length).toBeGreaterThan(0); + for (const id of specDrawingIds) + expect(id).toMatch(/^diagram\.spec:probe\.root\.requirement\./u); + expect(specPage).not.toContain(".realization."); + const impact = specPage.split('id="panel.impact"')[1] ?? ""; + expect(impact).toContain('id="diagram.spec:probe.root.text"'); + const html = pages.map((row) => row.content).join("\n"); + const sources = [...html.matchAll(/<textarea[^>]*>([\s\S]*?)<\/textarea>/gu)].map( + (match) => match[1]?.trim().split("\n")[0], + ); + expect(sources.length).toBeGreaterThan(0); + for (const firstLine of sources) + expect(firstLine).toMatch(/^(flowchart (TB|BT)|mindmap|block-beta|requirementDiagram)$/u); + expect(after).toBe(before); + expect(repeated).toEqual(pages); + }); +}); diff --git a/test/studio-diagram-core.test.ts b/test/studio-diagram-core.test.ts new file mode 100644 index 000000000..85f5a8126 --- /dev/null +++ b/test/studio-diagram-core.test.ts @@ -0,0 +1,738 @@ +import { requirementDiagram } from "../src/projections/studio/diagrams/requirement.js"; +import { beforeAll, describe, expect, it, vi } from "vitest"; +import { specTest, testAnchorId, ref } from "../src/anchors.js"; +import { labelLines, shortIds, layoutDiagram } from "../src/projections/studio/diagrams/layout.js"; +import { routeDiagram } from "../src/projections/studio/diagrams/routing.js"; +import { renderDiagram } from "../src/projections/studio/diagrams/svg.js"; +import { mermaidSource } from "../src/projections/studio/diagrams/mermaid.js"; +import type { Diagram } from "../src/projections/studio/diagrams/model.js"; + +function required<T>(value: T | undefined): T { + if (value === undefined) throw new Error("Missing test value"); + return value; +} + +const drawing: Diagram = { + subject: "spec:probe", + caption: "Probe", + boxes: Array.from({ length: 6 }, (_, i) => ({ + id: `spec:probe.item${String(i)}`, + words: `Title ${String(i)}`, + target: `#item${String(i)}`, + band: i < 3 ? "First" : "Second", + })), + edges: [ + { + from: "spec:probe.item0", + to: "spec:probe.item4", + kind: "dependsOn", + claim: "declared", + reason: "Needs its contract", + }, + { from: "spec:probe.item0", to: "spec:probe.item5", kind: "refines", claim: "declared" }, + { from: "spec:probe.item4", to: "spec:probe.item0", kind: "verifies", claim: "anchored" }, + { from: "spec:probe.item1", to: "spec:probe.item2", kind: "uses", claim: "anchored" }, + ], +}; +const core = specTest({ + id: testAnchorId("test:protocol.studio-diagram-core"), + verifies: [ + ref("spec:consumers.spec-studio.diagram-kinds.legibility"), + ref("spec:consumers.spec-studio.diagram-kinds.edge-routing"), + ref("spec:consumers.spec-studio.diagram-kinds.mermaid-source"), + ref("spec:consumers.spec-studio.diagram-kinds.requirement-trace"), + ref("spec:consumers.spec-studio.diagrams"), + ], +}); +void core; +describe("shared diagram core", () => { + it("wraps by code point and shortens shared segments without collisions", () => { + expect(labelLines("😀".repeat(31))).toEqual(["😀".repeat(30), "😀"]); + expect(shortIds(["spec:probe.a.one", "spec:probe.a.two"])).toEqual( + new Map([ + ["spec:probe.a.one", "…one"], + ["spec:probe.a.two", "…two"], + ]), + ); + const ids = ["spec:probe.same", "impl:probe.same", "spec:other.same"]; + expect(new Set(shortIds(ids).values()).size).toBe(3); + const html = renderDiagram(drawing); + expect(html).toContain('<tspan class="box-title"'); + expect(html).toContain('textLength="56" lengthAdjust="spacingAndGlyphs">Title 0'); + expect(html.indexOf(">Title 0</tspan>")).toBeLessThan(html.indexOf(">…item0</tspan>")); + expect(html).toContain("<title>spec:probe.item0"); + expect(html).toContain('tabindex="0" role="region"'); + }); + it("wraps boxes within the width budget and writes an instruction only for final wide drawings", () => { + const l = layoutDiagram({ ...drawing, edges: [] }); + expect(l.width).toBeLessThanOrEqual(880); + expect(renderDiagram({ ...drawing, edges: [] })).not.toContain('class="scroll-help"'); + const wide = { + ...drawing, + grid: true, + boxes: drawing.boxes.map((b, i) => ({ + ...b, + context: `context${String(i)}`, + layer: "domain", + })), + }; + expect(renderDiagram(wide)).toContain("Scroll horizontally to read the whole drawing."); + }); + it("routes forward, backward and same-band edges with distinct ports and no shared segments", () => { + const layout = layoutDiagram(drawing); + const routed = routeDiagram(layout, drawing.edges); + expect(routed).not.toBeNull(); + if (!routed) throw new Error("No routes"); + expect(routed.routes).toHaveLength(4); + assertGeometry(layout, routed); + const ports = routed.routes + .flatMap((r) => [r.points[0], r.points.at(-1)]) + .map((p) => JSON.stringify(p)); + expect(new Set(ports).size).toBe(ports.length); + for (const route of routed.routes) { + expect(route.points.every((p) => Number.isInteger(p.x) && Number.isInteger(p.y))).toBe(true); + for (let i = 1; i < route.points.length; i++) { + const a = required(route.points[i - 1]), + b = required(route.points[i]); + expect(a.x === b.x || a.y === b.y).toBe(true); + for (const other of routed.routes) + if (other !== route) + for (let j = 1; j < other.points.length; j++) { + const c = required(other.points[j - 1]), + e = required(other.points[j]); + if (a.x === b.x && c.x === e.x && a.x === c.x) + expect( + Math.min(Math.max(a.y, b.y), Math.max(c.y, e.y)) - + Math.max(Math.min(a.y, b.y), Math.min(c.y, e.y)), + ).toBeLessThanOrEqual(0); + if (a.y === b.y && c.y === e.y && a.y === c.y) + expect( + Math.min(Math.max(a.x, b.x), Math.max(c.x, e.x)) - + Math.max(Math.min(a.x, b.x), Math.min(c.x, e.x)), + ).toBeLessThanOrEqual(0); + } + } + } + expect(routeDiagram(layout, drawing.edges)).toEqual(routed); + expect(renderDiagram(drawing)).toBe(renderDiagram(drawing)); + }); + it("fails closed when routing cannot fit, retaining complete text and no partial source", () => { + expect(routeDiagram(layoutDiagram(drawing), drawing.edges, 0)).toBeNull(); + const starBoxes = Array.from({ length: 40 }, (_, i) => ({ + id: `component:star.${String(i)}`, + words: `Unit ${String(i)}`, + target: "#main", + context: "star", + layer: "domain", + })); + const star = { + subject: "star", + caption: "Star", + grid: true, + boxes: starBoxes, + edges: starBoxes.slice(1).map((b) => ({ + from: required(starBoxes[0]).id, + to: b.id, + kind: "uses", + claim: "anchored", + })), + }; + const starLayout = layoutDiagram(star), + starRoutes = routeDiagram(starLayout, star.edges); + expect(starRoutes?.routes).toHaveLength(39); + expect(renderDiagram(star)).toContain(" { + const d = { + ...drawing, + boxes: [ + { + ...required(drawing.boxes[0]), + words: '`code` "#