From b17637a1eed0d0b64cf3a69341a8d46ccf87cdd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:08:34 +0200 Subject: [PATCH 01/33] plans: plan 45, design at the adopter's scale The next chunk unlocks the Protocol for the first adopter's design work: installing from a commit, a design-pass loop that fits a large corpus, corpus checks and planned architecture, and the Spec Studio's phase 0, with plan 42 as that lane's brief. The plan records the rulings made at its start on the owner's delegation. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 15 ++-- plans/42-spec-studio-phase-zero.md | 4 +- plans/45-design-at-the-adopters-scale.md | 87 ++++++++++++++++++++++++ test/check-self-hosting-gates.test.ts | 8 +-- 4 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 plans/45-design-at-the-adopters-scale.md diff --git a/AGENTS.md b/AGENTS.md index 4c5c1008..0401ee47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,10 +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 44 is EXECUTED** on its branch. A whole-corpus refresh: every Spec read against the +> **plan 45 is EXECUTING** on `feature/design-at-scale-v0`. Design at the adopter's scale: the +> adopter installs from a commit, the design-pass loop fits a large corpus, corpus checks and +> planned architecture get their design and build, and the Spec Studio's phase 0 (plan 42 is its +> brief) is designed and built. The plan lists its lanes and the rulings made at its start. +> **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 42 follows it. +> 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 @@ -27,9 +31,10 @@ code behavior into intent. > 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 DRAFTED**. The Spec Studio's phase 0, a read-only static view (`sdp studio`), -> re-entered on the owner's rulings; intent in `pack:spec-studio-v1`. A design pass splits the -> Studio's Specs for honest binding and prototypes the Pack page before any code. +> **plan 42 is DRAFTED**, and briefs the Studio lane above. The Spec Studio's phase 0, a +> read-only static view (`sdp studio`), re-entered on the owner's rulings; intent in +> `pack:spec-studio-v1`. A design pass splits the Studio's Specs for honest binding and +> prototypes the Pack page before any code. > **plan 41 is EXECUTED**. The design-management arc, intent in `pack:design-management-v1`: the > Design Review's Pack page shows a Pack's design (the Pack design page, MD-39), the graph > records entry lines, recipes take `--params` and ship under `dist/recipes/`, recipes 29 to 32 diff --git a/plans/42-spec-studio-phase-zero.md b/plans/42-spec-studio-phase-zero.md index 4c35ee1f..a0e62005 100644 --- a/plans/42-spec-studio-phase-zero.md +++ b/plans/42-spec-studio-phase-zero.md @@ -1,7 +1,7 @@ # Plan 42. The Spec Studio, phase 0: a read-only static view -> **Status:** DRAFTED after plan 41's fold, merged to `main` with PR 30; it is executed on its own -> branch from `main` once PR 31 merges. A thin lineage +> **Status:** DRAFTED after plan 41's fold, merged to `main` with PR 30; it executes as plan 45's +> lane S, on that plan's branch. A thin lineage > pointer: the Studio's intent lives in `pack:spec-studio-v1`, and its readiness, open questions and > backlog are read from the graph (recipes 1, 9, 11, 20, 29), never from this file. If this file and > the graph disagree, the graph wins and this file is stale. diff --git a/plans/45-design-at-the-adopters-scale.md b/plans/45-design-at-the-adopters-scale.md new file mode 100644 index 00000000..0fe551cd --- /dev/null +++ b/plans/45-design-at-the-adopters-scale.md @@ -0,0 +1,87 @@ +# Plan 45. Design at the adopter's scale + +> **Status:** EXECUTING on `feature/design-at-scale-v0`, from `main` at `7eef29b`, the merge of +> PR 32. A thin lineage pointer: readiness, backlog and open questions are read from the graph +> (recipes 1, 9, 11, 20, 29, 33), never from this file. If this file and the graph disagree, the +> graph wins and this file is stale. + +## Why + +The first adopter, libar-platform, designs a Convex transactional domain platform as a corpus of +about 240 Specs in six Packs. It is a large design, and most of it will stay design for months: +layers 3 to 6 exist only on paper. On 2026-10-13 the owner asked for the next chunk to unlock as +much of the Protocol as possible for that design work, without machinery no reader needs. + +A read of the platform, its feedback and the Protocol's backlog found four needs, in order: + +1. **Reach `main` cheaply.** The platform pins the Protocol at `822aea4`, a vendored tarball from + before PRs 30 to 32. About fifteen of its asks are delivered on `main` and unusable at its pin. + Main's engine validates its corpus identically and passes its tests; one script must change. +2. **Design passes that fit a model's budget.** The context bundle (recipe 36) overflows its + default budget on every platform Pack: most of the cut bundle is repeated locations and + pinned declarations carried twice. +3. **A page a person can read.** The owner wants to present designs ahead of their code, and the + platform's reviewers read carrier files because the Markdown Pack page is a 13-column table. + This is the Spec Studio's phase 0 (plan 42). +4. **The platform's own rules inside `sdp validate`.** The platform's gate (`design/tools/check.py`) + parses carriers to check citation edges, isolation and readiness policy. Corpus rules and + architecture constraints give those checks a Protocol home; planned architecture lets layers 3 + to 6 state their components before their code. + +## The lanes + +| Lane | Delivers | Kind | Models | +| --- | --- | --- | --- | +| **P** the adopter on-ramp | Install from a commit; `bindExample` runs `after` in a `finally`; `check-carrier-rule.mjs` wired or deleted; an adopter migration note | build | gpt-6.1-sol, reviewed by Opus 5.5 | +| **L** the loop at scale | A compact context bundle; `declarations: "addresses"`; recipe 41, the key-family register; a diagnostic for a parameter a recipe does not take | design, then build | Opus 5.5 designs, gpt-6.1-sol builds, Opus 5.5 reviews | +| **B** corpus checks | Design: keyed Design entries for corpus rules, architecture constraints and planned architecture, and `pack:corpus-checks-v1`. Build: `sdp validate --rules`, recipe 42 (the architecture walk), recipe 43 (planned against realized components) | design, then build | Opus 5.5 designs, gpt-6-sol reviews; gpt-6.1-sol builds | +| **S** the Studio's phase 0 | Plan 42's design pass (child Specs, the phase-0 questions, a Pack-page prototype, the data and element contracts), then its lanes S1 to S3 | design, then build | Opus 5.5 designs, Fable 5.1 and gpt-6-sol review; gpt-6.1-sol builds | +| **X** integration and close | Recipe catalog, self-hosting oracle, skills, `CONTEXT.md`, help text; an adversarial review; the Studio's acceptance on the platform; the PR | close | gpt-6.1-sol, gpt-6-sol, gpt-6-astra | + +Wave 1 runs P, L and the design passes of B and S in parallel worktrees. The main thread then +rules on what the design passes recommend and states the `ready` the builds start from. Wave 2 +builds B and S. Wave 3 integrates, reviews and measures. Recipe numbers are pre-assigned so lanes +do not collide: 41 to L, 42 and 43 to B. + +The platform's re-pin is the platform's own work. This chunk prepares it as a branch in that +repository, with the migration the read found, for the owner to merge. + +## Rulings at the start + +Made by the main thread on the owner's delegation, before the lanes; each awaits the owner's +review. + +| Ruling | Carrying Spec | +| --- | --- | +| One branch and one PR for the chunk; plan 42 is lane S's brief | — | +| An adopter installs from a commit through a `prepare` script that builds the package; no npm prerelease while the version is `0.0.0` | `spec:consumers.adopter-on-ramp` | +| The context bundle may cut pinned declarations to entry addresses, opt-in through `params.declarations`; a partial bundle stays refused | `spec:consumers.context-bundle` | +| The key-family register is one recipe and needs no model change | `spec:consumers.agent-surface.register-recipes` | +| A parameter name a recipe does not take is reported in its answer, never ignored | `spec:consumers.agent-surface.recipe-parameters` | +| A corpus-checks Pack holds corpus rules, architecture constraints and planned architecture | `spec:consumers.graph-first-planning` | +| The provisional answers on corpus rules and architecture constraints stand | `spec:validation.corpus-rules`, `spec:validation.architecture-constraints` | +| Choosing a provider stays prose; one runtime owner per concern is a corpus's own rule, not a Protocol shape | `spec:model.planned-architecture` | +| The provisional answers on the Studio's Specs stand, `spec-page#sourceLinks` among them; phase 0 has no AI slices | `pack:spec-studio-v1` | +| The vault is its own projection, not the Studio's local half, and waits for a reader | `spec:consumers.obsidian-vault` | + +## Deferred, with their triggers + +| Item | Comes back when | +| --- | --- | +| Run evidence | The platform's CI writes CTRF for its native tier, or the Studio's verification panels open | +| Example-space coverage | A design pass asks which slot values no example exercises | +| The Obsidian vault | A reader asks for it after the Studio's phase 0 ships | +| Lineage roots and checked external references | The owner rules what of the earlier system the platform may cite | +| A readiness-statement planner | The owner cannot order the platform's `ready` statements with recipes 9, 21, 29 and 30 | +| Wording-stable step keys | Rewording breaks bound examples during the platform's next slices | + +## Acceptance + +- `npm run check` passes; the Markdown Design Review, census, Mermaid and Gherkin projections are + unchanged. +- The platform's history-view Pack fits the context bundle's default budget with + `declarations: "addresses"`, measured read-only on a copy of the platform. +- The graph-side half of the platform's `check.py` runs as rules files under + `sdp validate --rules`, on a copy of the platform. +- An Astra job given only the Studio pages built from the platform, and the files they link to, + completes plan 42's five reader tasks, with the two associations applied on its copy. diff --git a/test/check-self-hosting-gates.test.ts b/test/check-self-hosting-gates.test.ts index e5fd9f22..e2d9394e 100644 --- a/test/check-self-hosting-gates.test.ts +++ b/test/check-self-hosting-gates.test.ts @@ -76,11 +76,11 @@ afterEach(() => { }); describe("the self-hosting records gate", () => { - it("discovers plan 44 as the current primary plan", () => { - // re-measured after plan 44's record commit + it("discovers plan 45 as the current primary plan", () => { + // re-measured after plan 45's opening commit expect(currentPlan).toMatchObject({ - number: 44, - name: "44-whole-corpus-refresh.md", + number: 45, + name: "45-design-at-the-adopters-scale.md", }); }); From b01dc0e6a4d6488d7332135f642cc0628e4caffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:16:32 +0200 Subject: [PATCH 02/33] plans: plan 45 moves the platform onto the chunk's head as lane E Co-Authored-By: Claude Opus 5.5 --- plans/45-design-at-the-adopters-scale.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plans/45-design-at-the-adopters-scale.md b/plans/45-design-at-the-adopters-scale.md index 0fe551cd..21f9f7d5 100644 --- a/plans/45-design-at-the-adopters-scale.md +++ b/plans/45-design-at-the-adopters-scale.md @@ -36,6 +36,7 @@ A read of the platform, its feedback and the Protocol's backlog found four needs | **L** the loop at scale | A compact context bundle; `declarations: "addresses"`; recipe 41, the key-family register; a diagnostic for a parameter a recipe does not take | design, then build | Opus 5.5 designs, gpt-6.1-sol builds, Opus 5.5 reviews | | **B** corpus checks | Design: keyed Design entries for corpus rules, architecture constraints and planned architecture, and `pack:corpus-checks-v1`. Build: `sdp validate --rules`, recipe 42 (the architecture walk), recipe 43 (planned against realized components) | design, then build | Opus 5.5 designs, gpt-6-sol reviews; gpt-6.1-sol builds | | **S** the Studio's phase 0 | Plan 42's design pass (child Specs, the phase-0 questions, a Pack-page prototype, the data and element contracts), then its lanes S1 to S3 | design, then build | Opus 5.5 designs, Fable 5.1 and gpt-6-sol review; gpt-6.1-sol builds | +| **E** the platform on `main` | In libar-platform: the re-pin, its migration, the tooling the Protocol now replaces, and the two associations the Studio's reader tasks need | adopter build | gpt-6.1-sol, reviewed by Opus 5.5 | | **X** integration and close | Recipe catalog, self-hosting oracle, skills, `CONTEXT.md`, help text; an adversarial review; the Studio's acceptance on the platform; the PR | close | gpt-6.1-sol, gpt-6-sol, gpt-6-astra | Wave 1 runs P, L and the design passes of B and S in parallel worktrees. The main thread then @@ -43,8 +44,12 @@ rules on what the design passes recommend and states the `ready` the builds star builds B and S. Wave 3 integrates, reviews and measures. Recipe numbers are pre-assigned so lanes do not collide: 41 to L, 42 and 43 to B. -The platform's re-pin is the platform's own work. This chunk prepares it as a branch in that -repository, with the migration the read found, for the owner to merge. +When the platform needs a capability, the Protocol builds it; the platform does not work around +it. Lane **E** then moves the platform onto the chunk's head in its own repository, on the owner's +word of 2026-10-13: install from the commit, apply the migration the read found, and retire the +platform's home-grown tooling where the Protocol now serves (the graph-side half of `check.py` as +rules files, recipe bodies cut from the catalog by regular expression, the hand-built Pack page). +A gap E finds comes back to the Protocol as a fix in this chunk. ## Rulings at the start From 99caecf95da739ca16cd6cdd95cbc772f49f6d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:21:29 +0200 Subject: [PATCH 03/33] feat(adopter): install commit pins and clean up failed examples --- .github/workflows/ci.yml | 6 +- README.md | 61 ++++++++++++- check-carrier-rule.mjs | 115 ------------------------- check-git-install.mjs | 95 ++++++++++++++++++++ package.json | 2 + specs/consumers/adopter-on-ramp.sdp.md | 3 + specs/extraction/example-runner.sdp.md | 1 + src/adapters/vitest.ts | 14 ++- test/package-smoke.test.ts | 21 ++--- test/vitest-adapter.test.ts | 106 +++++++++++++++++++++++ 10 files changed, 291 insertions(+), 133 deletions(-) delete mode 100644 check-carrier-rule.mjs create mode 100644 check-git-install.mjs create mode 100644 test/vitest-adapter.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e323da41..0f9048d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,11 @@ jobs: cache: npm - name: Install dependencies - run: npm ci + # The check gate builds the package. Skip prepare here to avoid building it twice. + run: npm ci --ignore-scripts - name: Check run: npm run check + + - name: Install the committed package + run: npm run test:git-install diff --git a/README.md b/README.md index 629fdb35..a51a0bd7 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,10 @@ Read the [concept](docs/concept/README.md), the [ubiquitous language](CONTEXT.md ## Source-checkout quick start -Install from the committed lockfile and build the CLI: +Install from the committed lockfile. The `prepare` script builds the CLI: ```sh npm ci -npm run build ``` Then query this repository's self-hosting graph with either supported script runner: @@ -53,6 +52,44 @@ disables pnpm 11's dependency auto-reconciliation for repository scripts, so the ## Installed CLI (adopter repositories) +Pin the Protocol to a full commit SHA in your adopter's `package.json`: + +```json +{ + "dependencies": { + "@libar-dev/software-delivery-protocol": "github:libar-dev/software-delivery-protocol#" + } +} +``` + +Replace `` with the commit you reviewed, then run `npm install` and commit the lockfile. +The package's `prepare` script builds `dist/`, the CLI, subpath exports and shipped recipes during +the git install. npm installs the build dependencies for this step. While the version is `0.0.0`, +use a commit pin; no npm prerelease is published. See [npm's prepare lifecycle](https://docs.npmjs.com/cli/v11/using-npm/scripts/). + +pnpm 10 blocks dependency build scripts unless allowed. Before `pnpm install`, add the Protocol to +your adopter's `pnpm-workspace.yaml`: + +```yaml +onlyBuiltDependencies: + - "@libar-dev/software-delivery-protocol" +``` + +On pnpm 10.26 or later, including pnpm 11, use `allowBuilds` instead of `onlyBuiltDependencies`: + +```yaml +allowBuilds: + "@libar-dev/software-delivery-protocol": true +``` + +Keep this alongside your existing build permissions. The permission lets pnpm prepare the git +dependency. See the [pnpm 10 build settings](https://pnpm.io/10.x/settings#allowbuilds). +Verify either install through the linked binary: + +```sh +./node_modules/.bin/sdp --version +``` + In an adopter repository where the Protocol is installed as a dependency, its binary **is** linked into `node_modules/.bin`, so the package runner resolves it: @@ -71,6 +108,26 @@ carrier, `import` converts TypeScript Spec carriers to Markdown, and `q` evaluat JavaScript query body against a freshly derived graph. Run `sdp --help` for the complete option contract. Adopters own their root and exclusion policy. +### Upgrade recipe callers + +Recipe 25, address resolution, now reads `params.addresses`. A script that replaces the recipe's +sample address array with a regular expression can silently stop matching. Remove that rewrite +and pass the subject as data to the shipped recipe: + +```sh +./node_modules/.bin/sdp q "$(cat node_modules/@libar-dev/software-delivery-protocol/dist/recipes/25-address-resolution.js)" --params '{"addresses":["spec:orders.create-order#rule.validCart"]}' --json +``` + +Use your corpus's addresses, or pass `--params @addresses.json` with an object containing +`addresses`. Other parameterized recipes follow the same pattern. The +[recipe catalog](docs/agent-surface/recipes.md) names each parameter. + +In the Protocol source checkout, `npm run test:git-install -- ` proves a committed revision +installs through a local git URL with npm and, when available, pnpm. It checks the installed CLI, +export files and a parameterized shipped recipe. This separate gate downloads dependencies and +builds the committed package, so it runs after the unit suite in CI. It does not test uncommitted +edits. An optional second argument selects the parent directory for its temporary projects. + ## Gherkin carrier Behavior and example Specs may use a `.sdp.gherkin` file as their one canonical surface. diff --git a/check-carrier-rule.mjs b/check-carrier-rule.mjs deleted file mode 100644 index 60b8b1cd..00000000 --- a/check-carrier-rule.mjs +++ /dev/null @@ -1,115 +0,0 @@ -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -// check-carrier-rule — the structured consistency gate for the canonical-default carrier rule -// (the carrier ruling, MD-18). -// -// Asserts, and NAMES each disagreeing surface on failure: -// 1. the SAME post-flip rule sentence in the three current-rule surfaces (the decision diary, -// the glossary, the agent handbook): Specs default to Markdown; Packs remain TS until a -// Pack syntax ruling; the TS DSL survives as import source and a lawful per-ID option; -// 2. the obsolete sole-canonical-TS wording gone from the diary and the glossary, and the -// surviving law (one canonical surface per ID, no mixing) still present in the diary; -// 3. the extension-law re-point (the `.sdp.ts` extension law, MD-15) present in the diary; -// 4. the logical/physical relations distinction present on BOTH surviving surfaces, each in its -// own words — the carrying Spec (the Markdown envelope contract) and JS-A1: relations stay -// OPTIONAL in the logical Spec; the physical Markdown envelope writes `relations: {}` when the -// logical set is empty — honest carrier syntax, never a new logical relation requirement. The -// concept doc that used to hold the JS-A1-verbatim twin dissolved into its carrying Specs, so -// this leg proves co-presence of the law, not verbatim sameness across the two surfaces: -// JS-A1 keeps the verbatim sentence, the Spec states the same law as it authored it. -// -// Usage: node check-carrier-rule.mjs [rootDir] -// rootDir — tree holding the operative surfaces; defaults to this repo root. QA passes a -// temp tree of mutated copies to prove disagreeing surfaces are named. - -const rootDir = process.argv[2] ?? dirname(fileURLToPath(import.meta.url)); - -const SURFACES = { - decisions: "docs/concept/DECISIONS.md", - glossary: "CONTEXT.md", - agents: "AGENTS.md", - envelopeContract: "specs/carrier/envelope-contract.sdp.md", - jsA1: "jtbd-stories/01-capture-and-evolve-intent.md", -}; - -// The canonical-default rule. Verbatim (modulo whitespace) in the diary, the glossary, and the -// agent handbook — never paraphrased per surface. The trailing period is surface-local -// punctuation (the AGENTS status row continues with its `·` separator), so it is not asserted. -const CARRIER_RULE = - "Specs default to Markdown; Packs remain TS until a Pack syntax ruling; the TS DSL survives as import source and a lawful per-ID option"; - -// The owner-confirmed logical/physical relations distinction. Verbatim in JS-A1. -const RELATIONS_DISTINCTION = - "Relations are optional in the logical `Spec` model. A physical Markdown envelope writes `relations: {}` when the logical set is empty: honest carrier syntax, not a new logical relation requirement."; - -// The same law in the carrying Spec's own authored words. -const ENVELOPE_RELATIONS_CLAUSE = - "`relations: {}` is written explicitly when the logical relation set is empty — honest carrier syntax, not a new logical requirement: the physical key catches a truncated envelope at reification while the model itself stays relation-optional."; - -const SURVIVING_LAW = "one canonical surface per ID, no mixing"; -const OBSOLETE_SOLE_CANONICAL = "stays sole-canonical"; -const EXTENSION_LAW_REPAIR = "re-pointed, not repealed"; - -// Sameness is about WORDS, so markdown quoting furniture is stripped before whitespace -// normalization: a sentence wrapped inside a `>` blockquote (the AGENTS status row) must read -// identically to its plain-paragraph twins. -const norm = (text) => text.replace(/^>\s?/gm, "").replace(/\s+/g, " "); -const read = (rel) => norm(readFileSync(join(rootDir, rel), "utf8")); - -const failures = []; -const expectContains = (surface, needle, why) => { - if (!read(surface).includes(norm(needle))) { - failures.push(`${surface} — ${why}`); - } -}; -const expectOmits = (surface, needle, why) => { - if (read(surface).includes(norm(needle))) { - failures.push(`${surface} — ${why}`); - } -}; - -for (const surface of [SURFACES.decisions, SURFACES.glossary, SURFACES.agents]) { - expectContains(surface, CARRIER_RULE, "missing the canonical-default carrier-rule sentence"); -} -expectContains( - SURFACES.decisions, - SURVIVING_LAW, - "dropped the surviving one-canonical-surface law", -); -expectOmits( - SURFACES.decisions, - OBSOLETE_SOLE_CANONICAL, - "still carries the obsolete sole-canonical-TS wording", -); -expectOmits( - SURFACES.glossary, - OBSOLETE_SOLE_CANONICAL, - "still carries the obsolete sole-canonical-TS wording", -); -expectContains( - SURFACES.decisions, - EXTENSION_LAW_REPAIR, - "missing the extension-law re-point (the `.sdp.ts` extension law, MD-15)", -); -expectContains( - SURFACES.envelopeContract, - ENVELOPE_RELATIONS_CLAUSE, - "missing the logical/physical relations distinction", -); -expectContains( - SURFACES.jsA1, - RELATIONS_DISTINCTION, - "missing the logical/physical relations distinction", -); -if (failures.length > 0) { - console.error("check-carrier-rule — disagreeing surfaces:\n"); - console.error(failures.join("\n")); - process.exit(1); -} - -console.log( - "check-carrier-rule — all surfaces agree: canonical-default carrier rule and relations distinction.", -); -process.exit(0); diff --git a/check-git-install.mjs b/check-git-install.mjs new file mode 100644 index 00000000..7c47c529 --- /dev/null +++ b/check-git-install.mjs @@ -0,0 +1,95 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import assert from "node:assert/strict"; + +// A committed-source install needs network access and dependency builds. Keep it outside the +// pooled unit suite and run it explicitly, or as the final CI step, against the selected commit. +const root = dirname(fileURLToPath(import.meta.url)); +const packageName = "@libar-dev/software-delivery-protocol"; +const revision = process.argv[2] ?? "HEAD"; +const parent = resolve(process.argv[3] ?? tmpdir()); +mkdirSync(parent, { recursive: true }); +const scratch = mkdtempSync(join(parent, "sdp-git-install-")); +const run = (command, args, cwd = root) => + execFileSync(command, args, { + cwd, + encoding: "utf8", + timeout: 300_000, + maxBuffer: 8 * 1024 * 1024, + }); +const commit = run("git", ["rev-parse", "--verify", `${revision}^{commit}`]).trim(); +const dependency = `git+${pathToFileURL(root).href}#${commit}`; + +try { + const managers = ["npm"]; + const pnpm = spawnSync("pnpm", ["--version"], { encoding: "utf8" }); + if (pnpm.status === 0) managers.push("pnpm"); + else console.log("pnpm unavailable; npm install only"); + for (const manager of managers) { + const consumer = join(scratch, manager); + mkdirSync(consumer); + writeFileSync( + join(consumer, "package.json"), + JSON.stringify({ + name: "sdp-git-consumer", + private: true, + type: "module", + dependencies: { [packageName]: dependency }, + }), + ); + if (manager === "pnpm") { + const major = Number(pnpm.stdout.trim().split(".")[0]); + writeFileSync( + join(consumer, "pnpm-workspace.yaml"), + major >= 11 + ? `allowBuilds:\n '${packageName}': true\n` + : `onlyBuiltDependencies:\n - '${packageName}'\n`, + ); + } + const started = Date.now(); + run( + manager, + [ + "install", + ...(manager === "npm" ? ["--no-audit", "--no-fund"] : ["--reporter=append-only"]), + ], + consumer, + ); + const installed = join(consumer, "node_modules", ...packageName.split("/")); + const cli = join(consumer, "node_modules", ".bin", "sdp"); + const version = run(cli, ["--version"], consumer).trim(); + assert.match(version, /0\.0\.0/u); + const manifest = JSON.parse(readFileSync(join(installed, "package.json"), "utf8")); + for (const surface of Object.values(manifest.exports)) { + for (const path of Object.values(surface)) assert.ok(existsSync(join(installed, path)), path); + } + const recipe = readFileSync(join(installed, "dist/recipes/25-address-resolution.js"), "utf8"); + writeFileSync( + join(consumer, "own.sdp.md"), + `---\nid: spec:probe.installed\nkind: behavior\naltitude: story\nreadiness: idea\nrelations: {}\n---\n# Installed corpus\n\n## Intent\n- outcome: Read an installed recipe.\n\n## Behavior\n- rule: [#installed] Resolve this rule.\n`, + ); + const result = JSON.parse( + run( + cli, + [ + "q", + recipe, + "--params", + '{"addresses":["spec:probe.installed#rule.installed"]}', + "--json", + ], + consumer, + ), + ); + assert.equal(result.totals.addresses, 1); + assert.equal(result.totals.resolved, 1); + console.log( + `${manager}: ${version}; exports and shipped recipe work; ${((Date.now() - started) / 1000).toFixed(1)}s; commit ${commit}`, + ); + } +} finally { + rmSync(scratch, { recursive: true, force: true }); +} diff --git a/package.json b/package.json index 3eabbcff..7550cf32 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,11 @@ }, "scripts": { "build": "tsup", + "prepare": "npm run build 1>&2", "typecheck": "tsc --noEmit -p tsconfig.json", "typecheck:examples": "tsc --noEmit -p tsconfig.examples.json", "test": "node ./vitest-test.mjs", + "test:git-install": "node ./check-git-install.mjs", "test:watch": "vitest", "sdp": "node ./dist/cli/sdp.js", "sdp:q": "node ./dist/cli/sdp.js q --exclude explorations --exclude examples --exclude test/fixtures/import/parity", diff --git a/specs/consumers/adopter-on-ramp.sdp.md b/specs/consumers/adopter-on-ramp.sdp.md index 8914a8ef..c9b629f3 100644 --- a/specs/consumers/adopter-on-ramp.sdp.md +++ b/specs/consumers/adopter-on-ramp.sdp.md @@ -24,6 +24,9 @@ The help names the on-ramps by path because an operator can reach the CLI withou - outcome: Teach each ruled home in the shipped skills, one line each with its carrying Spec, so an adopter states those truths in the graph and not beside it. ## Behavior +- rule: [#commitInstall] An adopter can pin the package to a git commit; its `prepare` script builds the CLI, subpath exports and shipped recipes before installation completes. While the package version is `0.0.0`, the installation guidance uses a commit pin rather than an npm prerelease. +- rule: [#installGuidance] The shipped README teaches commit installation with npm and the dependency build permission pnpm needs, and checks the installed CLI with `sdp --version`. +- rule: [#recipeUpgrade] The upgrade guidance teaches callers to pass recipe subjects through `--params` and run the shipped recipe files; address resolution reads `params.addresses`, so rewriting a sample in the recipe body is no longer an input mechanism. - rule: The authoring skill teaches a deferral as a blocking open question whose `trigger:` field names what brings the Spec back, plus `dependsOn` for a true precondition. Its blocking question holds the Spec below `defined` without an adopter-specific tag. Example children may still state `defined` once their bound points are complete and match the parent's example vocabulary when it has one; their `ready` waits until the parent states at least `defined`. - rule: It teaches an unsettled fact as a constraint Spec whose blocking open question names the check that would settle it. Specs bounded by it declare `constrainedBy`; they may state `defined` when their own floor clears, and their `ready` waits until the constraint states at least `defined`. - rule: It teaches a ruling that awaits its owner as a decision Spec below `ready`, with `decidedBy` from each Spec the ruling shapes. diff --git a/specs/extraction/example-runner.sdp.md b/specs/extraction/example-runner.sdp.md index ca03dc7c..443e3305 100644 --- a/specs/extraction/example-runner.sdp.md +++ b/specs/extraction/example-runner.sdp.md @@ -17,6 +17,7 @@ relations: ## Behavior - rule: [#contractOrder] The core plans every contract step in authored order and runs it against the world the caller hands in; creating a fresh world per example is the adapter's lifecycle, never the core's. +- rule: [#worldCleanup] Once the Vitest adapter creates a world, it awaits the optional `after` hook in a `finally` whether the steps pass or fail. A step failure still fails the test when cleanup also fails; a cleanup failure fails a test whose steps passed. A world factory that throws has created no world to clean up. - rule: [#oneHandlerPerStep] Duplicate step text within one example binds one handler, and every occurrence runs it; an occurrence that binds a slot to a different value is a conflicting re-binding, named at generation, and every occurrence runs with the first binding, because an example is one point. - rule: [#redStepNamesItself] A red step names itself before the assertion detail: the failure message leads with the step's natural reading — the Spec's own words with bound values inlined — and the original error is preserved, carried as `cause` when it cannot be re-messaged, and wrapped when the thrown value is not an error. - rule: A missing or stale step handler is a compile-time refusal rather than a silent skip: the bindings type covers every step and only the steps, so spec-side drift fails the typecheck instead of the run. diff --git a/src/adapters/vitest.ts b/src/adapters/vitest.ts index 545615b2..03acf36e 100644 --- a/src/adapters/vitest.ts +++ b/src/adapters/vitest.ts @@ -46,8 +46,18 @@ export function bindExample { // The adapter's lifecycle law: the fresh world is created here, per example. const freshWorld = world(); - await runExamplePlan(plan, freshWorld); - await after?.(freshWorld); + let stepsPassed = false; + try { + await runExamplePlan(plan, freshWorld); + stepsPassed = true; + } finally { + try { + await after?.(freshWorld); + } catch (error) { + // eslint-disable-next-line no-unsafe-finally -- Only a passing step run can be replaced by a cleanup error. + if (stepsPassed) throw error; + } + } }); }); } diff --git a/test/package-smoke.test.ts b/test/package-smoke.test.ts index 59c23230..c085115b 100644 --- a/test/package-smoke.test.ts +++ b/test/package-smoke.test.ts @@ -184,11 +184,13 @@ describe("published package surface", () => { const consumer = join(packageRoot, "consumer"); try { - run("npm", ["run", "build"], repositoryRoot); - const dryRun = JSON.parse( - run("npm", ["pack", "--dry-run", "--json"], repositoryRoot), - ) as readonly [{ readonly files: readonly { readonly path: string }[] }]; - const dryRunPaths = dryRun[0].files.map((file) => file.path); + // npm pack runs prepare. One pack proves the lifecycle and supplies the file inventory. + const packed = JSON.parse( + run("npm", ["pack", "--json", "--pack-destination", packageRoot], repositoryRoot), + ) as readonly [ + { readonly filename: string; readonly files: readonly { readonly path: string }[] }, + ]; + const dryRunPaths = packed[0].files.map((file) => file.path); const adopterAssets = [ "CONTEXT.md", ".agents/skills/sdp-agent-surface/SKILL.md", @@ -234,14 +236,7 @@ describe("published package surface", () => { dryRunPaths.filter((path) => !path.startsWith("dist/") && /\.tsx?$/u.test(path)), ).toEqual([]); - run("npm", ["pack", "--pack-destination", packageRoot], repositoryRoot); - - const tarballs = await readdir(packageRoot); - const tarball = tarballs.find((entry) => entry.endsWith(".tgz")); - - if (tarball === undefined) { - throw new Error("npm pack did not create a tarball."); - } + const tarball = packed[0].filename; await mkdir(consumer); await writeFile( diff --git a/test/vitest-adapter.test.ts b/test/vitest-adapter.test.ts new file mode 100644 index 00000000..1787db92 --- /dev/null +++ b/test/vitest-adapter.test.ts @@ -0,0 +1,106 @@ +import { expect, test, vi } from "vitest"; + +import { bindExample } from "../src/adapters/vitest.js"; +import type { ExampleContract } from "../src/runner/index.js"; + +const registered = vi.hoisted(() => ({ run: undefined as (() => Promise) | undefined })); + +// Capture the adapter's registered body so expected failures do not fail the enclosing suite. +vi.mock("vitest", async (importOriginal) => ({ + ...(await importOriginal()), + describe: (_title: string, body: () => void) => { + body(); + }, + it: (_title: string, body: () => Promise) => { + registered.run = body; + }, +})); + +/** + * @sdpAnchor test:protocol.example-runner-cleanup + * @sdpVerifies spec:extraction.example-runner + */ + +const contract: ExampleContract<"a step", Record<"a step", Record>> = { + spec: "spec:probe.cleanup", + title: "A world is cleaned up", + steps: [{ kind: "when", text: "a step", params: {} }], +}; + +async function runRegistered(): Promise { + expect(registered.run).toBeDefined(); + await registered.run?.(); +} + +test.each([false, true])( + "awaits cleanup after a failing step, cleanup also fails: %s", + async (cleanupFails) => { + const world = { cleaned: false }; + const stepError = new Error("step failed"); + bindExample( + contract, + () => world, + { + "a step": () => { + throw stepError; + }, + }, + async (created) => { + await Promise.resolve(); + expect(created).toBe(world); + created.cleaned = true; + if (cleanupFails) throw new Error("cleanup failed"); + }, + ); + await expect(runRegistered()).rejects.toBe(stepError); + expect(stepError.message).toBe("at step: When a step\nstep failed"); + expect(world.cleaned).toBe(true); + }, +); + +test("fails with cleanup's error when the steps pass", async () => { + const cleanupError = new Error("cleanup failed"); + bindExample( + contract, + () => ({}), + { "a step": () => undefined }, + () => { + throw cleanupError; + }, + ); + await expect(runRegistered()).rejects.toBe(cleanupError); +}); + +test("awaits cleanup after passing steps", async () => { + const trace: string[] = []; + bindExample( + contract, + () => trace, + { + "a step": (world) => { + world.push("step"); + }, + }, + async (world) => { + await Promise.resolve(); + world.push("cleanup"); + }, + ); + await runRegistered(); + expect(trace).toEqual(["step", "cleanup"]); +}); + +test("preserves a factory failure without invoking cleanup", async () => { + const factoryError = new Error("factory failed"); + const cleanup = vi.fn(); + bindExample( + contract, + () => { + throw factoryError; + }, + { "a step": () => undefined }, + cleanup, + ); + await expect(runRegistered()).rejects.toBe(factoryError); + expect(cleanup).not.toHaveBeenCalled(); +}); From dcf8d3235f8df70a283b44b5ecc171a764e37848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:23:58 +0200 Subject: [PATCH 04/33] fix(adopter): qualify pnpm git build permissions by source --- README.md | 12 ++++++++---- check-git-install.mjs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a51a0bd7..9aba88f2 100644 --- a/README.md +++ b/README.md @@ -75,15 +75,19 @@ onlyBuiltDependencies: - "@libar-dev/software-delivery-protocol" ``` -On pnpm 10.26 or later, including pnpm 11, use `allowBuilds` instead of `onlyBuiltDependencies`: +pnpm 11 requires the git source in the build permission. With pnpm 11.19 or later and the GitHub +pin above, use this repository permission instead: ```yaml allowBuilds: - "@libar-dev/software-delivery-protocol": true + "@libar-dev/software-delivery-protocol@git+https://github.com/libar-dev/software-delivery-protocol.git": true ``` -Keep this alongside your existing build permissions. The permission lets pnpm prepare the git -dependency. See the [pnpm 10 build settings](https://pnpm.io/10.x/settings#allowbuilds). +Keep this alongside your existing build permissions. It permits builds from this repository +across commit updates. A package name alone does not approve a git build on pnpm 11. To allow +only one commit, or on earlier pnpm 11 versions, use the exact source and commit key shown by +`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`. See the [pnpm 10 build settings](https://pnpm.io/10.x/settings#onlybuiltdependencies) +and [source-qualified permissions](https://pnpm.io/settings/build#allowbuilds). Verify either install through the linked binary: ```sh diff --git a/check-git-install.mjs b/check-git-install.mjs index 7c47c529..a1666ce1 100644 --- a/check-git-install.mjs +++ b/check-git-install.mjs @@ -45,7 +45,7 @@ try { writeFileSync( join(consumer, "pnpm-workspace.yaml"), major >= 11 - ? `allowBuilds:\n '${packageName}': true\n` + ? `allowBuilds:\n ${JSON.stringify(`${packageName}@${dependency}`)}: true\n` : `onlyBuiltDependencies:\n - '${packageName}'\n`, ); } From 03a73acb7a1b96901f91f09694c93b9fd4ae58c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:27:39 +0200 Subject: [PATCH 05/33] test(adopter): reuse the prepared package in smoke checks --- test/package-smoke.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/package-smoke.test.ts b/test/package-smoke.test.ts index c085115b..e278a4d5 100644 --- a/test/package-smoke.test.ts +++ b/test/package-smoke.test.ts @@ -184,9 +184,14 @@ describe("published package surface", () => { const consumer = join(packageRoot, "consumer"); try { - // npm pack runs prepare. One pack proves the lifecycle and supplies the file inventory. + // npm ci or the check gate has built dist/. Reuse that build here; the separate git-install + // gate proves prepare from a clean commit without rebuilding inside the pooled suite. const packed = JSON.parse( - run("npm", ["pack", "--json", "--pack-destination", packageRoot], repositoryRoot), + run( + "npm", + ["pack", "--ignore-scripts", "--json", "--pack-destination", packageRoot], + repositoryRoot, + ), ) as readonly [ { readonly filename: string; readonly files: readonly { readonly path: string }[] }, ]; From fab8a6190353f47abca41f5f07ba5b3d05b88bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:30:21 +0200 Subject: [PATCH 06/33] specs: design corpus checks, the architecture walk and component designs Folds the ruled answers on corpus rules, architecture constraints and planned architecture into rules and keyed Design entries: the rules file and its answer, sdp validate --rules and --params, the corpus section and summary line, exit codes, recipe 42 (the architecture walk) and recipe 43 (planned against realized components). Adds unbound example Specs for each, groups the three Specs and their examples in pack:corpus-checks-v1, and deletes the answered nextPacks question. Co-Authored-By: Claude Opus 5.5 --- specs/consumers/graph-first-planning.sdp.md | 6 -- specs/corpus-checks.pack.sdp.md | 20 +++++++ ...chitecture.differing-layer-and-uses.sdp.md | 23 ++++++++ ...lanned-architecture.matching-design.sdp.md | 23 ++++++++ specs/model/planned-architecture.sdp.md | 35 +++++++---- ...nned-architecture.unrealized-design.sdp.md | 22 +++++++ ...re-constraints.forbidden-layer-pair.sdp.md | 24 ++++++++ ...onstraints.isolated-context-as-rule.sdp.md | 23 ++++++++ .../architecture-constraints.sdp.md | 33 +++++++---- ...traints.unstated-layer-unclassified.sdp.md | 24 ++++++++ ...corpus-rules.copies-contain-changes.sdp.md | 26 +++++++++ .../corpus-rules.error-finding-fails.sdp.md | 25 ++++++++ ...orpus-rules.malformed-finding-fails.sdp.md | 25 ++++++++ specs/validation/corpus-rules.sdp.md | 58 +++++++++++++------ ...orpus-rules.warning-finding-informs.sdp.md | 25 ++++++++ 15 files changed, 347 insertions(+), 45 deletions(-) create mode 100644 specs/corpus-checks.pack.sdp.md create mode 100644 specs/model/planned-architecture.differing-layer-and-uses.sdp.md create mode 100644 specs/model/planned-architecture.matching-design.sdp.md create mode 100644 specs/model/planned-architecture.unrealized-design.sdp.md create mode 100644 specs/validation/architecture-constraints.forbidden-layer-pair.sdp.md create mode 100644 specs/validation/architecture-constraints.isolated-context-as-rule.sdp.md create mode 100644 specs/validation/architecture-constraints.unstated-layer-unclassified.sdp.md create mode 100644 specs/validation/corpus-rules.copies-contain-changes.sdp.md create mode 100644 specs/validation/corpus-rules.error-finding-fails.sdp.md create mode 100644 specs/validation/corpus-rules.malformed-finding-fails.sdp.md create mode 100644 specs/validation/corpus-rules.warning-finding-informs.sdp.md diff --git a/specs/consumers/graph-first-planning.sdp.md b/specs/consumers/graph-first-planning.sdp.md index f1e6870a..9286beea 100644 --- a/specs/consumers/graph-first-planning.sdp.md +++ b/specs/consumers/graph-first-planning.sdp.md @@ -20,12 +20,6 @@ relations: - outcome: Capture the intent of the next body of work as Specs below `ready`, so planning sessions derive backlog and readiness from the graph and plan files point to their carrying Specs. -### Open questions - -- [non-blocking #nextPacks] Which Packs group the next bodies of work once each is commissioned? - - decider: owner - - provisional: a focused corpus-checks Pack holds corpus rules, architecture constraints and planned architecture; coverage and run evidence join the Spec Studio Pack only if they join its design - ## Behavior - rule: `spec:consumers.delivery-session-on-ramp` owns per-session routing from graph state; this Spec owns how the next body of work's intent enters the corpus and is read back. diff --git a/specs/corpus-checks.pack.sdp.md b/specs/corpus-checks.pack.sdp.md new file mode 100644 index 00000000..a19cd2b3 --- /dev/null +++ b/specs/corpus-checks.pack.sdp.md @@ -0,0 +1,20 @@ +--- +id: pack:corpus-checks-v1 +specs: + - spec:validation.corpus-rules + - spec:validation.corpus-rules.error-finding-fails + - spec:validation.corpus-rules.warning-finding-informs + - spec:validation.corpus-rules.malformed-finding-fails + - spec:validation.corpus-rules.copies-contain-changes + - spec:validation.architecture-constraints + - spec:validation.architecture-constraints.forbidden-layer-pair + - spec:validation.architecture-constraints.isolated-context-as-rule + - spec:validation.architecture-constraints.unstated-layer-unclassified + - spec:model.planned-architecture + - spec:model.planned-architecture.unrealized-design + - spec:model.planned-architecture.differing-layer-and-uses + - spec:model.planned-architecture.matching-design +--- +# Corpus checks + +This Pack groups what a corpus needs to check its own policy on the one validation path, in reading order. Corpus rules are the corpus's own graph bodies that `sdp validate --rules` runs and reports apart from the Protocol's findings. The architecture walk is a catalog recipe a corpus runs as one of those rules, over the layers, contexts and uses its anchors state. Component designs state the components a design adds before their code exists, and a catalog recipe compares them with the components the anchors later realize. Each Spec is followed by the examples that will verify it. diff --git a/specs/model/planned-architecture.differing-layer-and-uses.sdp.md b/specs/model/planned-architecture.differing-layer-and-uses.sdp.md new file mode 100644 index 00000000..c1928e6c --- /dev/null +++ b/specs/model/planned-architecture.differing-layer-and-uses.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:model.planned-architecture.differing-layer-and-uses +kind: example +altitude: story +readiness: defined +relations: + refines: spec:model.planned-architecture + verifies: spec:model.planned-architecture +--- +# A realized component that differs from its design gives one row per field + +## Intent +- outcome: Execute the realization comparison on one realized component whose layer and uses differ from its design. +- assumption: The world writes the probe root of the unrealized-design example and adds a comment-form anchor for the planned component that satisfies the design Spec and states the realized layer, with one `impl:` member whose anchor `uses` the member of the component the realized-uses slot names. + +```gwt +Given a component design for {componentId: "component:probe.history"} that plans the layer {plannedLayer: "domain"} and uses {plannedUses: "component:probe.kernel"} +Given the component anchor for that design {anchor: "satisfies the design"} +Given that anchor states the layer {realizedLayer: "application"} and its member uses {realizedUses: "component:probe.audit"} +When the realization comparison runs +Then the comparison lists {unrealized: 0} unrealized designs, {matching: 0} matching designs and {differing: 2} differing rows +Then the differing fields are {fields: "layer, uses"} +``` diff --git a/specs/model/planned-architecture.matching-design.sdp.md b/specs/model/planned-architecture.matching-design.sdp.md new file mode 100644 index 00000000..3aa6d66a --- /dev/null +++ b/specs/model/planned-architecture.matching-design.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:model.planned-architecture.matching-design +kind: example +altitude: story +readiness: defined +relations: + refines: spec:model.planned-architecture + verifies: spec:model.planned-architecture +--- +# A realized component that matches its design gives no row + +## Intent +- outcome: Execute the realization comparison on one realized component whose layer and uses match its design. +- assumption: The world writes the probe root of the differing example, with the realized layer and uses the slots give. + +```gwt +Given a component design for {componentId: "component:probe.history"} that plans the layer {plannedLayer: "domain"} and uses {plannedUses: "component:probe.kernel"} +Given the component anchor for that design {anchor: "satisfies the design"} +Given that anchor states the layer {realizedLayer: "domain"} and its member uses {realizedUses: "component:probe.kernel"} +When the realization comparison runs +Then the comparison lists {unrealized: 0} unrealized designs, {matching: 1} matching designs and {differing: 0} differing rows +Then the differing fields are {fields: ""} +``` diff --git a/specs/model/planned-architecture.sdp.md b/specs/model/planned-architecture.sdp.md index f0982d5e..a16ea73a 100644 --- a/specs/model/planned-architecture.sdp.md +++ b/specs/model/planned-architecture.sdp.md @@ -15,22 +15,33 @@ The original design declared a component before its code, as a Component node wi The Protocol records the architecture the code realizes. A `component:` anchor states its layer and context, and anchored `memberOf` and `uses` edges join the code units. Under the architectural annotation decision, a component no code realizes is a Spec, and the graph holds no node for it until an anchor exists. The first adopter states its component list and its context rule in prose (`libar-platform/design/PLAN.md` §2.1) and checks context isolation over anchored `uses` in its own script (`libar-platform/design/tools/check.py`). The owner ruled that the architectural annotation decision stands: a component designed before its code is a component design Spec, and the `component:` anchor that later realizes it satisfies that Spec, which is the join a comparison reads. +Choosing a provider stays prose. The original's distinctions between the interface a unit requires, the providers that supply it, the one a composition root selects and the one a test substitutes help a reader, and none of them needs graph structure beyond `uses` to be compared. Keeping one composition mechanism per application is a corpus's own architecture rule when the corpus wants it, stated in a `constraint` Spec and checked by its own corpus rule. A component design names its component with the keys a component anchor uses, so the comparison reads the same words on both sides, and a key the design leaves out is not compared, so a design states only what it has decided. + ## Intent - problem: A design ahead of its code cannot state the components it adds, their layer and context and what they use, in a form the graph compares with the components the code later anchors. - outcome: Let a design name the components it will add, with their layer, context and uses, and compare that design with the anchored components once code exists. - risk: A planned component kept apart from its realization is a second place for the component to drift, which is why the architectural annotation decision refused a separate architecture file and why the design lives in a Spec the realizing anchor satisfies. -### Open questions -- [non-blocking #providerSelection] The original kept apart the interface a unit requires, the providers that supply it, the provider a composition root selects and the one a test substitutes, for Fastify, Effect, Awilix and plain factories alike. Does a component design state these without naming a framework, and does any of them earn graph structure beyond `uses`? - - decider: owner -- [non-blocking #oneRuntimeOwner] Is the original's one-runtime-truth rule, one composition mechanism per application, a Protocol rule or a corpus's own architecture rule? - - decider: owner -- [non-blocking #comparisonShape] How is a component design selected, what value form does its `uses` entry take, and what rows does the realization comparison return for an unrealized design and for a layer, context or `uses` that differs? - - decider: owner - - provisional: a Spec is a component design when its Design `id` value, bare or in the code span that opens it, is a `component:` id, as the curation report reads it - ## Model -- **component design** — A Spec that states a component the code does not yet realize and what it is for. Its Design entries are keyed as the anchor's attributes are named: `id` for the planned `component:` anchor's own id, `layer`, `context`, `uses` for the components it will use, and optionally `role`. The keys are a convention a recipe reads; the Protocol closes no Design shape, and the Spec confers nothing and mints no code unit. +- **component design** — A Spec that states a component the code does not yet realize and what it is for. Its Design entries are keyed as the anchor's attributes are named: `id` for the planned `component:` anchor's own id, `layer`, `context`, `uses` for the components it will use, and optionally `role`. The keys are a convention recipes read; the Protocol closes no Design shape, and the Spec confers nothing and mints no code unit. - **planned component** — The component a component design names. It stays intent until a `component:` anchor realizes it, and that anchor satisfies the component design; the graph holds no component node before the anchor exists. -- **realization comparison** — A catalog recipe that reads each component design beside the `component:` anchor that satisfies it, through the component data recipe 32 joins, and lists a design no anchor satisfies yet, a layer or context that differs, and a `uses` named on one side only. It reports and refuses nothing. -- **provider** — A code unit that supplies an interface another unit requires; a composition selects one provider for each requirement, and a test may select another. +- **realization comparison** — Recipe 43, planned against realized components, which reads each component design beside the `component:` anchor that satisfies it, resolving members through `memberOf` as recipe 32 does, and lists a design no anchor satisfies yet, a layer, context or role that differs, and a `uses` named on one side only. It reports and refuses nothing. +- **provider** — A code unit that supplies an interface another unit requires; a composition selects one provider for each requirement, and a test may select another. A component design names its providers and that selection in prose, and none of them earns graph structure beyond `uses`. + +## Design +- componentDesign: a Spec is a component design when its Design `id` value, bare or in the code span that opens it, is a `component:` id; the curation report and the realization comparison select designs this way, and no validator reads the convention. +- designKeys: the comparison reads five Design keys, `id`, `layer`, `context`, `role` and `uses`, and no other; `layer` takes one of the five layers, `context` and `role` one token on the role grammar, and `uses` a comma-separated list of `component:` ids; each value is read bare when it is the whole value, or from the code span that opens it, with prose after the span free to say why. +- comparisonRecipe: recipe 43, planned against realized components, takes no parameter and returns `{ totals, unrealized, differing, unreadable }`, where `totals` is `{ designs, realized, matching, unrealized, differing, unreadable }`, `matching` counts the realized designs with no differing row, and `differing` counts rows. +- realizedWhen: a design is realized when its `component:` CodeNode is anchored and has an anchored `satisfies` edge to the design, as the curation report reads it; a design whose component exists without that edge stays unrealized, with `anchorExists: true`. +- comparedFields: for a realized design, each of `layer`, `context`, `role` and `uses` the design states is compared with the anchored component, and a key the design leaves out is not compared; the realized `uses` are the components other than its own that an anchored `uses` edge from the component or one of its members reaches, each end resolved to its component as recipe 32 resolves it. +- comparisonRows: `unrealized` rows are `{ spec, componentId, anchorExists, layer, context, role, uses }` with the planned values, null or an empty list when unstated; `differing` rows are `{ spec, componentId, field, planned, realized }`, one for each differing field, where `layer`, `context` and `role` carry the two values or null and `uses` carries the two sorted lists of components named on that side only; `unreadable` rows are `{ spec, key, value }` for a value outside its key's form, and that key is not compared; rows sort by `spec`, then by `field` or `key`. + +## Example space +```gwt-vocabulary +Given a component design for {componentId:string} that plans the layer {plannedLayer:"domain"|"application"} and uses {plannedUses:string} +Given the component anchor for that design {anchor:"is absent"|"satisfies the design"} +Given that anchor states the layer {realizedLayer:"domain"|"application"} and its member uses {realizedUses:string} +When the realization comparison runs +Then the comparison lists {unrealized:number} unrealized designs, {matching:number} matching designs and {differing:number} differing rows +Then the differing fields are {fields:string} +``` diff --git a/specs/model/planned-architecture.unrealized-design.sdp.md b/specs/model/planned-architecture.unrealized-design.sdp.md new file mode 100644 index 00000000..ada80939 --- /dev/null +++ b/specs/model/planned-architecture.unrealized-design.sdp.md @@ -0,0 +1,22 @@ +--- +id: spec:model.planned-architecture.unrealized-design +kind: example +altitude: story +readiness: defined +relations: + refines: spec:model.planned-architecture + verifies: spec:model.planned-architecture +--- +# A component design no anchor satisfies is listed as unrealized + +## Intent +- outcome: Execute the realization comparison on one component design whose component no anchor realizes yet. +- assumption: The world writes a probe root holding one story-altitude model Spec stating `idea` with an Intent outcome, one Model term and a Design section whose `id`, `layer` and `uses` entries carry the slots' planned values, bare. Beside it the root holds comment-form anchors for `component:probe.kernel` and `component:probe.audit`, each with one `impl:` member. The world runs the comparison's shipped recipe file through `sdp q --json`; the differing fields are the `field` values of the `differing` rows joined by a comma and a space, in answer order, and an empty string names none. + +```gwt +Given a component design for {componentId: "component:probe.history"} that plans the layer {plannedLayer: "domain"} and uses {plannedUses: "component:probe.kernel"} +Given the component anchor for that design {anchor: "is absent"} +When the realization comparison runs +Then the comparison lists {unrealized: 1} unrealized designs, {matching: 0} matching designs and {differing: 0} differing rows +Then the differing fields are {fields: ""} +``` diff --git a/specs/validation/architecture-constraints.forbidden-layer-pair.sdp.md b/specs/validation/architecture-constraints.forbidden-layer-pair.sdp.md new file mode 100644 index 00000000..80aaddd4 --- /dev/null +++ b/specs/validation/architecture-constraints.forbidden-layer-pair.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:validation.architecture-constraints.forbidden-layer-pair +kind: example +altitude: story +readiness: defined +relations: + refines: spec:validation.architecture-constraints + verifies: + target: spec:validation.architecture-constraints + exercises: [walk] +--- +# A domain member that uses an infrastructure member breaks a forbidden pair + +## Intent +- outcome: Execute the walk through `sdp q` on one `uses` edge between members of two components whose layers form a forbidden pair. +- assumption: The world writes a probe root with no Spec and two comment-form `component:` anchors, each with one `impl:` member; the using member's anchor `uses` the used member. Each component states the layer and context its slots give, and the Protocol's validators find the root clean. The world runs the walk's shipped recipe file through `sdp q --json` with `params` `{ "forbidden": [{ "from": "domain", "to": "infrastructure" }], "isolated": ["platform"] }`; the rule ids are the findings' `validatorId` values joined by a comma and a space, in answer order. + +```gwt +Given a member of a component at the layer {fromLayer: "domain"} in the context {fromContext: "orders"} that uses a member of a component at the layer {toLayer: "infrastructure"} in the context {toContext: "orders"} +When the architecture walk runs {invocation: "through sdp q"} with the forbidden pair domain to infrastructure and the isolated context platform +Then the walk reports {findings: 1} findings with the rule ids {ruleIds: "forbidden-layer"} +Then the walk lists {unclassified: 0} unclassified edges +Then the command exits {exitCode: 0} +``` diff --git a/specs/validation/architecture-constraints.isolated-context-as-rule.sdp.md b/specs/validation/architecture-constraints.isolated-context-as-rule.sdp.md new file mode 100644 index 00000000..921d4c8b --- /dev/null +++ b/specs/validation/architecture-constraints.isolated-context-as-rule.sdp.md @@ -0,0 +1,23 @@ +--- +id: spec:validation.architecture-constraints.isolated-context-as-rule +kind: example +altitude: story +readiness: defined +relations: + refines: spec:validation.architecture-constraints + verifies: + target: spec:validation.architecture-constraints + exercises: [walk, walkAsRule] +--- +# The walk named as a rules file fails validation on an isolation breach + +## Intent +- outcome: Execute the walk's shipped recipe file, unchanged, as a rules file under `sdp validate --rules` on one `uses` edge that leaves an isolated context. +- assumption: The world writes the probe root of the forbidden-pair example, with the layers and contexts the slots give. It runs `sdp validate` on that root with the walk's shipped recipe file named by `--rules` and the parameters of that example passed by `--params`. The findings are the lines in that file's corpus section, and the rule ids are their rule ids joined by a comma and a space, in output order. + +```gwt +Given a member of a component at the layer {fromLayer: "application"} in the context {fromContext: "platform"} that uses a member of a component at the layer {toLayer: "adapter"} in the context {toContext: "orders"} +When the architecture walk runs {invocation: "as a rules file named by sdp validate --rules"} with the forbidden pair domain to infrastructure and the isolated context platform +Then the walk reports {findings: 1} findings with the rule ids {ruleIds: "isolated-context"} +Then the command exits {exitCode: 1} +``` diff --git a/specs/validation/architecture-constraints.sdp.md b/specs/validation/architecture-constraints.sdp.md index 336c6dbd..27823998 100644 --- a/specs/validation/architecture-constraints.sdp.md +++ b/specs/validation/architecture-constraints.sdp.md @@ -15,22 +15,35 @@ The original design gave architecture a validation tier of its own. Its rules sa The first adopter checks that a component in its `platform` context has no `uses` edge to a component in another context, resolving each end through `memberOf`. Architecture crossings, recipe 32, reports differences in context or layer and refuses nothing. The architectural annotation decision keeps architecture checks in the corpus's own gate, and the Protocol supplies the reusable graph walk. +The walk returns the edges a rule forbids as corpus findings, so one body answers a reader through `sdp q` and fails a gate when the corpus names it as a rules file; the first adopter's isolation check becomes the walk named by `sdp validate --rules` with the isolated context `platform`, and none of its own code. An edge the walk cannot decide, because a component states no layer or context or an end belongs to no component, is listed apart: a breach the anchors cannot show is a curation gap, not a finding. + ## Intent - problem: A corpus that states an architecture rule in a Spec cannot have it checked by the Protocol, so each adopter writes the same walk over `uses`, `memberOf`, `layer` and `context` in its own gate. - outcome: Check a corpus's declared architecture rules, such as a forbidden uses direction or context isolation, against the layers, contexts and uses its anchors state. -### Open questions -- [non-blocking #ruleForms] Do corpus rules need to require a component to state a layer or context, or check ports and routes that anchors do not carry, beyond the selected forbidden layer pairs and isolated contexts? - - decider: owner - - provisional: the supplied walk checks forbidden layer pairs and isolated contexts; absent attributes are unclassified rather than breaches -- [non-blocking #walkInvocation] Which pure callable walk serves both the catalog recipe and a loaded corpus rule, and what rule-data and result shapes do they share? - - decider: owner - - provisional: one pure walk takes `params.forbidden`, a list of `{ from, to }` layer pairs, and `params.isolated`, a list of context tokens whose components may have no `uses` edge into another context, either list possibly empty; it returns each forbidden edge as `{ from, to, fromComponent, toComponent, rule }` and lists unclassified edges apart; recipe 32's result stays as it is - ## Rule - The Protocol's validators never judge a corpus's architecture; an architecture rule runs as one of the corpus's own rules, the corpus's gate decides what fails, and that gate may be the corpus's own invocation of `sdp validate --rules` with the severity the rule states. - A corpus states each architecture rule as intent in a `constraint` Spec that says what is forbidden and why, and keeps the rule's machine form, its forbidden layer pairs and isolated contexts, as data its corpus rule passes to the walk. -- [#walk] The Protocol supplies the walk as a catalog recipe that takes forbidden layer pairs and isolated contexts as recipe parameters, resolves each end of every anchored `uses` edge to its component, directly or through `memberOf`, and returns each edge a rule forbids with both units, both components and the rule. +- [#walk] The Protocol supplies the walk as a catalog recipe, recipe 42, that takes forbidden layer pairs and isolated contexts as recipe parameters, resolves each end of every anchored `uses` edge to its component, directly or through `memberOf`, and returns each edge a rule forbids with both units, both components and the rule. +- [#walkAsRule] The walk returns the edges it forbids as corpus findings in its answer's `findings`, so the shipped recipe file runs unchanged as a rules file under `sdp validate --rules`, at the severity the corpus passes it. +- [#forms] The walk checks two forms, forbidden layer pairs and isolated contexts. It requires no component to state a layer or a context and checks no port, route or other fact the anchors do not carry; a corpus that wants such a check writes a rule of its own. - The walk reads the given rules and anchored structure only; it infers no edge from imports, reads no file path, judges no role and refuses nothing. -- A component that states no layer or context falls outside a rule that names one, and the walk reports it as unclassified rather than as a breach. +- [#unclassified] A component that states no layer or context falls outside a rule that names one, and the walk lists the edge as unclassified rather than as a breach, as it lists an edge with an end that belongs to no component. - The corpus binds a discovered TypeScript check site to its constraint Spec through a test anchor; that check invokes the corpus rule, and `has-verifier` states that the check exists while its result stays in the corpus's run. + +## Design +- walkParams: recipe 42, the architecture walk, takes `params.forbidden`, a list of `{ from, to }` pairs of layers from the closed set; `params.isolated`, a list of context tokens; and `params.severity`, `"error"` by default or `"warning"`; either list may be empty, a pair or context given twice counts once, and neither list given, a layer outside the set, a malformed token or another severity returns `{ diagnostics }` with no `findings`, which fails the file when it runs as a rules file. +- walkAnswer: `{ input: { forbidden, isolated, severity }, totals: { usesEdges, resolved, findings, unclassified }, findings, unclassified }`, where `resolved` counts the `uses` edges whose two ends both resolve to a component; findings sort by `from`, then `to`, then rule id, and unclassified rows by `from`, then `to`; recipe 32's answer stays as it is. +- walkBreach: an anchored `uses` edge whose ends resolve to two distinct components breaks the pair `{ from, to }` when the using component states layer `from` and the used one states layer `to`, and breaks the isolation of a context when the using component states that context and the used one states another; two units of one component never break a rule, as recipe 32 never counts them as crossing. +- walkFinding: `{ validatorId, subjectId, relatedId, severity, message, from, to, fromComponent, toComponent, rule }`, a corpus finding in the shape `spec:validation.corpus-rules#design.ruleFinding` pins, where `subjectId` is the using unit `from`, `relatedId` the used unit `to`, both components are `{ id, layer, context }` with null for an unstated attribute, and `validatorId` is `forbidden-layer` with `rule: { from, to }` the pair it breaks, or `isolated-context` with `rule: { context }` the isolated context it leaves. +- walkMessages: a `forbidden-layer` message reads ` uses : layer uses layer ( to ), a forbidden pair`, and an `isolated-context` message reads ` uses : in the isolated context uses in the context `. +- walkUnclassified: `unclassified` rows `{ from, to, fromComponent, toComponent, missing }` list each anchored `uses` edge the walk cannot decide while a list is non-empty: an edge with an end that belongs to no component, that end's component null and `missing: ["component"]`, and an edge between two distinct components where either leaves unstated an attribute a non-empty list reads, `layer` for `forbidden` and `context` for `isolated`, named in `missing`. + +## Example space +```gwt-vocabulary +Given a member of a component at the layer {fromLayer:"domain"|"application"} in the context {fromContext:"platform"|"orders"} that uses a member of a component at the layer {toLayer:"infrastructure"|"adapter"|"unstated"} in the context {toContext:"platform"|"orders"} +When the architecture walk runs {invocation:"through sdp q"|"as a rules file named by sdp validate --rules"} with the forbidden pair domain to infrastructure and the isolated context platform +Then the walk reports {findings:number} findings with the rule ids {ruleIds:string} +Then the walk lists {unclassified:number} unclassified edges +Then the command exits {exitCode:number} +``` diff --git a/specs/validation/architecture-constraints.unstated-layer-unclassified.sdp.md b/specs/validation/architecture-constraints.unstated-layer-unclassified.sdp.md new file mode 100644 index 00000000..067acef5 --- /dev/null +++ b/specs/validation/architecture-constraints.unstated-layer-unclassified.sdp.md @@ -0,0 +1,24 @@ +--- +id: spec:validation.architecture-constraints.unstated-layer-unclassified +kind: example +altitude: story +readiness: defined +relations: + refines: spec:validation.architecture-constraints + verifies: + target: spec:validation.architecture-constraints + exercises: [unclassified] +--- +# A component with no layer leaves its edge unclassified + +## Intent +- outcome: Execute the walk through `sdp q` on one `uses` edge whose used component states no layer. +- assumption: The world writes the probe root of the forbidden-pair example, except that the used component's anchor states no layer, which the `unstated` value names. It runs the walk as that example does; an empty rule-id string names no finding. + +```gwt +Given a member of a component at the layer {fromLayer: "domain"} in the context {fromContext: "platform"} that uses a member of a component at the layer {toLayer: "unstated"} in the context {toContext: "platform"} +When the architecture walk runs {invocation: "through sdp q"} with the forbidden pair domain to infrastructure and the isolated context platform +Then the walk reports {findings: 0} findings with the rule ids {ruleIds: ""} +Then the walk lists {unclassified: 1} unclassified edges +Then the command exits {exitCode: 0} +``` diff --git a/specs/validation/corpus-rules.copies-contain-changes.sdp.md b/specs/validation/corpus-rules.copies-contain-changes.sdp.md new file mode 100644 index 00000000..8878addd --- /dev/null +++ b/specs/validation/corpus-rules.copies-contain-changes.sdp.md @@ -0,0 +1,26 @@ +--- +id: spec:validation.corpus-rules.copies-contain-changes +kind: example +altitude: story +readiness: defined +relations: + refines: spec:validation.corpus-rules + verifies: + target: spec:validation.corpus-rules + exercises: [copiesPerFile, namedOrder] +--- +# A rules file that empties its graph leaves the next file's graph whole + +## Intent +- outcome: Execute the copy rule on two rules files, the first changing the graph it receives and the second reading its own. +- assumption: The world writes the probe corpus of the error example. The first rules file's body sets the length of `graph.nodes` and of `graph.edges` to zero and returns `{ findings: [] }`. The second rules file's body returns one warning finding when `graph.nodes` is empty and `{ findings: [] }` otherwise. The world runs `sdp validate` on the probe root with the first file, then the second, named by `--rules`, and reads the exit code and standard output. + +```gwt +Given a probe corpus the Protocol's validators find clean +Given a first rules file that {first: "empties the graph it receives"} +Given a second rules file that returns one warning when the graph it receives holds no node +When the probe corpus is validated with the rules files named by --rules in that order +Then the command exits {exitCode: 0} +Then the Protocol summary line reads {protocolSummary: "validate: 0 errors · 0 warnings (conformance + honesty over the one graph)"} +Then the corpus summary line reads {corpusSummary: "corpus rules: 0 errors · 0 warnings · 0 of 2 files failed (the corpus's own rules)"} +``` diff --git a/specs/validation/corpus-rules.error-finding-fails.sdp.md b/specs/validation/corpus-rules.error-finding-fails.sdp.md new file mode 100644 index 00000000..153a009d --- /dev/null +++ b/specs/validation/corpus-rules.error-finding-fails.sdp.md @@ -0,0 +1,25 @@ +--- +id: spec:validation.corpus-rules.error-finding-fails +kind: example +altitude: story +readiness: defined +relations: + refines: spec:validation.corpus-rules + verifies: + target: spec:validation.corpus-rules + exercises: [exitCode] +--- +# An error from a corpus rule fails validation and leaves the Protocol's line alone + +## Intent +- outcome: Execute the exit rule on one rules file that returns one error finding over a corpus the Protocol finds clean. +- assumption: The world writes a probe corpus of two story-altitude behavior Specs stating `idea`, each with an Intent outcome, the second declaring `refines` on the first, so the Protocol's validators report no finding. The rules file's body returns `{ findings: [{ validatorId: "probe-rule", subjectId: "probe-subject", severity: "error", message: "the probe rule reports its subject" }] }`. The world runs `sdp validate` on the probe root with that file named by `--rules` and reads the exit code and standard output. + +```gwt +Given a probe corpus the Protocol's validators find clean +Given a first rules file that {first: "returns one error finding"} +When the probe corpus is validated with the rules files named by --rules in that order +Then the command exits {exitCode: 1} +Then the Protocol summary line reads {protocolSummary: "validate: 0 errors · 0 warnings (conformance + honesty over the one graph)"} +Then the corpus summary line reads {corpusSummary: "corpus rules: 1 errors · 0 warnings · 0 of 1 files failed (the corpus's own rules)"} +``` diff --git a/specs/validation/corpus-rules.malformed-finding-fails.sdp.md b/specs/validation/corpus-rules.malformed-finding-fails.sdp.md new file mode 100644 index 00000000..991accec --- /dev/null +++ b/specs/validation/corpus-rules.malformed-finding-fails.sdp.md @@ -0,0 +1,25 @@ +--- +id: spec:validation.corpus-rules.malformed-finding-fails +kind: example +altitude: story +readiness: defined +relations: + refines: spec:validation.corpus-rules + verifies: + target: spec:validation.corpus-rules + exercises: [brokenRuleFails] +--- +# A finding with no subject fails its rules file + +## Intent +- outcome: Execute the broken-rule rule on one rules file whose only finding carries no subject. +- assumption: The world writes the probe corpus of the error example. The rules file's body returns `{ findings: [{ validatorId: "probe-rule", severity: "warning", message: "the probe rule forgets its subject" }] }`, a warning that would pass if it were well formed. The world runs `sdp validate` on the probe root with that file named by `--rules`, reads the exit code and standard output, and reads on standard error the failure line naming the first finding's `subjectId`. + +```gwt +Given a probe corpus the Protocol's validators find clean +Given a first rules file that {first: "returns a finding with no subject"} +When the probe corpus is validated with the rules files named by --rules in that order +Then the command exits {exitCode: 1} +Then the Protocol summary line reads {protocolSummary: "validate: 0 errors · 0 warnings (conformance + honesty over the one graph)"} +Then the corpus summary line reads {corpusSummary: "corpus rules: 0 errors · 0 warnings · 1 of 1 files failed (the corpus's own rules)"} +``` diff --git a/specs/validation/corpus-rules.sdp.md b/specs/validation/corpus-rules.sdp.md index c13cf13e..e13cab62 100644 --- a/specs/validation/corpus-rules.sdp.md +++ b/specs/validation/corpus-rules.sdp.md @@ -8,6 +8,12 @@ relations: - spec:consumers.agent-surface - target: spec:validation.diagnostic-rendering reason: corpus findings use the shared diagnostic currency + - target: spec:consumers.agent-surface.evaluation-sink + reason: a rules file holds the body the sink evaluates, with the sink's bindings and trust + - target: spec:consumers.agent-surface.recipe-parameters + reason: validate takes `--params` in the sink's grammar + - target: spec:consumers.validate-watch + reason: the watch loop reruns the named rules in each cycle constrainedBy: spec:validation.two-check-families decidedBy: target: spec:decisions.architectural-annotation @@ -19,28 +25,46 @@ The original design let a team add its own rules beside the built-in validators, The first adopter's gate runs `sdp validate` and parses its summary line, then checks its own context isolation, entry addresses, dependency cycles, question keys, prose mentions, cited laws and Pack pages. It also parses carrier frontmatter to read kind, readiness and relations, a second read model that a graph-based corpus-rule runner would remove. +A rules file holds the body `sdp q` evaluates, so a rule tried through `sdp q` runs unchanged under `sdp validate`, and a catalog recipe whose answer carries findings, such as the architecture walk, is a rules file as shipped. The runner reads only the answer's findings, so the recipe keeps the rest of its answer for a reader. Corpus findings print after the Protocol's and are tallied on a line of their own, so a gate that reads the Protocol's summary line reads it unchanged; any error, the Protocol's or the corpus's, exits 1, and the two lines say which. Each rules file receives its own copy of what it reads, so a careless rule cannot change another rule's input and the order of the files changes only the order of the output. + ## Intent - problem: A corpus with rules of its own checks them off the one validation path, reading the carriers a second way and reporting in a form no Protocol surface reads. - outcome: Let a corpus author its own graph rules and have their findings join the one validation report, so no corpus re-parses its carriers to check itself. -### Open questions -- [non-blocking #rulesRepresentation] Does an operator-named rules file hold one async body returning findings or a module exporting named rule functions, how are parameters supplied, in what order do several files run, and does `sdp validate --watch` rerun them? - - decider: owner - - provisional: one async body per file, given `g`, `graph`, `report` and `params`, with `params` from `sdp validate --params` and `{}` without it; files run in the order named, each to completion; a rule id is unique within its file; `sdp validate --watch` runs the same named rules each cycle -- [non-blocking #findingCurrency] What shape does a corpus finding take beside the one diagnostic currency, where does the report carry it, and does it reach `report`, the reader's `findings()` or the Design Review? - - decider: owner - - provisional: the shared finding contract has a Protocol branch carrying `validatorId`, `family`, severity and message, and a corpus branch carrying the corpus, the rule id as `validatorId`, `subjectId`, severity and message and no family; `relatedId`, `path`, `file` and `line` stay optional on both branches; corpus findings are collected as `corpusFindings` in the same report and rendered under a corpus-labelled section with the existing location, severity, identifier and message composition; Protocol findings, the summary line and the Design Review stay unchanged, as the frozen-projections decision requires -- [non-blocking #ruleIsolation] Does the runner enforce that a rule reads only the graph and changes nothing, or are both obligations on the rule's author? - - decider: owner - - provisional: changing nothing is a guarantee, with the Protocol's validators run first and each rule given a frozen or cloned graph; reading only the graph is the author's obligation, run with the trust `sdp q` asks and no sandbox - ## Behavior - rule: A corpus rule is a graph body the corpus authors, given `g`, `graph`, `report` and `params` as a recipe body is, and returning findings that each carry a rule id, a subject, a message and a severity. -- rule: A corpus rule reads the one derived graph and nothing else, so no rule reads a carrier, a source file or the network. -- rule: Each corpus finding names its rule and its corpus as its source and stands apart from the Protocol's own findings; the Protocol states none of these rules and ships none. -- rule: Corpus findings stand outside the conformance and honesty families, in a section of the report labelled as the corpus's own, so the two families keep their meaning and the guardrail that checks never judge content quality or workflow keeps binding the Protocol's own validators. +- rule: [#readsGraphOnly] A corpus rule reads the one derived graph, the Protocol's report and the `params` the operator supplies, and no carrier, source file or network; that is the rule author's obligation, because the runner evaluates the body with the trust `sdp q` asks and claims no sandbox. - rule: [#operatorNamed] Validation runs corpus rules only from a file the operator names with `sdp validate --rules PATH`, which may repeat, and never from a file it finds in the corpus or in a configuration; it asks the trust `sdp q` already asks, to run code the operator chose. +- rule: [#namedOrder] Rules files run one at a time in the order named, each to completion, after the Protocol's validators; every file receives the same Protocol report and none sees another file's findings, so the order changes only the order of the output. +- rule: [#copiesPerFile] Each rules file receives its own copy of the graph, the report and `params`, made after the graph is written, so a body that changes what it receives changes no node, edge, delivery fact or Protocol finding, and nothing another rules file receives. +- rule: A rule id names one check within its rules file; two files may use the same id, and their findings stay apart by the file that reported them. +- rule: Each corpus finding names its rule and its rules file as its source. The Protocol states none of these rules: a shipped recipe that a corpus names as a rules file checks the rule data the corpus passes it. +- rule: Corpus findings stand outside the conformance and honesty families, in a section of the output labelled as the corpus's own and tallied on a summary line of their own, so the two families keep their meaning, the guardrail that checks never judge content quality or workflow keeps binding the Protocol's own validators, and no corpus finding adds to, suppresses or changes a Protocol finding or the exit reason it sets. - rule: [#exitCode] A corpus finding carries severity `error` or `warning`. When the operator names `--rules`, an `error` corpus finding makes `sdp validate` exit nonzero and a `warning` stays informative; without `--rules` no corpus rule runs and only the Protocol's validators set the exit code. -- rule: Corpus findings keep their own source and report section, are tallied apart from the Protocol's summary line, and neither add to, suppress nor change a Protocol finding or the exit reason it sets. -- rule: [#brokenRuleFails] A rules file that cannot load, a rule that throws, or a rule that returns a finding without a rule id, subject, message and admitted severity is a rule execution failure; `sdp validate` reports it in the corpus section and exits nonzero, so a broken rule never passes as a clean run. -- rule: A corpus rule changes no node, edge or delivery fact; it reports on the graph it is given. +- rule: [#neverMovesFloor] A corpus finding moves no readiness floor and reaches no other surface: not the report `sdp q` hands a body, not the reader, not the written graph, and not the Design Review or another projection. +- rule: [#brokenRuleFails] A rules file that cannot be read or does not parse, a body that throws, or an answer that is not an object with a `findings` array of well-formed findings is a rule execution failure; `sdp validate` reports it in the corpus section and exits nonzero, so a broken rule never passes as a clean run. A failed file reports none of its findings, and the other files still run. +- rule: [#watchReruns] `sdp validate --watch` with `--rules` reads and runs every named rules file afresh in each cycle, after the Protocol's validators; an edit to a rules file alone starts no cycle, because the watcher watches carriers. +- rule: When the graph does not derive, no rules file runs, and the corpus summary line says so. + +## Design +- rulesFlag: `sdp validate [root] [--exclude PATH]... [--check-clean | --watch] [--rules PATH]... [--params JSON | --params @PATH | --params KEY=@PATH]...`; `--rules` and `--params` are options of `validate` alone, `--params` without `--rules` is refused, one file named twice is refused, and each PATH resolves from the working directory, as a `--params @PATH` file does. +- paramsFlag: `--params` on `validate` takes the three forms of `sdp q --params` with the same refusals, before the graph derives, and every rules file receives the one merged object as `params`, or `{}` without the flag. +- rulesFile: a rules file is UTF-8 text holding one plain JavaScript async function body, the form `sdp q` evaluates, with the bindings `g`, `graph`, `report` and `params`; the runner reads the text and never imports it as a module, so no file extension is required and a catalog recipe file under `dist/recipes/` is a lawful rules file. +- ruleAnswer: `{ findings: [] }`: the body returns an object whose `findings` is an array, possibly empty; the runner reads `findings` and nothing else, so the answer's other keys stay the body's own data and are never printed. +- ruleFinding: `{ validatorId, subjectId, severity, message, relatedId?, path?, file?, line? }`, where `validatorId` is the rule id, one token matching `^[a-z][a-z0-9-]*$`, so it never reads as a Protocol validator id; `subjectId` and `message` are non-empty strings; `severity` is `"error"` or `"warning"`; `relatedId` and `path` are strings; `file` is a root-relative POSIX path and `line` a positive integer that requires `file`; the runner ignores a finding's other keys. +- corpusFinding: `{ rulesFile, validatorId, subjectId, severity, message, relatedId?, path?, file?, line? }` is the corpus branch of the finding currency, where `rulesFile` is the path as the operator named it; it carries no `family`, and the Protocol's findings keep their shape. +- corpusSection: on standard error, after every Protocol finding, each rules file in the order named prints the line `corpus rules ` and then its findings in the order returned, each in the one composition of location, severity in brackets, rule id and message; a failed file prints `corpus rules failed: ` in place of its findings. +- corpusSummary: `corpus rules: errors · warnings · of files failed (the corpus's own rules)`, written to standard output directly after the Protocol's summary line, which stays as `spec:validation.diagnostic-rendering#design.summaryLine` pins it; without `--rules` no such line prints, and when the graph does not derive the line reads `corpus rules: not run, the graph did not derive`. +- exitCodes: with `--rules`, `sdp validate` exits 1 when a Protocol finding is an error, a corpus finding is an error, a rules file fails or the graph does not derive, and 0 otherwise; the two summary lines say which. +- failureReasons: `` is `cannot be read ()`, `does not parse at line , column : `, `threw: `, `returned no findings array`, followed by `; diagnostics: ` when the answer carries `diagnostics`, or `finding has no lawful `, with N counted from 1. + +## Example space +```gwt-vocabulary +Given a probe corpus the Protocol's validators find clean +Given a first rules file that {first:"returns one error finding"|"returns one warning finding"|"returns a finding with no subject"|"empties the graph it receives"} +Given a second rules file that returns one warning when the graph it receives holds no node +When the probe corpus is validated with the rules files named by --rules in that order +Then the command exits {exitCode:number} +Then the Protocol summary line reads {protocolSummary:string} +Then the corpus summary line reads {corpusSummary:string} +``` diff --git a/specs/validation/corpus-rules.warning-finding-informs.sdp.md b/specs/validation/corpus-rules.warning-finding-informs.sdp.md new file mode 100644 index 00000000..917af186 --- /dev/null +++ b/specs/validation/corpus-rules.warning-finding-informs.sdp.md @@ -0,0 +1,25 @@ +--- +id: spec:validation.corpus-rules.warning-finding-informs +kind: example +altitude: story +readiness: defined +relations: + refines: spec:validation.corpus-rules + verifies: + target: spec:validation.corpus-rules + exercises: [exitCode] +--- +# A warning from a corpus rule informs and passes + +## Intent +- outcome: Execute the exit rule on one rules file that returns one warning finding over a corpus the Protocol finds clean. +- assumption: The world writes the probe corpus of the error example. The rules file's body returns `{ findings: [{ validatorId: "probe-rule", subjectId: "probe-subject", severity: "warning", message: "the probe rule reports its subject" }] }`. The world runs `sdp validate` on the probe root with that file named by `--rules` and reads the exit code and standard output. + +```gwt +Given a probe corpus the Protocol's validators find clean +Given a first rules file that {first: "returns one warning finding"} +When the probe corpus is validated with the rules files named by --rules in that order +Then the command exits {exitCode: 0} +Then the Protocol summary line reads {protocolSummary: "validate: 0 errors · 0 warnings (conformance + honesty over the one graph)"} +Then the corpus summary line reads {corpusSummary: "corpus rules: 0 errors · 1 warnings · 0 of 1 files failed (the corpus's own rules)"} +``` From 9dcdd70a3cd7d22b903988c870b9cc0dfdd93860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:33:44 +0200 Subject: [PATCH 07/33] specs: the design loop at the adopter's scale Lane L's design pass for plan 45, Specs only. - spec:consumers.context-bundle: a compact bundle. Each member and example row names its file once, a cut entry keeps only its line, each entry sits in one place, footing cuts to ids, and declarations: "addresses" opts in to a last cut of pinned declarations to their entry addresses. The declarationCut question is answered and deleted. - spec:consumers.agent-surface.register-recipes: recipe 41, the key-family register, over Design and UI keys only. - spec:consumers.agent-surface.recipe-parameters: a name a recipe does not take is answered as { unknownParams, accepts }, from a declared list each body opens with. Co-Authored-By: Claude Opus 5.5 --- .../agent-surface.recipe-parameters.sdp.md | 10 +++++++ .../agent-surface.register-recipes.sdp.md | 12 ++++++++ specs/consumers/context-bundle.sdp.md | 30 +++++++++++-------- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/specs/consumers/agent-surface.recipe-parameters.sdp.md b/specs/consumers/agent-surface.recipe-parameters.sdp.md index 1e980652..78a4dfbb 100644 --- a/specs/consumers/agent-surface.recipe-parameters.sdp.md +++ b/specs/consumers/agent-surface.recipe-parameters.sdp.md @@ -9,6 +9,8 @@ relations: --- # Recipes take parameters as data and ship as runnable files +A body reads its parameters by name, so a name it does not read would be dropped without a word, and a recipe with a sample would answer for the sample: a misspelled name would look like a successful call. A recipe therefore answers a name it does not take with that name and the names it does take, and nothing else. A body cannot import and the reader holds no recipe metadata, so each body declares its own names in its first statement, and the recipe check holds that declaration to the catalog. + ## Intent - problem: A recipe that takes its input in its own source makes a caller rewrite the body for each input, a long list included, and a script that runs a recipe must first cut its body out of the Markdown catalog. - outcome: Pass a recipe's parameter as data through the one evaluation sink, and ship every catalog body as a file a caller runs as written. @@ -18,4 +20,12 @@ relations: - rule: `--params` is repeatable in three forms: a JSON object, `@PATH` naming a file that holds one, and `KEY=@PATH` binding the JSON value in the file, of any type, to `params.KEY`, with `KEY` on the lower-camel grammar. Objects merge at the top level from left to right, and a key given twice by any form is refused before the body runs, with exit 1, one line on stderr and no output on stdout, so a recipe takes a large file such as a base graph beside its other parameters. - rule: The bindings `g`, `graph` and `report` keep their names and meaning beside `params`. The sink evaluates one operator-supplied body and adds no query vocabulary: a parameter is input to a body, never a verb. - rule: Every recipe reads its parameters under the names the catalog states. A parameter with a documented sample falls back to it when absent, so each body runs as written; a required input with no sample, such as a base graph, a proposal or an imports file, returns diagnostics when absent. Recipe 4 reads its changed files from `params.files`. +- rule: [#declaredNames] Each catalog body opens by declaring the parameter names it takes, in the order the catalog's index lists them, and checks `params` against that list before it reads anything else; a recipe that takes no parameter declares an empty list. +- rule: [#unknownNames] A recipe given a parameter name it does not take returns `{ unknownParams, accepts }` as its whole answer, and `sdp q` exits 0; it never answers for its sample, for an empty input or for the names it does take in that case. +- rule: [#namesChecked] The recipe check holds each body's declared names to the catalog's index and to the `params` names the body reads, and runs each body with a name it does not take. - rule: The package build writes each catalog body to `dist/recipes/-.js`, where `NN` is the two-digit recipe number and the slug is the recipe's heading in lower kebab case, and the package ships that directory. The catalog stays the one owner of the bodies; the files are derived, and the recipe test checks that each file equals its catalog body byte for byte. + +## Design + +- acceptsPreamble: `const accepts = [...]; const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); if (unknownParams.length > 0) return { unknownParams, accepts };` opens every catalog body as three statements on three lines, with the recipe's names in the brackets as double-quoted strings. +- unknownAnswer: `{ unknownParams, accepts }` is the whole answer for a name a recipe does not take, where `unknownParams` lists every such name in the order `params` holds them, and `accepts` repeats the recipe's declared names. diff --git a/specs/consumers/agent-surface.register-recipes.sdp.md b/specs/consumers/agent-surface.register-recipes.sdp.md index 0d6d816a..aa8da8b3 100644 --- a/specs/consumers/agent-surface.register-recipes.sdp.md +++ b/specs/consumers/agent-surface.register-recipes.sdp.md @@ -12,9 +12,13 @@ relations: reason: the open-question register lists each question's fields - target: spec:model.relation-reasons reason: the mention audit reads a caller's intended mentions with their reasons + - target: spec:extraction.entry-locations + reason: the key-family register gives each row the line the location table records --- # Register questions stay executable graph recipes +A corpus keeps registers of its own values, such as its limits, its error messages and its storage tables, as families of keyed Design entries whose keys share a prefix: `limit`, `error`, `table`. The key-family register lists one or more families across a scope each time it runs, so no one keeps those tables by hand. It reads Design and UI entries only. A rule or flow key names a law that an example exercises, and its text is a statement of law rather than a value two parties hold to; rule coverage and entry search already list rule keys, and a register that mixed laws with values would misread both. + ## Intent - problem: An adopter that keeps open-question tables, dependency lists and reference checks by hand lets each copy go stale. - outcome: Answer register, dependency, and mention questions with catalog recipes, so the tables an adopter would keep by hand are derived on demand. @@ -28,5 +32,13 @@ relations: - rule: Entry search gives an entry its address when one exists: `spec:#
.` for a `design` or `ui` key matching `^[a-z][A-Za-z0-9]*$` other than `description`, `spec:#question.` for the text of an open question that carries a key, and `spec:#rule.` for a keyed rule or flow; every other entry has `address: null`. - rule: Whole tokens are maximal runs of Unicode letters and digits, split at camelCase humps and compared without case; a multiword term matches only as a consecutive run in the same order inside one key or text. - rule: Pinned declarations reports each keyed Design entry whose value opens with a code span, giving the Spec, the key, and the span content as authored with the count of entries and of Specs, and parses no language inside the span. +- rule: [#keyFamily] The key-family register lists every keyed Design and UI entry whose key starts with one of the given prefixes, across the corpus, the members of one Pack, or a list of Specs, each with its Spec, section, key, value as the graph holds it, file and line, with no row cap. +- rule: [#familySections] The key-family register reads Design and UI keys only, never rule, flow or question keys, and never the `description` entry. - rule: Each body composes the reader and adds no reader join or CLI verb. - rule: The recipe check executes each catalog body as written, and the on-ramps point to the catalog for its contents. + +## Design + +- familyRecipe: recipe 41, the key-family register, takes `params.prefixes`, a non-empty list of unique non-empty strings, each matched case-sensitively against the start of a key as authored, with `["finding", "impact"]` when absent, and at most one scope, `params.pack` or `params.specs`, reading the whole corpus with neither and a listed Spec once however often it is listed; a malformed prefix list, both scopes, or a Pack or Spec id that does not resolve returns `{ diagnostics: [{ parameter, problem }] }` and no register, `parameter` naming the parameter at fault and `specs` when both scopes are given. +- familyShape: `{ prefixes, scope, totals: { rows, specs, byPrefix }, rows }` is the register, where `scope` is `{ corpus: true }`, `{ pack }` or `{ specs }`, `totals.specs` counts the Specs with a row, and `byPrefix` maps each prefix, in the given order, to the rows it matches, a key that matches two prefixes counting under each. +- familyRow: `{ key, spec, section, value, file, line }` is one row, with `section` either `design` or `ui`, and `line` null when the location table records no line for the entry; rows sort by key, then Spec id, then section, in code-unit order. diff --git a/specs/consumers/context-bundle.sdp.md b/specs/consumers/context-bundle.sdp.md index f151cbf0..0b0a5fa8 100644 --- a/specs/consumers/context-bundle.sdp.md +++ b/specs/consumers/context-bundle.sdp.md @@ -14,16 +14,13 @@ The original design precomputed graph slices for different reading scopes and a The first adopter's design passes assemble the same reading material by hand: the Pack design, member questions, dependency footing, pinned declarations and carrying Spec text where the graph answer falls short. Its perspectives read the same Pack for different concerns. The agent front door composes a bundle on each invocation; the bundle is never a committed answer in the graph's name. It builds on Pack design. +At an adopter's scale a bundle is mostly structure, so its shape is compact: each member and each example row names its carrier file once, a cut entry keeps only its line, since its place in the bundle already names it, and each entry appears in one place. Pinned declarations stay whole by default, because a builder and a test author hold to them. A caller whose scope still does not fit may let the bundle cut them to their entry addresses, and then reads each one in its Spec when the pass needs it. A partial bundle stays refused, because a bundle missing members would read as the whole scope. + ## Intent - actor: An agent starting one design pass over a Pack or an ordered list of Specs. - problem: Each pass assembles the same reading material by running the same recipes and joining their answers by hand, and nothing bounds what it hands the model. - outcome: Give an agent the reading material for one design pass in one composed slice of the graph within a stated budget, so no session hand-assembles it. -### Open questions -- [non-blocking #declarationCut] No cut reduces a pinned declaration, so a Pack whose pinned declarations alone exceed the budget fits no bundle; may a caller ask the bundle to reduce pinned declarations to their entry addresses? - - decider: owner - - provisional: pinned declarations stay whole, and a caller narrows the scope to fewer members, such as one perspective's - ## Behavior - rule: A bundle is a catalog recipe that takes its scope and its budget as recipe parameters; it adds no entrance and no query verb. - rule: A bundle is composed from the graph on each invocation by the reader every recipe uses; nothing is precomputed, saved or committed, and two invocations over the same graph with the same parameters compose the same bundle. @@ -31,7 +28,9 @@ The first adopter's design passes assemble the same reading material by hand: th - rule: With `pack`, a bundle may take `members`, an ordered subset of the Pack's member ids, which the perspective filter returns; it keeps the Pack's framing and boundary, lists only those members in that order, and states that it is a subset, so one filter implementation serves both recipes. - rule: A bundle for a Pack carries the Pack design assembly, its members in authored order with their design columns and its boundary, then each member's open questions with their addresses and lines, its dependency footing, its pinned declarations and its examples. - rule: The budget is a hard limit on a successful bundle, counted in UTF-16 code units, the `length` a recipe body reads, of the compact `JSON.stringify` of the value the recipe returns, its scope, budget and left-out statement included; `sdp q --json` prints a longer indented form, and the budget never measures printed text. The bundle also reports a token estimate labeled as an estimate, so no model's tokenizer binds the Protocol. -- rule: [#cutOrder] When the scope exceeds the budget, the bundle cuts in a fixed order: boundary rows down to their ids, member narrative, example step text, non-blocking questions, then remaining section values other than protected content. It never cuts member ids, the stated rung, the floor reached, the stated next rung, blocking questions, pinned declarations or entry addresses. +- rule: [#oneCopy] A bundle carries each entry in one place: a member's open questions only in its `questions`, a pinned declaration's value only in its member's Design section, and an example's steps only in its own member row when the example is a member of the bundle. +- rule: [#cutOrder] When the scope exceeds the budget, the bundle cuts in a fixed order and stops at the first cut after which it fits: boundary rows down to their ids, footing rows down to their ids, member narrative, example step text, non-blocking question text, remaining section values other than pinned declarations, then, only when the caller allows it, pinned declarations down to their entry addresses. It never cuts member ids, the stated rung, the floor reached, the stated next rung, blocking questions or entry addresses, and it cuts nothing else. +- rule: [#declarationAddresses] A caller allows the last cut by passing `declarations: "addresses"`; without it, or with `declarations: "whole"`, pinned declarations stay whole, and any other value is a diagnostic result. - rule: When the protected content alone exceeds the budget after every permitted cut, the recipe returns a budget-exceeded result as data and no partial bundle: the requested scope, the budget, and the minimum count the protected content needs in the same unit; `sdp q` exits 0, because the result is the body's return and not a refusal. - rule: A bundle gives the file and line of an entry or a binding in place of a source excerpt, because the reader never reads source. - rule: A bundle states its scope, its budget and what it left out to stay within the budget, so a reader knows what to read in the Specs themselves. @@ -39,10 +38,17 @@ The first adopter's design passes assemble the same reading material by hand: th ## Design -- bundleRecipe: recipe 36, the context bundle, takes exactly one scope, `params.pack` or `params.specs`, an optional `params.members` beside `pack`, and `params.budget`, 100,000 code units by default; a missing, doubled or unknown scope returns `{ diagnostics }` and no bundle. -- bundleShape: `{ scope, budget, used, tokenEstimate: { value, method }, leftOut, pack, members, boundary }`, where `pack` is `{ id, title, framing }` for a Pack scope and null otherwise, and `tokenEstimate.method` reads `UTF-16 code units divided by four, an estimate`. -- memberShape: `{ id, title, specKind, altitude, statedReadiness, floorReached, statedNextRung, holds, narrative, sections, questions, footing, declarations, examples }`, with questions carrying their address, blocking flag, line and fields, footing as dependency footing (recipe 21) reports it, declarations as pinned declarations (recipe 24) report them, and examples with their stated rung, whether they are enabled and their steps. `holds` lists the member's stated-next-rung failures as the reader returns them, each with its `clauseId` and `description`, and with `targets` on a typed-dependency failure. +- bundleRecipe: recipe 36, the context bundle, takes exactly one scope, `params.pack` or `params.specs`, an optional `params.members` beside `pack`, `params.budget`, 100,000 code units by default, and `params.declarations`, `"whole"` by default or `"addresses"`; a missing, doubled or unknown scope, or another `declarations` value, returns `{ diagnostics }` and no bundle. +- bundleShape: `{ scope, budget, declarations, used, tokenEstimate: { value, method }, leftOut, pack, members, boundary }`, where `declarations` is the mode in force, `pack` is `{ id, title, framing }` for a Pack scope and null otherwise, and `tokenEstimate.method` reads `UTF-16 code units divided by four, an estimate`. +- memberShape: `{ id, file, title, specKind, altitude, statedReadiness, floorReached, statedNextRung, holds, narrative, sections, questions, footing, declarations, examples, design, decisions, implementedBy, referencedBy, verifiers }`, with `file` the member's carrier path; `sections` as the graph holds them, without `intent.openQuestions`; `questions` one row per open question in authored order, `{ index, question, blocking, key, address, line }` with its fields, `key` absent and `address` null when it has none; `footing` as dependency footing (recipe 21) reports it; `declarations` the keys of the member's pinned declarations in Design order; `design` the counts of entries, declarations, open questions and blocking questions; `decisions` the decisions its design columns name; `implementedBy` and `referencedBy` as `{ id, claim, file, line, role, component }`; and `verifiers` as `{ id, claim, enabled, file, line }`. `holds` lists the member's stated-next-rung failures as the reader returns them, each with its `clauseId` and `description`, and with `targets` on a typed-dependency failure. +- exampleRow: `{ id, member, file, statedReadiness, enabled, steps }` lists one example that refines or verifies the member, in id order, with `member` true and `steps` null when the example is itself a member of the bundle, its steps then sitting in its own member row. +- pointer: `{ line }` replaces a cut entry that has no entry address, `line` being the line the location table records for it, or null when it records none; the entry's Spec and file are those of the row that holds it, and its place in the bundle names its path. - boundaryRows: a Pack scope's boundary is the Pack design's; a Spec-list scope's boundary is every Spec outside the list one authored relation away from a listed Spec, in the same row shape. -- fifthCut: The fifth cut reduces an addressable entry to its entry address and another entry to `{ spec, path, file, line }`, with `line` null when not located. Other entry addresses in removed text stay beside that pointer, and non-blocking questions retain their pointers and links; pinned declarations and blocking questions stay whole. -- cutEverywhere: every cut removes the affected text from each place it appears in the bundle, `sections` included, and blocking questions, pinned declarations and entry addresses stay whole in each place. -- budgetExceeded: `{ scope, budget, budgetExceeded: true, protectedLength, leftOut }` is the whole result when the protected content alone exceeds the budget, with `protectedLength` in the budget's unit. +- footingCut: `{ refines, dependsOn, constrainedBy, decidedBy }` replaces a member's footing at the footing cut, each a list of target ids in code-unit order, with a type that has no target left out. +- narrativeCut: The narrative cut replaces a member's narrative with the entry addresses written in it, in order of first appearance and joined by single spaces, or with null when it holds none. +- exampleCut: The example cut replaces each entry of a member's `behavior.examples`, and its `behavior.exampleSpace`, with its pointer, and each example row's `steps` that are not null with the pointer of that example's `behavior.examples`; entry addresses written in the cut text stay beside the pointer, in a list that ends with the pointer. +- questionCut: The question cut replaces each non-blocking question in `questions` with its address, or with its pointer when it has no key, and with `{ pointer, links }` when it has links, `pointer` being that address or pointer; blocking questions stay whole. +- sectionCut: The section cut replaces each section entry other than a pinned declaration with its entry address when it has one, and otherwise with its pointer; entry addresses written in a cut value stay beside its pointer, in a list that ends with the pointer. +- declarationCut: The declaration cut, taken only under `declarations: "addresses"`, replaces each pinned declaration in its member's Design section with its entry address, or with its pointer when its key has none; the member's `declarations` keeps the keys. +- leftOutLines: One fixed line per cut taken, in cut order, makes up `leftOut`: `Boundary details; ids kept per direction.`, `Footing details; ids kept per relation type.`, `Member narrative.`, `Example step text, including member sections and example-space vocabulary.`, `Non-blocking question text; addresses and links kept.`, `Section entry values other than pinned declarations; addresses and lines kept.` and `Pinned declaration text; entry addresses kept.` +- budgetExceeded: `{ scope, budget, declarations, budgetExceeded: true, protectedLength, leftOut }` is the whole result when the protected content alone exceeds the budget after every permitted cut, with `protectedLength` in the budget's unit. From 3b4a28dcb83a64322390152863d834f0a2d5d5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:37:16 +0200 Subject: [PATCH 08/33] specs: name a corpus finding's rule and rules file in the glossary Co-Authored-By: Claude Opus 5.5 --- CONTEXT.md | 2 +- specs/validation/corpus-rules.sdp.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index b67c1d71..f6e61dad 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -129,7 +129,7 @@ facts literal while the canonical recipe reports both audited exclusions. | **`validator`** | one individual deterministic check; groups into the two families below | — | | **conformance checks** | "is this *well-formed* against the meta-model?" — referential integrity · duplicate IDs · `claim` separation · grouping coherence · ambiguity-is-loud | — | | **honesty checks** | "is this *not pretending*?" — authoring-shape honesty (no hand-authored derived edges/facts) · honest readiness against the floor | — | -| **corpus rule** | an operator-selected graph body a corpus runs through its own `validate --rules` invocation, designed and not yet built; its findings name their corpus and sit outside the conformance and honesty families; neither a rule-kind Spec nor a Protocol validator (`spec:validation.corpus-rules`) | "corpus check" · "custom validator" | +| **corpus rule** | an operator-selected graph body a corpus runs through its own `validate --rules` invocation, designed and not yet built; its findings name their rule and rules file and sit outside the conformance and honesty families; neither a rule-kind Spec nor a Protocol validator (`spec:validation.corpus-rules`) | "corpus check" · "custom validator" | | **readiness floor** | the **minimum structural requirement to *state*** a readiness rung — a floor to clear, **never a quota to fill** or a score | "readiness profile" | | **`gap`** | a surfaced absence (e.g. `ready` with no verifier) — informative, never a gate | — | | **`orphan`** | a `Spec` with no relations and nothing pointing at it — informative | — | diff --git a/specs/validation/corpus-rules.sdp.md b/specs/validation/corpus-rules.sdp.md index e13cab62..119c8e3c 100644 --- a/specs/validation/corpus-rules.sdp.md +++ b/specs/validation/corpus-rules.sdp.md @@ -51,7 +51,7 @@ A rules file holds the body `sdp q` evaluates, so a rule tried through `sdp q` r - paramsFlag: `--params` on `validate` takes the three forms of `sdp q --params` with the same refusals, before the graph derives, and every rules file receives the one merged object as `params`, or `{}` without the flag. - rulesFile: a rules file is UTF-8 text holding one plain JavaScript async function body, the form `sdp q` evaluates, with the bindings `g`, `graph`, `report` and `params`; the runner reads the text and never imports it as a module, so no file extension is required and a catalog recipe file under `dist/recipes/` is a lawful rules file. - ruleAnswer: `{ findings: [] }`: the body returns an object whose `findings` is an array, possibly empty; the runner reads `findings` and nothing else, so the answer's other keys stay the body's own data and are never printed. -- ruleFinding: `{ validatorId, subjectId, severity, message, relatedId?, path?, file?, line? }`, where `validatorId` is the rule id, one token matching `^[a-z][a-z0-9-]*$`, so it never reads as a Protocol validator id; `subjectId` and `message` are non-empty strings; `severity` is `"error"` or `"warning"`; `relatedId` and `path` are strings; `file` is a root-relative POSIX path and `line` a positive integer that requires `file`; the runner ignores a finding's other keys. +- ruleFinding: `{ validatorId, subjectId, severity, message, relatedId?, path?, file?, line? }`, where `validatorId` is the rule id, one token matching `^[a-z][a-z0-9-]*$`, so it never reads as a Protocol validator id; `subjectId` and `message` are non-empty strings; `severity` is `"error"` or `"warning"`; `relatedId` and `path` are strings; `file` is a non-empty string read as a root-relative POSIX path, and `line` a positive integer that requires `file`; the runner ignores a finding's other keys. - corpusFinding: `{ rulesFile, validatorId, subjectId, severity, message, relatedId?, path?, file?, line? }` is the corpus branch of the finding currency, where `rulesFile` is the path as the operator named it; it carries no `family`, and the Protocol's findings keep their shape. - corpusSection: on standard error, after every Protocol finding, each rules file in the order named prints the line `corpus rules ` and then its findings in the order returned, each in the one composition of location, severity in brackets, rule id and message; a failed file prints `corpus rules failed: ` in place of its findings. - corpusSummary: `corpus rules: errors · warnings · of files failed (the corpus's own rules)`, written to standard output directly after the Protocol's summary line, which stays as `spec:validation.diagnostic-rendering#design.summaryLine` pins it; without `--rules` no such line prints, and when the graph does not derive the line reads `corpus rules: not run, the graph did not derive`. From e09c4a6da77213de54876ca1c2c42401937a84aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:47:30 +0200 Subject: [PATCH 09/33] specs: the Studio's static page set, split for honest binding The Studio's design pass. Six child Specs carry the static page set so its code can satisfy whole Specs: the publisher, search, the index, the Pack page, the Spec page's core and the page elements. The shell and the data Spec now describe only what that page set builds. Every provisional answer on the Studio's Specs becomes a rule or a keyed entry and its question is deleted, as plan 45's rulings delegate; the questions without one that the page set needs (header fields, rung colors, schema bindings, UI stories) are answered the same way. Phase 0 holds no AI slice. The vault is a projection of its own and waits for a reader. The data, element, verb and stamp contracts are keyed Design entries, pinned where two lanes must agree on a shape. Co-Authored-By: Claude Opus 5.5 --- CONTEXT.md | 9 ++-- ...gent-surface.example-space-coverage.sdp.md | 2 +- specs/consumers/obsidian-vault.sdp.md | 8 ++-- specs/consumers/spec-studio.components.sdp.md | 16 +++---- specs/consumers/spec-studio.data.sdp.md | 26 +++++------ specs/consumers/spec-studio.index-page.sdp.md | 37 +++++++++++++++ .../consumers/spec-studio.intent-panel.sdp.md | 11 +---- specs/consumers/spec-studio.lenses.sdp.md | 21 +++------ specs/consumers/spec-studio.pack-page.sdp.md | 43 +++++++++++++++++ specs/consumers/spec-studio.pack.sdp.md | 8 +++- .../spec-studio.page-elements.sdp.md | 33 +++++++++++++ specs/consumers/spec-studio.publisher.sdp.md | 46 +++++++++++++++++++ specs/consumers/spec-studio.responsive.sdp.md | 13 ++---- specs/consumers/spec-studio.sdp.md | 27 +---------- specs/consumers/spec-studio.search.sdp.md | 36 +++++++++++++++ specs/consumers/spec-studio.sharing.sdp.md | 8 +--- specs/consumers/spec-studio.shell.sdp.md | 39 +++++++++------- .../spec-studio.spec-page.core.sdp.md | 45 ++++++++++++++++++ specs/consumers/spec-studio.spec-page.sdp.md | 35 ++++---------- .../spec-studio.verification-panels.sdp.md | 21 +++------ test/recipes.test.ts | 6 ++- 21 files changed, 334 insertions(+), 156 deletions(-) create mode 100644 specs/consumers/spec-studio.index-page.sdp.md create mode 100644 specs/consumers/spec-studio.pack-page.sdp.md create mode 100644 specs/consumers/spec-studio.page-elements.sdp.md create mode 100644 specs/consumers/spec-studio.publisher.sdp.md create mode 100644 specs/consumers/spec-studio.search.sdp.md create mode 100644 specs/consumers/spec-studio.spec-page.core.sdp.md diff --git a/CONTEXT.md b/CONTEXT.md index b67c1d71..14a64e0b 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 its grouping is held open in `spec:consumers.spec-studio.lenses#question.capabilityGrouping` | a kind or an altitude | +| **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#rule.packGrouping`) | a kind or an altitude | ## Sections (→ `spec:model.spec-sections`) @@ -258,9 +258,10 @@ delivery fact, workflow state, or graph state. revisit when `doc:`-target relations or pack-targeting arrive. - **"vault"** is Obsidian's word for a folder of linked notes, used by the `idea` captures in - `pack:obsidian-vault-v1` for a projection regenerated from the graph. Whether that projection - stands alone or is the Studio's local half is open (`spec:consumers.obsidian-vault`), and the - term waits on that ruling; a **term address** (`spec:model.term-addresses`) is a candidate too. + `pack:obsidian-vault-v1` for a projection regenerated from the graph. That projection stands + alone, beside the Studio and not as its local half (`spec:consumers.obsidian-vault`), and waits + for a reader, so the term stays unratified until it is built; a **term address** + (`spec:model.term-addresses`) is a candidate too. ## Term ledger (locked / rejected / resolved) diff --git a/specs/consumers/agent-surface.example-space-coverage.sdp.md b/specs/consumers/agent-surface.example-space-coverage.sdp.md index 70f2c9d1..61ca747e 100644 --- a/specs/consumers/agent-surface.example-space-coverage.sdp.md +++ b/specs/consumers/agent-surface.example-space-coverage.sdp.md @@ -21,7 +21,7 @@ The original design rendered a coverage heatmap from each example space: which c ### Open questions - [non-blocking #combinationCoverage] Is coverage per slot value enough, or does a design pass need the combinations of two or more slots, as the original heatmap drew them? - decider: owner - - links: spec:consumers.spec-studio.verification-panels#question.coverageSource + - links: spec:consumers.spec-studio.verification-panels#ui.coverageGrid - provisional: the recipe reports per-slot literal-union values - [non-blocking #coverageShape] What reader field names and decoded slot and bound-point shapes, recipe parameters and result rows, eligible-example relation policy, and treatment of an unbound slot form the coverage contract? - decider: owner diff --git a/specs/consumers/obsidian-vault.sdp.md b/specs/consumers/obsidian-vault.sdp.md index cebce97e..050df2c2 100644 --- a/specs/consumers/obsidian-vault.sdp.md +++ b/specs/consumers/obsidian-vault.sdp.md @@ -16,7 +16,7 @@ relations: --- # An Obsidian vault lets a person iterate on designs from the one graph -The Design Review links a Spec's relations both ways and gives keyed Pack questions a separate carrier-line link, but it offers no built-in filtering of a Pack or navigation from arbitrary prose mentions to their entries. The Spec Studio is designed for both a person who iterates on a design in the repository and a pull request reviewer who holds only a link. A linked-notes view would let a person follow relations, filter members by rung and design columns, open entries by address and see each pass's edits arrive; whether it is the Studio's local half is the question below. +The Design Review links a Spec's relations both ways and gives keyed Pack questions a separate carrier-line link, but it offers no built-in filtering of a Pack or navigation from arbitrary prose mentions to their entries. The Spec Studio is designed for both a person who iterates on a design in the repository and a pull request reviewer who holds only a link. A linked-notes view would let a person follow relations, filter members by rung and design columns, open entries by address and see each pass's edits arrive. It is a projection of its own, not the Studio's local half: the Studio's page set serves a reader who holds a link, the vault serves a person who iterates in a notes app, and each has its own verb and root. It waits for a reader who asks for it once the Studio's page set exists. Obsidian already does that for linked Markdown notes with properties: backlinks, a graph view, search, and filterable tables it calls Bases. The prototype in `explorations/obsidian-vault/` wrote notes from dumped graph and reader values, with relations and checked mentions as wikilinks, entry addresses as block links, and Bases for Pack members and standing queries. Pack members could be read as a sortable table. The prototype joined relation lists and computed flat properties itself, and its dump renamed `statedNextRungFailures` to `nextRungFailures`; those shortcuts are evidence for the product design, not the reader's contract. @@ -30,14 +30,12 @@ The principle is a linked-notes view regenerated from the one graph; Obsidian an - risk: A notes app invites editing, and an edit to a generated note is lost at the next regeneration without reaching the carrier. ### Open questions -- [blocking #studioRelation] Is the vault a projection of its own, or the Studio's local half beside the static page set a reviewer with only a link reads? - - decider: owner - - links: spec:consumers.spec-studio - - provisional: a projection of its own, written by its own verb under `generated/obsidian-vault/`, which the Studio's design pass may adopt as its local half - [non-blocking #otherNotesTools] Does the vault keep to the wikilinks and properties other notes tools read, or use Obsidian's Bases freely? - provisional: notes and links stay plain Markdown with wikilinks any such tool reads, and the Bases are Obsidian's alone - [blocking #regenerationScope] Which files does the generator own, how are stale generated files removed, and where do Obsidian settings and a person's intent notes live so that regeneration and build invalidation never remove them? - decider: owner - provisional: the generator owns only its generated note folders and Bases; Obsidian settings and authored intent notes sit outside that owned set, and a completed regeneration removes stale generated notes + - trigger: a person asks for a vault beside the Studio's static page set ## Behavior +- rule: [#ownProjection] The vault is a projection of its own, written by its own verb under `generated/obsidian-vault/`, and no part of the Studio's page set. diff --git a/specs/consumers/spec-studio.components.sdp.md b/specs/consumers/spec-studio.components.sdp.md index 12b8ab92..c5232b0d 100644 --- a/specs/consumers/spec-studio.components.sdp.md +++ b/specs/consumers/spec-studio.components.sdp.md @@ -10,28 +10,24 @@ relations: --- # The Studio's custom elements each render one view in pages that load their script and supply graph data +The original's harness element has no subject in the graph, which holds example spaces, bound points and oracle bindings but no harness node, so its successor takes the Spec that owns an example space. No diagram renderer is chosen yet: the design that adds the first diagram names one within the custom-elements decision. + ## Intent - outcome: Give the Studio a small set of custom elements that each render one view of the graph for a Studio page or an HTML report that can load the element script and supply its graph data. -### Open questions -- [non-blocking #embeddedDiagrams] The original embeds interactive LikeC4 diagrams through an element that wraps LikeC4's own runtime; a LikeC4 export is designed-for and deferred, and the shipped Mermaid projection draws bounded one-hop Spec and Pack diagrams and never the whole graph. Which renderer draws a Studio diagram, and does that bound hold for it? - - decider: owner - - provisional: a later phase picks a renderer compatible with `spec:decisions.studio-web-components`, or a decision that refines it admits a framework runtime -- [non-blocking #harnessElement] The original's harness element takes a harness id and keeps its coverage current as a person works it; the graph holds example spaces, bound points and oracle bindings, and has no harness node and no `harness:` namespace. What does the element take as its subject? - - decider: owner - - provisional: a later-phase element takes `spec-id` naming the Spec that owns the example space and renders that space's dimensions, its children's bound points and its expected-outcome oracle; coverage stays a report, never a verifier run or a delivery fact ## Contract - Each element is self-contained and light on dependencies, and works in an HTML page that can load its script and supply its graph data. -- Each element reads the one data object its page sets, as `spec:consumers.spec-studio.data` states. An element with a graph subject takes that subject's graph id in an attribute named by its Design entry; an element over accumulated intents has no single graph subject, and a renderer-specific subject waits for its named design question. +- An element that renders a view of the graph reads the one data object its page sets, as `spec:consumers.spec-studio.data` states. An element with a graph subject takes that subject's graph id in an attribute named by its Design entry; an element over accumulated intents has no single graph subject. - Every element's name starts with `sdp-`. +- [#diagramRenderer] A diagram is drawn by a renderer that runs as a custom element under `spec:decisions.studio-web-components`, or by a framework runtime that a decision refining it admits; the design that adds the first diagram names the renderer and the bound on what one diagram draws. ## Design The elements in the original's order, with their attributes. - specCard: `sdp-spec-card` with `spec-id`, `variant` and `readiness-pill`: one Spec as a card, in full or short form, with its readiness pill shown or hidden. - traceGraph: `sdp-trace-graph` with `spec-id`, `depth` and `highlight`: the Spec's relations drawn as SVG to the given depth, highlighting what `highlight` names, such as Specs with no verifier binding. - maturityMap: `sdp-maturity-map` with `pack-id` and `group-by`: a Pack's members by readiness, grouped by the named field of the member values, such as kind or stated rung. -- harness: an interactive element over an example space; its name, subject attribute and coverage behavior await `spec:consumers.spec-studio.components#question.harnessElement`. -- diagramView: a diagram element whose renderer, name and subject attribute await `spec:consumers.spec-studio.components#question.embeddedDiagrams`. +- exampleSpace: `sdp-example-space` with `spec-id`, naming the Spec that owns the example space: the space's dimensions, its children's bound points and its expected-outcome oracle binding; its coverage is a report, never a verifier run or a delivery fact. +- diagramView: a diagram element whose name and subject attribute the design that adds it names, under the diagram-renderer rule. - scenarioEditor: `sdp-scenario-editor` with `spec-id` and `mode`: a given, when and then editor for one example, which in `propose` mode composes intent for a new one. - impactView: `sdp-impact-view` with `spec-id`: a Spec's impact panel on its own. - intentPanel: `sdp-intent-panel` with no graph-subject attribute, rendering the gathered intents across their scopes. diff --git a/specs/consumers/spec-studio.data.sdp.md b/specs/consumers/spec-studio.data.sdp.md index cf0d2cdd..3cc235e4 100644 --- a/specs/consumers/spec-studio.data.sdp.md +++ b/specs/consumers/spec-studio.data.sdp.md @@ -13,22 +13,22 @@ relations: - spec:extraction.entry-locations constrainedBy: spec:extraction.determinism --- -# The Studio hydrates from the serialized graph and the reader's values +# The Studio renders from the reader's values and embeds only what its script reads + +The build writes each value into the page's markup, so a page reads in full as HTML. Search is the exception: a browser refuses a fetch from a page opened through `file://`, so the search page carries its index inside it. On the first adopter's corpus that index is too large to repeat on every page, which is why search runs on a page of its own. ## Intent -- outcome: Give every Studio page its data as files the build writes beside it, so the page reads the graph the agent surface reads and derives nothing of its own. -### Open questions -- [non-blocking #aiSlices] The original hydrates the Studio from AI slices beside the graph and the validation report; the context bundle is a catalog recipe composed on each invocation and never saved, so no Spec states a saved slice. Does the Studio's data hold a slice, and which Spec would state it? - - decider: owner +- 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 -- The Studio hydrates from data files under `generated/spec-studio/data/`, written by the build that writes its pages. -- Each data file holds values the graph serializes or the reader returns, and the page follows `spec:consumers.spec-studio`'s rule against recomputing graph joins, claims, delivery facts, readiness and floor failures. -- A page embeds the data it reads in one `script` element of type `application/json` with the id `sdp-graph`, parses it once into `window.__sdp__.graph`, and every element on the page reads from that one object. +- [#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, 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`, written with every `<` as `<` 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. ## Design -- graphFile: the serialized graph exactly as `sdp build` writes `generated/graph.json`, with its `schemaVersion`. -- reportFile: the validation report, the findings the reader returns from `findings()`. -- specContexts: for each Spec, the reader's `specContext` value whole: descriptors, narrative, sections, `designColumns`, stated and derived readiness, `floorFailures`, the stated next rung with `statedNextRungFailures`, the floor's `nextRungFailures`, relations both ways with their reason and exercises when present, implementations, references, verifiers, the oracle binding when present, `entryLocations` and findings. -- packContexts: for each Pack, the reader's `packContext` value whole: framing, model references, members in authored order with each member's stated next rung, design columns and bindings, verifier gaps, boundary and findings. -- searchIndex: for each node, the fields the reader's `findByConcept` matches: id, title, anchor label, Pack framing, narrative and section content, so a search on the page matches what the concept entry matches. +- 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. +- railValues: `{ packs: { id: string; title?: string; members: { id: string; title?: string; statedReadiness?: string; ownsExampleSpace: boolean }[] }[]; findings: { errors: number; warnings: number } }`, the values every page's rail renders: each Pack in id order with its members in authored order, a member owning an example space when its Spec context carries one, and the validation report's findings counted by severity. +- searchIndex: `{ records: SearchRecord[] }`, one record per graph node, in id order. +- searchRecord: `{ id: string; nodeType: "Primitive" | "Pack" | "Anchor" | "CodeNode"; page: string | null; title?: string; label?: string; framing?: string; narrative?: string; sections?: Record; file?: string; line?: number; specKind?: string; statedReadiness?: string; floorReached?: string | null; bindings: { edge: string; specId: string }[]; entries: SearchEntry[] }`: `title`, `label`, `framing`, `narrative` and `sections` exactly as the graph node holds them, the fields `findByConcept` reads; `page` the node's page path in the page set, null for an Anchor or a CodeNode; `file` and `line` as recorded; the kind and the two rungs on a Spec only, `floorReached` null when no rung derives; `bindings` the Specs an Anchor's or CodeNode's outgoing edges reach, with the edge type, and empty for any other node. +- searchEntry: `{ section: string; entry: string | null; key: string | null; address: string | null; line: number | null; text: string }`, one per entry recipe 23's walk visits in a Spec's narrative and sections, in that walk's order, with that recipe's entry path, coined key, address and line; a value that is not a string is its JSON text. diff --git a/specs/consumers/spec-studio.index-page.sdp.md b/specs/consumers/spec-studio.index-page.sdp.md new file mode 100644 index 00000000..5e7ec3f8 --- /dev/null +++ b/specs/consumers/spec-studio.index-page.sdp.md @@ -0,0 +1,37 @@ +--- +id: spec:consumers.spec-studio.index-page +kind: behavior +altitude: story +readiness: idea +relations: + refines: + target: spec:consumers.spec-studio.lenses + reason: the index is where the Packs lens lands + dependsOn: + - spec:consumers.spec-studio.data + - spec:consumers.spec-studio.shell + - spec:consumers.derived-readiness-banner + - spec:consumers.binding-language-views +--- +# The index says where the corpus stands and narrows every Spec + +A reader of a large design needs two ways in: by the task they came to do and by a word they remember. The index gives the first and links to search for the second, opens with a summary the build counts, and replaces one long table with a list a person narrows. + +## Intent +- outcome: Give a person one landing page that says where the corpus stands, routes them by task, lists every Pack with its members, and narrows every Spec by the coordinates and bindings they ask about. + +## Behavior +- rule: [#summary] The page opens with a summary the build counts from reader values, each count in a sentence: the Specs and Packs; the Specs at each stated rung; the Specs stated above their floor; the open and blocking questions; the Specs with an implementation binding and with a verifier binding; and the findings by severity. +- 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: [#findings] The findings section lists the validation report's findings in its order, each with its severity in words, its validator id, its subject linked to its page, its message, and its recorded file and line as text. + +## UI +- summary: a short list of sentences under the page title, each with a count. +- taskRoutes: four or five links under a `Start here` heading. +- packList: one closed disclosure per Pack, its summary line the Pack's title, id and member count, its body the members in authored order as links with their rung badges. +- specRow: two lines per Spec, the title linked to its page with the id beside it, then badges for kind, altitude, stated rung and floor, the two binding values, the question counts and the Packs. +- filterBar: one labelled select per filter above the Specs, each listing any and the values the rows hold, and a line saying how many of all rows show. +- findingsList: one line per finding, the severity word first. diff --git a/specs/consumers/spec-studio.intent-panel.sdp.md b/specs/consumers/spec-studio.intent-panel.sdp.md index 4187c6c3..1dd9fa5d 100644 --- a/specs/consumers/spec-studio.intent-panel.sdp.md +++ b/specs/consumers/spec-studio.intent-panel.sdp.md @@ -14,15 +14,6 @@ relations: ## Intent - outcome: Let a person gather the changes they want while they read, each bounded by a Spec, its neighbors, a Pack or one open question, and hand them to an agent that edits source. -### Open questions -- [non-blocking #proposalBase] The change-proposal contract requires the commit whose graph the person read, and a generated page set carries no commit; where does the panel get the proposal's base, and does it ask the person for a reason? - - decider: owner - - links: spec:consumers.intent-composition.proposal - - provisional: the base is the commit a present build stamp names, otherwise one the person supplies, and the person supplies one reason for the whole proposal; without a base and a reason the panel exports scoped intent as text, and the receiving agent writes the proposal against a known commit -- [non-blocking #packScope] The change-proposal contract scopes a change by Spec ids and entry addresses only, while the panel gathers intent bounded by a Pack or a Spec's neighbors; does a Pack or neighbors scope expand to its member or neighbor Spec ids at hand-off, or does a later proposal schema admit Pack scopes? - - decider: owner - - links: spec:consumers.intent-composition.proposal - - provisional: the ready proposal contract stands; a Pack-scoped request expands to the member Spec ids read from that Pack at the proposal base, and a request that changes Pack membership or framing, or names an empty Pack, stays scoped-intent text ## Behavior - rule: The composing panel is visible on every page. @@ -31,6 +22,8 @@ relations: - rule: Gathered intents persist in the browser's local storage, so refreshing a tab on a phone loses none. - rule: A composed intent names its scope by id or entry address, so the agent that receives it reads the same Spec, Pack, entry or question through the graph. - rule: The panel hands its gathered intents off as one change proposal, on the change-proposal contract, which the person copies as text or saves as a document. The page opens no pull request, because a static page holds no credentials; any pull request comes from the agent's ordinary source edit. +- rule: [#proposalBase] A proposal's base is the commit a present build stamp names, otherwise one the person supplies, and the person gives one reason for the whole proposal; without a base and a reason the panel exports its intents as scoped-intent text, and the agent that receives them writes the proposal against a commit it knows. +- rule: [#packScope] A request scoped to a Pack expands at hand-off to the member Spec ids that Pack holds at the proposal's base; a request that changes a Pack's membership or framing, or names an empty Pack, stays scoped-intent text, so the change-proposal contract keeps its Spec and entry scopes. ## UI - gatheredIntents: the intents gathered so far, headed with their count, each on one line naming its scope and the change it asks for, such as adding an example or changing a constraint's target. diff --git a/specs/consumers/spec-studio.lenses.sdp.md b/specs/consumers/spec-studio.lenses.sdp.md index 33de7949..ca95da3e 100644 --- a/specs/consumers/spec-studio.lenses.sdp.md +++ b/specs/consumers/spec-studio.lenses.sdp.md @@ -20,31 +20,24 @@ relations: --- # Four lenses read the graph as Packs, architecture, tests and evidence +A capability is read as a Pack, or as a projection over high-altitude behavior Specs that no view draws yet, so the Packs lens groups by Pack and draws no capability tree. A verifier binding states that a verifier exists, and pass and fail stay in CI, so the tests lens shows bindings and no run result. + ## Intent - outcome: Let a person see the graph four ways, by Pack, by architecture, by verification and by evidence, from one top navigation. ### Open questions -- [non-blocking #capabilityGrouping] A capability is a projection over high-altitude behavior Specs or a Pack grouping, and an arc's Pack is named for the capability it designs. Which grouping does the capability tree use, and which graph values supply it? - - decider: owner - - provisional: the Packs lens groups Specs by Pack, and no capability tree is drawn - [non-blocking #architectureVocabulary] The original's architecture lens draws runtime layers, ports and external systems beside components and their dependencies; `spec:decisions.architectural-annotation` gives a component anchor a layer and a bounded context and any code anchor a role, beside component membership and `uses`, and admits no port or external-system node. Does the lens group components by layer and context and draw only what those anchors hold? - links: spec:consumers.structural-mermaid#question.layerAndContext -- [non-blocking #testRunResults] The original's tests lens shows each test's last run result; a verifier binding states that a verifier exists, and pass and fail stay in CI, as `spec:decisions.binding-not-liveness` rules. Does the lens show run results read from CI output, or bindings only? - - decider: owner - - links: spec:observation.run-evidence - - provisional: The first Studio build shows bindings only; admitting run records in a later phase awaits an explicit input and reproducibility ruling. -- [non-blocking #ruleCoverage] The original's tests lens names the rules of a Spec that no test exercises; a rule may carry a key, an example's `verifies` may name the keys it exercises, and recipe 34 lists each keyed rule with its examples, while the graph cannot tell which examples exercise a rule with no key. Does the lens show keyed-rule coverage with the unkeyed rules counted apart, or name the uncovered Specs only? - - decider: owner - - links: spec:model.rule-keys - - provisional: the tests lens lists each keyed rule with the examples recipe 34 reports and counts the unkeyed rules apart ## Behavior - rule: The Packs lens is the default landing page. +- rule: [#packGrouping] The Packs lens groups Specs by Pack and draws no capability tree. - rule: Clicking a component in the architecture lens opens its detail. +- rule: [#bindingsOnly] The tests lens shows verifier bindings and no run result; run records join it only through `spec:observation.run-evidence`, under an input and reproducibility ruling of their own. - rule: The evidence lens is read-only. ## UI The lenses in the order of the top navigation. -- packsLens: a Spec-by-Spec tree grouped by Pack, with each Pack's members in authored order; any capability grouping awaits `spec:consumers.spec-studio.lenses#question.capabilityGrouping`. -- architectureLens: the graph drawn as components and their dependencies, from component membership and `uses` edges, in an embedded interactive diagram whose renderer is the open question `spec:consumers.spec-studio.components#question.embeddedDiagrams`. -- testsLens: a coverage view: for each Spec, its verifiers with their bindings, and the rules no example exercises. +- packsLens: a Spec-by-Spec tree grouped by Pack, with each Pack's members in authored order, which the index page holds. +- architectureLens: the graph drawn as components and their dependencies, from component membership and `uses` edges, in an embedded interactive diagram whose renderer follows `spec:consumers.spec-studio.components#rule.diagramRenderer`. +- testsLens: a coverage view: for each Spec, its verifiers with their bindings, then each keyed rule with the examples recipe 34 reports as exercising it, with the unkeyed rules counted apart. - evidenceLens: the graph-recorded evidence fields and absence labels `spec:consumers.spec-studio.verification-panels` states; while the graph records no observation or other evidence, each reads not tracked. diff --git a/specs/consumers/spec-studio.pack-page.sdp.md b/specs/consumers/spec-studio.pack-page.sdp.md new file mode 100644 index 00000000..242f0f36 --- /dev/null +++ b/specs/consumers/spec-studio.pack-page.sdp.md @@ -0,0 +1,43 @@ +--- +id: spec:consumers.spec-studio.pack-page +kind: behavior +altitude: story +readiness: idea +relations: + refines: spec:consumers.spec-studio + dependsOn: + - spec:consumers.spec-studio.data + - spec:consumers.spec-studio.shell + - target: spec:consumers.pack-design + reason: the page renders the Pack design the Pack context carries + - spec:consumers.derived-readiness-banner + - spec:consumers.binding-language-views + - spec:model.open-question-fields +--- +# A Pack page shows the Pack's design as one card per member + +The Markdown Pack page renders the Pack design as one wide member table, and the first adopter's reviewers read carrier files instead. This page renders the same assembly as a summary, then a card per member that a reviewer opens where they need the detail. + +## Intent +- 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, its carrier file as text with a copy action, its framing as prose, and its vocabulary references, linked. +- rule: [#summary] A summary follows with each of the Pack totals in a sentence: the members and the unresolved ones, the members held below their stated next rung and those waiting only for their author, the design entries, pinned declarations, open and blocking questions, the members with implementing and with referencing units, and the boundary in each direction. +- 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 clauses hold it, 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; each clause that holds it below its stated next rung, with the targets the clause names and their stated rungs; its counts of design entries and pinned declarations, and the decisions it names, linked, each with its stated rung; each open question with its blocking flag in words, its address linked to the question on the Spec page, its recorded line, its text, and its decider, links, provisional reading and trigger when present; each unit that implements it and each that references it, by id, label, and recorded file and line as text with a copy action, with its role and its component's id, layer and context; and its verifiers and verifying examples, each count beside how many are enabled. +- rule: An unresolved member's card says only that the manifest names an id the graph does not hold. +- rule: [#holds] After the cards, the page lists each member held below its stated next rung with every clause and its targets, then names the members whose next rung waits only for their author's statement; it never presents a rung as approved. +- rule: [#boundary] The boundary follows in four closed sections, Rests on, Rested on by, Verified by and Superseded by, each headed with its count. Each row shows the outside Spec, linked, its stated rung, its implementation binding as present or none, and the members it joins under each relation type; an empty section reads None. +- rule: Then come the members with no verifier binding, those stated `ready` first, and the findings that name the Pack. +- rule: [#openAll] One control opens or closes 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; the framing; the vocabulary references. +- summaryList: the totals as a short list of sentences, each count first. +- memberCard: a disclosure whose element id is the member's Spec id; its summary line reads `. `, then badges for kind, stated rung and floor, the next rung in words, the question counts and the two binding values, wrapping onto further lines on a narrow screen. +- cardBody: in order, the id link, the holds, the design columns and decisions, the open questions, the code, and the verifiers. +- questionItem: the blocking flag in words, the address as a link, `line <n>` when recorded, the text, then each present field on its own labelled line. +- unitItem: the unit id as code, its label, `<file>:<line>` with a copy action, its role, and its component with layer and context. +- holdsList: one entry per held member with its clauses; then one sentence naming the members that wait for their author. +- boundaryTable: four columns, Spec, stated rung, implementation binding and joined members, the last listing each relation type with its members. diff --git a/specs/consumers/spec-studio.pack.sdp.md b/specs/consumers/spec-studio.pack.sdp.md index ec28d473..eb8d9193 100644 --- a/specs/consumers/spec-studio.pack.sdp.md +++ b/specs/consumers/spec-studio.pack.sdp.md @@ -4,13 +4,19 @@ specs: - spec:consumers.spec-studio - spec:decisions.studio-html-surface - spec:consumers.spec-studio.data + - spec:consumers.spec-studio.publisher - spec:consumers.spec-studio.shell + - spec:consumers.spec-studio.search - spec:consumers.spec-studio.lenses + - spec:consumers.spec-studio.index-page + - spec:consumers.spec-studio.pack-page - spec:consumers.spec-studio.spec-page + - spec:consumers.spec-studio.spec-page.core - spec:consumers.spec-studio.verification-panels - spec:consumers.spec-studio.intent-panel - spec:consumers.spec-studio.components - spec:decisions.studio-web-components + - spec:consumers.spec-studio.page-elements - spec:consumers.spec-studio.responsive - spec:consumers.spec-studio.sharing modelRefs: @@ -18,4 +24,4 @@ modelRefs: --- # Spec Studio -The Spec Studio of the original design, `docs/lineage/v0-design/07-spec-studio-and-projections.md` sections 1 to 3, 11 and 12, in the order a reviewer reads it: what the Studio is and why it renders HTML, the data it reads, its shell and lenses, the Spec page and its panels, the composing panel, its custom elements, its phone layout, and how a build is shared. Recipe 27, references into a design, shows each member's code bindings and the implemented Specs it builds on. +The Spec Studio of the original design, `docs/lineage/v0-design/07-spec-studio-and-projections.md` sections 1 to 3, 11 and 12, in the order a reviewer reads it: what the Studio is and why it renders HTML, the data it reads and the verb that publishes it, its shell and search, its lenses and the index, the Pack page, the Spec page with its core and its panels, the composing panel, its custom elements, its phone layout, and how a build is shared. The publisher, the shell, search, the index, the Pack page, the Spec page's core and the page elements make up the static page set; the other panels, the other lenses and the composing panel build on it. Recipe 27, references into a design, shows each member's code bindings and the implemented Specs it builds on. diff --git a/specs/consumers/spec-studio.page-elements.sdp.md b/specs/consumers/spec-studio.page-elements.sdp.md new file mode 100644 index 00000000..93ebf24f --- /dev/null +++ b/specs/consumers/spec-studio.page-elements.sdp.md @@ -0,0 +1,33 @@ +--- +id: spec:consumers.spec-studio.page-elements +kind: contract +altitude: story +readiness: idea +relations: + refines: spec:consumers.spec-studio.components + dependsOn: + - spec:consumers.spec-studio.shell + - spec:consumers.spec-studio.search + decidedBy: spec:decisions.studio-web-components +--- +# A few custom elements filter, search, open and copy on a rendered page + +The build renders every page whole, so these elements add behavior to markup that is already there rather than rendering views of their own. A browser refuses a module script on a page opened through `file://`, so they ship as one classic script. + +## Intent +- outcome: Give the Studio's pages their few interactions through custom elements that enhance markup the build wrote, so a page with its script filters, searches and copies, and a page without it still reads in full. + +## Contract +- [#oneScript] The elements ship in one classic script, `assets/studio.js`, with no module syntax and nothing loaded at run time; the build bundles it from the package's source, with the concept matcher. +- [#enhanceOnly] Each element enhances markup the build wrote and renders no graph value of its own, except that the search results element renders values from the search index its page embeds. +- [#noRequest] No element requests anything on a page opened through `file://`; the stamp element requests `stamp.json` only over http or https. +- [#nativeControls] Every control an element adds is a native button, link, select or input with a text label, reachable from the keyboard in reading order. +- [#liveCounts] An element that changes what a page shows says so in words in a polite live region, such as how many rows a filter shows. + +## 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-facet="<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. +- 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. +- 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 new file mode 100644 index 00000000..4384cd87 --- /dev/null +++ b/specs/consumers/spec-studio.publisher.sdp.md @@ -0,0 +1,46 @@ +--- +id: spec:consumers.spec-studio.publisher +kind: behavior +altitude: story +readiness: idea +relations: + refines: spec:consumers.spec-studio + dependsOn: + - spec:consumers.spec-studio.data + - target: spec:consumers.wholesale-view-rewrite + reason: the Studio's root is written, invalidated and certified the way the shipped roots are + constrainedBy: spec:extraction.determinism + decidedBy: + target: spec:decisions.shipped-projections-frozen + reason: the Studio publishes to a root of its own beside the four frozen ones +--- +# `sdp studio` publishes the Studio as one page set + +The Studio has its own verb and root beside the four shipped publishers, and is written the way they are. Its page set opens from the file system and uploads to a static host unchanged. The run adds no stamp, commit or time, so two runs over one graph write the same bytes, and a publishing step that wants build context writes its stamp beside the page set. + +## Intent +- outcome: Publish the Studio under its own root as one page set the graph alone determines, which opens through `file://` and uploads to a static host unchanged. + +## Behavior +- rule: [#verb] `sdp studio [root] [--exclude PATH]... [--check-clean]` validates the graph and writes the Studio's page set to `<root>/generated/spec-studio/`; `root` defaults to the working directory, and each exclusion is a root-relative path prefix, as for the shipped publishers. +- rule: [#ownRoot] A run writes nothing outside its root, writes no stamp, and leaves every carrier and source file under the root byte-identical. +- rule: [#wholesale] A run writes its page set to `generated/spec-studio.tmp/`, removes the prior root and renames the temporary into place, so no page of an earlier run survives a later one and no temporary survives a completed run. +- 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 on the page of its subject. 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: [#deterministic] Two runs over one graph write byte-identical files, and no file holds a time, a commit, an absolute path or a host name. +- 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. No page links a carrier or source file, an editor URI or an external address. + +## Example space +```gwt-vocabulary +Given an extraction root holding {corpus:"a Pack and its member Specs"|"a Spec with a validation error"|"a carrier the extractor refuses"} and a stale Studio page {stalePage:string} +When the studio command runs twice at that root, with check-clean {checkClean:boolean} +Then each run exits {exitCode:number} +Then the page set holds the entry page {entryPage:string} +Then the two runs wrote byte-identical files: {byteIdentical:boolean} +Then the stale page survives: {staleSurvives:boolean} +Then a temporary Studio sibling survives: {temporarySurvives:boolean} +Then every carrier and source file under the root stays byte-identical: {rootUntouched:boolean} +Then every link a page writes resolves inside the page set: {linksResolve:boolean} +``` diff --git a/specs/consumers/spec-studio.responsive.sdp.md b/specs/consumers/spec-studio.responsive.sdp.md index 5b787cb3..727964d6 100644 --- a/specs/consumers/spec-studio.responsive.sdp.md +++ b/specs/consumers/spec-studio.responsive.sdp.md @@ -9,24 +9,19 @@ relations: --- # The Studio reads on a phone as on a desktop +The curated graph's edges carry no confidence, and the impact graph's inferred imports stay outside it, so a compact diagram shortens by distance alone. + ## Intent - actor: A person reviewing a design on a phone, including a pull request reviewer who holds only a link. - outcome: Let a design be reviewed on a phone through the same pages a desktop shows. -### Open questions -- [non-blocking #summaryMode] The curated graph's edges carry no confidence, and the impact graph's file-level inferred imports stay outside it; which details does a compact diagram presentation hide? - - decider: owner - - provisional: a compact diagram drops relations past one hop -- [non-blocking #phoneBreakpoint] Which exact CSS viewport width switches the layout to one column? - - decider: owner - - provisional: 720 CSS pixels ## Behavior - rule: Every page of the Studio is responsive, so the same pages serve a desktop and a phone. ## UI -- singleColumn: every layout collapses to one column below the breakpoint settled by `spec:consumers.spec-studio.responsive#question.phoneBreakpoint`. +- singleColumn: every layout collapses to one column below 720 CSS pixels of viewport width. - pinchZoom: on a touch device, every diagram zooms with a pinch. -- summaryMode: a compact diagram presentation for narrow screens, whose contents await `spec:consumers.spec-studio.responsive#question.summaryMode`; it assigns no confidence to an edge. +- summaryMode: a compact diagram presentation for narrow screens that drops relations past one hop and assigns no confidence to an edge. ## Example space ```gwt-vocabulary diff --git a/specs/consumers/spec-studio.sdp.md b/specs/consumers/spec-studio.sdp.md index 9799ea2c..6eb3cba6 100644 --- a/specs/consumers/spec-studio.sdp.md +++ b/specs/consumers/spec-studio.sdp.md @@ -21,41 +21,18 @@ relations: - actor: A person in the repository who designs a capability ahead of its code and iterates on it in passes, and a pull request reviewer who holds only a link; both read far more than they change. - problem: The generated Markdown Design Review offers no built-in search or filters, and navigation is through its links, so reviewers in the first adopter open carrier files alongside it. - outcome: Give a person reviewing a design one generated HTML workbench that explores the whole graph through lenses, shows each Spec and Pack in context, and turns a wanted change into scoped intent rather than an edit. -### Open questions -- [non-blocking #phaseZeroSplit] No Spec states phase 0's deliverables: the index, the Pack page, search and the `sdp studio` publisher with its flags, output root, invalidation, clean check and exit code. Which child Specs carry them, and do pull request previews join phase 0? - - decider: owner - - provisional: child Specs for the index, the Pack page, search and the publisher; the publisher's draft: `sdp studio [root] [--exclude PATH]... [--check-clean]` writes only `generated/spec-studio/` under the root, which defaults to the working directory, with each exclusion a root-relative path prefix; a run writes its page set to `generated/spec-studio.tmp/` and replaces the live root with it wholesale; every `sdp build` attempt invalidates the live and temporary Studio roots before extraction, and an unsuccessful Studio run removes any Studio root it cannot certify; under `--check-clean` it compares an independent twin with the current output, returns nonzero and removes divergent, missing or drifted output, and replaces certified output wholesale; it shows validation findings without withholding publication and returns the validation exit code; previews stay a later phase -- [non-blocking #pageModel] The Studio is built as a single-page application, while its data contract both writes data files, which a page opened through `file://` cannot fetch, and embeds each page's data; is the Studio one page or a static page set, and how does each page get its data with no request? - - decider: owner - - links: spec:consumers.spec-studio.data - - provisional: a static page set of an index page and one page per Pack and per Spec, each embedding the data it reads in one JSON script element, with its local assets; a standalone data file, if any, duplicates the embedded values for consumers outside the page -- [non-blocking #recipeValues] The Studio shows only values the reader computes, while its pages need answers only catalog recipes compute, such as pinned declarations and keyed-rule coverage; may the Studio's build compose recipe answers over the same graph, and which? - - decider: owner - - links: spec:consumers.spec-studio.data - - provisional: the build composes a recipe's answer over the same graph only when the Studio's design names that recipe, as recipe 24's pinned declarations and recipe 34's keyed-rule coverage; the page recomputes no join, and no context bundle is saved as Studio data -- [non-blocking #acceptanceMeasure] The first build's acceptance names one adopter's corpus, a register id and a path in another repository; does phase 0 keep those five named tasks, or state a measure any corpus can run? - - decider: owner - - provisional: keep the first adopter's five named tasks until phase 0 is accepted ## 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: The Studio is a mostly static single-page application, buildable as an `index.html` page and its assets, and it needs no server. +- rule: [#staticPageSet] The Studio is a static page set: an index page, a search page, and one page per Pack and per Spec, with their local assets. Its build renders each page whole, so a page needs no server and reads without its script, which adds filtering, search and copying. - 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 renders 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. - rule: The Studio is write-careful: it writes nothing to canonical source, and a change a person wants leaves it as scoped intent for an agent, as `spec:consumers.edit-model` states. - rule: [#noSecondGraph] The Studio never builds a second graph: every join, claim, delivery fact, derived readiness, floor failure and finding it shows is a value the reader computed when the Studio was built, and the page computes none of them again. +- rule: [#recipeAnswers] A page shows a catalog recipe's answer only when the Studio's design names that recipe, and the build composes the answer over the graph it derived; the page recomputes none of it. - rule: The Studio stands beside the shipped Design Review, census, Mermaid and Gherkin projections and re-specifies none of them, which `spec:decisions.shipped-projections-frozen` keeps as they are. -## Example space -```gwt-vocabulary -Given an extraction root whose graph holds a Pack and its member Specs -When the Studio renders twice from freshly derived graphs of that root -Then the page set holds the entry page {entryPage:string} -Then the two renders are byte-identical: {byteIdentical:boolean} -Then every carrier and source file under the root stays byte-identical: {rootUntouched:boolean} -``` - ## Verification — manual - The Studio's first build is accepted when a person completes, from the Studio's page set and the files it links to, the five tasks the first adopter set over its history-view Pack (`libar-platform/docs/feedback/sdp-feedback-01.md`): find the history contract, locate OD-157, explain the aggregate dependency, find one declaration's type comparison, and say whether a paused-rebuild example has native-backend evidence. The first build reads no run evidence, so the last task passes when the person finds that the example requires the native backend tier and has a bound verifier, and reports that no run result is supplied. diff --git a/specs/consumers/spec-studio.search.sdp.md b/specs/consumers/spec-studio.search.sdp.md new file mode 100644 index 00000000..c83b9ca8 --- /dev/null +++ b/specs/consumers/spec-studio.search.sdp.md @@ -0,0 +1,36 @@ +--- +id: spec:consumers.spec-studio.search +kind: behavior +altitude: story +readiness: idea +relations: + refines: spec:consumers.spec-studio.shell + dependsOn: + - spec:consumers.spec-studio.data + - target: spec:consumers.reader + reason: the page matches with the reader's concept matching +--- +# Search finds a Spec, a Pack, an entry or a bound file by a word + +The first adopter asked to search entry keys, pinned declarations and file paths. A Spec's envelope holds no tags, so search reads what the graph holds: the fields concept search reads, which already include every pinned declaration, plus the keys authors coin and the files the graph records. The page matches with the reader's own code, so a term finds on the page what `findByConcept` finds through `sdp q`. + +## Intent +- outcome: Let a person find a Spec, a Pack, a keyed entry or the code bound at a file by a word they remember of it, from any page and with no request. + +## Behavior +- rule: [#oneMatcher] Search matches with the reader's concept matching, one implementation the package exports without Node dependencies and the page script bundles, so over the fields `findByConcept` reads, the page returns the nodes it returns, in its order. +- rule: [#addedFields] Search also matches the keys authors coin, Design and UI keys, open question keys and rule keys, and a node's recorded file path; a node matched only through these follows every node `findByConcept` matches, in id order. It indexes nothing else. +- rule: A term matches where it appears in a field, without regard to case, as `findByConcept` matches; an empty term matches nothing. +- rule: [#searchPage] The search field on any page opens the search page with the term in its fragment, `search.html#q=<term>`; the search page matches as the person types and keeps the term in its fragment, so a search is a link. +- rule: [#resultShape] Each result names its node type in words. A Spec result shows its title, id, kind, and stated rung beside the floor reached, and opens its page; a Pack result shows its title and id and opens its page, with no readiness; an Anchor or CodeNode result shows its id, label, and recorded file and line as text with a copy action, and links each Spec its edges reach, with the edge type. +- rule: [#entryHits] Under a Spec result, the page lists the Spec's entries whose text or coined key holds the term, in the index's order, each with its section, key and recorded line, linked to the entry on the Spec page when it has an entry address and to its section otherwise. +- rule: The page shows fifty results at a time, with a control that shows the next fifty. +- rule: The search page reads only the index it embeds; it fetches nothing and joins nothing. + +## UI +- searchField: a labelled text field and a Search button in the top navigation of every page. +- resultsHeader: the term, how many nodes and entries match, and the fields searched. +- resultCard: one card per node, in result order: its node type in words, title and id, the fields that matched, and the Spec's rungs or the unit's location as the result rule states; then its entry hits. +- entryHit: one line per entry: its section, its key as code, its recorded line, and the text around the term, shortened. +- noMatch: `No node matches` and the term, then the fields searched. +- needsScript: without script, the search page says that search needs it and links to the index. diff --git a/specs/consumers/spec-studio.sharing.sdp.md b/specs/consumers/spec-studio.sharing.sdp.md index 4bc99c9a..4fe74a4c 100644 --- a/specs/consumers/spec-studio.sharing.sdp.md +++ b/specs/consumers/spec-studio.sharing.sdp.md @@ -14,18 +14,14 @@ relations: ## Intent - outcome: Let a stakeholder open the Studio of a pull request from a link, and let its author know which graph and which build the stakeholder read. -### Open questions -- [non-blocking #stampLoading] How does the shell read the publishing stamp while generated pages stay a function of the graph and a page opened through `file://` makes no request, which keys does the stamp hold, and which step publishes a preview? - - decider: owner - - links: spec:consumers.spec-studio#question.phaseZeroSplit - - provisional: the shell reads `stamp.json` only when served over http or https, so a `file://` page makes no stamp request; its keys are `schemaVersion`, `corpusCommit`, `engine` and `builtAt`; publishing a preview and writing its stamp are the adopter's step, and the Studio build writes only its graph-derived page set ## Behavior - rule: A build is shared by uploading its page set to a static host, such as S3, Vercel or GitHub Pages. - rule: Each pull request can publish its Studio as a preview. - rule: A stakeholder opens the link, navigates the Studio and composes intent, and the pull request's author hands that intent to an agent that edits the source. - rule: The Studio's build writes no stamp, so every page and data file it writes stays a function of the graph alone. The publishing step that uploads a build writes one stamp file beside the page set, and the shell shows it as build context when it is present. +- rule: [#publishingStep] Publishing a preview and writing its stamp are a step the adopter runs, never the Studio's build; the shell reads the stamp as `spec:consumers.spec-studio.shell#rule.stamp` states. ## Design -- buildStamp: `stamp.json` beside the page set, written by the publishing step and never by the Studio's build, holding the graph's `schemaVersion`, the git commit the build came from or `unknown` for a local build, the engine `sdp --version` names, and the build time. +- buildStamp: `{ schemaVersion: string; corpusCommit: string; engine: string; builtAt: string }` in `stamp.json` beside the page set, written by the publishing step and never by the Studio's build: the graph's `schemaVersion`, the git commit the build came from or `unknown` for a local build, the engine `sdp --version` names, and the build time as an ISO 8601 UTC timestamp. - previewPath: a pull request's preview is served at `<host>/preview/<pr-number>/`. diff --git a/specs/consumers/spec-studio.shell.sdp.md b/specs/consumers/spec-studio.shell.sdp.md index 6ea97366..9ccfc3e5 100644 --- a/specs/consumers/spec-studio.shell.sdp.md +++ b/specs/consumers/spec-studio.shell.sdp.md @@ -9,30 +9,37 @@ relations: - spec:consumers.spec-studio.data - target: spec:consumers.spec-studio.lenses reason: the top navigation opens each lens + - target: spec:consumers.spec-studio.responsive + reason: the rail moves below the canvas under the phone breakpoint + - target: spec:validation.prose-mentions + reason: a mention links where the validator resolves it + - target: spec:consumers.spec-studio.sharing + reason: the shell shows the stamp a publishing step writes beside the page set --- # The Studio's shell frames every page with navigation, a rail and a canvas +The shell carries what every page shares, including the rules that keep a page readable: by keyboard, without color, on a phone and without its script. Recent edits stay out of it, because a page is a function of the graph alone and git holds the history. + ## Intent -- outcome: Let a person move between lenses, Packs and Specs from any page, and find a Spec by what they remember of it. -### Open questions -- [non-blocking #railHarnesses] The original's left rail shows the current Pack's tree, its specs and then its harnesses; the graph has no harness node. Does the rail list harnesses, and from which graph value? - - decider: owner - - provisional: the rail lists the current Pack's members that own an example space, each linked to its Spec page, and creates no harness identity -- [non-blocking #recentEdits] The original's left rail lists recent edits; the graph carries only current state, git is the event log, and the shipped views are a function of the graph alone. Does the rail read recent edits from git when the Studio is built, or leave them out? - - decider: owner - - provisional: leave recent edits out, so a Studio page stays a function of the graph alone -- [non-blocking #searchTags] The original's search indexes tags; the Spec envelope is closed to `id`, `kind`, `altitude`, `readiness` and `relations` with the H1 as title, and no free-form tag vocabulary is admitted. What, if anything, does search index in place of tags? - - decider: owner - - provisional: search matches what `findByConcept` matches, and indexing any further value needs the phase-0 search Spec's own contract +- outcome: Let a person move between the index, Packs and Specs from any page, find a Spec by what they remember of it, and read every page by keyboard, on a phone and without its script. ## Behavior -- rule: Every page has the same shell: a top navigation, a left rail and a main canvas. -- rule: Search is persistent on every page and uses the searchIndex data entry's scope; each result identifies its node type. A Spec result shows its stated rung beside the floor reached and opens its page; a Pack result opens its page without a readiness label; an Anchor or CodeNode result shows its recorded source location. +- rule: Every page has the same shell: a top navigation, a left rail and a main canvas, and it opens with a link that skips to the canvas. +- rule: A search field is on every page; `spec:consumers.spec-studio.search` states what it matches and how its results show. - rule: The rail's count of findings is the count of the validation report's findings, never a count the page derives. +- rule: [#noRecentEdits] The shell shows no recent edits and no other history: git holds it. +- rule: [#stamp] Served over http or https, the shell reads `stamp.json` beside the page set and, when it loads, shows the build context it names; a page opened through `file://` requests nothing and shows no stamp. +- rule: [#rungBadge] A stated rung shows as a badge that carries the rung's word, colored from a palette of four that stays distinct under color blindness; the floor reached shows beside it as words with no color of its own, and only the divergence banner adds a warning color, beside its words. +- rule: [#wordsBesideColor] Every status a color marks is also written in words, so no reading depends on color. +- rule: [#keyboard] Every link and control is reachable from the keyboard in reading order and shows where the focus is. +- rule: [#lineLength] Prose runs no wider than about 80 characters, and a table wider than the canvas scrolls inside its own box, never the page. +- rule: [#text] Authored text renders as plain text with each inline code span as code, and no other Markdown is read. A mention the prose-mentions validator reads links to its target's page when the target resolves, and to the entry when the mention is an entry address. +- rule: [#withoutScript] Every page reads in full without its script: its collapsible parts are native disclosure elements, and only filtering, search, copying and the stamp need the script. ## UI The parts of the shell, top to bottom and left to right. -- topNavigation: Packs, Architecture, Tests, Evidence and Search; a capability grouping awaits `spec:consumers.spec-studio.lenses#question.capabilityGrouping`. -- leftRail: the current Pack's tree, its member Specs in authored order and the validation report's finding counts by severity; recent edits await `spec:consumers.spec-studio.shell#question.recentEdits`. +- topNavigation: the Studio's name linking to the index, then Packs, linking to the index's Packs section, then the search field; each lens page the page set holds adds its entry in the order `spec:consumers.spec-studio.lenses` gives. +- stampLine: at the end of the top navigation, the stamp's corpus commit, engine and build time, when the stamp loads. +- leftRail: on a Pack page, the Pack's members in authored order, each linked to its card on the page; on a Spec page, each Pack the Spec belongs to with its members in authored order, each linked to its Spec page and the current one marked; on any other page, every Pack; then each member of the rail's Packs that owns an example space, linked to its Spec page, with no harness named; then the validation report's finding counts by severity, linked to the index's findings. +- railOnPhone: below the phone breakpoint the rail follows the canvas, so a page opens on its content. - mainCanvas: the page of the Spec or Pack selected in the rail or in a lens. -- search: matches the fields `spec:consumers.spec-studio.data#design.searchIndex` lists, with the same matching and ordering as `findByConcept`; results follow the search rule. diff --git a/specs/consumers/spec-studio.spec-page.core.sdp.md b/specs/consumers/spec-studio.spec-page.core.sdp.md new file mode 100644 index 00000000..b7801227 --- /dev/null +++ b/specs/consumers/spec-studio.spec-page.core.sdp.md @@ -0,0 +1,45 @@ +--- +id: spec:consumers.spec-studio.spec-page.core +kind: behavior +altitude: story +readiness: idea +relations: + refines: spec:consumers.spec-studio.spec-page + dependsOn: + - spec:consumers.spec-studio.data + - spec:consumers.spec-studio.shell + - spec:consumers.derived-readiness-banner + - spec:consumers.binding-language-views + - spec:model.open-question-fields + - spec:model.rule-keys + - spec:model.relation-reasons + - spec:extraction.entry-locations + decidedBy: spec:decisions.authored-entry-order +--- +# A Spec page's core shows the whole Spec, with an anchor for every entry + +The full Spec page adds panels to this core: diagrams, a runtime neighborhood, verification and evidence panels, impact, and actions that compose intent. The core shows what the Spec context holds, as text, so a reviewer with only a link reads the whole Spec and can cite any entry by its address. + +## Intent +- outcome: Show everything the graph holds about one Spec on one page that reads without script: its header, its sections in order with an anchor for every addressable entry, its relations both ways, its bindings and its findings. + +## Behavior +- rule: [#header] The header shows the title; the id with a copy action; the kind's display label and the altitude; the stated rung beside the floor reached on one line, with the divergence banner only in the dishonest direction; the stated next rung with `nothing blocks` or each unmet clause and its targets; the Packs the Spec belongs to, linked; its design columns; its findings by severity; and its carrier file as text with a copy action. It shows no owner, capability, tag or last-verified time: the envelope holds none, the Packs stand for a capability, and a binding never records a run. +- rule: [#summary] A summary in words follows the header: the stated rung against the floor, what holds the stated next rung, the open and blocking questions, and the implementation, verifier and oracle bindings as present or none. +- rule: [#sections] The narrative comes next, then each section the Spec carries as a panel open by default, in the order intent, behavior, constraints, model, design, decision, verification, ui, with its entries in authored order. A section the Spec lacks is absent, and the page adds no section the graph does not hold. +- rule: [#entryIds] Every entry an entry address can name, a keyed Design or UI entry, a keyed rule or flow and a keyed open question, carries an element id equal to its address after the `#`, such as `design.<key>`, and shows its key and its recorded line. +- rule: [#questions] Each open question shows its blocking flag in words, its key and address when it has one, its text, its decider, its links, linked, its provisional reading and its trigger when present, and its recorded line. It shows no date: git records when a line changed. +- rule: A `gwt` or `gwt-vocabulary` fence shows as preformatted step text, as authored. +- rule: [#relations] Relations show both ways, each with its type, its direction, its claim, the other endpoint, linked when it is a Spec or a Pack, whether it resolves, and its reason and the rule keys it exercises when present. +- rule: [#bindings] Bindings speak binding language. The implementation binding, design reference, verifier binding and expected-outcome oracle each read present or none, and list each unit, test anchor, example or oracle by id, label, and recorded file and line as text with a copy action; a unit adds its role and component, and an example verifier says whether it is enabled. Runtime observation reads not tracked, and run results read not recorded in the graph. +- rule: [#findings] The findings that name the Spec close the page, each with its severity in words, its validator id and its message. + +## UI +- headerBlock: the title as the page heading; below it a line with the id, kind, altitude, the rung badge with the floor in words, and the next rung; then the Packs, the design columns, the finding counts and the carrier file. +- summaryLine: one or two sentences under the header. +- sectionPanel: an open disclosure per section, headed with the section's name, and for Verification its mode. +- entryRow: an entry's key as code, its value, and `line <n>` when recorded, in a definition list. +- questionItem: the blocking flag in words, the key and address, the text, each present field on its own labelled line, and `line <n>` when recorded. +- relationsTable: five columns, relation and direction, other end, claim, reason, and exercised keys, with an unresolved endpoint marked in words. +- bindingsBlock: four labelled lines, each present or none with its items below it, then runtime observation and run results. +- findingsList: one line per finding, the severity word first. diff --git a/specs/consumers/spec-studio.spec-page.sdp.md b/specs/consumers/spec-studio.spec-page.sdp.md index 4c288192..ac10bcaf 100644 --- a/specs/consumers/spec-studio.spec-page.sdp.md +++ b/specs/consumers/spec-studio.spec-page.sdp.md @@ -21,44 +21,25 @@ relations: The original runtime panel linked Fastify routes to Effect programs, showed what Effect layers provided and required, and recorded Awilix registrations and their lifetimes, beside ports and external systems. That representation is lineage evidence in `docs/lineage/v0-design/07-spec-studio-and-projections.md`, section 2.3, Section F. The current structural anchors record identity, component membership, uses, role, and a component's architectural layer and context; the question below asks whether a later runtime panel needs more. +The page shows only what the graph holds. The original header's owner, tags and last-verified time, its schema bindings and its `UI / Stories` section have no graph value behind them, so the page shows none of them. + ## Intent - outcome: Show everything the graph holds about one Spec on one page, in panels a person opens and closes one at a time. ### Open questions -- [non-blocking #headerFields] The original header shows an owner, a capability, tags and a last-verified time; the envelope is closed to `id`, `kind`, `altitude`, `readiness` and `relations` with the H1 as title, a capability is a projection or a Pack, and a verifier binding never records when a test last ran. Which of the four does the header show, and from which graph value? - - decider: owner -- [non-blocking #rungColors] The original pins one color for each of seven rungs: light grey for `sketch`, blue for `framed`, cyan for `specified`, green for `designed`, emerald for `bound`, indigo for `executable` and violet for `verified`; readiness has four rungs, idea through ready, and the floor reached renders beside the stated rung. Does the palette color the stated rung, the floor reached, or both? - - decider: owner -- [non-blocking #questionMetadata] The original shows each open question's owner and the date it was added; an open question carries its text, whether it blocks, an optional key and the optional fields `decider`, `links`, `provisional` and `trigger`, where `decider` names who settles it as a corpus token rather than a person, and git, not the graph, records when its line was added. Which fields does the panel show, and does a date stay off a page that is a function of the graph? - - decider: owner - - links: spec:model.open-question-fields - - provisional: the panel shows every field the graph supplies, as the intent panel states, and no date - [non-blocking #runtimeComposition] Does the runtime panel show more than the structural neighborhood of the Spec's implementation anchors? -- [non-blocking #schemaBindings] The original's bindings panel lists schema bindings beside code and test bindings; anchors bind implementation code, tests and oracles, and no anchor binds a schema. Does a schema binding need a home? -- [non-blocking #uiStories] The original's page outline names a section `UI / Stories`, whose UI half the UI panel carries; the original never says what a story is. What is a story, and does the page show one? -- [blocking #sourceLinks] Panels name code units, components and anchors, and the architecture lens selects a component, yet no Studio page or data value designs a destination for them. Does each get a page, or show its id and recorded repository-relative file and line as text? - - decider: owner - - provisional: no code-unit pages in phase 0; a code unit or anchor shows its id and recorded repository-relative file and line as text, with an action that copies file:line and no editor URI, absolute path or assumed source URL; source navigation waits for a separately designed source target, and a component's attributes, members and uses show in the architecture lens ## Behavior - rule: Each panel collapses and expands on its own. -- rule: The header renders readiness as `spec:consumers.derived-readiness-banner` states: the stated rung beside the floor reached, with the divergence banner only in the dishonest direction. -- rule: The bindings panel speaks binding language as `spec:consumers.binding-language-views` states: each binding present or none, and the runtime observation not tracked. -- rule: Every Spec and Pack a panel names links to its page; how a code unit or anchor is reached awaits `spec:consumers.spec-studio.spec-page#question.sourceLinks`. -- rule: The design panel lists the `design` entries in authored order with their keys as code, and the UI panel lists the `ui` entries the same way, as `spec:decisions.authored-entry-order` rules for the Design Review. +- rule: The page's core, its header, sections, relations, bindings and findings, is the one `spec:consumers.spec-studio.spec-page.core` states, and the panels below add to it. +- rule: [#sourceText] Every Spec and Pack a panel names links to its page. A code unit, component or anchor shows its id and its recorded repository-relative file and line as text, with an action that copies `file:line`; the page set holds no code-unit page and writes no editor URI, absolute path or assumed source URL, source navigation waits for a source target designed for it, and a component's attributes, members and uses show in the architecture lens. ## UI -The panels in page order. -- header: the Spec's id, its stated readiness beside the floor reached, its stated next rung with the clauses that keep it below that rung, its altitude and kind; its title; its design columns; the Packs it belongs to; and its warning and error counts as pill badges; the header infers no promotion. -- readinessColors: a palette distinct under color blindness, with text beside every color; whether it colors the stated rung, floor reached or both awaits `spec:consumers.spec-studio.spec-page#question.rungColors`. -- intentPanel: the intent section as prose, then an open-questions panel headed with its count; each question shows its text, blocking flag, key and address when keyed, its decider, links, provisional reading and trigger when present, and its carrier line when recorded, with an action that composes intent scoped to it. -- behaviorPanel: rules and flows in authored order, a keyed entry with its rule address and the examples that exercise it as recipe 34 reports, followed by example cards laid out as `spec:consumers.spec-studio.verification-panels` states. +The panels the core does not hold, in page order. +- intentPanel: beside each open question, an action that composes intent scoped to it. +- behaviorPanel: beside each keyed rule or flow, the examples that exercise it as recipe 34 reports, then example cards laid out as `spec:consumers.spec-studio.verification-panels` states. - constraintsPanel: each constraint with its target, laid out as `spec:consumers.spec-studio.verification-panels` states. -- designPanel: a diagram of the components that realize the Spec, from its implementation anchors' `component` and `uses` edges, drawn as SVG whose nodes show their id, source file and line, with their destination awaiting `spec:consumers.spec-studio.spec-page#question.sourceLinks`; below it, the `design` entries, then the decision records the Spec names by `decidedBy` with each decision's text inline. +- designPanel: a diagram of the components that realize the Spec, from its implementation anchors' `component` and `uses` edges, drawn as SVG whose nodes show their id, source file and line as the source-text rule states; below the design entries, the decision records the Spec names by `decidedBy`, each with its text inline. - runtimePanel: the structural neighborhood of the Spec's implementation anchors: each anchor's component and the code units it uses, with file and line. -- bindingsPanel: implementation bindings, design references, verifier bindings and the expected-outcome oracle, each with its id and recorded repository-relative file and line, shown as text with an action that copies file:line, plus runtime observation reading not tracked; source navigation awaits `spec:consumers.spec-studio.spec-page#question.sourceLinks`. - verificationPanel: the verifiers and the coverage of the example space, laid out as `spec:consumers.spec-studio.verification-panels` states. - evidencePanel: the evidence fields and absence labels `spec:consumers.spec-studio.verification-panels` states, with no invented build, deployment, measurement or SBOM data. -- relationsPanel: relations both ways, each with its type, claim, endpoint, resolution, reason and exercises when present; a resolving Spec endpoint links to its page. - impactPanel: the upstream parents and downstream children, the Specs that depend on this one, the decision records it names, the tests that verify it, and the code its bindings reach. -- uiPanel: the `ui` entries. -- entryIds: every addressable keyed Design or UI entry, every keyed entry of the primary behavior owner and every keyed open question carries an HTML element id equal to its entry address suffix, the section and lower-camel key without the Spec id or `#`, so a link opens the entry on its page. diff --git a/specs/consumers/spec-studio.verification-panels.sdp.md b/specs/consumers/spec-studio.verification-panels.sdp.md index 34ffd86a..ff6172a0 100644 --- a/specs/consumers/spec-studio.verification-panels.sdp.md +++ b/specs/consumers/spec-studio.verification-panels.sdp.md @@ -18,23 +18,14 @@ relations: --- # The panels for examples, targets, verifiers and evidence +The graph records that a verifier binding exists and never a run, and a static page runs nothing, so the panels show bindings and no run result, and offer no run of an example. No Spec designs a source for build attestations, deployments or SBOMs, so each reads not tracked. + ## Intent - outcome: Show on a Spec page how far each example, constraint target and verifier goes, and what runtime evidence exists, without stating more than the graph records. -### Open questions -- [non-blocking #runResults] The original shows each test's runner, whether it passed, its duration and its last run, and offers to run an example again; the graph records that a verifier binding exists and never a run, and a static page runs nothing. Does a panel show run results read from CI output when the Studio is built, or bindings only? - - decider: owner - - links: spec:observation.run-evidence - - provisional: The first Studio build shows bindings only; admitting run records in a later phase awaits an explicit input and reproducibility ruling. -- [non-blocking #coverageSource] Is per-slot coverage sufficient for the Studio grid, or does it need the combinations still open in spec:consumers.agent-surface.example-space-coverage#question.combinationCoverage? - - decider: owner - - links: spec:consumers.agent-surface.example-space-coverage#question.combinationCoverage - - provisional: per-slot coverage for the grid; combinations wait for an adopter's design pass -- [non-blocking #deliveryArtifacts] The original's evidence panel shows the last build with its SLSA attestation, the last deployment and a CycloneDX SBOM; no Spec designs a source for any of them. Does the Studio show them, and from which input? - - decider: owner - - provisional: each reads not tracked until a Spec designs its source ## Behavior - rule: An example card shows the example's verifier as a binding that exists, and the example as an Executable Spec when a test anchor binds it, never as a test that passed. +- rule: [#bindingsOnly] A panel shows verifier bindings and no run result; run records join a panel only through `spec:observation.run-evidence`, under an input and reproducibility ruling of their own. - rule: [#evidenceNotTracked] The evidence panel shows only runtime evidence the graph records, and reads not tracked while no delivery fact records an observation. - rule: Each panel action gathers intent through the composing panel's contract. @@ -45,6 +36,6 @@ The panels' parts in page order. - unwrittenExampleCard: an example with no given, when and then steps yet says so, and offers to compose intent proposing them. - constraintTarget: each constraint with its flavor and statement, its target drawn as a bar beside the number, and a measured value only when the graph records one, otherwise not tracked; moving the bar composes intent to change the target, and the new value shows on the page only while the intent waits in the composing panel. - unsetTarget: a constraint with no quantitative target says so, and offers to compose intent proposing one. -- verifierList: each verifier with its id, its binding, and its recorded repository-relative file and line; no line is invented for a verifier the graph does not locate, and source navigation awaits `spec:consumers.spec-studio.spec-page#question.sourceLinks`. -- coverageGrid: when the Spec owns an example space, the coverage `spec:consumers.agent-surface.example-space-coverage` reports, listing each literal value of each slot with the examples that bind it and listing a non-literal-union slot as not enumerable; an action composes intent proposing examples for missing values. -- evidencePanel: runtime observation reads not tracked under the binding-language rule; build attestations, deployments and SBOMs show no payload while the graph has no designed record for them, and each reads not tracked instead. +- verifierList: each verifier with its id, its binding, and its recorded repository-relative file and line as text, as `spec:consumers.spec-studio.spec-page#rule.sourceText` states; no line is invented for a verifier the graph does not locate. +- coverageGrid: when the Spec owns an example space, the per-slot coverage `spec:consumers.agent-surface.example-space-coverage` reports, listing each literal value of each slot with the examples that bind it and listing a non-literal-union slot as not enumerable; combinations of slots join the grid only when a design pass asks for them; an action composes intent proposing examples for missing values. +- evidencePanel: runtime observation reads not tracked under the binding-language rule; build attestations, deployments and SBOMs show no payload and each reads not tracked until a Spec designs its source. diff --git a/test/recipes.test.ts b/test/recipes.test.ts index ff6b0e02..9ffce717 100644 --- a/test/recipes.test.ts +++ b/test/recipes.test.ts @@ -4073,7 +4073,11 @@ describe("design references and the architecture taxonomy", () => { // none of its consumers' designs. expect(reader.specContext(readerId)?.deliveryFacts).toContain("implemented"); expect(readerUnits.map((unit) => unit.file)).toContain("src/reader/reader.ts"); - expect(dependents).toEqual(["spec:consumers.spec-studio", "spec:consumers.spec-studio.data"]); + expect(dependents).toEqual([ + "spec:consumers.spec-studio", + "spec:consumers.spec-studio.data", + "spec:consumers.spec-studio.search", + ]); for (const id of dependents) { expect(asArray(rows.find((row) => row.id === id)?.buildsOn), id).toContainEqual({ id: readerId, From e33896abdef2cc1eac4af2483c10ac1be84cc049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= <darko.mijic@gmail.com> Date: Tue, 13 Oct 2026 14:47:37 +0200 Subject: [PATCH 10/33] explorations: a prototype of the Studio's Pack page on real data One hand-written page, opened from file:// with no request and no build step, rendered from recipe 29's answer and the Pack context for pack:design-management-v1 here and for the first adopter's pack:history-view. Custom elements without a framework open cards and copy locations. The README names the commands that regenerate the inputs, what the design pass took from the page, and the two associations the adopter's re-pin applies for the five reader tasks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- explorations/studio-prototype/README.md | 114 + .../data/design-management-v1.js | 4610 +++++++++++ .../design-management-v1.pack-context.json | 2412 ++++++ .../data/design-management-v1.recipe-29.json | 2196 ++++++ .../studio-prototype/data/history-view.js | 6824 +++++++++++++++++ .../data/history-view.pack-context.json | 3513 +++++++++ .../data/history-view.recipe-29.json | 3309 ++++++++ .../design-management-v1.html | 42 + .../studio-prototype/history-view.html | 42 + explorations/studio-prototype/studio.css | 509 ++ explorations/studio-prototype/studio.js | 512 ++ 11 files changed, 24083 insertions(+) create mode 100644 explorations/studio-prototype/README.md create mode 100644 explorations/studio-prototype/data/design-management-v1.js create mode 100644 explorations/studio-prototype/data/design-management-v1.pack-context.json create mode 100644 explorations/studio-prototype/data/design-management-v1.recipe-29.json create mode 100644 explorations/studio-prototype/data/history-view.js create mode 100644 explorations/studio-prototype/data/history-view.pack-context.json create mode 100644 explorations/studio-prototype/data/history-view.recipe-29.json create mode 100644 explorations/studio-prototype/design-management-v1.html create mode 100644 explorations/studio-prototype/history-view.html create mode 100644 explorations/studio-prototype/studio.css create mode 100644 explorations/studio-prototype/studio.js diff --git a/explorations/studio-prototype/README.md b/explorations/studio-prototype/README.md new file mode 100644 index 00000000..b3e6588d --- /dev/null +++ b/explorations/studio-prototype/README.md @@ -0,0 +1,114 @@ +# explorations/studio-prototype: the Studio's Pack page on real data + +> **Exploration record, illustrative, nothing ratified.** A hand-written prototype of the Spec +> Studio's Pack page (`spec:consumers.spec-studio.pack-page`), made in the Studio's design pass so +> the owner and reviewers can react to the page before its templates are fixed. Never promote this +> code into the product: the product renders the same markup at build time. + +## Open it + +Open either page straight from the file system; nothing is fetched and nothing is built. + +- `design-management-v1.html`: `pack:design-management-v1` from this repository, which has members + held below their next rung. +- `history-view.html`: `pack:history-view` from the first adopter, libar-platform at `d310eb1`, + the Pack its five reader tasks are set on. + +Try a member's fragment, such as `history-view.html#spec:application.history-projection`: the card +opens. `Open all` and `Close all` act on every card and section. Narrow the window below 720 pixels +for the phone layout, where the rail follows the content. + +## What it is + +- `studio.css`: the shell, rung badges, cards and the phone layout, in light and dark. +- `studio.js`: one classic script in two halves. The template turns the data into markup, standing + in for the build's page template. The elements, `sdp-expander` and `sdp-copy`, enhance that + markup as `spec:consumers.spec-studio.page-elements` designs them. +- `data/<pack>.recipe-29.json`: recipe 29's answer with `detail: true`. +- `data/<pack>.pack-context.json`: the reader's Pack context, for what recipe 29 leaves out: the + framing, the question fields, unit labels, verifier gaps and findings. +- `data/<pack>.js`: both answers in one classic script, because a page opened through `file://` + cannot fetch a JSON file. + +Regenerate the data from this repository's root, after `npm run build`, with `PLATFORM` set to a +clone of libar-platform: + +```sh +D=explorations/studio-prototype/data +pnpm --silent sdp:q "$(cat dist/recipes/29-pack-design.js)" --params '{"pack":"pack:design-management-v1","detail":true}' --json > $D/design-management-v1.recipe-29.json +pnpm --silent sdp:q 'return g.packContext(params.pack)' --params '{"pack":"pack:design-management-v1"}' --json > $D/design-management-v1.pack-context.json +node dist/cli/sdp.js q "$(cat dist/recipes/29-pack-design.js)" --root "$PLATFORM" --params '{"pack":"pack:history-view","detail":true}' --json > $D/history-view.recipe-29.json +node dist/cli/sdp.js q 'return g.packContext(params.pack)' --root "$PLATFORM" --params '{"pack":"pack:history-view"}' --json > $D/history-view.pack-context.json +for name in design-management-v1 history-view; do + printf 'window.SDP_PROTOTYPE = {\n"design": %s,\n"context": %s\n};\n' "$(cat $D/$name.recipe-29.json)" "$(cat $D/$name.pack-context.json)" > $D/$name.js +done +``` + +## Where it differs from the product + +- The script renders the page; the product's build writes the markup, and the page reads without + its script. +- Only the Pack page exists, so links to the index, search and Spec pages do not open. +- The rail lists the Pack's members and its findings; it leaves out the members that own an example + space, which neither input carries. +- The findings count is the Pack's, where the product's rail counts the whole validation report. + +## What the design pass took from it + +- Recipe 29's answer alone cannot carry the page: it drops the framing, the question fields, unit + labels, verifier gaps and findings. The data Spec renders a Pack page from the Pack context and + recipe 29's totals instead. +- A browser refuses a module script on a page opened through `file://`, so the elements ship as one + classic script (`spec:consumers.spec-studio.page-elements#rule.oneScript`). +- Native disclosure elements give the cards their collapse without a script; the one element the + cards need is the one that opens the card a fragment targets. +- A phone rail that starts closed needs a script, so the rail follows the content on a phone + instead. +- Desktop Chrome will not open a window narrower than about 500 pixels, so a phone layout is checked + with a headless shell such as the one Playwright caches. +- Questions the owner may want to react to: the full question address inside its own member's card + is long, and a short `question.<key>` would read faster; the summary states counts as sentences + rather than a table. + +## The two associations the reader tasks need + +The first adopter's five reader tasks need two associations its graph does not hold. Both are +adopter authoring, applied when the adopter re-pins; on a copy of the platform at `d310eb1`, with +this branch's engine, validate stays at 0 errors and 12 warnings, and the summary line moves from +180 anchors, 429 nodes and 1,499 edges to 181, 430 and 1,510. + +1. **OD-157 in the question it registers.** Add the register id to the citation list that closes + the text of `spec:application.history-rebuild#question.verifyPassUnderPause`, as other questions + cite `OD-075`: `… is the owner's (D9, Sc L2-6, OD-157)`. `findByConcept("OD-157")` and entry + search (recipe 23) then find the question, with its address and line. +2. **A test anchor on the type-comparison suite.** At the top of `tests/types/pinned.test-d.ts`, + bind the suite to every Spec whose pinned declarations it compares, in the form + `tests/types/hosted-record.test-d.ts` already uses: + + ```ts + import { ref, specTest, testAnchorId } from "@libar-dev/software-delivery-protocol"; + + // Binds this suite to the Specs whose pinned declarations it compares with the code. + const anchor = specTest({ + id: testAnchorId("test:history-view.pinned-declarations"), + label: "compares pack:history-view's pinned declarations with the code", + verifies: [ + ref("spec:application.history-projection"), + ref("spec:application.projection-contract"), + ref("spec:application.read-models"), + ref("spec:application.rebuild"), + ref("spec:application.write-pause"), + ref("spec:application.generation-registry"), + ref("spec:command.command-declaration"), + ref("spec:command.command-pipeline"), + ref("spec:context.event-envelope"), + ref("spec:context.journal"), + ref("spec:context.queries"), + ], + }); + void anchor; + ``` + + `spec:application.history-projection` then carries `has-verifier`, its Spec context lists the + suite with its file and line, and `byFile` on the suite names all eleven Specs. + The adopter's `design/README.md` states the validate summary line, which moves with the anchor. diff --git a/explorations/studio-prototype/data/design-management-v1.js b/explorations/studio-prototype/data/design-management-v1.js new file mode 100644 index 00000000..c646c1dc --- /dev/null +++ b/explorations/studio-prototype/data/design-management-v1.js @@ -0,0 +1,4610 @@ +window.SDP_PROTOTYPE = { +"design": { + "found": true, + "id": "pack:design-management-v1", + "title": "Design management", + "totals": { + "members": 24, + "unresolved": 0, + "heldBelowNextRung": 3, + "waitingForAuthor": 5, + "entries": 48, + "declarations": 17, + "openQuestions": 24, + "blockingQuestions": 3, + "withImplementations": 15, + "withReferences": 4, + "restsOn": 39, + "restedOnBy": 23, + "verifiedBy": 0, + "supersededBy": 0 + }, + "members": [ + { + "id": "spec:decisions.pack-design-page", + "resolved": true, + "title": "The Design Review's Pack page presents the Pack's design", + "specKind": "decision", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:protocol.pack-design-page", + "file": "src/projections/design-review-pages.ts", + "line": 128, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.pack-design", + "resolved": true, + "title": "The Pack context carries the Pack's design", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.pack-design", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.design-review.pack-design-page", + "resolved": true, + "title": "A Pack page shows where the Pack's design stands", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [ + { + "id": "spec:decisions.pack-design-page", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.pack-design-page", + "file": "src/projections/design-review-pages.ts", + "line": 128, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "referencedBy": [ + { + "id": "impl:protocol.pack-design-sections", + "file": "src/projections/design-review-pack-sections.ts", + "line": 19, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:extraction.entry-locations", + "resolved": true, + "title": "The graph records where each addressable entry is written", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.entry-location-recipes", + "file": "test/recipes.test.ts", + "line": 5689, + "role": null, + "component": null + }, + { + "id": "impl:protocol.entry-locations", + "file": "src/extract/derive.ts", + "line": 78, + "role": "extractor", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + }, + { + "id": "impl:protocol.reader", + "file": "src/reader/reader.ts", + "line": 725, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "referencedBy": [ + { + "id": "impl:protocol.pack-design", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.recipe-parameters", + "resolved": true, + "title": "Recipes take parameters as data and ship as runnable files", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [ + { + "id": "spec:decisions.agent-front-door", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.build-artifacts", + "file": "src/cli/build-artifacts.ts", + "line": 177, + "role": "publisher", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "id": "impl:protocol.query-params", + "file": "src/cli/q-command.ts", + "line": 179, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "id": "impl:protocol.recipe-parameters-catalog", + "file": "test/recipes.test.ts", + "line": 4241, + "role": null, + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 3, + "enabled": 3 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.design-recipes", + "resolved": true, + "title": "Design work reads its answers from catalog recipes", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.design-recipes", + "file": "test/recipes.test.ts", + "line": 4528, + "role": null, + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.example-space-coverage", + "resolved": true, + "title": "Example spaces report which values their examples cover", + "specKind": "behavior", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 2, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "combinationCoverage", + "blocking": false, + "address": "spec:consumers.agent-surface.example-space-coverage#question.combinationCoverage", + "line": 22, + "question": "Is coverage per slot value enough, or does a design pass need the combinations of two or more slots, as the original heatmap drew them?" + }, + { + "key": "coverageShape", + "blocking": false, + "address": "spec:consumers.agent-surface.example-space-coverage#question.coverageShape", + "line": 26, + "question": "What reader field names and decoded slot and bound-point shapes, recipe parameters and result rows, eligible-example relation policy, and treatment of an unbound slot form the coverage contract?" + } + ], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.engine-provenance", + "resolved": true, + "title": "The CLI names the engine that produced a result", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.build-artifacts", + "file": "src/cli/build-artifacts.ts", + "line": 177, + "role": "publisher", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "id": "impl:protocol.engine-provenance-cli", + "file": "src/cli/sdp.ts", + "line": 193, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.validate-watch", + "resolved": true, + "title": "Validation re-runs while an author edits carriers", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.validate-watch", + "file": "src/cli/validate-watch.ts", + "line": 474, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.design-maturity", + "resolved": true, + "title": "What a Spec's design carries is derived, beside its readiness", + "specKind": "model", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "designColumnsSufficiency", + "blocking": false, + "address": "spec:model.design-maturity#question.designColumnsSufficiency", + "line": 20, + "question": "If the first adopter's design passes show that the design columns cannot distinguish settled design from intent alone, should this representation be revisited?" + } + ], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.pack-design", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.planned-architecture", + "resolved": true, + "title": "Components are designed ahead of their code", + "specKind": "model", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "providerSelection", + "blocking": false, + "address": "spec:model.planned-architecture#question.providerSelection", + "line": 24, + "question": "The original kept apart the interface a unit requires, the providers that supply it, the provider a composition root selects and the one a test substitutes, for Fastify, Effect, Awilix and plain factories alike. Does a component design state these without naming a framework, and does any of them earn graph structure beyond `uses`?" + }, + { + "key": "oneRuntimeOwner", + "blocking": false, + "address": "spec:model.planned-architecture#question.oneRuntimeOwner", + "line": 26, + "question": "Is the original's one-runtime-truth rule, one composition mechanism per application, a Protocol rule or a corpus's own architecture rule?" + }, + { + "key": "comparisonShape", + "blocking": false, + "address": "spec:model.planned-architecture#question.comparisonShape", + "line": 28, + "question": "How is a component design selected, what value form does its `uses` entry take, and what rows does the realization comparison return for an unrealized design and for a layer, context or `uses` that differs?" + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:protocol.curation-recipe", + "file": "test/curation-recipes.test.ts", + "line": 32, + "role": null, + "component": null + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.open-question-fields", + "resolved": true, + "title": "An open question may name who decides it, its links, a provisional reading and a re-entry trigger", + "specKind": "rule", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 1, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "droppedQuestionList", + "blocking": false, + "address": "spec:model.open-question-fields#question.droppedQuestionList", + "line": 27, + "question": "A non-static question text, blocking flag or spread in a TypeScript open-question list also drops the whole list, blocking questions included, and stays a warning; is every dropped question list an error?" + } + ], + "decisions": [ + { + "id": "spec:decisions.adopt-the-nouns", + "statedReadiness": "ready", + "resolved": true + }, + { + "id": "spec:decisions.planning-truths-placement", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.authoring-grammar-recipes", + "file": "test/recipes.test.ts", + "line": 6307, + "role": null, + "component": null + }, + { + "id": "impl:protocol.authoring-normalization", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.rule-keys", + "resolved": true, + "title": "A rule or flow entry may carry a key that addresses it", + "specKind": "rule", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 10, + "declarations": 6, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [ + { + "id": "spec:decisions.checked-mentions", + "statedReadiness": "ready", + "resolved": true + }, + { + "id": "spec:decisions.shipped-projections-frozen", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.authoring-grammar-recipes", + "file": "test/recipes.test.ts", + "line": 6307, + "role": null, + "component": null + }, + { + "id": "impl:protocol.authoring-normalization", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + }, + { + "id": "impl:protocol.rule-exercises", + "file": "src/validate/validators.ts", + "line": 1484, + "role": "validator", + "component": { + "id": "component:protocol.validate", + "layer": "domain", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.relation-reasons", + "resolved": true, + "title": "An authored relation may carry its reason", + "specKind": "rule", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 5, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [ + { + "id": "spec:decisions.prose-ownership", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.authoring-grammar-recipes", + "file": "test/recipes.test.ts", + "line": 6307, + "role": null, + "component": null + }, + { + "id": "impl:protocol.authoring-normalization", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.review-perspectives", + "resolved": true, + "title": "A Pack is read from a named perspective", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 4, + "declarations": 1, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.perspective-filter-recipe", + "file": "test/iteration-recipes.test.ts", + "line": 751, + "role": "projection", + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.context-bundle", + "resolved": true, + "title": "A context bundle is composed per invocation within a stated budget", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 3, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "declarationCut", + "blocking": false, + "address": "spec:consumers.context-bundle#question.declarationCut", + "line": 23, + "question": "No cut reduces a pinned declaration, so a Pack whose pinned declarations alone exceed the budget fits no bundle; may a caller ask the bundle to reduce pinned declarations to their entry addresses?" + } + ], + "decisions": [ + { + "id": "spec:decisions.agent-front-door", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.context-bundle-recipe", + "file": "test/iteration-recipes.test.ts", + "line": 763, + "role": "projection", + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.design-diff", + "resolved": true, + "title": "Designs are compared between a base commit and the working tree at entry grain", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 8, + "declarations": 4, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.design-diff-recipe", + "file": "test/iteration-recipes.test.ts", + "line": 27, + "role": "projection", + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.intent-composition.proposal", + "resolved": true, + "title": "A change proposal is addressed, based and checked before it lands", + "specKind": "contract", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 2, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "memoRecipe", + "blocking": false, + "address": "spec:consumers.intent-composition.proposal#question.memoRecipe", + "line": 25, + "question": "Which catalog recipe renders a proposal as the Markdown memo a person reviews, so the Studio's composing panel and an agent hand off the same text?" + } + ], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.proposal-schema-and-recipe", + "file": "test/iteration-recipes.test.ts", + "line": 1291, + "role": "contract", + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.reference-projection", + "resolved": true, + "title": "A reference projection serves a reader outside the repository", + "specKind": "behavior", + "statedReadiness": "idea", + "floorReached": "idea", + "statedNextRung": "scoped", + "statedNextRungFailures": [ + { + "clauseId": "kind-evidence-present", + "description": "The kind's natural evidence is present (per-kind evidence table)." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 2, + "blockingQuestions": 1 + }, + "questions": [ + { + "key": "namedOutsideReader", + "blocking": true, + "address": "spec:consumers.reference-projection#question.namedOutsideReader", + "line": 17, + "question": "Does a human reader outside the repository need a reference projection the four shipped roots do not serve? The taxonomy row is not that reader." + }, + { + "key": "referenceSource", + "blocking": false, + "address": "spec:consumers.reference-projection#question.referenceSource", + "line": 20, + "question": "Would the reference read pinned declarations, contract-kind Specs or `api:` code anchors, and does any of them hold enough to document an interface?" + } + ], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.structural-mermaid", + "resolved": true, + "title": "Mermaid diagrams can be rooted at code units", + "specKind": "behavior", + "statedReadiness": "idea", + "floorReached": "idea", + "statedNextRung": "scoped", + "statedNextRungFailures": [ + { + "clauseId": "kind-evidence-present", + "description": "The kind's natural evidence is present (per-kind evidence table)." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 2, + "blockingQuestions": 1 + }, + "questions": [ + { + "key": "namedStructuralReader", + "blocking": true, + "address": "spec:consumers.structural-mermaid#question.namedStructuralReader", + "line": 18, + "question": "Does a reader need diagrams rooted at code units, which census tables and Spec-rooted one-hop Mermaid do not serve?" + }, + { + "key": "layerAndContext", + "blocking": false, + "address": "spec:consumers.structural-mermaid#question.layerAndContext", + "line": 21, + "question": "Would a component diagram group its nodes by context and order them by layer, and would that make it the architecture view the Studio's architecture lens draws?" + } + ], + "decisions": [ + { + "id": "spec:decisions.shipped-projections-frozen", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.superseded-decision-signal", + "resolved": true, + "title": "A Spec shaped by a superseded decision warns", + "specKind": "rule", + "statedReadiness": "scoped", + "floorReached": "scoped", + "statedNextRung": "defined", + "statedNextRungFailures": [ + { + "clauseId": "no-blocking-open-questions", + "description": "Spec has no blocking open question in intent.openQuestions." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 3, + "blockingQuestions": 1 + }, + "questions": [ + { + "key": "keptRecord", + "blocking": true, + "address": "spec:validation.superseded-decision-signal#question.keptRecord", + "line": 25, + "question": "Does a corpus keep a replaced decision whose text a reader still needs beside its `supersedes` edge?" + }, + { + "key": "signalReach", + "blocking": false, + "address": "spec:validation.superseded-decision-signal#question.signalReach", + "line": 28, + "question": "Does the signal read only `decidedBy`, or also a `refines` or `dependsOn` that names a superseded decision?" + }, + { + "key": "supersededChain", + "blocking": false, + "address": "spec:validation.superseded-decision-signal#question.supersededChain", + "line": 31, + "question": "When a superseding decision is itself superseded, does the warning name the decision at the end of the chain?" + } + ], + "decisions": [ + { + "id": "spec:decisions.planning-truths-placement", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.architecture-constraints", + "resolved": true, + "title": "Declared architecture rules are checked against anchored structure", + "specKind": "rule", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 2, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "ruleForms", + "blocking": false, + "address": "spec:validation.architecture-constraints#question.ruleForms", + "line": 23, + "question": "Do corpus rules need to require a component to state a layer or context, or check ports and routes that anchors do not carry, beyond the selected forbidden layer pairs and isolated contexts?" + }, + { + "key": "walkInvocation", + "blocking": false, + "address": "spec:validation.architecture-constraints#question.walkInvocation", + "line": 26, + "question": "Which pure callable walk serves both the catalog recipe and a loaded corpus rule, and what rule-data and result shapes do they share?" + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.corpus-rules", + "resolved": true, + "title": "Corpus-authored rules run on the one validation path", + "specKind": "behavior", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "rulesRepresentation", + "blocking": false, + "address": "spec:validation.corpus-rules#question.rulesRepresentation", + "line": 27, + "question": "Does an operator-named rules file hold one async body returning findings or a module exporting named rule functions, how are parameters supplied, in what order do several files run, and does `sdp validate --watch` rerun them?" + }, + { + "key": "findingCurrency", + "blocking": false, + "address": "spec:validation.corpus-rules#question.findingCurrency", + "line": 30, + "question": "What shape does a corpus finding take beside the one diagnostic currency, where does the report carry it, and does it reach `report`, the reader's `findings()` or the Design Review?" + }, + { + "key": "ruleIsolation", + "blocking": false, + "address": "spec:validation.corpus-rules#question.ruleIsolation", + "line": 33, + "question": "Does the runner enforce that a rule reads only the graph and changes nothing, or are both obligations on the rule's author?" + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:observation.run-evidence", + "resolved": true, + "title": "CI run records are joined to verifiers in a view", + "specKind": "behavior", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "freshness", + "blocking": false, + "address": "spec:observation.run-evidence#question.freshness", + "line": 25, + "question": "How does a view say a run is stale: by its commit against the commit being read, or by whether the bound files changed between the two?" + }, + { + "key": "recipeContract", + "blocking": false, + "address": "spec:observation.run-evidence#question.recipeContract", + "line": 28, + "question": "Which parameter and result shapes, CTRF revision and run-commit field, corpus-commit input, input label, and file-path normalization rule define the run-evidence recipe, and how does it report a record that joins no test anchor, or missing commit or path information?" + }, + { + "key": "studioRuns", + "blocking": false, + "address": "spec:observation.run-evidence#question.studioRuns", + "line": 31, + "question": "Should a later Studio phase admit run records beside the graph, and which input and reproducibility contracts would that require?" + } + ], + "decisions": [ + { + "id": "spec:decisions.binding-not-liveness", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + } + ], + "boundary": { + "restsOn": [ + { + "id": "spec:carrier.slot-notation", + "title": "Slot notation declares, binds, and refuses to guess", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage" + ] + } + ] + }, + { + "id": "spec:consumers.adopter-on-ramp", + "title": "An adopter learns graph homes, carrier grammar and shipped paths from the on-ramps", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.engine-provenance" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface", + "title": "Agents script a visible typed graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:consumers.context-bundle" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.address-and-cycle-recipes", + "title": "Address resolution and dependency cycles stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.register-recipes", + "title": "Register questions stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.agent-surface.design-recipes" + ] + } + ] + }, + { + "id": "spec:consumers.authoring-on-ramp", + "title": "Authors move one Spec from intent to reviewed evidence", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:consumers.binding-language-views", + "title": "Views speak binding language, never the internal fact name", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.derived-readiness-banner", + "title": "Derived readiness renders beside the stated rung and warns in one direction", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.design-review", + "title": "Design Review renders graph context without becoming a gate", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.edit-model", + "title": "Views compose scoped intent instead of patching canonical source", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.graph-first-planning", + "title": "The next body of work is planned from the graph, not from a prose index", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:consumers.intent-composition", + "title": "Composed intent hands off on one proposal shape", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.projections-model", + "title": "Projections fan out from one graph without becoming truth stores", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.review-perspectives", + "spec:consumers.design-diff", + "spec:consumers.reference-projection", + "spec:consumers.structural-mermaid" + ] + } + ] + }, + { + "id": "spec:consumers.reader", + "title": "The reader bridges agent entry points to composable graph context", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:decisions.adopt-the-nouns", + "title": "Delivery nouns remain familiar without workflow gates", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:decisions.agent-front-door", + "title": "The agent front door is one evaluation sink over the exported reader", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:consumers.context-bundle" + ] + } + ] + }, + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:consumers.pack-design", + "spec:model.planned-architecture", + "spec:validation.architecture-constraints", + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:decisions.binding-not-liveness", + "title": "Bindings state existence, not liveness", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:observation.run-evidence" + ] + } + ] + }, + { + "id": "spec:decisions.checked-mentions", + "title": "A Spec id written in prose is a checked mention", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:decisions.one-validation-path", + "title": "Validation follows one graph", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:decisions.planning-truths-placement", + "title": "Planning truths live in ruled graph homes", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.open-question-fields", + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:decisions.prose-ownership", + "title": "Prose belongs to typed graph owners", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:decisions.shipped-projections-frozen", + "title": "The shipped projections stay frozen", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:decisions.pack-design-page" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:model.rule-keys", + "spec:consumers.structural-mermaid" + ] + } + ] + }, + { + "id": "spec:extraction.contract-declarations", + "title": "Pinned declarations give an adopter a derived module to typecheck", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:extraction.delivery-facts", + "title": "Resolving bindings confer direct delivery facts", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:observation.run-evidence" + ] + } + ] + }, + { + "id": "spec:extraction.derive-graph", + "title": "Carrier reification derives the one graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:extraction.entry-locations" + ] + } + ] + }, + { + "id": "spec:extraction.determinism", + "title": "Committed source derives byte-identical output", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:extraction.entry-locations", + "spec:consumers.engine-provenance", + "spec:consumers.design-diff" + ] + } + ] + }, + { + "id": "spec:extraction.excludes", + "title": "Extraction exclusions are strict consumer input", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:extraction.executable-contracts", + "title": "The build derives step and space contracts from graph examples", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage" + ] + } + ] + }, + { + "id": "spec:extraction.pack-member-order", + "title": "A Pack node keeps its members in authored order", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:model.anchors", + "title": "Source anchors bind code without carrying intent", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.planned-architecture", + "spec:validation.architecture-constraints" + ] + } + ] + }, + { + "id": "spec:model.core-model", + "title": "The Protocol models delivery with one enrichable Spec", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.design-maturity" + ] + } + ] + }, + { + "id": "spec:model.open-question-keys", + "title": "An open question may carry a key that addresses it", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.open-question-fields" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:extraction.entry-locations", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:model.relations", + "title": "Specs declare typed directed relations", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.relation-reasons" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:model.spec-sections", + "title": "Spec sections carry typed detail and direct verifier semantics", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:validation.diagnostic-rendering", + "title": "One diagnostic currency, its location composed from structured fields", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:validation.next-rung-floor", + "title": "The floor names what blocks the floor's next rung", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:validation.two-check-families", + "title": "Validation separates well-formedness from non-pretending", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:validation.warn-level-signals", + "title": "Missing connective evidence warns without failing", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + } + ], + "restedOnBy": [ + { + "id": "spec:carrier.envelope-contract", + "title": "The Markdown envelope is explicit and bounded", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:carrier.gherkin-authoring", + "title": "Gherkin authoring enters the one graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:carrier.markdown-body-grammar", + "title": "The Markdown body accepts one closed form per section", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:carrier.prose-ownership-rule", + "title": "Every prose value has one typed owner", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.address-and-cycle-recipes", + "title": "Address resolution and dependency cycles stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.answer-shapes", + "title": "Recipe answers fit an agent's context and explain their misses", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.design-recipes" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.register-recipes", + "title": "Register questions stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.authoring-on-ramp", + "title": "Authors move one Spec from intent to reviewed evidence", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.delivery-session-on-ramp", + "title": "Delivery sessions route work from current graph state", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.review-perspectives", + "spec:consumers.context-bundle", + "spec:consumers.design-diff", + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.graph-first-planning", + "title": "The next body of work is planned from the graph, not from a prose index", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:consumers.impact-graph.curation-assist", + "title": "File-level imports show where curation is missing", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.planned-architecture" + ] + } + ] + }, + { + "id": "spec:consumers.obsidian-vault.bases", + "title": "A Pack's members and the standing queries are tables over the reader's values", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:consumers.obsidian-vault.iteration", + "title": "A design pass is followed and answered from the vault", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.data", + "title": "The Studio renders from the reader's values and embeds only what its script reads", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:extraction.entry-locations" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.intent-panel", + "title": "A composing panel on every page gathers scoped intent for an agent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.lenses", + "title": "Four lenses read the graph as Packs, architecture, tests and evidence", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.pack-page", + "title": "A Pack page shows the Pack's design as one card per member", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.sharing", + "title": "A Studio build is stamped and shared from a static host", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.engine-provenance" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.spec-page", + "title": "A Spec page shows one Spec in collapsible panels, in a fixed order", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:extraction.entry-locations", + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.spec-page.core", + "title": "A Spec page's core shows the whole Spec, with an anchor for every entry", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:extraction.entry-locations", + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.verification-panels", + "title": "The panels for examples, targets, verifiers and evidence", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:model.term-addresses", + "title": "A Model term may carry a key that addresses it", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:validation.prose-mentions", + "title": "Every Spec id written in prose resolves", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + } + ], + "verifiedBy": [], + "supersededBy": [] + } +}, +"context": { + "id": "pack:design-management-v1", + "title": "Design management", + "framing": "The design-management arc, in review order: the Pack page and the reader assembly it renders; entry locations, recipe parameters and shipped recipe files; the recipes used in design work; the engine build identity and the authoring watch loop. The remaining Specs capture promises of the original design across the model, consumers, validation and observation. Pack design shows each member's stated next rung, design columns and code bindings.", + "file": "specs/design-management.pack.sdp.md", + "modelRefs": [ + "spec:model.design-maturity" + ], + "members": [ + { + "id": "spec:decisions.pack-design-page", + "resolved": true, + "title": "The Design Review's Pack page presents the Pack's design", + "specKind": "decision", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/decisions/pack-design-page.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:protocol.pack-design-page", + "claim": "anchored", + "label": "renders the Pack page from the Pack design", + "file": "src/projections/design-review-pages.ts", + "line": 128, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.pack-design", + "resolved": true, + "title": "The Pack context carries the Pack's design", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/pack-design.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.pack-design", + "claim": "anchored", + "label": "the Pack design helpers and the Pack context assembly they serve", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.design-review.pack-design-page", + "resolved": true, + "title": "A Pack page shows where the Pack's design stands", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/design-review.pack-design-page.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [ + { + "id": "spec:decisions.pack-design-page", + "title": "The Design Review's Pack page presents the Pack's design", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.pack-design-page", + "claim": "anchored", + "label": "renders the Pack page from the Pack design", + "file": "src/projections/design-review-pages.ts", + "line": 128, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "references": [ + { + "codeId": "impl:protocol.pack-design-sections", + "claim": "anchored", + "label": "renders the Pack page's next-rung, boundary, question and code sections", + "file": "src/projections/design-review-pack-sections.ts", + "line": 19, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:extraction.entry-locations", + "resolved": true, + "title": "The graph records where each addressable entry is written", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/extraction/entry-locations.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.entry-location-recipes", + "claim": "anchored", + "label": "asserts realization of the entry lines the entry search and address resolution recipes return", + "file": "test/recipes.test.ts", + "line": 5689 + }, + { + "codeId": "impl:protocol.entry-locations", + "claim": "anchored", + "label": "builds the carrier entry location table", + "file": "src/extract/derive.ts", + "line": 78, + "role": "extractor", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + }, + { + "codeId": "impl:protocol.reader", + "claim": "anchored", + "label": "thin typed graph reader construction", + "file": "src/reader/reader.ts", + "line": 725, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "references": [ + { + "codeId": "impl:protocol.pack-design", + "claim": "anchored", + "label": "the Pack design helpers and the Pack context assembly they serve", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.recipe-parameters", + "resolved": true, + "title": "Recipes take parameters as data and ship as runnable files", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/agent-surface.recipe-parameters.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [ + { + "id": "spec:decisions.agent-front-door", + "title": "The agent front door is one evaluation sink over the exported reader", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.build-artifacts", + "claim": "anchored", + "label": "the build step that writes each catalog recipe to dist/recipes and records the build commit beside the CLI", + "file": "src/cli/build-artifacts.ts", + "line": 177, + "role": "publisher", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "codeId": "impl:protocol.query-params", + "claim": "anchored", + "label": "reads --params as JSON data and injects it as the sink's fourth binding, params", + "file": "src/cli/q-command.ts", + "line": 179, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "codeId": "impl:protocol.recipe-parameters-catalog", + "claim": "anchored", + "label": "asserts realization of the catalog recipes that read their parameter from params", + "file": "test/recipes.test.ts", + "line": 4241 + } + ], + "references": [], + "verifiers": { + "total": 3, + "enabled": 3 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.design-recipes", + "resolved": true, + "title": "Design work reads its answers from catalog recipes", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/agent-surface.design-recipes.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.design-recipes", + "claim": "anchored", + "label": "asserts realization of the shipped design recipes", + "file": "test/recipes.test.ts", + "line": 4528 + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.example-space-coverage", + "resolved": true, + "title": "Example spaces report which values their examples cover", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/consumers/agent-surface.example-space-coverage.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Is coverage per slot value enough, or does a design pass need the combinations of two or more slots, as the original heatmap drew them?", + "blocking": false, + "key": "combinationCoverage", + "decider": "owner", + "links": [ + "spec:consumers.spec-studio.verification-panels#ui.coverageGrid" + ], + "provisional": "the recipe reports per-slot literal-union values", + "line": 22 + }, + { + "index": 1, + "question": "What reader field names and decoded slot and bound-point shapes, recipe parameters and result rows, eligible-example relation policy, and treatment of an unbound slot form the coverage contract?", + "blocking": false, + "key": "coverageShape", + "decider": "owner", + "links": [ + "spec:extraction.executable-contracts" + ], + "provisional": "the tests-lens design pass settles the decoded fields and the row shape and records them as keyed Design entries before implementation", + "line": 26 + } + ], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.engine-provenance", + "resolved": true, + "title": "The CLI names the engine that produced a result", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/engine-provenance.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.build-artifacts", + "claim": "anchored", + "label": "the build step that writes each catalog recipe to dist/recipes and records the build commit beside the CLI", + "file": "src/cli/build-artifacts.ts", + "line": 177, + "role": "publisher", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "codeId": "impl:protocol.engine-provenance-cli", + "claim": "anchored", + "label": "prints sdp version and the commit parsed from the build-info file beside the compiled CLI", + "file": "src/cli/sdp.ts", + "line": 193, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.validate-watch", + "resolved": true, + "title": "Validation re-runs while an author edits carriers", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/validate-watch.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.validate-watch", + "claim": "anchored", + "label": "watches carrier directories and coalesces events into a pending validation rerun", + "file": "src/cli/validate-watch.ts", + "line": 474, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.design-maturity", + "resolved": true, + "title": "What a Spec's design carries is derived, beside its readiness", + "specKind": "model", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/model/design-maturity.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "If the first adopter's design passes show that the design columns cannot distinguish settled design from intent alone, should this representation be revisited?", + "blocking": false, + "key": "designColumnsSufficiency", + "decider": "owner", + "provisional": "derive what design a Spec carries beside readiness without a new rung, floor clause or delivery fact", + "line": 20 + } + ], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.pack-design", + "claim": "anchored", + "label": "the Pack design helpers and the Pack context assembly they serve", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.planned-architecture", + "resolved": true, + "title": "Components are designed ahead of their code", + "specKind": "model", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/model/planned-architecture.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "The original kept apart the interface a unit requires, the providers that supply it, the provider a composition root selects and the one a test substitutes, for Fastify, Effect, Awilix and plain factories alike. Does a component design state these without naming a framework, and does any of them earn graph structure beyond `uses`?", + "blocking": false, + "key": "providerSelection", + "decider": "owner", + "line": 24 + }, + { + "index": 1, + "question": "Is the original's one-runtime-truth rule, one composition mechanism per application, a Protocol rule or a corpus's own architecture rule?", + "blocking": false, + "key": "oneRuntimeOwner", + "decider": "owner", + "line": 26 + }, + { + "index": 2, + "question": "How is a component design selected, what value form does its `uses` entry take, and what rows does the realization comparison return for an unrealized design and for a layer, context or `uses` that differs?", + "blocking": false, + "key": "comparisonShape", + "decider": "owner", + "provisional": "a Spec is a component design when its Design `id` value, bare or in the code span that opens it, is a `component:` id, as the curation report reads it", + "line": 28 + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:protocol.curation-recipe", + "claim": "anchored", + "label": "asserts the curation report over external inferred imports", + "file": "test/curation-recipes.test.ts", + "line": 32 + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.open-question-fields", + "resolved": true, + "title": "An open question may name who decides it, its links, a provisional reading and a re-entry trigger", + "specKind": "rule", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/model/open-question-fields.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 1, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "A non-static question text, blocking flag or spread in a TypeScript open-question list also drops the whole list, blocking questions included, and stays a warning; is every dropped question list an error?", + "blocking": false, + "key": "droppedQuestionList", + "decider": "owner", + "provisional": "only a failure under the four fields is an error", + "line": 27 + } + ], + "decisions": [ + { + "id": "spec:decisions.adopt-the-nouns", + "title": "Delivery nouns remain familiar without workflow gates", + "statedReadiness": "ready", + "resolved": true + }, + { + "id": "spec:decisions.planning-truths-placement", + "title": "Planning truths live in ruled graph homes", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.authoring-grammar-recipes", + "claim": "anchored", + "label": "asserts the authoring grammar's realization in docs/agent-surface/recipes.md", + "file": "test/recipes.test.ts", + "line": 6307 + }, + { + "codeId": "impl:protocol.authoring-normalization", + "claim": "anchored", + "label": "normalizes rule markers and question fields after carrier reification", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.rule-keys", + "resolved": true, + "title": "A rule or flow entry may carry a key that addresses it", + "specKind": "rule", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/model/rule-keys.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 10, + "declarations": 6, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [ + { + "id": "spec:decisions.checked-mentions", + "title": "A Spec id written in prose is a checked mention", + "statedReadiness": "ready", + "resolved": true + }, + { + "id": "spec:decisions.shipped-projections-frozen", + "title": "The shipped projections stay frozen", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.authoring-grammar-recipes", + "claim": "anchored", + "label": "asserts the authoring grammar's realization in docs/agent-surface/recipes.md", + "file": "test/recipes.test.ts", + "line": 6307 + }, + { + "codeId": "impl:protocol.authoring-normalization", + "claim": "anchored", + "label": "normalizes rule markers and question fields after carrier reification", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + }, + { + "codeId": "impl:protocol.rule-exercises", + "claim": "anchored", + "label": "checks the keys named by declared verifies relations", + "file": "src/validate/validators.ts", + "line": 1484, + "role": "validator", + "component": { + "id": "component:protocol.validate", + "layer": "domain", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.relation-reasons", + "resolved": true, + "title": "An authored relation may carry its reason", + "specKind": "rule", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/model/relation-reasons.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 5, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [ + { + "id": "spec:decisions.prose-ownership", + "title": "Prose belongs to typed graph owners", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.authoring-grammar-recipes", + "claim": "anchored", + "label": "asserts the authoring grammar's realization in docs/agent-surface/recipes.md", + "file": "test/recipes.test.ts", + "line": 6307 + }, + { + "codeId": "impl:protocol.authoring-normalization", + "claim": "anchored", + "label": "normalizes rule markers and question fields after carrier reification", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.review-perspectives", + "resolved": true, + "title": "A Pack is read from a named perspective", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/review-perspectives.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 4, + "declarations": 1, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.perspective-filter-recipe", + "claim": "anchored", + "label": "realizes the perspective filter in the shipped recipe catalog", + "file": "test/iteration-recipes.test.ts", + "line": 751, + "role": "projection" + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.context-bundle", + "resolved": true, + "title": "A context bundle is composed per invocation within a stated budget", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/context-bundle.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 3, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "No cut reduces a pinned declaration, so a Pack whose pinned declarations alone exceed the budget fits no bundle; may a caller ask the bundle to reduce pinned declarations to their entry addresses?", + "blocking": false, + "key": "declarationCut", + "decider": "owner", + "provisional": "pinned declarations stay whole, and a caller narrows the scope to fewer members, such as one perspective's", + "line": 23 + } + ], + "decisions": [ + { + "id": "spec:decisions.agent-front-door", + "title": "The agent front door is one evaluation sink over the exported reader", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.context-bundle-recipe", + "claim": "anchored", + "label": "realizes the context bundle in the shipped recipe catalog", + "file": "test/iteration-recipes.test.ts", + "line": 763, + "role": "projection" + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.design-diff", + "resolved": true, + "title": "Designs are compared between a base commit and the working tree at entry grain", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/design-diff.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 8, + "declarations": 4, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.design-diff-recipe", + "claim": "anchored", + "label": "realizes the design diff in the shipped recipe catalog", + "file": "test/iteration-recipes.test.ts", + "line": 27, + "role": "projection" + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.intent-composition.proposal", + "resolved": true, + "title": "A change proposal is addressed, based and checked before it lands", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/intent-composition.proposal.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 2, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Which catalog recipe renders a proposal as the Markdown memo a person reviews, so the Studio's composing panel and an agent hand off the same text?", + "blocking": false, + "key": "memoRecipe", + "decider": "owner", + "links": [ + "spec:consumers.spec-studio.intent-panel" + ], + "provisional": "a person reviews the proposal's JSON beside the proposal check's rows", + "line": 25 + } + ], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.proposal-schema-and-recipe", + "claim": "anchored", + "label": "realizes the shipped proposal schema and proposal check in the recipe catalog", + "file": "test/iteration-recipes.test.ts", + "line": 1291, + "role": "contract" + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.reference-projection", + "resolved": true, + "title": "A reference projection serves a reader outside the repository", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "idea", + "derivedReadiness": "idea", + "deliveryFacts": [], + "file": "specs/consumers/reference-projection.sdp.md", + "statedNextRung": "scoped", + "statedNextRungFailures": [ + { + "clauseId": "kind-evidence-present", + "description": "The kind's natural evidence is present (per-kind evidence table)." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 1, + "openQuestions": [ + { + "index": 0, + "question": "Does a human reader outside the repository need a reference projection the four shipped roots do not serve? The taxonomy row is not that reader.", + "blocking": true, + "key": "namedOutsideReader", + "decider": "owner", + "trigger": "a named consumer outside the repository, with a way to measure that demand inside the arc that builds the projection", + "line": 17 + }, + { + "index": 1, + "question": "Would the reference read pinned declarations, contract-kind Specs or `api:` code anchors, and does any of them hold enough to document an interface?", + "blocking": false, + "key": "referenceSource", + "line": 20 + } + ], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.structural-mermaid", + "resolved": true, + "title": "Mermaid diagrams can be rooted at code units", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "idea", + "derivedReadiness": "idea", + "deliveryFacts": [], + "file": "specs/consumers/structural-mermaid.sdp.md", + "statedNextRung": "scoped", + "statedNextRungFailures": [ + { + "clauseId": "kind-evidence-present", + "description": "The kind's natural evidence is present (per-kind evidence table)." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 1, + "openQuestions": [ + { + "index": 0, + "question": "Does a reader need diagrams rooted at code units, which census tables and Spec-rooted one-hop Mermaid do not serve?", + "blocking": true, + "key": "namedStructuralReader", + "decider": "owner", + "trigger": "a named reader with that need, plus a decision that refines `spec:decisions.shipped-projections-frozen` for the affected Mermaid rendering or supersedes that ruling whole", + "line": 18 + }, + { + "index": 1, + "question": "Would a component diagram group its nodes by context and order them by layer, and would that make it the architecture view the Studio's architecture lens draws?", + "blocking": false, + "key": "layerAndContext", + "decider": "owner", + "links": [ + "spec:consumers.spec-studio.lenses#question.architectureVocabulary" + ], + "provisional": "the diagram folds into the Studio's architecture lens when that lens is designed, rather than becoming a Mermaid root", + "line": 21 + } + ], + "decisions": [ + { + "id": "spec:decisions.shipped-projections-frozen", + "title": "The shipped projections stay frozen", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.superseded-decision-signal", + "resolved": true, + "title": "A Spec shaped by a superseded decision warns", + "specKind": "rule", + "altitude": "story", + "statedReadiness": "scoped", + "derivedReadiness": "scoped", + "deliveryFacts": [], + "file": "specs/validation/superseded-decision-signal.sdp.md", + "statedNextRung": "defined", + "statedNextRungFailures": [ + { + "clauseId": "no-blocking-open-questions", + "description": "Spec has no blocking open question in intent.openQuestions." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 1, + "openQuestions": [ + { + "index": 0, + "question": "Does a corpus keep a replaced decision whose text a reader still needs beside its `supersedes` edge?", + "blocking": true, + "key": "keptRecord", + "decider": "owner", + "trigger": "a corpus keeps a replaced decision record whose text a reader still needs", + "line": 25 + }, + { + "index": 1, + "question": "Does the signal read only `decidedBy`, or also a `refines` or `dependsOn` that names a superseded decision?", + "blocking": false, + "key": "signalReach", + "decider": "owner", + "provisional": "the signal reads `decidedBy` only", + "line": 28 + }, + { + "index": 2, + "question": "When a superseding decision is itself superseded, does the warning name the decision at the end of the chain?", + "blocking": false, + "key": "supersededChain", + "decider": "owner", + "line": 31 + } + ], + "decisions": [ + { + "id": "spec:decisions.planning-truths-placement", + "title": "Planning truths live in ruled graph homes", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.architecture-constraints", + "resolved": true, + "title": "Declared architecture rules are checked against anchored structure", + "specKind": "rule", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/validation/architecture-constraints.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Do corpus rules need to require a component to state a layer or context, or check ports and routes that anchors do not carry, beyond the selected forbidden layer pairs and isolated contexts?", + "blocking": false, + "key": "ruleForms", + "decider": "owner", + "provisional": "the supplied walk checks forbidden layer pairs and isolated contexts; absent attributes are unclassified rather than breaches", + "line": 23 + }, + { + "index": 1, + "question": "Which pure callable walk serves both the catalog recipe and a loaded corpus rule, and what rule-data and result shapes do they share?", + "blocking": false, + "key": "walkInvocation", + "decider": "owner", + "provisional": "one pure walk takes `params.forbidden`, a list of `{ from, to }` layer pairs, and `params.isolated`, a list of context tokens whose components may have no `uses` edge into another context, either list possibly empty; it returns each forbidden edge as `{ from, to, fromComponent, toComponent, rule }` and lists unclassified edges apart; recipe 32's result stays as it is", + "line": 26 + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.corpus-rules", + "resolved": true, + "title": "Corpus-authored rules run on the one validation path", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/validation/corpus-rules.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Does an operator-named rules file hold one async body returning findings or a module exporting named rule functions, how are parameters supplied, in what order do several files run, and does `sdp validate --watch` rerun them?", + "blocking": false, + "key": "rulesRepresentation", + "decider": "owner", + "provisional": "one async body per file, given `g`, `graph`, `report` and `params`, with `params` from `sdp validate --params` and `{}` without it; files run in the order named, each to completion; a rule id is unique within its file; `sdp validate --watch` runs the same named rules each cycle", + "line": 27 + }, + { + "index": 1, + "question": "What shape does a corpus finding take beside the one diagnostic currency, where does the report carry it, and does it reach `report`, the reader's `findings()` or the Design Review?", + "blocking": false, + "key": "findingCurrency", + "decider": "owner", + "provisional": "the shared finding contract has a Protocol branch carrying `validatorId`, `family`, severity and message, and a corpus branch carrying the corpus, the rule id as `validatorId`, `subjectId`, severity and message and no family; `relatedId`, `path`, `file` and `line` stay optional on both branches; corpus findings are collected as `corpusFindings` in the same report and rendered under a corpus-labelled section with the existing location, severity, identifier and message composition; Protocol findings, the summary line and the Design Review stay unchanged, as the frozen-projections decision requires", + "line": 30 + }, + { + "index": 2, + "question": "Does the runner enforce that a rule reads only the graph and changes nothing, or are both obligations on the rule's author?", + "blocking": false, + "key": "ruleIsolation", + "decider": "owner", + "provisional": "changing nothing is a guarantee, with the Protocol's validators run first and each rule given a frozen or cloned graph; reading only the graph is the author's obligation, run with the trust `sdp q` asks and no sandbox", + "line": 33 + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:observation.run-evidence", + "resolved": true, + "title": "CI run records are joined to verifiers in a view", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/observation/run-evidence.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "How does a view say a run is stale: by its commit against the commit being read, or by whether the bound files changed between the two?", + "blocking": false, + "key": "freshness", + "decider": "owner", + "provisional": "Display the run and source commit identifiers when they differ, as the differing-commit rule requires; whether freshness also follows changes to bound files remains open.", + "line": 25 + }, + { + "index": 1, + "question": "Which parameter and result shapes, CTRF revision and run-commit field, corpus-commit input, input label, and file-path normalization rule define the run-evidence recipe, and how does it report a record that joins no test anchor, or missing commit or path information?", + "blocking": false, + "key": "recipeContract", + "decider": "owner", + "provisional": "Records stay external, join only through the named test file, and confer no delivery fact; the ruled contract lands as keyed Design entries before the view is built.", + "line": 28 + }, + { + "index": 2, + "question": "Should a later Studio phase admit run records beside the graph, and which input and reproducibility contracts would that require?", + "blocking": false, + "key": "studioRuns", + "decider": "owner", + "provisional": "The Studio's first build stays graph-only; a later phase may admit named run records under its own input and reproducibility contract.", + "line": 31 + } + ], + "decisions": [ + { + "id": "spec:decisions.binding-not-liveness", + "title": "Bindings state existence, not liveness", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + } + ], + "verifierGaps": [ + { + "id": "spec:consumers.agent-surface.example-space-coverage", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:model.planned-architecture", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:consumers.reference-projection", + "statedReadiness": "idea", + "priority": false + }, + { + "id": "spec:consumers.structural-mermaid", + "statedReadiness": "idea", + "priority": false + }, + { + "id": "spec:validation.superseded-decision-signal", + "statedReadiness": "scoped", + "priority": false + }, + { + "id": "spec:validation.architecture-constraints", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:validation.corpus-rules", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:observation.run-evidence", + "statedReadiness": "defined", + "priority": false + } + ], + "boundary": { + "restsOn": [ + { + "id": "spec:carrier.slot-notation", + "title": "Slot notation declares, binds, and refuses to guess", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage" + ] + } + ] + }, + { + "id": "spec:consumers.adopter-on-ramp", + "title": "An adopter learns graph homes, carrier grammar and shipped paths from the on-ramps", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.engine-provenance" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface", + "title": "Agents script a visible typed graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:consumers.context-bundle" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.address-and-cycle-recipes", + "title": "Address resolution and dependency cycles stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.register-recipes", + "title": "Register questions stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.agent-surface.design-recipes" + ] + } + ] + }, + { + "id": "spec:consumers.authoring-on-ramp", + "title": "Authors move one Spec from intent to reviewed evidence", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:consumers.binding-language-views", + "title": "Views speak binding language, never the internal fact name", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.derived-readiness-banner", + "title": "Derived readiness renders beside the stated rung and warns in one direction", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.design-review", + "title": "Design Review renders graph context without becoming a gate", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.edit-model", + "title": "Views compose scoped intent instead of patching canonical source", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.graph-first-planning", + "title": "The next body of work is planned from the graph, not from a prose index", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:consumers.intent-composition", + "title": "Composed intent hands off on one proposal shape", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.projections-model", + "title": "Projections fan out from one graph without becoming truth stores", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.review-perspectives", + "spec:consumers.design-diff", + "spec:consumers.reference-projection", + "spec:consumers.structural-mermaid" + ] + } + ] + }, + { + "id": "spec:consumers.reader", + "title": "The reader bridges agent entry points to composable graph context", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:decisions.adopt-the-nouns", + "title": "Delivery nouns remain familiar without workflow gates", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:decisions.agent-front-door", + "title": "The agent front door is one evaluation sink over the exported reader", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:consumers.context-bundle" + ] + } + ] + }, + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:consumers.pack-design", + "spec:model.planned-architecture", + "spec:validation.architecture-constraints", + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:decisions.binding-not-liveness", + "title": "Bindings state existence, not liveness", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:observation.run-evidence" + ] + } + ] + }, + { + "id": "spec:decisions.checked-mentions", + "title": "A Spec id written in prose is a checked mention", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:decisions.one-validation-path", + "title": "Validation follows one graph", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:decisions.planning-truths-placement", + "title": "Planning truths live in ruled graph homes", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.open-question-fields", + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:decisions.prose-ownership", + "title": "Prose belongs to typed graph owners", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:decisions.shipped-projections-frozen", + "title": "The shipped projections stay frozen", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:decisions.pack-design-page" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:model.rule-keys", + "spec:consumers.structural-mermaid" + ] + } + ] + }, + { + "id": "spec:extraction.contract-declarations", + "title": "Pinned declarations give an adopter a derived module to typecheck", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:extraction.delivery-facts", + "title": "Resolving bindings confer direct delivery facts", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:observation.run-evidence" + ] + } + ] + }, + { + "id": "spec:extraction.derive-graph", + "title": "Carrier reification derives the one graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:extraction.entry-locations" + ] + } + ] + }, + { + "id": "spec:extraction.determinism", + "title": "Committed source derives byte-identical output", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:extraction.entry-locations", + "spec:consumers.engine-provenance", + "spec:consumers.design-diff" + ] + } + ] + }, + { + "id": "spec:extraction.excludes", + "title": "Extraction exclusions are strict consumer input", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:extraction.executable-contracts", + "title": "The build derives step and space contracts from graph examples", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage" + ] + } + ] + }, + { + "id": "spec:extraction.pack-member-order", + "title": "A Pack node keeps its members in authored order", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:model.anchors", + "title": "Source anchors bind code without carrying intent", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.planned-architecture", + "spec:validation.architecture-constraints" + ] + } + ] + }, + { + "id": "spec:model.core-model", + "title": "The Protocol models delivery with one enrichable Spec", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.design-maturity" + ] + } + ] + }, + { + "id": "spec:model.open-question-keys", + "title": "An open question may carry a key that addresses it", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.open-question-fields" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:extraction.entry-locations", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:model.relations", + "title": "Specs declare typed directed relations", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.relation-reasons" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:model.spec-sections", + "title": "Spec sections carry typed detail and direct verifier semantics", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:validation.diagnostic-rendering", + "title": "One diagnostic currency, its location composed from structured fields", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:validation.next-rung-floor", + "title": "The floor names what blocks the floor's next rung", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:validation.two-check-families", + "title": "Validation separates well-formedness from non-pretending", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:validation.warn-level-signals", + "title": "Missing connective evidence warns without failing", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + } + ], + "restedOnBy": [ + { + "id": "spec:carrier.envelope-contract", + "title": "The Markdown envelope is explicit and bounded", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:carrier.gherkin-authoring", + "title": "Gherkin authoring enters the one graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:carrier.markdown-body-grammar", + "title": "The Markdown body accepts one closed form per section", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:carrier.prose-ownership-rule", + "title": "Every prose value has one typed owner", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.address-and-cycle-recipes", + "title": "Address resolution and dependency cycles stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.answer-shapes", + "title": "Recipe answers fit an agent's context and explain their misses", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.design-recipes" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.register-recipes", + "title": "Register questions stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.authoring-on-ramp", + "title": "Authors move one Spec from intent to reviewed evidence", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.delivery-session-on-ramp", + "title": "Delivery sessions route work from current graph state", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.review-perspectives", + "spec:consumers.context-bundle", + "spec:consumers.design-diff", + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.graph-first-planning", + "title": "The next body of work is planned from the graph, not from a prose index", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:consumers.impact-graph.curation-assist", + "title": "File-level imports show where curation is missing", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.planned-architecture" + ] + } + ] + }, + { + "id": "spec:consumers.obsidian-vault.bases", + "title": "A Pack's members and the standing queries are tables over the reader's values", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:consumers.obsidian-vault.iteration", + "title": "A design pass is followed and answered from the vault", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.data", + "title": "The Studio renders from the reader's values and embeds only what its script reads", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:extraction.entry-locations" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.intent-panel", + "title": "A composing panel on every page gathers scoped intent for an agent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.lenses", + "title": "Four lenses read the graph as Packs, architecture, tests and evidence", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.pack-page", + "title": "A Pack page shows the Pack's design as one card per member", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.sharing", + "title": "A Studio build is stamped and shared from a static host", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.engine-provenance" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.spec-page", + "title": "A Spec page shows one Spec in collapsible panels, in a fixed order", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:extraction.entry-locations", + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.spec-page.core", + "title": "A Spec page's core shows the whole Spec, with an anchor for every entry", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:extraction.entry-locations", + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.verification-panels", + "title": "The panels for examples, targets, verifiers and evidence", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:model.term-addresses", + "title": "A Model term may carry a key that addresses it", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:validation.prose-mentions", + "title": "Every Spec id written in prose resolves", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + } + ], + "verifiedBy": [], + "supersededBy": [] + }, + "findings": [] +} +}; diff --git a/explorations/studio-prototype/data/design-management-v1.pack-context.json b/explorations/studio-prototype/data/design-management-v1.pack-context.json new file mode 100644 index 00000000..9848f154 --- /dev/null +++ b/explorations/studio-prototype/data/design-management-v1.pack-context.json @@ -0,0 +1,2412 @@ +{ + "id": "pack:design-management-v1", + "title": "Design management", + "framing": "The design-management arc, in review order: the Pack page and the reader assembly it renders; entry locations, recipe parameters and shipped recipe files; the recipes used in design work; the engine build identity and the authoring watch loop. The remaining Specs capture promises of the original design across the model, consumers, validation and observation. Pack design shows each member's stated next rung, design columns and code bindings.", + "file": "specs/design-management.pack.sdp.md", + "modelRefs": [ + "spec:model.design-maturity" + ], + "members": [ + { + "id": "spec:decisions.pack-design-page", + "resolved": true, + "title": "The Design Review's Pack page presents the Pack's design", + "specKind": "decision", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/decisions/pack-design-page.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:protocol.pack-design-page", + "claim": "anchored", + "label": "renders the Pack page from the Pack design", + "file": "src/projections/design-review-pages.ts", + "line": 128, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.pack-design", + "resolved": true, + "title": "The Pack context carries the Pack's design", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/pack-design.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.pack-design", + "claim": "anchored", + "label": "the Pack design helpers and the Pack context assembly they serve", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.design-review.pack-design-page", + "resolved": true, + "title": "A Pack page shows where the Pack's design stands", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/design-review.pack-design-page.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [ + { + "id": "spec:decisions.pack-design-page", + "title": "The Design Review's Pack page presents the Pack's design", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.pack-design-page", + "claim": "anchored", + "label": "renders the Pack page from the Pack design", + "file": "src/projections/design-review-pages.ts", + "line": 128, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "references": [ + { + "codeId": "impl:protocol.pack-design-sections", + "claim": "anchored", + "label": "renders the Pack page's next-rung, boundary, question and code sections", + "file": "src/projections/design-review-pack-sections.ts", + "line": 19, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:extraction.entry-locations", + "resolved": true, + "title": "The graph records where each addressable entry is written", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/extraction/entry-locations.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.entry-location-recipes", + "claim": "anchored", + "label": "asserts realization of the entry lines the entry search and address resolution recipes return", + "file": "test/recipes.test.ts", + "line": 5689 + }, + { + "codeId": "impl:protocol.entry-locations", + "claim": "anchored", + "label": "builds the carrier entry location table", + "file": "src/extract/derive.ts", + "line": 78, + "role": "extractor", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + }, + { + "codeId": "impl:protocol.reader", + "claim": "anchored", + "label": "thin typed graph reader construction", + "file": "src/reader/reader.ts", + "line": 725, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "references": [ + { + "codeId": "impl:protocol.pack-design", + "claim": "anchored", + "label": "the Pack design helpers and the Pack context assembly they serve", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.recipe-parameters", + "resolved": true, + "title": "Recipes take parameters as data and ship as runnable files", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/agent-surface.recipe-parameters.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [ + { + "id": "spec:decisions.agent-front-door", + "title": "The agent front door is one evaluation sink over the exported reader", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.build-artifacts", + "claim": "anchored", + "label": "the build step that writes each catalog recipe to dist/recipes and records the build commit beside the CLI", + "file": "src/cli/build-artifacts.ts", + "line": 177, + "role": "publisher", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "codeId": "impl:protocol.query-params", + "claim": "anchored", + "label": "reads --params as JSON data and injects it as the sink's fourth binding, params", + "file": "src/cli/q-command.ts", + "line": 179, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "codeId": "impl:protocol.recipe-parameters-catalog", + "claim": "anchored", + "label": "asserts realization of the catalog recipes that read their parameter from params", + "file": "test/recipes.test.ts", + "line": 4241 + } + ], + "references": [], + "verifiers": { + "total": 3, + "enabled": 3 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.design-recipes", + "resolved": true, + "title": "Design work reads its answers from catalog recipes", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/agent-surface.design-recipes.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.design-recipes", + "claim": "anchored", + "label": "asserts realization of the shipped design recipes", + "file": "test/recipes.test.ts", + "line": 4528 + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.example-space-coverage", + "resolved": true, + "title": "Example spaces report which values their examples cover", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/consumers/agent-surface.example-space-coverage.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Is coverage per slot value enough, or does a design pass need the combinations of two or more slots, as the original heatmap drew them?", + "blocking": false, + "key": "combinationCoverage", + "decider": "owner", + "links": [ + "spec:consumers.spec-studio.verification-panels#ui.coverageGrid" + ], + "provisional": "the recipe reports per-slot literal-union values", + "line": 22 + }, + { + "index": 1, + "question": "What reader field names and decoded slot and bound-point shapes, recipe parameters and result rows, eligible-example relation policy, and treatment of an unbound slot form the coverage contract?", + "blocking": false, + "key": "coverageShape", + "decider": "owner", + "links": [ + "spec:extraction.executable-contracts" + ], + "provisional": "the tests-lens design pass settles the decoded fields and the row shape and records them as keyed Design entries before implementation", + "line": 26 + } + ], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.engine-provenance", + "resolved": true, + "title": "The CLI names the engine that produced a result", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/engine-provenance.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.build-artifacts", + "claim": "anchored", + "label": "the build step that writes each catalog recipe to dist/recipes and records the build commit beside the CLI", + "file": "src/cli/build-artifacts.ts", + "line": 177, + "role": "publisher", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "codeId": "impl:protocol.engine-provenance-cli", + "claim": "anchored", + "label": "prints sdp version and the commit parsed from the build-info file beside the compiled CLI", + "file": "src/cli/sdp.ts", + "line": 193, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.validate-watch", + "resolved": true, + "title": "Validation re-runs while an author edits carriers", + "specKind": "behavior", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/validate-watch.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.validate-watch", + "claim": "anchored", + "label": "watches carrier directories and coalesces events into a pending validation rerun", + "file": "src/cli/validate-watch.ts", + "line": 474, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.design-maturity", + "resolved": true, + "title": "What a Spec's design carries is derived, beside its readiness", + "specKind": "model", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/model/design-maturity.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "If the first adopter's design passes show that the design columns cannot distinguish settled design from intent alone, should this representation be revisited?", + "blocking": false, + "key": "designColumnsSufficiency", + "decider": "owner", + "provisional": "derive what design a Spec carries beside readiness without a new rung, floor clause or delivery fact", + "line": 20 + } + ], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.pack-design", + "claim": "anchored", + "label": "the Pack design helpers and the Pack context assembly they serve", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.planned-architecture", + "resolved": true, + "title": "Components are designed ahead of their code", + "specKind": "model", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/model/planned-architecture.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "The original kept apart the interface a unit requires, the providers that supply it, the provider a composition root selects and the one a test substitutes, for Fastify, Effect, Awilix and plain factories alike. Does a component design state these without naming a framework, and does any of them earn graph structure beyond `uses`?", + "blocking": false, + "key": "providerSelection", + "decider": "owner", + "line": 24 + }, + { + "index": 1, + "question": "Is the original's one-runtime-truth rule, one composition mechanism per application, a Protocol rule or a corpus's own architecture rule?", + "blocking": false, + "key": "oneRuntimeOwner", + "decider": "owner", + "line": 26 + }, + { + "index": 2, + "question": "How is a component design selected, what value form does its `uses` entry take, and what rows does the realization comparison return for an unrealized design and for a layer, context or `uses` that differs?", + "blocking": false, + "key": "comparisonShape", + "decider": "owner", + "provisional": "a Spec is a component design when its Design `id` value, bare or in the code span that opens it, is a `component:` id, as the curation report reads it", + "line": 28 + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:protocol.curation-recipe", + "claim": "anchored", + "label": "asserts the curation report over external inferred imports", + "file": "test/curation-recipes.test.ts", + "line": 32 + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.open-question-fields", + "resolved": true, + "title": "An open question may name who decides it, its links, a provisional reading and a re-entry trigger", + "specKind": "rule", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/model/open-question-fields.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 1, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "A non-static question text, blocking flag or spread in a TypeScript open-question list also drops the whole list, blocking questions included, and stays a warning; is every dropped question list an error?", + "blocking": false, + "key": "droppedQuestionList", + "decider": "owner", + "provisional": "only a failure under the four fields is an error", + "line": 27 + } + ], + "decisions": [ + { + "id": "spec:decisions.adopt-the-nouns", + "title": "Delivery nouns remain familiar without workflow gates", + "statedReadiness": "ready", + "resolved": true + }, + { + "id": "spec:decisions.planning-truths-placement", + "title": "Planning truths live in ruled graph homes", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.authoring-grammar-recipes", + "claim": "anchored", + "label": "asserts the authoring grammar's realization in docs/agent-surface/recipes.md", + "file": "test/recipes.test.ts", + "line": 6307 + }, + { + "codeId": "impl:protocol.authoring-normalization", + "claim": "anchored", + "label": "normalizes rule markers and question fields after carrier reification", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.rule-keys", + "resolved": true, + "title": "A rule or flow entry may carry a key that addresses it", + "specKind": "rule", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/model/rule-keys.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 10, + "declarations": 6, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [ + { + "id": "spec:decisions.checked-mentions", + "title": "A Spec id written in prose is a checked mention", + "statedReadiness": "ready", + "resolved": true + }, + { + "id": "spec:decisions.shipped-projections-frozen", + "title": "The shipped projections stay frozen", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.authoring-grammar-recipes", + "claim": "anchored", + "label": "asserts the authoring grammar's realization in docs/agent-surface/recipes.md", + "file": "test/recipes.test.ts", + "line": 6307 + }, + { + "codeId": "impl:protocol.authoring-normalization", + "claim": "anchored", + "label": "normalizes rule markers and question fields after carrier reification", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + }, + { + "codeId": "impl:protocol.rule-exercises", + "claim": "anchored", + "label": "checks the keys named by declared verifies relations", + "file": "src/validate/validators.ts", + "line": 1484, + "role": "validator", + "component": { + "id": "component:protocol.validate", + "layer": "domain", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.relation-reasons", + "resolved": true, + "title": "An authored relation may carry its reason", + "specKind": "rule", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/model/relation-reasons.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 5, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [ + { + "id": "spec:decisions.prose-ownership", + "title": "Prose belongs to typed graph owners", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.authoring-grammar-recipes", + "claim": "anchored", + "label": "asserts the authoring grammar's realization in docs/agent-surface/recipes.md", + "file": "test/recipes.test.ts", + "line": 6307 + }, + { + "codeId": "impl:protocol.authoring-normalization", + "claim": "anchored", + "label": "normalizes rule markers and question fields after carrier reification", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + } + ], + "references": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.review-perspectives", + "resolved": true, + "title": "A Pack is read from a named perspective", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/review-perspectives.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 4, + "declarations": 1, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.perspective-filter-recipe", + "claim": "anchored", + "label": "realizes the perspective filter in the shipped recipe catalog", + "file": "test/iteration-recipes.test.ts", + "line": 751, + "role": "projection" + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.context-bundle", + "resolved": true, + "title": "A context bundle is composed per invocation within a stated budget", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/context-bundle.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 3, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "No cut reduces a pinned declaration, so a Pack whose pinned declarations alone exceed the budget fits no bundle; may a caller ask the bundle to reduce pinned declarations to their entry addresses?", + "blocking": false, + "key": "declarationCut", + "decider": "owner", + "provisional": "pinned declarations stay whole, and a caller narrows the scope to fewer members, such as one perspective's", + "line": 23 + } + ], + "decisions": [ + { + "id": "spec:decisions.agent-front-door", + "title": "The agent front door is one evaluation sink over the exported reader", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:protocol.context-bundle-recipe", + "claim": "anchored", + "label": "realizes the context bundle in the shipped recipe catalog", + "file": "test/iteration-recipes.test.ts", + "line": 763, + "role": "projection" + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.design-diff", + "resolved": true, + "title": "Designs are compared between a base commit and the working tree at entry grain", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/design-diff.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 8, + "declarations": 4, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.design-diff-recipe", + "claim": "anchored", + "label": "realizes the design diff in the shipped recipe catalog", + "file": "test/iteration-recipes.test.ts", + "line": 27, + "role": "projection" + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.intent-composition.proposal", + "resolved": true, + "title": "A change proposal is addressed, based and checked before it lands", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "ready", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "specs/consumers/intent-composition.proposal.sdp.md", + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 2, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Which catalog recipe renders a proposal as the Markdown memo a person reviews, so the Studio's composing panel and an agent hand off the same text?", + "blocking": false, + "key": "memoRecipe", + "decider": "owner", + "links": [ + "spec:consumers.spec-studio.intent-panel" + ], + "provisional": "a person reviews the proposal's JSON beside the proposal check's rows", + "line": 25 + } + ], + "decisions": [] + }, + "implementations": [ + { + "codeId": "impl:protocol.proposal-schema-and-recipe", + "claim": "anchored", + "label": "realizes the shipped proposal schema and proposal check in the recipe catalog", + "file": "test/iteration-recipes.test.ts", + "line": 1291, + "role": "contract" + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.reference-projection", + "resolved": true, + "title": "A reference projection serves a reader outside the repository", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "idea", + "derivedReadiness": "idea", + "deliveryFacts": [], + "file": "specs/consumers/reference-projection.sdp.md", + "statedNextRung": "scoped", + "statedNextRungFailures": [ + { + "clauseId": "kind-evidence-present", + "description": "The kind's natural evidence is present (per-kind evidence table)." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 1, + "openQuestions": [ + { + "index": 0, + "question": "Does a human reader outside the repository need a reference projection the four shipped roots do not serve? The taxonomy row is not that reader.", + "blocking": true, + "key": "namedOutsideReader", + "decider": "owner", + "trigger": "a named consumer outside the repository, with a way to measure that demand inside the arc that builds the projection", + "line": 17 + }, + { + "index": 1, + "question": "Would the reference read pinned declarations, contract-kind Specs or `api:` code anchors, and does any of them hold enough to document an interface?", + "blocking": false, + "key": "referenceSource", + "line": 20 + } + ], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.structural-mermaid", + "resolved": true, + "title": "Mermaid diagrams can be rooted at code units", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "idea", + "derivedReadiness": "idea", + "deliveryFacts": [], + "file": "specs/consumers/structural-mermaid.sdp.md", + "statedNextRung": "scoped", + "statedNextRungFailures": [ + { + "clauseId": "kind-evidence-present", + "description": "The kind's natural evidence is present (per-kind evidence table)." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 1, + "openQuestions": [ + { + "index": 0, + "question": "Does a reader need diagrams rooted at code units, which census tables and Spec-rooted one-hop Mermaid do not serve?", + "blocking": true, + "key": "namedStructuralReader", + "decider": "owner", + "trigger": "a named reader with that need, plus a decision that refines `spec:decisions.shipped-projections-frozen` for the affected Mermaid rendering or supersedes that ruling whole", + "line": 18 + }, + { + "index": 1, + "question": "Would a component diagram group its nodes by context and order them by layer, and would that make it the architecture view the Studio's architecture lens draws?", + "blocking": false, + "key": "layerAndContext", + "decider": "owner", + "links": [ + "spec:consumers.spec-studio.lenses#question.architectureVocabulary" + ], + "provisional": "the diagram folds into the Studio's architecture lens when that lens is designed, rather than becoming a Mermaid root", + "line": 21 + } + ], + "decisions": [ + { + "id": "spec:decisions.shipped-projections-frozen", + "title": "The shipped projections stay frozen", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.superseded-decision-signal", + "resolved": true, + "title": "A Spec shaped by a superseded decision warns", + "specKind": "rule", + "altitude": "story", + "statedReadiness": "scoped", + "derivedReadiness": "scoped", + "deliveryFacts": [], + "file": "specs/validation/superseded-decision-signal.sdp.md", + "statedNextRung": "defined", + "statedNextRungFailures": [ + { + "clauseId": "no-blocking-open-questions", + "description": "Spec has no blocking open question in intent.openQuestions." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 1, + "openQuestions": [ + { + "index": 0, + "question": "Does a corpus keep a replaced decision whose text a reader still needs beside its `supersedes` edge?", + "blocking": true, + "key": "keptRecord", + "decider": "owner", + "trigger": "a corpus keeps a replaced decision record whose text a reader still needs", + "line": 25 + }, + { + "index": 1, + "question": "Does the signal read only `decidedBy`, or also a `refines` or `dependsOn` that names a superseded decision?", + "blocking": false, + "key": "signalReach", + "decider": "owner", + "provisional": "the signal reads `decidedBy` only", + "line": 28 + }, + { + "index": 2, + "question": "When a superseding decision is itself superseded, does the warning name the decision at the end of the chain?", + "blocking": false, + "key": "supersededChain", + "decider": "owner", + "line": 31 + } + ], + "decisions": [ + { + "id": "spec:decisions.planning-truths-placement", + "title": "Planning truths live in ruled graph homes", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.architecture-constraints", + "resolved": true, + "title": "Declared architecture rules are checked against anchored structure", + "specKind": "rule", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/validation/architecture-constraints.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Do corpus rules need to require a component to state a layer or context, or check ports and routes that anchors do not carry, beyond the selected forbidden layer pairs and isolated contexts?", + "blocking": false, + "key": "ruleForms", + "decider": "owner", + "provisional": "the supplied walk checks forbidden layer pairs and isolated contexts; absent attributes are unclassified rather than breaches", + "line": 23 + }, + { + "index": 1, + "question": "Which pure callable walk serves both the catalog recipe and a loaded corpus rule, and what rule-data and result shapes do they share?", + "blocking": false, + "key": "walkInvocation", + "decider": "owner", + "provisional": "one pure walk takes `params.forbidden`, a list of `{ from, to }` layer pairs, and `params.isolated`, a list of context tokens whose components may have no `uses` edge into another context, either list possibly empty; it returns each forbidden edge as `{ from, to, fromComponent, toComponent, rule }` and lists unclassified edges apart; recipe 32's result stays as it is", + "line": 26 + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.corpus-rules", + "resolved": true, + "title": "Corpus-authored rules run on the one validation path", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/validation/corpus-rules.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Does an operator-named rules file hold one async body returning findings or a module exporting named rule functions, how are parameters supplied, in what order do several files run, and does `sdp validate --watch` rerun them?", + "blocking": false, + "key": "rulesRepresentation", + "decider": "owner", + "provisional": "one async body per file, given `g`, `graph`, `report` and `params`, with `params` from `sdp validate --params` and `{}` without it; files run in the order named, each to completion; a rule id is unique within its file; `sdp validate --watch` runs the same named rules each cycle", + "line": 27 + }, + { + "index": 1, + "question": "What shape does a corpus finding take beside the one diagnostic currency, where does the report carry it, and does it reach `report`, the reader's `findings()` or the Design Review?", + "blocking": false, + "key": "findingCurrency", + "decider": "owner", + "provisional": "the shared finding contract has a Protocol branch carrying `validatorId`, `family`, severity and message, and a corpus branch carrying the corpus, the rule id as `validatorId`, `subjectId`, severity and message and no family; `relatedId`, `path`, `file` and `line` stay optional on both branches; corpus findings are collected as `corpusFindings` in the same report and rendered under a corpus-labelled section with the existing location, severity, identifier and message composition; Protocol findings, the summary line and the Design Review stay unchanged, as the frozen-projections decision requires", + "line": 30 + }, + { + "index": 2, + "question": "Does the runner enforce that a rule reads only the graph and changes nothing, or are both obligations on the rule's author?", + "blocking": false, + "key": "ruleIsolation", + "decider": "owner", + "provisional": "changing nothing is a guarantee, with the Protocol's validators run first and each rule given a frozen or cloned graph; reading only the graph is the author's obligation, run with the trust `sdp q` asks and no sandbox", + "line": 33 + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:observation.run-evidence", + "resolved": true, + "title": "CI run records are joined to verifiers in a view", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "specs/observation/run-evidence.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "How does a view say a run is stale: by its commit against the commit being read, or by whether the bound files changed between the two?", + "blocking": false, + "key": "freshness", + "decider": "owner", + "provisional": "Display the run and source commit identifiers when they differ, as the differing-commit rule requires; whether freshness also follows changes to bound files remains open.", + "line": 25 + }, + { + "index": 1, + "question": "Which parameter and result shapes, CTRF revision and run-commit field, corpus-commit input, input label, and file-path normalization rule define the run-evidence recipe, and how does it report a record that joins no test anchor, or missing commit or path information?", + "blocking": false, + "key": "recipeContract", + "decider": "owner", + "provisional": "Records stay external, join only through the named test file, and confer no delivery fact; the ruled contract lands as keyed Design entries before the view is built.", + "line": 28 + }, + { + "index": 2, + "question": "Should a later Studio phase admit run records beside the graph, and which input and reproducibility contracts would that require?", + "blocking": false, + "key": "studioRuns", + "decider": "owner", + "provisional": "The Studio's first build stays graph-only; a later phase may admit named run records under its own input and reproducibility contract.", + "line": 31 + } + ], + "decisions": [ + { + "id": "spec:decisions.binding-not-liveness", + "title": "Bindings state existence, not liveness", + "statedReadiness": "ready", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + } + ], + "verifierGaps": [ + { + "id": "spec:consumers.agent-surface.example-space-coverage", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:model.planned-architecture", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:consumers.reference-projection", + "statedReadiness": "idea", + "priority": false + }, + { + "id": "spec:consumers.structural-mermaid", + "statedReadiness": "idea", + "priority": false + }, + { + "id": "spec:validation.superseded-decision-signal", + "statedReadiness": "scoped", + "priority": false + }, + { + "id": "spec:validation.architecture-constraints", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:validation.corpus-rules", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:observation.run-evidence", + "statedReadiness": "defined", + "priority": false + } + ], + "boundary": { + "restsOn": [ + { + "id": "spec:carrier.slot-notation", + "title": "Slot notation declares, binds, and refuses to guess", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage" + ] + } + ] + }, + { + "id": "spec:consumers.adopter-on-ramp", + "title": "An adopter learns graph homes, carrier grammar and shipped paths from the on-ramps", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.engine-provenance" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface", + "title": "Agents script a visible typed graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:consumers.context-bundle" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.address-and-cycle-recipes", + "title": "Address resolution and dependency cycles stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.register-recipes", + "title": "Register questions stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.agent-surface.design-recipes" + ] + } + ] + }, + { + "id": "spec:consumers.authoring-on-ramp", + "title": "Authors move one Spec from intent to reviewed evidence", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:consumers.binding-language-views", + "title": "Views speak binding language, never the internal fact name", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.derived-readiness-banner", + "title": "Derived readiness renders beside the stated rung and warns in one direction", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.design-review", + "title": "Design Review renders graph context without becoming a gate", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.edit-model", + "title": "Views compose scoped intent instead of patching canonical source", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.graph-first-planning", + "title": "The next body of work is planned from the graph, not from a prose index", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:consumers.intent-composition", + "title": "Composed intent hands off on one proposal shape", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.projections-model", + "title": "Projections fan out from one graph without becoming truth stores", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.review-perspectives", + "spec:consumers.design-diff", + "spec:consumers.reference-projection", + "spec:consumers.structural-mermaid" + ] + } + ] + }, + { + "id": "spec:consumers.reader", + "title": "The reader bridges agent entry points to composable graph context", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:decisions.adopt-the-nouns", + "title": "Delivery nouns remain familiar without workflow gates", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:decisions.agent-front-door", + "title": "The agent front door is one evaluation sink over the exported reader", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:consumers.context-bundle" + ] + } + ] + }, + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:consumers.pack-design", + "spec:model.planned-architecture", + "spec:validation.architecture-constraints", + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:decisions.binding-not-liveness", + "title": "Bindings state existence, not liveness", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:observation.run-evidence" + ] + } + ] + }, + { + "id": "spec:decisions.checked-mentions", + "title": "A Spec id written in prose is a checked mention", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:decisions.one-validation-path", + "title": "Validation follows one graph", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:decisions.planning-truths-placement", + "title": "Planning truths live in ruled graph homes", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.open-question-fields", + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:decisions.prose-ownership", + "title": "Prose belongs to typed graph owners", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:decisions.shipped-projections-frozen", + "title": "The shipped projections stay frozen", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:decisions.pack-design-page" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:model.rule-keys", + "spec:consumers.structural-mermaid" + ] + } + ] + }, + { + "id": "spec:extraction.contract-declarations", + "title": "Pinned declarations give an adopter a derived module to typecheck", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:extraction.delivery-facts", + "title": "Resolving bindings confer direct delivery facts", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:observation.run-evidence" + ] + } + ] + }, + { + "id": "spec:extraction.derive-graph", + "title": "Carrier reification derives the one graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:extraction.entry-locations" + ] + } + ] + }, + { + "id": "spec:extraction.determinism", + "title": "Committed source derives byte-identical output", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:extraction.entry-locations", + "spec:consumers.engine-provenance", + "spec:consumers.design-diff" + ] + } + ] + }, + { + "id": "spec:extraction.excludes", + "title": "Extraction exclusions are strict consumer input", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:extraction.executable-contracts", + "title": "The build derives step and space contracts from graph examples", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage" + ] + } + ] + }, + { + "id": "spec:extraction.pack-member-order", + "title": "A Pack node keeps its members in authored order", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:model.anchors", + "title": "Source anchors bind code without carrying intent", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.planned-architecture", + "spec:validation.architecture-constraints" + ] + } + ] + }, + { + "id": "spec:model.core-model", + "title": "The Protocol models delivery with one enrichable Spec", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.design-maturity" + ] + } + ] + }, + { + "id": "spec:model.open-question-keys", + "title": "An open question may carry a key that addresses it", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.open-question-fields" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:extraction.entry-locations", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:model.relations", + "title": "Specs declare typed directed relations", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.relation-reasons" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:model.spec-sections", + "title": "Spec sections carry typed detail and direct verifier semantics", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:validation.diagnostic-rendering", + "title": "One diagnostic currency, its location composed from structured fields", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:validation.next-rung-floor", + "title": "The floor names what blocks the floor's next rung", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:validation.two-check-families", + "title": "Validation separates well-formedness from non-pretending", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:validation.warn-level-signals", + "title": "Missing connective evidence warns without failing", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + } + ], + "restedOnBy": [ + { + "id": "spec:carrier.envelope-contract", + "title": "The Markdown envelope is explicit and bounded", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:carrier.gherkin-authoring", + "title": "Gherkin authoring enters the one graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:carrier.markdown-body-grammar", + "title": "The Markdown body accepts one closed form per section", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:carrier.prose-ownership-rule", + "title": "Every prose value has one typed owner", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.address-and-cycle-recipes", + "title": "Address resolution and dependency cycles stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.answer-shapes", + "title": "Recipe answers fit an agent's context and explain their misses", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.design-recipes" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.register-recipes", + "title": "Register questions stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.authoring-on-ramp", + "title": "Authors move one Spec from intent to reviewed evidence", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.delivery-session-on-ramp", + "title": "Delivery sessions route work from current graph state", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.review-perspectives", + "spec:consumers.context-bundle", + "spec:consumers.design-diff", + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.graph-first-planning", + "title": "The next body of work is planned from the graph, not from a prose index", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:consumers.impact-graph.curation-assist", + "title": "File-level imports show where curation is missing", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.planned-architecture" + ] + } + ] + }, + { + "id": "spec:consumers.obsidian-vault.bases", + "title": "A Pack's members and the standing queries are tables over the reader's values", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:consumers.obsidian-vault.iteration", + "title": "A design pass is followed and answered from the vault", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.data", + "title": "The Studio renders from the reader's values and embeds only what its script reads", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:extraction.entry-locations" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.intent-panel", + "title": "A composing panel on every page gathers scoped intent for an agent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.lenses", + "title": "Four lenses read the graph as Packs, architecture, tests and evidence", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.pack-page", + "title": "A Pack page shows the Pack's design as one card per member", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.sharing", + "title": "A Studio build is stamped and shared from a static host", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.engine-provenance" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.spec-page", + "title": "A Spec page shows one Spec in collapsible panels, in a fixed order", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:extraction.entry-locations", + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.spec-page.core", + "title": "A Spec page's core shows the whole Spec, with an anchor for every entry", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:extraction.entry-locations", + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.verification-panels", + "title": "The panels for examples, targets, verifiers and evidence", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:model.term-addresses", + "title": "A Model term may carry a key that addresses it", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:validation.prose-mentions", + "title": "Every Spec id written in prose resolves", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + } + ], + "verifiedBy": [], + "supersededBy": [] + }, + "findings": [] +} diff --git a/explorations/studio-prototype/data/design-management-v1.recipe-29.json b/explorations/studio-prototype/data/design-management-v1.recipe-29.json new file mode 100644 index 00000000..88322b8a --- /dev/null +++ b/explorations/studio-prototype/data/design-management-v1.recipe-29.json @@ -0,0 +1,2196 @@ +{ + "found": true, + "id": "pack:design-management-v1", + "title": "Design management", + "totals": { + "members": 24, + "unresolved": 0, + "heldBelowNextRung": 3, + "waitingForAuthor": 5, + "entries": 48, + "declarations": 17, + "openQuestions": 24, + "blockingQuestions": 3, + "withImplementations": 15, + "withReferences": 4, + "restsOn": 39, + "restedOnBy": 23, + "verifiedBy": 0, + "supersededBy": 0 + }, + "members": [ + { + "id": "spec:decisions.pack-design-page", + "resolved": true, + "title": "The Design Review's Pack page presents the Pack's design", + "specKind": "decision", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:protocol.pack-design-page", + "file": "src/projections/design-review-pages.ts", + "line": 128, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.pack-design", + "resolved": true, + "title": "The Pack context carries the Pack's design", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.pack-design", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.design-review.pack-design-page", + "resolved": true, + "title": "A Pack page shows where the Pack's design stands", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [ + { + "id": "spec:decisions.pack-design-page", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.pack-design-page", + "file": "src/projections/design-review-pages.ts", + "line": 128, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "referencedBy": [ + { + "id": "impl:protocol.pack-design-sections", + "file": "src/projections/design-review-pack-sections.ts", + "line": 19, + "role": "renderer", + "component": { + "id": "component:protocol.projections", + "layer": "edge", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:extraction.entry-locations", + "resolved": true, + "title": "The graph records where each addressable entry is written", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.entry-location-recipes", + "file": "test/recipes.test.ts", + "line": 5689, + "role": null, + "component": null + }, + { + "id": "impl:protocol.entry-locations", + "file": "src/extract/derive.ts", + "line": 78, + "role": "extractor", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + }, + { + "id": "impl:protocol.reader", + "file": "src/reader/reader.ts", + "line": 725, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "referencedBy": [ + { + "id": "impl:protocol.pack-design", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.recipe-parameters", + "resolved": true, + "title": "Recipes take parameters as data and ship as runnable files", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [ + { + "id": "spec:decisions.agent-front-door", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.build-artifacts", + "file": "src/cli/build-artifacts.ts", + "line": 177, + "role": "publisher", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "id": "impl:protocol.query-params", + "file": "src/cli/q-command.ts", + "line": 179, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "id": "impl:protocol.recipe-parameters-catalog", + "file": "test/recipes.test.ts", + "line": 4241, + "role": null, + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 3, + "enabled": 3 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.design-recipes", + "resolved": true, + "title": "Design work reads its answers from catalog recipes", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.design-recipes", + "file": "test/recipes.test.ts", + "line": 4528, + "role": null, + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.agent-surface.example-space-coverage", + "resolved": true, + "title": "Example spaces report which values their examples cover", + "specKind": "behavior", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 2, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "combinationCoverage", + "blocking": false, + "address": "spec:consumers.agent-surface.example-space-coverage#question.combinationCoverage", + "line": 22, + "question": "Is coverage per slot value enough, or does a design pass need the combinations of two or more slots, as the original heatmap drew them?" + }, + { + "key": "coverageShape", + "blocking": false, + "address": "spec:consumers.agent-surface.example-space-coverage#question.coverageShape", + "line": 26, + "question": "What reader field names and decoded slot and bound-point shapes, recipe parameters and result rows, eligible-example relation policy, and treatment of an unbound slot form the coverage contract?" + } + ], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.engine-provenance", + "resolved": true, + "title": "The CLI names the engine that produced a result", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.build-artifacts", + "file": "src/cli/build-artifacts.ts", + "line": 177, + "role": "publisher", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + }, + { + "id": "impl:protocol.engine-provenance-cli", + "file": "src/cli/sdp.ts", + "line": 193, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.validate-watch", + "resolved": true, + "title": "Validation re-runs while an author edits carriers", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.validate-watch", + "file": "src/cli/validate-watch.ts", + "line": 474, + "role": "service", + "component": { + "id": "component:protocol.cli", + "layer": "edge", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.design-maturity", + "resolved": true, + "title": "What a Spec's design carries is derived, beside its readiness", + "specKind": "model", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "designColumnsSufficiency", + "blocking": false, + "address": "spec:model.design-maturity#question.designColumnsSufficiency", + "line": 20, + "question": "If the first adopter's design passes show that the design columns cannot distinguish settled design from intent alone, should this representation be revisited?" + } + ], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.pack-design", + "file": "src/reader/reader.ts", + "line": 537, + "role": "reader", + "component": { + "id": "component:protocol.reader", + "layer": "application", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.planned-architecture", + "resolved": true, + "title": "Components are designed ahead of their code", + "specKind": "model", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "providerSelection", + "blocking": false, + "address": "spec:model.planned-architecture#question.providerSelection", + "line": 24, + "question": "The original kept apart the interface a unit requires, the providers that supply it, the provider a composition root selects and the one a test substitutes, for Fastify, Effect, Awilix and plain factories alike. Does a component design state these without naming a framework, and does any of them earn graph structure beyond `uses`?" + }, + { + "key": "oneRuntimeOwner", + "blocking": false, + "address": "spec:model.planned-architecture#question.oneRuntimeOwner", + "line": 26, + "question": "Is the original's one-runtime-truth rule, one composition mechanism per application, a Protocol rule or a corpus's own architecture rule?" + }, + { + "key": "comparisonShape", + "blocking": false, + "address": "spec:model.planned-architecture#question.comparisonShape", + "line": 28, + "question": "How is a component design selected, what value form does its `uses` entry take, and what rows does the realization comparison return for an unrealized design and for a layer, context or `uses` that differs?" + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:protocol.curation-recipe", + "file": "test/curation-recipes.test.ts", + "line": 32, + "role": null, + "component": null + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.open-question-fields", + "resolved": true, + "title": "An open question may name who decides it, its links, a provisional reading and a re-entry trigger", + "specKind": "rule", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 1, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "droppedQuestionList", + "blocking": false, + "address": "spec:model.open-question-fields#question.droppedQuestionList", + "line": 27, + "question": "A non-static question text, blocking flag or spread in a TypeScript open-question list also drops the whole list, blocking questions included, and stays a warning; is every dropped question list an error?" + } + ], + "decisions": [ + { + "id": "spec:decisions.adopt-the-nouns", + "statedReadiness": "ready", + "resolved": true + }, + { + "id": "spec:decisions.planning-truths-placement", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.authoring-grammar-recipes", + "file": "test/recipes.test.ts", + "line": 6307, + "role": null, + "component": null + }, + { + "id": "impl:protocol.authoring-normalization", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.rule-keys", + "resolved": true, + "title": "A rule or flow entry may carry a key that addresses it", + "specKind": "rule", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 10, + "declarations": 6, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [ + { + "id": "spec:decisions.checked-mentions", + "statedReadiness": "ready", + "resolved": true + }, + { + "id": "spec:decisions.shipped-projections-frozen", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.authoring-grammar-recipes", + "file": "test/recipes.test.ts", + "line": 6307, + "role": null, + "component": null + }, + { + "id": "impl:protocol.authoring-normalization", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + }, + { + "id": "impl:protocol.rule-exercises", + "file": "src/validate/validators.ts", + "line": 1484, + "role": "validator", + "component": { + "id": "component:protocol.validate", + "layer": "domain", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:model.relation-reasons", + "resolved": true, + "title": "An authored relation may carry its reason", + "specKind": "rule", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 5, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [ + { + "id": "spec:decisions.prose-ownership", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.authoring-grammar-recipes", + "file": "test/recipes.test.ts", + "line": 6307, + "role": null, + "component": null + }, + { + "id": "impl:protocol.authoring-normalization", + "file": "src/extract/authoring-normalization.ts", + "line": 7, + "role": "codec", + "component": { + "id": "component:protocol.extract", + "layer": "application", + "context": "protocol" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.review-perspectives", + "resolved": true, + "title": "A Pack is read from a named perspective", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 4, + "declarations": 1, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.perspective-filter-recipe", + "file": "test/iteration-recipes.test.ts", + "line": 751, + "role": "projection", + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.context-bundle", + "resolved": true, + "title": "A context bundle is composed per invocation within a stated budget", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 3, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "declarationCut", + "blocking": false, + "address": "spec:consumers.context-bundle#question.declarationCut", + "line": 23, + "question": "No cut reduces a pinned declaration, so a Pack whose pinned declarations alone exceed the budget fits no bundle; may a caller ask the bundle to reduce pinned declarations to their entry addresses?" + } + ], + "decisions": [ + { + "id": "spec:decisions.agent-front-door", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:protocol.context-bundle-recipe", + "file": "test/iteration-recipes.test.ts", + "line": 763, + "role": "projection", + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.design-diff", + "resolved": true, + "title": "Designs are compared between a base commit and the working tree at entry grain", + "specKind": "behavior", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 8, + "declarations": 4, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.design-diff-recipe", + "file": "test/iteration-recipes.test.ts", + "line": 27, + "role": "projection", + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.intent-composition.proposal", + "resolved": true, + "title": "A change proposal is addressed, based and checked before it lands", + "specKind": "contract", + "statedReadiness": "ready", + "floorReached": "ready", + "statedNextRung": null, + "statedNextRungFailures": [], + "design": { + "entries": 7, + "declarations": 2, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "memoRecipe", + "blocking": false, + "address": "spec:consumers.intent-composition.proposal#question.memoRecipe", + "line": 25, + "question": "Which catalog recipe renders a proposal as the Markdown memo a person reviews, so the Studio's composing panel and an agent hand off the same text?" + } + ], + "decisions": [], + "implementedBy": [ + { + "id": "impl:protocol.proposal-schema-and-recipe", + "file": "test/iteration-recipes.test.ts", + "line": 1291, + "role": "contract", + "component": null + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.reference-projection", + "resolved": true, + "title": "A reference projection serves a reader outside the repository", + "specKind": "behavior", + "statedReadiness": "idea", + "floorReached": "idea", + "statedNextRung": "scoped", + "statedNextRungFailures": [ + { + "clauseId": "kind-evidence-present", + "description": "The kind's natural evidence is present (per-kind evidence table)." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 2, + "blockingQuestions": 1 + }, + "questions": [ + { + "key": "namedOutsideReader", + "blocking": true, + "address": "spec:consumers.reference-projection#question.namedOutsideReader", + "line": 17, + "question": "Does a human reader outside the repository need a reference projection the four shipped roots do not serve? The taxonomy row is not that reader." + }, + { + "key": "referenceSource", + "blocking": false, + "address": "spec:consumers.reference-projection#question.referenceSource", + "line": 20, + "question": "Would the reference read pinned declarations, contract-kind Specs or `api:` code anchors, and does any of them hold enough to document an interface?" + } + ], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:consumers.structural-mermaid", + "resolved": true, + "title": "Mermaid diagrams can be rooted at code units", + "specKind": "behavior", + "statedReadiness": "idea", + "floorReached": "idea", + "statedNextRung": "scoped", + "statedNextRungFailures": [ + { + "clauseId": "kind-evidence-present", + "description": "The kind's natural evidence is present (per-kind evidence table)." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 2, + "blockingQuestions": 1 + }, + "questions": [ + { + "key": "namedStructuralReader", + "blocking": true, + "address": "spec:consumers.structural-mermaid#question.namedStructuralReader", + "line": 18, + "question": "Does a reader need diagrams rooted at code units, which census tables and Spec-rooted one-hop Mermaid do not serve?" + }, + { + "key": "layerAndContext", + "blocking": false, + "address": "spec:consumers.structural-mermaid#question.layerAndContext", + "line": 21, + "question": "Would a component diagram group its nodes by context and order them by layer, and would that make it the architecture view the Studio's architecture lens draws?" + } + ], + "decisions": [ + { + "id": "spec:decisions.shipped-projections-frozen", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.superseded-decision-signal", + "resolved": true, + "title": "A Spec shaped by a superseded decision warns", + "specKind": "rule", + "statedReadiness": "scoped", + "floorReached": "scoped", + "statedNextRung": "defined", + "statedNextRungFailures": [ + { + "clauseId": "no-blocking-open-questions", + "description": "Spec has no blocking open question in intent.openQuestions." + } + ], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 3, + "blockingQuestions": 1 + }, + "questions": [ + { + "key": "keptRecord", + "blocking": true, + "address": "spec:validation.superseded-decision-signal#question.keptRecord", + "line": 25, + "question": "Does a corpus keep a replaced decision whose text a reader still needs beside its `supersedes` edge?" + }, + { + "key": "signalReach", + "blocking": false, + "address": "spec:validation.superseded-decision-signal#question.signalReach", + "line": 28, + "question": "Does the signal read only `decidedBy`, or also a `refines` or `dependsOn` that names a superseded decision?" + }, + { + "key": "supersededChain", + "blocking": false, + "address": "spec:validation.superseded-decision-signal#question.supersededChain", + "line": 31, + "question": "When a superseding decision is itself superseded, does the warning name the decision at the end of the chain?" + } + ], + "decisions": [ + { + "id": "spec:decisions.planning-truths-placement", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.architecture-constraints", + "resolved": true, + "title": "Declared architecture rules are checked against anchored structure", + "specKind": "rule", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 2, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "ruleForms", + "blocking": false, + "address": "spec:validation.architecture-constraints#question.ruleForms", + "line": 23, + "question": "Do corpus rules need to require a component to state a layer or context, or check ports and routes that anchors do not carry, beyond the selected forbidden layer pairs and isolated contexts?" + }, + { + "key": "walkInvocation", + "blocking": false, + "address": "spec:validation.architecture-constraints#question.walkInvocation", + "line": 26, + "question": "Which pure callable walk serves both the catalog recipe and a loaded corpus rule, and what rule-data and result shapes do they share?" + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:validation.corpus-rules", + "resolved": true, + "title": "Corpus-authored rules run on the one validation path", + "specKind": "behavior", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "rulesRepresentation", + "blocking": false, + "address": "spec:validation.corpus-rules#question.rulesRepresentation", + "line": 27, + "question": "Does an operator-named rules file hold one async body returning findings or a module exporting named rule functions, how are parameters supplied, in what order do several files run, and does `sdp validate --watch` rerun them?" + }, + { + "key": "findingCurrency", + "blocking": false, + "address": "spec:validation.corpus-rules#question.findingCurrency", + "line": 30, + "question": "What shape does a corpus finding take beside the one diagnostic currency, where does the report carry it, and does it reach `report`, the reader's `findings()` or the Design Review?" + }, + { + "key": "ruleIsolation", + "blocking": false, + "address": "spec:validation.corpus-rules#question.ruleIsolation", + "line": 33, + "question": "Does the runner enforce that a rule reads only the graph and changes nothing, or are both obligations on the rule's author?" + } + ], + "decisions": [ + { + "id": "spec:decisions.architectural-annotation", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:observation.run-evidence", + "resolved": true, + "title": "CI run records are joined to verifiers in a view", + "specKind": "behavior", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "freshness", + "blocking": false, + "address": "spec:observation.run-evidence#question.freshness", + "line": 25, + "question": "How does a view say a run is stale: by its commit against the commit being read, or by whether the bound files changed between the two?" + }, + { + "key": "recipeContract", + "blocking": false, + "address": "spec:observation.run-evidence#question.recipeContract", + "line": 28, + "question": "Which parameter and result shapes, CTRF revision and run-commit field, corpus-commit input, input label, and file-path normalization rule define the run-evidence recipe, and how does it report a record that joins no test anchor, or missing commit or path information?" + }, + { + "key": "studioRuns", + "blocking": false, + "address": "spec:observation.run-evidence#question.studioRuns", + "line": 31, + "question": "Should a later Studio phase admit run records beside the graph, and which input and reproducibility contracts would that require?" + } + ], + "decisions": [ + { + "id": "spec:decisions.binding-not-liveness", + "statedReadiness": "ready", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + } + ], + "boundary": { + "restsOn": [ + { + "id": "spec:carrier.slot-notation", + "title": "Slot notation declares, binds, and refuses to guess", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage" + ] + } + ] + }, + { + "id": "spec:consumers.adopter-on-ramp", + "title": "An adopter learns graph homes, carrier grammar and shipped paths from the on-ramps", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.engine-provenance" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface", + "title": "Agents script a visible typed graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:consumers.context-bundle" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.address-and-cycle-recipes", + "title": "Address resolution and dependency cycles stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.register-recipes", + "title": "Register questions stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.agent-surface.design-recipes" + ] + } + ] + }, + { + "id": "spec:consumers.authoring-on-ramp", + "title": "Authors move one Spec from intent to reviewed evidence", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:consumers.binding-language-views", + "title": "Views speak binding language, never the internal fact name", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.derived-readiness-banner", + "title": "Derived readiness renders beside the stated rung and warns in one direction", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.design-review", + "title": "Design Review renders graph context without becoming a gate", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.design-review.pack-design-page" + ] + } + ] + }, + { + "id": "spec:consumers.edit-model", + "title": "Views compose scoped intent instead of patching canonical source", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.graph-first-planning", + "title": "The next body of work is planned from the graph, not from a prose index", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:consumers.intent-composition", + "title": "Composed intent hands off on one proposal shape", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.projections-model", + "title": "Projections fan out from one graph without becoming truth stores", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.review-perspectives", + "spec:consumers.design-diff", + "spec:consumers.reference-projection", + "spec:consumers.structural-mermaid" + ] + } + ] + }, + { + "id": "spec:consumers.reader", + "title": "The reader bridges agent entry points to composable graph context", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:decisions.adopt-the-nouns", + "title": "Delivery nouns remain familiar without workflow gates", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:decisions.agent-front-door", + "title": "The agent front door is one evaluation sink over the exported reader", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:consumers.context-bundle" + ] + } + ] + }, + { + "id": "spec:decisions.architectural-annotation", + "title": "Architectural significance is annotated where it is realized", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:consumers.pack-design", + "spec:model.planned-architecture", + "spec:validation.architecture-constraints", + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:decisions.binding-not-liveness", + "title": "Bindings state existence, not liveness", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:observation.run-evidence" + ] + } + ] + }, + { + "id": "spec:decisions.checked-mentions", + "title": "A Spec id written in prose is a checked mention", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:decisions.one-validation-path", + "title": "Validation follows one graph", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:decisions.planning-truths-placement", + "title": "Planning truths live in ruled graph homes", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.open-question-fields", + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:decisions.prose-ownership", + "title": "Prose belongs to typed graph owners", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:decisions.shipped-projections-frozen", + "title": "The shipped projections stay frozen", + "statedReadiness": "ready", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:decisions.pack-design-page" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:model.rule-keys", + "spec:consumers.structural-mermaid" + ] + } + ] + }, + { + "id": "spec:extraction.contract-declarations", + "title": "Pinned declarations give an adopter a derived module to typecheck", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:extraction.delivery-facts", + "title": "Resolving bindings confer direct delivery facts", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:observation.run-evidence" + ] + } + ] + }, + { + "id": "spec:extraction.derive-graph", + "title": "Carrier reification derives the one graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:extraction.entry-locations" + ] + } + ] + }, + { + "id": "spec:extraction.determinism", + "title": "Committed source derives byte-identical output", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:extraction.entry-locations", + "spec:consumers.engine-provenance", + "spec:consumers.design-diff" + ] + } + ] + }, + { + "id": "spec:extraction.excludes", + "title": "Extraction exclusions are strict consumer input", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.validate-watch" + ] + } + ] + }, + { + "id": "spec:extraction.executable-contracts", + "title": "The build derives step and space contracts from graph examples", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage" + ] + } + ] + }, + { + "id": "spec:extraction.pack-member-order", + "title": "A Pack node keeps its members in authored order", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:model.anchors", + "title": "Source anchors bind code without carrying intent", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.planned-architecture", + "spec:validation.architecture-constraints" + ] + } + ] + }, + { + "id": "spec:model.core-model", + "title": "The Protocol models delivery with one enrichable Spec", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.design-maturity" + ] + } + ] + }, + { + "id": "spec:model.open-question-keys", + "title": "An open question may carry a key that addresses it", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.open-question-fields" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:extraction.entry-locations", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:model.relations", + "title": "Specs declare typed directed relations", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.relation-reasons" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + }, + { + "id": "spec:model.spec-sections", + "title": "Spec sections carry typed detail and direct verifier semantics", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:validation.diagnostic-rendering", + "title": "One diagnostic currency, its location composed from structured fields", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:validation.next-rung-floor", + "title": "The floor names what blocks the floor's next rung", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:validation.two-check-families", + "title": "Validation separates well-formedness from non-pretending", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:validation.corpus-rules" + ] + } + ] + }, + { + "id": "spec:validation.warn-level-signals", + "title": "Missing connective evidence warns without failing", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:validation.superseded-decision-signal" + ] + } + ] + } + ], + "restedOnBy": [ + { + "id": "spec:carrier.envelope-contract", + "title": "The Markdown envelope is explicit and bounded", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:carrier.gherkin-authoring", + "title": "Gherkin authoring enters the one graph", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:carrier.markdown-body-grammar", + "title": "The Markdown body accepts one closed form per section", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:carrier.prose-ownership-rule", + "title": "Every prose value has one typed owner", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.address-and-cycle-recipes", + "title": "Address resolution and dependency cycles stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.recipe-parameters", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.answer-shapes", + "title": "Recipe answers fit an agent's context and explain their misses", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.design-recipes" + ] + } + ] + }, + { + "id": "spec:consumers.agent-surface.register-recipes", + "title": "Register questions stay executable graph recipes", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.authoring-on-ramp", + "title": "Authors move one Spec from intent to reviewed evidence", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.delivery-session-on-ramp", + "title": "Delivery sessions route work from current graph state", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.review-perspectives", + "spec:consumers.context-bundle", + "spec:consumers.design-diff", + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.graph-first-planning", + "title": "The next body of work is planned from the graph, not from a prose index", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:consumers.impact-graph.curation-assist", + "title": "File-level imports show where curation is missing", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.planned-architecture" + ] + } + ] + }, + { + "id": "spec:consumers.obsidian-vault.bases", + "title": "A Pack's members and the standing queries are tables over the reader's values", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design" + ] + } + ] + }, + { + "id": "spec:consumers.obsidian-vault.iteration", + "title": "A design pass is followed and answered from the vault", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.data", + "title": "The Studio renders from the reader's values and embeds only what its script reads", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:extraction.entry-locations" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.intent-panel", + "title": "A composing panel on every page gathers scoped intent for an agent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.intent-composition.proposal" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.lenses", + "title": "Four lenses read the graph as Packs, architecture, tests and evidence", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.pack-page", + "title": "A Pack page shows the Pack's design as one card per member", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:model.open-question-fields" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.sharing", + "title": "A Studio build is stamped and shared from a static host", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.engine-provenance" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.spec-page", + "title": "A Spec page shows one Spec in collapsible panels, in a fixed order", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.pack-design", + "spec:extraction.entry-locations", + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.spec-page.core", + "title": "A Spec page's core shows the whole Spec, with an anchor for every entry", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:extraction.entry-locations", + "spec:model.open-question-fields", + "spec:model.rule-keys", + "spec:model.relation-reasons" + ] + } + ] + }, + { + "id": "spec:consumers.spec-studio.verification-panels", + "title": "The panels for examples, targets, verifiers and evidence", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:consumers.agent-surface.example-space-coverage", + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:model.term-addresses", + "title": "A Model term may carry a key that addresses it", + "statedReadiness": "idea", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.rule-keys" + ] + } + ] + }, + { + "id": "spec:validation.prose-mentions", + "title": "Every Spec id written in prose resolves", + "statedReadiness": "ready", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:model.open-question-fields" + ] + } + ] + } + ], + "verifiedBy": [], + "supersededBy": [] + } +} diff --git a/explorations/studio-prototype/data/history-view.js b/explorations/studio-prototype/data/history-view.js new file mode 100644 index 00000000..e760500c --- /dev/null +++ b/explorations/studio-prototype/data/history-view.js @@ -0,0 +1,6824 @@ +window.SDP_PROTOTYPE = { +"design": { + "found": true, + "id": "pack:history-view", + "title": "History view", + "totals": { + "members": 19, + "unresolved": 0, + "heldBelowNextRung": 0, + "waitingForAuthor": 19, + "entries": 283, + "declarations": 169, + "openQuestions": 53, + "blockingQuestions": 0, + "withImplementations": 2, + "withReferences": 10, + "restsOn": 56, + "restedOnBy": 39, + "verifiedBy": 21, + "supersededBy": 0 + }, + "members": [ + { + "id": "spec:application.history-projection", + "resolved": true, + "title": "History projection", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 10, + "declarations": 8, + "openQuestions": 4, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "historyFoldOrder", + "blocking": false, + "address": "spec:application.history-projection#question.historyFoldOrder", + "line": 37, + "question": "Extension E-9, the history fold: D2 orders events only within a stream, and the two paths feed one history row in different orders, the live path command by command and the backfill stream by stream; the option taken here is a fold that gives one row whatever the order across streams and however its events are split between calls, a row no fold deletes, which keeps its subject's ended state, and baselines dropped before `keyOf`, so a view whose meaning needs the order across streams waits for the ordered consumer of D18 (E-9, D2, D9, D18, E-25)" + }, + { + "key": "historyProjectionCost", + "blocking": false, + "address": "spec:application.history-projection#question.historyProjectionCost", + "line": 38, + "question": "The cost of the binding on the live path, one row read and one write of up to `rowBudgetBytes` per command and the measure of the row, is taken by the first experiment's latency cell with the order allocation history bound, for `PlaceOrder` and for `CancelOrder` of an order of the maximum lines, before the view is declared essential anywhere (E-9, E-47, F13)" + }, + { + "key": "historyFoldPureTest", + "blocking": false, + "address": "spec:application.history-projection#question.historyFoldPureTest", + "line": 39, + "question": "Extension E-9, the check of the history fold: the order independence the Contract requires is checked for the order allocation history alone; the option taken here is that every history projection of an application carries a pure test that folds a sample of one key's events in every interleaving of its streams and every split into calls, and a library helper that runs that test is added with a second history view (E-9, D2, D18)" + }, + { + "key": "historySubjectErasure", + "blocking": false, + "address": "spec:application.history-projection#question.historySubjectErasure", + "line": 40, + "question": "A history row keeps a deleted subject's history and every rebuild writes it again from the journal; whether a history view falls under the retention and deletion policy of D19, and what its rebuild does after an erasure, is answered by that policy (D19, D9, E-9)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.order-allocation-history", + "file": "example/domain/orderAllocationHistory.ts", + "line": 8, + "role": "projection", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.projection-contract", + "resolved": true, + "title": "Projection contract", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 21, + "declarations": 11, + "openQuestions": 5, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "projectionRowContract", + "blocking": false, + "address": "spec:application.projection-contract#question.projectionRowContract", + "line": 33, + "question": "Extension E-9: the doc says a projection is named, versioned and deterministic and that live update and rebuild share its logic; it does not give the signature or the row shape, for counts and sums it names the marker but not the aggregate row, and for the read models that depend on history across several streams, the ones the write pause exists for, it names no input. The option taken here: a per-entity read model declared as a `ReadModel<D, Row>` with `name`, `table`, `rowBudgetBytes` and its `projections`, a list of `Projection<D, Row>` with the latest first, each holding `version`, `keyOf` and `project` alone, rows with `tenantId`, `generation`, `key`, `projectionVersion` and `sourceVersions`, one `applyProjection` helper with a mode per caller that writes each generation by its role, active, building or verifying, with the projection whose version the generation records, for the aggregate form the row and the writer `spec:application.aggregate-projection` pins, and for a history-dependent view a `HistoryProjection<Row>` that folds events into the prior row, fed on the live path by the events the operation appended, which the adapter's `streams` entry carries, and on the backfill by the context's `history` pages under the write pause, with `sourceVersions` recording the highest version folded per stream so that an event is never folded twice; a row that derives from more than one source stream type is a history row, its projection bound once per source stream type, and a small cross-context view of current state is D8's parent query over bounded component reads; the owner confirms the three forms and has not ruled on the split of a per-entity read model into `ReadModel` and `Projection`, on the write by generation role or on the row measured against its budget at every write (E-9, E-23, E-24, D8, D9)" + }, + { + "key": "projectionGenerationVersions", + "blocking": false, + "address": "spec:application.projection-contract#question.projectionGenerationVersions", + "line": 34, + "question": "Extension E-9, two projection versions: the doc says live update and rebuild share one projection and does not say how a command writes two generations built by two versions; the option taken here is that a read model declares its projection versions as a list, the latest first, and `applyProjection` writes each generation with the projection whose version the generation row records, failing as a technical failure when the read model declares none of that version, so the rows of one generation are one truth of one version; the cost is that an earlier version stays in the code while a generation that records it is written or can be rolled back to, and a projection changed without a version change is still undetected (E-9, D8, D9)" + }, + { + "key": "backfillVersionOverwrite", + "blocking": false, + "address": "spec:application.projection-contract#question.backfillVersionOverwrite", + "line": 35, + "question": "The rule that a row with a newer version is never overwritten is read as the backfill path's rule only, so a live write to the active generation always lands, as `step3` has it; the owner has not confirmed that reading (D9, E-9)" + }, + { + "key": "compositeCurrentStateRow", + "blocking": false, + "address": "spec:application.projection-contract#question.compositeCurrentStateRow", + "line": 36, + "question": "Extension E-9, a composite row of current state: D8 lets a small cross-context view be a read model updated in the command, and no form here writes one row from the DTOs of two sources, because `project` builds a whole row from one DTO and a backfill enumerates one source; the option taken here is D8's other default, a parent query over bounded component reads, until a view needs such a row, which then needs a form whose `project` takes the DTO of every matching entry of one command and whose backfill reads the other context in the same mutation, online (E-9, D8, E-45)" + }, + { + "key": "historySourceVersionTenant", + "blocking": false, + "address": "spec:application.projection-contract#question.historySourceVersionTenant", + "line": 37, + "question": "Extension E-9, the cost of `sourceVersions`: a history row's entries repeat the row's tenant ID once per source stream, about three quarters of the largest order allocation history row by `spec:application.history-projection#design.tableOrderAllocationHistory`; whether a row's `sourceVersions` may omit `tenantId`, equal to the row's on every entry, and the write rule compare three identity fields, is a choice the owner may take with the pinned conventions (E-9, Law 11, F13)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.generation-registry.tables", + "file": "src/read-model/tables.ts", + "line": 5, + "role": "schema", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "id": "impl:application.projection-contract", + "file": "src/read-model/projection.ts", + "line": 3, + "role": "projection", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.read-models", + "resolved": true, + "title": "Read models", + "specKind": "behavior", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 11, + "openQuestions": 6, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "parentListGenerationSwitch", + "blocking": false, + "address": "spec:application.read-models#question.parentListGenerationSwitch", + "line": 49, + "question": "Probe 5 showed that the built-in `.paginate` treats `maximumRowsRead` as advice once a page has an end cursor and that a cursor it issued is refused by any other query, so a subscription over the active generation threw `InvalidCursor` at a switch; a parent list therefore pages with `paginator` of `convex-helpers`, as a context list does, and a pinned page is bounded on every run by the row cap and by the byte cap as `spec:context.queries` states it, the helper stopping at the row that reaches it and admitting that row whole, and answers `SplitRequired` beyond them (Probe 5, F13, D8, D9)" + }, + { + "key": "readModelListBounds", + "blocking": false, + "address": "spec:application.read-models#question.readModelListBounds", + "line": 50, + "question": "Extension E-24, as it reaches read models: a list over a parent read-model table calls `paginator` of `convex-helpers` with the options of `boundedPage`, a row cap of twice the item cap, with each cursor of the client's pair moved into the active generation by `pageInGeneration`, and returns through `paginationResultValidator` with its rows mapped by `readModelView`, and a client pages every list by an explicit cursor pair with no hook; these are options taken here and the owner has not ruled (E-24, S4, Probe 5, D8)" + }, + { + "key": "parentQueryRefusal", + "blocking": false, + "address": "spec:application.read-models#question.parentQueryRefusal", + "line": 51, + "question": "Extension E-6, as it reaches queries: a parent query establishes and authorizes its actor through `authorizeQuery`, and its refusal is the command's rejection shape, `RejectionData` with the query's name in the field `entry` and the codes `unauthenticated` or `forbidden`; the owner confirmed the word, and nothing else of the shape is new (E-6, D7, D11, Law 5)" + }, + { + "key": "contextQueryCost", + "blocking": false, + "address": "spec:application.read-models#question.contextQueryCost", + "line": 52, + "question": "Probe 3 pending: the cost of one component call in latency and function-call quota decides whether one entity's detail stays a component query or hot reads move, which is OQ1 (Probe 3, F4, OQ1)" + }, + { + "key": "parentQueryReadBounds", + "blocking": false, + "address": "spec:application.read-models#question.parentQueryReadBounds", + "line": 53, + "question": "Extension E-45: the doc says a small cross-context view is a parent query over bounded component reads and does not give the bound; the option taken here is at most 8 component reads per parent query and at most 4 read-model rows written per command, both measured by the first experiment (E-45, D8)" + }, + { + "key": "crossContextReadForm", + "blocking": false, + "address": "spec:application.read-models#question.crossContextReadForm", + "line": 54, + "question": "Extension E-9, as it reaches read models: D8 lets a small cross-context view be a parent query over bounded component reads or a read model updated in the command; the option taken here is the parent query for a view of current state, because no form of `spec:application.projection-contract` writes one row from the DTOs of two sources, and the history projection for a view that folds the events of several streams; a composite row of current state is a form for the view that needs it (E-9, E-45, D8)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.read-models", + "file": "src/read-model/write.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "id": "impl:application.read-models.queries", + "file": "src/read-model/queries.ts", + "line": 3, + "role": "query", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 5, + "enabled": 5 + }, + "examples": { + "total": 5, + "enabled": 5 + } + }, + { + "id": "spec:application.rebuild", + "resolved": true, + "title": "Rebuild: generations, backfill and the write pause", + "specKind": "workflow", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 42, + "declarations": 27, + "openQuestions": 5, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "rebuildBatchDriver", + "blocking": false, + "address": "spec:application.rebuild#question.rebuildBatchDriver", + "line": 59, + "question": "`@convex-dev/migrations` walks only the tables of the component it is defined in, reaches a context's rows only through a parent internal mutation written by hand against a contract its README does not document and the compiled tier needs a cast for, keeps a status apart from the generation's state, and leaves no error on its row when a batch fails past the read limit, so every batch is driven by the rebuild's own self-scheduled internal mutation and the progress row is the one checkpoint; a hosted deployment and sustained contention are not observed (Probe 6, F1, F17, D9)" + }, + { + "key": "rebuildBatchBounds", + "blocking": false, + "address": "spec:application.rebuild#question.rebuildBatchBounds", + "line": 60, + "question": "Extension E-40: the doc binds resumable batching to `@convex-dev/migrations` and gives no batch size; the component iterates a parent table while the sources of a read model live in a context component. The option taken here: a batch is one mutation with one cursor on the progress row and a bound of at most 100 entities, capped by the read model's declared `rowBudgetBytes` so that the parent's point reads and writes stay under 4 MiB, and by the context's byte-derived `list` page bound, which is 16 items at the default 256 KiB stream budget; every batch, the backfill and the verify pass over a context's enumeration and the purge over a parent table, is driven by the platform's own self-scheduled internal mutation, and the rebuild uses the component for nothing, because Probe 6 showed that it walks only the tables of the component it is defined in and drives a context's rows only through a parent internal mutation written by hand against a contract its README does not document; the progress row is the one checkpoint (E-40, E-24, E-9, F17, F13, F1)" + }, + { + "key": "rebuildGenerationPauseContract", + "blocking": false, + "address": "spec:application.rebuild#question.rebuildGenerationPauseContract", + "line": 61, + "question": "Extension E-8: the registry states, the rollback period, what a rollback does, the gate document, the scope a paused rebuild closes and the reach of a generation are design choices the doc does not make; they are pinned in the two contracts under this workflow (E-8, E-9, E-38, D9)" + }, + { + "key": "rebuildReadModelForms", + "blocking": false, + "address": "spec:application.rebuild#question.rebuildReadModelForms", + "line": 62, + "question": "Extension E-8, what this workflow does not offer: the online path is pinned for a per-entity read model with one source stream type and the paused path for a history view, a `HistoryRebuildTarget` with its sources, in `spec:application.history-rebuild`; the aggregate form's rebuild is deferred with the form in its own Spec, the aggregate projection, and a row made from the current state of more than one source has no form at all, as `spec:application.projection-contract` records; `rollbackGeneration` refuses a generation that needs a write pause, because nothing would hold its sources while live commands fold onto a row that missed the events since its retirement, so such a view returns to earlier rules only through a new generation under the pause (E-8, E-9, D9, D18)" + }, + { + "key": "tenantListFill", + "blocking": false, + "address": "spec:application.rebuild#question.tenantListFill", + "line": 63, + "question": "Extension E-49, a deployment whose grants predate the tenant list: the tenant list is written by `insertGrant`, so on such a deployment it holds no row while `grants` holds many, and a pass over an empty list visits no tenant and still ends; the option taken here is that `startGeneration` refuses an empty list beside a grant with a message naming `fillTenants`, and `fillTenants`, an operator entry, walks the grants in bounded batches through a progress row of its own and inserts each missing tenant once; a list that holds a tenant whose first grant came after the table and misses an older one is not detected by that refusal, so the operator runs `fillTenants` on such a deployment before its first `startGeneration` (E-49, E-48, E-40, Law 11)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.rebuild", + "file": "src/read-model/rebuild.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 1, + "enabled": 1 + } + }, + { + "id": "spec:application.history-rebuild", + "resolved": true, + "title": "Rebuild of a history view under a write pause", + "specKind": "workflow", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 12, + "declarations": 3, + "openQuestions": 6, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "historyBatchShape", + "blocking": false, + "address": "spec:application.history-rebuild#question.historyBatchShape", + "line": 41, + "question": "Extension E-40, the shape of a history batch: a batch takes one stream, so a pass costs at least one scheduled mutation per source stream and the pause lasts about twice the stream count times one batch; the alternative is a batch that takes streams from one `list` page until its event budget is spent, one `history` call per stream, with the byte bound counting one baseline per stream; the first paused rebuild on the native backend measures one batch, and the shape is chosen with that number (E-40, E-9, F13, F19, D9)" + }, + { + "key": "verifyPassUnderPause", + "blocking": false, + "address": "spec:application.history-rebuild#question.verifyPassUnderPause", + "line": 42, + "question": "The verify pass under the write pause: a history view's generation is built while no source changes, so its verify pass repeats the backfill's batches and, by `spec:application.history-rebuild#design.historyVerify`, finds nothing over a covered generation, which doubles the pause; the option kept here is the pass, as D9's \"Verify coverage, then switch\" and Sc L2-6's \"writes reopen only after verification\" read, and whether a paused generation is `verified` when its backfill ends is the owner's (D9, Sc L2-6)" + }, + { + "key": "historyBatchWriteThroughput", + "blocking": false, + "address": "spec:application.history-rebuild#question.historyBatchWriteThroughput", + "line": 43, + "question": "Whether a history view's batch chain, at up to 2 MiB of rows per batch scheduled with `runAfter(0)`, is held by the deployment class's write throughput limit, `spec:facts.f13-transactions-have-limits#design.limitWriteThroughput`, which the first paused rebuild on the native backend and one hosted run show (F13, E-40)" + }, + { + "key": "historyMigrationBeforePause", + "blocking": false, + "address": "spec:application.history-rebuild#question.historyMigrationBeforePause", + "line": 44, + "question": "Must every source's migration that reads history finish before `startGeneration` closes its source scopes, with abort, migration completion and a fresh generation as the recovery if it did not? If enumeration must instead remain possible during that migration, which context query provides it without constructing a DTO of the wrong meaning?" + }, + { + "key": "abortSwitchAtomicity", + "blocking": false, + "address": "spec:application.history-rebuild#question.abortSwitchAtomicity", + "line": 45, + "question": "Should the native-backend abort example inject failure in the second source's audit insert, assert unchanged generation, progress, gate entries and audit records, then retry after removing the fault and assert one successful abort with both sources reopened? The same atomicity check should cover the switch that reopens them." + }, + { + "key": "historyTenantPauseIsolation", + "blocking": false, + "address": "spec:application.history-rebuild#question.historyTenantPauseIsolation", + "line": 46, + "question": "Should the native-backend example use two tenants with equal local order and stock item IDs, check their folds separately, and refuse a command that writes a paused source without a history binding in both tenants, while a command writing only an unrelated source remains allowed? Include a retry of resume after its response is lost so the same saved event page cannot change the fold twice." + } + ], + "decisions": [ + { + "id": "spec:decisions.d04-four-outcomes", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 5, + "enabled": 0 + }, + "examples": { + "total": 5, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-interrupt", + "resolved": true, + "title": "Rebuild a cross-stream history view under a write pause; interrupt, then resume", + "specKind": "example", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-abort", + "resolved": true, + "title": "Rebuild a cross-stream history view under a write pause; interrupt, then abort", + "specKind": "example", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-version-change", + "resolved": true, + "title": "Deploy a changed history projection version through the handoff", + "specKind": "example", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-installation", + "resolved": true, + "title": "Install a history view through the handoff", + "specKind": "example", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-source-order-mismatch", + "resolved": true, + "title": "Reorder a history view's sources while its generation is interrupted", + "specKind": "example", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.write-pause", + "resolved": true, + "title": "Write pause", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 14, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "writePauseSourceScopes", + "blocking": false, + "address": "spec:application.write-pause#question.writePauseSourceScopes", + "line": 39, + "question": "Extension E-8: the doc names a maintenance gate every writer reads, fencing or draining of background writers, and operator resume and abort; the option taken here is one gate document per deployment holding the closed scopes, each in one `closed` state, with no fence of its own and no drain interval, because optimistic concurrency serializes every writer's gate read against the close and the generation row's fence already fences a rebuild's batches, and a refusal answered with `writePaused`, the transient code the outcome boundary reserves for this contract under its E-35, which this contract confirms; the scope a paused rebuild closes is the paused view's sources, one `source:` entry per context and stream type its bindings name, because a consistent cut must hold every writer of those streams and a use case that writes a source stream without maintaining the view would otherwise pass the gate, which is why the command declaration names what it writes under E-38 and the pipeline proves the list at its step 9; an operator entry closes only a tenant or `all`, the coarser and always safe scopes, and no scope names a read model, because nothing would close it (E-8, E-38, D9, D4, F1)" + }, + { + "key": "generationGateExemption", + "blocking": false, + "address": "spec:application.write-pause#question.generationGateExemption", + "line": 40, + "question": "Extension E-8, the generation exemption: a batch reads the gate with the scopes `all` and its tenant, and no entry of either scope ever names a generation, so the exemption `gateAllows` makes for a given generation is reached by no reader; either the exemption goes, or a history batch reads the gate with its source scopes as well and passes them by it (E-8, D9)" + }, + { + "key": "writePauseOperatingInstructions", + "blocking": false, + "address": "spec:application.write-pause#question.writePauseOperatingInstructions", + "line": 41, + "question": "Should its operating instructions distinguish a stopped pass from a verified generation awaiting switch, using `getGenerations` and `getGate`, and measure the closed interval through the successful switch or abort? Keep any acceptable pause length for the owner's decision rather than deriving a timeout from a batch estimate." + } + ], + "decisions": [ + { + "id": "spec:decisions.d04-four-outcomes", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "component:platform.gate", + "file": "src/gate/index.ts", + "line": 3, + "role": null, + "component": null + }, + { + "id": "impl:application.write-pause", + "file": "src/gate/gate.ts", + "line": 8, + "role": "service", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + }, + { + "id": "impl:application.write-pause.entries", + "file": "src/gate/entries.ts", + "line": 7, + "role": "operator-entry", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + }, + { + "id": "impl:application.write-pause.tables", + "file": "src/gate/tables.ts", + "line": 4, + "role": "schema", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 1, + "enabled": 1 + } + }, + { + "id": "spec:application.generation-registry", + "resolved": true, + "title": "Generation registry", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 13, + "openQuestions": 6, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "generationRollbackPolicy", + "blocking": false, + "address": "spec:application.generation-registry#question.generationRollbackPolicy", + "line": 37, + "question": "Extension E-8: the doc names a generation registry, a marker and a gate without their shapes, and says to keep the old generation for a rollback period without saying whether live commands keep writing it; the option taken here is the `generations` table below with seven states, a rollback period of 7 days, one generation being built per read model at a time, and a retired generation that is kept and not written, so that a rollback reopens it as `verifying`, one verify pass repairs what the writes since its retirement changed, and the ordinary switch makes it active again; a rollback is therefore a verify pass and then a switch of one write, never one write by itself (E-8, D9, Sc L2-5)" + }, + { + "key": "generationTenantReach", + "blocking": false, + "address": "spec:application.generation-registry#question.generationTenantReach", + "line": 38, + "question": "Extension E-8, reach of a generation: the doc says nothing about tenants and rebuild; one generation per read model spans every tenant, the registry has no tenant dimension, one row is active for all tenants and the batches iterate the tenant list of `spec:command.actor-and-scope`; the registry is operational data of the deployment, so its indexes lead with the read model name and not with `tenantId`, a confirmed deviation from Law 11's index rule of the same kind the obligation module records under E-57, while every table that holds tenant data, the read-model rows among them, leads with the tenant; a rebuild for one tenant is not offered (E-8, D9, D11, Law 11, E-57)" + }, + { + "key": "restoreGenerationFence", + "blocking": false, + "address": "spec:application.generation-registry#question.restoreGenerationFence", + "line": 39, + "question": "Extension E-42, as it touches the registry: a restore brings back a generation row and its progress row at their checkpoint and drops the scheduled batch that was driving them, so the row's fence at inventory time is the restore's proof that the generation is still stranded, and `resumeChain` bumping it is the proof that it is not (E-42, F12)" + }, + { + "key": "readModelInstallation", + "blocking": false, + "address": "spec:application.generation-registry#question.readModelInstallation", + "line": 40, + "question": "Extension E-8, the first generation: the doc says how a generation is rebuilt and not how a read model's first generation comes to exist; the option taken here is that a read model is installed through its first rebuild, generation 1 starting as `building` like any other, so a read model added to a deployment that has history serves that history from its first switch and no transition claims coverage it did not build; a command that finds no generation to write fails as a technical failure and a query that finds no active generation fails the same way (E-8, D9, D19)" + }, + { + "key": "rebuildProgressRow", + "blocking": false, + "address": "spec:application.generation-registry#question.rebuildProgressRow", + "line": 41, + "question": "Extension E-40, the checkpoint: the cursor, the batch size and the counts live on the progress row and not on the generation row, because a batch that wrote its checkpoint on the row every command reads would re-run every command in flight on the read model once per batch; the progress row is the one checkpoint of every batch whatever drives it (E-40, F1)" + }, + { + "key": "tenantListGenerationStart", + "blocking": false, + "address": "spec:application.generation-registry#question.tenantListGenerationStart", + "line": 42, + "question": "Extension E-49, as it touches the registry: on a deployment whose grants predate the tenant list the list is empty while grants exist, and a generation started then visits no tenant; the option taken here is that `startGeneration` refuses then, with a message naming `fillTenants`, the operator entry of `spec:application.rebuild` that fills the list from the grants, and refuses while that fill runs (E-49, E-8, Law 11)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.generation-registry", + "file": "src/read-model/generations.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "id": "impl:application.generation-registry.tables", + "file": "src/read-model/tables.ts", + "line": 5, + "role": "schema", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 2, + "enabled": 2 + } + }, + { + "id": "spec:application.orders-inventory-example", + "resolved": true, + "title": "Orders and Inventory, the example domain", + "specKind": "model", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "orderInventoryCommandChoices", + "blocking": false, + "address": "spec:application.orders-inventory-example#question.orderInventoryCommandChoices", + "line": 36, + "question": "Extension E-46: the second lifecycle command, the essential summary, the command that creates stock, the unit price an order line carries, the bound on an order line's stock item ID, the rejection reasons, the stream budgets of the two stream types, the order of the two context calls and the history-dependent view Sc L2-6 builds are choices the doc leaves open; two of them are decided, that the stock item keeps totals only and `release` takes its quantities from the cancelled order's own lines, and that the order allocation history view stays in the experiment; the owner may substitute the others, and the experiment's terms move with them (E-46, E-2, E-9, First experiment)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d03-events-only-source-of-next-state", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.order-allocation-history", + "file": "example/domain/orderAllocationHistory.ts", + "line": 8, + "role": "projection", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "id": "impl:application.orders-inventory-example", + "file": "example/domain/order.ts", + "line": 10, + "role": "decider", + "component": { + "id": "component:example.orders-domain", + "layer": "domain", + "context": "orders" + } + }, + { + "id": "impl:application.orders-inventory-example.inventory-operations", + "file": "example/convex/inventory/operations.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "id": "impl:application.orders-inventory-example.inventory-schema", + "file": "example/convex/inventory/schema.ts", + "line": 1, + "role": "schema", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "id": "impl:application.orders-inventory-example.inventory-stock-item-query", + "file": "example/convex/inventory/queries/stockItem.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "id": "impl:application.orders-inventory-example.inventory-streams", + "file": "example/convex/inventory/streams.ts", + "line": 2, + "role": null, + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "id": "impl:application.orders-inventory-example.order-summary", + "file": "example/convex/orderSummary.ts", + "line": 4, + "role": "read-model", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "id": "impl:application.orders-inventory-example.orders-operations", + "file": "example/convex/orders/operations.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "id": "impl:application.orders-inventory-example.orders-order-query", + "file": "example/convex/orders/queries/order.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "id": "impl:application.orders-inventory-example.orders-schema", + "file": "example/convex/orders/schema.ts", + "line": 1, + "role": "schema", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "id": "impl:application.orders-inventory-example.orders-streams", + "file": "example/convex/orders/streams.ts", + "line": 3, + "role": null, + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "id": "impl:application.orders-inventory-example.stock-item", + "file": "example/domain/stockItem.ts", + "line": 11, + "role": "decider", + "component": { + "id": "component:example.inventory-domain", + "layer": "domain", + "context": "inventory" + } + }, + { + "id": "impl:application.parent-use-cases", + "file": "example/convex/ordering.ts", + "line": 8, + "role": "use-case", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "id": "impl:application.parent-use-cases.receiving", + "file": "example/convex/receiving.ts", + "line": 4, + "role": "use-case", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + } + ], + "verifiers": { + "total": 6, + "enabled": 6 + }, + "examples": { + "total": 6, + "enabled": 6 + } + }, + { + "id": "spec:command.command-declaration", + "resolved": true, + "title": "Command declaration and bindings", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 19, + "declarations": 18, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "commandEntrySignatures", + "blocking": false, + "address": "spec:command.command-declaration#question.commandEntrySignatures", + "line": 38, + "question": "Extension E-7: the doc says a small composition helper and shared schema values; the helper signatures, the executor signature and the read-model binding below are the design's; the cost is two registered mutations per command instead of one, and the cheaper alternative the design refuted is one public export per command plus one generic internal dispatcher mutation keyed by command type, as D7's conditional dispatcher already does; it lost because trusted server callers would then reach every command through one untyped `internalMutation`, so a renamed handler or a broken argument contract would pass `tsc` at the caller and fail only at runtime, which is the check Sc L2-4 exists to keep before traffic, and because the receipt fingerprint's contract version would be looked up by string rather than typed; the owner may still prefer the dispatcher form once code generation makes the per-command internal entry free (E-7, D12, D7, Sc L2-4, Decision method rule 5)" + }, + { + "key": "commandDeclarationLookup", + "blocking": false, + "address": "spec:command.command-declaration#question.commandDeclarationLookup", + "line": 39, + "question": "Extension E-7: the pipeline receives the declaration and takes the fingerprint's contract version from it, and the type check at a typed caller is the check of Sc L2-4, so no standing map from command type to declaration exists; the conditional dispatcher of `spec:command.outcome-boundary` is the one thing that would need such a map and brings it with it; this is a provisional reading the owner rules (E-7, D12, D18, OQ2)" + }, + { + "key": "commandDeclarationFields", + "blocking": false, + "address": "spec:command.command-declaration#question.commandDeclarationFields", + "line": 40, + "question": "Extension E-38: the doc names five declaration fields; this design adds `contractVersion` for the fingerprint, `retention` and `irreversible` for receipts, `admission`, `bounds`, `rejections`, `audit`, `diagnosticSink` and `writes`, each serving a cited rule of another Spec, and asks whether the owner accepts them as part of the one declaration; `bounds` is the one input bound in the corpus, `maxItems` and `maxBytes`, checked by the pipeline's step 1 and by no executor body, so `spec:application.parent-use-cases` reads it rather than declaring a bound of its own; `writes` is required on every declaration, because an absent list would be an unchecked one, and `readModels` is optional, absent meaning none, which the owner has not ruled; `writes` names the context and stream type pairs the executor may write, from which the pipeline's step 7 derives the source scopes the write pause closes for a consistent cut and which step 9 proves against the `streams` entries the executor returns, because a use case that writes a source stream without maintaining the paused view would otherwise pass the gate; the declaration still imports nothing from the obligation family, so a command's static reactions are not a field here but the `createObligation` calls of its executor, as `spec:obligations.fan-out-and-chains` rules under E-54 (E-38, E-8, E-54, D6, D9, D10, D19, Thesis)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d12-one-declaration-per-command", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:command.command-declaration", + "file": "src/command/declaration.ts", + "line": 3, + "role": "contract", + "component": { + "id": "component:platform.command", + "layer": "application", + "context": "platform" + } + }, + { + "id": "impl:command.command-declaration.depot-commands", + "file": "fixture/convex/depotCommands.ts", + "line": 3, + "role": null, + "component": null + } + ], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 2, + "enabled": 2 + } + }, + { + "id": "spec:command.command-pipeline", + "resolved": true, + "title": "The command pipeline", + "specKind": "workflow", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 35, + "declarations": 18, + "openQuestions": 7, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "commandOutcomeWireShape", + "blocking": false, + "address": "spec:command.command-pipeline#question.commandOutcomeWireShape", + "line": 68, + "question": "Extension E-30: the doc names the outcome, the operation ID and the affected stream versions as what a command returns, but not a wire shape; the shape here is a union discriminated by `replayed`, whose executed member carries the declaration's result and whose replayed member carries `result` null because a receipt stores no result, so the entry's returns validator admits a replay as a value; affected IDs are application IDs, never private document IDs (E-30, D2, D6, D8)" + }, + { + "key": "operationIdentityCause", + "blocking": false, + "address": "spec:command.command-pipeline#question.operationIdentityCause", + "line": 69, + "question": "Extension E-31: the doc says an operation's ID ties together everything one request caused but not how it is minted, and it says a derived command carries causation and an event's `causedBy` may be a triggering event without saying how that cause reaches the pipeline; here the pipeline mints the operation ID once per top-level mutation with `crypto.randomUUID()` before the first context call, a duplicate returns the operation ID stored on the receipt, and the internal entry takes an optional `causedBy` argument that step 7 copies onto the `OperationRef` when present, so a reaction that issues a derived command passes `causedBy` of kind `event` naming its publication event and the envelope's `event` variant has exactly one producer, while an entry called without it mints `causedBy` of kind `command` from the declaration's name; the obligation and attempt behind a derived command are found through its receipt, whose request key is the obligation's effect key, and never through the envelope (E-31, E-54, D2, D13, Vocab: operation)" + }, + { + "key": "commandAdmissionPolicy", + "blocking": false, + "address": "spec:command.command-pipeline#question.commandAdmissionPolicy", + "line": 70, + "question": "Extension E-32: the doc places rate admission for new intent after the duplicate check and makes a rate or capacity refusal a retryable error, but names no admission mechanism; here admission is an optional per-command policy in the declaration, absent by default, evaluated inside the mutation so that a refusal that throws leaves no consumption behind; the flip side is that a consumption the policy writes commits only with the command, so the policy counts committed intent and cannot bound refused or unauthorized traffic, and a product that must bound refused traffic needs the dispatcher form of D7, where the parent survives the body's rejection (E-32, D4, D6, D7)" + }, + { + "key": "publicRequestKey", + "blocking": false, + "address": "spec:command.command-pipeline#question.publicRequestKey", + "line": 71, + "question": "Extension E-33: the doc lists the callers that need receipts and leaves UI commands to the client guarantee; here a public caller may still supply a request key to opt into a receipt, which Probe 1 showed a caller of `ConvexHttpClient` needs when it retries, and the internal entry always requires one (E-33, D6, Probe 1)" + }, + { + "key": "contextCallBudgetReservation", + "blocking": false, + "address": "spec:command.command-pipeline#question.contextCallBudgetReservation", + "line": 72, + "question": "Probe 4 showed on the pinned backend that the transaction limits are one budget across the parent and its component sub-transactions, so the per-command bounds are checked against one budget; `ctx.runMutation` accepts a `transactionLimits` option, the design passes none on a context call, and whether a call should reserve room with it for the work after it stays open (Probe 4, F13, S3)" + }, + { + "key": "commandReadModelWrites", + "blocking": false, + "address": "spec:command.command-pipeline#question.commandReadModelWrites", + "line": 73, + "question": "Extension E-9: the doc says essential read models are updated inside the command and does not say from what or in what order; here step 9 writes a per-entity read model from the DTO of each matching `streams` entry, never from its events, and a history read model from the events of every matching entry in one call per command, checks `writes` first, then that each bound read model has a generation to write, then the count of the rows it writes against `limitReadModelWritesPerCommand`, each failure a plain error, and the library helpers that step 9 calls are generic over the application's data model; the owner has not ruled on any of it (E-9, E-7, E-8, E-45, D8, D9)" + }, + { + "key": "commandTextBounds", + "blocking": false, + "address": "spec:command.command-pipeline#question.commandTextBounds", + "line": 74, + "question": "Extension E-34: step 1 bounds every text field a caller sets beside the input, the tenant ID, the request key, the correlation ID, the actor's fields and the fields of `causedBy`, in UTF-8 bytes as `limitCallText` lists them, as rejection `invalidInput`, and treats a declaration name above `limitIdLength` as a defect, a plain error, because these fields become fields of the receipt row or of every event's envelope; the doc says events stay small and names no number; the owner has not ruled (E-34, D6, D12, D19)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d12-one-declaration-per-command", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.read-models", + "file": "src/read-model/write.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "id": "impl:command.command-pipeline", + "file": "src/command/pipeline.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.command", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 3, + "enabled": 3 + }, + "examples": { + "total": 3, + "enabled": 3 + } + }, + { + "id": "spec:context.event-envelope", + "resolved": true, + "title": "The event envelope", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 16, + "declarations": 10, + "openQuestions": 2, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "eventEnvelopeFields", + "blocking": false, + "address": "spec:context.event-envelope#question.eventEnvelopeFields", + "line": 31, + "question": "Extension E-26: the doc lists the fifteen fields and their meaning; this Spec fixes the validator shapes, the `CausedBy` union with a migration variant for baseline events, the `OperationRef` the parent passes, and the event ID as a version 4 UUID from the runtime's `crypto`; the owner confirms (D2, E-26)" + }, + { + "key": "envelopeTextBounds", + "blocking": false, + "address": "spec:context.event-envelope#question.envelopeTextBounds", + "line": 32, + "question": "Extension E-12: the doc says events stay small and gives no number for the envelope; this Spec bounds the envelope beside the payload at `limitEnvelopeBytes`, 4,096 bytes, and each text field of it that a caller can set at 256 bytes of UTF-8, 512 for an actor's ID, so the largest stored event, `limitEventBytes`, is derived and enforced and never assumed; the owner may set other numbers, and the fold bound of `spec:context.journal` moves with them (E-12, D19, F13)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:context.event-envelope", + "file": "src/context/envelope.ts", + "line": 2, + "role": "contract", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:context.journal", + "resolved": true, + "title": "The journal", + "specKind": "behavior", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 39, + "declarations": 18, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "baselineMigrationContract", + "blocking": false, + "address": "spec:context.journal#question.baselineMigrationContract", + "line": 51, + "question": "Extension E-25: the doc says a migration writes a baseline event holding the migrated state and that rebuild starts from the latest baseline, and it does not say who computes that state, who drives the migration, or what a live command does on a stream the migration has not reached; this Spec fixes the representation, an event of the reserved type `baseline` whose payload holds the state and its schema version and is bounded by the stream's `budgetBytes` instead of the payload bound, a `baselineVersion` on the stream row, a `BaselineMigration` registered on the stream type whose `migrate` function computes the migrated state from the saved state, or, for a step that declares `readsHistory`, from the saved state and the events since the last baseline in the sweep only, the rule that `stateSchemaVersion` counts meaning only and lives on the registration, one helper `migrateToSchemaVersion` that writes the baseline chain, called by the adapter on load for a chain that reads no history and by the `writeBaseline` maintenance mutation for every chain, the code-older-than-data error, the `writeBaseline` mutation built by `defineMaintenance` and declared with the component's public `mutation` builder, the parent-side scheduled chain that drives it by cursor, the batch bound derived from `budgetBytes`, the upcasting rule for old event schema versions, the fold bound of 500 events with the refusals `rebuildStream` returns above it and for a stream awaiting its baseline, the retirement rule for a migration, and the payload byte check in `append`; the option of refusing commands on unmigrated streams with a transient code until the sweep arrives was not taken for a state-only migration, because it turns a deploy into a partial write pause the doc reserves for history-dependent views, and is taken for a history-reading migration, the exception, where it holds only the streams the sweep has not reached and keeps D3 whole on the command path, which the owner confirmed; a consequence the owner should weigh is that every baseline bumps its stream's version, so a reviewed version held across the sweep is answered `staleVersion` and re-reviewed rather than silently accepted, because Sc L1-10 forbids reinterpreting a reviewed version against fresh state and the baseline is a real event of the stream, while a live stream migrated on load never trips it because its expected-version check runs before the chain; and the driver reads the write pause's gate before every batch and reschedules itself when refused, so a sweep never changes a source under a paused rebuild's cut; the owner confirms (D5, D9, S4, Sc L1-10, E-2, E-8, E-12, E-25)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:kernel.state-document-mapping", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:context.journal", + "file": "src/context/journal.ts", + "line": 2, + "role": "event-store", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 0 + }, + "examples": { + "total": 1, + "enabled": 0 + } + }, + { + "id": "spec:context.queries", + "resolved": true, + "title": "The context queries", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 29, + "declarations": 18, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "contextPaginationSplit", + "blocking": false, + "address": "spec:context.queries#question.contextPaginationSplit", + "line": 41, + "question": "Probe 5 showed on the pinned backend with `convex-helpers` 0.1.124 that the parent query stays reactive, that pages pinned by their end cursors stay contiguous, that a capped page crosses the boundary as `SplitRequired` with its `splitCursor`, and that the `usePaginatedQuery` hook of `convex-helpers/react` then shows 40 of 45 rows; this design uses no hook across the boundary, and the examples are rerun when either pin moves (Probe 5, F15, S4, D8, E-24)" + }, + { + "key": "contextQueryContract", + "blocking": false, + "address": "spec:context.queries#question.contextQueryContract", + "line": 42, + "question": "Extension E-24: the doc says component queries are authorized and return DTOs; this Spec fixes one definer per query, the one `tenantId` argument with no separate scope, the `toDto` mapping with the stream version on every DTO, pagination through `paginator`, the item cap with a row cap of twice the item cap and a byte cap passed on every run, the relay of `pageStatus` and `splitCursor` unchanged, paging by an explicit cursor pair with no client hook, the parent query's one `authorizeQuery` call and its refusal as a rejection, the `maxEvents` argument of the `rebuild` query and its `budgetExhausted` refusal for a batching caller, and the fold refusal of the `rebuild` query; this is a provisional reading the owner has not ruled (D8, S4, E-24, E-42)" + }, + { + "key": "queryRefusalEntry", + "blocking": false, + "address": "spec:context.queries#question.queryRefusalEntry", + "line": 43, + "question": "A parent query's refusal carries the name of the entry that refused in the field `entry` of the rejection data; the wire shape stays `RejectionData` of `spec:command.outcome-boundary` unchanged, and the owner confirmed the word (D7, E-24)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:context.queries", + "file": "src/context/queries.ts", + "line": 3, + "role": "query", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "resolved": true, + "title": "Rebuild online by default, pause writes only for history-dependent views", + "specKind": "decision", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "rebuildContentionCoverage", + "blocking": false, + "address": "spec:decisions.d09-rebuild-online-by-default#question.rebuildContentionCoverage", + "line": 30, + "question": "Probe 6 ran a backfill batch racing a live command and the migrations component on the native backend, with its results on F17; a hosted deployment and sustained contention are not run (Probe 6, F1, F17, D9)" + } + ], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + } + ], + "boundary": { + "restsOn": [ + { + "id": "spec:application.first-experiment", + "title": "The first experiment", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:command.actor-and-scope", + "title": "Actor, tenant scope, namespace and grants", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.idempotency-and-receipts", + "title": "Idempotency and receipts", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.outcome-boundary", + "title": "The outcome and rejection boundary", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.receipt-table", + "title": "The receipts table", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.tenancy-and-authority", + "title": "Tenancy and authority", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:constraints.bulk-operations-bounded", + "title": "Every bulk operation has a stated bound", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:constraints.events-stay-small", + "title": "Events stay small", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:constraints.no-queue-recovery-for-essential-reads", + "title": "No queue recovery needed for essential reads", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:constraints.zero-core-projection-jobs", + "title": "Zero core projection jobs", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:context.batch-shaped-api", + "title": "Batch-shaped context APIs", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:context.context-component", + "title": "The context component", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:context.journal", + "spec:context.queries" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.persistence-adapter", + "title": "The persistence adapter", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.tables", + "title": "The context tables", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "title": "One mutation per business operation", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.read-models", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "title": "Each bounded context owns its state and its journal", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d03-events-only-source-of-next-state", + "title": "Events are the only source of the next state", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "title": "Rebuildable history by default, with baseline events", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "title": "Convex covers UI retries, receipts cover the rest", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "title": "Rejections are thrown, not stored, at the public boundary", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "title": "Contexts meet only in parent use cases", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "title": "Tenant scope and authority from day one", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d12-one-declaration-per-command", + "title": "One declaration per command, generated bindings later", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "title": "Everything else waits for its trigger", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection" + ] + } + ] + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "title": "Operations travel with the capability", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.rebuild", + "spec:application.history-rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f01-serializable-mutations-under-occ", + "title": "Mutations are serializable under optimistic concurrency", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:facts.f02-component-calls-commit-with-caller", + "title": "Component calls commit or roll back with the calling mutation", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f03-nested-run-mutation-partial-rollback", + "title": "A nested runMutation gives partial rollback", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:facts.f04-nested-calls-cost-more-than-helpers", + "title": "Nested calls cost more than helper calls", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:facts.f07-queries-reactive-not-durable-delivery", + "title": "Queries are reactive, subscriptions are not durable delivery", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f08-scheduling-commits-with-mutation", + "title": "Scheduling from a mutation commits with it", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "title": "Scheduled mutations run once, scheduled actions are not retried", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f11-components-have-no-ctx-auth", + "title": "Components have no ctx.auth", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.write-pause", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f12-backups-exclude-pending-scheduled-functions", + "title": "Backups exclude pending scheduled functions", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:facts.f13-transactions-have-limits", + "title": "Transactions have limits", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f14-convex-error-survives-nested-and-component-boundary", + "title": "ConvexError data survives a nested mutation and a component boundary", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:facts.f15-parent-query-over-component-query-stays-reactive", + "title": "A parent query over a component query stays reactive", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f17-migrations-fits-generation-backfill", + "title": "The migrations component fits a generation backfill", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.generation-registry", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:facts.f18-commit-timestamps", + "title": "Commit timestamps order committed transactions", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f19-nested-calls-share-time-budgets", + "title": "Nested calls draw on a system-operation time budget", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f20-pagination-checks-bytes-after-reading", + "title": "Pagination checks bytes after reading", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:kernel.outcome-model", + "title": "The outcome model", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:kernel.state-document-mapping", + "title": "One document per stream, with a derived mapping above the budget", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law01-sanctioned-writes-only", + "title": "Business writes go only through sanctioned operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law02-state-and-events-commit-together", + "title": "State and its required events commit together", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law03-events-only-source-of-state", + "title": "Events are the only source of the next state", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law04-server-scoped-idempotency-key", + "title": "A retryable command carries a server-scoped idempotency key", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law05-authorization-before-execution-and-disclosure", + "title": "Authorization comes before execution and disclosure", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:laws.law06-technical-failure-never-a-rejection", + "title": "A technical failure is never stored as a rejection", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law09-no-invariant-on-late-read-model", + "title": "No invariant depends on a late read model", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law10-replay-never-runs-commands-or-effects", + "title": "Replay never runs commands or repeats effects", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law11-tenant-scope-named", + "title": "Every command and query names its tenant scope", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:operations.baseline-operations", + "title": "Baseline operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:platform.transactional-domain-platform", + "title": "Convex transactional domain platform", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + } + ], + "restedOnBy": [ + { + "id": "spec:advanced.cross-stream-online-rebuild", + "title": "Online rebuild of cross-stream history views", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:agents.agent-runs", + "title": "Agent runs", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:application.aggregate-projection", + "title": "Aggregate projection", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:application.projection-contract", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:application.first-experiment", + "title": "The first experiment", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.history-projection", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.command-without-generation-fails", + "title": "A command whose read model has no generation fails", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.first-rebuild-installs-read-model", + "title": "A read model is installed through its first rebuild", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-duplicate-answered-from-receipt", + "title": "A repeated cancel request is answered from its receipt", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-placed-order", + "title": "A placed order is cancelled", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-second-context-rejects", + "title": "Orders cancels, then Inventory refuses the release", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-twice-rejected", + "title": "A second cancel of one order is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-unknown-order-rejected", + "title": "A cancel of an order never placed is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-without-grant-refused", + "title": "A caller without the cancel grant is refused", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.parent-use-cases", + "title": "Parent use cases", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-read-through-query", + "title": "A successful command, then a query", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-visible", + "title": "A successful command, then a query or subscription", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.list-pages-by-cursor", + "title": "A client reads a tenant's order list in pages by cursor", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-before-disclosure", + "title": "A caller with no identity reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-without-grant", + "title": "A caller with no grant in the tenant reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.rebuild.online-rebuild-interrupt-resume", + "title": "Rebuild a per-entity read model online while commands run; interrupt and resume", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:application.restore", + "title": "Restore", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.projection-contract", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:context.event-envelope", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:application.write-pause.restore-door-without-deploy", + "title": "The restore door closes and opens with no deploy", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.broken-argument-contract-fails-build", + "title": "Break a typed handler's argument contract", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.renamed-handler-fails-build", + "title": "Rename a typed handler", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-journal-append", + "title": "Inject a failure after the journal append", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-state-write", + "title": "Inject a failure after the state write", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-before-receipt", + "title": "Inject a failure before the receipt insert", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.idempotency-and-receipts", + "title": "Idempotency and receipts", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.outcome-boundary", + "title": "The outcome and rejection boundary", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.receipt-table", + "title": "The receipts table", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.tenancy-and-authority", + "title": "Tenancy and authority", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:context.context-component", + "title": "The context component", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:context.journal.rebuild-from-baseline", + "title": "Rebuild a stream that has a baseline event", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:context.persistence-adapter", + "title": "The persistence adapter", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:context.tables", + "title": "The context tables", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "title": "Everything else waits for its trigger", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:obligations.fan-out-and-chains", + "title": "Fan-out and reaction chains", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:obligations.local-reaction-wrapper", + "title": "Local reaction wrapper", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:obligations.obligation-module", + "title": "Obligation module", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:operations.baseline-operations", + "title": "Baseline operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + } + ], + "verifiedBy": [ + { + "id": "spec:application.generation-registry.command-without-generation-fails", + "title": "A command whose read model has no generation fails", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.first-rebuild-installs-read-model", + "title": "A read model is installed through its first rebuild", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-duplicate-answered-from-receipt", + "title": "A repeated cancel request is answered from its receipt", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-placed-order", + "title": "A placed order is cancelled", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-second-context-rejects", + "title": "Orders cancels, then Inventory refuses the release", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-twice-rejected", + "title": "A second cancel of one order is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-unknown-order-rejected", + "title": "A cancel of an order never placed is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-without-grant-refused", + "title": "A caller without the cancel grant is refused", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-read-through-query", + "title": "A successful command, then a query", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-visible", + "title": "A successful command, then a query or subscription", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.list-pages-by-cursor", + "title": "A client reads a tenant's order list in pages by cursor", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-before-disclosure", + "title": "A caller with no identity reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-without-grant", + "title": "A caller with no grant in the tenant reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.rebuild.online-rebuild-interrupt-resume", + "title": "Rebuild a per-entity read model online while commands run; interrupt and resume", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:application.write-pause.restore-door-without-deploy", + "title": "The restore door closes and opens with no deploy", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.broken-argument-contract-fails-build", + "title": "Break a typed handler's argument contract", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.renamed-handler-fails-build", + "title": "Rename a typed handler", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-journal-append", + "title": "Inject a failure after the journal append", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-state-write", + "title": "Inject a failure after the state write", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-before-receipt", + "title": "Inject a failure before the receipt insert", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.journal.rebuild-from-baseline", + "title": "Rebuild a stream that has a baseline event", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:context.journal" + ] + } + ] + } + ], + "supersededBy": [] + } +}, +"context": { + "id": "pack:history-view", + "title": "History view", + "framing": "The Specs a history view rests on, in reading order. A history view is a read model whose row folds the events of several streams, such as the order allocation history of the example domain. The history projection comes first because it pins the form, its fold and that view; the projection contract and the read models follow with the row conventions and the read-model rules every view shares. The rebuild, the history rebuild with its five examples, the write pause and the generation registry say how such a view comes to exist and is rebuilt while the commands that write its sources are refused. The rebuild comes first because it holds the operator entries and the batch chain both paths share; the history rebuild holds the paused path alone and the five examples verify it, so the rebuild's implementation binding claims the online path and the history rebuild shows the paused path designed apart from it. The example domain names the events the view folds, and the command declaration and the command pipeline bind the view to the commands that feed it and write it inside each command. The event envelope, the journal and the queries are where the events come from on the live path and on the backfill, and D9 is the decision that a read model which depends on history across several streams rebuilds under a write pause.", + "file": "design/specs/history-view.pack.sdp.md", + "modelRefs": [ + "spec:platform.vocabulary" + ], + "members": [ + { + "id": "spec:application.history-projection", + "resolved": true, + "title": "History projection", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-projection.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 10, + "declarations": 8, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-9, the history fold: D2 orders events only within a stream, and the two paths feed one history row in different orders, the live path command by command and the backfill stream by stream; the option taken here is a fold that gives one row whatever the order across streams and however its events are split between calls, a row no fold deletes, which keeps its subject's ended state, and baselines dropped before `keyOf`, so a view whose meaning needs the order across streams waits for the ordered consumer of D18 (E-9, D2, D9, D18, E-25)", + "blocking": false, + "key": "historyFoldOrder", + "line": 37 + }, + { + "index": 1, + "question": "The cost of the binding on the live path, one row read and one write of up to `rowBudgetBytes` per command and the measure of the row, is taken by the first experiment's latency cell with the order allocation history bound, for `PlaceOrder` and for `CancelOrder` of an order of the maximum lines, before the view is declared essential anywhere (E-9, E-47, F13)", + "blocking": false, + "key": "historyProjectionCost", + "line": 38 + }, + { + "index": 2, + "question": "Extension E-9, the check of the history fold: the order independence the Contract requires is checked for the order allocation history alone; the option taken here is that every history projection of an application carries a pure test that folds a sample of one key's events in every interleaving of its streams and every split into calls, and a library helper that runs that test is added with a second history view (E-9, D2, D18)", + "blocking": false, + "key": "historyFoldPureTest", + "line": 39 + }, + { + "index": 3, + "question": "A history row keeps a deleted subject's history and every rebuild writes it again from the journal; whether a history view falls under the retention and deletion policy of D19, and what its rebuild does after an erasure, is answered by that policy (D19, D9, E-9)", + "blocking": false, + "key": "historySubjectErasure", + "line": 40 + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "title": "Each bounded context owns its state and its journal", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "title": "Rebuildable history by default, with baseline events", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "title": "Everything else waits for its trigger", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "title": "Operations travel with the capability", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.order-allocation-history", + "claim": "anchored", + "label": "the order allocation history's keyOf and project rules", + "file": "example/domain/orderAllocationHistory.ts", + "line": 8, + "role": "projection", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.projection-contract", + "resolved": true, + "title": "Projection contract", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/projection-contract.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 21, + "declarations": 11, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-9: the doc says a projection is named, versioned and deterministic and that live update and rebuild share its logic; it does not give the signature or the row shape, for counts and sums it names the marker but not the aggregate row, and for the read models that depend on history across several streams, the ones the write pause exists for, it names no input. The option taken here: a per-entity read model declared as a `ReadModel<D, Row>` with `name`, `table`, `rowBudgetBytes` and its `projections`, a list of `Projection<D, Row>` with the latest first, each holding `version`, `keyOf` and `project` alone, rows with `tenantId`, `generation`, `key`, `projectionVersion` and `sourceVersions`, one `applyProjection` helper with a mode per caller that writes each generation by its role, active, building or verifying, with the projection whose version the generation records, for the aggregate form the row and the writer `spec:application.aggregate-projection` pins, and for a history-dependent view a `HistoryProjection<Row>` that folds events into the prior row, fed on the live path by the events the operation appended, which the adapter's `streams` entry carries, and on the backfill by the context's `history` pages under the write pause, with `sourceVersions` recording the highest version folded per stream so that an event is never folded twice; a row that derives from more than one source stream type is a history row, its projection bound once per source stream type, and a small cross-context view of current state is D8's parent query over bounded component reads; the owner confirms the three forms and has not ruled on the split of a per-entity read model into `ReadModel` and `Projection`, on the write by generation role or on the row measured against its budget at every write (E-9, E-23, E-24, D8, D9)", + "blocking": false, + "key": "projectionRowContract", + "line": 33 + }, + { + "index": 1, + "question": "Extension E-9, two projection versions: the doc says live update and rebuild share one projection and does not say how a command writes two generations built by two versions; the option taken here is that a read model declares its projection versions as a list, the latest first, and `applyProjection` writes each generation with the projection whose version the generation row records, failing as a technical failure when the read model declares none of that version, so the rows of one generation are one truth of one version; the cost is that an earlier version stays in the code while a generation that records it is written or can be rolled back to, and a projection changed without a version change is still undetected (E-9, D8, D9)", + "blocking": false, + "key": "projectionGenerationVersions", + "line": 34 + }, + { + "index": 2, + "question": "The rule that a row with a newer version is never overwritten is read as the backfill path's rule only, so a live write to the active generation always lands, as `step3` has it; the owner has not confirmed that reading (D9, E-9)", + "blocking": false, + "key": "backfillVersionOverwrite", + "line": 35 + }, + { + "index": 3, + "question": "Extension E-9, a composite row of current state: D8 lets a small cross-context view be a read model updated in the command, and no form here writes one row from the DTOs of two sources, because `project` builds a whole row from one DTO and a backfill enumerates one source; the option taken here is D8's other default, a parent query over bounded component reads, until a view needs such a row, which then needs a form whose `project` takes the DTO of every matching entry of one command and whose backfill reads the other context in the same mutation, online (E-9, D8, E-45)", + "blocking": false, + "key": "compositeCurrentStateRow", + "line": 36 + }, + { + "index": 4, + "question": "Extension E-9, the cost of `sourceVersions`: a history row's entries repeat the row's tenant ID once per source stream, about three quarters of the largest order allocation history row by `spec:application.history-projection#design.tableOrderAllocationHistory`; whether a row's `sourceVersions` may omit `tenantId`, equal to the row's on every entry, and the write rule compare three identity fields, is a choice the owner may take with the pinned conventions (E-9, Law 11, F13)", + "blocking": false, + "key": "historySourceVersionTenant", + "line": 37 + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.generation-registry.tables", + "claim": "anchored", + "label": "the generations, progress, marker and tenant fill tables and rowConventions", + "file": "src/read-model/tables.ts", + "line": 5, + "role": "schema", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "codeId": "impl:application.projection-contract", + "claim": "anchored", + "label": "ReadModel, Projection and applyProjection", + "file": "src/read-model/projection.ts", + "line": 3, + "role": "projection", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.read-models", + "resolved": true, + "title": "Read models", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/application/read-models.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 11, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Probe 5 showed that the built-in `.paginate` treats `maximumRowsRead` as advice once a page has an end cursor and that a cursor it issued is refused by any other query, so a subscription over the active generation threw `InvalidCursor` at a switch; a parent list therefore pages with `paginator` of `convex-helpers`, as a context list does, and a pinned page is bounded on every run by the row cap and by the byte cap as `spec:context.queries` states it, the helper stopping at the row that reaches it and admitting that row whole, and answers `SplitRequired` beyond them (Probe 5, F13, D8, D9)", + "blocking": false, + "key": "parentListGenerationSwitch", + "line": 49 + }, + { + "index": 1, + "question": "Extension E-24, as it reaches read models: a list over a parent read-model table calls `paginator` of `convex-helpers` with the options of `boundedPage`, a row cap of twice the item cap, with each cursor of the client's pair moved into the active generation by `pageInGeneration`, and returns through `paginationResultValidator` with its rows mapped by `readModelView`, and a client pages every list by an explicit cursor pair with no hook; these are options taken here and the owner has not ruled (E-24, S4, Probe 5, D8)", + "blocking": false, + "key": "readModelListBounds", + "line": 50 + }, + { + "index": 2, + "question": "Extension E-6, as it reaches queries: a parent query establishes and authorizes its actor through `authorizeQuery`, and its refusal is the command's rejection shape, `RejectionData` with the query's name in the field `entry` and the codes `unauthenticated` or `forbidden`; the owner confirmed the word, and nothing else of the shape is new (E-6, D7, D11, Law 5)", + "blocking": false, + "key": "parentQueryRefusal", + "line": 51 + }, + { + "index": 3, + "question": "Probe 3 pending: the cost of one component call in latency and function-call quota decides whether one entity's detail stays a component query or hot reads move, which is OQ1 (Probe 3, F4, OQ1)", + "blocking": false, + "key": "contextQueryCost", + "line": 52 + }, + { + "index": 4, + "question": "Extension E-45: the doc says a small cross-context view is a parent query over bounded component reads and does not give the bound; the option taken here is at most 8 component reads per parent query and at most 4 read-model rows written per command, both measured by the first experiment (E-45, D8)", + "blocking": false, + "key": "parentQueryReadBounds", + "line": 53 + }, + { + "index": 5, + "question": "Extension E-9, as it reaches read models: D8 lets a small cross-context view be a parent query over bounded component reads or a read model updated in the command; the option taken here is the parent query for a view of current state, because no form of `spec:application.projection-contract` writes one row from the DTOs of two sources, and the history projection for a view that folds the events of several streams; a composite row of current state is a form for the view that needs it (E-9, E-45, D8)", + "blocking": false, + "key": "crossContextReadForm", + "line": 54 + } + ], + "decisions": [ + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "title": "One mutation per business operation", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "title": "Tenant scope and authority from day one", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.read-models", + "claim": "anchored", + "label": "writeReadModels, step 9's read-model half", + "file": "src/read-model/write.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "codeId": "impl:application.read-models.queries", + "claim": "anchored", + "label": "readModelView, pageInGeneration and limitReadModelList", + "file": "src/read-model/queries.ts", + "line": 3, + "role": "query", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 5, + "enabled": 5 + }, + "examples": { + "total": 5, + "enabled": 5 + } + }, + { + "id": "spec:application.rebuild", + "resolved": true, + "title": "Rebuild: generations, backfill and the write pause", + "specKind": "workflow", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/application/rebuild.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 42, + "declarations": 27, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "`@convex-dev/migrations` walks only the tables of the component it is defined in, reaches a context's rows only through a parent internal mutation written by hand against a contract its README does not document and the compiled tier needs a cast for, keeps a status apart from the generation's state, and leaves no error on its row when a batch fails past the read limit, so every batch is driven by the rebuild's own self-scheduled internal mutation and the progress row is the one checkpoint; a hosted deployment and sustained contention are not observed (Probe 6, F1, F17, D9)", + "blocking": false, + "key": "rebuildBatchDriver", + "line": 59 + }, + { + "index": 1, + "question": "Extension E-40: the doc binds resumable batching to `@convex-dev/migrations` and gives no batch size; the component iterates a parent table while the sources of a read model live in a context component. The option taken here: a batch is one mutation with one cursor on the progress row and a bound of at most 100 entities, capped by the read model's declared `rowBudgetBytes` so that the parent's point reads and writes stay under 4 MiB, and by the context's byte-derived `list` page bound, which is 16 items at the default 256 KiB stream budget; every batch, the backfill and the verify pass over a context's enumeration and the purge over a parent table, is driven by the platform's own self-scheduled internal mutation, and the rebuild uses the component for nothing, because Probe 6 showed that it walks only the tables of the component it is defined in and drives a context's rows only through a parent internal mutation written by hand against a contract its README does not document; the progress row is the one checkpoint (E-40, E-24, E-9, F17, F13, F1)", + "blocking": false, + "key": "rebuildBatchBounds", + "line": 60 + }, + { + "index": 2, + "question": "Extension E-8: the registry states, the rollback period, what a rollback does, the gate document, the scope a paused rebuild closes and the reach of a generation are design choices the doc does not make; they are pinned in the two contracts under this workflow (E-8, E-9, E-38, D9)", + "blocking": false, + "key": "rebuildGenerationPauseContract", + "line": 61 + }, + { + "index": 3, + "question": "Extension E-8, what this workflow does not offer: the online path is pinned for a per-entity read model with one source stream type and the paused path for a history view, a `HistoryRebuildTarget` with its sources, in `spec:application.history-rebuild`; the aggregate form's rebuild is deferred with the form in its own Spec, the aggregate projection, and a row made from the current state of more than one source has no form at all, as `spec:application.projection-contract` records; `rollbackGeneration` refuses a generation that needs a write pause, because nothing would hold its sources while live commands fold onto a row that missed the events since its retirement, so such a view returns to earlier rules only through a new generation under the pause (E-8, E-9, D9, D18)", + "blocking": false, + "key": "rebuildReadModelForms", + "line": 62 + }, + { + "index": 4, + "question": "Extension E-49, a deployment whose grants predate the tenant list: the tenant list is written by `insertGrant`, so on such a deployment it holds no row while `grants` holds many, and a pass over an empty list visits no tenant and still ends; the option taken here is that `startGeneration` refuses an empty list beside a grant with a message naming `fillTenants`, and `fillTenants`, an operator entry, walks the grants in bounded batches through a progress row of its own and inserts each missing tenant once; a list that holds a tenant whose first grant came after the table and misses an older one is not detected by that refusal, so the operator runs `fillTenants` on such a deployment before its first `startGeneration` (E-49, E-48, E-40, Law 11)", + "blocking": false, + "key": "tenantListFill", + "line": 63 + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "title": "Tenant scope and authority from day one", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "title": "Operations travel with the capability", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.rebuild", + "claim": "anchored", + "label": "the online rebuild's operator entries, batch chains, tenant fill and batch bounds", + "file": "src/read-model/rebuild.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 1, + "enabled": 1 + } + }, + { + "id": "spec:application.history-rebuild", + "resolved": true, + "title": "Rebuild of a history view under a write pause", + "specKind": "workflow", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 12, + "declarations": 3, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-40, the shape of a history batch: a batch takes one stream, so a pass costs at least one scheduled mutation per source stream and the pause lasts about twice the stream count times one batch; the alternative is a batch that takes streams from one `list` page until its event budget is spent, one `history` call per stream, with the byte bound counting one baseline per stream; the first paused rebuild on the native backend measures one batch, and the shape is chosen with that number (E-40, E-9, F13, F19, D9)", + "blocking": false, + "key": "historyBatchShape", + "line": 41 + }, + { + "index": 1, + "question": "The verify pass under the write pause: a history view's generation is built while no source changes, so its verify pass repeats the backfill's batches and, by `spec:application.history-rebuild#design.historyVerify`, finds nothing over a covered generation, which doubles the pause; the option kept here is the pass, as D9's \"Verify coverage, then switch\" and Sc L2-6's \"writes reopen only after verification\" read, and whether a paused generation is `verified` when its backfill ends is the owner's (D9, Sc L2-6)", + "blocking": false, + "key": "verifyPassUnderPause", + "line": 42 + }, + { + "index": 2, + "question": "Whether a history view's batch chain, at up to 2 MiB of rows per batch scheduled with `runAfter(0)`, is held by the deployment class's write throughput limit, `spec:facts.f13-transactions-have-limits#design.limitWriteThroughput`, which the first paused rebuild on the native backend and one hosted run show (F13, E-40)", + "blocking": false, + "key": "historyBatchWriteThroughput", + "line": 43 + }, + { + "index": 3, + "question": "Must every source's migration that reads history finish before `startGeneration` closes its source scopes, with abort, migration completion and a fresh generation as the recovery if it did not? If enumeration must instead remain possible during that migration, which context query provides it without constructing a DTO of the wrong meaning?", + "blocking": false, + "key": "historyMigrationBeforePause", + "line": 44 + }, + { + "index": 4, + "question": "Should the native-backend abort example inject failure in the second source's audit insert, assert unchanged generation, progress, gate entries and audit records, then retry after removing the fault and assert one successful abort with both sources reopened? The same atomicity check should cover the switch that reopens them.", + "blocking": false, + "key": "abortSwitchAtomicity", + "line": 45 + }, + { + "index": 5, + "question": "Should the native-backend example use two tenants with equal local order and stock item IDs, check their folds separately, and refuse a command that writes a paused source without a history binding in both tenants, while a command writing only an unrelated source remains allowed? Include a retry of resume after its response is lost so the same saved event page cannot change the fold twice.", + "blocking": false, + "key": "historyTenantPauseIsolation", + "line": 46 + } + ], + "decisions": [ + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "title": "Operations travel with the capability", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 5, + "enabled": 0 + }, + "examples": { + "total": 5, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-interrupt", + "resolved": true, + "title": "Rebuild a cross-stream history view under a write pause; interrupt, then resume", + "specKind": "example", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.write-pause-rebuild-interrupt.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-abort", + "resolved": true, + "title": "Rebuild a cross-stream history view under a write pause; interrupt, then abort", + "specKind": "example", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.write-pause-rebuild-abort.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-version-change", + "resolved": true, + "title": "Deploy a changed history projection version through the handoff", + "specKind": "example", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.write-pause-rebuild-version-change.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-installation", + "resolved": true, + "title": "Install a history view through the handoff", + "specKind": "example", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.write-pause-rebuild-installation.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-source-order-mismatch", + "resolved": true, + "title": "Reorder a history view's sources while its generation is interrupted", + "specKind": "example", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.write-pause-rebuild-source-order-mismatch.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.write-pause", + "resolved": true, + "title": "Write pause", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "design/specs/application/write-pause.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 14, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-8: the doc names a maintenance gate every writer reads, fencing or draining of background writers, and operator resume and abort; the option taken here is one gate document per deployment holding the closed scopes, each in one `closed` state, with no fence of its own and no drain interval, because optimistic concurrency serializes every writer's gate read against the close and the generation row's fence already fences a rebuild's batches, and a refusal answered with `writePaused`, the transient code the outcome boundary reserves for this contract under its E-35, which this contract confirms; the scope a paused rebuild closes is the paused view's sources, one `source:` entry per context and stream type its bindings name, because a consistent cut must hold every writer of those streams and a use case that writes a source stream without maintaining the view would otherwise pass the gate, which is why the command declaration names what it writes under E-38 and the pipeline proves the list at its step 9; an operator entry closes only a tenant or `all`, the coarser and always safe scopes, and no scope names a read model, because nothing would close it (E-8, E-38, D9, D4, F1)", + "blocking": false, + "key": "writePauseSourceScopes", + "line": 39 + }, + { + "index": 1, + "question": "Extension E-8, the generation exemption: a batch reads the gate with the scopes `all` and its tenant, and no entry of either scope ever names a generation, so the exemption `gateAllows` makes for a given generation is reached by no reader; either the exemption goes, or a history batch reads the gate with its source scopes as well and passes them by it (E-8, D9)", + "blocking": false, + "key": "generationGateExemption", + "line": 40 + }, + { + "index": 2, + "question": "Should its operating instructions distinguish a stopped pass from a verified generation awaiting switch, using `getGenerations` and `getGate`, and measure the closed interval through the successful switch or abort? Keep any acceptable pause length for the owner's decision rather than deriving a timeout from a batch estimate.", + "blocking": false, + "key": "writePauseOperatingInstructions", + "line": 41 + } + ], + "decisions": [ + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "component:platform.gate", + "claim": "anchored", + "label": "the maintenance gate: its table, the writer check, the two writers and the operator entries", + "file": "src/gate/index.ts", + "line": 3, + "layer": "infrastructure", + "context": "platform" + }, + { + "codeId": "impl:application.write-pause", + "claim": "anchored", + "label": "gateAllows, assertWritable and the two scope writers", + "file": "src/gate/gate.ts", + "line": 8, + "role": "service", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + }, + { + "codeId": "impl:application.write-pause.entries", + "claim": "anchored", + "label": "the four gate operator entries", + "file": "src/gate/entries.ts", + "line": 7, + "role": "operator-entry", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + }, + { + "codeId": "impl:application.write-pause.tables", + "claim": "anchored", + "label": "the gate document's table fragment", + "file": "src/gate/tables.ts", + "line": 4, + "role": "schema", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 1, + "enabled": 1 + } + }, + { + "id": "spec:application.generation-registry", + "resolved": true, + "title": "Generation registry", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/application/generation-registry.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 13, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-8: the doc names a generation registry, a marker and a gate without their shapes, and says to keep the old generation for a rollback period without saying whether live commands keep writing it; the option taken here is the `generations` table below with seven states, a rollback period of 7 days, one generation being built per read model at a time, and a retired generation that is kept and not written, so that a rollback reopens it as `verifying`, one verify pass repairs what the writes since its retirement changed, and the ordinary switch makes it active again; a rollback is therefore a verify pass and then a switch of one write, never one write by itself (E-8, D9, Sc L2-5)", + "blocking": false, + "key": "generationRollbackPolicy", + "line": 37 + }, + { + "index": 1, + "question": "Extension E-8, reach of a generation: the doc says nothing about tenants and rebuild; one generation per read model spans every tenant, the registry has no tenant dimension, one row is active for all tenants and the batches iterate the tenant list of `spec:command.actor-and-scope`; the registry is operational data of the deployment, so its indexes lead with the read model name and not with `tenantId`, a confirmed deviation from Law 11's index rule of the same kind the obligation module records under E-57, while every table that holds tenant data, the read-model rows among them, leads with the tenant; a rebuild for one tenant is not offered (E-8, D9, D11, Law 11, E-57)", + "blocking": false, + "key": "generationTenantReach", + "line": 38 + }, + { + "index": 2, + "question": "Extension E-42, as it touches the registry: a restore brings back a generation row and its progress row at their checkpoint and drops the scheduled batch that was driving them, so the row's fence at inventory time is the restore's proof that the generation is still stranded, and `resumeChain` bumping it is the proof that it is not (E-42, F12)", + "blocking": false, + "key": "restoreGenerationFence", + "line": 39 + }, + { + "index": 3, + "question": "Extension E-8, the first generation: the doc says how a generation is rebuilt and not how a read model's first generation comes to exist; the option taken here is that a read model is installed through its first rebuild, generation 1 starting as `building` like any other, so a read model added to a deployment that has history serves that history from its first switch and no transition claims coverage it did not build; a command that finds no generation to write fails as a technical failure and a query that finds no active generation fails the same way (E-8, D9, D19)", + "blocking": false, + "key": "readModelInstallation", + "line": 40 + }, + { + "index": 4, + "question": "Extension E-40, the checkpoint: the cursor, the batch size and the counts live on the progress row and not on the generation row, because a batch that wrote its checkpoint on the row every command reads would re-run every command in flight on the read model once per batch; the progress row is the one checkpoint of every batch whatever drives it (E-40, F1)", + "blocking": false, + "key": "rebuildProgressRow", + "line": 41 + }, + { + "index": 5, + "question": "Extension E-49, as it touches the registry: on a deployment whose grants predate the tenant list the list is empty while grants exist, and a generation started then visits no tenant; the option taken here is that `startGeneration` refuses then, with a message naming `fillTenants`, the operator entry of `spec:application.rebuild` that fills the list from the grants, and refuses while that fill runs (E-49, E-8, Law 11)", + "blocking": false, + "key": "tenantListGenerationStart", + "line": 42 + } + ], + "decisions": [ + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.generation-registry", + "claim": "anchored", + "label": "generationsToWrite and activeGeneration", + "file": "src/read-model/generations.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "codeId": "impl:application.generation-registry.tables", + "claim": "anchored", + "label": "the generations, progress, marker and tenant fill tables and rowConventions", + "file": "src/read-model/tables.ts", + "line": 5, + "role": "schema", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 2, + "enabled": 2 + } + }, + { + "id": "spec:application.orders-inventory-example", + "resolved": true, + "title": "Orders and Inventory, the example domain", + "specKind": "model", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/application/orders-inventory-example.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-46: the second lifecycle command, the essential summary, the command that creates stock, the unit price an order line carries, the bound on an order line's stock item ID, the rejection reasons, the stream budgets of the two stream types, the order of the two context calls and the history-dependent view Sc L2-6 builds are choices the doc leaves open; two of them are decided, that the stock item keeps totals only and `release` takes its quantities from the cancelled order's own lines, and that the order allocation history view stays in the experiment; the owner may substitute the others, and the experiment's terms move with them (E-46, E-2, E-9, First experiment)", + "blocking": false, + "key": "orderInventoryCommandChoices", + "line": 36 + } + ], + "decisions": [ + { + "id": "spec:decisions.d03-events-only-source-of-next-state", + "title": "Events are the only source of the next state", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "title": "Convex covers UI retries, receipts cover the rest", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "title": "Rejections are thrown, not stored, at the public boundary", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "title": "Contexts meet only in parent use cases", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.order-allocation-history", + "claim": "anchored", + "label": "the order allocation history's keyOf and project rules", + "file": "example/domain/orderAllocationHistory.ts", + "line": 8, + "role": "projection", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example", + "claim": "anchored", + "label": "the order decider", + "file": "example/domain/order.ts", + "line": 10, + "role": "decider", + "component": { + "id": "component:example.orders-domain", + "layer": "domain", + "context": "orders" + } + }, + { + "codeId": "impl:application.orders-inventory-example.inventory-operations", + "claim": "anchored", + "label": "the Inventory sanctioned operations", + "file": "example/convex/inventory/operations.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example.inventory-schema", + "claim": "anchored", + "label": "the Inventory context schema", + "file": "example/convex/inventory/schema.ts", + "line": 1, + "role": "schema", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example.inventory-stock-item-query", + "claim": "anchored", + "label": "the stock item query", + "file": "example/convex/inventory/queries/stockItem.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example.inventory-streams", + "claim": "anchored", + "label": "the stock item stream registration", + "file": "example/convex/inventory/streams.ts", + "line": 2, + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example.order-summary", + "claim": "anchored", + "label": "the order summary read model", + "file": "example/convex/orderSummary.ts", + "line": 4, + "role": "read-model", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example.orders-operations", + "claim": "anchored", + "label": "the Orders sanctioned operations", + "file": "example/convex/orders/operations.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "codeId": "impl:application.orders-inventory-example.orders-order-query", + "claim": "anchored", + "label": "the order query", + "file": "example/convex/orders/queries/order.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "codeId": "impl:application.orders-inventory-example.orders-schema", + "claim": "anchored", + "label": "the Orders context schema", + "file": "example/convex/orders/schema.ts", + "line": 1, + "role": "schema", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "codeId": "impl:application.orders-inventory-example.orders-streams", + "claim": "anchored", + "label": "the order stream registration", + "file": "example/convex/orders/streams.ts", + "line": 3, + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "codeId": "impl:application.orders-inventory-example.stock-item", + "claim": "anchored", + "label": "the stock item decider", + "file": "example/domain/stockItem.ts", + "line": 11, + "role": "decider", + "component": { + "id": "component:example.inventory-domain", + "layer": "domain", + "context": "inventory" + } + }, + { + "codeId": "impl:application.parent-use-cases", + "claim": "anchored", + "label": "PlaceOrder and CancelOrder over Orders and then Inventory", + "file": "example/convex/ordering.ts", + "line": 8, + "role": "use-case", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "codeId": "impl:application.parent-use-cases.receiving", + "claim": "anchored", + "label": "ReceiveStock", + "file": "example/convex/receiving.ts", + "line": 4, + "role": "use-case", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + } + ], + "verifiers": { + "total": 6, + "enabled": 6 + }, + "examples": { + "total": 6, + "enabled": 6 + } + }, + { + "id": "spec:command.command-declaration", + "resolved": true, + "title": "Command declaration and bindings", + "specKind": "contract", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/command/command-declaration.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 19, + "declarations": 18, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-7: the doc says a small composition helper and shared schema values; the helper signatures, the executor signature and the read-model binding below are the design's; the cost is two registered mutations per command instead of one, and the cheaper alternative the design refuted is one public export per command plus one generic internal dispatcher mutation keyed by command type, as D7's conditional dispatcher already does; it lost because trusted server callers would then reach every command through one untyped `internalMutation`, so a renamed handler or a broken argument contract would pass `tsc` at the caller and fail only at runtime, which is the check Sc L2-4 exists to keep before traffic, and because the receipt fingerprint's contract version would be looked up by string rather than typed; the owner may still prefer the dispatcher form once code generation makes the per-command internal entry free (E-7, D12, D7, Sc L2-4, Decision method rule 5)", + "blocking": false, + "key": "commandEntrySignatures", + "line": 38 + }, + { + "index": 1, + "question": "Extension E-7: the pipeline receives the declaration and takes the fingerprint's contract version from it, and the type check at a typed caller is the check of Sc L2-4, so no standing map from command type to declaration exists; the conditional dispatcher of `spec:command.outcome-boundary` is the one thing that would need such a map and brings it with it; this is a provisional reading the owner rules (E-7, D12, D18, OQ2)", + "blocking": false, + "key": "commandDeclarationLookup", + "line": 39 + }, + { + "index": 2, + "question": "Extension E-38: the doc names five declaration fields; this design adds `contractVersion` for the fingerprint, `retention` and `irreversible` for receipts, `admission`, `bounds`, `rejections`, `audit`, `diagnosticSink` and `writes`, each serving a cited rule of another Spec, and asks whether the owner accepts them as part of the one declaration; `bounds` is the one input bound in the corpus, `maxItems` and `maxBytes`, checked by the pipeline's step 1 and by no executor body, so `spec:application.parent-use-cases` reads it rather than declaring a bound of its own; `writes` is required on every declaration, because an absent list would be an unchecked one, and `readModels` is optional, absent meaning none, which the owner has not ruled; `writes` names the context and stream type pairs the executor may write, from which the pipeline's step 7 derives the source scopes the write pause closes for a consistent cut and which step 9 proves against the `streams` entries the executor returns, because a use case that writes a source stream without maintaining the paused view would otherwise pass the gate; the declaration still imports nothing from the obligation family, so a command's static reactions are not a field here but the `createObligation` calls of its executor, as `spec:obligations.fan-out-and-chains` rules under E-54 (E-38, E-8, E-54, D6, D9, D10, D19, Thesis)", + "blocking": false, + "key": "commandDeclarationFields", + "line": 40 + } + ], + "decisions": [ + { + "id": "spec:decisions.d12-one-declaration-per-command", + "title": "One declaration per command, generated bindings later", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:command.command-declaration", + "claim": "anchored", + "label": "CommandDeclaration, publicCommand and internalCommand", + "file": "src/command/declaration.ts", + "line": 3, + "role": "contract", + "component": { + "id": "component:platform.command", + "layer": "application", + "context": "platform" + } + }, + { + "codeId": "impl:command.command-declaration.depot-commands", + "claim": "anchored", + "label": "the depot fixture's command declarations", + "file": "fixture/convex/depotCommands.ts", + "line": 3 + } + ], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 2, + "enabled": 2 + } + }, + { + "id": "spec:command.command-pipeline", + "resolved": true, + "title": "The command pipeline", + "specKind": "workflow", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/command/command-pipeline.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 35, + "declarations": 18, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-30: the doc names the outcome, the operation ID and the affected stream versions as what a command returns, but not a wire shape; the shape here is a union discriminated by `replayed`, whose executed member carries the declaration's result and whose replayed member carries `result` null because a receipt stores no result, so the entry's returns validator admits a replay as a value; affected IDs are application IDs, never private document IDs (E-30, D2, D6, D8)", + "blocking": false, + "key": "commandOutcomeWireShape", + "line": 68 + }, + { + "index": 1, + "question": "Extension E-31: the doc says an operation's ID ties together everything one request caused but not how it is minted, and it says a derived command carries causation and an event's `causedBy` may be a triggering event without saying how that cause reaches the pipeline; here the pipeline mints the operation ID once per top-level mutation with `crypto.randomUUID()` before the first context call, a duplicate returns the operation ID stored on the receipt, and the internal entry takes an optional `causedBy` argument that step 7 copies onto the `OperationRef` when present, so a reaction that issues a derived command passes `causedBy` of kind `event` naming its publication event and the envelope's `event` variant has exactly one producer, while an entry called without it mints `causedBy` of kind `command` from the declaration's name; the obligation and attempt behind a derived command are found through its receipt, whose request key is the obligation's effect key, and never through the envelope (E-31, E-54, D2, D13, Vocab: operation)", + "blocking": false, + "key": "operationIdentityCause", + "line": 69 + }, + { + "index": 2, + "question": "Extension E-32: the doc places rate admission for new intent after the duplicate check and makes a rate or capacity refusal a retryable error, but names no admission mechanism; here admission is an optional per-command policy in the declaration, absent by default, evaluated inside the mutation so that a refusal that throws leaves no consumption behind; the flip side is that a consumption the policy writes commits only with the command, so the policy counts committed intent and cannot bound refused or unauthorized traffic, and a product that must bound refused traffic needs the dispatcher form of D7, where the parent survives the body's rejection (E-32, D4, D6, D7)", + "blocking": false, + "key": "commandAdmissionPolicy", + "line": 70 + }, + { + "index": 3, + "question": "Extension E-33: the doc lists the callers that need receipts and leaves UI commands to the client guarantee; here a public caller may still supply a request key to opt into a receipt, which Probe 1 showed a caller of `ConvexHttpClient` needs when it retries, and the internal entry always requires one (E-33, D6, Probe 1)", + "blocking": false, + "key": "publicRequestKey", + "line": 71 + }, + { + "index": 4, + "question": "Probe 4 showed on the pinned backend that the transaction limits are one budget across the parent and its component sub-transactions, so the per-command bounds are checked against one budget; `ctx.runMutation` accepts a `transactionLimits` option, the design passes none on a context call, and whether a call should reserve room with it for the work after it stays open (Probe 4, F13, S3)", + "blocking": false, + "key": "contextCallBudgetReservation", + "line": 72 + }, + { + "index": 5, + "question": "Extension E-9: the doc says essential read models are updated inside the command and does not say from what or in what order; here step 9 writes a per-entity read model from the DTO of each matching `streams` entry, never from its events, and a history read model from the events of every matching entry in one call per command, checks `writes` first, then that each bound read model has a generation to write, then the count of the rows it writes against `limitReadModelWritesPerCommand`, each failure a plain error, and the library helpers that step 9 calls are generic over the application's data model; the owner has not ruled on any of it (E-9, E-7, E-8, E-45, D8, D9)", + "blocking": false, + "key": "commandReadModelWrites", + "line": 73 + }, + { + "index": 6, + "question": "Extension E-34: step 1 bounds every text field a caller sets beside the input, the tenant ID, the request key, the correlation ID, the actor's fields and the fields of `causedBy`, in UTF-8 bytes as `limitCallText` lists them, as rejection `invalidInput`, and treats a declaration name above `limitIdLength` as a defect, a plain error, because these fields become fields of the receipt row or of every event's envelope; the doc says events stay small and names no number; the owner has not ruled (E-34, D6, D12, D19)", + "blocking": false, + "key": "commandTextBounds", + "line": 74 + } + ], + "decisions": [ + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "title": "One mutation per business operation", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "title": "Convex covers UI retries, receipts cover the rest", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "title": "Rejections are thrown, not stored, at the public boundary", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "title": "Contexts meet only in parent use cases", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "title": "Tenant scope and authority from day one", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d12-one-declaration-per-command", + "title": "One declaration per command, generated bindings later", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.read-models", + "claim": "anchored", + "label": "writeReadModels, step 9's read-model half", + "file": "src/read-model/write.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "codeId": "impl:command.command-pipeline", + "claim": "anchored", + "label": "runPipeline and the command response", + "file": "src/command/pipeline.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.command", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 3, + "enabled": 3 + }, + "examples": { + "total": 3, + "enabled": 3 + } + }, + { + "id": "spec:context.event-envelope", + "resolved": true, + "title": "The event envelope", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented" + ], + "file": "design/specs/context/event-envelope.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 16, + "declarations": 10, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-26: the doc lists the fifteen fields and their meaning; this Spec fixes the validator shapes, the `CausedBy` union with a migration variant for baseline events, the `OperationRef` the parent passes, and the event ID as a version 4 UUID from the runtime's `crypto`; the owner confirms (D2, E-26)", + "blocking": false, + "key": "eventEnvelopeFields", + "line": 31 + }, + { + "index": 1, + "question": "Extension E-12: the doc says events stay small and gives no number for the envelope; this Spec bounds the envelope beside the payload at `limitEnvelopeBytes`, 4,096 bytes, and each text field of it that a caller can set at 256 bytes of UTF-8, 512 for an actor's ID, so the largest stored event, `limitEventBytes`, is derived and enforced and never assumed; the owner may set other numbers, and the fold bound of `spec:context.journal` moves with them (E-12, D19, F13)", + "blocking": false, + "key": "envelopeTextBounds", + "line": 32 + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "title": "Each bounded context owns its state and its journal", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:context.event-envelope", + "claim": "anchored", + "label": "the envelope types and validators", + "file": "src/context/envelope.ts", + "line": 2, + "role": "contract", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:context.journal", + "resolved": true, + "title": "The journal", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/context/journal.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 39, + "declarations": 18, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-25: the doc says a migration writes a baseline event holding the migrated state and that rebuild starts from the latest baseline, and it does not say who computes that state, who drives the migration, or what a live command does on a stream the migration has not reached; this Spec fixes the representation, an event of the reserved type `baseline` whose payload holds the state and its schema version and is bounded by the stream's `budgetBytes` instead of the payload bound, a `baselineVersion` on the stream row, a `BaselineMigration` registered on the stream type whose `migrate` function computes the migrated state from the saved state, or, for a step that declares `readsHistory`, from the saved state and the events since the last baseline in the sweep only, the rule that `stateSchemaVersion` counts meaning only and lives on the registration, one helper `migrateToSchemaVersion` that writes the baseline chain, called by the adapter on load for a chain that reads no history and by the `writeBaseline` maintenance mutation for every chain, the code-older-than-data error, the `writeBaseline` mutation built by `defineMaintenance` and declared with the component's public `mutation` builder, the parent-side scheduled chain that drives it by cursor, the batch bound derived from `budgetBytes`, the upcasting rule for old event schema versions, the fold bound of 500 events with the refusals `rebuildStream` returns above it and for a stream awaiting its baseline, the retirement rule for a migration, and the payload byte check in `append`; the option of refusing commands on unmigrated streams with a transient code until the sweep arrives was not taken for a state-only migration, because it turns a deploy into a partial write pause the doc reserves for history-dependent views, and is taken for a history-reading migration, the exception, where it holds only the streams the sweep has not reached and keeps D3 whole on the command path, which the owner confirmed; a consequence the owner should weigh is that every baseline bumps its stream's version, so a reviewed version held across the sweep is answered `staleVersion` and re-reviewed rather than silently accepted, because Sc L1-10 forbids reinterpreting a reviewed version against fresh state and the baseline is a real event of the stream, while a live stream migrated on load never trips it because its expected-version check runs before the chain; and the driver reads the write pause's gate before every batch and reschedules itself when refused, so a sweep never changes a source under a paused rebuild's cut; the owner confirms (D5, D9, S4, Sc L1-10, E-2, E-8, E-12, E-25)", + "blocking": false, + "key": "baselineMigrationContract", + "line": 51 + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "title": "Each bounded context owns its state and its journal", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "title": "Rebuildable history by default, with baseline events", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:kernel.state-document-mapping", + "title": "One document per stream, with a derived mapping above the budget", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:context.journal", + "claim": "anchored", + "label": "createJournal, load and append", + "file": "src/context/journal.ts", + "line": 2, + "role": "event-store", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 0 + }, + "examples": { + "total": 1, + "enabled": 0 + } + }, + { + "id": "spec:context.queries", + "resolved": true, + "title": "The context queries", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/context/queries.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 29, + "declarations": 18, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Probe 5 showed on the pinned backend with `convex-helpers` 0.1.124 that the parent query stays reactive, that pages pinned by their end cursors stay contiguous, that a capped page crosses the boundary as `SplitRequired` with its `splitCursor`, and that the `usePaginatedQuery` hook of `convex-helpers/react` then shows 40 of 45 rows; this design uses no hook across the boundary, and the examples are rerun when either pin moves (Probe 5, F15, S4, D8, E-24)", + "blocking": false, + "key": "contextPaginationSplit", + "line": 41 + }, + { + "index": 1, + "question": "Extension E-24: the doc says component queries are authorized and return DTOs; this Spec fixes one definer per query, the one `tenantId` argument with no separate scope, the `toDto` mapping with the stream version on every DTO, pagination through `paginator`, the item cap with a row cap of twice the item cap and a byte cap passed on every run, the relay of `pageStatus` and `splitCursor` unchanged, paging by an explicit cursor pair with no client hook, the parent query's one `authorizeQuery` call and its refusal as a rejection, the `maxEvents` argument of the `rebuild` query and its `budgetExhausted` refusal for a batching caller, and the fold refusal of the `rebuild` query; this is a provisional reading the owner has not ruled (D8, S4, E-24, E-42)", + "blocking": false, + "key": "contextQueryContract", + "line": 42 + }, + { + "index": 2, + "question": "A parent query's refusal carries the name of the entry that refused in the field `entry` of the rejection data; the wire shape stays `RejectionData` of `spec:command.outcome-boundary` unchanged, and the owner confirmed the word (D7, E-24)", + "blocking": false, + "key": "queryRefusalEntry", + "line": 43 + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:context.queries", + "claim": "anchored", + "label": "defineGet, defineList and boundedPage", + "file": "src/context/queries.ts", + "line": 3, + "role": "query", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "resolved": true, + "title": "Rebuild online by default, pause writes only for history-dependent views", + "specKind": "decision", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/decisions/d09-rebuild-online-by-default.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Probe 6 ran a backfill batch racing a live command and the migrations component on the native backend, with its results on F17; a hosted deployment and sustained contention are not run (Probe 6, F1, F17, D9)", + "blocking": false, + "key": "rebuildContentionCoverage", + "line": 30 + } + ], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + } + ], + "verifierGaps": [ + { + "id": "spec:application.history-projection", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.projection-contract", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-interrupt", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-abort", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-version-change", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-installation", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-source-order-mismatch", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:context.event-envelope", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:context.journal", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:context.queries", + "statedReadiness": "defined", + "priority": false + } + ], + "boundary": { + "restsOn": [ + { + "id": "spec:application.first-experiment", + "title": "The first experiment", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:command.actor-and-scope", + "title": "Actor, tenant scope, namespace and grants", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.idempotency-and-receipts", + "title": "Idempotency and receipts", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.outcome-boundary", + "title": "The outcome and rejection boundary", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.receipt-table", + "title": "The receipts table", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.tenancy-and-authority", + "title": "Tenancy and authority", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:constraints.bulk-operations-bounded", + "title": "Every bulk operation has a stated bound", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:constraints.events-stay-small", + "title": "Events stay small", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:constraints.no-queue-recovery-for-essential-reads", + "title": "No queue recovery needed for essential reads", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:constraints.zero-core-projection-jobs", + "title": "Zero core projection jobs", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:context.batch-shaped-api", + "title": "Batch-shaped context APIs", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:context.context-component", + "title": "The context component", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:context.journal", + "spec:context.queries" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.persistence-adapter", + "title": "The persistence adapter", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.tables", + "title": "The context tables", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "title": "One mutation per business operation", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.read-models", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "title": "Each bounded context owns its state and its journal", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d03-events-only-source-of-next-state", + "title": "Events are the only source of the next state", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "title": "Rebuildable history by default, with baseline events", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "title": "Convex covers UI retries, receipts cover the rest", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "title": "Rejections are thrown, not stored, at the public boundary", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "title": "Contexts meet only in parent use cases", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "title": "Tenant scope and authority from day one", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d12-one-declaration-per-command", + "title": "One declaration per command, generated bindings later", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "title": "Everything else waits for its trigger", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection" + ] + } + ] + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "title": "Operations travel with the capability", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.rebuild", + "spec:application.history-rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f01-serializable-mutations-under-occ", + "title": "Mutations are serializable under optimistic concurrency", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:facts.f02-component-calls-commit-with-caller", + "title": "Component calls commit or roll back with the calling mutation", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f03-nested-run-mutation-partial-rollback", + "title": "A nested runMutation gives partial rollback", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:facts.f04-nested-calls-cost-more-than-helpers", + "title": "Nested calls cost more than helper calls", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:facts.f07-queries-reactive-not-durable-delivery", + "title": "Queries are reactive, subscriptions are not durable delivery", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f08-scheduling-commits-with-mutation", + "title": "Scheduling from a mutation commits with it", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "title": "Scheduled mutations run once, scheduled actions are not retried", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f11-components-have-no-ctx-auth", + "title": "Components have no ctx.auth", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.write-pause", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f12-backups-exclude-pending-scheduled-functions", + "title": "Backups exclude pending scheduled functions", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:facts.f13-transactions-have-limits", + "title": "Transactions have limits", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f14-convex-error-survives-nested-and-component-boundary", + "title": "ConvexError data survives a nested mutation and a component boundary", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:facts.f15-parent-query-over-component-query-stays-reactive", + "title": "A parent query over a component query stays reactive", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f17-migrations-fits-generation-backfill", + "title": "The migrations component fits a generation backfill", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.generation-registry", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:facts.f18-commit-timestamps", + "title": "Commit timestamps order committed transactions", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f19-nested-calls-share-time-budgets", + "title": "Nested calls draw on a system-operation time budget", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f20-pagination-checks-bytes-after-reading", + "title": "Pagination checks bytes after reading", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:kernel.outcome-model", + "title": "The outcome model", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:kernel.state-document-mapping", + "title": "One document per stream, with a derived mapping above the budget", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law01-sanctioned-writes-only", + "title": "Business writes go only through sanctioned operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law02-state-and-events-commit-together", + "title": "State and its required events commit together", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law03-events-only-source-of-state", + "title": "Events are the only source of the next state", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law04-server-scoped-idempotency-key", + "title": "A retryable command carries a server-scoped idempotency key", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law05-authorization-before-execution-and-disclosure", + "title": "Authorization comes before execution and disclosure", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:laws.law06-technical-failure-never-a-rejection", + "title": "A technical failure is never stored as a rejection", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law09-no-invariant-on-late-read-model", + "title": "No invariant depends on a late read model", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law10-replay-never-runs-commands-or-effects", + "title": "Replay never runs commands or repeats effects", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law11-tenant-scope-named", + "title": "Every command and query names its tenant scope", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:operations.baseline-operations", + "title": "Baseline operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:platform.transactional-domain-platform", + "title": "Convex transactional domain platform", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + } + ], + "restedOnBy": [ + { + "id": "spec:advanced.cross-stream-online-rebuild", + "title": "Online rebuild of cross-stream history views", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:agents.agent-runs", + "title": "Agent runs", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:application.aggregate-projection", + "title": "Aggregate projection", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:application.projection-contract", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:application.first-experiment", + "title": "The first experiment", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.history-projection", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.command-without-generation-fails", + "title": "A command whose read model has no generation fails", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.first-rebuild-installs-read-model", + "title": "A read model is installed through its first rebuild", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-duplicate-answered-from-receipt", + "title": "A repeated cancel request is answered from its receipt", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-placed-order", + "title": "A placed order is cancelled", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-second-context-rejects", + "title": "Orders cancels, then Inventory refuses the release", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-twice-rejected", + "title": "A second cancel of one order is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-unknown-order-rejected", + "title": "A cancel of an order never placed is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-without-grant-refused", + "title": "A caller without the cancel grant is refused", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.parent-use-cases", + "title": "Parent use cases", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-read-through-query", + "title": "A successful command, then a query", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-visible", + "title": "A successful command, then a query or subscription", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.list-pages-by-cursor", + "title": "A client reads a tenant's order list in pages by cursor", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-before-disclosure", + "title": "A caller with no identity reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-without-grant", + "title": "A caller with no grant in the tenant reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.rebuild.online-rebuild-interrupt-resume", + "title": "Rebuild a per-entity read model online while commands run; interrupt and resume", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:application.restore", + "title": "Restore", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.projection-contract", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:context.event-envelope", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:application.write-pause.restore-door-without-deploy", + "title": "The restore door closes and opens with no deploy", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.broken-argument-contract-fails-build", + "title": "Break a typed handler's argument contract", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.renamed-handler-fails-build", + "title": "Rename a typed handler", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-journal-append", + "title": "Inject a failure after the journal append", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-state-write", + "title": "Inject a failure after the state write", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-before-receipt", + "title": "Inject a failure before the receipt insert", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.idempotency-and-receipts", + "title": "Idempotency and receipts", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.outcome-boundary", + "title": "The outcome and rejection boundary", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.receipt-table", + "title": "The receipts table", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.tenancy-and-authority", + "title": "Tenancy and authority", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:context.context-component", + "title": "The context component", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:context.journal.rebuild-from-baseline", + "title": "Rebuild a stream that has a baseline event", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:context.persistence-adapter", + "title": "The persistence adapter", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:context.tables", + "title": "The context tables", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "title": "Everything else waits for its trigger", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:obligations.fan-out-and-chains", + "title": "Fan-out and reaction chains", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:obligations.local-reaction-wrapper", + "title": "Local reaction wrapper", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:obligations.obligation-module", + "title": "Obligation module", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:operations.baseline-operations", + "title": "Baseline operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + } + ], + "verifiedBy": [ + { + "id": "spec:application.generation-registry.command-without-generation-fails", + "title": "A command whose read model has no generation fails", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.first-rebuild-installs-read-model", + "title": "A read model is installed through its first rebuild", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-duplicate-answered-from-receipt", + "title": "A repeated cancel request is answered from its receipt", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-placed-order", + "title": "A placed order is cancelled", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-second-context-rejects", + "title": "Orders cancels, then Inventory refuses the release", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-twice-rejected", + "title": "A second cancel of one order is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-unknown-order-rejected", + "title": "A cancel of an order never placed is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-without-grant-refused", + "title": "A caller without the cancel grant is refused", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-read-through-query", + "title": "A successful command, then a query", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-visible", + "title": "A successful command, then a query or subscription", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.list-pages-by-cursor", + "title": "A client reads a tenant's order list in pages by cursor", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-before-disclosure", + "title": "A caller with no identity reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-without-grant", + "title": "A caller with no grant in the tenant reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.rebuild.online-rebuild-interrupt-resume", + "title": "Rebuild a per-entity read model online while commands run; interrupt and resume", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:application.write-pause.restore-door-without-deploy", + "title": "The restore door closes and opens with no deploy", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.broken-argument-contract-fails-build", + "title": "Break a typed handler's argument contract", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.renamed-handler-fails-build", + "title": "Rename a typed handler", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-journal-append", + "title": "Inject a failure after the journal append", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-state-write", + "title": "Inject a failure after the state write", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-before-receipt", + "title": "Inject a failure before the receipt insert", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.journal.rebuild-from-baseline", + "title": "Rebuild a stream that has a baseline event", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:context.journal" + ] + } + ] + } + ], + "supersededBy": [] + }, + "findings": [] +} +}; diff --git a/explorations/studio-prototype/data/history-view.pack-context.json b/explorations/studio-prototype/data/history-view.pack-context.json new file mode 100644 index 00000000..65971320 --- /dev/null +++ b/explorations/studio-prototype/data/history-view.pack-context.json @@ -0,0 +1,3513 @@ +{ + "id": "pack:history-view", + "title": "History view", + "framing": "The Specs a history view rests on, in reading order. A history view is a read model whose row folds the events of several streams, such as the order allocation history of the example domain. The history projection comes first because it pins the form, its fold and that view; the projection contract and the read models follow with the row conventions and the read-model rules every view shares. The rebuild, the history rebuild with its five examples, the write pause and the generation registry say how such a view comes to exist and is rebuilt while the commands that write its sources are refused. The rebuild comes first because it holds the operator entries and the batch chain both paths share; the history rebuild holds the paused path alone and the five examples verify it, so the rebuild's implementation binding claims the online path and the history rebuild shows the paused path designed apart from it. The example domain names the events the view folds, and the command declaration and the command pipeline bind the view to the commands that feed it and write it inside each command. The event envelope, the journal and the queries are where the events come from on the live path and on the backfill, and D9 is the decision that a read model which depends on history across several streams rebuilds under a write pause.", + "file": "design/specs/history-view.pack.sdp.md", + "modelRefs": [ + "spec:platform.vocabulary" + ], + "members": [ + { + "id": "spec:application.history-projection", + "resolved": true, + "title": "History projection", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-projection.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 10, + "declarations": 8, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-9, the history fold: D2 orders events only within a stream, and the two paths feed one history row in different orders, the live path command by command and the backfill stream by stream; the option taken here is a fold that gives one row whatever the order across streams and however its events are split between calls, a row no fold deletes, which keeps its subject's ended state, and baselines dropped before `keyOf`, so a view whose meaning needs the order across streams waits for the ordered consumer of D18 (E-9, D2, D9, D18, E-25)", + "blocking": false, + "key": "historyFoldOrder", + "line": 37 + }, + { + "index": 1, + "question": "The cost of the binding on the live path, one row read and one write of up to `rowBudgetBytes` per command and the measure of the row, is taken by the first experiment's latency cell with the order allocation history bound, for `PlaceOrder` and for `CancelOrder` of an order of the maximum lines, before the view is declared essential anywhere (E-9, E-47, F13)", + "blocking": false, + "key": "historyProjectionCost", + "line": 38 + }, + { + "index": 2, + "question": "Extension E-9, the check of the history fold: the order independence the Contract requires is checked for the order allocation history alone; the option taken here is that every history projection of an application carries a pure test that folds a sample of one key's events in every interleaving of its streams and every split into calls, and a library helper that runs that test is added with a second history view (E-9, D2, D18)", + "blocking": false, + "key": "historyFoldPureTest", + "line": 39 + }, + { + "index": 3, + "question": "A history row keeps a deleted subject's history and every rebuild writes it again from the journal; whether a history view falls under the retention and deletion policy of D19, and what its rebuild does after an erasure, is answered by that policy (D19, D9, E-9)", + "blocking": false, + "key": "historySubjectErasure", + "line": 40 + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "title": "Each bounded context owns its state and its journal", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "title": "Rebuildable history by default, with baseline events", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "title": "Everything else waits for its trigger", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "title": "Operations travel with the capability", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.order-allocation-history", + "claim": "anchored", + "label": "the order allocation history's keyOf and project rules", + "file": "example/domain/orderAllocationHistory.ts", + "line": 8, + "role": "projection", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.projection-contract", + "resolved": true, + "title": "Projection contract", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/projection-contract.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 21, + "declarations": 11, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-9: the doc says a projection is named, versioned and deterministic and that live update and rebuild share its logic; it does not give the signature or the row shape, for counts and sums it names the marker but not the aggregate row, and for the read models that depend on history across several streams, the ones the write pause exists for, it names no input. The option taken here: a per-entity read model declared as a `ReadModel<D, Row>` with `name`, `table`, `rowBudgetBytes` and its `projections`, a list of `Projection<D, Row>` with the latest first, each holding `version`, `keyOf` and `project` alone, rows with `tenantId`, `generation`, `key`, `projectionVersion` and `sourceVersions`, one `applyProjection` helper with a mode per caller that writes each generation by its role, active, building or verifying, with the projection whose version the generation records, for the aggregate form the row and the writer `spec:application.aggregate-projection` pins, and for a history-dependent view a `HistoryProjection<Row>` that folds events into the prior row, fed on the live path by the events the operation appended, which the adapter's `streams` entry carries, and on the backfill by the context's `history` pages under the write pause, with `sourceVersions` recording the highest version folded per stream so that an event is never folded twice; a row that derives from more than one source stream type is a history row, its projection bound once per source stream type, and a small cross-context view of current state is D8's parent query over bounded component reads; the owner confirms the three forms and has not ruled on the split of a per-entity read model into `ReadModel` and `Projection`, on the write by generation role or on the row measured against its budget at every write (E-9, E-23, E-24, D8, D9)", + "blocking": false, + "key": "projectionRowContract", + "line": 33 + }, + { + "index": 1, + "question": "Extension E-9, two projection versions: the doc says live update and rebuild share one projection and does not say how a command writes two generations built by two versions; the option taken here is that a read model declares its projection versions as a list, the latest first, and `applyProjection` writes each generation with the projection whose version the generation row records, failing as a technical failure when the read model declares none of that version, so the rows of one generation are one truth of one version; the cost is that an earlier version stays in the code while a generation that records it is written or can be rolled back to, and a projection changed without a version change is still undetected (E-9, D8, D9)", + "blocking": false, + "key": "projectionGenerationVersions", + "line": 34 + }, + { + "index": 2, + "question": "The rule that a row with a newer version is never overwritten is read as the backfill path's rule only, so a live write to the active generation always lands, as `step3` has it; the owner has not confirmed that reading (D9, E-9)", + "blocking": false, + "key": "backfillVersionOverwrite", + "line": 35 + }, + { + "index": 3, + "question": "Extension E-9, a composite row of current state: D8 lets a small cross-context view be a read model updated in the command, and no form here writes one row from the DTOs of two sources, because `project` builds a whole row from one DTO and a backfill enumerates one source; the option taken here is D8's other default, a parent query over bounded component reads, until a view needs such a row, which then needs a form whose `project` takes the DTO of every matching entry of one command and whose backfill reads the other context in the same mutation, online (E-9, D8, E-45)", + "blocking": false, + "key": "compositeCurrentStateRow", + "line": 36 + }, + { + "index": 4, + "question": "Extension E-9, the cost of `sourceVersions`: a history row's entries repeat the row's tenant ID once per source stream, about three quarters of the largest order allocation history row by `spec:application.history-projection#design.tableOrderAllocationHistory`; whether a row's `sourceVersions` may omit `tenantId`, equal to the row's on every entry, and the write rule compare three identity fields, is a choice the owner may take with the pinned conventions (E-9, Law 11, F13)", + "blocking": false, + "key": "historySourceVersionTenant", + "line": 37 + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.generation-registry.tables", + "claim": "anchored", + "label": "the generations, progress, marker and tenant fill tables and rowConventions", + "file": "src/read-model/tables.ts", + "line": 5, + "role": "schema", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "codeId": "impl:application.projection-contract", + "claim": "anchored", + "label": "ReadModel, Projection and applyProjection", + "file": "src/read-model/projection.ts", + "line": 3, + "role": "projection", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.read-models", + "resolved": true, + "title": "Read models", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/application/read-models.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 11, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Probe 5 showed that the built-in `.paginate` treats `maximumRowsRead` as advice once a page has an end cursor and that a cursor it issued is refused by any other query, so a subscription over the active generation threw `InvalidCursor` at a switch; a parent list therefore pages with `paginator` of `convex-helpers`, as a context list does, and a pinned page is bounded on every run by the row cap and by the byte cap as `spec:context.queries` states it, the helper stopping at the row that reaches it and admitting that row whole, and answers `SplitRequired` beyond them (Probe 5, F13, D8, D9)", + "blocking": false, + "key": "parentListGenerationSwitch", + "line": 49 + }, + { + "index": 1, + "question": "Extension E-24, as it reaches read models: a list over a parent read-model table calls `paginator` of `convex-helpers` with the options of `boundedPage`, a row cap of twice the item cap, with each cursor of the client's pair moved into the active generation by `pageInGeneration`, and returns through `paginationResultValidator` with its rows mapped by `readModelView`, and a client pages every list by an explicit cursor pair with no hook; these are options taken here and the owner has not ruled (E-24, S4, Probe 5, D8)", + "blocking": false, + "key": "readModelListBounds", + "line": 50 + }, + { + "index": 2, + "question": "Extension E-6, as it reaches queries: a parent query establishes and authorizes its actor through `authorizeQuery`, and its refusal is the command's rejection shape, `RejectionData` with the query's name in the field `entry` and the codes `unauthenticated` or `forbidden`; the owner confirmed the word, and nothing else of the shape is new (E-6, D7, D11, Law 5)", + "blocking": false, + "key": "parentQueryRefusal", + "line": 51 + }, + { + "index": 3, + "question": "Probe 3 pending: the cost of one component call in latency and function-call quota decides whether one entity's detail stays a component query or hot reads move, which is OQ1 (Probe 3, F4, OQ1)", + "blocking": false, + "key": "contextQueryCost", + "line": 52 + }, + { + "index": 4, + "question": "Extension E-45: the doc says a small cross-context view is a parent query over bounded component reads and does not give the bound; the option taken here is at most 8 component reads per parent query and at most 4 read-model rows written per command, both measured by the first experiment (E-45, D8)", + "blocking": false, + "key": "parentQueryReadBounds", + "line": 53 + }, + { + "index": 5, + "question": "Extension E-9, as it reaches read models: D8 lets a small cross-context view be a parent query over bounded component reads or a read model updated in the command; the option taken here is the parent query for a view of current state, because no form of `spec:application.projection-contract` writes one row from the DTOs of two sources, and the history projection for a view that folds the events of several streams; a composite row of current state is a form for the view that needs it (E-9, E-45, D8)", + "blocking": false, + "key": "crossContextReadForm", + "line": 54 + } + ], + "decisions": [ + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "title": "One mutation per business operation", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "title": "Tenant scope and authority from day one", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.read-models", + "claim": "anchored", + "label": "writeReadModels, step 9's read-model half", + "file": "src/read-model/write.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "codeId": "impl:application.read-models.queries", + "claim": "anchored", + "label": "readModelView, pageInGeneration and limitReadModelList", + "file": "src/read-model/queries.ts", + "line": 3, + "role": "query", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 5, + "enabled": 5 + }, + "examples": { + "total": 5, + "enabled": 5 + } + }, + { + "id": "spec:application.rebuild", + "resolved": true, + "title": "Rebuild: generations, backfill and the write pause", + "specKind": "workflow", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/application/rebuild.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 42, + "declarations": 27, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "`@convex-dev/migrations` walks only the tables of the component it is defined in, reaches a context's rows only through a parent internal mutation written by hand against a contract its README does not document and the compiled tier needs a cast for, keeps a status apart from the generation's state, and leaves no error on its row when a batch fails past the read limit, so every batch is driven by the rebuild's own self-scheduled internal mutation and the progress row is the one checkpoint; a hosted deployment and sustained contention are not observed (Probe 6, F1, F17, D9)", + "blocking": false, + "key": "rebuildBatchDriver", + "line": 59 + }, + { + "index": 1, + "question": "Extension E-40: the doc binds resumable batching to `@convex-dev/migrations` and gives no batch size; the component iterates a parent table while the sources of a read model live in a context component. The option taken here: a batch is one mutation with one cursor on the progress row and a bound of at most 100 entities, capped by the read model's declared `rowBudgetBytes` so that the parent's point reads and writes stay under 4 MiB, and by the context's byte-derived `list` page bound, which is 16 items at the default 256 KiB stream budget; every batch, the backfill and the verify pass over a context's enumeration and the purge over a parent table, is driven by the platform's own self-scheduled internal mutation, and the rebuild uses the component for nothing, because Probe 6 showed that it walks only the tables of the component it is defined in and drives a context's rows only through a parent internal mutation written by hand against a contract its README does not document; the progress row is the one checkpoint (E-40, E-24, E-9, F17, F13, F1)", + "blocking": false, + "key": "rebuildBatchBounds", + "line": 60 + }, + { + "index": 2, + "question": "Extension E-8: the registry states, the rollback period, what a rollback does, the gate document, the scope a paused rebuild closes and the reach of a generation are design choices the doc does not make; they are pinned in the two contracts under this workflow (E-8, E-9, E-38, D9)", + "blocking": false, + "key": "rebuildGenerationPauseContract", + "line": 61 + }, + { + "index": 3, + "question": "Extension E-8, what this workflow does not offer: the online path is pinned for a per-entity read model with one source stream type and the paused path for a history view, a `HistoryRebuildTarget` with its sources, in `spec:application.history-rebuild`; the aggregate form's rebuild is deferred with the form in its own Spec, the aggregate projection, and a row made from the current state of more than one source has no form at all, as `spec:application.projection-contract` records; `rollbackGeneration` refuses a generation that needs a write pause, because nothing would hold its sources while live commands fold onto a row that missed the events since its retirement, so such a view returns to earlier rules only through a new generation under the pause (E-8, E-9, D9, D18)", + "blocking": false, + "key": "rebuildReadModelForms", + "line": 62 + }, + { + "index": 4, + "question": "Extension E-49, a deployment whose grants predate the tenant list: the tenant list is written by `insertGrant`, so on such a deployment it holds no row while `grants` holds many, and a pass over an empty list visits no tenant and still ends; the option taken here is that `startGeneration` refuses an empty list beside a grant with a message naming `fillTenants`, and `fillTenants`, an operator entry, walks the grants in bounded batches through a progress row of its own and inserts each missing tenant once; a list that holds a tenant whose first grant came after the table and misses an older one is not detected by that refusal, so the operator runs `fillTenants` on such a deployment before its first `startGeneration` (E-49, E-48, E-40, Law 11)", + "blocking": false, + "key": "tenantListFill", + "line": 63 + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "title": "Tenant scope and authority from day one", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "title": "Operations travel with the capability", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.rebuild", + "claim": "anchored", + "label": "the online rebuild's operator entries, batch chains, tenant fill and batch bounds", + "file": "src/read-model/rebuild.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 1, + "enabled": 1 + } + }, + { + "id": "spec:application.history-rebuild", + "resolved": true, + "title": "Rebuild of a history view under a write pause", + "specKind": "workflow", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 12, + "declarations": 3, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-40, the shape of a history batch: a batch takes one stream, so a pass costs at least one scheduled mutation per source stream and the pause lasts about twice the stream count times one batch; the alternative is a batch that takes streams from one `list` page until its event budget is spent, one `history` call per stream, with the byte bound counting one baseline per stream; the first paused rebuild on the native backend measures one batch, and the shape is chosen with that number (E-40, E-9, F13, F19, D9)", + "blocking": false, + "key": "historyBatchShape", + "line": 41 + }, + { + "index": 1, + "question": "The verify pass under the write pause: a history view's generation is built while no source changes, so its verify pass repeats the backfill's batches and, by `spec:application.history-rebuild#design.historyVerify`, finds nothing over a covered generation, which doubles the pause; the option kept here is the pass, as D9's \"Verify coverage, then switch\" and Sc L2-6's \"writes reopen only after verification\" read, and whether a paused generation is `verified` when its backfill ends is the owner's (D9, Sc L2-6)", + "blocking": false, + "key": "verifyPassUnderPause", + "line": 42 + }, + { + "index": 2, + "question": "Whether a history view's batch chain, at up to 2 MiB of rows per batch scheduled with `runAfter(0)`, is held by the deployment class's write throughput limit, `spec:facts.f13-transactions-have-limits#design.limitWriteThroughput`, which the first paused rebuild on the native backend and one hosted run show (F13, E-40)", + "blocking": false, + "key": "historyBatchWriteThroughput", + "line": 43 + }, + { + "index": 3, + "question": "Must every source's migration that reads history finish before `startGeneration` closes its source scopes, with abort, migration completion and a fresh generation as the recovery if it did not? If enumeration must instead remain possible during that migration, which context query provides it without constructing a DTO of the wrong meaning?", + "blocking": false, + "key": "historyMigrationBeforePause", + "line": 44 + }, + { + "index": 4, + "question": "Should the native-backend abort example inject failure in the second source's audit insert, assert unchanged generation, progress, gate entries and audit records, then retry after removing the fault and assert one successful abort with both sources reopened? The same atomicity check should cover the switch that reopens them.", + "blocking": false, + "key": "abortSwitchAtomicity", + "line": 45 + }, + { + "index": 5, + "question": "Should the native-backend example use two tenants with equal local order and stock item IDs, check their folds separately, and refuse a command that writes a paused source without a history binding in both tenants, while a command writing only an unrelated source remains allowed? Include a retry of resume after its response is lost so the same saved event page cannot change the fold twice.", + "blocking": false, + "key": "historyTenantPauseIsolation", + "line": 46 + } + ], + "decisions": [ + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "title": "Operations travel with the capability", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 5, + "enabled": 0 + }, + "examples": { + "total": 5, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-interrupt", + "resolved": true, + "title": "Rebuild a cross-stream history view under a write pause; interrupt, then resume", + "specKind": "example", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.write-pause-rebuild-interrupt.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-abort", + "resolved": true, + "title": "Rebuild a cross-stream history view under a write pause; interrupt, then abort", + "specKind": "example", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.write-pause-rebuild-abort.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-version-change", + "resolved": true, + "title": "Deploy a changed history projection version through the handoff", + "specKind": "example", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.write-pause-rebuild-version-change.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-installation", + "resolved": true, + "title": "Install a history view through the handoff", + "specKind": "example", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.write-pause-rebuild-installation.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-source-order-mismatch", + "resolved": true, + "title": "Reorder a history view's sources while its generation is interrupted", + "specKind": "example", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/application/history-rebuild.write-pause-rebuild-source-order-mismatch.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.write-pause", + "resolved": true, + "title": "Write pause", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented", + "has-verifier" + ], + "file": "design/specs/application/write-pause.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 14, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-8: the doc names a maintenance gate every writer reads, fencing or draining of background writers, and operator resume and abort; the option taken here is one gate document per deployment holding the closed scopes, each in one `closed` state, with no fence of its own and no drain interval, because optimistic concurrency serializes every writer's gate read against the close and the generation row's fence already fences a rebuild's batches, and a refusal answered with `writePaused`, the transient code the outcome boundary reserves for this contract under its E-35, which this contract confirms; the scope a paused rebuild closes is the paused view's sources, one `source:` entry per context and stream type its bindings name, because a consistent cut must hold every writer of those streams and a use case that writes a source stream without maintaining the view would otherwise pass the gate, which is why the command declaration names what it writes under E-38 and the pipeline proves the list at its step 9; an operator entry closes only a tenant or `all`, the coarser and always safe scopes, and no scope names a read model, because nothing would close it (E-8, E-38, D9, D4, F1)", + "blocking": false, + "key": "writePauseSourceScopes", + "line": 39 + }, + { + "index": 1, + "question": "Extension E-8, the generation exemption: a batch reads the gate with the scopes `all` and its tenant, and no entry of either scope ever names a generation, so the exemption `gateAllows` makes for a given generation is reached by no reader; either the exemption goes, or a history batch reads the gate with its source scopes as well and passes them by it (E-8, D9)", + "blocking": false, + "key": "generationGateExemption", + "line": 40 + }, + { + "index": 2, + "question": "Should its operating instructions distinguish a stopped pass from a verified generation awaiting switch, using `getGenerations` and `getGate`, and measure the closed interval through the successful switch or abort? Keep any acceptable pause length for the owner's decision rather than deriving a timeout from a batch estimate.", + "blocking": false, + "key": "writePauseOperatingInstructions", + "line": 41 + } + ], + "decisions": [ + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "component:platform.gate", + "claim": "anchored", + "label": "the maintenance gate: its table, the writer check, the two writers and the operator entries", + "file": "src/gate/index.ts", + "line": 3, + "layer": "infrastructure", + "context": "platform" + }, + { + "codeId": "impl:application.write-pause", + "claim": "anchored", + "label": "gateAllows, assertWritable and the two scope writers", + "file": "src/gate/gate.ts", + "line": 8, + "role": "service", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + }, + { + "codeId": "impl:application.write-pause.entries", + "claim": "anchored", + "label": "the four gate operator entries", + "file": "src/gate/entries.ts", + "line": 7, + "role": "operator-entry", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + }, + { + "codeId": "impl:application.write-pause.tables", + "claim": "anchored", + "label": "the gate document's table fragment", + "file": "src/gate/tables.ts", + "line": 4, + "role": "schema", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + } + ], + "references": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 1, + "enabled": 1 + } + }, + { + "id": "spec:application.generation-registry", + "resolved": true, + "title": "Generation registry", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/application/generation-registry.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 13, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-8: the doc names a generation registry, a marker and a gate without their shapes, and says to keep the old generation for a rollback period without saying whether live commands keep writing it; the option taken here is the `generations` table below with seven states, a rollback period of 7 days, one generation being built per read model at a time, and a retired generation that is kept and not written, so that a rollback reopens it as `verifying`, one verify pass repairs what the writes since its retirement changed, and the ordinary switch makes it active again; a rollback is therefore a verify pass and then a switch of one write, never one write by itself (E-8, D9, Sc L2-5)", + "blocking": false, + "key": "generationRollbackPolicy", + "line": 37 + }, + { + "index": 1, + "question": "Extension E-8, reach of a generation: the doc says nothing about tenants and rebuild; one generation per read model spans every tenant, the registry has no tenant dimension, one row is active for all tenants and the batches iterate the tenant list of `spec:command.actor-and-scope`; the registry is operational data of the deployment, so its indexes lead with the read model name and not with `tenantId`, a confirmed deviation from Law 11's index rule of the same kind the obligation module records under E-57, while every table that holds tenant data, the read-model rows among them, leads with the tenant; a rebuild for one tenant is not offered (E-8, D9, D11, Law 11, E-57)", + "blocking": false, + "key": "generationTenantReach", + "line": 38 + }, + { + "index": 2, + "question": "Extension E-42, as it touches the registry: a restore brings back a generation row and its progress row at their checkpoint and drops the scheduled batch that was driving them, so the row's fence at inventory time is the restore's proof that the generation is still stranded, and `resumeChain` bumping it is the proof that it is not (E-42, F12)", + "blocking": false, + "key": "restoreGenerationFence", + "line": 39 + }, + { + "index": 3, + "question": "Extension E-8, the first generation: the doc says how a generation is rebuilt and not how a read model's first generation comes to exist; the option taken here is that a read model is installed through its first rebuild, generation 1 starting as `building` like any other, so a read model added to a deployment that has history serves that history from its first switch and no transition claims coverage it did not build; a command that finds no generation to write fails as a technical failure and a query that finds no active generation fails the same way (E-8, D9, D19)", + "blocking": false, + "key": "readModelInstallation", + "line": 40 + }, + { + "index": 4, + "question": "Extension E-40, the checkpoint: the cursor, the batch size and the counts live on the progress row and not on the generation row, because a batch that wrote its checkpoint on the row every command reads would re-run every command in flight on the read model once per batch; the progress row is the one checkpoint of every batch whatever drives it (E-40, F1)", + "blocking": false, + "key": "rebuildProgressRow", + "line": 41 + }, + { + "index": 5, + "question": "Extension E-49, as it touches the registry: on a deployment whose grants predate the tenant list the list is empty while grants exist, and a generation started then visits no tenant; the option taken here is that `startGeneration` refuses then, with a message naming `fillTenants`, the operator entry of `spec:application.rebuild` that fills the list from the grants, and refuses while that fill runs (E-49, E-8, Law 11)", + "blocking": false, + "key": "tenantListGenerationStart", + "line": 42 + } + ], + "decisions": [ + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "title": "Rebuild online by default, pause writes only for history-dependent views", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.generation-registry", + "claim": "anchored", + "label": "generationsToWrite and activeGeneration", + "file": "src/read-model/generations.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "codeId": "impl:application.generation-registry.tables", + "claim": "anchored", + "label": "the generations, progress, marker and tenant fill tables and rowConventions", + "file": "src/read-model/tables.ts", + "line": 5, + "role": "schema", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 2, + "enabled": 2 + } + }, + { + "id": "spec:application.orders-inventory-example", + "resolved": true, + "title": "Orders and Inventory, the example domain", + "specKind": "model", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/application/orders-inventory-example.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-46: the second lifecycle command, the essential summary, the command that creates stock, the unit price an order line carries, the bound on an order line's stock item ID, the rejection reasons, the stream budgets of the two stream types, the order of the two context calls and the history-dependent view Sc L2-6 builds are choices the doc leaves open; two of them are decided, that the stock item keeps totals only and `release` takes its quantities from the cancelled order's own lines, and that the order allocation history view stays in the experiment; the owner may substitute the others, and the experiment's terms move with them (E-46, E-2, E-9, First experiment)", + "blocking": false, + "key": "orderInventoryCommandChoices", + "line": 36 + } + ], + "decisions": [ + { + "id": "spec:decisions.d03-events-only-source-of-next-state", + "title": "Events are the only source of the next state", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "title": "Convex covers UI retries, receipts cover the rest", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "title": "Rejections are thrown, not stored, at the public boundary", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "title": "Contexts meet only in parent use cases", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.order-allocation-history", + "claim": "anchored", + "label": "the order allocation history's keyOf and project rules", + "file": "example/domain/orderAllocationHistory.ts", + "line": 8, + "role": "projection", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example", + "claim": "anchored", + "label": "the order decider", + "file": "example/domain/order.ts", + "line": 10, + "role": "decider", + "component": { + "id": "component:example.orders-domain", + "layer": "domain", + "context": "orders" + } + }, + { + "codeId": "impl:application.orders-inventory-example.inventory-operations", + "claim": "anchored", + "label": "the Inventory sanctioned operations", + "file": "example/convex/inventory/operations.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example.inventory-schema", + "claim": "anchored", + "label": "the Inventory context schema", + "file": "example/convex/inventory/schema.ts", + "line": 1, + "role": "schema", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example.inventory-stock-item-query", + "claim": "anchored", + "label": "the stock item query", + "file": "example/convex/inventory/queries/stockItem.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example.inventory-streams", + "claim": "anchored", + "label": "the stock item stream registration", + "file": "example/convex/inventory/streams.ts", + "line": 2, + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example.order-summary", + "claim": "anchored", + "label": "the order summary read model", + "file": "example/convex/orderSummary.ts", + "line": 4, + "role": "read-model", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "codeId": "impl:application.orders-inventory-example.orders-operations", + "claim": "anchored", + "label": "the Orders sanctioned operations", + "file": "example/convex/orders/operations.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "codeId": "impl:application.orders-inventory-example.orders-order-query", + "claim": "anchored", + "label": "the order query", + "file": "example/convex/orders/queries/order.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "codeId": "impl:application.orders-inventory-example.orders-schema", + "claim": "anchored", + "label": "the Orders context schema", + "file": "example/convex/orders/schema.ts", + "line": 1, + "role": "schema", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "codeId": "impl:application.orders-inventory-example.orders-streams", + "claim": "anchored", + "label": "the order stream registration", + "file": "example/convex/orders/streams.ts", + "line": 3, + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "codeId": "impl:application.orders-inventory-example.stock-item", + "claim": "anchored", + "label": "the stock item decider", + "file": "example/domain/stockItem.ts", + "line": 11, + "role": "decider", + "component": { + "id": "component:example.inventory-domain", + "layer": "domain", + "context": "inventory" + } + }, + { + "codeId": "impl:application.parent-use-cases", + "claim": "anchored", + "label": "PlaceOrder and CancelOrder over Orders and then Inventory", + "file": "example/convex/ordering.ts", + "line": 8, + "role": "use-case", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "codeId": "impl:application.parent-use-cases.receiving", + "claim": "anchored", + "label": "ReceiveStock", + "file": "example/convex/receiving.ts", + "line": 4, + "role": "use-case", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + } + ], + "verifiers": { + "total": 6, + "enabled": 6 + }, + "examples": { + "total": 6, + "enabled": 6 + } + }, + { + "id": "spec:command.command-declaration", + "resolved": true, + "title": "Command declaration and bindings", + "specKind": "contract", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/command/command-declaration.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 19, + "declarations": 18, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-7: the doc says a small composition helper and shared schema values; the helper signatures, the executor signature and the read-model binding below are the design's; the cost is two registered mutations per command instead of one, and the cheaper alternative the design refuted is one public export per command plus one generic internal dispatcher mutation keyed by command type, as D7's conditional dispatcher already does; it lost because trusted server callers would then reach every command through one untyped `internalMutation`, so a renamed handler or a broken argument contract would pass `tsc` at the caller and fail only at runtime, which is the check Sc L2-4 exists to keep before traffic, and because the receipt fingerprint's contract version would be looked up by string rather than typed; the owner may still prefer the dispatcher form once code generation makes the per-command internal entry free (E-7, D12, D7, Sc L2-4, Decision method rule 5)", + "blocking": false, + "key": "commandEntrySignatures", + "line": 38 + }, + { + "index": 1, + "question": "Extension E-7: the pipeline receives the declaration and takes the fingerprint's contract version from it, and the type check at a typed caller is the check of Sc L2-4, so no standing map from command type to declaration exists; the conditional dispatcher of `spec:command.outcome-boundary` is the one thing that would need such a map and brings it with it; this is a provisional reading the owner rules (E-7, D12, D18, OQ2)", + "blocking": false, + "key": "commandDeclarationLookup", + "line": 39 + }, + { + "index": 2, + "question": "Extension E-38: the doc names five declaration fields; this design adds `contractVersion` for the fingerprint, `retention` and `irreversible` for receipts, `admission`, `bounds`, `rejections`, `audit`, `diagnosticSink` and `writes`, each serving a cited rule of another Spec, and asks whether the owner accepts them as part of the one declaration; `bounds` is the one input bound in the corpus, `maxItems` and `maxBytes`, checked by the pipeline's step 1 and by no executor body, so `spec:application.parent-use-cases` reads it rather than declaring a bound of its own; `writes` is required on every declaration, because an absent list would be an unchecked one, and `readModels` is optional, absent meaning none, which the owner has not ruled; `writes` names the context and stream type pairs the executor may write, from which the pipeline's step 7 derives the source scopes the write pause closes for a consistent cut and which step 9 proves against the `streams` entries the executor returns, because a use case that writes a source stream without maintaining the paused view would otherwise pass the gate; the declaration still imports nothing from the obligation family, so a command's static reactions are not a field here but the `createObligation` calls of its executor, as `spec:obligations.fan-out-and-chains` rules under E-54 (E-38, E-8, E-54, D6, D9, D10, D19, Thesis)", + "blocking": false, + "key": "commandDeclarationFields", + "line": 40 + } + ], + "decisions": [ + { + "id": "spec:decisions.d12-one-declaration-per-command", + "title": "One declaration per command, generated bindings later", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:command.command-declaration", + "claim": "anchored", + "label": "CommandDeclaration, publicCommand and internalCommand", + "file": "src/command/declaration.ts", + "line": 3, + "role": "contract", + "component": { + "id": "component:platform.command", + "layer": "application", + "context": "platform" + } + }, + { + "codeId": "impl:command.command-declaration.depot-commands", + "claim": "anchored", + "label": "the depot fixture's command declarations", + "file": "fixture/convex/depotCommands.ts", + "line": 3 + } + ], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 2, + "enabled": 2 + } + }, + { + "id": "spec:command.command-pipeline", + "resolved": true, + "title": "The command pipeline", + "specKind": "workflow", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "has-verifier" + ], + "file": "design/specs/command/command-pipeline.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 35, + "declarations": 18, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-30: the doc names the outcome, the operation ID and the affected stream versions as what a command returns, but not a wire shape; the shape here is a union discriminated by `replayed`, whose executed member carries the declaration's result and whose replayed member carries `result` null because a receipt stores no result, so the entry's returns validator admits a replay as a value; affected IDs are application IDs, never private document IDs (E-30, D2, D6, D8)", + "blocking": false, + "key": "commandOutcomeWireShape", + "line": 68 + }, + { + "index": 1, + "question": "Extension E-31: the doc says an operation's ID ties together everything one request caused but not how it is minted, and it says a derived command carries causation and an event's `causedBy` may be a triggering event without saying how that cause reaches the pipeline; here the pipeline mints the operation ID once per top-level mutation with `crypto.randomUUID()` before the first context call, a duplicate returns the operation ID stored on the receipt, and the internal entry takes an optional `causedBy` argument that step 7 copies onto the `OperationRef` when present, so a reaction that issues a derived command passes `causedBy` of kind `event` naming its publication event and the envelope's `event` variant has exactly one producer, while an entry called without it mints `causedBy` of kind `command` from the declaration's name; the obligation and attempt behind a derived command are found through its receipt, whose request key is the obligation's effect key, and never through the envelope (E-31, E-54, D2, D13, Vocab: operation)", + "blocking": false, + "key": "operationIdentityCause", + "line": 69 + }, + { + "index": 2, + "question": "Extension E-32: the doc places rate admission for new intent after the duplicate check and makes a rate or capacity refusal a retryable error, but names no admission mechanism; here admission is an optional per-command policy in the declaration, absent by default, evaluated inside the mutation so that a refusal that throws leaves no consumption behind; the flip side is that a consumption the policy writes commits only with the command, so the policy counts committed intent and cannot bound refused or unauthorized traffic, and a product that must bound refused traffic needs the dispatcher form of D7, where the parent survives the body's rejection (E-32, D4, D6, D7)", + "blocking": false, + "key": "commandAdmissionPolicy", + "line": 70 + }, + { + "index": 3, + "question": "Extension E-33: the doc lists the callers that need receipts and leaves UI commands to the client guarantee; here a public caller may still supply a request key to opt into a receipt, which Probe 1 showed a caller of `ConvexHttpClient` needs when it retries, and the internal entry always requires one (E-33, D6, Probe 1)", + "blocking": false, + "key": "publicRequestKey", + "line": 71 + }, + { + "index": 4, + "question": "Probe 4 showed on the pinned backend that the transaction limits are one budget across the parent and its component sub-transactions, so the per-command bounds are checked against one budget; `ctx.runMutation` accepts a `transactionLimits` option, the design passes none on a context call, and whether a call should reserve room with it for the work after it stays open (Probe 4, F13, S3)", + "blocking": false, + "key": "contextCallBudgetReservation", + "line": 72 + }, + { + "index": 5, + "question": "Extension E-9: the doc says essential read models are updated inside the command and does not say from what or in what order; here step 9 writes a per-entity read model from the DTO of each matching `streams` entry, never from its events, and a history read model from the events of every matching entry in one call per command, checks `writes` first, then that each bound read model has a generation to write, then the count of the rows it writes against `limitReadModelWritesPerCommand`, each failure a plain error, and the library helpers that step 9 calls are generic over the application's data model; the owner has not ruled on any of it (E-9, E-7, E-8, E-45, D8, D9)", + "blocking": false, + "key": "commandReadModelWrites", + "line": 73 + }, + { + "index": 6, + "question": "Extension E-34: step 1 bounds every text field a caller sets beside the input, the tenant ID, the request key, the correlation ID, the actor's fields and the fields of `causedBy`, in UTF-8 bytes as `limitCallText` lists them, as rejection `invalidInput`, and treats a declaration name above `limitIdLength` as a defect, a plain error, because these fields become fields of the receipt row or of every event's envelope; the doc says events stay small and names no number; the owner has not ruled (E-34, D6, D12, D19)", + "blocking": false, + "key": "commandTextBounds", + "line": 74 + } + ], + "decisions": [ + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "title": "One mutation per business operation", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "title": "Convex covers UI retries, receipts cover the rest", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "title": "Rejections are thrown, not stored, at the public boundary", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "title": "Contexts meet only in parent use cases", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "title": "Tenant scope and authority from day one", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d12-one-declaration-per-command", + "title": "One declaration per command, generated bindings later", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:application.read-models", + "claim": "anchored", + "label": "writeReadModels, step 9's read-model half", + "file": "src/read-model/write.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "codeId": "impl:command.command-pipeline", + "claim": "anchored", + "label": "runPipeline and the command response", + "file": "src/command/pipeline.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.command", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 3, + "enabled": 3 + }, + "examples": { + "total": 3, + "enabled": 3 + } + }, + { + "id": "spec:context.event-envelope", + "resolved": true, + "title": "The event envelope", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [ + "implemented" + ], + "file": "design/specs/context/event-envelope.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 16, + "declarations": 10, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-26: the doc lists the fifteen fields and their meaning; this Spec fixes the validator shapes, the `CausedBy` union with a migration variant for baseline events, the `OperationRef` the parent passes, and the event ID as a version 4 UUID from the runtime's `crypto`; the owner confirms (D2, E-26)", + "blocking": false, + "key": "eventEnvelopeFields", + "line": 31 + }, + { + "index": 1, + "question": "Extension E-12: the doc says events stay small and gives no number for the envelope; this Spec bounds the envelope beside the payload at `limitEnvelopeBytes`, 4,096 bytes, and each text field of it that a caller can set at 256 bytes of UTF-8, 512 for an actor's ID, so the largest stored event, `limitEventBytes`, is derived and enforced and never assumed; the owner may set other numbers, and the fold bound of `spec:context.journal` moves with them (E-12, D19, F13)", + "blocking": false, + "key": "envelopeTextBounds", + "line": 32 + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "title": "Each bounded context owns its state and its journal", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [ + { + "codeId": "impl:context.event-envelope", + "claim": "anchored", + "label": "the envelope types and validators", + "file": "src/context/envelope.ts", + "line": 2, + "role": "contract", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:context.journal", + "resolved": true, + "title": "The journal", + "specKind": "behavior", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/context/journal.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 39, + "declarations": 18, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Extension E-25: the doc says a migration writes a baseline event holding the migrated state and that rebuild starts from the latest baseline, and it does not say who computes that state, who drives the migration, or what a live command does on a stream the migration has not reached; this Spec fixes the representation, an event of the reserved type `baseline` whose payload holds the state and its schema version and is bounded by the stream's `budgetBytes` instead of the payload bound, a `baselineVersion` on the stream row, a `BaselineMigration` registered on the stream type whose `migrate` function computes the migrated state from the saved state, or, for a step that declares `readsHistory`, from the saved state and the events since the last baseline in the sweep only, the rule that `stateSchemaVersion` counts meaning only and lives on the registration, one helper `migrateToSchemaVersion` that writes the baseline chain, called by the adapter on load for a chain that reads no history and by the `writeBaseline` maintenance mutation for every chain, the code-older-than-data error, the `writeBaseline` mutation built by `defineMaintenance` and declared with the component's public `mutation` builder, the parent-side scheduled chain that drives it by cursor, the batch bound derived from `budgetBytes`, the upcasting rule for old event schema versions, the fold bound of 500 events with the refusals `rebuildStream` returns above it and for a stream awaiting its baseline, the retirement rule for a migration, and the payload byte check in `append`; the option of refusing commands on unmigrated streams with a transient code until the sweep arrives was not taken for a state-only migration, because it turns a deploy into a partial write pause the doc reserves for history-dependent views, and is taken for a history-reading migration, the exception, where it holds only the streams the sweep has not reached and keeps D3 whole on the command path, which the owner confirmed; a consequence the owner should weigh is that every baseline bumps its stream's version, so a reviewed version held across the sweep is answered `staleVersion` and re-reviewed rather than silently accepted, because Sc L1-10 forbids reinterpreting a reviewed version against fresh state and the baseline is a real event of the stream, while a live stream migrated on load never trips it because its expected-version check runs before the chain; and the driver reads the write pause's gate before every batch and reschedules itself when refused, so a sweep never changes a source under a paused rebuild's cut; the owner confirms (D5, D9, S4, Sc L1-10, E-2, E-8, E-12, E-25)", + "blocking": false, + "key": "baselineMigrationContract", + "line": 51 + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "title": "Each bounded context owns its state and its journal", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "title": "Rebuildable history by default, with baseline events", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:kernel.state-document-mapping", + "title": "One document per stream, with a derived mapping above the budget", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:context.journal", + "claim": "anchored", + "label": "createJournal, load and append", + "file": "src/context/journal.ts", + "line": 2, + "role": "event-store", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 0 + }, + "examples": { + "total": 1, + "enabled": 0 + } + }, + { + "id": "spec:context.queries", + "resolved": true, + "title": "The context queries", + "specKind": "contract", + "altitude": "story", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/context/queries.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 29, + "declarations": 18, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Probe 5 showed on the pinned backend with `convex-helpers` 0.1.124 that the parent query stays reactive, that pages pinned by their end cursors stay contiguous, that a capped page crosses the boundary as `SplitRequired` with its `splitCursor`, and that the `usePaginatedQuery` hook of `convex-helpers/react` then shows 40 of 45 rows; this design uses no hook across the boundary, and the examples are rerun when either pin moves (Probe 5, F15, S4, D8, E-24)", + "blocking": false, + "key": "contextPaginationSplit", + "line": 41 + }, + { + "index": 1, + "question": "Extension E-24: the doc says component queries are authorized and return DTOs; this Spec fixes one definer per query, the one `tenantId` argument with no separate scope, the `toDto` mapping with the stream version on every DTO, pagination through `paginator`, the item cap with a row cap of twice the item cap and a byte cap passed on every run, the relay of `pageStatus` and `splitCursor` unchanged, paging by an explicit cursor pair with no client hook, the parent query's one `authorizeQuery` call and its refusal as a rejection, the `maxEvents` argument of the `rebuild` query and its `budgetExhausted` refusal for a batching caller, and the fold refusal of the `rebuild` query; this is a provisional reading the owner has not ruled (D8, S4, E-24, E-42)", + "blocking": false, + "key": "contextQueryContract", + "line": 42 + }, + { + "index": 2, + "question": "A parent query's refusal carries the name of the entry that refused in the field `entry` of the rejection data; the wire shape stays `RejectionData` of `spec:command.outcome-boundary` unchanged, and the owner confirmed the word (D7, E-24)", + "blocking": false, + "key": "queryRefusalEntry", + "line": 43 + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "resolved": true + } + ] + }, + "implementations": [], + "references": [ + { + "codeId": "impl:context.queries", + "claim": "anchored", + "label": "defineGet, defineList and boundedPage", + "file": "src/context/queries.ts", + "line": 3, + "role": "query", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "resolved": true, + "title": "Rebuild online by default, pause writes only for history-dependent views", + "specKind": "decision", + "altitude": "feature", + "statedReadiness": "defined", + "derivedReadiness": "ready", + "deliveryFacts": [], + "file": "design/specs/decisions/d09-rebuild-online-by-default.sdp.md", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "blockingQuestions": 0, + "openQuestions": [ + { + "index": 0, + "question": "Probe 6 ran a backfill batch racing a live command and the migrations component on the native backend, with its results on F17; a hosted deployment and sustained contention are not run (Probe 6, F1, F17, D9)", + "blocking": false, + "key": "rebuildContentionCoverage", + "line": 30 + } + ], + "decisions": [] + }, + "implementations": [], + "references": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + } + ], + "verifierGaps": [ + { + "id": "spec:application.history-projection", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.projection-contract", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-interrupt", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-abort", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-version-change", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-installation", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-source-order-mismatch", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:context.event-envelope", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:context.journal", + "statedReadiness": "defined", + "priority": false + }, + { + "id": "spec:context.queries", + "statedReadiness": "defined", + "priority": false + } + ], + "boundary": { + "restsOn": [ + { + "id": "spec:application.first-experiment", + "title": "The first experiment", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:command.actor-and-scope", + "title": "Actor, tenant scope, namespace and grants", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.idempotency-and-receipts", + "title": "Idempotency and receipts", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.outcome-boundary", + "title": "The outcome and rejection boundary", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.receipt-table", + "title": "The receipts table", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.tenancy-and-authority", + "title": "Tenancy and authority", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:constraints.bulk-operations-bounded", + "title": "Every bulk operation has a stated bound", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:constraints.events-stay-small", + "title": "Events stay small", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:constraints.no-queue-recovery-for-essential-reads", + "title": "No queue recovery needed for essential reads", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:constraints.zero-core-projection-jobs", + "title": "Zero core projection jobs", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:context.batch-shaped-api", + "title": "Batch-shaped context APIs", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:context.context-component", + "title": "The context component", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:context.journal", + "spec:context.queries" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.persistence-adapter", + "title": "The persistence adapter", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.tables", + "title": "The context tables", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "title": "One mutation per business operation", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.read-models", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "title": "Each bounded context owns its state and its journal", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d03-events-only-source-of-next-state", + "title": "Events are the only source of the next state", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "title": "Rebuildable history by default, with baseline events", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "title": "Convex covers UI retries, receipts cover the rest", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "title": "Rejections are thrown, not stored, at the public boundary", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "title": "Contexts meet only in parent use cases", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "title": "Tenant scope and authority from day one", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d12-one-declaration-per-command", + "title": "One declaration per command, generated bindings later", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "title": "Everything else waits for its trigger", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection" + ] + } + ] + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "title": "Operations travel with the capability", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.rebuild", + "spec:application.history-rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f01-serializable-mutations-under-occ", + "title": "Mutations are serializable under optimistic concurrency", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:facts.f02-component-calls-commit-with-caller", + "title": "Component calls commit or roll back with the calling mutation", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f03-nested-run-mutation-partial-rollback", + "title": "A nested runMutation gives partial rollback", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:facts.f04-nested-calls-cost-more-than-helpers", + "title": "Nested calls cost more than helper calls", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:facts.f07-queries-reactive-not-durable-delivery", + "title": "Queries are reactive, subscriptions are not durable delivery", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f08-scheduling-commits-with-mutation", + "title": "Scheduling from a mutation commits with it", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "title": "Scheduled mutations run once, scheduled actions are not retried", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f11-components-have-no-ctx-auth", + "title": "Components have no ctx.auth", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.write-pause", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f12-backups-exclude-pending-scheduled-functions", + "title": "Backups exclude pending scheduled functions", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:facts.f13-transactions-have-limits", + "title": "Transactions have limits", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f14-convex-error-survives-nested-and-component-boundary", + "title": "ConvexError data survives a nested mutation and a component boundary", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:facts.f15-parent-query-over-component-query-stays-reactive", + "title": "A parent query over a component query stays reactive", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f17-migrations-fits-generation-backfill", + "title": "The migrations component fits a generation backfill", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.generation-registry", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:facts.f18-commit-timestamps", + "title": "Commit timestamps order committed transactions", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f19-nested-calls-share-time-budgets", + "title": "Nested calls draw on a system-operation time budget", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f20-pagination-checks-bytes-after-reading", + "title": "Pagination checks bytes after reading", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:kernel.outcome-model", + "title": "The outcome model", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:kernel.state-document-mapping", + "title": "One document per stream, with a derived mapping above the budget", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law01-sanctioned-writes-only", + "title": "Business writes go only through sanctioned operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law02-state-and-events-commit-together", + "title": "State and its required events commit together", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law03-events-only-source-of-state", + "title": "Events are the only source of the next state", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law04-server-scoped-idempotency-key", + "title": "A retryable command carries a server-scoped idempotency key", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law05-authorization-before-execution-and-disclosure", + "title": "Authorization comes before execution and disclosure", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:laws.law06-technical-failure-never-a-rejection", + "title": "A technical failure is never stored as a rejection", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law09-no-invariant-on-late-read-model", + "title": "No invariant depends on a late read model", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law10-replay-never-runs-commands-or-effects", + "title": "Replay never runs commands or repeats effects", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law11-tenant-scope-named", + "title": "Every command and query names its tenant scope", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:operations.baseline-operations", + "title": "Baseline operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:platform.transactional-domain-platform", + "title": "Convex transactional domain platform", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + } + ], + "restedOnBy": [ + { + "id": "spec:advanced.cross-stream-online-rebuild", + "title": "Online rebuild of cross-stream history views", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:agents.agent-runs", + "title": "Agent runs", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:application.aggregate-projection", + "title": "Aggregate projection", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:application.projection-contract", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:application.first-experiment", + "title": "The first experiment", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.history-projection", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.command-without-generation-fails", + "title": "A command whose read model has no generation fails", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.first-rebuild-installs-read-model", + "title": "A read model is installed through its first rebuild", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-duplicate-answered-from-receipt", + "title": "A repeated cancel request is answered from its receipt", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-placed-order", + "title": "A placed order is cancelled", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-second-context-rejects", + "title": "Orders cancels, then Inventory refuses the release", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-twice-rejected", + "title": "A second cancel of one order is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-unknown-order-rejected", + "title": "A cancel of an order never placed is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-without-grant-refused", + "title": "A caller without the cancel grant is refused", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.parent-use-cases", + "title": "Parent use cases", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-read-through-query", + "title": "A successful command, then a query", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-visible", + "title": "A successful command, then a query or subscription", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.list-pages-by-cursor", + "title": "A client reads a tenant's order list in pages by cursor", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-before-disclosure", + "title": "A caller with no identity reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-without-grant", + "title": "A caller with no grant in the tenant reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.rebuild.online-rebuild-interrupt-resume", + "title": "Rebuild a per-entity read model online while commands run; interrupt and resume", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:application.restore", + "title": "Restore", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.projection-contract", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:context.event-envelope", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:application.write-pause.restore-door-without-deploy", + "title": "The restore door closes and opens with no deploy", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.broken-argument-contract-fails-build", + "title": "Break a typed handler's argument contract", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.renamed-handler-fails-build", + "title": "Rename a typed handler", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-journal-append", + "title": "Inject a failure after the journal append", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-state-write", + "title": "Inject a failure after the state write", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-before-receipt", + "title": "Inject a failure before the receipt insert", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.idempotency-and-receipts", + "title": "Idempotency and receipts", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.outcome-boundary", + "title": "The outcome and rejection boundary", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.receipt-table", + "title": "The receipts table", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.tenancy-and-authority", + "title": "Tenancy and authority", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:context.context-component", + "title": "The context component", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:context.journal.rebuild-from-baseline", + "title": "Rebuild a stream that has a baseline event", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:context.persistence-adapter", + "title": "The persistence adapter", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:context.tables", + "title": "The context tables", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "title": "Everything else waits for its trigger", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:obligations.fan-out-and-chains", + "title": "Fan-out and reaction chains", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:obligations.local-reaction-wrapper", + "title": "Local reaction wrapper", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:obligations.obligation-module", + "title": "Obligation module", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:operations.baseline-operations", + "title": "Baseline operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + } + ], + "verifiedBy": [ + { + "id": "spec:application.generation-registry.command-without-generation-fails", + "title": "A command whose read model has no generation fails", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.first-rebuild-installs-read-model", + "title": "A read model is installed through its first rebuild", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-duplicate-answered-from-receipt", + "title": "A repeated cancel request is answered from its receipt", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-placed-order", + "title": "A placed order is cancelled", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-second-context-rejects", + "title": "Orders cancels, then Inventory refuses the release", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-twice-rejected", + "title": "A second cancel of one order is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-unknown-order-rejected", + "title": "A cancel of an order never placed is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-without-grant-refused", + "title": "A caller without the cancel grant is refused", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-read-through-query", + "title": "A successful command, then a query", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-visible", + "title": "A successful command, then a query or subscription", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.list-pages-by-cursor", + "title": "A client reads a tenant's order list in pages by cursor", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-before-disclosure", + "title": "A caller with no identity reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-without-grant", + "title": "A caller with no grant in the tenant reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.rebuild.online-rebuild-interrupt-resume", + "title": "Rebuild a per-entity read model online while commands run; interrupt and resume", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:application.write-pause.restore-door-without-deploy", + "title": "The restore door closes and opens with no deploy", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.broken-argument-contract-fails-build", + "title": "Break a typed handler's argument contract", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.renamed-handler-fails-build", + "title": "Rename a typed handler", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-journal-append", + "title": "Inject a failure after the journal append", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-state-write", + "title": "Inject a failure after the state write", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-before-receipt", + "title": "Inject a failure before the receipt insert", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.journal.rebuild-from-baseline", + "title": "Rebuild a stream that has a baseline event", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:context.journal" + ] + } + ] + } + ], + "supersededBy": [] + }, + "findings": [] +} diff --git a/explorations/studio-prototype/data/history-view.recipe-29.json b/explorations/studio-prototype/data/history-view.recipe-29.json new file mode 100644 index 00000000..86427dc6 --- /dev/null +++ b/explorations/studio-prototype/data/history-view.recipe-29.json @@ -0,0 +1,3309 @@ +{ + "found": true, + "id": "pack:history-view", + "title": "History view", + "totals": { + "members": 19, + "unresolved": 0, + "heldBelowNextRung": 0, + "waitingForAuthor": 19, + "entries": 283, + "declarations": 169, + "openQuestions": 53, + "blockingQuestions": 0, + "withImplementations": 2, + "withReferences": 10, + "restsOn": 56, + "restedOnBy": 39, + "verifiedBy": 21, + "supersededBy": 0 + }, + "members": [ + { + "id": "spec:application.history-projection", + "resolved": true, + "title": "History projection", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 10, + "declarations": 8, + "openQuestions": 4, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "historyFoldOrder", + "blocking": false, + "address": "spec:application.history-projection#question.historyFoldOrder", + "line": 37, + "question": "Extension E-9, the history fold: D2 orders events only within a stream, and the two paths feed one history row in different orders, the live path command by command and the backfill stream by stream; the option taken here is a fold that gives one row whatever the order across streams and however its events are split between calls, a row no fold deletes, which keeps its subject's ended state, and baselines dropped before `keyOf`, so a view whose meaning needs the order across streams waits for the ordered consumer of D18 (E-9, D2, D9, D18, E-25)" + }, + { + "key": "historyProjectionCost", + "blocking": false, + "address": "spec:application.history-projection#question.historyProjectionCost", + "line": 38, + "question": "The cost of the binding on the live path, one row read and one write of up to `rowBudgetBytes` per command and the measure of the row, is taken by the first experiment's latency cell with the order allocation history bound, for `PlaceOrder` and for `CancelOrder` of an order of the maximum lines, before the view is declared essential anywhere (E-9, E-47, F13)" + }, + { + "key": "historyFoldPureTest", + "blocking": false, + "address": "spec:application.history-projection#question.historyFoldPureTest", + "line": 39, + "question": "Extension E-9, the check of the history fold: the order independence the Contract requires is checked for the order allocation history alone; the option taken here is that every history projection of an application carries a pure test that folds a sample of one key's events in every interleaving of its streams and every split into calls, and a library helper that runs that test is added with a second history view (E-9, D2, D18)" + }, + { + "key": "historySubjectErasure", + "blocking": false, + "address": "spec:application.history-projection#question.historySubjectErasure", + "line": 40, + "question": "A history row keeps a deleted subject's history and every rebuild writes it again from the journal; whether a history view falls under the retention and deletion policy of D19, and what its rebuild does after an erasure, is answered by that policy (D19, D9, E-9)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.order-allocation-history", + "file": "example/domain/orderAllocationHistory.ts", + "line": 8, + "role": "projection", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.projection-contract", + "resolved": true, + "title": "Projection contract", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 21, + "declarations": 11, + "openQuestions": 5, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "projectionRowContract", + "blocking": false, + "address": "spec:application.projection-contract#question.projectionRowContract", + "line": 33, + "question": "Extension E-9: the doc says a projection is named, versioned and deterministic and that live update and rebuild share its logic; it does not give the signature or the row shape, for counts and sums it names the marker but not the aggregate row, and for the read models that depend on history across several streams, the ones the write pause exists for, it names no input. The option taken here: a per-entity read model declared as a `ReadModel<D, Row>` with `name`, `table`, `rowBudgetBytes` and its `projections`, a list of `Projection<D, Row>` with the latest first, each holding `version`, `keyOf` and `project` alone, rows with `tenantId`, `generation`, `key`, `projectionVersion` and `sourceVersions`, one `applyProjection` helper with a mode per caller that writes each generation by its role, active, building or verifying, with the projection whose version the generation records, for the aggregate form the row and the writer `spec:application.aggregate-projection` pins, and for a history-dependent view a `HistoryProjection<Row>` that folds events into the prior row, fed on the live path by the events the operation appended, which the adapter's `streams` entry carries, and on the backfill by the context's `history` pages under the write pause, with `sourceVersions` recording the highest version folded per stream so that an event is never folded twice; a row that derives from more than one source stream type is a history row, its projection bound once per source stream type, and a small cross-context view of current state is D8's parent query over bounded component reads; the owner confirms the three forms and has not ruled on the split of a per-entity read model into `ReadModel` and `Projection`, on the write by generation role or on the row measured against its budget at every write (E-9, E-23, E-24, D8, D9)" + }, + { + "key": "projectionGenerationVersions", + "blocking": false, + "address": "spec:application.projection-contract#question.projectionGenerationVersions", + "line": 34, + "question": "Extension E-9, two projection versions: the doc says live update and rebuild share one projection and does not say how a command writes two generations built by two versions; the option taken here is that a read model declares its projection versions as a list, the latest first, and `applyProjection` writes each generation with the projection whose version the generation row records, failing as a technical failure when the read model declares none of that version, so the rows of one generation are one truth of one version; the cost is that an earlier version stays in the code while a generation that records it is written or can be rolled back to, and a projection changed without a version change is still undetected (E-9, D8, D9)" + }, + { + "key": "backfillVersionOverwrite", + "blocking": false, + "address": "spec:application.projection-contract#question.backfillVersionOverwrite", + "line": 35, + "question": "The rule that a row with a newer version is never overwritten is read as the backfill path's rule only, so a live write to the active generation always lands, as `step3` has it; the owner has not confirmed that reading (D9, E-9)" + }, + { + "key": "compositeCurrentStateRow", + "blocking": false, + "address": "spec:application.projection-contract#question.compositeCurrentStateRow", + "line": 36, + "question": "Extension E-9, a composite row of current state: D8 lets a small cross-context view be a read model updated in the command, and no form here writes one row from the DTOs of two sources, because `project` builds a whole row from one DTO and a backfill enumerates one source; the option taken here is D8's other default, a parent query over bounded component reads, until a view needs such a row, which then needs a form whose `project` takes the DTO of every matching entry of one command and whose backfill reads the other context in the same mutation, online (E-9, D8, E-45)" + }, + { + "key": "historySourceVersionTenant", + "blocking": false, + "address": "spec:application.projection-contract#question.historySourceVersionTenant", + "line": 37, + "question": "Extension E-9, the cost of `sourceVersions`: a history row's entries repeat the row's tenant ID once per source stream, about three quarters of the largest order allocation history row by `spec:application.history-projection#design.tableOrderAllocationHistory`; whether a row's `sourceVersions` may omit `tenantId`, equal to the row's on every entry, and the write rule compare three identity fields, is a choice the owner may take with the pinned conventions (E-9, Law 11, F13)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.generation-registry.tables", + "file": "src/read-model/tables.ts", + "line": 5, + "role": "schema", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "id": "impl:application.projection-contract", + "file": "src/read-model/projection.ts", + "line": 3, + "role": "projection", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.read-models", + "resolved": true, + "title": "Read models", + "specKind": "behavior", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 11, + "openQuestions": 6, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "parentListGenerationSwitch", + "blocking": false, + "address": "spec:application.read-models#question.parentListGenerationSwitch", + "line": 49, + "question": "Probe 5 showed that the built-in `.paginate` treats `maximumRowsRead` as advice once a page has an end cursor and that a cursor it issued is refused by any other query, so a subscription over the active generation threw `InvalidCursor` at a switch; a parent list therefore pages with `paginator` of `convex-helpers`, as a context list does, and a pinned page is bounded on every run by the row cap and by the byte cap as `spec:context.queries` states it, the helper stopping at the row that reaches it and admitting that row whole, and answers `SplitRequired` beyond them (Probe 5, F13, D8, D9)" + }, + { + "key": "readModelListBounds", + "blocking": false, + "address": "spec:application.read-models#question.readModelListBounds", + "line": 50, + "question": "Extension E-24, as it reaches read models: a list over a parent read-model table calls `paginator` of `convex-helpers` with the options of `boundedPage`, a row cap of twice the item cap, with each cursor of the client's pair moved into the active generation by `pageInGeneration`, and returns through `paginationResultValidator` with its rows mapped by `readModelView`, and a client pages every list by an explicit cursor pair with no hook; these are options taken here and the owner has not ruled (E-24, S4, Probe 5, D8)" + }, + { + "key": "parentQueryRefusal", + "blocking": false, + "address": "spec:application.read-models#question.parentQueryRefusal", + "line": 51, + "question": "Extension E-6, as it reaches queries: a parent query establishes and authorizes its actor through `authorizeQuery`, and its refusal is the command's rejection shape, `RejectionData` with the query's name in the field `entry` and the codes `unauthenticated` or `forbidden`; the owner confirmed the word, and nothing else of the shape is new (E-6, D7, D11, Law 5)" + }, + { + "key": "contextQueryCost", + "blocking": false, + "address": "spec:application.read-models#question.contextQueryCost", + "line": 52, + "question": "Probe 3 pending: the cost of one component call in latency and function-call quota decides whether one entity's detail stays a component query or hot reads move, which is OQ1 (Probe 3, F4, OQ1)" + }, + { + "key": "parentQueryReadBounds", + "blocking": false, + "address": "spec:application.read-models#question.parentQueryReadBounds", + "line": 53, + "question": "Extension E-45: the doc says a small cross-context view is a parent query over bounded component reads and does not give the bound; the option taken here is at most 8 component reads per parent query and at most 4 read-model rows written per command, both measured by the first experiment (E-45, D8)" + }, + { + "key": "crossContextReadForm", + "blocking": false, + "address": "spec:application.read-models#question.crossContextReadForm", + "line": 54, + "question": "Extension E-9, as it reaches read models: D8 lets a small cross-context view be a parent query over bounded component reads or a read model updated in the command; the option taken here is the parent query for a view of current state, because no form of `spec:application.projection-contract` writes one row from the DTOs of two sources, and the history projection for a view that folds the events of several streams; a composite row of current state is a form for the view that needs it (E-9, E-45, D8)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.read-models", + "file": "src/read-model/write.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "id": "impl:application.read-models.queries", + "file": "src/read-model/queries.ts", + "line": 3, + "role": "query", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 5, + "enabled": 5 + }, + "examples": { + "total": 5, + "enabled": 5 + } + }, + { + "id": "spec:application.rebuild", + "resolved": true, + "title": "Rebuild: generations, backfill and the write pause", + "specKind": "workflow", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 42, + "declarations": 27, + "openQuestions": 5, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "rebuildBatchDriver", + "blocking": false, + "address": "spec:application.rebuild#question.rebuildBatchDriver", + "line": 59, + "question": "`@convex-dev/migrations` walks only the tables of the component it is defined in, reaches a context's rows only through a parent internal mutation written by hand against a contract its README does not document and the compiled tier needs a cast for, keeps a status apart from the generation's state, and leaves no error on its row when a batch fails past the read limit, so every batch is driven by the rebuild's own self-scheduled internal mutation and the progress row is the one checkpoint; a hosted deployment and sustained contention are not observed (Probe 6, F1, F17, D9)" + }, + { + "key": "rebuildBatchBounds", + "blocking": false, + "address": "spec:application.rebuild#question.rebuildBatchBounds", + "line": 60, + "question": "Extension E-40: the doc binds resumable batching to `@convex-dev/migrations` and gives no batch size; the component iterates a parent table while the sources of a read model live in a context component. The option taken here: a batch is one mutation with one cursor on the progress row and a bound of at most 100 entities, capped by the read model's declared `rowBudgetBytes` so that the parent's point reads and writes stay under 4 MiB, and by the context's byte-derived `list` page bound, which is 16 items at the default 256 KiB stream budget; every batch, the backfill and the verify pass over a context's enumeration and the purge over a parent table, is driven by the platform's own self-scheduled internal mutation, and the rebuild uses the component for nothing, because Probe 6 showed that it walks only the tables of the component it is defined in and drives a context's rows only through a parent internal mutation written by hand against a contract its README does not document; the progress row is the one checkpoint (E-40, E-24, E-9, F17, F13, F1)" + }, + { + "key": "rebuildGenerationPauseContract", + "blocking": false, + "address": "spec:application.rebuild#question.rebuildGenerationPauseContract", + "line": 61, + "question": "Extension E-8: the registry states, the rollback period, what a rollback does, the gate document, the scope a paused rebuild closes and the reach of a generation are design choices the doc does not make; they are pinned in the two contracts under this workflow (E-8, E-9, E-38, D9)" + }, + { + "key": "rebuildReadModelForms", + "blocking": false, + "address": "spec:application.rebuild#question.rebuildReadModelForms", + "line": 62, + "question": "Extension E-8, what this workflow does not offer: the online path is pinned for a per-entity read model with one source stream type and the paused path for a history view, a `HistoryRebuildTarget` with its sources, in `spec:application.history-rebuild`; the aggregate form's rebuild is deferred with the form in its own Spec, the aggregate projection, and a row made from the current state of more than one source has no form at all, as `spec:application.projection-contract` records; `rollbackGeneration` refuses a generation that needs a write pause, because nothing would hold its sources while live commands fold onto a row that missed the events since its retirement, so such a view returns to earlier rules only through a new generation under the pause (E-8, E-9, D9, D18)" + }, + { + "key": "tenantListFill", + "blocking": false, + "address": "spec:application.rebuild#question.tenantListFill", + "line": 63, + "question": "Extension E-49, a deployment whose grants predate the tenant list: the tenant list is written by `insertGrant`, so on such a deployment it holds no row while `grants` holds many, and a pass over an empty list visits no tenant and still ends; the option taken here is that `startGeneration` refuses an empty list beside a grant with a message naming `fillTenants`, and `fillTenants`, an operator entry, walks the grants in bounded batches through a progress row of its own and inserts each missing tenant once; a list that holds a tenant whose first grant came after the table and misses an older one is not detected by that refusal, so the operator runs `fillTenants` on such a deployment before its first `startGeneration` (E-49, E-48, E-40, Law 11)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.rebuild", + "file": "src/read-model/rebuild.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 1, + "enabled": 1 + } + }, + { + "id": "spec:application.history-rebuild", + "resolved": true, + "title": "Rebuild of a history view under a write pause", + "specKind": "workflow", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 12, + "declarations": 3, + "openQuestions": 6, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "historyBatchShape", + "blocking": false, + "address": "spec:application.history-rebuild#question.historyBatchShape", + "line": 41, + "question": "Extension E-40, the shape of a history batch: a batch takes one stream, so a pass costs at least one scheduled mutation per source stream and the pause lasts about twice the stream count times one batch; the alternative is a batch that takes streams from one `list` page until its event budget is spent, one `history` call per stream, with the byte bound counting one baseline per stream; the first paused rebuild on the native backend measures one batch, and the shape is chosen with that number (E-40, E-9, F13, F19, D9)" + }, + { + "key": "verifyPassUnderPause", + "blocking": false, + "address": "spec:application.history-rebuild#question.verifyPassUnderPause", + "line": 42, + "question": "The verify pass under the write pause: a history view's generation is built while no source changes, so its verify pass repeats the backfill's batches and, by `spec:application.history-rebuild#design.historyVerify`, finds nothing over a covered generation, which doubles the pause; the option kept here is the pass, as D9's \"Verify coverage, then switch\" and Sc L2-6's \"writes reopen only after verification\" read, and whether a paused generation is `verified` when its backfill ends is the owner's (D9, Sc L2-6)" + }, + { + "key": "historyBatchWriteThroughput", + "blocking": false, + "address": "spec:application.history-rebuild#question.historyBatchWriteThroughput", + "line": 43, + "question": "Whether a history view's batch chain, at up to 2 MiB of rows per batch scheduled with `runAfter(0)`, is held by the deployment class's write throughput limit, `spec:facts.f13-transactions-have-limits#design.limitWriteThroughput`, which the first paused rebuild on the native backend and one hosted run show (F13, E-40)" + }, + { + "key": "historyMigrationBeforePause", + "blocking": false, + "address": "spec:application.history-rebuild#question.historyMigrationBeforePause", + "line": 44, + "question": "Must every source's migration that reads history finish before `startGeneration` closes its source scopes, with abort, migration completion and a fresh generation as the recovery if it did not? If enumeration must instead remain possible during that migration, which context query provides it without constructing a DTO of the wrong meaning?" + }, + { + "key": "abortSwitchAtomicity", + "blocking": false, + "address": "spec:application.history-rebuild#question.abortSwitchAtomicity", + "line": 45, + "question": "Should the native-backend abort example inject failure in the second source's audit insert, assert unchanged generation, progress, gate entries and audit records, then retry after removing the fault and assert one successful abort with both sources reopened? The same atomicity check should cover the switch that reopens them." + }, + { + "key": "historyTenantPauseIsolation", + "blocking": false, + "address": "spec:application.history-rebuild#question.historyTenantPauseIsolation", + "line": 46, + "question": "Should the native-backend example use two tenants with equal local order and stock item IDs, check their folds separately, and refuse a command that writes a paused source without a history binding in both tenants, while a command writing only an unrelated source remains allowed? Include a retry of resume after its response is lost so the same saved event page cannot change the fold twice." + } + ], + "decisions": [ + { + "id": "spec:decisions.d04-four-outcomes", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 5, + "enabled": 0 + }, + "examples": { + "total": 5, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-interrupt", + "resolved": true, + "title": "Rebuild a cross-stream history view under a write pause; interrupt, then resume", + "specKind": "example", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-abort", + "resolved": true, + "title": "Rebuild a cross-stream history view under a write pause; interrupt, then abort", + "specKind": "example", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-version-change", + "resolved": true, + "title": "Deploy a changed history projection version through the handoff", + "specKind": "example", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-installation", + "resolved": true, + "title": "Install a history view through the handoff", + "specKind": "example", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.history-rebuild.write-pause-rebuild-source-order-mismatch", + "resolved": true, + "title": "Reorder a history view's sources while its generation is interrupted", + "specKind": "example", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 0, + "blockingQuestions": 0 + }, + "questions": [], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:application.write-pause", + "resolved": true, + "title": "Write pause", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 14, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "writePauseSourceScopes", + "blocking": false, + "address": "spec:application.write-pause#question.writePauseSourceScopes", + "line": 39, + "question": "Extension E-8: the doc names a maintenance gate every writer reads, fencing or draining of background writers, and operator resume and abort; the option taken here is one gate document per deployment holding the closed scopes, each in one `closed` state, with no fence of its own and no drain interval, because optimistic concurrency serializes every writer's gate read against the close and the generation row's fence already fences a rebuild's batches, and a refusal answered with `writePaused`, the transient code the outcome boundary reserves for this contract under its E-35, which this contract confirms; the scope a paused rebuild closes is the paused view's sources, one `source:` entry per context and stream type its bindings name, because a consistent cut must hold every writer of those streams and a use case that writes a source stream without maintaining the view would otherwise pass the gate, which is why the command declaration names what it writes under E-38 and the pipeline proves the list at its step 9; an operator entry closes only a tenant or `all`, the coarser and always safe scopes, and no scope names a read model, because nothing would close it (E-8, E-38, D9, D4, F1)" + }, + { + "key": "generationGateExemption", + "blocking": false, + "address": "spec:application.write-pause#question.generationGateExemption", + "line": 40, + "question": "Extension E-8, the generation exemption: a batch reads the gate with the scopes `all` and its tenant, and no entry of either scope ever names a generation, so the exemption `gateAllows` makes for a given generation is reached by no reader; either the exemption goes, or a history batch reads the gate with its source scopes as well and passes them by it (E-8, D9)" + }, + { + "key": "writePauseOperatingInstructions", + "blocking": false, + "address": "spec:application.write-pause#question.writePauseOperatingInstructions", + "line": 41, + "question": "Should its operating instructions distinguish a stopped pass from a verified generation awaiting switch, using `getGenerations` and `getGate`, and measure the closed interval through the successful switch or abort? Keep any acceptable pause length for the owner's decision rather than deriving a timeout from a batch estimate." + } + ], + "decisions": [ + { + "id": "spec:decisions.d04-four-outcomes", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "component:platform.gate", + "file": "src/gate/index.ts", + "line": 3, + "role": null, + "component": null + }, + { + "id": "impl:application.write-pause", + "file": "src/gate/gate.ts", + "line": 8, + "role": "service", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + }, + { + "id": "impl:application.write-pause.entries", + "file": "src/gate/entries.ts", + "line": 7, + "role": "operator-entry", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + }, + { + "id": "impl:application.write-pause.tables", + "file": "src/gate/tables.ts", + "line": 4, + "role": "schema", + "component": { + "id": "component:platform.gate", + "layer": "infrastructure", + "context": "platform" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 1, + "enabled": 1 + }, + "examples": { + "total": 1, + "enabled": 1 + } + }, + { + "id": "spec:application.generation-registry", + "resolved": true, + "title": "Generation registry", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 20, + "declarations": 13, + "openQuestions": 6, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "generationRollbackPolicy", + "blocking": false, + "address": "spec:application.generation-registry#question.generationRollbackPolicy", + "line": 37, + "question": "Extension E-8: the doc names a generation registry, a marker and a gate without their shapes, and says to keep the old generation for a rollback period without saying whether live commands keep writing it; the option taken here is the `generations` table below with seven states, a rollback period of 7 days, one generation being built per read model at a time, and a retired generation that is kept and not written, so that a rollback reopens it as `verifying`, one verify pass repairs what the writes since its retirement changed, and the ordinary switch makes it active again; a rollback is therefore a verify pass and then a switch of one write, never one write by itself (E-8, D9, Sc L2-5)" + }, + { + "key": "generationTenantReach", + "blocking": false, + "address": "spec:application.generation-registry#question.generationTenantReach", + "line": 38, + "question": "Extension E-8, reach of a generation: the doc says nothing about tenants and rebuild; one generation per read model spans every tenant, the registry has no tenant dimension, one row is active for all tenants and the batches iterate the tenant list of `spec:command.actor-and-scope`; the registry is operational data of the deployment, so its indexes lead with the read model name and not with `tenantId`, a confirmed deviation from Law 11's index rule of the same kind the obligation module records under E-57, while every table that holds tenant data, the read-model rows among them, leads with the tenant; a rebuild for one tenant is not offered (E-8, D9, D11, Law 11, E-57)" + }, + { + "key": "restoreGenerationFence", + "blocking": false, + "address": "spec:application.generation-registry#question.restoreGenerationFence", + "line": 39, + "question": "Extension E-42, as it touches the registry: a restore brings back a generation row and its progress row at their checkpoint and drops the scheduled batch that was driving them, so the row's fence at inventory time is the restore's proof that the generation is still stranded, and `resumeChain` bumping it is the proof that it is not (E-42, F12)" + }, + { + "key": "readModelInstallation", + "blocking": false, + "address": "spec:application.generation-registry#question.readModelInstallation", + "line": 40, + "question": "Extension E-8, the first generation: the doc says how a generation is rebuilt and not how a read model's first generation comes to exist; the option taken here is that a read model is installed through its first rebuild, generation 1 starting as `building` like any other, so a read model added to a deployment that has history serves that history from its first switch and no transition claims coverage it did not build; a command that finds no generation to write fails as a technical failure and a query that finds no active generation fails the same way (E-8, D9, D19)" + }, + { + "key": "rebuildProgressRow", + "blocking": false, + "address": "spec:application.generation-registry#question.rebuildProgressRow", + "line": 41, + "question": "Extension E-40, the checkpoint: the cursor, the batch size and the counts live on the progress row and not on the generation row, because a batch that wrote its checkpoint on the row every command reads would re-run every command in flight on the read model once per batch; the progress row is the one checkpoint of every batch whatever drives it (E-40, F1)" + }, + { + "key": "tenantListGenerationStart", + "blocking": false, + "address": "spec:application.generation-registry#question.tenantListGenerationStart", + "line": 42, + "question": "Extension E-49, as it touches the registry: on a deployment whose grants predate the tenant list the list is empty while grants exist, and a generation started then visits no tenant; the option taken here is that `startGeneration` refuses then, with a message naming `fillTenants`, the operator entry of `spec:application.rebuild` that fills the list from the grants, and refuses while that fill runs (E-49, E-8, Law 11)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.generation-registry", + "file": "src/read-model/generations.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "id": "impl:application.generation-registry.tables", + "file": "src/read-model/tables.ts", + "line": 5, + "role": "schema", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 2, + "enabled": 2 + } + }, + { + "id": "spec:application.orders-inventory-example", + "resolved": true, + "title": "Orders and Inventory, the example domain", + "specKind": "model", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "orderInventoryCommandChoices", + "blocking": false, + "address": "spec:application.orders-inventory-example#question.orderInventoryCommandChoices", + "line": 36, + "question": "Extension E-46: the second lifecycle command, the essential summary, the command that creates stock, the unit price an order line carries, the bound on an order line's stock item ID, the rejection reasons, the stream budgets of the two stream types, the order of the two context calls and the history-dependent view Sc L2-6 builds are choices the doc leaves open; two of them are decided, that the stock item keeps totals only and `release` takes its quantities from the cancelled order's own lines, and that the order allocation history view stays in the experiment; the owner may substitute the others, and the experiment's terms move with them (E-46, E-2, E-9, First experiment)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d03-events-only-source-of-next-state", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.order-allocation-history", + "file": "example/domain/orderAllocationHistory.ts", + "line": 8, + "role": "projection", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "id": "impl:application.orders-inventory-example", + "file": "example/domain/order.ts", + "line": 10, + "role": "decider", + "component": { + "id": "component:example.orders-domain", + "layer": "domain", + "context": "orders" + } + }, + { + "id": "impl:application.orders-inventory-example.inventory-operations", + "file": "example/convex/inventory/operations.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "id": "impl:application.orders-inventory-example.inventory-schema", + "file": "example/convex/inventory/schema.ts", + "line": 1, + "role": "schema", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "id": "impl:application.orders-inventory-example.inventory-stock-item-query", + "file": "example/convex/inventory/queries/stockItem.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "id": "impl:application.orders-inventory-example.inventory-streams", + "file": "example/convex/inventory/streams.ts", + "line": 2, + "role": null, + "component": { + "id": "component:example.inventory-context", + "layer": "adapter", + "context": "inventory" + } + }, + { + "id": "impl:application.orders-inventory-example.order-summary", + "file": "example/convex/orderSummary.ts", + "line": 4, + "role": "read-model", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "id": "impl:application.orders-inventory-example.orders-operations", + "file": "example/convex/orders/operations.ts", + "line": 2, + "role": "service", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "id": "impl:application.orders-inventory-example.orders-order-query", + "file": "example/convex/orders/queries/order.ts", + "line": 2, + "role": "query", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "id": "impl:application.orders-inventory-example.orders-schema", + "file": "example/convex/orders/schema.ts", + "line": 1, + "role": "schema", + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "id": "impl:application.orders-inventory-example.orders-streams", + "file": "example/convex/orders/streams.ts", + "line": 3, + "role": null, + "component": { + "id": "component:example.orders-context", + "layer": "adapter", + "context": "orders" + } + }, + { + "id": "impl:application.orders-inventory-example.stock-item", + "file": "example/domain/stockItem.ts", + "line": 11, + "role": "decider", + "component": { + "id": "component:example.inventory-domain", + "layer": "domain", + "context": "inventory" + } + }, + { + "id": "impl:application.parent-use-cases", + "file": "example/convex/ordering.ts", + "line": 8, + "role": "use-case", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + }, + { + "id": "impl:application.parent-use-cases.receiving", + "file": "example/convex/receiving.ts", + "line": 4, + "role": "use-case", + "component": { + "id": "component:example.parent", + "layer": "application", + "context": "orders-inventory" + } + } + ], + "verifiers": { + "total": 6, + "enabled": 6 + }, + "examples": { + "total": 6, + "enabled": 6 + } + }, + { + "id": "spec:command.command-declaration", + "resolved": true, + "title": "Command declaration and bindings", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 19, + "declarations": 18, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "commandEntrySignatures", + "blocking": false, + "address": "spec:command.command-declaration#question.commandEntrySignatures", + "line": 38, + "question": "Extension E-7: the doc says a small composition helper and shared schema values; the helper signatures, the executor signature and the read-model binding below are the design's; the cost is two registered mutations per command instead of one, and the cheaper alternative the design refuted is one public export per command plus one generic internal dispatcher mutation keyed by command type, as D7's conditional dispatcher already does; it lost because trusted server callers would then reach every command through one untyped `internalMutation`, so a renamed handler or a broken argument contract would pass `tsc` at the caller and fail only at runtime, which is the check Sc L2-4 exists to keep before traffic, and because the receipt fingerprint's contract version would be looked up by string rather than typed; the owner may still prefer the dispatcher form once code generation makes the per-command internal entry free (E-7, D12, D7, Sc L2-4, Decision method rule 5)" + }, + { + "key": "commandDeclarationLookup", + "blocking": false, + "address": "spec:command.command-declaration#question.commandDeclarationLookup", + "line": 39, + "question": "Extension E-7: the pipeline receives the declaration and takes the fingerprint's contract version from it, and the type check at a typed caller is the check of Sc L2-4, so no standing map from command type to declaration exists; the conditional dispatcher of `spec:command.outcome-boundary` is the one thing that would need such a map and brings it with it; this is a provisional reading the owner rules (E-7, D12, D18, OQ2)" + }, + { + "key": "commandDeclarationFields", + "blocking": false, + "address": "spec:command.command-declaration#question.commandDeclarationFields", + "line": 40, + "question": "Extension E-38: the doc names five declaration fields; this design adds `contractVersion` for the fingerprint, `retention` and `irreversible` for receipts, `admission`, `bounds`, `rejections`, `audit`, `diagnosticSink` and `writes`, each serving a cited rule of another Spec, and asks whether the owner accepts them as part of the one declaration; `bounds` is the one input bound in the corpus, `maxItems` and `maxBytes`, checked by the pipeline's step 1 and by no executor body, so `spec:application.parent-use-cases` reads it rather than declaring a bound of its own; `writes` is required on every declaration, because an absent list would be an unchecked one, and `readModels` is optional, absent meaning none, which the owner has not ruled; `writes` names the context and stream type pairs the executor may write, from which the pipeline's step 7 derives the source scopes the write pause closes for a consistent cut and which step 9 proves against the `streams` entries the executor returns, because a use case that writes a source stream without maintaining the paused view would otherwise pass the gate; the declaration still imports nothing from the obligation family, so a command's static reactions are not a field here but the `createObligation` calls of its executor, as `spec:obligations.fan-out-and-chains` rules under E-54 (E-38, E-8, E-54, D6, D9, D10, D19, Thesis)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d12-one-declaration-per-command", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:command.command-declaration", + "file": "src/command/declaration.ts", + "line": 3, + "role": "contract", + "component": { + "id": "component:platform.command", + "layer": "application", + "context": "platform" + } + }, + { + "id": "impl:command.command-declaration.depot-commands", + "file": "fixture/convex/depotCommands.ts", + "line": 3, + "role": null, + "component": null + } + ], + "verifiers": { + "total": 2, + "enabled": 2 + }, + "examples": { + "total": 2, + "enabled": 2 + } + }, + { + "id": "spec:command.command-pipeline", + "resolved": true, + "title": "The command pipeline", + "specKind": "workflow", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 35, + "declarations": 18, + "openQuestions": 7, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "commandOutcomeWireShape", + "blocking": false, + "address": "spec:command.command-pipeline#question.commandOutcomeWireShape", + "line": 68, + "question": "Extension E-30: the doc names the outcome, the operation ID and the affected stream versions as what a command returns, but not a wire shape; the shape here is a union discriminated by `replayed`, whose executed member carries the declaration's result and whose replayed member carries `result` null because a receipt stores no result, so the entry's returns validator admits a replay as a value; affected IDs are application IDs, never private document IDs (E-30, D2, D6, D8)" + }, + { + "key": "operationIdentityCause", + "blocking": false, + "address": "spec:command.command-pipeline#question.operationIdentityCause", + "line": 69, + "question": "Extension E-31: the doc says an operation's ID ties together everything one request caused but not how it is minted, and it says a derived command carries causation and an event's `causedBy` may be a triggering event without saying how that cause reaches the pipeline; here the pipeline mints the operation ID once per top-level mutation with `crypto.randomUUID()` before the first context call, a duplicate returns the operation ID stored on the receipt, and the internal entry takes an optional `causedBy` argument that step 7 copies onto the `OperationRef` when present, so a reaction that issues a derived command passes `causedBy` of kind `event` naming its publication event and the envelope's `event` variant has exactly one producer, while an entry called without it mints `causedBy` of kind `command` from the declaration's name; the obligation and attempt behind a derived command are found through its receipt, whose request key is the obligation's effect key, and never through the envelope (E-31, E-54, D2, D13, Vocab: operation)" + }, + { + "key": "commandAdmissionPolicy", + "blocking": false, + "address": "spec:command.command-pipeline#question.commandAdmissionPolicy", + "line": 70, + "question": "Extension E-32: the doc places rate admission for new intent after the duplicate check and makes a rate or capacity refusal a retryable error, but names no admission mechanism; here admission is an optional per-command policy in the declaration, absent by default, evaluated inside the mutation so that a refusal that throws leaves no consumption behind; the flip side is that a consumption the policy writes commits only with the command, so the policy counts committed intent and cannot bound refused or unauthorized traffic, and a product that must bound refused traffic needs the dispatcher form of D7, where the parent survives the body's rejection (E-32, D4, D6, D7)" + }, + { + "key": "publicRequestKey", + "blocking": false, + "address": "spec:command.command-pipeline#question.publicRequestKey", + "line": 71, + "question": "Extension E-33: the doc lists the callers that need receipts and leaves UI commands to the client guarantee; here a public caller may still supply a request key to opt into a receipt, which Probe 1 showed a caller of `ConvexHttpClient` needs when it retries, and the internal entry always requires one (E-33, D6, Probe 1)" + }, + { + "key": "contextCallBudgetReservation", + "blocking": false, + "address": "spec:command.command-pipeline#question.contextCallBudgetReservation", + "line": 72, + "question": "Probe 4 showed on the pinned backend that the transaction limits are one budget across the parent and its component sub-transactions, so the per-command bounds are checked against one budget; `ctx.runMutation` accepts a `transactionLimits` option, the design passes none on a context call, and whether a call should reserve room with it for the work after it stays open (Probe 4, F13, S3)" + }, + { + "key": "commandReadModelWrites", + "blocking": false, + "address": "spec:command.command-pipeline#question.commandReadModelWrites", + "line": 73, + "question": "Extension E-9: the doc says essential read models are updated inside the command and does not say from what or in what order; here step 9 writes a per-entity read model from the DTO of each matching `streams` entry, never from its events, and a history read model from the events of every matching entry in one call per command, checks `writes` first, then that each bound read model has a generation to write, then the count of the rows it writes against `limitReadModelWritesPerCommand`, each failure a plain error, and the library helpers that step 9 calls are generic over the application's data model; the owner has not ruled on any of it (E-9, E-7, E-8, E-45, D8, D9)" + }, + { + "key": "commandTextBounds", + "blocking": false, + "address": "spec:command.command-pipeline#question.commandTextBounds", + "line": 74, + "question": "Extension E-34: step 1 bounds every text field a caller sets beside the input, the tenant ID, the request key, the correlation ID, the actor's fields and the fields of `causedBy`, in UTF-8 bytes as `limitCallText` lists them, as rejection `invalidInput`, and treats a declaration name above `limitIdLength` as a defect, a plain error, because these fields become fields of the receipt row or of every event's envelope; the doc says events stay small and names no number; the owner has not ruled (E-34, D6, D12, D19)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d04-four-outcomes", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d12-one-declaration-per-command", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:application.read-models", + "file": "src/read-model/write.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.read-model", + "layer": "application", + "context": "platform" + } + }, + { + "id": "impl:command.command-pipeline", + "file": "src/command/pipeline.ts", + "line": 3, + "role": "service", + "component": { + "id": "component:platform.command", + "layer": "application", + "context": "platform" + } + } + ], + "verifiers": { + "total": 3, + "enabled": 3 + }, + "examples": { + "total": 3, + "enabled": 3 + } + }, + { + "id": "spec:context.event-envelope", + "resolved": true, + "title": "The event envelope", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 16, + "declarations": 10, + "openQuestions": 2, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "eventEnvelopeFields", + "blocking": false, + "address": "spec:context.event-envelope#question.eventEnvelopeFields", + "line": 31, + "question": "Extension E-26: the doc lists the fifteen fields and their meaning; this Spec fixes the validator shapes, the `CausedBy` union with a migration variant for baseline events, the `OperationRef` the parent passes, and the event ID as a version 4 UUID from the runtime's `crypto`; the owner confirms (D2, E-26)" + }, + { + "key": "envelopeTextBounds", + "blocking": false, + "address": "spec:context.event-envelope#question.envelopeTextBounds", + "line": 32, + "question": "Extension E-12: the doc says events stay small and gives no number for the envelope; this Spec bounds the envelope beside the payload at `limitEnvelopeBytes`, 4,096 bytes, and each text field of it that a caller can set at 256 bytes of UTF-8, 512 for an actor's ID, so the largest stored event, `limitEventBytes`, is derived and enforced and never assumed; the owner may set other numbers, and the fold bound of `spec:context.journal` moves with them (E-12, D19, F13)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [ + { + "id": "impl:context.event-envelope", + "file": "src/context/envelope.ts", + "line": 2, + "role": "contract", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:context.journal", + "resolved": true, + "title": "The journal", + "specKind": "behavior", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 39, + "declarations": 18, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "baselineMigrationContract", + "blocking": false, + "address": "spec:context.journal#question.baselineMigrationContract", + "line": 51, + "question": "Extension E-25: the doc says a migration writes a baseline event holding the migrated state and that rebuild starts from the latest baseline, and it does not say who computes that state, who drives the migration, or what a live command does on a stream the migration has not reached; this Spec fixes the representation, an event of the reserved type `baseline` whose payload holds the state and its schema version and is bounded by the stream's `budgetBytes` instead of the payload bound, a `baselineVersion` on the stream row, a `BaselineMigration` registered on the stream type whose `migrate` function computes the migrated state from the saved state, or, for a step that declares `readsHistory`, from the saved state and the events since the last baseline in the sweep only, the rule that `stateSchemaVersion` counts meaning only and lives on the registration, one helper `migrateToSchemaVersion` that writes the baseline chain, called by the adapter on load for a chain that reads no history and by the `writeBaseline` maintenance mutation for every chain, the code-older-than-data error, the `writeBaseline` mutation built by `defineMaintenance` and declared with the component's public `mutation` builder, the parent-side scheduled chain that drives it by cursor, the batch bound derived from `budgetBytes`, the upcasting rule for old event schema versions, the fold bound of 500 events with the refusals `rebuildStream` returns above it and for a stream awaiting its baseline, the retirement rule for a migration, and the payload byte check in `append`; the option of refusing commands on unmigrated streams with a transient code until the sweep arrives was not taken for a state-only migration, because it turns a deploy into a partial write pause the doc reserves for history-dependent views, and is taken for a history-reading migration, the exception, where it holds only the streams the sweep has not reached and keeps D3 whole on the command path, which the owner confirmed; a consequence the owner should weigh is that every baseline bumps its stream's version, so a reviewed version held across the sweep is answered `staleVersion` and re-reviewed rather than silently accepted, because Sc L1-10 forbids reinterpreting a reviewed version against fresh state and the baseline is a real event of the stream, while a live stream migrated on load never trips it because its expected-version check runs before the chain; and the driver reads the write pause's gate before every batch and reschedules itself when refused, so a sweep never changes a source under a paused rebuild's cut; the owner confirms (D5, D9, S4, Sc L1-10, E-2, E-8, E-12, E-25)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "statedReadiness": "defined", + "resolved": true + }, + { + "id": "spec:kernel.state-document-mapping", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:context.journal", + "file": "src/context/journal.ts", + "line": 2, + "role": "event-store", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "verifiers": { + "total": 1, + "enabled": 0 + }, + "examples": { + "total": 1, + "enabled": 0 + } + }, + { + "id": "spec:context.queries", + "resolved": true, + "title": "The context queries", + "specKind": "contract", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 29, + "declarations": 18, + "openQuestions": 3, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "contextPaginationSplit", + "blocking": false, + "address": "spec:context.queries#question.contextPaginationSplit", + "line": 41, + "question": "Probe 5 showed on the pinned backend with `convex-helpers` 0.1.124 that the parent query stays reactive, that pages pinned by their end cursors stay contiguous, that a capped page crosses the boundary as `SplitRequired` with its `splitCursor`, and that the `usePaginatedQuery` hook of `convex-helpers/react` then shows 40 of 45 rows; this design uses no hook across the boundary, and the examples are rerun when either pin moves (Probe 5, F15, S4, D8, E-24)" + }, + { + "key": "contextQueryContract", + "blocking": false, + "address": "spec:context.queries#question.contextQueryContract", + "line": 42, + "question": "Extension E-24: the doc says component queries are authorized and return DTOs; this Spec fixes one definer per query, the one `tenantId` argument with no separate scope, the `toDto` mapping with the stream version on every DTO, pagination through `paginator`, the item cap with a row cap of twice the item cap and a byte cap passed on every run, the relay of `pageStatus` and `splitCursor` unchanged, paging by an explicit cursor pair with no client hook, the parent query's one `authorizeQuery` call and its refusal as a rejection, the `maxEvents` argument of the `rebuild` query and its `budgetExhausted` refusal for a batching caller, and the fold refusal of the `rebuild` query; this is a provisional reading the owner has not ruled (D8, S4, E-24, E-42)" + }, + { + "key": "queryRefusalEntry", + "blocking": false, + "address": "spec:context.queries#question.queryRefusalEntry", + "line": 43, + "question": "A parent query's refusal carries the name of the entry that refused in the field `entry` of the rejection data; the wire shape stays `RejectionData` of `spec:command.outcome-boundary` unchanged, and the owner confirmed the word (D7, E-24)" + } + ], + "decisions": [ + { + "id": "spec:decisions.d08-read-models-in-command", + "statedReadiness": "defined", + "resolved": true + } + ], + "implementedBy": [], + "referencedBy": [ + { + "id": "impl:context.queries", + "file": "src/context/queries.ts", + "line": 3, + "role": "query", + "component": { + "id": "component:platform.context", + "layer": "adapter", + "context": "platform" + } + } + ], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + }, + { + "id": "spec:decisions.d09-rebuild-online-by-default", + "resolved": true, + "title": "Rebuild online by default, pause writes only for history-dependent views", + "specKind": "decision", + "statedReadiness": "defined", + "floorReached": "ready", + "statedNextRung": "ready", + "statedNextRungFailures": [], + "design": { + "entries": 0, + "declarations": 0, + "openQuestions": 1, + "blockingQuestions": 0 + }, + "questions": [ + { + "key": "rebuildContentionCoverage", + "blocking": false, + "address": "spec:decisions.d09-rebuild-online-by-default#question.rebuildContentionCoverage", + "line": 30, + "question": "Probe 6 ran a backfill batch racing a live command and the migrations component on the native backend, with its results on F17; a hosted deployment and sustained contention are not run (Probe 6, F1, F17, D9)" + } + ], + "decisions": [], + "implementedBy": [], + "referencedBy": [], + "verifiers": { + "total": 0, + "enabled": 0 + }, + "examples": { + "total": 0, + "enabled": 0 + } + } + ], + "boundary": { + "restsOn": [ + { + "id": "spec:application.first-experiment", + "title": "The first experiment", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:command.actor-and-scope", + "title": "Actor, tenant scope, namespace and grants", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.idempotency-and-receipts", + "title": "Idempotency and receipts", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.outcome-boundary", + "title": "The outcome and rejection boundary", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.receipt-table", + "title": "The receipts table", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.tenancy-and-authority", + "title": "Tenancy and authority", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:constraints.bulk-operations-bounded", + "title": "Every bulk operation has a stated bound", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:constraints.events-stay-small", + "title": "Events stay small", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:constraints.no-queue-recovery-for-essential-reads", + "title": "No queue recovery needed for essential reads", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:constraints.zero-core-projection-jobs", + "title": "Zero core projection jobs", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:context.batch-shaped-api", + "title": "Batch-shaped context APIs", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:context.context-component", + "title": "The context component", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:context.journal", + "spec:context.queries" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.persistence-adapter", + "title": "The persistence adapter", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.tables", + "title": "The context tables", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d01-one-mutation-per-operation", + "title": "One mutation per business operation", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.read-models", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d02-context-owns-state-and-journal", + "title": "Each bounded context owns its state and its journal", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d03-events-only-source-of-next-state", + "title": "Events are the only source of the next state", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:decisions.d04-four-outcomes", + "title": "Four outcomes, not a boolean", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d05-rebuildable-history-with-baselines", + "title": "Rebuildable history by default, with baseline events", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:decisions.d06-idempotency-client-and-receipts", + "title": "Convex covers UI retries, receipts cover the rest", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d07-rejections-thrown-not-stored", + "title": "Rejections are thrown, not stored, at the public boundary", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d08-read-models-in-command", + "title": "Read models update inside the command", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:decisions.d10-contexts-meet-in-parent-use-cases", + "title": "Contexts meet only in parent use cases", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d11-tenant-scope-and-authority", + "title": "Tenant scope and authority from day one", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d12-one-declaration-per-command", + "title": "One declaration per command, generated bindings later", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "title": "Everything else waits for its trigger", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection" + ] + } + ] + }, + { + "id": "spec:decisions.d19-operations-travel-with-capability", + "title": "Operations travel with the capability", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:application.history-projection", + "spec:application.rebuild", + "spec:application.history-rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f01-serializable-mutations-under-occ", + "title": "Mutations are serializable under optimistic concurrency", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:facts.f02-component-calls-commit-with-caller", + "title": "Component calls commit or roll back with the calling mutation", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f03-nested-run-mutation-partial-rollback", + "title": "A nested runMutation gives partial rollback", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:facts.f04-nested-calls-cost-more-than-helpers", + "title": "Nested calls cost more than helper calls", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:facts.f07-queries-reactive-not-durable-delivery", + "title": "Queries are reactive, subscriptions are not durable delivery", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f08-scheduling-commits-with-mutation", + "title": "Scheduling from a mutation commits with it", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f09-scheduled-mutation-and-action-retry-semantics", + "title": "Scheduled mutations run once, scheduled actions are not retried", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f11-components-have-no-ctx-auth", + "title": "Components have no ctx.auth", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.write-pause", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f12-backups-exclude-pending-scheduled-functions", + "title": "Backups exclude pending scheduled functions", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.write-pause", + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:facts.f13-transactions-have-limits", + "title": "Transactions have limits", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f14-convex-error-survives-nested-and-component-boundary", + "title": "ConvexError data survives a nested mutation and a component boundary", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:facts.f15-parent-query-over-component-query-stays-reactive", + "title": "A parent query over a component query stays reactive", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.rebuild", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:facts.f17-migrations-fits-generation-backfill", + "title": "The migrations component fits a generation backfill", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.rebuild", + "spec:application.generation-registry", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:facts.f18-commit-timestamps", + "title": "Commit timestamps order committed transactions", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:facts.f19-nested-calls-share-time-budgets", + "title": "Nested calls draw on a system-operation time budget", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-rebuild" + ] + } + ] + }, + { + "id": "spec:facts.f20-pagination-checks-bytes-after-reading", + "title": "Pagination checks bytes after reading", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:kernel.outcome-model", + "title": "The outcome model", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:kernel.state-document-mapping", + "title": "One document per stream, with a derived mapping above the budget", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "decidedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law01-sanctioned-writes-only", + "title": "Business writes go only through sanctioned operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law02-state-and-events-commit-together", + "title": "State and its required events commit together", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law03-events-only-source-of-state", + "title": "Events are the only source of the next state", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law04-server-scoped-idempotency-key", + "title": "A retryable command carries a server-scoped idempotency key", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law05-authorization-before-execution-and-disclosure", + "title": "Authorization comes before execution and disclosure", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:command.command-pipeline", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:laws.law06-technical-failure-never-a-rejection", + "title": "A technical failure is never stored as a rejection", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law09-no-invariant-on-late-read-model", + "title": "No invariant depends on a late read model", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.read-models", + "spec:application.orders-inventory-example", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:laws.law10-replay-never-runs-commands-or-effects", + "title": "Replay never runs commands or repeats effects", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.history-projection", + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:laws.law11-tenant-scope-named", + "title": "Every command and query names its tenant scope", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "constrainedBy", + "members": [ + "spec:application.projection-contract", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.generation-registry", + "spec:command.command-pipeline", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:operations.baseline-operations", + "title": "Baseline operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:platform.transactional-domain-platform", + "title": "Convex transactional domain platform", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + } + ], + "restedOnBy": [ + { + "id": "spec:advanced.cross-stream-online-rebuild", + "title": "Online rebuild of cross-stream history views", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:agents.agent-runs", + "title": "Agent runs", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:application.aggregate-projection", + "title": "Aggregate projection", + "statedReadiness": "scoped", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:application.projection-contract", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + }, + { + "type": "decidedBy", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:application.first-experiment", + "title": "The first experiment", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.history-projection", + "spec:application.read-models", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.generation-registry", + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.command-without-generation-fails", + "title": "A command whose read model has no generation fails", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.first-rebuild-installs-read-model", + "title": "A read model is installed through its first rebuild", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-duplicate-answered-from-receipt", + "title": "A repeated cancel request is answered from its receipt", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-placed-order", + "title": "A placed order is cancelled", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-second-context-rejects", + "title": "Orders cancels, then Inventory refuses the release", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-twice-rejected", + "title": "A second cancel of one order is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-unknown-order-rejected", + "title": "A cancel of an order never placed is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-without-grant-refused", + "title": "A caller without the cancel grant is refused", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.parent-use-cases", + "title": "Parent use cases", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-read-through-query", + "title": "A successful command, then a query", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-visible", + "title": "A successful command, then a query or subscription", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.list-pages-by-cursor", + "title": "A client reads a tenant's order list in pages by cursor", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-before-disclosure", + "title": "A caller with no identity reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-without-grant", + "title": "A caller with no grant in the tenant reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.rebuild.online-rebuild-interrupt-resume", + "title": "Rebuild a per-entity read model online while commands run; interrupt and resume", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:application.restore", + "title": "Restore", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.projection-contract", + "spec:application.rebuild", + "spec:application.history-rebuild", + "spec:application.write-pause", + "spec:application.generation-registry", + "spec:context.event-envelope", + "spec:context.journal", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:application.write-pause.restore-door-without-deploy", + "title": "The restore door closes and opens with no deploy", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.broken-argument-contract-fails-build", + "title": "Break a typed handler's argument contract", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.renamed-handler-fails-build", + "title": "Rename a typed handler", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-journal-append", + "title": "Inject a failure after the journal append", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-state-write", + "title": "Inject a failure after the state write", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-before-receipt", + "title": "Inject a failure before the receipt insert", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.idempotency-and-receipts", + "title": "Idempotency and receipts", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + }, + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.outcome-boundary", + "title": "The outcome and rejection boundary", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "refines", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.receipt-table", + "title": "The receipts table", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause", + "spec:context.queries" + ] + } + ] + }, + { + "id": "spec:command.tenancy-and-authority", + "title": "Tenancy and authority", + "statedReadiness": "defined", + "implemented": true, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:context.context-component", + "title": "The context component", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:context.journal.rebuild-from-baseline", + "title": "Rebuild a stream that has a baseline event", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "refines", + "members": [ + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:context.persistence-adapter", + "title": "The persistence adapter", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope", + "spec:context.journal" + ] + } + ] + }, + { + "id": "spec:context.tables", + "title": "The context tables", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:decisions.d18-everything-else-waits-for-trigger", + "title": "Everything else waits for its trigger", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:decisions.d09-rebuild-online-by-default" + ] + } + ] + }, + { + "id": "spec:obligations.fan-out-and-chains", + "title": "Fan-out and reaction chains", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-declaration", + "spec:command.command-pipeline", + "spec:context.event-envelope" + ] + } + ] + }, + { + "id": "spec:obligations.local-reaction-wrapper", + "title": "Local reaction wrapper", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:obligations.obligation-module", + "title": "Obligation module", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:operations.baseline-operations", + "title": "Baseline operations", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "dependsOn", + "members": [ + "spec:context.event-envelope" + ] + } + ] + } + ], + "verifiedBy": [ + { + "id": "spec:application.generation-registry.command-without-generation-fails", + "title": "A command whose read model has no generation fails", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.generation-registry.first-rebuild-installs-read-model", + "title": "A read model is installed through its first rebuild", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.generation-registry" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-duplicate-answered-from-receipt", + "title": "A repeated cancel request is answered from its receipt", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-placed-order", + "title": "A placed order is cancelled", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-second-context-rejects", + "title": "Orders cancels, then Inventory refuses the release", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-twice-rejected", + "title": "A second cancel of one order is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-unknown-order-rejected", + "title": "A cancel of an order never placed is rejected", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.orders-inventory-example.cancel-without-grant-refused", + "title": "A caller without the cancel grant is refused", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.orders-inventory-example" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-read-through-query", + "title": "A successful command, then a query", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.committed-state-visible", + "title": "A successful command, then a query or subscription", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.list-pages-by-cursor", + "title": "A client reads a tenant's order list in pages by cursor", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-before-disclosure", + "title": "A caller with no identity reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.read-models.query-refused-without-grant", + "title": "A caller with no grant in the tenant reads an order through the parent", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.read-models" + ] + } + ] + }, + { + "id": "spec:application.rebuild.online-rebuild-interrupt-resume", + "title": "Rebuild a per-entity read model online while commands run; interrupt and resume", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.rebuild" + ] + } + ] + }, + { + "id": "spec:application.write-pause.restore-door-without-deploy", + "title": "The restore door closes and opens with no deploy", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:application.write-pause" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.broken-argument-contract-fails-build", + "title": "Break a typed handler's argument contract", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-declaration.renamed-handler-fails-build", + "title": "Rename a typed handler", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-declaration" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-journal-append", + "title": "Inject a failure after the journal append", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-after-state-write", + "title": "Inject a failure after the state write", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:command.command-pipeline.failure-before-receipt", + "title": "Inject a failure before the receipt insert", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:command.command-pipeline" + ] + } + ] + }, + { + "id": "spec:context.journal.rebuild-from-baseline", + "title": "Rebuild a stream that has a baseline event", + "statedReadiness": "defined", + "implemented": false, + "via": [ + { + "type": "verifies", + "members": [ + "spec:context.journal" + ] + } + ] + } + ], + "supersededBy": [] + } +} diff --git a/explorations/studio-prototype/design-management-v1.html b/explorations/studio-prototype/design-management-v1.html new file mode 100644 index 00000000..abd3162f --- /dev/null +++ b/explorations/studio-prototype/design-management-v1.html @@ -0,0 +1,42 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>Pack · Spec Studio prototype + + + + +
+

+ A prototype of the Spec Studio's Pack page, rendered from recipe 29's answer and the Pack + context of this repository's corpus. It holds this page only, so links to the index, search and Spec pages + point where the build will write them and do not open here. The same page for the other + Pack: history-view. +

+
+
+ Spec Studio + +
+ + + +
+
+
+ +
+ +
+
+ + + + diff --git a/explorations/studio-prototype/history-view.html b/explorations/studio-prototype/history-view.html new file mode 100644 index 00000000..45fe3192 --- /dev/null +++ b/explorations/studio-prototype/history-view.html @@ -0,0 +1,42 @@ + + + + + + Pack · Spec Studio prototype + + + + +
+

+ A prototype of the Spec Studio's Pack page, rendered from recipe 29's answer and the Pack + context of the first adopter's corpus. It holds this page only, so links to the index, search and Spec pages + point where the build will write them and do not open here. The same page for the other + Pack: design-management-v1. +

+
+
+ Spec Studio + +
+ + + +
+
+
+ +
+ +
+
+ + + + diff --git a/explorations/studio-prototype/studio.css b/explorations/studio-prototype/studio.css new file mode 100644 index 00000000..2e6055e9 --- /dev/null +++ b/explorations/studio-prototype/studio.css @@ -0,0 +1,509 @@ +/* The Studio's Pack page, prototype. Hand-written, no build step, no network. */ + +:root { + --bg: #fbfbfa; + --surface: #ffffff; + --surface-2: #f3f4f2; + --text: #1d2125; + --muted: #5b636b; + --line: #d9dcd8; + --link: #1f5fa8; + --focus: #c25e00; + --warn-bg: #fff4d6; + --warn-text: #6b4500; + --warn-line: #e0b54a; + --code-bg: #eef0ec; + /* The rung palette: one hue in four lightness steps, so it stays ordered under any color + vision; the word is always inside the badge. */ + --rung-idea-bg: #eef3f9; + --rung-idea-text: #26425f; + --rung-scoped-bg: #c8daf0; + --rung-scoped-text: #1c3854; + --rung-defined-bg: #5d8fca; + --rung-defined-text: #ffffff; + --rung-ready-bg: #1f4878; + --rung-ready-text: #ffffff; + --measure: 80ch; + --radius: 6px; + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #15181b; + --surface: #1c2024; + --surface-2: #23282d; + --text: #e6e8ea; + --muted: #a3abb3; + --line: #353b41; + --link: #8cbcf2; + --focus: #ffb062; + --warn-bg: #3a2e12; + --warn-text: #f3d68f; + --warn-line: #8a6a1e; + --code-bg: #262c31; + --rung-idea-bg: #26313d; + --rung-idea-text: #d6e3f2; + --rung-scoped-bg: #2f4b6b; + --rung-scoped-text: #e4eefa; + --rung-defined-bg: #4c7fbf; + --rung-defined-text: #ffffff; + --rung-ready-bg: #9cc3f0; + --rung-ready-text: #0d2238; + color-scheme: dark; + } +} + +* { + box-sizing: border-box; +} + +html { + font-size: 100%; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font: + 15px/1.55 system-ui, + -apple-system, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + sans-serif; +} + +a { + color: var(--link); +} + +a:focus-visible, +button:focus-visible, +summary:focus-visible, +input:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +code { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.88em; + background: var(--code-bg); + border-radius: 3px; + padding: 0.05em 0.3em; + overflow-wrap: anywhere; +} + +.skip-link { + position: absolute; + left: -999px; + top: 0; + background: var(--surface); + padding: 0.5rem 1rem; + z-index: 10; +} + +.skip-link:focus { + left: 1rem; +} + +/* The prototype's own note, which the product does not have. */ +.prototype-note { + background: var(--warn-bg); + color: var(--warn-text); + border-bottom: 1px solid var(--warn-line); + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +.prototype-note p { + margin: 0; + max-width: 110ch; +} + +/* The shell. */ +.top { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 1.25rem; + align-items: center; + padding: 0.6rem 1rem; + border-bottom: 1px solid var(--line); + background: var(--surface); +} + +.top .brand { + font-weight: 650; + text-decoration: none; + color: var(--text); +} + +.top nav a { + margin-right: 0.9rem; +} + +.top form { + display: flex; + gap: 0.4rem; + margin-left: auto; +} + +.top input[type="search"] { + font: inherit; + padding: 0.3rem 0.5rem; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--bg); + color: var(--text); + width: 16rem; + max-width: 50vw; +} + +button { + font: inherit; + font-size: 0.9rem; + padding: 0.3rem 0.7rem; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--surface-2); + color: var(--text); + cursor: pointer; +} + +.layout { + display: grid; + grid-template-columns: minmax(14rem, 18rem) minmax(0, 1fr); + gap: 0 2rem; + max-width: 82rem; + margin: 0 auto; + padding: 1rem; +} + +.rail { + font-size: 0.9rem; + position: sticky; + top: 0.5rem; + align-self: start; + max-height: calc(100vh - 1rem); + overflow: auto; + padding-right: 0.5rem; +} + +.rail h2 { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--muted); + margin: 1rem 0 0.4rem; +} + +.rail ol, +.rail ul { + margin: 0; + padding-left: 1.4rem; +} + +.rail li { + margin: 0.2rem 0; +} + +main { + min-width: 0; +} + +.page-head .eyebrow { + margin: 0; + color: var(--muted); + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +h1 { + font-size: 1.7rem; + line-height: 1.25; + margin: 0.15rem 0 0.5rem; +} + +h2 { + font-size: 1.25rem; + margin: 2rem 0 0.6rem; +} + +h3 { + font-size: 1rem; + margin: 1rem 0 0.35rem; +} + +.prose, +.prose p, +.summary { + max-width: var(--measure); +} + +.ids { + display: flex; + flex-wrap: wrap; + gap: 0.3rem 1rem; + align-items: center; + margin: 0.2rem 0 0.8rem; + color: var(--muted); +} + +.summary { + margin: 0; + padding-left: 1.2rem; +} + +.summary li { + margin: 0.15rem 0; +} + +.summary strong { + font-variant-numeric: tabular-nums; +} + +.toolbar { + display: flex; + gap: 0.5rem; + align-items: center; + margin: 0.4rem 0 0.8rem; +} + +/* Rung badges and the other words that carry a status. */ +.badge { + display: inline-block; + font-size: 0.78rem; + line-height: 1.4; + padding: 0.05rem 0.45rem; + border-radius: 999px; + border: 1px solid var(--line); + white-space: nowrap; +} + +.rung-idea { + background: var(--rung-idea-bg); + color: var(--rung-idea-text); + border-color: transparent; +} + +.rung-scoped { + background: var(--rung-scoped-bg); + color: var(--rung-scoped-text); + border-color: transparent; +} + +.rung-defined { + background: var(--rung-defined-bg); + color: var(--rung-defined-text); + border-color: transparent; +} + +.rung-ready { + background: var(--rung-ready-bg); + color: var(--rung-ready-text); + border-color: transparent; +} + +.floor { + color: var(--muted); + font-size: 0.85rem; + white-space: nowrap; +} + +.kind { + font-size: 0.8rem; + color: var(--muted); + white-space: nowrap; +} + +.diverges { + background: var(--warn-bg); + color: var(--warn-text); + border-color: var(--warn-line); +} + +.held { + background: var(--warn-bg); + color: var(--warn-text); + border-color: var(--warn-line); +} + +/* Member cards. */ +.card { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius); + margin: 0.45rem 0; +} + +.card:target, +.card:has(:target) { + border-color: var(--focus); +} + +.card > summary { + cursor: pointer; + padding: 0.55rem 0.8rem; + list-style-position: outside; + margin-left: 1rem; +} + +.card > summary .line1 { + font-weight: 600; +} + +.card > summary .line2 { + display: flex; + flex-wrap: wrap; + gap: 0.25rem 0.5rem; + align-items: center; + margin-top: 0.25rem; + font-weight: 400; +} + +.card-body { + padding: 0.2rem 1rem 0.9rem 1.8rem; + border-top: 1px solid var(--line); +} + +.card-body h3 { + font-size: 0.95rem; + color: var(--muted); + margin-top: 0.9rem; +} + +.card-body p, +.card-body li { + max-width: var(--measure); +} + +.questions { + padding-left: 1.2rem; + margin: 0.3rem 0; +} + +.questions > li { + margin: 0.5rem 0; +} + +.field { + display: block; + margin-top: 0.15rem; + font-size: 0.9rem; +} + +.field b { + font-weight: 600; + color: var(--muted); +} + +.units { + list-style: none; + padding: 0; + margin: 0.2rem 0; +} + +.units li { + margin: 0.35rem 0; +} + +.loc { + white-space: nowrap; +} + +/* Sections that close: the boundary and the lists after the cards. */ +.section { + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--surface); + margin: 0.45rem 0; +} + +.section > summary { + cursor: pointer; + padding: 0.5rem 0.8rem; + font-weight: 600; +} + +.section > .section-body { + padding: 0 0.8rem 0.8rem; +} + +.table-scroll { + overflow-x: auto; +} + +table { + border-collapse: collapse; + width: 100%; + font-size: 0.9rem; +} + +th, +td { + text-align: left; + vertical-align: top; + padding: 0.35rem 0.5rem; + border-bottom: 1px solid var(--line); +} + +th { + color: var(--muted); + font-weight: 600; +} + +.plain-list { + padding-left: 1.2rem; +} + +sdp-copy button { + font-size: 0.75rem; + padding: 0.05rem 0.45rem; + margin-left: 0.25rem; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +/* Below the phone breakpoint every layout is one column and the rail follows the canvas. */ +@media (max-width: 720px) { + .layout { + grid-template-columns: minmax(0, 1fr); + } + + .rail { + position: static; + max-height: none; + order: 2; + border-top: 1px solid var(--line); + margin-top: 2rem; + } + + main { + order: 1; + } + + .top form { + margin-left: 0; + width: 100%; + } + + .top input[type="search"] { + flex: 1; + max-width: none; + } + + .card-body { + padding-left: 1rem; + } +} diff --git a/explorations/studio-prototype/studio.js b/explorations/studio-prototype/studio.js new file mode 100644 index 00000000..ce0717d4 --- /dev/null +++ b/explorations/studio-prototype/studio.js @@ -0,0 +1,512 @@ +// The Studio's Pack page, prototype. A classic script with no build step and no request: a +// browser refuses a module script on a page opened through file://. +// +// Two halves, split the way the product's build lanes split: +// 1. The template. It turns recipe 29's answer and the Pack context into the page's markup. +// In the product the build writes this markup at publish time and the page needs no script +// to read it; here the script stands in for the build. +// 2. The elements. sdp-expander and sdp-copy enhance markup that is already on the page, as the +// page elements Spec designs them. +(function () { + "use strict"; + + /* ------------------------------------------------------------------------------------------ */ + /* 1. The template */ + /* ------------------------------------------------------------------------------------------ */ + + var RUNGS = ["idea", "scoped", "defined", "ready"]; + + function esc(value) { + return String(value).replace(/[&<>"']/g, function (c) { + return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; + }); + } + + // The page-set path rule: the id after its namespace, each uppercase ASCII letter written as + // `_` and its lowercase. + function pageName(id) { + return id.slice(id.indexOf(":") + 1).replace(/[A-Z]/g, function (c) { + return "_" + c.toLowerCase(); + }); + } + + // Links are written as they would be from `packs/.html`. This prototype holds only the + // Pack page, so a link to a Spec page does not resolve here. + function specHref(address) { + var hash = address.indexOf("#"); + var id = hash === -1 ? address : address.slice(0, hash); + return "../specs/" + pageName(id) + ".html" + (hash === -1 ? "" : "#" + address.slice(hash + 1)); + } + + function cardHref(id) { + return "#" + id; + } + + // A mention: `spec:` after a character that is not a letter, digit or `-`, ending before + // trailing punctuation, close to the prose-mentions validator's token rule. + var MENTION = /(^|[^A-Za-z0-9-])(spec:[A-Za-z0-9.#-]*[A-Za-z0-9])/g; + + function linkMentions(html) { + return html.replace(MENTION, function (_match, before, id) { + return before + '' + id + ""; + }); + } + + // Authored text: plain text, inline code spans as code, mentions as links, nothing else read. + function inline(text) { + var out = ""; + var re = /`([^`]+)`/g; + var last = 0; + var match; + while ((match = re.exec(text)) !== null) { + out += linkMentions(esc(text.slice(last, match.index))); + out += "" + linkMentions(esc(match[1])) + ""; + last = re.lastIndex; + } + return out + linkMentions(esc(text.slice(last))); + } + + function prose(text) { + return text + .split(/\n\s*\n/) + .map(function (paragraph) { + return "

" + inline(paragraph) + "

"; + }) + .join(""); + } + + function count(n, one, many) { + return n === 0 ? "no " + many : n + " " + (n === 1 ? one : many); + } + + function capital(text) { + return text.charAt(0).toUpperCase() + text.slice(1); + } + + function rungBadge(rung) { + return rung === null || rung === undefined || rung === "none" + ? 'no rung' + : '' + esc(rung) + ""; + } + + function stated(rung, floor) { + var diverges = RUNGS.indexOf(floor) < RUNGS.indexOf(rung); + return ( + rungBadge(rung) + + ' floor ' + + esc(floor) + + "" + + (diverges ? ' stated above its floor' : "") + ); + } + + function nextRung(member) { + if (member.statedNextRung === null) return { text: "states ready", held: false }; + var n = member.statedNextRungFailures.length; + return n === 0 + ? { text: "next rung " + member.statedNextRung + ": nothing blocks", held: false } + : { + text: "next rung " + member.statedNextRung + ": held by " + count(n, "clause", "clauses"), + held: true, + }; + } + + function copy(text) { + return '' + esc(text) + ''; + } + + function target(t) { + return '' + esc(t.id) + " " + rungBadge(t.statedReadiness); + } + + function clauses(failures) { + return ( + '
    ' + + failures + .map(function (f) { + var targets = f.targets && f.targets.length > 0 + ? '
    Targets: ' + f.targets.map(target).join(", ") + "" + : ""; + return "
  • " + inline(f.description) + " " + esc(f.clauseId) + "" + targets + "
  • "; + }) + .join("") + + "
" + ); + } + + function unit(u, label) { + var c = u.component; + return ( + "
  • " + + esc(u.id) + + "" + + (label ? " — " + inline(label) : "") + + (u.file + ? ' ' + copy(u.file + (u.line === null ? "" : ":" + u.line)) + "" + : " no recorded file") + + (u.role ? ' Role: ' + esc(u.role) + "" : '') + + (c + ? " Component: " + + esc(c.id) + + "" + + (c.layer || c.context + ? " (" + [c.layer && "layer " + esc(c.layer), c.context && "context " + esc(c.context)].filter(Boolean).join(", ") + ")" + : "") + : "") + + "
  • " + ); + } + + function units(list, labels, none) { + if (list.length === 0) return '

    ' + none + "

    "; + return '
      ' + list.map(function (u) { return unit(u, labels[u.id]); }).join("") + "
    "; + } + + function question(q, fields) { + var f = fields || {}; + var lines = []; + if (f.decider) lines.push("Decider: " + esc(f.decider)); + if (f.links && f.links.length > 0) { + lines.push( + "Links: " + + f.links + .map(function (l) { + return '' + esc(l) + ""; + }) + .join(", "), + ); + } + if (f.provisional) lines.push("Provisional: " + inline(f.provisional)); + if (f.trigger) lines.push("Trigger: " + inline(f.trigger)); + return ( + "
  • " + + '' + (q.blocking ? "blocking" : "non-blocking") + " " + + (q.address ? '' + esc(q.address) + " " : "") + + (q.line === null ? "" : 'line ' + q.line + "") + + "
    " + inline(q.question) + "
    " + + lines.map(function (l) { return '' + l + ""; }).join("") + + "
  • " + ); + } + + function card(member, position, context) { + var id = member.id; + if (!member.resolved) { + return ( + '
    ' + position + ". " + esc(id) + + "

    The manifest names an id the graph does not hold.

    " + ); + } + var facts = context.deliveryFacts || []; + var implementation = facts.indexOf("implemented") !== -1 ? "present" : "none"; + var verifier = facts.indexOf("has-verifier") !== -1 ? "present" : "none"; + var next = nextRung(member); + var d = member.design; + var questionsText = + d.openQuestions === 0 + ? "no open questions" + : count(d.openQuestions, "question", "questions") + ", " + (d.blockingQuestions === 0 ? "none" : d.blockingQuestions) + " blocking"; + var labels = {}; + (context.implementations || []).concat(context.references || []).forEach(function (b) { + if (b.label) labels[b.codeId] = b.label; + }); + var fieldsByLine = {}; + ((context.design && context.design.openQuestions) || []).forEach(function (q) { + fieldsByLine[q.index] = q; + }); + var decisionTitles = {}; + ((context.design && context.design.decisions) || []).forEach(function (dd) { + decisionTitles[dd.id] = dd.title; + }); + + var nextBody = + member.statedNextRung === null + ? "

    States ready, so it has no next rung.

    " + : member.statedNextRungFailures.length === 0 + ? "

    " + esc(member.statedNextRung) + ": nothing blocks. The floor already holds it, so it waits only for its author's statement.

    " + : "

    " + esc(member.statedNextRung) + " is held by:

    " + clauses(member.statedNextRungFailures); + + var decisions = + member.decisions.length === 0 + ? "" + : "

    Decided by: " + + member.decisions + .map(function (dd) { + return dd.resolved + ? '' + esc(decisionTitles[dd.id] || dd.id) + " " + rungBadge(dd.statedReadiness) + : "" + esc(dd.id) + " (not in the graph)"; + }) + .join(", ") + + "

    "; + + return ( + '
    ' + + "" + + '' + position + ". " + esc(member.title || id) + "" + + '' + + '' + esc(member.specKind) + " " + + stated(member.statedReadiness, member.floorReached) + " " + + '' + esc(next.text) + " " + + '' + esc(questionsText) + " " + + 'implementation ' + implementation + " " + + 'verifier ' + verifier + "" + + "" + + '
    ' + + '

    ' + esc(id) + "

    " + + "

    Next rung

    " + nextBody + + "

    Design

    " + + capital(count(d.entries, "Design or UI entry", "Design and UI entries")) + ", " + + count(d.declarations, "pinned declaration", "pinned declarations") + ", " + + questionsText + ".

    " + decisions + + (member.questions.length === 0 + ? "" + : "

    Open questions

      " + + member.questions.map(function (q, i) { return question(q, fieldsByLine[i]); }).join("") + + "
    ") + + "

    Code

    " + + "

    Implementation binding: " + implementation + "

    " + + units(member.implementedBy, labels, "No unit realizes it.") + + "

    Design references: " + (member.referencedBy.length === 0 ? "none" : "present") + "

    " + + units(member.referencedBy, labels, "No unit answers to its design.") + + "

    Verification

    " + + "

    Verifier binding: " + verifier + ". " + + count(member.verifiers.total, "verifier", "verifiers") + ", " + member.verifiers.enabled + " enabled; " + + count(member.examples.total, "verifying example", "verifying examples") + ", " + member.examples.enabled + " enabled. " + + "No run result: the graph records bindings, never runs.

    " + + "
    " + ); + } + + function boundarySection(title, rows, membersById, verb) { + var body = + rows.length === 0 + ? "

    None.

    " + : '
    " + + rows + .map(function (row) { + return ( + "" + + "" + + "" + + "" + ); + }) + .join("") + + "
    SpecStatedImplementation' + + esc(verb) + + "
    " + '' + esc(row.title || row.id) + "
    " + esc(row.id) + "
    " + rungBadge(row.statedReadiness) + "" + (row.implemented ? "present" : "none") + "" + + row.via + .map(function (v) { + return "" + esc(v.type) + ": " + v.members + .map(function (m) { + return '' + esc((membersById[m] && membersById[m].title) || m) + ""; + }) + .join(", "); + }) + .join("
    ") + + "
    "; + return ( + '
    ' + esc(title) + + " (" + rows.length + ")
    " + body + "
    " + ); + } + + function renderPack(design, context) { + var t = design.totals; + var contextById = {}; + context.members.forEach(function (m) { contextById[m.id] = m; }); + var membersById = {}; + design.members.forEach(function (m) { membersById[m.id] = m; }); + var resolved = design.members.filter(function (m) { return m.resolved; }); + var statesReady = resolved.filter(function (m) { return m.statedNextRung === null; }).length; + var errors = context.findings.filter(function (f) { return f.severity === "error"; }).length; + var warnings = context.findings.length - errors; + + var summary = [ + count(t.members, "member", "members") + (t.unresolved === 0 ? ", all in the graph." : ", " + t.unresolved + " of them not in the graph."), + (t.heldBelowNextRung === 0 ? "No member is held below its stated next rung; " : count(t.heldBelowNextRung, "member is", "members are") + " held below their stated next rung; ") + + count(t.waitingForAuthor, "member waits", "members wait") + " only for their author's statement" + + (statesReady === 0 ? "." : ", and " + count(statesReady, "states", "state") + " ready."), + count(t.entries, "Design or UI entry", "Design and UI entries") + ", " + t.declarations + " of them pinned declarations.", + count(t.openQuestions, "open question", "open questions") + ", " + (t.blockingQuestions === 0 ? "none" : t.blockingQuestions) + " blocking.", + count(t.withImplementations, "member has", "members have") + " implementing units, and " + count(t.withReferences, "member has", "members have") + " referencing units.", + "The Pack rests on " + count(t.restsOn, "Spec", "Specs") + " outside it and is rested on by " + t.restedOnBy + "; " + + count(t.verifiedBy, "outside Spec verifies", "outside Specs verify") + " a member, and " + (t.supersededBy === 0 ? "none supersedes" : count(t.supersededBy, "supersedes", "supersede")) + " one.", + count(errors, "error", "errors") + " and " + count(warnings, "warning", "warnings") + " name the Pack.", + ]; + + var held = resolved.filter(function (m) { return m.statedNextRungFailures.length > 0; }); + var waiting = resolved.filter(function (m) { return m.statedNextRung !== null && m.statedNextRungFailures.length === 0; }); + var link = function (m) { return '' + esc(m.title || m.id) + ""; }; + + var gaps = context.verifierGaps.slice().sort(function (a, b) { return (b.priority ? 1 : 0) - (a.priority ? 1 : 0); }); + + var html = + '
    ' + + '

    Pack

    ' + + "

    " + esc(design.title || design.id) + "

    " + + '

    ' + copy(design.id) + "" + copy(context.file) + "

    " + + (context.framing ? '
    ' + prose(context.framing) + "
    " : "") + + (context.modelRefs.length === 0 + ? "" + : "

    Vocabulary: " + context.modelRefs.map(function (r) { return '' + esc(r) + ""; }).join(", ") + "

    ") + + "
    " + + '' + + '

    Where the design stands

      ' + + summary.map(function (s) { return "
    • " + capital(s) + "
    • "; }).join("") + + "
    " + + '

    Members, in reading order

    ' + + design.members.map(function (m, i) { return card(m, i + 1, contextById[m.id] || {}); }).join("") + + "
    " + + '

    What holds members below their next rung

    ' + + (held.length === 0 + ? "

    No member is held below its stated next rung.

    " + : "
      " + + held.map(function (m) { return "
    • " + link(m) + " is held below " + esc(m.statedNextRung) + " by:" + clauses(m.statedNextRungFailures) + "
    • "; }).join("") + + "
    ") + + (waiting.length === 0 + ? "" + : "

    " + count(waiting.length, "member waits", "members wait") + " only for their author's statement, which is never an approval: " + waiting.map(link).join(", ") + ".

    ") + + "
    " + + '

    Outside the Pack

    ' + + "

    Specs outside the Pack joined to a member by an authored relation. The Pack rests on a Spec a member refines, depends on, is constrained by or decided by, and is rested on by a Spec that relates to a member the same ways.

    " + + boundarySection("Rests on", design.boundary.restsOn, membersById, "Members that rest on it") + + boundarySection("Rested on by", design.boundary.restedOnBy, membersById, "Members it rests on") + + boundarySection("Verified by", design.boundary.verifiedBy, membersById, "Members it verifies") + + boundarySection("Superseded by", design.boundary.supersededBy, membersById, "Members it supersedes") + + "
    " + + '

    Members with no verifier binding

    ' + + (gaps.length === 0 + ? "

    Every member has a verifier binding.

    " + : '
      ' + + gaps.map(function (g) { return "
    • " + link(membersById[g.id] || { id: g.id }) + " " + rungBadge(g.statedReadiness) + (g.priority ? " stated ready" : "") + "
    • "; }).join("") + + "
    ") + + "
    " + + '

    Findings that name the Pack

    ' + + (context.findings.length === 0 + ? "

    None.

    " + : '
      ' + + context.findings.map(function (f) { return "
    • " + esc(f.severity) + " " + esc(f.validatorId) + " " + inline(f.message) + "
    • "; }).join("") + + "
    ") + + "
    " + + "
    "; + + var rail = + '

    Members

      ' + + design.members + .map(function (m) { + return "
    1. " + link(m) + (m.resolved ? " " + rungBadge(m.statedReadiness) : "") + "
    2. "; + }) + .join("") + + "
    " + + '

    Findings

    ' + count(errors, "error", "errors") + ", " + count(warnings, "warning", "warnings") + " name this Pack.

    "; + + return { html: html, rail: rail }; + } + + /* ------------------------------------------------------------------------------------------ */ + /* 2. The elements */ + /* ------------------------------------------------------------------------------------------ */ + + function liveRegion() { + var span = document.createElement("span"); + span.className = "visually-hidden"; + span.setAttribute("aria-live", "polite"); + return span; + } + + function button(label) { + var b = document.createElement("button"); + b.type = "button"; + b.textContent = label; + return b; + } + + // sdp-expander: Open all and Close all for the details inside it, and the details that hold the + // fragment's target open on load and on each fragment change. + class SdpExpander extends HTMLElement { + connectedCallback() { + if (this.dataset.ready) return; + this.dataset.ready = "true"; + var self = this; + var bar = document.createElement("div"); + bar.className = "toolbar"; + var open = button("Open all"); + var close = button("Close all"); + var live = liveRegion(); + open.addEventListener("click", function () { + self.querySelectorAll("details").forEach(function (d) { d.open = true; }); + live.textContent = "Every card and section is open."; + }); + close.addEventListener("click", function () { + self.querySelectorAll("details").forEach(function (d) { d.open = false; }); + live.textContent = "Every card and section is closed."; + }); + bar.append(open, close, live); + var members = this.querySelector("#members h2"); + (members ? members.parentNode : this).insertBefore(bar, members ? members.nextSibling : this.firstChild); + function reveal() { + var id = decodeURIComponent(location.hash.slice(1)); + if (id === "") return; + var target = document.getElementById(id); + if (target === null || !self.contains(target)) return; + for (var node = target; node !== null && node !== self; node = node.parentElement) { + if (node.tagName === "DETAILS") node.open = true; + } + target.scrollIntoView(); + } + window.addEventListener("hashchange", reveal); + reveal(); + } + } + + // sdp-copy: a Copy button for its text; where the clipboard is unavailable it selects the text + // the build wrote beside it. + class SdpCopy extends HTMLElement { + connectedCallback() { + if (this.dataset.ready) return; + this.dataset.ready = "true"; + var text = this.getAttribute("text") || ""; + var self = this; + var b = button("Copy"); + b.setAttribute("aria-label", "Copy " + text); + var live = liveRegion(); + function selectBeside() { + var shown = self.previousElementSibling; + if (shown === null) return; + var range = document.createRange(); + range.selectNodeContents(shown); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + live.textContent = "Selected " + text + "; copy it with your keyboard."; + } + b.addEventListener("click", function () { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(function () { + b.textContent = "Copied"; + live.textContent = "Copied " + text; + setTimeout(function () { b.textContent = "Copy"; }, 1500); + }, selectBeside); + } else { + selectBeside(); + } + }); + this.append(b, live); + } + } + + /* ------------------------------------------------------------------------------------------ */ + /* The prototype's bootstrap: render once, then define the elements so they upgrade the markup. */ + /* ------------------------------------------------------------------------------------------ */ + + var input = window.SDP_PROTOTYPE; + var main = document.getElementById("main"); + if (!input || !main) return; + var page = renderPack(input.design, input.context); + document.title = (input.design.title || input.design.id) + " · Pack · Spec Studio prototype"; + main.innerHTML = page.html; + document.getElementById("rail").innerHTML = page.rail; + customElements.define("sdp-expander", SdpExpander); + customElements.define("sdp-copy", SdpCopy); +})(); From 764e576a231651f1253f3a2326de7a639a4ef4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:49:01 +0200 Subject: [PATCH 11/33] specs: the Studio's acceptance reads the verifier binding as the graph holds it At the adopter's commit the paused-rebuild examples have no bound verifier, so the fifth reader task asks the reader to read the binding rather than assert one. Co-Authored-By: Claude Opus 5.5 --- specs/consumers/spec-studio.sdp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/consumers/spec-studio.sdp.md b/specs/consumers/spec-studio.sdp.md index 6eb3cba6..778e239f 100644 --- a/specs/consumers/spec-studio.sdp.md +++ b/specs/consumers/spec-studio.sdp.md @@ -35,4 +35,4 @@ relations: - rule: The Studio stands beside the shipped Design Review, census, Mermaid and Gherkin projections and re-specifies none of them, which `spec:decisions.shipped-projections-frozen` keeps as they are. ## Verification — manual -- The Studio's first build is accepted when a person completes, from the Studio's page set and the files it links to, the five tasks the first adopter set over its history-view Pack (`libar-platform/docs/feedback/sdp-feedback-01.md`): find the history contract, locate OD-157, explain the aggregate dependency, find one declaration's type comparison, and say whether a paused-rebuild example has native-backend evidence. The first build reads no run evidence, so the last task passes when the person finds that the example requires the native backend tier and has a bound verifier, and reports that no run result is supplied. +- The Studio's first build is accepted when a person completes, from the Studio's page set and the files it links to, the five tasks the first adopter set over its history-view Pack (`libar-platform/docs/feedback/sdp-feedback-01.md`): find the history contract, locate OD-157, explain the aggregate dependency, find one declaration's type comparison, and say whether a paused-rebuild example has native-backend evidence. The first build reads no run evidence, so the last task passes when the person finds that the example requires the native backend tier, reads whether a verifier is bound to it, and reports that no run result is supplied. From c5a913f7e4a242d03836e43b49e429bbbb132293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:53:08 +0200 Subject: [PATCH 12/33] specs: per-file rule parameters, the corpus report and unknown names Each --params now belongs to the --rules before it, so the architecture walk never receives the names another rules file reads and answers them as unknown. The validate report carries corpusFindings and corpusFailures; the corpus line stands alone when the graph does not derive; recipes 42 and 43 answer an unknown name as every catalog recipe does; matching excludes unreadable designs; uses states none for an empty list. Co-Authored-By: Claude Opus 5.5 --- specs/model/planned-architecture.sdp.md | 8 ++++---- ...e-constraints.isolated-context-as-rule.sdp.md | 2 +- specs/validation/architecture-constraints.sdp.md | 4 ++-- specs/validation/corpus-rules.sdp.md | 16 +++++++++------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/specs/model/planned-architecture.sdp.md b/specs/model/planned-architecture.sdp.md index a16ea73a..7a7573c2 100644 --- a/specs/model/planned-architecture.sdp.md +++ b/specs/model/planned-architecture.sdp.md @@ -15,7 +15,7 @@ The original design declared a component before its code, as a Component node wi The Protocol records the architecture the code realizes. A `component:` anchor states its layer and context, and anchored `memberOf` and `uses` edges join the code units. Under the architectural annotation decision, a component no code realizes is a Spec, and the graph holds no node for it until an anchor exists. The first adopter states its component list and its context rule in prose (`libar-platform/design/PLAN.md` §2.1) and checks context isolation over anchored `uses` in its own script (`libar-platform/design/tools/check.py`). The owner ruled that the architectural annotation decision stands: a component designed before its code is a component design Spec, and the `component:` anchor that later realizes it satisfies that Spec, which is the join a comparison reads. -Choosing a provider stays prose. The original's distinctions between the interface a unit requires, the providers that supply it, the one a composition root selects and the one a test substitutes help a reader, and none of them needs graph structure beyond `uses` to be compared. Keeping one composition mechanism per application is a corpus's own architecture rule when the corpus wants it, stated in a `constraint` Spec and checked by its own corpus rule. A component design names its component with the keys a component anchor uses, so the comparison reads the same words on both sides, and a key the design leaves out is not compared, so a design states only what it has decided. +Choosing a provider stays prose. The original's distinctions between the interface a unit requires, the providers that supply it, the one a composition root selects and the one a test substitutes help a reader, and none of them needs graph structure beyond `uses` to be compared. Keeping one composition mechanism per application is a corpus's own architecture rule when the corpus wants it, stated in a `constraint` Spec and checked by its own corpus rule. A component design names its component with the keys a component anchor uses, so the comparison reads the same words on both sides, and a key the design leaves out is not compared, so a design states only what it has decided; a design that has decided its component uses nothing says `none`, which a left-out key cannot say. ## Intent - problem: A design ahead of its code cannot state the components it adds, their layer and context and what they use, in a form the graph compares with the components the code later anchors. @@ -30,11 +30,11 @@ Choosing a provider stays prose. The original's distinctions between the interfa ## Design - componentDesign: a Spec is a component design when its Design `id` value, bare or in the code span that opens it, is a `component:` id; the curation report and the realization comparison select designs this way, and no validator reads the convention. -- designKeys: the comparison reads five Design keys, `id`, `layer`, `context`, `role` and `uses`, and no other; `layer` takes one of the five layers, `context` and `role` one token on the role grammar, and `uses` a comma-separated list of `component:` ids; each value is read bare when it is the whole value, or from the code span that opens it, with prose after the span free to say why. -- comparisonRecipe: recipe 43, planned against realized components, takes no parameter and returns `{ totals, unrealized, differing, unreadable }`, where `totals` is `{ designs, realized, matching, unrealized, differing, unreadable }`, `matching` counts the realized designs with no differing row, and `differing` counts rows. +- designKeys: the comparison reads five Design keys, `id`, `layer`, `context`, `role` and `uses`, and no other; `layer` takes one of the five layers, `context` and `role` one token on the role grammar, and `uses` either `none` for the empty list or `component:` ids separated by commas, each trimmed, read as the set of distinct ids; each value is read bare when it is the whole value, or from the code span that opens it, with prose after the span free to say why. +- comparisonRecipe: recipe 43, planned against realized components, takes no parameter, so any parameter name returns `{ unknownParams, accepts: [] }` alone, as every catalog recipe answers a name it does not take; otherwise it returns `{ totals, unrealized, differing, unreadable }`, where `totals` is `{ designs, realized, matching, unrealized, differing, unreadable }`, `matching` counts the realized designs with neither a differing row nor an unreadable row, and `differing` counts rows. - realizedWhen: a design is realized when its `component:` CodeNode is anchored and has an anchored `satisfies` edge to the design, as the curation report reads it; a design whose component exists without that edge stays unrealized, with `anchorExists: true`. - comparedFields: for a realized design, each of `layer`, `context`, `role` and `uses` the design states is compared with the anchored component, and a key the design leaves out is not compared; the realized `uses` are the components other than its own that an anchored `uses` edge from the component or one of its members reaches, each end resolved to its component as recipe 32 resolves it. -- comparisonRows: `unrealized` rows are `{ spec, componentId, anchorExists, layer, context, role, uses }` with the planned values, null or an empty list when unstated; `differing` rows are `{ spec, componentId, field, planned, realized }`, one for each differing field, where `layer`, `context` and `role` carry the two values or null and `uses` carries the two sorted lists of components named on that side only; `unreadable` rows are `{ spec, key, value }` for a value outside its key's form, and that key is not compared; rows sort by `spec`, then by `field` or `key`. +- comparisonRows: `unrealized` rows are `{ spec, componentId, anchorExists, layer, context, role, uses }` with the planned values, null when unstated, and `uses` an empty list when the design states `none`; `differing` rows are `{ spec, componentId, field, planned, realized }`, one for each differing field, where `layer`, `context` and `role` carry the two values or null and `uses` carries the two sorted lists of components named on that side only; `unreadable` rows are `{ spec, key, value }` for a value outside its key's form, and that key is not compared; rows sort by `spec`, then by `field` or `key`. ## Example space ```gwt-vocabulary diff --git a/specs/validation/architecture-constraints.isolated-context-as-rule.sdp.md b/specs/validation/architecture-constraints.isolated-context-as-rule.sdp.md index 921d4c8b..c1c6989b 100644 --- a/specs/validation/architecture-constraints.isolated-context-as-rule.sdp.md +++ b/specs/validation/architecture-constraints.isolated-context-as-rule.sdp.md @@ -13,7 +13,7 @@ relations: ## Intent - outcome: Execute the walk's shipped recipe file, unchanged, as a rules file under `sdp validate --rules` on one `uses` edge that leaves an isolated context. -- assumption: The world writes the probe root of the forbidden-pair example, with the layers and contexts the slots give. It runs `sdp validate` on that root with the walk's shipped recipe file named by `--rules` and the parameters of that example passed by `--params`. The findings are the lines in that file's corpus section, and the rule ids are their rule ids joined by a comma and a space, in output order. +- assumption: The world writes the probe root of the forbidden-pair example, with the layers and contexts the slots give. It runs `sdp validate` on that root with the walk's shipped recipe file named by `--rules` and followed by `--params` with the parameters of that example. The findings are the lines in that file's corpus section, and the rule ids are their rule ids joined by a comma and a space, in output order. ```gwt Given a member of a component at the layer {fromLayer: "application"} in the context {fromContext: "platform"} that uses a member of a component at the layer {toLayer: "adapter"} in the context {toContext: "orders"} diff --git a/specs/validation/architecture-constraints.sdp.md b/specs/validation/architecture-constraints.sdp.md index 27823998..7dcad774 100644 --- a/specs/validation/architecture-constraints.sdp.md +++ b/specs/validation/architecture-constraints.sdp.md @@ -32,8 +32,8 @@ The walk returns the edges a rule forbids as corpus findings, so one body answer - The corpus binds a discovered TypeScript check site to its constraint Spec through a test anchor; that check invokes the corpus rule, and `has-verifier` states that the check exists while its result stays in the corpus's run. ## Design -- walkParams: recipe 42, the architecture walk, takes `params.forbidden`, a list of `{ from, to }` pairs of layers from the closed set; `params.isolated`, a list of context tokens; and `params.severity`, `"error"` by default or `"warning"`; either list may be empty, a pair or context given twice counts once, and neither list given, a layer outside the set, a malformed token or another severity returns `{ diagnostics }` with no `findings`, which fails the file when it runs as a rules file. -- walkAnswer: `{ input: { forbidden, isolated, severity }, totals: { usesEdges, resolved, findings, unclassified }, findings, unclassified }`, where `resolved` counts the `uses` edges whose two ends both resolve to a component; findings sort by `from`, then `to`, then rule id, and unclassified rows by `from`, then `to`; recipe 32's answer stays as it is. +- walkParams: recipe 42, the architecture walk, takes `params.forbidden`, a list of `{ from, to }` pairs of layers from the closed set; `params.isolated`, a list of context tokens; and `params.severity`, `"error"` by default or `"warning"`; either list may be empty, and a pair or context given twice counts once; a parameter name outside these three returns `{ unknownParams, accepts }` alone, before any other check, as every catalog recipe answers a name it does not take; neither list given, a layer outside the set, a malformed token or another severity returns `{ diagnostics }` with no `findings`; and either answer fails the file when the walk runs as a rules file. +- walkAnswer: `{ input: { forbidden, isolated, severity }, totals: { usesEdges, resolved, findings, unclassified }, findings, unclassified }` is the answer when every parameter name is known and every value lawful, where `resolved` counts the `uses` edges whose two ends both resolve to a component; findings sort by `from`, then `to`, then rule id, and unclassified rows by `from`, then `to`; recipe 32's answer stays as it is. - walkBreach: an anchored `uses` edge whose ends resolve to two distinct components breaks the pair `{ from, to }` when the using component states layer `from` and the used one states layer `to`, and breaks the isolation of a context when the using component states that context and the used one states another; two units of one component never break a rule, as recipe 32 never counts them as crossing. - walkFinding: `{ validatorId, subjectId, relatedId, severity, message, from, to, fromComponent, toComponent, rule }`, a corpus finding in the shape `spec:validation.corpus-rules#design.ruleFinding` pins, where `subjectId` is the using unit `from`, `relatedId` the used unit `to`, both components are `{ id, layer, context }` with null for an unstated attribute, and `validatorId` is `forbidden-layer` with `rule: { from, to }` the pair it breaks, or `isolated-context` with `rule: { context }` the isolated context it leaves. - walkMessages: a `forbidden-layer` message reads ` uses : layer uses layer ( to ), a forbidden pair`, and an `isolated-context` message reads ` uses : in the isolated context uses in the context `. diff --git a/specs/validation/corpus-rules.sdp.md b/specs/validation/corpus-rules.sdp.md index 119c8e3c..8acd4537 100644 --- a/specs/validation/corpus-rules.sdp.md +++ b/specs/validation/corpus-rules.sdp.md @@ -25,7 +25,7 @@ The original design let a team add its own rules beside the built-in validators, The first adopter's gate runs `sdp validate` and parses its summary line, then checks its own context isolation, entry addresses, dependency cycles, question keys, prose mentions, cited laws and Pack pages. It also parses carrier frontmatter to read kind, readiness and relations, a second read model that a graph-based corpus-rule runner would remove. -A rules file holds the body `sdp q` evaluates, so a rule tried through `sdp q` runs unchanged under `sdp validate`, and a catalog recipe whose answer carries findings, such as the architecture walk, is a rules file as shipped. The runner reads only the answer's findings, so the recipe keeps the rest of its answer for a reader. Corpus findings print after the Protocol's and are tallied on a line of their own, so a gate that reads the Protocol's summary line reads it unchanged; any error, the Protocol's or the corpus's, exits 1, and the two lines say which. Each rules file receives its own copy of what it reads, so a careless rule cannot change another rule's input and the order of the files changes only the order of the output. +A rules file holds the body `sdp q` evaluates, so a rule tried through `sdp q` runs unchanged under `sdp validate`, and a catalog recipe whose answer carries findings, such as the architecture walk, is a rules file as shipped. The runner reads only the answer's findings, so the recipe keeps the rest of its answer for a reader. Corpus findings print after the Protocol's and are tallied on a line of their own, so a gate that reads the Protocol's summary line reads it unchanged; any error, the Protocol's or the corpus's, exits 1, and the two lines say which. Each rules file receives its own copy of what it reads, so a careless rule cannot change another rule's input and the order of the files changes only the order of the output. Its parameters are its own too: a catalog recipe answers a parameter name it does not take with that name and no findings, so parameters shared by every file would fail the walk on the names another file reads. ## Intent - problem: A corpus with rules of its own checks them off the one validation path, reading the carriers a second way and reporting in a form no Protocol surface reads. @@ -33,10 +33,11 @@ A rules file holds the body `sdp q` evaluates, so a rule tried through `sdp q` r ## Behavior - rule: A corpus rule is a graph body the corpus authors, given `g`, `graph`, `report` and `params` as a recipe body is, and returning findings that each carry a rule id, a subject, a message and a severity. -- rule: [#readsGraphOnly] A corpus rule reads the one derived graph, the Protocol's report and the `params` the operator supplies, and no carrier, source file or network; that is the rule author's obligation, because the runner evaluates the body with the trust `sdp q` asks and claims no sandbox. +- rule: [#readsGraphOnly] A corpus rule reads the one derived graph, the Protocol's report and the `params` the operator supplies for its file, and no carrier, source file or network; that is the rule author's obligation, because the runner evaluates the body with the trust `sdp q` asks and claims no sandbox. - rule: [#operatorNamed] Validation runs corpus rules only from a file the operator names with `sdp validate --rules PATH`, which may repeat, and never from a file it finds in the corpus or in a configuration; it asks the trust `sdp q` already asks, to run code the operator chose. - rule: [#namedOrder] Rules files run one at a time in the order named, each to completion, after the Protocol's validators; every file receives the same Protocol report and none sees another file's findings, so the order changes only the order of the output. -- rule: [#copiesPerFile] Each rules file receives its own copy of the graph, the report and `params`, made after the graph is written, so a body that changes what it receives changes no node, edge, delivery fact or Protocol finding, and nothing another rules file receives. +- rule: [#copiesPerFile] Each rules file receives its own copy of the graph and the report, made after the graph is written, and its own `params`, so a body that changes what it receives changes no node, edge, delivery fact or Protocol finding, and nothing another rules file receives. +- rule: Each rules file receives as `params` only the parameters the operator names after its `--rules` and before the next `--rules`, or `{}` when none follow it, so no file receives a parameter meant for another. - rule: A rule id names one check within its rules file; two files may use the same id, and their findings stay apart by the file that reported them. - rule: Each corpus finding names its rule and its rules file as its source. The Protocol states none of these rules: a shipped recipe that a corpus names as a rules file checks the rule data the corpus passes it. - rule: Corpus findings stand outside the conformance and honesty families, in a section of the output labelled as the corpus's own and tallied on a summary line of their own, so the two families keep their meaning, the guardrail that checks never judge content quality or workflow keeps binding the Protocol's own validators, and no corpus finding adds to, suppresses or changes a Protocol finding or the exit reason it sets. @@ -47,16 +48,17 @@ A rules file holds the body `sdp q` evaluates, so a rule tried through `sdp q` r - rule: When the graph does not derive, no rules file runs, and the corpus summary line says so. ## Design -- rulesFlag: `sdp validate [root] [--exclude PATH]... [--check-clean | --watch] [--rules PATH]... [--params JSON | --params @PATH | --params KEY=@PATH]...`; `--rules` and `--params` are options of `validate` alone, `--params` without `--rules` is refused, one file named twice is refused, and each PATH resolves from the working directory, as a `--params @PATH` file does. -- paramsFlag: `--params` on `validate` takes the three forms of `sdp q --params` with the same refusals, before the graph derives, and every rules file receives the one merged object as `params`, or `{}` without the flag. +- rulesFlag: `sdp validate [root] [--exclude PATH]... [--check-clean | --watch] [--rules PATH [--params JSON | --params @PATH | --params KEY=@PATH]...]...`; `--rules` and `--params` are options of `validate` alone, a `--params` before the first `--rules` is refused, one file named twice is refused, and each PATH resolves from the working directory, as a `--params @PATH` file does. +- paramsFlag: `--params` on `validate` takes the three forms of `sdp q --params` with the same refusals, before the graph derives; each `--params` belongs to the nearest `--rules` before it in the argument list, whatever other options stand between them; one file's parameters merge as `sdp q` merges them, a key given twice for one file is refused, two files may each take the same key, and a file with no `--params` after it receives `{}`. - rulesFile: a rules file is UTF-8 text holding one plain JavaScript async function body, the form `sdp q` evaluates, with the bindings `g`, `graph`, `report` and `params`; the runner reads the text and never imports it as a module, so no file extension is required and a catalog recipe file under `dist/recipes/` is a lawful rules file. - ruleAnswer: `{ findings: [] }`: the body returns an object whose `findings` is an array, possibly empty; the runner reads `findings` and nothing else, so the answer's other keys stay the body's own data and are never printed. - ruleFinding: `{ validatorId, subjectId, severity, message, relatedId?, path?, file?, line? }`, where `validatorId` is the rule id, one token matching `^[a-z][a-z0-9-]*$`, so it never reads as a Protocol validator id; `subjectId` and `message` are non-empty strings; `severity` is `"error"` or `"warning"`; `relatedId` and `path` are strings; `file` is a non-empty string read as a root-relative POSIX path, and `line` a positive integer that requires `file`; the runner ignores a finding's other keys. - corpusFinding: `{ rulesFile, validatorId, subjectId, severity, message, relatedId?, path?, file?, line? }` is the corpus branch of the finding currency, where `rulesFile` is the path as the operator named it; it carries no `family`, and the Protocol's findings keep their shape. +- corpusReport: the report a run with `--rules` builds keeps the Protocol's findings unchanged in `findings` and adds `corpusFindings`, the corpus findings of each file that did not fail, in the order the files are named and then in the order returned, and `corpusFailures`, one `{ rulesFile, reason }` for each failed file in the order named, never a finding; the report a rules file receives, like the one `sdp q` hands a body, carries neither. - corpusSection: on standard error, after every Protocol finding, each rules file in the order named prints the line `corpus rules ` and then its findings in the order returned, each in the one composition of location, severity in brackets, rule id and message; a failed file prints `corpus rules failed: ` in place of its findings. -- corpusSummary: `corpus rules: errors · warnings · of files failed (the corpus's own rules)`, written to standard output directly after the Protocol's summary line, which stays as `spec:validation.diagnostic-rendering#design.summaryLine` pins it; without `--rules` no such line prints, and when the graph does not derive the line reads `corpus rules: not run, the graph did not derive`. +- corpusSummary: `corpus rules: errors · warnings · of files failed (the corpus's own rules)`, written to standard output directly after the Protocol's summary line, which stays as `spec:validation.diagnostic-rendering#design.summaryLine` pins it; without `--rules` no such line prints; when the graph does not derive, no rules file runs and no Protocol summary line prints, and the corpus line, the last line on standard output, reads `corpus rules: not run, the graph did not derive`. - exitCodes: with `--rules`, `sdp validate` exits 1 when a Protocol finding is an error, a corpus finding is an error, a rules file fails or the graph does not derive, and 0 otherwise; the two summary lines say which. -- failureReasons: `` is `cannot be read ()`, `does not parse at line , column : `, `threw: `, `returned no findings array`, followed by `; diagnostics: ` when the answer carries `diagnostics`, or `finding has no lawful `, with N counted from 1. +- failureReasons: `` is `cannot be read ()`, `does not parse at line , column : `, `threw: `, `returned no findings array`, followed by `; diagnostics: ` when the answer carries `diagnostics` or by `; unknownParams: ; accepts: ` when it carries `unknownParams`, or `finding has no lawful `, with N counted from 1. ## Example space ```gwt-vocabulary From dd6559758dbc9699a1a7d883653e6e4a4dc568c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:53:26 +0200 Subject: [PATCH 13/33] Build compact design bundles and key-family registers --- docs/agent-surface/recipes.md | 273 +++++++++++++++++++++++++++------ test/iteration-recipes.test.ts | 223 +++++++++++++++++++++++---- test/recipes.test.ts | 175 ++++++++++++++++++++- 3 files changed, 599 insertions(+), 72 deletions(-) diff --git a/docs/agent-surface/recipes.md b/docs/agent-surface/recipes.md index be06d833..6c36b939 100644 --- a/docs/agent-surface/recipes.md +++ b/docs/agent-surface/recipes.md @@ -37,11 +37,12 @@ | 33 | The design backlog | none | Pick defined design work | | 34 | Rule coverage | spec or pack | Find keyed rules no example exercises | | 35 | The design diff | base, pack, detail | Compare design at entry grain | -| 36 | The context bundle | pack or specs, members, budget | Compose one pass within a budget | +| 36 | The context bundle | pack or specs, members, budget, declarations | Compose one pass within a budget | | 37 | The perspective filter | pack, perspective | Select and order Pack members | | 38 | The proposal check | proposal, base | Resolve proposal scopes and compare intent | | 39 | Slimming candidates | pack | Compare pinned declarations with bound code | | 40 | Curation report | imports, limit | Find curation gaps from file imports | +| 41 | The key-family register | prefixes, pack or specs | List a key family's entries | 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: @@ -81,7 +82,7 @@ repeatable for root-relative path prefixes. `PATH` is a placeholder, not a liter Recipes read their documented input from `params`: Spec or component ids, changed files, Pack ids, search terms, Spec or address lists, base graphs, proposals, perspectives, imports objects, -budgets and detail selections. Each recipe names its parameter. Pass it as JSON data with +budgets and detail selections. Each recipe declares its accepted parameter names before reading its input. A name it does not take returns only `{ unknownParams, accepts }`, with exit 0. Pass it as JSON data with `--params`, never by editing the body: ```sh @@ -118,10 +119,10 @@ pnpm --silent sdp:q "$(cat dist/recipes/25-address-resolution.js)" --params @add This catalog stays the one owner of the bodies. The files are derived from it, and the recipe check holds each one byte for byte to its body here. -To filter a shipped answer, wrap its body in an async arrow function and return the part you need: +To filter a shipped answer, wrap its body in an async arrow function and return the part you need. Give the inner body only its own parameters, so an outer caller's parameters do not become unknown names in the recipe: ```sh -pnpm --silent sdp:q "$(printf '%s\n' 'const answer = await (async () => {'; cat dist/recipes/33-the-design-backlog.js; printf '%s\n' '})();' 'return answer.rows.filter((row) => row.specKind === "behavior");')" --json +pnpm --silent sdp:q "$(printf '%s\n' 'const answer = await (async (params) => {'; cat dist/recipes/33-the-design-backlog.js; printf '%s\n' '})({});' 'return answer.rows.filter((row) => row.specKind === "behavior");')" --json ``` The caller's part must hold no shell expansion. Single-quote it as above so the shell inserts only @@ -177,6 +178,9 @@ for a new query verb. A join freezes into the reader only when a second machine example evidence and decision records — while keeping both exclusions visible in the result.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const ready = g.specs().filter((spec) => spec.statedReadiness === "ready"); const backlog = ready.filter( (spec) => @@ -239,6 +243,9 @@ These columns are derived, never a score, rung or approval. finished, `implemented ∧ ¬ready`.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const rungs = ["idea", "scoped", "defined", "ready"]; const alarms = g .specs() @@ -273,6 +280,9 @@ The parameter is `params.spec`, the Spec id to read; an unknown id returns `{ id rather than failing. ```js +const accepts = ["spec"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const id = params.spec ?? "spec:consumers.reader"; const context = g.specContext(id); @@ -361,6 +371,9 @@ filenames. `--params` parses data, so quotes, shell metacharacters, spaces, Unic newlines remain filenames rather than JavaScript or shell syntax. The reader never shells to git. ```js +const accepts = ["files", "detail"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const detail = params.detail ?? false; const changed = params.files ?? []; const radius = g.blastRadius(changed); @@ -439,6 +452,9 @@ The parameter is `params.pack`, the Pack to review; without it the body reads th the graph. An unknown Pack returns `{ id, found: false }`. ```js +const accepts = ["pack"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const id = params.pack ?? g.packs()[0]?.id; if (id === undefined) { @@ -486,6 +502,9 @@ aggregate over Specs that do. The parameter is `params.term`, the phrase to look for. ```js +const accepts = ["term"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const term = params.term ?? "blast radius"; const matches = g.findByConcept(term); @@ -510,6 +529,9 @@ deterministic substring, never fuzzy-scored: the same query returns the same row *When you need this: you want the Specs stating a rung their structure does not earn.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const rungs = ["idea", "scoped", "defined", "ready"]; const rank = (rung) => (rung === undefined ? -1 : rungs.indexOf(rung)); @@ -541,6 +563,9 @@ the stated rung — is information, not a problem: a floor is a floor, never a q *When you need this: you want the warn-level signals as data — informative absences, not a gate.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const warnings = report.findings.filter((finding) => finding.severity === "warning"); const byValidator = {}; @@ -581,6 +606,9 @@ refusals; a green corpus has no errors, but may carry intentional warnings. evidence before touching the carrier.* ```js +const accepts = ["spec"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const id = params.spec ?? "spec:model.enrichment-lifecycle"; const context = g.specContext(id); @@ -630,6 +658,9 @@ A typed-dependency failure lists its `targets`; neither answer confers a rung. *When you need this: you want example intent and graph-visible verifier realization kept distinct.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const rows = []; for (const spec of g.specs()) { @@ -666,6 +697,9 @@ contract no suite binds, and it never reports runner pass or fail. and the first clause that holds it below that rung.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const lower = g.specs().filter((spec) => spec.statedReadiness !== "ready"); const byFamily = Object.create(null); @@ -693,6 +727,9 @@ blocks that rung. The author still decides whether to state it; the recipe never *When you need this: you want to know which code units belong to each declared component.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const componentIds = new Set( graph.nodes .filter((node) => node.nodeType === "CodeNode" && node.id.startsWith("component:")) @@ -722,6 +759,9 @@ and the component ids come from the graph's declared component nodes. *When you need this: you want to know which components depend on which, with both directions visible.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const componentIds = new Set( graph.nodes .filter((node) => node.nodeType === "CodeNode" && node.id.startsWith("component:")) @@ -772,6 +812,9 @@ The parameter is `params.component`, the component you are reviewing; an unknown returns the exact not-found shape and does not throw. ```js +const accepts = ["component"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const subject = params.component ?? "component:protocol.reader"; const component = graph.nodes.find((node) => node.nodeType === "CodeNode" && node.id === subject); @@ -811,6 +854,9 @@ This reads validation findings already supplied by `report`; it never revalidate dangling references. ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const components = graph.nodes .filter((node) => node.nodeType === "CodeNode" && node.id.startsWith("component:")) .map((node) => node.id) @@ -849,6 +895,9 @@ keeps census's validator-owned honesty signal distinct from structural data. *When you need this: you want the graph-side slice each shipped projection root is allowed to render.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const specs = graph.nodes.filter((node) => node.nodeType === "Primitive"); const packs = graph.nodes.filter((node) => node.nodeType === "Pack"); const anchors = graph.nodes.filter( @@ -874,6 +923,9 @@ refusal or inclusion can therefore make rendered output smaller than these count satisfied Specs, and shaping decisions in one map.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const nodes = graph.nodes; const edges = graph.edges; const codeNodesById = new Map( @@ -1025,6 +1077,9 @@ For worked examples and the gen-1 design comparison, see [Architecture through t (dependsOn, refines, supersedes) and the Specs it decides, ranked by how much it shapes.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const decisionNodes = graph.nodes .filter( (node) => @@ -1126,6 +1181,9 @@ The parameter is `params.spec`, the Spec you are planning around; an unknown id `{ id, found: false }` rather than failing. ```js +const accepts = ["spec"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const id = params.spec ?? "spec:consumers.agent-surface"; const context = g.specContext(id); @@ -1302,6 +1360,9 @@ Dependency neighbors carry `statedReadiness` because an unready first, without keeping that table by hand.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const rows = []; const malformed = []; @@ -1412,6 +1473,9 @@ The parameter is `params.spec`, the Spec you are weighing; an unknown id returns `{ id, found: false }` rather than failing. ```js +const accepts = ["spec"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const id = params.spec ?? "spec:extraction.derive-graph"; const context = g.specContext(id); @@ -1485,6 +1549,9 @@ The parameter is `params.scope`, a list of Spec ids to audit mentions from those empty or absent list audits the whole corpus. ```js +const accepts = ["scope", "intended"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const scope = params.scope ?? []; const intendedRows = params.intended ?? []; const backingTypes = [ @@ -1691,6 +1758,9 @@ search (recipe 6) stops at the section.* The parameter is `params.term`, your word or phrase; an empty term returns no matches. ```js +const accepts = ["term"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const term = params.term ?? "suffix"; const tokensOf = (text) => text @@ -1893,6 +1963,9 @@ skipped. Keys carry the adopter's own role prefixes. The Protocol fixes no vocab rows by prefix in your own corpus if you need to. ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const rows = []; for (const spec of g.specs()) { const design = g.specContext(spec.id)?.sections?.design; @@ -1915,6 +1988,9 @@ The parameter is `params.addresses`, the list of addresses you hold; an empty li rows. A long list belongs in a file passed as `--params @PATH`. ```js +const accepts = ["addresses"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const addresses = params.addresses ?? [ "spec:consumers.design-review#ui.packPage", "spec:consumers.design-review#ui.memberTable", @@ -1986,6 +2062,9 @@ Specs, and this recipe resolves ones written anywhere else. through `dependsOn`.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const ids = g.specs().map((spec) => spec.id); const known = new Set(ids); const targets = new Map(ids.map((id) => [id, []])); @@ -2103,6 +2182,9 @@ The parameter is `params.pack`, the Pack you are reviewing; an unknown Pack retu `{ id, found: false }`. ```js +const accepts = ["pack"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const id = params.pack ?? "pack:spec-studio-v1"; const pack = g.packContext(id); @@ -2177,6 +2259,9 @@ role, layer, and context value with the units that carry it, before you name a n reconcile two spellings of one.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const units = graph.nodes.filter((node) => node.nodeType === "CodeNode"); const taxonomy = (field) => { const byValue = new Map(); @@ -2229,6 +2314,9 @@ for summaries, true for full rows, or a list of member ids for those full rows. `{ id, found: false }`. ```js +const accepts = ["pack", "detail"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const detail = params.detail ?? false; const id = params.pack ?? "pack:spec-studio-v1"; const pack = g.packContext(id); @@ -2407,6 +2495,9 @@ The parameter is `params.specs`, the Spec ids you are changing; an id the graph back in `changed` with `resolved: false` and reaches nothing. ```js +const accepts = ["specs"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const changed = params.specs ?? ["spec:validation.readiness-floor"]; const followed = new Set(["refines", "dependsOn", "constrainedBy", "decidedBy"]); const specIds = new Set(g.specs().map((spec) => spec.id)); @@ -2499,6 +2590,9 @@ ones, and `totals.units` the distinct units across all rows. The law is it, which decisions it refines and which refine it, what it shapes, and what it leaves open, read from the graph instead of kept by hand.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const byCodeUnit = (left, right) => (left < right ? -1 : left > right ? 1 : 0); const ends = (relations, type) => relations @@ -2574,6 +2668,9 @@ decisions by shaping fan-in. The law is `spec:consumers.agent-surface.design-rec layer, and which way it runs, before you write or review an architecture rule.* ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const byCodeUnit = (left, right) => (left < right ? -1 : left > right ? 1 : 0); const nodes = new Map(graph.nodes.map((node) => [node.id, node])); const componentOf = new Map(); @@ -2680,6 +2777,9 @@ references, before maturing the next design.* The body takes no parameter. ```js +const accepts = []; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const defined = g.specs().filter((spec) => spec.statedReadiness === "defined" && !spec.deliveryFacts.includes("implemented")); const backlog = defined.filter((spec) => spec.specKind !== "example" && spec.specKind !== "decision"); const byFamily = Object.create(null); @@ -2727,6 +2827,9 @@ code-unit order. The law is `spec:consumers.agent-surface.answer-shapes`. Pass one scope, `params.spec` or `params.pack`. Without one the recipe asks for a scope. ```js +const accepts = ["spec", "pack"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; if (params.spec === undefined && params.pack === undefined) { return {found: false, reason: "scope", expected: "params.spec or params.pack"}; } @@ -2795,6 +2898,9 @@ its own exclusions. A base written by another engine is accepted and reports wha The caller keeps the base commit outside the graph. The recipe reads only the two graph objects. ```js +const accepts = ["base", "pack", "detail"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const base = params.base; const packId = params.pack; const detail = params.detail ?? false; @@ -3004,18 +3110,23 @@ with a budget and an account of what was left out.* Supply exactly one scope: `params.pack`, a Pack id, or `params.specs`, an ordered list of Spec ids. `params.members` optionally narrows a Pack to an ordered subset, such as the member ids recipe 37 returns. The Pack's framing and full boundary stay visible. `params.budget` defaults to 100,000 -UTF-16 code units and accepts a non-negative integer, including zero. There is no sample scope. +UTF-16 code units and accepts a non-negative integer, including zero. `params.declarations` defaults to `"whole"`; `"addresses"` allows the final cut to reduce pinned declarations to entry addresses. Any other value returns diagnostics. There is no sample scope. ```js +const accepts = ["pack", "specs", "members", "budget", "declarations"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const packId = params.pack; const specIds = params.specs; const subset = params.members; const budget = params.budget ?? 100000; +const declarationMode = params.declarations ?? "whole"; const diagnostics = []; const bad = (scope, problem) => diagnostics.push({ scope, problem }); const strings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string"); if ((packId !== undefined) === (specIds !== undefined)) bad("scope", "Supply exactly one of pack or specs."); if (!Number.isInteger(budget) || budget < 0) bad("budget", "Expected a non-negative integer in UTF-16 code units."); +if (params.declarations === null || (declarationMode !== "whole" && declarationMode !== "addresses")) bad("declarations", "Expected \"whole\" or \"addresses\"."); let pack = null, ids = []; if (packId !== undefined) { pack = typeof packId === "string" ? g.packContext(packId) : undefined; @@ -3072,26 +3183,28 @@ const unitOf = (unit) => ({ id: unit.codeId, claim: unit.claim, file: unit.file const members = ids.map((id) => { const context = g.specContext(id); const sections = JSON.parse(JSON.stringify(context.sections ?? {})); + // One copy: open questions live in `questions`, never also in sections.intent. + if (sections.intent) delete sections.intent.openQuestions; const questions = context.designColumns.openQuestions.map((question) => ({ ...question, - address: question.key === undefined ? null : `${id}#question.${question.key}`, file: context.file, line: question.line ?? null })); + address: question.key === undefined ? null : `${id}#question.${question.key}`, line: question.line ?? null })); const footing = context.relationsOut.filter((end) => types.includes(end.type)).map((end) => { const target = summaries.get(end.otherId); return { type: end.type, id: end.otherId, claim: end.claim, resolved: end.resolved, ...(end.reason === undefined ? {} : { reason: end.reason }), statedReadiness: target?.statedReadiness ?? null, floorReached: target === undefined ? null : target.derivedReadiness ?? "none" }; }).sort((a, b) => types.indexOf(a.type) - types.indexOf(b.type) || byCodeUnit(a.id, b.id)); - const declarations = Object.entries(sections.design ?? {}).flatMap(([key, value]) => { - const declaration = key === "description" ? null : declarationOf(value); - return declaration === null ? [] : [{ spec: id, key, declaration }]; - }); + // One copy: each pinned value stays in sections.design; `declarations` names its key. + const declarations = Object.entries(sections.design ?? {}).flatMap(([key, value]) => + key !== "description" && declarationOf(value) !== null ? [key] : []); const exampleIds = [...new Set(context.relationsIn.filter((end) => ["refines", "verifies"].includes(end.type) && summaries.get(end.otherId)?.specKind === "example").map((end) => end.otherId))].sort(); const examples = exampleIds.map((exampleId) => { const example = g.specContext(exampleId); - return { id: exampleId, statedReadiness: example.statedReadiness, enabled: example.verifiers.some((verifier) => verifier.via === "test-anchor" && verifier.enabled), - file: example.file, steps: JSON.parse(JSON.stringify(example.sections?.behavior?.examples ?? [])) }; + const member = ids.includes(exampleId); + return { id: exampleId, member, file: example.file, statedReadiness: example.statedReadiness, enabled: example.verifiers.some((verifier) => verifier.via === "test-anchor" && verifier.enabled), + steps: member ? null : JSON.parse(JSON.stringify(example.sections?.behavior?.examples ?? [])) }; }); - return { id, title: context.title ?? null, specKind: context.specKind, altitude: context.altitude, + return { id, file: context.file, title: context.title ?? null, specKind: context.specKind, altitude: context.altitude, statedReadiness: context.statedReadiness, floorReached: context.derivedReadiness ?? "none", statedNextRung: context.statedNextRung ?? null, holds: context.statedNextRungFailures, narrative: context.narrative ?? null, sections, questions, footing, declarations, examples, design: { entries: context.designColumns.entries, declarations: context.designColumns.declarations, @@ -3100,7 +3213,7 @@ const members = ids.map((id) => { verifiers: context.verifiers.map((verifier) => ({ id: verifier.verifierId, claim: verifier.claim, enabled: verifier.enabled, file: verifier.file ?? null, line: verifier.line ?? null })) }; }); -const answer = { scope, budget, used: 0, tokenEstimate: { value: 0, method: "UTF-16 code units divided by four, an estimate" }, leftOut: [], +const answer = { scope, budget, declarations: declarationMode, used: 0, tokenEstimate: { value: 0, method: "UTF-16 code units divided by four, an estimate" }, leftOut: [], pack: pack ? { id: packId, title: pack.title ?? null, framing: pack.framing ?? null } : null, members, boundary }; const measure = () => { for (;;) { @@ -3111,16 +3224,19 @@ const measure = () => { answer.tokenEstimate.value = estimate; } }; +// The enclosing member names the Spec and file; the entry position names its path. const location = (id, path) => { const context = g.specContext(id); const question = /^intent\.openQuestions\[(\d+)\]/u.exec(path); const rule = /^behavior\.(rules|flows)\[(\d+)\]/u.exec(path); const entry = question ? `question[${question[1]}]` : rule ? `${rule[1] === "rules" ? "rule" : "flow"}[${rule[2]}]` : path; const at = context.entryLocations.find((row) => row.entry === entry); - return { spec: id, path, file: at?.file ?? context.file, line: at?.line ?? null }; + return { line: at?.line ?? null }; }; +const isPointer = (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && + (entry.line === null || typeof entry.line === "number") && + Object.keys(entry).length === 1 && Object.hasOwn(entry, "line"); const address = (id, section, key) => key !== "description" && /^[a-z][A-Za-z0-9]*$/u.test(key) ? `${id}#${section}.${key}` : null; -// Entry addresses remain reading pointers even when the text containing them is cut. const addressesIn = (value) => [...new Set((JSON.stringify(value) ?? "").match(/spec:[A-Za-z0-9][A-Za-z0-9-]*(?:\.[A-Za-z0-9][A-Za-z0-9-]*)*#(?:design|ui|question|rule)\.[a-z][A-Za-z0-9]*(?![A-Za-z0-9_.#-])/gu) ?? [])]; const reference = (id, path, value, own = null) => { const addresses = [...new Set([...(own === null ? [] : [own]), ...addressesIn(value)])]; @@ -3129,14 +3245,20 @@ const reference = (id, path, value, own = null) => { }; const questionReference = (id, question, index) => { const own = question?.key === undefined ? null : `${id}#question.${question.key}`; - const pointer = reference(id, `intent.openQuestions[${index}]`, question, own); - return question?.links?.length ? { pointer, links: question.links } : pointer; + const { links, ...text } = question; + const pointer = reference(id, `intent.openQuestions[${index}]`, text, own); + return links?.length ? { pointer, links } : pointer; }; +const order = ["refines", "dependsOn", "constrainedBy", "decidedBy"]; const cuts = [ () => { answer.boundary = Object.fromEntries(Object.entries(answer.boundary).map(([name, rows]) => [name, rows.map((row) => row.id)])); answer.leftOut.push("Boundary details; ids kept per direction."); }, + () => { + for (const member of members) member.footing = Object.fromEntries(order.map((type) => [type, member.footing.filter((row) => row.type === type).map((row) => row.id)]).filter(([, list]) => list.length)); + answer.leftOut.push("Footing details; ids kept per relation type."); + }, () => { for (const member of members) member.narrative = addressesIn(member.narrative).join(" ") || null; answer.leftOut.push("Member narrative."); @@ -3148,38 +3270,30 @@ const cuts = [ if (behavior.examples) behavior.examples = behavior.examples.map((entry, index) => reference(member.id, `behavior.examples[${index}]`, entry)); if (behavior.exampleSpace) behavior.exampleSpace = reference(member.id, "behavior.exampleSpace", behavior.exampleSpace); } - for (const example of member.examples) example.steps = reference(example.id, "behavior.examples", example.steps); + for (const example of member.examples) if (example.steps !== null) example.steps = reference(example.id, "behavior.examples", example.steps); } answer.leftOut.push("Example step text, including member sections and example-space vocabulary."); }, () => { for (const member of members) { member.questions = member.questions.map((question) => question.blocking ? question : questionReference(member.id, question, question.index)); - if (member.sections.intent?.openQuestions) member.sections.intent.openQuestions = member.sections.intent.openQuestions - .map((question, index) => question?.blocking ? question : questionReference(member.id, question, index)); } - answer.leftOut.push("Non-blocking question text, including member sections; addresses and links kept."); + answer.leftOut.push("Non-blocking question text; addresses and links kept."); }, () => { for (const member of members) { - const pinned = new Set(member.declarations.map((declaration) => declaration.key)); - // A keyed entry is read by its own key, which may contain a dot, never from the joined path. + const pinned = new Set(member.declarations); const keyedEntry = (section, key, value) => section === "design" && pinned.has(key) ? value : reference(member.id, `${section}.${key}`, value, address(member.id, section, key)); const reduce = (value, path) => { - // Protected entries stay whole; everything else becomes a graph location or address. - if (path.startsWith("intent.openQuestions[")) return value; const rule = /^behavior\.(rules|flows)\[(\d+)\]$/u.exec(path); if (rule) { const key = Object.keys(member.sections.behavior.ruleKeys ?? {}).find((key) => member.sections.behavior.ruleKeys[key] === `${rule[1]}[${rule[2]}]`); return reference(member.id, path, value, key === undefined ? null : `${member.id}#rule.${key}`); } if (path === "behavior.ruleKeys") return value; - const isLocation = (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && - typeof entry.spec === "string" && typeof entry.path === "string" && typeof entry.file === "string" && - (entry.line === null || typeof entry.line === "number") && Object.keys(entry).every((key) => ["spec", "path", "file", "line"].includes(key)); - if (isLocation(value)) return value; - if (Array.isArray(value) && value.some(isLocation) && value.every((entry) => isLocation(entry) || + if (isPointer(value)) return value; + if (Array.isArray(value) && value.some(isPointer) && value.every((entry) => isPointer(entry) || typeof entry === "string" && addressesIn(entry).includes(entry))) return value; if (Array.isArray(value)) return value.map((entry, index) => reduce(entry, `${path}[${index}]`)); if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, @@ -3188,12 +3302,19 @@ const cuts = [ }; member.sections = reduce(member.sections, ""); } - answer.leftOut.push("Section entry values other than pinned declarations and blocking questions; addresses and locations kept."); + answer.leftOut.push("Section entry values other than pinned declarations; addresses and lines kept."); }, ]; +if (declarationMode === "addresses") cuts.push(() => { + for (const member of members) for (const key of member.declarations) { + const value = member.sections.design[key]; + member.sections.design[key] = reference(member.id, `design.${key}`, value, address(member.id, "design", key)); + } + answer.leftOut.push("Pinned declaration text; entry addresses kept."); +}); if (measure() <= budget) return answer; for (const cut of cuts) { cut(); if (measure() <= budget) return answer; } -return { scope, budget, budgetExceeded: true, protectedLength: answer.used, leftOut: answer.leftOut }; +return { scope, budget, declarations: declarationMode, budgetExceeded: true, protectedLength: answer.used, leftOut: answer.leftOut }; ``` `scope` states a Pack and whether it is narrowed, with its ordered member ids, or the ordered @@ -3201,22 +3322,26 @@ Spec list. Each member keeps its stated rung, floor and stated next rung, with ` unmet floor clauses. `design` counts entries, declarations and questions, beside shaping decisions and the Pack design's bindings. Questions keep addresses, recorded lines and their fields. `footing` has recipe 21's dependency rows, including `reason` when the relation states one; -`declarations` has recipe 24's pinned declarations. -`examples` lists refining or verifying examples with their rung, enabled binding and steps. +`declarations` lists pinned keys, with each value kept once in `sections.design`. +Each member carries its carrier `file`; questions live only in `questions`, with no second copy +in `sections.intent`. `examples` lists refining or verifying examples with their file, rung, +enabled binding and steps. An example that is itself a member has `member: true` and `steps: null`, +with the steps in its own member row. Bindings carry their claims and recorded source locations. Boundary rows carry declared relations, grouped exactly as the Pack design does. -The fixed cuts are boundary details, member narrative, example step text, non-blocking questions, -then remaining section values. Each cut removes text everywhere it appears, including `sections`, while retaining entry addresses -and question links from that text. -The fifth cut retains pinned declarations and blocking questions whole, reduces keyed entries to -their addresses and other entries to `{ spec, path, file, line }`. If the cut value contains other -entry addresses, they stay beside that pointer in a list. Non-blocking questions retain their -addresses or locations and any links. Null lines mean unlocated. +The fixed cuts are boundary details, footing details, member narrative, example step text, +non-blocking question text, then section values other than pinned declarations. With +`declarations: "addresses"`, a final cut reduces pinned values to entry addresses. The recipe +stops at the first cut after which it fits. Footing becomes target ids grouped by relation type. +Blocking questions stay whole. Other entries keep their addresses or `{ line }`, with the +enclosing row and position naming the Spec, file and path. Entry addresses written in cut text +stay beside its pointer, and question links stay beside the question pointer. Null lines mean +unlocated. `leftOut` lists the cuts taken in that order. `used` counts the compact returned JSON, including itself, `scope`, `budget`, `tokenEstimate` and `leftOut`. The estimate divides code units by four, rounded up. A successful bundle always fits the requested budget. A budget-exceeded result carries -only the scope, budget, protected length and omission list, and the CLI exits 0. Invalid or unknown +only the scope, budget, declaration mode, protected length and omission list, and the CLI exits 0. Invalid or unknown scopes return diagnostics without a bundle. The law is `spec:consumers.context-bundle`. ## 37. The perspective filter @@ -3250,6 +3375,9 @@ contexts must match the same bound unit's component. An absent restriction match an empty restriction matches nothing. ```js +const accepts = ["pack", "perspective"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const packId = params.pack; const perspective = params.perspective; const diagnostics = []; @@ -3371,6 +3499,9 @@ and its type is one of the six authored relations. Scope entries are Spec ids or A proposer, status, date, approval, verdict or any other field is refused. ```js +const accepts = ["proposal", "base"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const proposal = params.proposal; const base = params.base; const schemaProblems = []; @@ -3615,6 +3746,9 @@ returns `{ pack, found: false }`. An existing Pack with no candidates keeps its rows. A row is a potential candidate, never permission to delete an entry. ```js +const accepts = ["pack"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const byCodeUnit = (left, right) => (left < right ? -1 : left > right ? 1 : 0); const pack = params.pack === undefined ? undefined : g.packContext(params.pack); if (params.pack !== undefined && !pack) return { pack: params.pack, found: false }; @@ -3690,6 +3824,9 @@ excluded intermediates, and records the normalized exclusions in its output. Onl component identity and `memberOf` assign files to components. ```js +const accepts = ["imports", "limit"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; const input = params.imports; const diagnostics = []; const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value); @@ -3810,3 +3947,53 @@ units with no component. Component designs are declared intent, bindings are anc imports are inferred. Every mismatch and ranking is advice; the body changes nothing and no finding enters validation. Bad parameters are input errors, not curation findings. The law is `spec:consumers.impact-graph.curation-assist`. + + +## 41. The key-family register + +*When you need this: you want a corpus's keyed values, such as limits, error messages or storage tables, as a register derived each time you read it.* + +`params.prefixes` is a non-empty list of unique non-empty strings, matched case-sensitively against the start of each Design or UI key. It defaults to `["finding", "impact"]`. Pass at most one scope, `params.pack` or `params.specs`; without either, the register reads the corpus. A repeated Spec id is read once. Rule, flow and question keys and `description` are left out. Rows have no cap and sort by key, Spec id and section in code-unit order. A malformed prefix list or scope returns only `{ diagnostics: [{ parameter, problem }] }`. + +```js +const accepts = ["prefixes", "pack", "specs"]; +const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name)); +if (unknownParams.length > 0) return { unknownParams, accepts }; +const prefixes = params.prefixes ?? ["finding", "impact"]; +const diagnostics = []; +const bad = (parameter, problem) => diagnostics.push({ parameter, problem }); +if (params.prefixes === null || !Array.isArray(prefixes) || prefixes.length === 0 || !prefixes.every((prefix) => typeof prefix === "string" && prefix.length > 0) || + new Set(prefixes).size !== prefixes.length) bad("prefixes", "Expected a non-empty list of unique non-empty strings."); +if (params.pack !== undefined && params.specs !== undefined) bad("specs", "Supply at most one of pack or specs."); +let scope = { corpus: true }, ids = g.specs().map((spec) => spec.id); +if (params.pack !== undefined) { + const pack = typeof params.pack === "string" ? g.packContext(params.pack) : undefined; + if (!pack) bad("pack", "Pack does not resolve."); + else { scope = { pack: params.pack }; ids = pack.members.map((member) => member.id); } +} else if (params.specs !== undefined) { + if (!Array.isArray(params.specs) || !params.specs.every((id) => typeof id === "string")) bad("specs", "Expected a list of Spec ids."); + else { + for (const id of params.specs) if (!g.specContext(id)) bad("specs", `${id} does not resolve.`); + scope = { specs: params.specs }; ids = [...new Set(params.specs)]; + } +} +if (diagnostics.length) return { diagnostics }; +const byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0; +const byPrefix = Object.fromEntries(prefixes.map((prefix) => [prefix, 0])); +const rows = []; +for (const id of ids) { + const context = g.specContext(id); + for (const section of ["design", "ui"]) { + for (const [key, value] of Object.entries(context.sections?.[section] ?? {})) { + if (key === "description") continue; + const matched = prefixes.filter((prefix) => key.startsWith(prefix)); + if (matched.length === 0) continue; + for (const prefix of matched) byPrefix[prefix] += 1; + const at = context.entryLocations.find((row) => row.entry === `${section}.${key}`); + rows.push({ key, spec: id, section, value, file: at?.file ?? context.file, line: at?.line ?? null }); + } + } +} +rows.sort((a, b) => byCodeUnit(a.key, b.key) || byCodeUnit(a.spec, b.spec) || byCodeUnit(a.section, b.section)); +return { prefixes, scope, totals: { rows: rows.length, specs: new Set(rows.map((row) => row.spec)).size, byPrefix }, rows }; +``` diff --git a/test/iteration-recipes.test.ts b/test/iteration-recipes.test.ts index 7a9f6989..c7158cfa 100644 --- a/test/iteration-recipes.test.ts +++ b/test/iteration-recipes.test.ts @@ -788,15 +788,18 @@ interface Perspective { interface Bundle { scope: { pack?: string; subset?: boolean; members?: string[]; specs?: string[] }; budget: number; + declarations: "whole" | "addresses"; used: number; tokenEstimate: { value: number; method: string }; leftOut: string[]; pack: { id: string; framing: string } | null; members: { id: string; + file: string; narrative: string | null; sections: Record; questions: { + index: number; question: string; blocking: boolean; address: string | null; @@ -804,8 +807,8 @@ interface Bundle { decider?: string; trigger?: string; }[]; - declarations: { spec: string; key: string; declaration: string }[]; - examples: { id: string; steps: unknown; enabled: boolean }[]; + declarations: string[]; + examples: { id: string; member: boolean; file: string; steps: unknown; enabled: boolean }[]; footing: { type: string; id: string; claim: string; reason?: string }[]; }[]; boundary: Record; @@ -813,6 +816,7 @@ interface Bundle { interface BudgetExceeded { scope: Bundle["scope"]; budget: number; + declarations: "whole" | "addresses"; budgetExceeded: true; protectedLength: number; leftOut: string[]; @@ -1089,7 +1093,166 @@ function prepareBundleCorpus(root: string): void { ); } +function asSection(value: unknown): Record { + return value as Record; +} + describe("iteration loop: context bundle", () => { + it("keeps questions and declarations in one place and member-example steps only in their member row", async () => { + await withCorpus(async (root) => { + prepareBundleCorpus(root); + edit( + root, + "review.pack.sdp.md", + " - spec:probe.model", + " - spec:probe.model\n - spec:probe.example", + ); + const derived = extract({ root }); + const bundle = await runRecipe(36, { pack: "pack:probe.review" }, derived); + expect(bundle.declarations).toBe("whole"); + const parent = bundle.members[0]; + expect(parent?.file).toBe("a.sdp.md"); + expect(parent?.declarations).toEqual(["alpha"]); + expect(asSection(parent?.sections.design).alpha).toBe("`Alpha` declaration"); + const questioning = bundle.members[1]; + expect(asSection(questioning?.sections.intent)).not.toHaveProperty("openQuestions"); + expect(questioning?.questions.map((question) => question.index)).toEqual([0, 1]); + expect(questioning?.questions[0]).not.toHaveProperty("file"); + expect(questioning?.questions[0]?.address).toBe("spec:probe.b#question.hold"); + expect(typeof questioning?.questions[0]?.line).toBe("number"); + expect(parent?.examples).toEqual([ + expect.objectContaining({ + id: "spec:probe.example", + member: true, + file: "example.sdp.md", + steps: null, + }), + ]); + expect(JSON.stringify(bundle.members[3]?.sections)).toContain("Step text secret"); + const subset = await runRecipe(36, { specs: ["spec:probe.a"] }, derived); + expect(subset.members[0]?.examples[0]?.member).toBe(false); + expect(JSON.stringify(subset.members[0]?.examples[0]?.steps)).toContain("Step text secret"); + }); + }); + + it("takes the declaration cut last only with addresses and keeps compact pointers and links", async () => { + await withCorpus(async (root) => { + prepareBundleCorpus(root); + const long = "Declaration detail ".repeat(100); + edit(root, "a.sdp.md", "`Alpha` declaration", `\`Alpha\` declaration ${long}`); + edit( + root, + "a.sdp.md", + "- outcome: Read a in context.", + "- outcome: Read a in context. spec:probe.a#rule.first", + ); + edit( + root, + "b.sdp.md", + "\n\n## Behavior", + "\n - links: spec:probe.a#rule.last\n\n## Behavior", + ); + const derived = extract({ root }); + const scope = { pack: "pack:probe.review", declarations: "addresses" }; + let prior = await runRecipe(36, scope, derived); + const lines = [ + "Boundary details; ids kept per direction.", + "Footing details; ids kept per relation type.", + "Member narrative.", + "Example step text, including member sections and example-space vocabulary.", + "Non-blocking question text; addresses and links kept.", + "Section entry values other than pinned declarations; addresses and lines kept.", + "Pinned declaration text; entry addresses kept.", + ]; + for (let cut = 1; cut <= 7; cut += 1) { + const budget = prior.used - 20; + const result = await runRecipe(36, { ...scope, budget }, derived); + expect(result.leftOut).toEqual(lines.slice(0, cut)); + expect(result.used).toBe(JSON.stringify(result).length); + expect(result.used).toBeLessThanOrEqual(budget); + expect(result.declarations).toBe("addresses"); + if (cut === 2) + expect(result.members[0]?.footing).toEqual({ dependsOn: ["spec:probe.outside"] }); + if (cut === 6) { + const sections = result.members[0]?.sections; + expect(asSection(sections?.intent).outcome).toEqual([ + "spec:probe.a#rule.first", + { line: null }, + ]); + expect(asSection(sections?.design).beta).toBe("spec:probe.a#design.beta"); + const ruleLine = derived.graph.locations?.find( + (at) => at.spec === "spec:probe.a" && at.entry === "rule[1]", + )?.line; + expect(typeof ruleLine).toBe("number"); + expect(asSection(sections?.behavior).rules).toEqual([ + "spec:probe.a#rule.first", + { line: ruleLine }, + ]); + expect(result.members[1]?.questions[1]).toEqual({ + pointer: "spec:probe.b#question.later", + links: ["spec:probe.a#rule.last"], + }); + expect(JSON.stringify(result)).not.toContain('"path":'); + expect(JSON.stringify(result)).not.toContain('"spec":"spec:probe.a"'); + } + if (cut === 7) { + expect(asSection(result.members[0]?.sections.design).alpha).toBe( + "spec:probe.a#design.alpha", + ); + expect(result.members[0]?.declarations).toEqual(["alpha"]); + const whole = await runRecipe(36, { pack: scope.pack, budget }, derived); + expect(whole).toMatchObject({ + budgetExceeded: true, + declarations: "whole", + leftOut: lines.slice(0, 6), + }); + expect(whole).not.toHaveProperty("members"); + } + expect(await runRecipe(36, { ...scope, budget }, derived)).toEqual(result); + prior = result; + } + const exceeded = await runRecipe(36, { ...scope, budget: 0 }, derived); + expect(exceeded).toMatchObject({ + declarations: "addresses", + budgetExceeded: true, + leftOut: lines, + }); + }); + }, 20_000); + + it("cuts a pinned dotted key to its line when it has no entry address", async () => { + await withCorpus(async (root) => { + writeFileSync( + join(root, "dotted.sdp.ts"), + `import { spec, specId } from "@libar-dev/software-delivery-protocol"; +export const dotted = spec({ + id: specId("spec:probe.dotted"), title: "Dotted", kind: "behavior", altitude: "story", readiness: "idea", + intent: { outcome: "Keep pinned detail" }, + design: { "a.b": "\`Thing\` ${"required detail ".repeat(100)}" }, +});`, + ); + const derived = extract({ root }); + const full = await runRecipe(36, { specs: ["spec:probe.dotted"] }, derived); + const whole = await runRecipe( + 36, + { specs: ["spec:probe.dotted"], budget: 1 }, + derived, + ); + const bundle = await runRecipe( + 36, + { + specs: ["spec:probe.dotted"], + budget: Math.min(full.used, whole.protectedLength) - 20, + declarations: "addresses", + }, + derived, + ); + expect(bundle.leftOut).toHaveLength(7); + expect(bundle.members[0]?.declarations).toEqual(["a.b"]); + expect(asSection(bundle.members[0]?.sections.design)["a.b"]).toEqual({ line: 5 }); + }); + }); + it("composes both scopes, dependency footing, declarations, example bindings and a perspective subset", async () => { await withCorpus(async (root) => { prepareBundleCorpus(root); @@ -1109,9 +1272,7 @@ describe("iteration loop: context bundle", () => { expect(result.members[0]?.footing).toEqual([ expect.objectContaining({ id: "spec:probe.outside", type: "dependsOn", claim: "declared" }), ]); - expect(result.members[0]?.declarations).toEqual([ - { spec: "spec:probe.a", key: "alpha", declaration: "Alpha" }, - ]); + expect(result.members[0]?.declarations).toEqual(["alpha"]); expect(result.members[0]?.examples).toEqual([ expect.objectContaining({ id: "spec:probe.example", enabled: true }), ]); @@ -1162,7 +1323,7 @@ describe("iteration loop: context bundle", () => { }); }, 20_000); - it("takes all five cuts in fixed order and counts the complete returned value including Unicode", async () => { + it("takes all six whole-mode cuts in fixed order and counts the complete returned value including Unicode", async () => { await withCorpus(async (root) => { prepareBundleCorpus(root); edit(root, "b.sdp.md", "# b", "# b 🐈"); @@ -1175,7 +1336,7 @@ describe("iteration loop: context bundle", () => { const full = prior; expect(Buffer.byteLength(JSON.stringify(full), "utf8")).toBeGreaterThan(full.used); const results: Bundle[] = []; - for (let cut = 1; cut <= 5; cut += 1) { + for (let cut = 1; cut <= 6; cut += 1) { const budget = prior.used - 20; const result = await runRecipe(36, { pack: "pack:probe.review", budget }, derived); expect(result.leftOut).toHaveLength(cut); @@ -1189,16 +1350,17 @@ describe("iteration loop: context bundle", () => { prior = result; } expect(results[0]?.boundary.restsOn).toEqual(["spec:probe.outside"]); - expect(results[1]?.members[0]?.narrative).toBeNull(); - expect(JSON.stringify(results[2])).not.toContain("Step text secret"); - expect(JSON.stringify(results[3])).not.toContain("Later question?"); - expect(JSON.stringify(results[3])).toContain("spec:probe.b#question.later"); - expect(JSON.stringify(results[4]?.members[0]?.sections)).not.toContain("Beta text"); - expect(JSON.stringify(results[4]?.members[0]?.sections)).toContain( + expect(results[1]?.members[0]?.footing).toEqual({ dependsOn: ["spec:probe.outside"] }); + expect(results[2]?.members[0]?.narrative).toBeNull(); + expect(JSON.stringify(results[3])).not.toContain("Step text secret"); + expect(JSON.stringify(results[4])).not.toContain("Later question?"); + expect(JSON.stringify(results[4])).toContain("spec:probe.b#question.later"); + expect(JSON.stringify(results[5]?.members[0]?.sections)).not.toContain("Beta text"); + expect(JSON.stringify(results[5]?.members[0]?.sections)).toContain( "spec:probe.a#design.beta", ); - expect(JSON.stringify(results[4]?.members[0]?.sections)).toContain("`Alpha` declaration"); - expect(JSON.stringify(results[4]?.members[1]?.sections)).toContain( + expect(JSON.stringify(results[5]?.members[0]?.sections)).toContain("`Alpha` declaration"); + expect(JSON.stringify(results[5]?.members[1]?.questions)).toContain( "Protected blocking question?", ); const equal = await runRecipe( @@ -1217,6 +1379,7 @@ describe("iteration loop: context bundle", () => { expect(exceeded).toEqual({ scope: full.scope, budget: 1, + declarations: "whole", budgetExceeded: true, protectedLength: exceeded.protectedLength, leftOut: prior.leftOut, @@ -1232,7 +1395,7 @@ describe("iteration loop: context bundle", () => { }); }, 20_000); - it("keeps a pinned declaration whole through the fifth cut when its key contains a dot", async () => { + it("keeps a pinned declaration whole through the sixth cut when its key contains a dot", async () => { await withCorpus(async (root) => { const long = "Reading material ".repeat(100); writeFileSync( @@ -1248,9 +1411,10 @@ export const dotted = spec({ expect(derived.report.findings.filter((finding) => finding.severity === "error")).toEqual([]); const specs = ["spec:probe.dotted"]; let result = await runRecipe(36, { specs, budget: 100000 }, derived); - while (result.leftOut.length < 5) { + for (let attempt = 0; result.leftOut.length < 6 && attempt < 6; attempt += 1) { result = await runRecipe(36, { specs, budget: result.used - 1 }, derived); } + expect(result.leftOut).toHaveLength(6); expect(result.members[0]?.sections.design).toMatchObject({ "a.b": "`Thing` required detail", ab: "`Thing` required detail", @@ -1273,6 +1437,9 @@ export const dotted = spec({ { specs: [], budget: -1 }, { specs: [], budget: 0.5 }, { specs: [], budget: "100" }, + { specs: [], declarations: "all" }, + { specs: [], declarations: true }, + { specs: [], declarations: null }, ]) { const result = await runRecipe<{ diagnostics: unknown[] }>(36, params, derived); expect(Object.keys(result)).toEqual(["diagnostics"]); @@ -1737,24 +1904,26 @@ describe("iteration loop: comparison and budget edges", () => { }); }, 20_000); - it("cuts example text in both member sections and related examples and preserves locations after the fifth cut", async () => { + it("cuts example text once and preserves compact pointers after the sixth cut", async () => { await withCorpus(async (root) => { prepareBundleCorpus(root); const derived = extract({ root }); const specs = ["spec:probe.a", "spec:probe.b", "spec:probe.example"]; let result = await runRecipe(36, { specs, budget: 100000 }, derived); - expect(JSON.stringify(result.members[0]?.examples)).toContain("Step text secret"); + expect(result.members[0]?.examples[0]?.steps).toBeNull(); expect(JSON.stringify(result.members[2]?.sections)).toContain("Step text secret"); - for (let cut = 1; cut <= 5; cut += 1) { + for (let cut = 1; cut <= 6; cut += 1) { result = await runRecipe(36, { specs, budget: result.used - 20 }, derived); expect(result.leftOut).toHaveLength(cut); - if (cut >= 3) expect(JSON.stringify(result)).not.toContain("Step text secret"); - if (cut >= 4) expect(JSON.stringify(result)).not.toContain("Later question?"); + if (cut >= 4) expect(JSON.stringify(result)).not.toContain("Step text secret"); + if (cut >= 5) expect(JSON.stringify(result)).not.toContain("Later question?"); } expect(JSON.stringify(result.members[0]?.sections)).toContain("spec:probe.a#rule.first"); expect(JSON.stringify(result.members[0]?.sections)).toContain('"line":null'); - expect(JSON.stringify(result.members[0]?.sections)).toContain('"path":"intent.outcome"'); - expect(JSON.stringify(result.members[1]?.sections)).toContain("Protected blocking question?"); + expect(asSection(result.members[0]?.sections.intent).outcome).toEqual({ line: null }); + expect(JSON.stringify(result.members[1]?.questions)).toContain( + "Protected blocking question?", + ); expect(result.used).toBe(JSON.stringify(result).length); }); }, 20_000); @@ -1903,7 +2072,7 @@ describe("iteration loop: protected entry addresses", () => { { specs: ["spec:probe.a", "spec:probe.b", "spec:probe.example"], budget: 100000 }, derived, ); - for (let cut = 1; cut <= 5; cut += 1) { + for (let cut = 1; cut <= 6; cut += 1) { result = await runRecipe( 36, { @@ -1915,12 +2084,12 @@ describe("iteration loop: protected entry addresses", () => { expect(result.leftOut).toHaveLength(cut); } expect(result.members[0]?.narrative).toBe("spec:probe.a#rule.first"); - expect(JSON.stringify(result.members[0]?.examples)).toContain("spec:probe.a#rule.first"); + expect(result.members[0]?.examples[0]?.steps).toBeNull(); expect(JSON.stringify(result.members[2]?.sections)).toContain("spec:probe.a#rule.first"); expect(JSON.stringify(result.members[1]?.questions)).toContain("spec:probe.b#question.later"); expect(JSON.stringify(result.members[1]?.questions)).toContain("spec:probe.a#design.beta"); expect(JSON.stringify(result.members[1]?.questions)).toContain("spec:probe.a#rule.last"); - expect(JSON.stringify(result.members[1]?.sections)).toContain("spec:probe.b#question.later"); + expect(asSection(result.members[1]?.sections.intent)).not.toHaveProperty("openQuestions"); expect(JSON.stringify(result.members[0]?.sections)).toContain("spec:probe.a#design.beta"); expect(JSON.stringify(result.members[0]?.sections)).toContain("spec:probe.a#ui.panel"); expect(result.used).toBe(JSON.stringify(result).length); diff --git a/test/recipes.test.ts b/test/recipes.test.ts index ff6b0e02..0fa0ee09 100644 --- a/test/recipes.test.ts +++ b/test/recipes.test.ts @@ -2049,6 +2049,115 @@ export const probe = spec({ } describe("register recipe semantics", () => { + it("lists key families across the corpus in key, Spec and section order with the documented sample", async () => { + const result = asRecord(await runRecipe(recipeByOrdinal(41))); + expect(result.prefixes).toEqual(["finding", "impact"]); + expect(result.scope).toEqual({ corpus: true }); + const rows = asArray(result.rows).map(asRecord); + expect(rows.length).toBeGreaterThan(0); + const tuples = rows.map( + (row) => `${String(row.key)}\0${String(row.spec)}\0${String(row.section)}`, + ); + expect(tuples).toEqual([...tuples].sort()); + const findingRows = rows.filter((row) => String(row.key).startsWith("finding")); + const impactRows = rows.filter((row) => String(row.key).startsWith("impact")); + expect(rows.slice(0, 2).map((row) => ({ key: row.key, spec: row.spec }))).toEqual([ + { key: "findingIds", spec: "spec:carrier.sdp-import" }, + { key: "findingIds", spec: "spec:extraction.executable-contracts" }, + ]); + expect(result.totals).toEqual({ + rows: rows.length, + specs: new Set(rows.map((row) => row.spec)).size, + byPrefix: { finding: findingRows.length, impact: impactRows.length }, + }); + }); + + it("lists Design and UI key families without a row cap, preserving values, lines and overlapping prefixes", async () => { + const extraction = markdownProbe("sdp-key-family-", { + "a.sdp.md": `---\nid: spec:probe.a\nkind: rule\naltitude: story\nreadiness: idea\nrelations: {}\n---\n# A\n\n## Intent\n- outcome: List keyed values.\n### Open questions\n- [non-blocking #limitQuestion] Who holds the limit?\n\n## Rule\n- [#limitRule] Hold the limit.\n\n## Design\nLimit description.\n${Array.from({ length: 30 }, (_, index) => `- limit${String(index).padStart(2, "0")}: Value ${String(index)}`).join("\n")}\n\n## UI\n- limit00: Visible limit\n`, + "b.sdp.md": + "---\nid: spec:probe.b\nkind: rule\naltitude: story\nreadiness: idea\nrelations: {}\n---\n# B\n## Intent\n- outcome: List keyed values.\n## Design\n- limit00: Another value\n- otherLimit: Case-sensitive value\n", + "p.pack.sdp.md": "---\nid: pack:probe.family\nspecs: [spec:probe.b]\n---\n# Family\n", + }); + const query = (params: Record) => + runRecipe(recipeByOrdinal(41), params, extraction); + const result = asRecord(await query({ prefixes: ["limit", "limit0", "description"] })); + const rows = asArray(result.rows).map(asRecord); + expect(rows).toHaveLength(32); + expect(rows.slice(0, 3)).toEqual([ + { + key: "limit00", + spec: "spec:probe.a", + section: "design", + value: "Value 0", + file: "a.sdp.md", + line: 20, + }, + { + key: "limit00", + spec: "spec:probe.a", + section: "ui", + value: "Visible limit", + file: "a.sdp.md", + line: 52, + }, + { + key: "limit00", + spec: "spec:probe.b", + section: "design", + value: "Another value", + file: "b.sdp.md", + line: 12, + }, + ]); + for (const row of rows) + expect(row.line).toBe( + extraction.graph.locations?.find( + (at) => at.spec === row.spec && at.entry === `${String(row.section)}.${String(row.key)}`, + )?.line, + ); + expect(result.totals).toEqual({ + rows: 32, + specs: 2, + byPrefix: { limit: 32, limit0: 12, description: 0 }, + }); + expect(asRecord(await query({ prefixes: ["Limit"] })).rows).toEqual([]); + const pack = asRecord(await query({ prefixes: ["limit"], pack: "pack:probe.family" })); + expect(pack.scope).toEqual({ pack: "pack:probe.family" }); + expect(asArray(pack.rows)).toEqual([rows[2]]); + const list = asRecord( + await query({ prefixes: ["limit"], specs: ["spec:probe.b", "spec:probe.b"] }), + ); + expect(asArray(list.rows)).toEqual([rows[2]]); + const missingLocations = { ...extraction, graph: { ...extraction.graph, locations: [] } }; + const unlocated = asRecord( + await runRecipe( + recipeByOrdinal(41), + { prefixes: ["limit"], specs: ["spec:probe.b"] }, + missingLocations, + ), + ); + expect(asArray(unlocated.rows).map(asRecord)[0]?.line).toBeNull(); + }); + + it.each([ + { prefixes: [] }, + { prefixes: [""] }, + { prefixes: ["a", "a"] }, + { prefixes: "limit" }, + { pack: "pack:absent" }, + { specs: ["spec:absent"] }, + { specs: "bad" }, + { pack: "pack:self-hosting-v1", specs: [] }, + ])("diagnoses malformed key-family parameters without a register: %j", async (params) => { + const result = asRecord(await runRecipe(recipeByOrdinal(41), params)); + expect(Object.keys(result)).toEqual(["diagnostics"]); + const diagnostics = asArray(result.diagnostics).map(asRecord); + expect(diagnostics.length).toBeGreaterThan(0); + expect(diagnostics[0]?.parameter).toBe( + "prefixes" in params ? "prefixes" : "specs" in params ? "specs" : "pack", + ); + }); it.each([20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32])( "runs recipe %s as written in default output", async (ordinal) => { @@ -4301,6 +4410,64 @@ async function runShipped( } describe("recipe parameters and the shipped recipe files", () => { + it.each(recipes)( + "declares and checks only its indexed parameter names first: $ordinal", + (recipe) => { + const row = source + .split("\n") + .find((line) => line.startsWith(`| ${String(recipe.ordinal)} |`)); + const accepts = (row?.split("|")[3]?.trim() ?? "none") + .split(/, | or /u) + .filter((name) => name !== "none"); + expect(recipe.body.split("\n").slice(0, 3)).toEqual([ + `const accepts = ${JSON.stringify(accepts).replaceAll(",", ", ")};`, + "const unknownParams = Object.keys(params).filter((name) => !accepts.includes(name));", + "if (unknownParams.length > 0) return { unknownParams, accepts };", + ]); + const read = [ + ...new Set([...recipe.body.matchAll(/\bparams\.([A-Za-z]+)/gu)].map((match) => match[1])), + ]; + expect([...accepts].sort()).toEqual(read.sort()); + }, + ); + + it.each(recipes)( + "returns only the unknown-name answer before reading recipe inputs: $ordinal", + async (recipe) => { + const row = source + .split("\n") + .find((line) => line.startsWith(`| ${String(recipe.ordinal)} |`)); + const accepts = (row?.split("|")[3]?.trim() ?? "none") + .split(/, | or /u) + .filter((name) => name !== "none"); + expect(await runRecipe(recipe, { zzUnknown: true })).toEqual({ + unknownParams: ["zzUnknown"], + accepts, + }); + }, + ); + + it("diagnoses wrong subject names, mixed known names and ordered unknown names through shipped bodies", async () => { + const impact = recipeByOrdinal(4); + expect(await runRecipe(impact, { file: ["src/reader/index.ts"] })).toEqual({ + unknownParams: ["file"], + accepts: ["files", "detail"], + }); + const change = recipeByOrdinal(30); + const shipped = { ...change, body: shippedBody(change) }; + expect(await runRecipe(shipped, { spec: "x" })).toEqual({ + unknownParams: ["spec"], + accepts: ["specs"], + }); + expect( + await runRecipe(shipped, { + specs: ["spec:validation.readiness-floor"], + zzUnknown: true, + spec: "x", + }), + ).toEqual({ unknownParams: ["zzUnknown", "spec"], accepts: ["specs"] }); + }); + it("reads and names each parameter, with a fallback only when the contract supplies one", () => { const parameterized = recipes.filter((recipe) => /\bparams\./u.test(recipe.body)); @@ -4316,9 +4483,13 @@ describe("recipe parameters and the shipped recipe files", () => { for (const name of names) { const hasSample = recipe.ordinal !== 34 && - !([35, 36, 37, 38].includes(recipe.ordinal) && !["budget", "detail"].includes(name)) && + !( + [35, 36, 37, 38].includes(recipe.ordinal) && + !["budget", "detail", "declarations"].includes(name) + ) && !(recipe.ordinal === 39 && name === "pack") && - !(recipe.ordinal === 40 && name === "imports"); + !(recipe.ordinal === 40 && name === "imports") && + !(recipe.ordinal === 41 && name !== "prefixes"); expect({ recipe: recipe.ordinal, fallback: recipe.body.includes(`params.${name} ?? `), From d640b7e29b7ed692e1e986df4cd3f7c331bedd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 14:54:02 +0200 Subject: [PATCH 14/33] fix(adopter): address lane P review findings --- .github/workflows/ci.yml | 2 +- README.md | 50 +++++++++++++++----------- check-git-install.mjs | 15 +++++--- package.json | 2 +- specs/consumers/adopter-on-ramp.sdp.md | 12 +++---- specs/extraction/example-runner.sdp.md | 2 +- test/package-smoke.test.ts | 10 +++--- test/skills.test.ts | 18 ++++++++++ test/vitest-adapter.test.ts | 3 ++ 9 files changed, 75 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f9048d5..7e778b05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,4 +26,4 @@ jobs: run: npm run check - name: Install the committed package - run: npm run test:git-install + run: npm run check:git-install diff --git a/README.md b/README.md index 9aba88f2..a2c0afe4 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ Install from the committed lockfile. The `prepare` script builds the CLI: npm ci ``` +`npm run check:git-install -- ` installs a committed revision from a local git URL with +npm and, when present, pnpm, then checks its CLI, export files and a shipped recipe. It needs +network access and tests only committed work, so CI runs it after `npm run check`. + Then query this repository's self-hosting graph with either supported script runner: ```sh @@ -62,32 +66,42 @@ Pin the Protocol to a full commit SHA in your adopter's `package.json`: } ``` -Replace `` with the commit you reviewed, then run `npm install` and commit the lockfile. +Replace `` with the commit you reviewed, then install and commit the lockfile. The package's `prepare` script builds `dist/`, the CLI, subpath exports and shipped recipes during -the git install. npm installs the build dependencies for this step. While the version is `0.0.0`, -use a commit pin; no npm prerelease is published. See [npm's prepare lifecycle](https://docs.npmjs.com/cli/v11/using-npm/scripts/). +the git install. While the version is `0.0.0`, use a commit pin; no npm prerelease is published. +See [npm's prepare lifecycle](https://docs.npmjs.com/cli/v11/using-npm/scripts/). -pnpm 10 blocks dependency build scripts unless allowed. Before `pnpm install`, add the Protocol to -your adopter's `pnpm-workspace.yaml`: +Both package managers approve this build by its git source, never by package name alone. -```yaml -onlyBuiltDependencies: - - "@libar-dev/software-delivery-protocol" +With npm, approve the repository in your adopter's `package.json` and allow root git +dependencies in its `.npmrc`. npm 11.16 and later warn without the approval; npm 12 refuses git +dependencies and skips unapproved scripts by default. + +```json +{ "allowScripts": { "github:libar-dev/software-delivery-protocol": true } } ``` -pnpm 11 requires the git source in the build permission. With pnpm 11.19 or later and the GitHub -pin above, use this repository permission instead: +```ini +allow-git=root +``` + +Then run `npm install`. + +With pnpm 11.19 or later, approve the repository once in `pnpm-workspace.yaml`. The approval +covers every commit you pin: ```yaml allowBuilds: "@libar-dev/software-delivery-protocol@git+https://github.com/libar-dev/software-delivery-protocol.git": true ``` -Keep this alongside your existing build permissions. It permits builds from this repository -across commit updates. A package name alone does not approve a git build on pnpm 11. To allow -only one commit, or on earlier pnpm 11 versions, use the exact source and commit key shown by -`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`. See the [pnpm 10 build settings](https://pnpm.io/10.x/settings#onlybuiltdependencies) -and [source-qualified permissions](https://pnpm.io/settings/build#allowbuilds). +On pnpm 10 and earlier pnpm 11 releases, approve the exact source and commit instead: copy the +key that `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` prints under `onlyBuiltDependencies` (pnpm 10) +or `allowBuilds` (pnpm 11), and replace it at each re-pin. Keep this alongside your existing +build permissions, then run `pnpm install`. See npm's +[`approve-scripts`](https://docs.npmjs.com/cli/v11/commands/npm-approve-scripts) and pnpm's +[source-qualified permissions](https://pnpm.io/settings/build#allowbuilds). + Verify either install through the linked binary: ```sh @@ -126,12 +140,6 @@ Use your corpus's addresses, or pass `--params @addresses.json` with an object c `addresses`. Other parameterized recipes follow the same pattern. The [recipe catalog](docs/agent-surface/recipes.md) names each parameter. -In the Protocol source checkout, `npm run test:git-install -- ` proves a committed revision -installs through a local git URL with npm and, when available, pnpm. It checks the installed CLI, -export files and a parameterized shipped recipe. This separate gate downloads dependencies and -builds the committed package, so it runs after the unit suite in CI. It does not test uncommitted -edits. An optional second argument selects the parent directory for its temporary projects. - ## Gherkin carrier Behavior and example Specs may use a `.sdp.gherkin` file as their one canonical surface. diff --git a/check-git-install.mjs b/check-git-install.mjs index a1666ce1..db0423d4 100644 --- a/check-git-install.mjs +++ b/check-git-install.mjs @@ -38,15 +38,20 @@ try { private: true, type: "module", dependencies: { [packageName]: dependency }, + ...(manager === "npm" + ? { allowScripts: { [`git+${pathToFileURL(root).href}`]: true } } + : {}), }), ); - if (manager === "pnpm") { + if (manager === "npm") { + writeFileSync(join(consumer, ".npmrc"), "allow-git=root\n"); + } else { const major = Number(pnpm.stdout.trim().split(".")[0]); writeFileSync( join(consumer, "pnpm-workspace.yaml"), major >= 11 ? `allowBuilds:\n ${JSON.stringify(`${packageName}@${dependency}`)}: true\n` - : `onlyBuiltDependencies:\n - '${packageName}'\n`, + : `onlyBuiltDependencies:\n - ${JSON.stringify(`${packageName}@${dependency}`)}\n`, ); } const started = Date.now(); @@ -54,14 +59,16 @@ try { manager, [ "install", - ...(manager === "npm" ? ["--no-audit", "--no-fund"] : ["--reporter=append-only"]), + ...(manager === "npm" + ? ["--no-audit", "--no-fund"] + : ["--reporter=append-only", "--store-dir", join(scratch, "pnpm-store")]), ], consumer, ); const installed = join(consumer, "node_modules", ...packageName.split("/")); const cli = join(consumer, "node_modules", ".bin", "sdp"); const version = run(cli, ["--version"], consumer).trim(); - assert.match(version, /0\.0\.0/u); + assert.ok(version.includes(commit), `${version} does not name ${commit}`); const manifest = JSON.parse(readFileSync(join(installed, "package.json"), "utf8")); for (const surface of Object.values(manifest.exports)) { for (const path of Object.values(surface)) assert.ok(existsSync(join(installed, path)), path); diff --git a/package.json b/package.json index 7550cf32..39bc8b2c 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "typecheck": "tsc --noEmit -p tsconfig.json", "typecheck:examples": "tsc --noEmit -p tsconfig.examples.json", "test": "node ./vitest-test.mjs", - "test:git-install": "node ./check-git-install.mjs", + "check:git-install": "node ./check-git-install.mjs", "test:watch": "vitest", "sdp": "node ./dist/cli/sdp.js", "sdp:q": "node ./dist/cli/sdp.js q --exclude explorations --exclude examples --exclude test/fixtures/import/parity", diff --git a/specs/consumers/adopter-on-ramp.sdp.md b/specs/consumers/adopter-on-ramp.sdp.md index c9b629f3..e3b7b79e 100644 --- a/specs/consumers/adopter-on-ramp.sdp.md +++ b/specs/consumers/adopter-on-ramp.sdp.md @@ -17,16 +17,16 @@ relations: --- # An adopter learns graph homes, carrier grammar and shipped paths from the on-ramps -The help names the on-ramps by path because an operator can reach the CLI without a package runner and still needs to find the shipped on-ramps and the law they cite. +The help names the on-ramps by path because an operator can reach the CLI without a package runner and still needs to find the shipped on-ramps and the law they cite. A commit pin lets an adopter take any reviewed revision without a registry release, and the package builds itself during that install, so a re-pin is one dependency edit rather than a rebuilt vendored tarball. ## Intent -- problem: An adopter that follows the shipped on-ramps cannot learn where a deferral, an unsettled fact, a ruling awaiting its owner, a statement a test checks, or a count belongs, so it invents tags of its own, hand-kept registers, and scripts that keep them true. -- outcome: Teach each ruled home in the shipped skills, one line each with its carrying Spec, so an adopter states those truths in the graph and not beside it. +- problem: An adopter needs a repeatable commit install and stable recipe inputs. An adopter that follows the shipped on-ramps cannot learn where a deferral, an unsettled fact, a ruling awaiting its owner, a statement a test checks, or a count belongs, so it invents tags of its own, hand-kept registers, and scripts that keep them true. +- outcome: Teach commit installation and stable recipe inputs in the README. Teach each ruled home in the shipped skills, one line each with its carrying Spec, so an adopter states those truths in the graph and not beside it. ## Behavior -- rule: [#commitInstall] An adopter can pin the package to a git commit; its `prepare` script builds the CLI, subpath exports and shipped recipes before installation completes. While the package version is `0.0.0`, the installation guidance uses a commit pin rather than an npm prerelease. -- rule: [#installGuidance] The shipped README teaches commit installation with npm and the dependency build permission pnpm needs, and checks the installed CLI with `sdp --version`. -- rule: [#recipeUpgrade] The upgrade guidance teaches callers to pass recipe subjects through `--params` and run the shipped recipe files; address resolution reads `params.addresses`, so rewriting a sample in the recipe body is no longer an input mechanism. +- rule: [#commitInstall] An adopter can pin the package to a git commit, and the package's `prepare` script builds the CLI, subpath exports and shipped recipes before installation completes. While the package version is `0.0.0`, the installation guidance uses a commit pin rather than an npm prerelease. +- rule: [#installGuidance] The shipped README teaches a commit install with npm and pnpm, the source-qualified build approval each needs, and a check of the installed CLI with `sdp --version`. +- rule: [#recipeUpgrade] The upgrade guidance teaches a recipe caller to pass the recipe's subject as `--params` data and run the shipped recipe file, never to rewrite the recipe body. - rule: The authoring skill teaches a deferral as a blocking open question whose `trigger:` field names what brings the Spec back, plus `dependsOn` for a true precondition. Its blocking question holds the Spec below `defined` without an adopter-specific tag. Example children may still state `defined` once their bound points are complete and match the parent's example vocabulary when it has one; their `ready` waits until the parent states at least `defined`. - rule: It teaches an unsettled fact as a constraint Spec whose blocking open question names the check that would settle it. Specs bounded by it declare `constrainedBy`; they may state `defined` when their own floor clears, and their `ready` waits until the constraint states at least `defined`. - rule: It teaches a ruling that awaits its owner as a decision Spec below `ready`, with `decidedBy` from each Spec the ruling shapes. diff --git a/specs/extraction/example-runner.sdp.md b/specs/extraction/example-runner.sdp.md index 443e3305..4a59b79c 100644 --- a/specs/extraction/example-runner.sdp.md +++ b/specs/extraction/example-runner.sdp.md @@ -17,7 +17,7 @@ relations: ## Behavior - rule: [#contractOrder] The core plans every contract step in authored order and runs it against the world the caller hands in; creating a fresh world per example is the adapter's lifecycle, never the core's. -- rule: [#worldCleanup] Once the Vitest adapter creates a world, it awaits the optional `after` hook in a `finally` whether the steps pass or fail. A step failure still fails the test when cleanup also fails; a cleanup failure fails a test whose steps passed. A world factory that throws has created no world to clean up. +- rule: [#worldCleanup] Once the Vitest adapter creates a world, it awaits the optional `after` hook whether the steps pass or fail. A step failure stays the test's failure and an error from `after` is then discarded; when the steps pass, an error from `after` fails the test. A world factory that throws creates no world, so `after` does not run. - rule: [#oneHandlerPerStep] Duplicate step text within one example binds one handler, and every occurrence runs it; an occurrence that binds a slot to a different value is a conflicting re-binding, named at generation, and every occurrence runs with the first binding, because an example is one point. - rule: [#redStepNamesItself] A red step names itself before the assertion detail: the failure message leads with the step's natural reading — the Spec's own words with bound values inlined — and the original error is preserved, carried as `cause` when it cannot be re-messaged, and wrapped when the thrown value is not an error. - rule: A missing or stale step handler is a compile-time refusal rather than a silent skip: the bindings type covers every step and only the steps, so spec-side drift fails the typecheck instead of the run. diff --git a/test/package-smoke.test.ts b/test/package-smoke.test.ts index e278a4d5..73c881e1 100644 --- a/test/package-smoke.test.ts +++ b/test/package-smoke.test.ts @@ -195,7 +195,7 @@ describe("published package surface", () => { ) as readonly [ { readonly filename: string; readonly files: readonly { readonly path: string }[] }, ]; - const dryRunPaths = packed[0].files.map((file) => file.path); + const packedPaths = packed[0].files.map((file) => file.path); const adopterAssets = [ "CONTEXT.md", ".agents/skills/sdp-agent-surface/SKILL.md", @@ -211,9 +211,9 @@ describe("published package surface", () => { "docs/concept/DECISIONS.md", ]; - expect(dryRunPaths).toEqual(expect.arrayContaining(packedAssets)); + expect(packedPaths).toEqual(expect.arrayContaining(packedAssets)); expect( - dryRunPaths.filter( + packedPaths.filter( (path) => !["LICENSE", "README.md", "package.json", ...packedAssets].includes(path) && !path.startsWith("dist/") && @@ -234,11 +234,11 @@ describe("published package surface", () => { } }; await walkCarriers("specs"); - expect(dryRunPaths.filter((path) => path.startsWith("specs/")).sort()).toEqual( + expect(packedPaths.filter((path) => path.startsWith("specs/")).sort()).toEqual( carrierPaths.sort(), ); expect( - dryRunPaths.filter((path) => !path.startsWith("dist/") && /\.tsx?$/u.test(path)), + packedPaths.filter((path) => !path.startsWith("dist/") && /\.tsx?$/u.test(path)), ).toEqual([]); const tarball = packed[0].filename; diff --git a/test/skills.test.ts b/test/skills.test.ts index 2098cb5b..22b532c4 100644 --- a/test/skills.test.ts +++ b/test/skills.test.ts @@ -765,6 +765,24 @@ describe("adopter rule meanings", () => { }); }); +const readmeClauses = [ + ["commit pin", /github:libar-dev\/software-delivery-protocol#/u], + ["npm script approval", /allowScripts/u], + ["npm git approval", /allow-git=root/u], + ["pnpm repository approval", /allowBuilds/u], + ["pnpm 10 exact approval", /onlyBuiltDependencies \(pnpm 10\)/u], + ["installed CLI check", /node_modules\/\.bin\/sdp --version/u], + ["recipe subject as params", /dist\/recipes\/25-address-resolution\.js\)" --params/u], +] as const; + +describe("adopter install and upgrade guidance", () => { + it.each(readmeClauses)("the README teaches %s", (_clause, pattern) => { + const readme = readFileSync(join(repoRoot, "README.md"), "utf8"); + // Keep underscores in executable paths while ignoring code spans and line wrapping. + expect(readme.replaceAll("`", "").replaceAll(/\s+/gu, " ")).toMatch(pattern); + }); +}); + function grammarProbe(section: string) { return parseMarkdownBody( `# Teaching probe\n\n${section}\n`, diff --git a/test/vitest-adapter.test.ts b/test/vitest-adapter.test.ts index 1787db92..667b53b9 100644 --- a/test/vitest-adapter.test.ts +++ b/test/vitest-adapter.test.ts @@ -17,7 +17,10 @@ vi.mock("vitest", async (importOriginal) => ({ })); /** + * The Vitest adapter cleans up each world it creates, whether the steps pass or fail. + * * @sdpAnchor test:protocol.example-runner-cleanup + * @sdpLabel the Vitest adapter cleans up a world whether its steps pass or fail * @sdpVerifies spec:extraction.example-runner */ From bd6238f5503fc3d0167ce97ab66f083982dc1dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 13 Oct 2026 15:11:28 +0200 Subject: [PATCH 15/33] specs: the Studio design pass's review findings The data Spec escapes every `<` in embedded JSON as `<` (it read as a tautology), and the search index's entries are defined without a term, matching as recipe 23 matches. A finding whose subject has no page shows on the index only; the search page's `#q=` fragment is exempt from the element-id check; the Spec page's narrative and sections carry ids that entry hits link to; and the search page is the one page that needs its script. The Pack page leads with its summary, each sentence opening with its count, folds the holds section into the summary, colors only the rung on a card's line, links a question by its key, splits the card body into citable rules, and says once that the graph records no run. The prototype follows. Questions left open gain a decider and a provisional reading, and two deleted questions' reasons are restored. Co-Authored-By: Claude Opus 5.5 --- explorations/studio-prototype/README.md | 10 +- explorations/studio-prototype/studio.css | 4 + explorations/studio-prototype/studio.js | 129 +++++++++++------- specs/consumers/obsidian-vault.sdp.md | 1 + specs/consumers/spec-studio.data.sdp.md | 4 +- specs/consumers/spec-studio.index-page.sdp.md | 7 +- .../consumers/spec-studio.intent-panel.sdp.md | 2 + specs/consumers/spec-studio.lenses.sdp.md | 4 +- specs/consumers/spec-studio.pack-page.sdp.md | 28 ++-- .../spec-studio.page-elements.sdp.md | 2 +- specs/consumers/spec-studio.publisher.sdp.md | 4 +- specs/consumers/spec-studio.sdp.md | 2 +- specs/consumers/spec-studio.search.sdp.md | 2 +- specs/consumers/spec-studio.shell.sdp.md | 4 +- .../spec-studio.spec-page.core.sdp.md | 6 +- specs/consumers/spec-studio.spec-page.sdp.md | 2 + 16 files changed, 131 insertions(+), 80 deletions(-) diff --git a/explorations/studio-prototype/README.md b/explorations/studio-prototype/README.md index b3e6588d..44660871 100644 --- a/explorations/studio-prototype/README.md +++ b/explorations/studio-prototype/README.md @@ -66,9 +66,13 @@ done instead. - Desktop Chrome will not open a window narrower than about 500 pixels, so a phone layout is checked with a headless shell such as the one Playwright caches. -- Questions the owner may want to react to: the full question address inside its own member's card - is long, and a short `question.` would read faster; the summary states counts as sentences - rather than a table. +- The review of the first version changed the page: the summary comes before the framing, each + summary sentence opens with its count, the held and waiting members are named in the summary + instead of a section repeating the cards, a card's line colors only its rung, a question links + as `question.` with its full address behind a copy action, and the note that the graph + records no run result appears once, above the members with no verifier binding. +- A question the owner may want to react to: the summary states counts as sentences rather than + a table. ## The two associations the reader tasks need diff --git a/explorations/studio-prototype/studio.css b/explorations/studio-prototype/studio.css index 2e6055e9..43b24c16 100644 --- a/explorations/studio-prototype/studio.css +++ b/explorations/studio-prototype/studio.css @@ -320,6 +320,10 @@ h3 { white-space: nowrap; } +.sep { + color: var(--muted); +} + .diverges { background: var(--warn-bg); color: var(--warn-text); diff --git a/explorations/studio-prototype/studio.js b/explorations/studio-prototype/studio.js index ce0717d4..79c7c55d 100644 --- a/explorations/studio-prototype/studio.js +++ b/explorations/studio-prototype/studio.js @@ -89,26 +89,26 @@ : '' + esc(rung) + ""; } + // The stated rung as the one colored badge, the floor beside it in words, and the warning color + // only on the divergence, beside its words. function stated(rung, floor) { var diverges = RUNGS.indexOf(floor) < RUNGS.indexOf(rung); return ( rungBadge(rung) + - ' floor ' + + ' floor ' + esc(floor) + "" + (diverges ? ' stated above its floor' : "") ); } + // The next rung in words, or null when the member states ready and has none. function nextRung(member) { - if (member.statedNextRung === null) return { text: "states ready", held: false }; + if (member.statedNextRung === null) return null; var n = member.statedNextRungFailures.length; return n === 0 - ? { text: "next rung " + member.statedNextRung + ": nothing blocks", held: false } - : { - text: "next rung " + member.statedNextRung + ": held by " + count(n, "clause", "clauses"), - held: true, - }; + ? "nothing blocks " + member.statedNextRung + : "waits on " + count(n, "unmet clause", "unmet clauses") + " for " + member.statedNextRung; } function copy(text) { @@ -181,7 +181,10 @@ return ( "
  • " + '' + (q.blocking ? "blocking" : "non-blocking") + " " + - (q.address ? '' + esc(q.address) + " " : "") + + (q.address + ? '' + esc(q.address.slice(q.address.indexOf("#") + 1)) + + ' ' + : "") + (q.line === null ? "" : 'line ' + q.line + "") + "
    " + inline(q.question) + "
    " + lines.map(function (l) { return '' + l + ""; }).join("") + @@ -189,6 +192,17 @@ ); } + function verifierCounts(member) { + var parts = []; + if (member.verifiers.total > 0) { + parts.push(count(member.verifiers.total, "verifier", "verifiers") + ", " + member.verifiers.enabled + " enabled"); + } + if (member.examples.total > 0) { + parts.push(count(member.examples.total, "verifying example", "verifying examples") + ", " + member.examples.enabled + " enabled"); + } + return parts.length === 0 ? "" : ": " + parts.join("; "); + } + function card(member, position, context) { var id = member.id; if (!member.resolved) { @@ -205,7 +219,7 @@ var questionsText = d.openQuestions === 0 ? "no open questions" - : count(d.openQuestions, "question", "questions") + ", " + (d.blockingQuestions === 0 ? "none" : d.blockingQuestions) + " blocking"; + : count(d.openQuestions, "open question", "open questions") + ", " + (d.blockingQuestions === 0 ? "none" : d.blockingQuestions) + " blocking"; var labels = {}; (context.implementations || []).concat(context.references || []).forEach(function (b) { if (b.label) labels[b.codeId] = b.label; @@ -224,7 +238,9 @@ ? "

    States ready, so it has no next rung.

    " : member.statedNextRungFailures.length === 0 ? "

    " + esc(member.statedNextRung) + ": nothing blocks. The floor already holds it, so it waits only for its author's statement.

    " - : "

    " + esc(member.statedNextRung) + " is held by:

    " + clauses(member.statedNextRungFailures); + : "

    " + esc(member.statedNextRung) + " waits on " + + count(member.statedNextRungFailures.length, "unmet clause", "unmet clauses") + ":

    " + + clauses(member.statedNextRungFailures); var decisions = member.decisions.length === 0 @@ -244,12 +260,16 @@ "" + '' + position + ". " + esc(member.title || id) + "" + '' + - '' + esc(member.specKind) + " " + - stated(member.statedReadiness, member.floorReached) + " " + - '' + esc(next.text) + " " + - '' + esc(questionsText) + " " + - 'implementation ' + implementation + " " + - 'verifier ' + verifier + "" + + [ + '' + esc(member.specKind) + "", + stated(member.statedReadiness, member.floorReached), + next === null ? null : '' + esc(next) + "", + '' + esc(questionsText) + "", + 'implementation ' + implementation + "", + 'verifier ' + verifier + "", + ] + .filter(Boolean) + .join('') + "" + '
    ' + '

    ' + esc(id) + "

    " + @@ -269,10 +289,7 @@ "

    Design references: " + (member.referencedBy.length === 0 ? "none" : "present") + "

    " + units(member.referencedBy, labels, "No unit answers to its design.") + "

    Verification

    " + - "

    Verifier binding: " + verifier + ". " + - count(member.verifiers.total, "verifier", "verifiers") + ", " + member.verifiers.enabled + " enabled; " + - count(member.examples.total, "verifying example", "verifying examples") + ", " + member.examples.enabled + " enabled. " + - "No run result: the graph records bindings, never runs.

    " + + "

    Verifier binding: " + verifier + verifierCounts(member) + ".

    " + "
    " ); } @@ -318,27 +335,46 @@ var membersById = {}; design.members.forEach(function (m) { membersById[m.id] = m; }); var resolved = design.members.filter(function (m) { return m.resolved; }); - var statesReady = resolved.filter(function (m) { return m.statedNextRung === null; }).length; var errors = context.findings.filter(function (f) { return f.severity === "error"; }).length; var warnings = context.findings.length - errors; + var link = function (m) { return '' + esc(m.title || m.id) + ""; }; + var held = resolved.filter(function (m) { return m.statedNextRungFailures.length > 0; }); + var waiting = resolved.filter(function (m) { return m.statedNextRung !== null && m.statedNextRungFailures.length === 0; }); + // Each Pack total in one sentence that opens with its count, a zero written as No. The held + // sentence links each held member to its card; the waiting sentence names its members unless + // every member waits. var summary = [ count(t.members, "member", "members") + (t.unresolved === 0 ? ", all in the graph." : ", " + t.unresolved + " of them not in the graph."), - (t.heldBelowNextRung === 0 ? "No member is held below its stated next rung; " : count(t.heldBelowNextRung, "member is", "members are") + " held below their stated next rung; ") + - count(t.waitingForAuthor, "member waits", "members wait") + " only for their author's statement" + - (statesReady === 0 ? "." : ", and " + count(statesReady, "states", "state") + " ready."), - count(t.entries, "Design or UI entry", "Design and UI entries") + ", " + t.declarations + " of them pinned declarations.", - count(t.openQuestions, "open question", "open questions") + ", " + (t.blockingQuestions === 0 ? "none" : t.blockingQuestions) + " blocking.", - count(t.withImplementations, "member has", "members have") + " implementing units, and " + count(t.withReferences, "member has", "members have") + " referencing units.", - "The Pack rests on " + count(t.restsOn, "Spec", "Specs") + " outside it and is rested on by " + t.restedOnBy + "; " + - count(t.verifiedBy, "outside Spec verifies", "outside Specs verify") + " a member, and " + (t.supersededBy === 0 ? "none supersedes" : count(t.supersededBy, "supersedes", "supersede")) + " one.", - count(errors, "error", "errors") + " and " + count(warnings, "warning", "warnings") + " name the Pack.", + held.length === 0 + ? "No member is held below its stated next rung." + : count(t.heldBelowNextRung, "member", "members") + " held below " + (t.heldBelowNextRung === 1 ? "its" : "their") + " stated next rung: " + + held + .map(function (m) { return link(m) + " (" + count(m.statedNextRungFailures.length, "unmet clause", "unmet clauses") + ")"; }) + .join(", ") + ".", + t.waitingForAuthor === 0 + ? "No member waits only for its author's statement." + : t.waitingForAuthor === t.members + ? "All " + t.members + " members wait only for their author's statement." + : count(t.waitingForAuthor, "member waits", "members wait") + " only for " + (t.waitingForAuthor === 1 ? "its" : "their") + + " author's statement: " + waiting.map(link).join(", ") + ".", + t.entries === 0 + ? "No Design or UI entries." + : count(t.entries, "Design or UI entry", "Design and UI entries") + ", " + (t.declarations === 0 ? "none" : t.declarations) + " of them pinned declarations.", + t.openQuestions === 0 + ? "No open questions." + : count(t.openQuestions, "open question", "open questions") + ", " + (t.blockingQuestions === 0 ? "none" : t.blockingQuestions) + " blocking.", + count(t.withImplementations, "member has", "members have") + " an implementation binding; " + + (t.withReferences === 0 ? "none has" : t.withReferences + (t.withReferences === 1 ? " has" : " have")) + " a design reference.", + count(t.restsOn, "outside Spec the Pack rests on", "outside Specs the Pack rests on") + "; " + + (t.restedOnBy === 0 ? "none rests" : t.restedOnBy + (t.restedOnBy === 1 ? " rests" : " rest")) + " on it; " + + (t.verifiedBy === 0 ? "none verifies" : t.verifiedBy + (t.verifiedBy === 1 ? " verifies" : " verify")) + " a member; " + + (t.supersededBy === 0 ? "none supersedes" : t.supersededBy + (t.supersededBy === 1 ? " supersedes" : " supersede")) + " one.", + errors === 0 && warnings === 0 + ? "No errors and no warnings name the Pack." + : count(errors, "error", "errors") + " and " + count(warnings, "warning", "warnings") + " name the Pack.", ]; - var held = resolved.filter(function (m) { return m.statedNextRungFailures.length > 0; }); - var waiting = resolved.filter(function (m) { return m.statedNextRung !== null && m.statedNextRungFailures.length === 0; }); - var link = function (m) { return '' + esc(m.title || m.id) + ""; }; - var gaps = context.verifierGaps.slice().sort(function (a, b) { return (b.priority ? 1 : 0) - (a.priority ? 1 : 0); }); var html = @@ -346,28 +382,22 @@ '

    Pack

    ' + "

    " + esc(design.title || design.id) + "

    " + '

    ' + copy(design.id) + "" + copy(context.file) + "

    " + - (context.framing ? '
    ' + prose(context.framing) + "
    " : "") + - (context.modelRefs.length === 0 - ? "" - : "

    Vocabulary: " + context.modelRefs.map(function (r) { return '' + esc(r) + ""; }).join(", ") + "

    ") + "" + '' + '

    Where the design stands

      ' + summary.map(function (s) { return "
    • " + capital(s) + "
    • "; }).join("") + "
    " + + (context.framing || context.modelRefs.length > 0 + ? '

    Framing

    ' + + (context.framing ? '
    ' + prose(context.framing) + "
    " : "") + + (context.modelRefs.length === 0 + ? "" + : "

    Vocabulary: " + context.modelRefs.map(function (r) { return '' + esc(r) + ""; }).join(", ") + "

    ") + + "
    " + : "") + '

    Members, in reading order

    ' + design.members.map(function (m, i) { return card(m, i + 1, contextById[m.id] || {}); }).join("") + "
    " + - '

    What holds members below their next rung

    ' + - (held.length === 0 - ? "

    No member is held below its stated next rung.

    " - : "
      " + - held.map(function (m) { return "
    • " + link(m) + " is held below " + esc(m.statedNextRung) + " by:" + clauses(m.statedNextRungFailures) + "
    • "; }).join("") + - "
    ") + - (waiting.length === 0 - ? "" - : "

    " + count(waiting.length, "member waits", "members wait") + " only for their author's statement, which is never an approval: " + waiting.map(link).join(", ") + ".

    ") + - "
    " + '

    Outside the Pack

    ' + "

    Specs outside the Pack joined to a member by an authored relation. The Pack rests on a Spec a member refines, depends on, is constrained by or decided by, and is rested on by a Spec that relates to a member the same ways.

    " + boundarySection("Rests on", design.boundary.restsOn, membersById, "Members that rest on it") + @@ -376,6 +406,7 @@ boundarySection("Superseded by", design.boundary.supersededBy, membersById, "Members it supersedes") + "
    " + '

    Members with no verifier binding

    ' + + "

    The graph records bindings, never runs, so no page shows a run result.

    " + (gaps.length === 0 ? "

    Every member has a verifier binding.

    " : '