From e1d2f59747c2cc4882e0e9d061a82c04fa909643 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 21 Oct 2026 22:55:55 +0000 Subject: [PATCH 01/36] docs(articles): add living media plan and first three articles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Darko Mijić --- ...01-the-spec-is-the-prompt-is-the-design.md | 204 +++++++++++++ .../02-walkthrough-intent-to-verified-code.md | 271 ++++++++++++++++++ ...p-building-verb-walls-for-coding-agents.md | 179 ++++++++++++ docs/articles/README.md | 182 ++++++++++++ 4 files changed, 836 insertions(+) create mode 100644 docs/articles/01-the-spec-is-the-prompt-is-the-design.md create mode 100644 docs/articles/02-walkthrough-intent-to-verified-code.md create mode 100644 docs/articles/03-stop-building-verb-walls-for-coding-agents.md create mode 100644 docs/articles/README.md diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md new file mode 100644 index 000000000..198d42236 --- /dev/null +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -0,0 +1,204 @@ +# The Spec Is the Prompt Is the Design: Why AI-Native Software Delivery Needs One Graph, Not Another Ticket Tracker + +> *Series: Introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (Part 1 of 8) · All links and measurements pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)).* + +Every software team knows the decay curve of documentation. + +At the start of a project, intent is sharp. You write an architecture doc, sketch a few sequence diagrams, record a couple of ADRs, file tickets in your issue tracker, and write tests in your repo. Six weeks later, a customer edge case forces a design change during a pull request. The code changes. The test changes. The architecture doc, the ADR, and the ticket do not. Within a quarter, your documentation is a museum of what you *meant* to build three sprints ago. + +Now drop AI coding agents into that repository. + +An LLM can write a function in seconds, but when you ask it to change an architectural seam—"update order creation so stock reservation respects our latency constraint"—how does it know what the system guarantees? It greps raw source files, reads stale Markdown, and **hallucinates structure**. It cannot tell an aspirational idea from a settled contract, an implementation shortcut from a ratified decision, or a stub from a verified feature—because **no typed model of the delivery lifecycle exists in the repository for it to reason over**. + +[**Libar Software Delivery Protocol**](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (`@libar-dev/software-delivery-protocol`, CLI `sdp`) is our answer: an **executable, self-validating meta-model of the software-delivery process**. + +Its premise fits in one sentence: + +> **Specs are code; the graph is derived; the `claim` stays honest; git is the event log.** + +--- + +## 1. A Second Take, Front-Loaded with First Principles + +The Protocol is not our first attempt at this problem. Its predecessor, [`@libar-dev/architect`](https://github.com/libar-dev/architect), ran in production across 349 architectural patterns. It proved three things that worked: keeping annotations in source beside the code, deriving a single immutable graph as the read model for both documentation and agents, and giving agents a scriptable graph handle instead of raw files. + +It also taught us what fails in production: + +- **Sprawling tag registries.** When every metadata wish becomes a new `@architect-*` tag across TypeScript and Gherkin, the formal taxonomy and the live parser inevitably drift apart. +- **Process state-machine gates.** Encoding ticket statuses (`candidate → roadmap → active → completed`) inside source files turns your compiler into a workflow bureaucracy. +- **Hand-authored delivery status.** If a human or an agent types `status: completed` into a spec file, that string will eventually lie. + +We built the Libar Software Delivery Protocol ([PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1)) as a clean-sheet second generation—zero backward compatibility, zero old sins carried over—around **three meta-levels** ([the executable meta-model decision, MD-1](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/executable-meta-model.sdp.md)): + +| Meta-level | What lives here | Role | +|---|---|---| +| **1. The Protocol** | The `Spec` primitive, descriptors, relations, and validators *as typed code* | Defines the conformance contract | +| **2. The Authored Model** | A project's `Spec`s, `Pack`s, and source `anchor`s committed in the repo | Conforms to the contract (checked in CI, never workflow-gated) | +| **3. Derived Facts** | Machine truth (`implemented`, `has-verifier`, `observed`) and projections | Computed from the graph, **never authored by hand** | + +And we bound the entire system with **two permanent honesty guardrails** ([`00-vision-scope-and-mvp-boundary.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md)): + +1. **Checks police conformance and honesty—never content quality, never workflow.** Validators check whether your model is well-formed and whether it is pretending to have evidence it lacks. They never judge whether your design is "good," and they never enforce sprint or ticket state machines ([adopt the nouns, reject the gates, MD-2](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/adopt-the-nouns.sdp.md)). +2. **We claim "deterministically validated," never "provably correct."** Conformance to a typed meta-model is mechanically verifiable; real-world business correctness requires human judgment and runtime evidence. + +--- + +## 2. One Enrichable Primitive: `Spec` + +In traditional delivery tooling, an idea changes artifact types as it matures. It starts as a sticky note or Epic, gets rewritten into a PRD, gets copied into an ADR or RFC, gets sliced into Jira Stories, and finally gets re-typed into Gherkin `.feature` files and unit tests. Every migration between artifact types loses context and breaks traceability. + +In the Protocol, there is **one authored truth-primitive: `Spec`** ([one primitive, named coordinates, MD-4](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/one-primitive.sdp.md); [`spec:model.core-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/core-model.sdp.md)). + +A `Spec` keeps its stable, namespaced identity (`spec:orders.create-order`) from its first two-line capture to production realization. Instead of migrating it to a new artifact type, you **enrich it in place** by adding typed sections (`Intent`, `Behavior`, `Rule`, `Workflow`, `Contract`, `Example space`, `Constraints`, `Model`, `Design`, `Decision`, `UI`, `Verification`) or by refining it into child `Spec`s. + +Every `Spec` is positioned by **three independent descriptors**: + +- **`kind`** *(the category of truth — a true subtype that determines required evidence)*: + `behavior` (Use Case / Behavior) · `workflow` (Workflow) · `example` (Example / Scenario) · `rule` (Business Rule) · `constraint` (Constraint / NFR) · `model` (Domain Model) · `decision` (Decision Record) · `contract` (Contract). +- **`altitude`** *(size and scope)*: + `epic → feature → story`. +- **`readiness`** *(author-stated design maturity, checked against a structural floor)*: + `idea → scoped → defined → ready`. + +Familiar delivery nouns—Use Case, NFR, ADR, Epic, Story, Executable Scenario—are **named coordinates on this single primitive**, not separate schemas or tools. Because `altitude` and `readiness` move independently, an `epic`-altitude `behavior` Spec can honestly sit at `scoped` while one of its `story`-altitude `example` children is already `ready` and verified. + +Alongside `Spec`, two other authored constructs enter the repository, and **neither states system truth of its own**: + +- **`Pack`** (`pack:checkout-v1`, [`spec:model.pack-aggregate`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/pack-aggregate.sdp.md)): a review and reading grouping over `Spec`s. A `Spec` can belong to multiple `Pack`s. +- **`anchor`** ([`spec:model.anchors`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/anchors.sdp.md)): a pointer in source code or tests that binds a code unit (`impl:`, `api:`, `component:`), a test (`test:`), or an expected-outcome oracle (`oracle:`) to `Spec` IDs. An anchor carries identity, bindings, and optional architectural structure—never behavior, rationale, or readiness. + +Here is an entire `idea`-rung `Spec` in its default Markdown carrier (`.sdp.md`), scaffolded by `sdp new spec`: + +```markdown +--- +id: spec:orders.create-order.api-contract +kind: contract +altitude: story +readiness: idea +relations: + refines: spec:orders.create-order +--- + +# HTTP contract for order creation + +## Intent + +- outcome: Define the HTTP request and response payload for POST /orders. + +### Open questions + +- [blocking #idempotencyHeader] Should the endpoint require an Idempotency-Key header on every POST? + - decider: owner + - provisional: Require Idempotency-Key on all order-creation requests. +``` + +No boilerplate. No mandatory 12-section template to fill out just to capture a thought. Yet it is already a typed node in the graph, joined to `spec:orders.create-order`, carrying a keyed blocking question (`spec:orders.create-order.api-contract#question.idempotencyHeader`) that the validator knows will hold this Spec below `defined` until settled. + +--- + +## 3. The Epistemic Boundary: Why `claim` Separation Changes Everything + +Why don't coding agents trust architecture tools? Because most tools mix together three completely different kinds of statements: + +1. What a human *intended* the system to do. +2. What a human *bound* in code or tests. +3. What a static-analysis heuristic *guessed* from import statements. + +Once those three collapse into a single "dependency" edge, a single bad heuristic poisons the graph. + +The Protocol enforces a strict epistemic boundary ([`01-founding-principles-and-invariants.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md), P9 & P10; [`spec:extraction.claim-taxonomy`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/claim-taxonomy.sdp.md)). Every node and every edge in the graph carries a **`claim`** field with one of three values, and **they are never collapsed**: + +| `claim` value | Who asserts it | What it means | Authority | +|---|---|---|---| +| **`declared`** | Human author in a `Spec` or `Pack` carrier | Authoritative delivery intent: relations (`refines`, `dependsOn`, `constrainedBy`, `decidedBy`, `verifies`, `supersedes`), stated readiness, and section content | **Authoritative intent** | +| **`anchored`** | Human author via an in-code `anchor` | Authoritative binding from source code or tests into the graph (`satisfies`, `references`, `verifies`, `models`, `memberOf`, `uses`) | **Authoritative binding** (carries no intent) | +| **`inferred`** | Static analyzer (e.g., `sdp build --imports`) | Mechanical code structure (file-level imports used for curation advice) | **Advisory only** — never confers delivery facts, never triggers validator errors, never silently promoted | + +### Stated Readiness vs. Derived Delivery Facts + +Because `declared` intent and `anchored` bindings are kept separate, **design maturity** (`readiness`) and **code realization** (`deliveryFacts`) live on orthogonal axes ([`spec:extraction.delivery-facts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/delivery-facts.sdp.md)): + +- **Author-stated `readiness` (`idea → scoped → defined → ready`):** What the author states about the maturity of the design, checked against the cumulative [**readiness floor**](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/readiness-floor.sdp.md) (`derivedReadiness`). You cannot state `defined` while a `[blocking]` open question remains open; you cannot state `ready` while a `dependsOn`, `refines`, `constrainedBy`, or `decidedBy` target sits below `defined` ([`spec:validation.typed-dependency-floor`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/typed-dependency-floor.sdp.md)). +- **Derived `deliveryFacts` (`implemented`, `has-verifier`, `observed`):** Computed deterministically from resolving `anchored` edges—**never authored by hand**. Writing `implemented: true` in a Spec file is rejected as an honesty violation (`honesty/authored-delivery-fact`). + - `implemented` is conferred when at least one resolving code anchor declares `satisfies` to that Spec. It never propagates through `refines` from parent to child or child to parent. + - `has-verifier` is conferred when a resolving `specTest` anchor verifies the Spec directly, or when a verifying `example` Spec is itself backed by a resolving `specTest` anchor (an **enabled verifier**). + +Look at the two payoff queries that fall out of keeping those axes separate: + +1. **The Operational Build Backlog ([Recipe 1](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md)):** + \[ + \text{statedReadiness} = \texttt{ready} \;\land\; \texttt{implemented} \notin \text{deliveryFacts} \;\land\; \text{specKind} \notin \{\texttt{example}, \texttt{decision}\} + \] + These are the Specs whose design is settled, whose dependencies are at least `defined`, and whose human author has stated `ready`—waiting to be built. +2. **The Drift Alarm ([Recipe 2](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md)):** + \[ + \texttt{implemented} \in \text{deliveryFacts} \;\land\; \text{statedReadiness} \neq \texttt{ready} + \] + This is code that ran ahead of its design: an implementation anchor claims to realize a Spec whose design has not been stated `ready`. + +--- + +## 4. Conformance and Honesty Checks (Plus Corpus Rules) + +When you run `sdp validate`, every carrier is extracted into the one graph and checked by two validator families ([`spec:validation.two-check-families`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/two-check-families.sdp.md)): + +- **Conformance checks** ask: *Is this model well-formed against the Protocol?* + - Do all relation targets and anchor targets resolve (`conformance/referential-integrity`, with edit-distance suggestions)? + - Are all IDs unique across `.sdp.md`, `.sdp.ts`, `.sdp.gherkin`, and source anchors (`conformance/duplicate-ids`)? + - Are `claim` values, descriptors, and structural anchor edges (`memberOf`, `uses`, `layer`, `context`) valid (`conformance/claim-separation`, `conformance/structural-anchors`)? + - Do `spec:` mentions and entry addresses (`spec:id#design.key`, `#ui.key`, `#question.key`, `#rule.key`) written in prose resolve and have a backing declared relation (`conformance/prose-mentions`)? +- **Honesty checks** ask: *Is this model pretending to be something it isn't?* + - Did someone try to hand-author a derived edge (`satisfies`, `belongsTo`) or a delivery fact (`implemented`, `has-verifier`) inside a Spec (`honesty/authoring-shape`, `honesty/authored-delivery-fact`)? + - Does every Spec clear the cumulative structural floor for the `readiness` rung its author stated (`honesty/readiness-floor`)? + - Are `ready` Specs missing an enabled verifier surfaced visibly as informative `honesty/gaps` warnings without blocking the build? + +And when an adopting team has project-specific architectural rules—for instance, "domain components must never use edge components" or "every question must name a decider"—they run **corpus rules** on the same validation path via `sdp validate --rules` ([`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md), introduced in [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). Corpus findings are reported in their own section, never corrupt Protocol readiness floors, and fail CI when they report an error. + +--- + +## 5. Self-Hosting by the Numbers + +The Protocol does not have a separate PRD. It **self-hosts** ([`spec:protocol.self-hosting`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/protocol/self-hosting.sdp.md)): every rule of its model, extractor, validators, projections, and agent surface is authored in its own `.sdp.md` and `.sdp.gherkin` carriers under [`specs/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs), bound to its own TypeScript engine under [`src/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src) and test suite under [`test/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/test), and certified on every commit by `npm run check`. + +At commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (re-run the queries below rather than quoting these numbers across commits), the Protocol's own self-hosting graph measures: + +| Dimension | Count at `5a0b22c` | Breakdown | +|---|---|---| +| **Authored `Spec`s** | **276** | `110 example` · `71 behavior` · `41 decision` · `35 rule` · `12 model` · `5 contract` · `1 workflow` · `1 constraint` | +| **Stated `readiness`** | **276** | `245 ready` · `17 defined` · `3 scoped` · `11 idea` | +| **Authored `Pack`s** | **6** | `self-hosting-v1` (276) · `design-management-v1` (24) · `spec-studio-v1` (18) · `corpus-checks-v1` (13) · `design-iteration-v1` (11) · `obsidian-vault-v1` (5) | +| **Source & Test Bindings** | **339** | `160 CodeNode` (`impl:`, `api:`, `component:` across 14 components and 4 layers) · `179 Anchor` (`test:` and `oracle:`) | +| **Derived Graph Size** | **621 nodes · 1,624 edges** | `1,063 declared` edges · `561 anchored` edges · `1,774` entry & relation source locations (schema `0.10.0`) | +| **Derived Delivery Facts** | **94 `implemented` · 205 `has-verifier`** | Operational build backlog (recipe 1): **0** · Drift alarm (recipe 2): **0** · Design backlog (recipe 33): **17** | + +Meanwhile, the first external adopter, `libar-platform`, models a six-layer transactional domain platform as **243 Specs across six Packs**, using the Protocol to run token-budgeted design passes, enforce context isolation through `sdp validate --rules`, and publish a 251-page static HTML **Spec Studio** workbench ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). + +![The Spec Studio Pack page for pack:corpus-checks-v1](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/protocol-pack.png) + +--- + +## 6. Try It Yourself in 60 Seconds + +Clone the repository and query the live self-hosting graph through `sdp q`—the evaluation sink that derives the graph in memory and runs any async JavaScript body you give it: + +```sh +git clone https://github.com/libar-dev/software-delivery-protocol.git +cd software-delivery-protocol +npm ci + +# 1. Validate the 276-Spec self-hosting corpus +npm run --silent sdp -- validate . --exclude explorations --exclude examples --exclude test/fixtures/import/parity + +# 2. Run Recipe 1 (the operational build backlog) and Recipe 2 (the drift alarm) +npm run --silent sdp:q -- "$(cat dist/recipes/01-the-build-backlog.js)" --json +npm run --silent sdp:q -- "$(cat dist/recipes/02-the-drift-alarm.js)" --json + +# 3. Publish the static HTML Spec Studio and open generated/spec-studio/index.html +npm run --silent sdp -- studio . --exclude explorations --exclude examples --exclude test/fixtures/import/parity +``` + +--- + +## Up Next in the Series + +In **[Part 2: From Rough Idea to Verified Code](./02-walkthrough-intent-to-verified-code.md)**, we open the hood on `examples/checkout-v1` and the Protocol's own source code to walk through the complete engineering loop: authoring `.sdp.md` and `.sdp.gherkin` carriers, binding code with constant-form and `@sdp*` comment-form anchors, generating typed step and space contracts, writing an expected-outcome `specOracle`, and deliberately breaking the graph to watch the conformance and honesty checks fire. diff --git a/docs/articles/02-walkthrough-intent-to-verified-code.md b/docs/articles/02-walkthrough-intent-to-verified-code.md new file mode 100644 index 000000000..1c5c328a0 --- /dev/null +++ b/docs/articles/02-walkthrough-intent-to-verified-code.md @@ -0,0 +1,271 @@ +# From Rough Idea to Verified Code: A Hands-On Tour of the Libar Software Delivery Protocol + +> *Series: Introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (Part 2 of 8) · All links and measurements pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)).* + +In [Part 1](./01-the-spec-is-the-prompt-is-the-design.md), we introduced the core thesis of the **Libar Software Delivery Protocol**: model the delivery lifecycle as instances of one enrichable primitive (`Spec`), bind code and tests with source `anchor`s, derive **one regenerable graph**, and check **conformance and honesty** in CI. + +That sounds clean in principle. What does it actually look like in a text editor and a terminal? + +In this post, we will trace a single bounded context—**Order Management** ([`examples/checkout-v1`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1)), alongside real comment-form bindings from the Protocol's own engine—from an initial `.sdp.md` Spec all the way to generated TypeScript contracts, an expected-outcome oracle, and deterministic CI checks. + +--- + +## 1. The Worked Example: 11 Specs, 1 Pack, 5 Anchors + +The repository ships a complete, self-contained bounded context under [`examples/checkout-v1`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md). It models Order Management (`pack:checkout-v1`) using all eight `Spec` kinds (`behavior`, `workflow`, `example`, `rule`, `constraint`, `model`, `decision`, `contract`) and all four readiness rungs (`idea`, `scoped`, `defined`, `ready`): + +```text +examples/checkout-v1/ + specs/ + checkout.pack.sdp.md # Pack manifest (pack:checkout-v1) + orders/order-management.sdp.md # behavior · epic · scoped + orders/order-placement-flow.sdp.md # workflow · feature · scoped + orders/create-order.sdp.md # behavior · feature · defined (owns example space) + orders/create-order-valid-cart.sdp.md # example · story · ready (enabled verifier) + orders/create-order-invalid-cart.sdp.md # example · story · defined (declared only) + orders/create-order-api-contract.sdp.md # contract · story · idea (blocking question) + orders/order-total-rule.sdp.md # rule · story · ready + orders/order-inventory-rule.sdp.md # rule · story · ready + orders/order-latency-constraint.sdp.md # constraint · feature · defined + orders/order-model.sdp.md # model · feature · defined + decisions/order-lifecycle.sdp.md # decision · feature · defined + src/orders/ + create-order.use-case.ts # impl:orders.create-order-use-case + impl:orders.order-total + create-order.route.ts # api:orders.post + test/orders/ + create-order.valid-cart.test.ts # test:orders.create-order.valid-cart + create-order.oracle.ts # oracle:orders.create-order + orders.create-order.valid-cart.test.generated.ts # committed adopted registrar (MD-31) +``` + +When you run `sdp build examples/checkout-v1`, the extractor parses the `.sdp.md` carriers and the TypeScript anchors and prints: + +```text +11 specs · 1 packs · 5 anchors → 17 nodes · 32 edges (0 errors, 0 warnings) +``` + +Let's inspect the pieces that produce those 17 nodes and 32 edges. + +--- + +## 2. Authoring Intent: The Parent Spec and Its Typed Example Space + +Open [`examples/checkout-v1/specs/orders/create-order.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/specs/orders/create-order.sdp.md): + +```markdown +--- +id: spec:orders.create-order +kind: behavior +altitude: feature +readiness: defined +relations: + refines: spec:orders.order-management + constrainedBy: spec:orders.order-latency-constraint + decidedBy: spec:decisions.order-lifecycle +--- +# Customer creates an order + +## Intent + +- actor: customer +- outcome: Turn a valid cart into an order. +- value: Customers can complete purchases without the example modeling the rest of checkout. + +## Example space + +```gwt-vocabulary +Given a customer has a cart with {n:number} line items +And every line item has quantity {q:number} and unit price {price:number} +And every cart item is {availability:"in stock"|"out of stock"} +When the customer submits the cart for order creation +Then an order is created with total {total:number} +And the order contains the original cart lines +And order creation is rejected because {reason:"empty cart"|"out of stock"} +``` +``` + +Notice three things about this carrier: + +1. **The YAML frontmatter is the closed `envelope` ([`spec:carrier.envelope-contract`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/envelope-contract.sdp.md)).** It carries `id`, `kind`, `altitude`, `readiness`, and `relations`. Nothing else is permitted in frontmatter—every derived property (`implemented`, `hasVerifier`, `claim`) is refused if authored by hand. +2. **Sections carry content; relations carry links ([content-only sections, MD-10](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/content-only-sections.sdp.md)).** `create-order` links to its parent (`refines: spec:orders.order-management`), its latency NFR (`constrainedBy: spec:orders.order-latency-constraint`), and the shaping ADR (`decidedBy: spec:decisions.order-lifecycle`) in `relations`, never buried inside bullet text. +3. **The `gwt-vocabulary` fence defines a typed `example space` ([`spec:carrier.slot-notation`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/slot-notation.sdp.md)).** Each `{slot:type}` placeholder—`{n:number}`, `{availability:"in stock"|"out of stock"}`, `{reason:"empty cart"|"out of stock"}`—declares the shared parameter vocabulary that child examples will bind. + +### Binding a Point in the Space + +Now look at the child example [`examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md): + +```markdown +--- +id: spec:orders.create-order.valid-cart +kind: example +altitude: story +readiness: ready +relations: + refines: spec:orders.create-order + verifies: spec:orders.create-order +--- +# Valid cart creates an order + +## Intent + +- outcome: Show that a valid cart can become an order. +- value: The authored example demonstrates the happy path for create-order. + +```gwt +Given a customer has a cart with {n: 2} line items +And every line item has quantity {q: 1} and unit price {price: 50} +And every cart item is {availability: "in stock"} +When the customer submits the cart for order creation +Then an order is created with total {total: 100} +And the order contains the original cart lines +``` + +## Verification — executable + +- The order result contains a stable id. +- The returned total matches the authored total — the spec's value, never the test's. +``` + +Under [the point-per-example law (MD-17)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/point-per-example.sdp.md), each `example` Spec binds one concrete point in its parent's example space (`n: 2, q: 1, price: 50, availability: "in stock" → total: 100`). Under the **concreteness law** ([`spec:validation.kind-evidence`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/kind-evidence.sdp.md)), if any slot used in a step is left unbound (for instance, `{n}` instead of `{n: 2}`), the example is capped below `defined` and `sdp build` withholds its step contract. + +> **Prefer Gherkin for behavior and example Specs?** Under [the Gherkin carrier option (MD-27)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/gherkin-carrier-option.sdp.md) and [the `.sdp.gherkin` extension law (MD-28)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/sdp-gherkin-extension.sdp.md), any `behavior` or `example` Spec ID can instead use a graph-aware `.sdp.gherkin` file as its one canonical surface, producing the exact same graph nodes and generated contracts. + +--- + +## 3. Binding Source Code: Constant Form and Comment Form + +How does source code connect to `spec:orders.create-order`? Through an in-code **anchor** ([`spec:model.anchors`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/anchors.sdp.md)). + +An anchor is a human-authored binding assertion in a `.ts` or `.tsx` file. It never carries Spec prose, readiness, or status. It can be written in **two representations** that feed the exact same closed envelope: + +### Representation A: The Constant Form +When your code may import `@libar-dev/software-delivery-protocol` (or its zero-dependency subpath `@libar-dev/software-delivery-protocol/anchors` for browser and edge runtimes), you write a top-level constant, as in [`examples/checkout-v1/src/orders/create-order.use-case.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/src/orders/create-order.use-case.ts): + +```ts +import { codeAnchor, codeAnchorId, ref } from "@libar-dev/software-delivery-protocol"; + +export const createOrderUseCaseAnchor = codeAnchor({ + id: codeAnchorId("impl:orders.create-order-use-case"), + label: "createOrderFromCart", + satisfies: ref("spec:orders.create-order"), +}); + +export const orderTotalRuleAnchor = codeAnchor({ + id: codeAnchorId("impl:orders.order-total"), + label: "cart-math order total", + satisfies: ref("spec:orders.order-total-rule"), +}); +``` + +### Representation B: The Comment Form (Zero Runtime Imports) +What if your production runtime is strict about zero external imports? In [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29), the first adopter's experience led to [the comment-form ruling (MD-36)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md), [the binding-grain ruling (MD-37)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md), and [the architectural-annotation ruling (MD-38)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/architectural-annotation.sdp.md). + +You can write a top-level `/** … */` comment using reserved camelCase `@sdp*` tags with **zero imports**. Here is a real anchor from the Protocol's own census projection in [`src/projections/census.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src/projections/census.ts): + +```ts +/** + * @sdpAnchor impl:protocol.census-page + * @sdpLabel renders the derived census and runtime taxonomy projection + * @sdpSatisfies spec:consumers.census-page + * @sdpComponent component:protocol.projections + * @sdpReferences spec:decisions.architectural-annotation + * @sdpRole projection + */ +``` + +Look closely at the distinction between `@sdpSatisfies` and `@sdpReferences` ([MD-37](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md)): + +- **`@sdpSatisfies` (`satisfies`):** This unit (together with any sibling units satisfying the same Spec) **realizes** the Spec's promises. It confers the `implemented` delivery fact on `spec:consumers.census-page`. +- **`@sdpReferences` (`references`):** This unit **answers to** a design or decision (`spec:decisions.architectural-annotation`) without claiming to realize it—or realizes only part of a Spec whose remaining promises are not yet built. It puts a `references` edge in the graph so `blastRadius` and impact queries traverse it, **without conferring `implemented` and without tripping the drift alarm**. + +If you write a stub or follow an ADR, you use `references`. You never have to lie with `satisfies` just to get traceability. + +--- + +## 4. The Executable Half: Generated Contracts, Registrars, and Oracles + +How do we verify `spec:orders.create-order.valid-cart` so that if someone changes `{total: 100}` to `{total: 120}` in the Markdown Spec, the test suite fails immediately—without brittle Cucumber regexes? + +When `sdp build examples/checkout-v1` runs, it emits **derived TypeScript contracts** under `generated/contracts/` and a **registrar** beside the test ([`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md)): + +1. **The Space Contract (`orders.create-order.space.ts`):** Exports `CreateOrderConditions` (`{ n: number; q: number; price: number; availability: "in stock" | "out of stock" }`), every child's bound point, and the `CreateOrderOutcome` discriminated union derived from the parent's `Then` steps. +2. **The Expected-Outcome Oracle ([`test/orders/create-order.oracle.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.oracle.ts)):** Models the expected outcome across the entire example space, bound by `specOracle`: + +```ts +export const createOrderOracle = specOracle({ + id: oracleAnchorId("oracle:orders.create-order"), + label: "expected create-order outcome over the example space", + models: ref("spec:orders.create-order"), +}); + +export function expected(conditions: Partial): CreateOrderOutcome { + if ( + conditions.n === undefined || + conditions.q === undefined || + conditions.price === undefined || + conditions.availability === undefined + ) { + return unspecified; + } + if (conditions.n === 0) { + return { kind: "order creation is rejected because {reason}", reason: "empty cart" }; + } + if (conditions.availability === "out of stock") { + return { kind: "order creation is rejected because {reason}", reason: "out of stock" }; + } + if (conditions.q <= 0 || conditions.price <= 0) { + return unspecified; + } + return { + kind: "an order is created with total {total}", + total: conditions.n * conditions.q * conditions.price, + }; +} +``` + +3. **The Bound Test ([`test/orders/create-order.valid-cart.test.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.valid-cart.test.ts)):** Anchors `test:orders.create-order.valid-cart` and activates the generated registrar with `createWorld`, `invoke`, `observe`, `expected`, and `assertions`: + +```ts +export const createOrderValidCartTest = specTest({ + id: testAnchorId("test:orders.create-order.valid-cart"), + label: "valid cart verifies the create-order happy path", + verifies: ref("spec:orders.create-order.valid-cart"), +}); + +registerValidCart({ createWorld, invoke, observe, expected, assertions }); +``` + +The generated registrar compares three things at runtime: +- **The Spec's authored outcome** (`{ total: 100 }` from the `.sdp.md` file), +- **The Oracle's expected outcome** (`2 * 1 * 50 = 100` from `expected(point)`), and +- **The System Under Test's observed outcome** (`world.order.total` from `createOrderFromCart`). + +If the Markdown Spec, the oracle, or the implementation drifts from either of the other two, the test fails and names the exact step that diverged. + +Meanwhile, `spec:orders.create-order.invalid-cart` is authored at `defined` and declares `verifies: spec:orders.create-order`, but has no `specTest` anchor yet. When you run Recipe 10 (declared versus enabled verifiers) against `examples/checkout-v1`: + +```sh +node dist/cli/sdp.js q "$(cat dist/recipes/10-declared-versus-enabled-verifiers.js)" --root examples/checkout-v1 --json +``` + +The graph reports both `valid-cart` and `invalid-cart` under `declaredExampleVerifiers`, but only `valid-cart` as `enabled: true`. Because `invalid-cart` states `defined` (not `ready`), it sits quietly as honest incomplete trace data without a false warning ([the unbound-example posture, `spec:validation.unbound-example-posture`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/unbound-example-posture.sdp.md)). + +--- + +## 5. Break It on Purpose: Watching the Honesty Checks Fire + +The fastest way to appreciate the validators is to trip them in `examples/checkout-v1` (revert each edit when done): + +| Experiment | What you edit | What happens when you run `sdp validate` or `vitest` | +|---|---|---| +| **1. Misspell a section heading** | In `order-total-rule.sdp.md`, change `## Rule` to `## Rul` | Exits `1` with `extract/unrecognized-heading` and suggests `Did you mean "## Rule"?` | +| **2. Dangle a relation target** | In `create-order.sdp.md`, change `refines: spec:orders.order-management` to `spec:orders.order-managment` | Exits `1` with `conformance/referential-integrity` and suggests `spec:orders.order-management` (edit distance $\le 2$) | +| **3. State readiness you haven't earned** | In `create-order.sdp.md` (stated `defined`), add `### Open questions` with `- [blocking] Wait?` | Exits `1` with `honesty/readiness-floor`, naming unmet clause `no-blocking-open-questions` | +| **4. Leave a parameter slot unbound** | In `create-order-valid-cart.sdp.md`, change `{n: 2}` to `{n}` | Exits `1` with `honesty/readiness-floor` (`kind-evidence-complete`) and withholds the step contract | +| **5. Drift a Spec value from code** | In `create-order-valid-cart.sdp.md`, change `{total: 100}` to `{total: 120}`, rebuild, and run `npx vitest --run examples/checkout-v1/test` | The bound test fails immediately because the generated contract supplies the Spec's `{total: 120}` against the oracle and implementation's `100` | + +--- + +## Up Next in the Series + +We have seen how Specs and anchors enter the repo and how the extractor and validators keep them honest. In **[Part 3: Stop Building 30-Verb CLI Walls for Coding Agents](./03-stop-building-verb-walls-for-coding-agents.md)**, we look at what happens *downstream* of the graph: how AI coding agents script `sdp q` across 43 runnable recipes, how design passes fit 240-Spec corpora into context bundles, and how humans review the same graph in the static HTML **Spec Studio**. diff --git a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md new file mode 100644 index 000000000..9a26030f9 --- /dev/null +++ b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md @@ -0,0 +1,179 @@ +# Stop Building 30-Verb CLI Walls for Coding Agents: Scripting the Delivery Graph with `sdp q` and Spec Studio + +> *Series: Introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (Part 3 of 8) · All links and measurements pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)).* + +When teams build CLI tools or MCP servers for AI coding agents, they almost always fall into the same trap: they build a **verb wall**. + +First, they add `cli list-specs`. Then an agent needs to filter by readiness, so they add `--readiness`. Then an agent needs to see which tests verify a spec, so they add `cli show-verifiers`. Soon there are 30 subcommands and 80 flags. Yet the first time an agent asks a composite question—"show me every `defined` Spec in this Pack that has no blocking questions, together with the files that reference it"—no single verb answers. The agent makes 15 sequential tool calls, blows its context window on verbose CLI output, and stitches the join together in its own scratchpad. + +In the **Libar Software Delivery Protocol**, we took the opposite path ([the agent-surface-scripts-graph decision, D5](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-surface-scripts-graph.sdp.md); [the agent front door, MD-22](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-front-door.sdp.md)): + +> **Don't give coding agents a wall of verbs. Give them a visible, typed graph and one evaluation sink they can script.** + +--- + +## 1. The Demand Map: How Coding Agents Actually Arrive + +Before designing [`spec:consumers.agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.sdp.md), we asked a simple question: *What is an agent holding in its hand when a session starts?* + +Almost never a `Spec` ID. In practice, an agent arrives holding one of three things—what the Protocol calls the **demand map**: + +1. **A concept string** from a user prompt ("where do we enforce the readiness floor?"). +2. **A file path** it is about to read or edit (`src/projections/census.ts`). +3. **A changeset** of modified files from `git diff` (`["src/reader/reader.ts", "src/cli/q-command.ts"]`). + +So the Protocol's typed [`reader`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/reader.sdp.md) (`g`) freezes **only** the decoded graph joins (`specs()`, `packs()`, `specContext(id)`, `packContext(id)`) plus those three entry adapters: + +- `g.findByConcept(term)` — searches titles, narratives, section content, coined camelCase keys, and model terms. +- `g.byFile(path)` — returns every `CodeNode` or `Anchor` recorded at `path` (including zero-import `@sdp*` comment-form anchors) and the `Spec`s reachable from it. +- `g.blastRadius(files)` — computes the file-level impact over the curated graph: directly impacted `Spec`s and `Pack`s, the one-hop `atRisk` neighborhood with each connecting edge's type and `claim`, changed identity-only `unlinked` units, and—crucially—**`coverageUnknown`**, the honest list of changed files the graph records nothing at. + +### One Evaluation Sink: `sdp q` + +Instead of minting CLI subcommands for every question, the CLI exposes one evaluation sink: **`sdp q`** ([`spec:consumers.agent-surface.evaluation-sink`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.evaluation-sink.sdp.md)). + +On every invocation, `sdp q` derives the graph in process (so a Spec you edited two seconds ago is immediately live, with no stale cache), injects four bindings—`g` (the reader), `graph` (the raw schema), `report` (the validation report), and `params` (caller-supplied JSON via `--params`)—and evaluates the plain JavaScript async function body you pass it: + +```sh +# What Specs and code units live in src/projections/census.ts? +npm run --silent sdp:q -- 'return g.byFile("src/projections/census.ts")' --json +``` + +```json +{ + "path": "src/projections/census.ts", + "nodes": [ + { + "id": "impl:protocol.census-page", + "nodeType": "CodeNode", + "line": 20 + } + ], + "specs": [ + "spec:consumers.census-page", + "spec:decisions.architectural-annotation" + ] +} +``` + +### Growing by Recipe, Not by Verb + +What happens when a new recurring question appears? We write a **recipe** in [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md). Every recipe in the catalog is executed verbatim by `test/recipes.test.ts` and shipped as a standalone runnable file under `dist/recipes/-.js` ([`spec:consumers.agent-surface.recipe-parameters`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.recipe-parameters.sdp.md)). + +A join only graduates into the `reader` at the **second-caller bar**: when at least two distinct consumers need it *and* hand-rolled scripts get the edge semantics wrong (for example, `packContext`'s Pack design assembly in [PR #30](https://github.com/libar-dev/software-delivery-protocol/pull/30)). Everything else stays a composable recipe. + +When we tested this surface in a usability trial across three frontier models (`gpt-6.1-sol`, `Opus 5.5`, `gpt-6-astra`; see [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31)), all three models answered 13 out of 13 delivery questions from the skills and `sdp q` alone without opening a single `.sdp.md` file—and their friction points directly shaped summary-first recipe outputs (`detail: true`), unknown-parameter diagnostics (`{ unknownParams, accepts }`), and rule-level source lines. + +--- + +## 2. Iterating on a Large Design Pass After Pass + +The real test of an AI-native delivery system is not a 10-Spec toy example. It is our first adopter, `libar-platform`, which models a Convex transactional domain platform across **243 Specs in six capability Packs**—most of it designed months ahead of its code ([PR #26](https://github.com/libar-dev/software-delivery-protocol/pull/26), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). + +At that scale, four problems hit immediately—and the Protocol's iteration loop solves each one on the one graph: + +### 1. Fitting a Pack into a Model's Context Budget (`recipe 36`) +When an agent runs a design pass over a capability Pack, hand-assembling the Pack design, member sections, open questions, dependency footing, pinned declarations, and examples overflows the context window (`pack:history-view` in `libar-platform` was nearly 300,000 UTF-16 code units). + +**The Context Bundle ([`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md), recipe 36)** composes a pass's reading material on demand within a hard character budget (default `100,000` UTF-16 code units). It stores each question and pinned declaration once, and if the Pack exceeds the budget, it applies a deterministic **seven-step cut order**—trimming boundary details to IDs, footing details to IDs, narratives, example steps, non-blocking questions, non-declaration section entries, and finally (when opted in with `declarations: "addresses"`) replacing pinned declarations with their entry addresses (`spec:id#design.key`) so the agent can look up only the signatures it needs. It never returns a partial member list. On `libar-platform`'s `pack:history-view`, `declarations: "addresses"` brings a 299k-unit Pack cleanly down to **93,806 units**. + +```sh +npm run --silent sdp:q -- "$(cat dist/recipes/36-the-context-bundle.js)" \ + --params '{"pack":"pack:design-iteration-v1","declarations":"addresses"}' --json +``` + +### 2. Comparing Design Passes at Entry Grain (`recipe 35`) +`git diff` shows changed lines; it does not tell you which Design keys changed, which open questions closed, or which readiness rungs moved since the last design pass. **The Design Diff ([`spec:consumers.design-diff`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/design-diff.sdp.md), recipe 35)** takes a base `graph.json` via `--params base=@path` and diffs the curated graph by Spec, section, and entry key, keeping code-binding changes separate from intent changes. + +### 3. Handing Changes Back Safely (`recipe 38`) +Views never patch source files directly ([`spec:consumers.edit-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/edit-model.sdp.md)). Instead, a design pass returns a structured **Change Proposal** ([`spec:consumers.intent-composition.proposal`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/intent-composition.proposal.sdp.md))—a JSON document validated against [`schemas/sdp-proposal-1.schema.json`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/schemas/sdp-proposal-1.schema.json)—where each item targets a Spec section or entry address (`spec:id#rule.key`, `#design.key`, `#question.key`). Before an agent applies the proposal as an ordinary source edit, **the Proposal Check (recipe 38)** verifies whether any target scope moved since the proposal's base commit; any scope that moved is routed to a human. + +### 4. Running Project-Specific Architecture & Policy Rules (`sdp validate --rules`) +In [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33), `libar-platform` retired the graph-side half of its custom Python checker (`check.py`) into `sdp validate --rules`. Because any `sdp q` body returning `{ findings }` is a valid rules file, shipped recipes like **the Architecture Walk ([`spec:validation.architecture-constraints`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/architecture-constraints.sdp.md), recipe 42)** run directly inside `sdp validate`: + +```sh +node dist/cli/sdp.js validate . \ + --exclude explorations --exclude examples --exclude test/fixtures/import/parity \ + --rules dist/recipes/42-the-architecture-walk.js \ + --params '{"forbidden":[{"from":"domain","to":"edge"}]}' +``` + +```text +validate: 0 errors · 6 warnings (conformance + honesty over the one graph) +corpus rules: 0 errors · 0 warnings · 0 of 1 files failed (the corpus's own rules) +``` + +--- + +## 3. Spec Studio: The Static HTML Workbench for Human Reviewers + +Agents love `sdp q` and JSON. Humans reviewing a 20-member capability Pack do not want to read JSON, and once a Pack grows rich with design columns, open questions, and code bindings, a flat Markdown table becomes 13 columns wide. + +In [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33), we shipped Phase 0 of the **Spec Studio** ([`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md); [the Studio renders derived HTML, MD-41](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-html-surface.sdp.md); [built from custom elements, MD-42](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-web-components.sdp.md)). + +Run one command: + +```sh +node dist/cli/sdp.js studio . --exclude explorations --exclude examples --exclude test/fixtures/import/parity +``` + +`sdp studio` publishes a deterministic, zero-network static HTML site under `generated/spec-studio/` that opens directly from `file://` in any browser or hosts on any static preview server: + +- **No second graph in the browser.** Every join, claim, delivery fact, readiness floor, and finding on the page was computed once by the `reader` during `sdp studio`. Six framework-free Web Components add filtering, collapsible cards, address-copy buttons, and instant search without recomputing graph logic. +- **Readable Pack pages ([`spec:consumers.spec-studio.pack-page`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.pack-page.sdp.md)).** Each Pack page leads with a plain-language summary of where the design stands, followed by collapsible member cards in authored reading order and the Pack's four-part boundary (`Rests on`, `Rested on by`, `Verified by`, `Superseded by`): + +![The platform's history-view Pack page in Spec Studio](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-pack.png) + +- **Addressable open questions and section entries ([`spec:consumers.spec-studio.spec-page.core`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.spec-page.core.sdp.md)).** Every keyed Design entry (`#design.`), UI entry (`#ui.`), rule (`#rule.`), and open question (`#question.`) has a deep link and a one-click address copy action, displaying its `decider`, `links`, `provisional` answer, and `trigger`: + +![A Spec page's open questions with keyed addresses and fields in Spec Studio](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-question.png) + +- **Shared concept search ([`spec:consumers.spec-studio.search`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.search.sdp.md)).** The browser search page bundles the exact same `./concept-match` matcher that powers `g.findByConcept` in `sdp q`, indexing Spec prose, coined camelCase keys (`verifyPassUnderPause`), pinned declarations, and file paths: + +![Searching for decision register ID OD-157 in Spec Studio](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-search.png) + +--- + +## 4. Three Mandatory Skills: How Agents Learn the Workflow + +A powerful graph surface only helps if coding agents actually use it instead of falling back to `grep` and file scanning. + +The Protocol ships three repository-owned agent skills under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills) (included in the npm package so every adopter gets them automatically): + +1. **[`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md)** — Teaches the graph shape, the task-to-recipe lookup table, the wrap-and-filter idiom, and the **anti-anecdote rule**: *the derived graph outranks any summary or cached note; query the graph first, read files last*. +2. **[`sdp-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md)** — Teaches the closed `.sdp.md` and `.sdp.gherkin` grammars, where unsettled truths live (blocking questions, `trigger:` deferrals, `provisional:` leans), how to choose honestly between `satisfies` and `references`, and when to slim implemented design detail past the **distillation boundary**. +3. **[`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md)** — Routes delivery sessions across five advisory work shapes (*Capture*, *Design*, *Implement*, *Review*, *Close/Slim*), complete with tripwires and a re-measurable handoff template—without ever turning those shapes into a workflow state machine. + +--- + +## 5. Try the Agent Surface and Spec Studio + +From a checkout of [`libar-dev/software-delivery-protocol`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) after `npm ci`: + +```sh +# 1. Inspect the Pack design summary for pack:spec-studio-v1 (Recipe 29) +npm run --silent sdp:q -- "$(cat dist/recipes/29-pack-design.js)" \ + --params '{"pack":"pack:spec-studio-v1"}' --json + +# 2. Check all 43 anchored uses edges across layers and contexts (Recipe 32) +npm run --silent sdp:q -- "$(cat dist/recipes/32-architecture-crossings.js)" --json + +# 3. Search for a family of keyed Design entries by prefix (Recipe 41) +npm run --silent sdp:q -- "$(cat dist/recipes/41-the-key-family-register.js)" \ + --params '{"prefixes":["bundle","rule"]}' --json + +# 4. Compare planned component designs against realized component: anchors (Recipe 43) +npm run --silent sdp:q -- "$(cat dist/recipes/43-planned-against-realized-components.js)" --json +``` + +--- + +## Where the Series Goes Next + +With the launch manifesto ([Part 1](./01-the-spec-is-the-prompt-is-the-design.md)), the hands-on code walkthrough ([Part 2](./02-walkthrough-intent-to-verified-code.md)), and the agent surface & Spec Studio tour (Part 3) in place, the upcoming installments in the [Media Plan](./README.md) dive into specific architectural stories from the Protocol's construction: + +- **Part 4:** *Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition* +- **Part 5:** *What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Annotation Reversal, and Honest Code Bindings* +- **Part 6:** *Executable Specifications Without Cucumber Glue: Typed Example Spaces, Generated Contracts, and Expected-Outcome Oracles* +- **Part 7:** *Designing a 240-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale* +- **Part 8:** *Auditing 276 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh* diff --git a/docs/articles/README.md b/docs/articles/README.md new file mode 100644 index 000000000..eeabf34d6 --- /dev/null +++ b/docs/articles/README.md @@ -0,0 +1,182 @@ +# Media Plan & Living Series Playbook — Libar Software Delivery Protocol + +> **Pinned baseline:** commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (`5a0b22c2d9ead98c900b60ed6209342fce2cbef6`), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33). +> **Posture:** This directory lives on a standing, unmerged pull request that is periodically refreshed as `main` advances. Every corpus number in this series is derived from the graph at the pinned commit—re-run the queries in [The Living-PR Refresh Playbook](#5-the-living-pr-refresh-playbook) rather than inheriting counts across updates. + +--- + +## 1. Narrative Positioning & Core Thesis + +### The problem we lead with +Software teams have more tools than ever, yet delivery truth is more fragmented than ever. Intent lives in tickets and product docs, architecture lives on whiteboards and stale wikis, decisions live in scattered ADR files, acceptance criteria live in `.feature` files, and verification lives in CI logs. Nothing joins them except rot-prone prose. Within weeks of implementation, documentation drifts from code. + +When AI coding agents enter that environment, the fragmentation compounds. Agents read raw source files and stale Markdown, guess at boundaries, and hallucinate structure because **no typed model of the system exists in the repository for them to reason over**. + +### The thesis +**Libar Software Delivery Protocol** (`@libar-dev/software-delivery-protocol`, CLI `sdp`) is a programmatic, AI-native software-delivery system built on one bet: **the spec is the prompt is the design**. Instead of syncing external trackers or bolting a workflow state machine onto git, you model the whole delivery lifecycle as typed code in the repository, derive **one regenerable graph** from it, check **conformance and honesty** in CI, and generate every human- and AI-facing artifact off that graph. + +### The four-part slogan +Every article in the series reinforces one or more clauses of the founding slogan from the [concept overview](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/README.md): + +1. **Specs are code** — One authored truth-primitive (`Spec`) enriched in place across three independent descriptors (`kind` × `altitude` × `readiness`), authored in Markdown (`.sdp.md`) by default, TypeScript (`.sdp.ts`), or graph-aware Gherkin (`.sdp.gherkin`). +2. **The graph is derived** — One flat, deterministic, regenerable graph (`graph = f(repo)`) serves as the sole read model for validators, human views, and coding agents. Delete `generated/` and rebuild it byte-for-byte. +3. **The `claim` stays honest** — Every fact carries its epistemic status (`declared` human intent, `anchored` source bindings, or `inferred` mechanical structure) and the three are never collapsed. Delivery facts (`implemented`, `has-verifier`, `observed`) are derived from resolving edges, never hand-authored. +4. **Git is the event log** — History and prior states live in git commits, not in a second database or ticket status machine. + +--- + +## 2. Target Audiences & Distribution Strategy + +| Audience segment | What hurts them today | What hooks them in the Protocol | Primary articles | +|---|---|---|---| +| **Staff / Principal Engineers & Software Architects** | Architecture diagrams and ADRs rot the week after a design review; no way to verify whether code still honors the design or which decisions shape a module. | One primitive with named coordinates; zero-import `@sdp*` comment-form anchors (`satisfies` vs. `references`, `role`, `layer`, `context`); deterministic conformance + honesty checks in CI. | Articles 1, 2, 5, 7 | +| **AI / Coding-Agent Builders & Power Users** | `AGENTS.md` files bloat into thousands of lines of prose; MCP tools and CLI "verb walls" force agents to make 20+ calls or grep raw files and guess. | "Agents script the graph, not a verb wall": `sdp q` + `createReader`, 43 runnable recipes, token-budgeted context bundles, and structured change proposals. | Articles 1, 3, 7, 8 | +| **Engineering Leaders & Technical Founders** | Process theatre—mandatory sprint gates, Jira status choreography, and "100% spec coverage" quotas that produce AI-generated boilerplate nobody reads. | "Adopt the nouns, reject the gates" (MD-2); readiness floors instead of quotas; `ready ∧ ¬implemented` (build backlog) and `implemented ∧ ¬ready` (drift alarm) as live graph queries. | Articles 1, 4, 7 | +| **BDD & Testing Practitioners** | Cucumber `.feature` files drift from step regexes, duplicate domain logic, and turn non-behavioral NFRs or domain models into awkward Given/When/Then prose. | Carrier-independent executable specs: typed `gwt-vocabulary` example spaces, generated step and space contracts, committed registrars, and `specOracle` expected-outcome functions. | Articles 2, 4, 6 | + +### Channel & format rollout +- **Long-form canonical posts** — Publish from the Markdown manuscripts in `docs/articles/` to the project blog, Substack, or dev.to, with commit-pinned links to the public repository (`libar-dev/software-delivery-protocol`) and `try-it` blocks readers can run in 60 seconds. +- **Hacker News / Lobsters launches** — Lead with Articles 1, 3, and 5. Title for technical substance and contrarian clarity (e.g., why we deleted a 24-tag annotation registry, why we built one JS evaluation sink instead of 30 CLI subcommands, why we ran a 4-way carrier competition to choose Markdown). +- **Visual & interactive companions** — Pair Article 3 and Article 7 with the static HTML **Spec Studio** screenshots from [`reviews/assets/pr33`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33) and live static-host previews generated by `sdp studio`. +- **Podcast / Audio briefings (NotebookLM)** — Feed the 24 core sources plus Articles 1–3 into NotebookLM to produce an audio overview for commuters and architecture-review prep. + +--- + +## 3. The 8-Part Article Series Plan + +| # | Title & file | Status | Core angle | Key repo sources & PRs | +|---|---|---|---|---| +| **01** | **[The Spec Is the Prompt Is the Design: Why AI-Native Software Delivery Needs One Graph, Not Another Ticket Tracker](./01-the-spec-is-the-prompt-is-the-design.md)** | **Ready in this PR** | The launch manifesto: why delivery truth fractures across tools, why AI agents hallucinate structure without a graph, and how one primitive (`Spec`), three claims (`declared` / `anchored` / `inferred`), and two check families (conformance + honesty) replace workflow gates. | [`docs/concept/00`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md), [`01`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md), [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md), [`spec:model.core-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/core-model.sdp.md), [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1) | +| **02** | **[From Rough Idea to Verified Code: A Hands-On Tour of the Libar Software Delivery Protocol](./02-walkthrough-intent-to-verified-code.md)** | **Ready in this PR** | The step-by-step engineer's tour through `pack:checkout-v1` and the self-hosted engine: authoring `.sdp.md` and `.sdp.gherkin` Specs, clearing readiness floors, binding code with constant-form and `@sdp*` comment-form anchors, generating step/space contracts, binding a `specOracle`, and tripping the honesty checks on purpose. | [`examples/checkout-v1/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md), [`docs/concept/04`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/04-authoring-and-binding.md), [`sdp-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md), [PR #3](https://github.com/libar-dev/software-delivery-protocol/pull/3), [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29) | +| **03** | **[Stop Building 30-Verb CLI Walls for Coding Agents: Scripting the Delivery Graph with `sdp q` and Spec Studio](./03-stop-building-verb-walls-for-coding-agents.md)** | **Ready in this PR** | How coding agents and human reviewers consume the same derived graph: the demand map (`findByConcept`, `byFile`, `blastRadius`), the `sdp q` evaluation sink, growing by recipe instead of CLI verbs, scaling to a 240-Spec adopter with context bundles and corpus rules, and browsing the static HTML Spec Studio. | [`docs/concept/06`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/06-consumers-and-projections.md), [`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md), [`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | +| **04** | **Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition** | Planned | Why settling words (`claim` vs. provenance, `altitude` vs. abstraction, `readiness floor` vs. profile) settled the architecture, and how a falsifiable four-entrant competition (Markdown vs. custom grammar vs. Gherkin fork vs. TSX) ruled `.sdp.md` as default while preserving per-ID universality. | [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md), [`DECISIONS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/DECISIONS.md), [`spec:decisions.carrier-ruling`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/carrier-ruling.sdp.md), [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1), [PR #8](https://github.com/libar-dev/software-delivery-protocol/pull/8), [PR #19](https://github.com/libar-dev/software-delivery-protocol/pull/19) | +| **05** | **What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Annotation Reversal, and Honest Code Bindings** | Planned | A candid post-mortem on Gen 1 (`@libar-dev/architect`), how an evidence-based scout report overturned our own early Gen 2 refusal of doc-comment tags, and why distinguishing `satisfies` (collective realization) from `references` ("answers to") solved the stub-coverage lie. | [`docs/lineage/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage/README.md), [`docs/agent-surface/architecture.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/architecture.md), [`spec:decisions.anchor-comment-form`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md), [`spec:decisions.anchor-binding-grain`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md), [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29) | +| **06** | **Executable Specifications Without Cucumber Glue: Typed Example Spaces, Generated Contracts, and Expected-Outcome Oracles** | Planned | Why traditional BDD rots at the regex boundary, and how the Protocol compiles `gwt-vocabulary` example spaces and bound points into TypeScript union contracts, deterministic registrars, and `specOracle` functions where `unspecified` is a first-class answer. | [`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md), [`spec:validation.oracle-target-eligibility`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/oracle-target-eligibility.sdp.md), [PR #16](https://github.com/libar-dev/software-delivery-protocol/pull/16), [PR #19](https://github.com/libar-dev/software-delivery-protocol/pull/19) | +| **07** | **Designing a 240-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale** | Planned | How the first external adopter (`libar-platform`) pressured the Protocol when designing six capability Packs months ahead of code: entry addresses (`#design.`, `#question.`, `#rule.`), pinned declarations, 100k-unit context bundles, planned components, and `sdp validate --rules`. | [`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md), [`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md), [`spec:model.planned-architecture`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/planned-architecture.sdp.md), [PR #26](https://github.com/libar-dev/software-delivery-protocol/pull/26), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | +| **08** | **Auditing 276 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh** | Planned | How a solo developer + multi-model agent teams audited the entire self-hosted corpus against the engine: splitting into 13 units, running Fidelity vs. Quality lenses across model families, cross-verifying 595 findings (refuting 59), executing 18 parallel worktree lanes, and pinning the self-hosting oracle. | [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md), [`specs/self-hosting.pack.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/self-hosting.pack.sdp.md), [`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md), [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32) | + +### Detailed outlines for Planned Articles (04–08) + +#### Article 04: *Shared Language Is a Performance Feature* +- **Hook:** Why most "AI prompt engineering" is compensating for an unratified domain vocabulary—and how ambiguous words like *status*, *abstraction*, and *provenance* silently corrupt both human design reviews and subagent reasoning. +- **Section 1 — Define first, name last:** The governing rubric from [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md): adopt established industry nouns, reject process state-machine gates (the adopt-the-nouns decision, MD-2), and require every coined word to name a genuine epistemic difference. +- **Section 2 — Seven renames that changed the engine:** Walkthrough of the term ledger table (`claim` replacing provenance, `anchor` replacing marker, `altitude` replacing abstraction, `section` replacing facet, `readiness floor` replacing profile, `Pack` replacing SpecPack). +- **Section 3 — The four-way carrier competition:** How [PR #8](https://github.com/libar-dev/software-delivery-protocol/pull/8) judged F2 Markdown, C2 custom grammar, Gherkin fork, and TSX on falsifiable scorecards—and why C2's permanent ecosystem tax (parser, formatter, LSP, zero LLM training distribution) lost to `.sdp.md` + per-ID `.sdp.gherkin` and `.sdp.ts` options (the carrier ruling, MD-18, and the Gherkin carrier option, MD-27). + +#### Article 05: *What We Learned Rebuilding `@libar-dev/architect`* +- **Hook:** Our first-generation tool (`@libar-dev/architect`) worked in production across 349 patterns—and taught us every trap of source-first architecture graphs. +- **Section 1 — The sins we refused to carry over:** Sprawling tag registries, workflow lifecycle gates (`candidate → roadmap → active → completed`), and hand-authored delivery status. +- **Section 2 — When git archaeology corrects memory:** How the [annotation scout report](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage) inspected Gen 1's commit history and discovered that the "50 tags shrank to 26" story was formal-spec cleanup, while the real failures were duplicate identities, fixture scan leaks, and overloaded `implements` edges. +- **Section 3 — The annotation reversal ([PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29)):** How the first adopter (`libar-platform`) proved why runtime code that cannot import the Protocol package needed the `@sdp*` comment form (MD-36), optional plural bindings and the non-conferring `references` edge (MD-37), and `role`, `layer`, and `context` on anchors (MD-38). + +#### Article 06: *Executable Specifications Without Cucumber Glue* +- **Hook:** Everyone loves readable scenarios until a one-word phrasing change breaks 40 regex step definitions—or worse, passes silently while testing the wrong value. +- **Section 1 — Carrier-independent executability:** How `sdp build` turns `gwt-vocabulary` example spaces and bound points (`{n: 2}`, `{total: 100}`) into typed TypeScript step and space contracts under `generated/contracts/`. +- **Section 2 — Authored → generated, never the reverse:** How committed adopted registrars (MD-31) wire `createWorld`, `invoke`, `observe`, and `expected` without the generated code ever importing product code. +- **Section 3 — The `specOracle` and `unspecified`:** Why expected-outcome semantics belong in a typed oracle beside the tests—and why returning `unspecified` for unmodeled regions surfaces honest coverage gaps without false gates. + +#### Article 07: *Designing a 240-Spec Platform Ahead of Its Code* +- **Hook:** What happens when an adopter models a six-layer transactional platform in 243 Specs before writing layers 3 through 6? Every toy assumption in your spec tool breaks—and forces the real design-at-scale loop. +- **Section 1 — Fine-grained addressing without sub-Spec explosion:** Entry addresses (`spec:#design.`, `#ui.`, `#question.`, `#rule.`), pinned declarations (recipe 24), and the key-family register (recipe 41). +- **Section 2 — Fitting a capability Pack into an LLM's context window:** How recipe 36 (the context bundle) uses a deterministic seven-step cut order and `declarations: "addresses"` to fit `pack:history-view` into 93,806 of 100,000 UTF-16 code units ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). +- **Section 3 — Corpus rules and planned architecture:** Running project-specific policies and the architecture walk (recipe 42) via `sdp validate --rules`, and comparing component design Specs against realized `component:` anchors with recipe 43. + +#### Article 08: *Auditing 276 Self-Hosted Specs with Cross-Family AI Reviewers* +- **Hook:** When your repository self-hosts 276 Specs, 339 anchors, and 43 recipes, how do you verify that the Specs, the engine, and the docs actually agree? +- **Section 1 — Two lenses, two model families:** The methodology behind [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md) ([PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32)): pairing a Fidelity lens (code + tests vs. Spec rules) with a Quality lens (cross-Spec consistency, vocabulary, readiness honesty), then cross-verifying every finding with the opposite model family. +- **Section 2 — What the 595 findings caught (and the 59 they got wrong):** Real bugs caught—from Gherkin reserved tag heads (`@status.ready`) passing as decoration to a collective-vs-per-unit `satisfies` ambiguity—plus why cross-verification was essential to refute false blockers. +- **Section 3 — The distillation boundary and living hygiene:** Using slimming candidates (recipe 39) and the file-level curation report (recipe 40) to keep Specs lean after implementation. + +--- + +## 4. Editorial House Rules & Guardrails + +Every article in `docs/articles/` must conform to the repository's ratified language and automated checks: + +1. **Ratified terminology only ([`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md)).** + - Use **`Spec`**, **`Pack`**, **`section`**, **`anchor`** (constant form / comment form), **`claim`** (`declared` / `anchored` / `inferred`), **`altitude`** (`epic` / `feature` / `story`), **`readiness`** (`idea` / `scoped` / `defined` / `ready`), **`readiness floor`**, **`delivery fact`** (`implemented` / `has-verifier` / `observed`). + - Readiness is **stated** or **asserted**, never "claimed" ("claim" is reserved for the `claim` taxonomy). + - The Protocol **defines a contract** and instances **conform**; never write that the Protocol "governs" or "polices" a project ("checks police conformance and honesty" is the sole guardrail phrasing). + - State **"deterministically validated," never "provably correct."** + - Lead decision references with meaning first, code in parentheses: e.g., *the carrier ruling (MD-18)*, *the agent front door (MD-22)*, *the comment form (MD-36)*. +2. **Temporal-guard compliance (`npm run check:temporal`).** + - `check-temporal.mjs` sweeps every tracked file outside the historical genres (`plans/`, `reviews/`, `explorations/`, `docs/lineage/`, `docs/concept/DECISIONS.md`). + - Never include raw calendar dates (`YYYY-MM-DD`), numbered plan paths (`plans/`), or session/wave/fold tokens in `docs/articles/*.md`. When linking to a numbered plan or dated lineage file inside `docs/articles/*.md`, either link to its pull request (`.../pull/33`), its parent directory (`.../tree/5a0b22c.../plans`), or percent-encode the leading digit in the URL path (`.../plans/%345-design-at-the-adopters-scale.md`). +3. **Every corpus count is derived, dated to a commit, and paired with its command.** + - Never quote a number without naming the commit it was measured at and giving the reader the `sdp validate` or `sdp q` command to re-derive it. + +--- + +## 5. The Living-PR Refresh Playbook + +Because this pull request stays open and is periodically updated as `main` evolves, refreshing the series after merging or rebasing onto a new `main` commit takes four deterministic steps: + +### Step 1: Build and run the green gate +```sh +npm ci && npm run build +``` + +### Step 2: Measure the design diff from the last pinned commit +Generate the base graph at the previously pinned commit (currently `5a0b22c2d9ead98c900b60ed6209342fce2cbef6`) in a temporary worktree and run recipe 35 (the design diff) to see every Spec, Pack, section, entry key, and binding that changed: + +```sh +BASE_SHA="5a0b22c2d9ead98c900b60ed6209342fce2cbef6" +git worktree add --detach .sdp-base "$BASE_SHA" && ln -s "$PWD/node_modules" .sdp-base/node_modules +npm --prefix .sdp-base run build +node .sdp-base/dist/cli/sdp.js build .sdp-base --exclude explorations --exclude examples --exclude test/fixtures/import/parity +pnpm --silent sdp:q "$(cat dist/recipes/35-the-design-diff.js)" --params base=@.sdp-base/generated/graph.json --params '{"detail":true}' --json +git worktree remove --force .sdp-base +``` + +### Step 3: Re-derive the headline corpus metrics at the new `HEAD` +```sh +# 1. Overall graph, readiness, kind, and delivery-fact census +pnpm --silent sdp:q 'return { + commit: "HEAD", + schemaVersion: graph.schemaVersion, + specs: g.specs().length, + packs: g.packs().length, + nodes: graph.nodes.length, + edges: graph.edges.length, + locations: graph.locations.length, + readiness: g.specs().reduce((a, s) => ((a[s.statedReadiness] = (a[s.statedReadiness] || 0) + 1), a), {}), + kinds: g.specs().reduce((a, s) => ((a[s.specKind] = (a[s.specKind] || 0) + 1), a), {}), + deliveryFacts: { + implemented: g.specs().filter((s) => s.deliveryFacts.includes("implemented")).length, + hasVerifier: g.specs().filter((s) => s.deliveryFacts.includes("has-verifier")).length, + observed: g.specs().filter((s) => s.deliveryFacts.includes("observed")).length + } +}' --json + +# 2. Operational build backlog (recipe 1) and drift alarm (recipe 2) +pnpm --silent sdp:q "$(cat dist/recipes/01-the-build-backlog.js)" --json +pnpm --silent sdp:q "$(cat dist/recipes/02-the-drift-alarm.js)" --json + +# 3. Pack design summaries across all Packs (recipe 29) +pnpm --silent sdp:q ' + const out = []; + for (const p of g.packs()) { + const r = await (async (params) => { '"$(cat dist/recipes/29-pack-design.js)"' })({ pack: p.id }); + out.push({ id: r.id, totals: r.totals }); + } + return out; +' --json +``` + +### Step 4: Baseline snapshot at `5a0b22c` (for comparison on next refresh) +Measured at `5a0b22c2d9ead98c900b60ed6209342fce2cbef6` (re-run the commands above rather than inheriting): + +| Metric | Value at `5a0b22c` | Source query | +|---|---|---| +| Schema version | `0.10.0` | `graph.schemaVersion` | +| Authored `Spec`s | `276` (`245 ready`, `17 defined`, `3 scoped`, `11 idea`) | `g.specs()` | +| Authored `Pack`s | `6` (`self-hosting-v1`, `design-management-v1`, `spec-studio-v1`, `corpus-checks-v1`, `design-iteration-v1`, `obsidian-vault-v1`) | `g.packs()` | +| Total nodes / edges / locations | `621` nodes (`276 Primitive`, `6 Pack`, `160 CodeNode`, `179 Anchor`) · `1,624` edges (`1,063 declared`, `561 anchored`) · `1,774` locations | `graph.nodes`, `graph.edges`, `graph.locations` | +| Delivery facts | `94 implemented` · `205 has-verifier` · `0 observed` | `s.deliveryFacts` | +| Operational build backlog | `0` (`110` ready examples and `41` ready decisions audited and excluded) | Recipe 1 (`01-the-build-backlog.js`) | +| Drift alarm (`implemented ∧ ¬ready`) | `0` | Recipe 2 (`02-the-drift-alarm.js`) | +| Design backlog (`defined ∧ ¬implemented`) | `17` Specs | Recipe 33 (`33-the-design-backlog.js`) | +| Open questions | `43` across `24` Specs (`16` blocking: `8` deferrals with triggers, `8` holds; `27` non-blocking) | Recipe 20 (`20-open-question-register.js`) | +| Shipped recipes | `43` runnable bodies under `dist/recipes/` | `docs/agent-surface/recipes.md` | + +After updating any changed counts or commit links, stage the files and run `npm run check` to verify that `check:temporal`, `lint`, `format:check`, `test`, and `preflight` all exit `0`. From 58a82023ec9c66784f09ccbb5be941a0beda39a0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 00:49:31 +0000 Subject: [PATCH 02/36] docs(articles): fold adversarial review findings across series and playbook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Darko Mijić --- ...01-the-spec-is-the-prompt-is-the-design.md | 257 ++++++++++++++---- .../02-walkthrough-intent-to-verified-code.md | 164 ++++++++--- ...p-building-verb-walls-for-coding-agents.md | 169 ++++++++---- docs/articles/README.md | 114 ++++++-- 4 files changed, 521 insertions(+), 183 deletions(-) diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md index 198d42236..b93a64572 100644 --- a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -1,6 +1,6 @@ # The Spec Is the Prompt Is the Design: Why AI-Native Software Delivery Needs One Graph, Not Another Ticket Tracker -> *Series: Introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (Part 1 of 8) · All links and measurements pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)).* +*Part 1 of 8 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* Every software team knows the decay curve of documentation. @@ -16,11 +16,13 @@ Its premise fits in one sentence: > **Specs are code; the graph is derived; the `claim` stays honest; git is the event log.** +This article takes those four clauses in turn, then measures the Protocol against its own corpus. + --- ## 1. A Second Take, Front-Loaded with First Principles -The Protocol is not our first attempt at this problem. Its predecessor, [`@libar-dev/architect`](https://github.com/libar-dev/architect), ran in production across 349 architectural patterns. It proved three things that worked: keeping annotations in source beside the code, deriving a single immutable graph as the read model for both documentation and agents, and giving agents a scriptable graph handle instead of raw files. +The Protocol is not our first attempt at this problem. Its predecessor, [`@libar-dev/architect`](https://github.com/libar-dev/architect), ran in production; its own self-hosted graph held 349 patterns (227 from TypeScript, 122 from Gherkin), as the [annotation scout report in `docs/lineage/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage) measured. It proved three things that worked: keeping annotations in source beside the code, deriving a single immutable graph as the read model for both documentation and agents, and giving agents a scriptable graph handle instead of raw files. It also taught us what fails in production: @@ -34,7 +36,7 @@ We built the Libar Software Delivery Protocol ([PR #1](https://github.com/libar- |---|---|---| | **1. The Protocol** | The `Spec` primitive, descriptors, relations, and validators *as typed code* | Defines the conformance contract | | **2. The Authored Model** | A project's `Spec`s, `Pack`s, and source `anchor`s committed in the repo | Conforms to the contract (checked in CI, never workflow-gated) | -| **3. Derived Facts** | Machine truth (`implemented`, `has-verifier`, `observed`) and projections | Computed from the graph, **never authored by hand** | +| **3. Derived Facts** | Machine truth (delivery facts such as `implemented` and `has-verifier`) and projections | Computed from the graph, **never authored by hand** | And we bound the entire system with **two permanent honesty guardrails** ([`00-vision-scope-and-mvp-boundary.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md)): @@ -49,7 +51,7 @@ In traditional delivery tooling, an idea changes artifact types as it matures. I In the Protocol, there is **one authored truth-primitive: `Spec`** ([one primitive, named coordinates, MD-4](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/one-primitive.sdp.md); [`spec:model.core-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/core-model.sdp.md)). -A `Spec` keeps its stable, namespaced identity (`spec:orders.create-order`) from its first two-line capture to production realization. Instead of migrating it to a new artifact type, you **enrich it in place** by adding typed sections (`Intent`, `Behavior`, `Rule`, `Workflow`, `Contract`, `Example space`, `Constraints`, `Model`, `Design`, `Decision`, `UI`, `Verification`) or by refining it into child `Spec`s. +A `Spec` keeps its stable, namespaced identity (`spec:orders.create-order`) from its first two-line capture to production realization. Instead of migrating it to a new artifact type, you **enrich it in place**, by adding detail to its typed sections or by refining it into child `Spec`s. There are **eight typed sections**, one concern each: `intent`, `behavior`, `constraints`, `model`, `design`, `decision`, `verification`, and `ui` ([`spec:model.spec-sections`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/spec-sections.sdp.md)). In the Markdown carrier you will also see `## Rule`, `## Workflow`, `## Contract`, and `## Example space` headings; those are carrier forms of the one `behavior` section, and they reify into it ([`spec:validation.kind-evidence`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/kind-evidence.sdp.md), [`spec:carrier.markdown-body-grammar`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/markdown-body-grammar.sdp.md)). Every `Spec` is positioned by **three independent descriptors**: @@ -60,43 +62,132 @@ Every `Spec` is positioned by **three independent descriptors**: - **`readiness`** *(author-stated design maturity, checked against a structural floor)*: `idea → scoped → defined → ready`. -Familiar delivery nouns—Use Case, NFR, ADR, Epic, Story, Executable Scenario—are **named coordinates on this single primitive**, not separate schemas or tools. Because `altitude` and `readiness` move independently, an `epic`-altitude `behavior` Spec can honestly sit at `scoped` while one of its `story`-altitude `example` children is already `ready` and verified. +Familiar delivery nouns—Use Case, NFR, Decision Record, Contract, Epic, Feature, Story, Scenario—are **named coordinates on this single primitive**, not separate schemas or tools. An **Executable Spec** is likewise not a type or a rung: it is an `example` Spec that carries the `has-verifier` delivery fact. + +Because a child's readiness statement is its own, parent and child can stand on different rungs. In the worked example the next article tours, the `epic`-altitude `behavior` Spec `spec:orders.order-management` states `defined`, as does the `feature` beneath it, while the `story`-altitude `example` `spec:orders.create-order.valid-cart` already states `ready` and has a bound verifier. The epic could honestly sit at `scoped`, because the example refines the `defined` feature, not the epic. There is one coupling: the `ready` floor requires every `refines`, `dependsOn`, `constrainedBy`, and `decidedBy` target to state at least `defined` (`spec:validation.readiness-floor#rule.readyFloor`, [`spec:validation.typed-dependency-floor`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/typed-dependency-floor.sdp.md)). So a `ready` Spec never refines a `scoped` parent directly. Alongside `Spec`, two other authored constructs enter the repository, and **neither states system truth of its own**: - **`Pack`** (`pack:checkout-v1`, [`spec:model.pack-aggregate`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/pack-aggregate.sdp.md)): a review and reading grouping over `Spec`s. A `Spec` can belong to multiple `Pack`s. - **`anchor`** ([`spec:model.anchors`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/anchors.sdp.md)): a pointer in source code or tests that binds a code unit (`impl:`, `api:`, `component:`), a test (`test:`), or an expected-outcome oracle (`oracle:`) to `Spec` IDs. An anchor carries identity, bindings, and optional architectural structure—never behavior, rationale, or readiness. -Here is an entire `idea`-rung `Spec` in its default Markdown carrier (`.sdp.md`), scaffolded by `sdp new spec`: +### A Spec at its first rung + +Suppose the team behind the worked example (`examples/checkout-v1`, Order Management) starts thinking about refunds. One command scaffolds an honest `idea`-rung Spec in the default Markdown carrier (`.sdp.md`): + +```sh +npm run --silent sdp -- new spec examples/checkout-v1/specs/orders/refund-order-api-contract.sdp.md \ + --id spec:orders.refund-order.api-contract --kind contract --altitude story \ + --title "HTTP contract for refunding an order" \ + --outcome "Define the HTTP request and response payload for POST /orders/{id}/refunds." +``` + +`sdp new spec` writes the envelope with `readiness: idea` and `relations: {}`, the `# Title`, an `## Intent` section holding the `- outcome:` line, and the kind's typed heading, here a bare `## Contract`, with no invented content. The author then edits the file in place: one `refines` relation to the Order Management epic, and one keyed blocking question with a decider and a provisional answer: ```markdown --- -id: spec:orders.create-order.api-contract +id: spec:orders.refund-order.api-contract kind: contract altitude: story readiness: idea relations: - refines: spec:orders.create-order + refines: spec:orders.order-management --- - -# HTTP contract for order creation +# HTTP contract for refunding an order ## Intent -- outcome: Define the HTTP request and response payload for POST /orders. +- outcome: Define the HTTP request and response payload for POST /orders/{id}/refunds. ### Open questions -- [blocking #idempotencyHeader] Should the endpoint require an Idempotency-Key header on every POST? +- [blocking #idempotencyHeader] Should the endpoint require an Idempotency-Key header on every refund request? - decider: owner - - provisional: Require Idempotency-Key on all order-creation requests. + - provisional: Require an Idempotency-Key header on every refund request. + +## Contract +``` + +Dropped into `examples/checkout-v1/specs/orders/`, that file validates cleanly: `npm run --silent sdp -- validate examples/checkout-v1` reports `12 specs · 1 packs · 5 anchors → 18 nodes · 33 edges` and `validate: 0 errors · 0 warnings`. (The title must follow the closing `---` directly; a blank line between them is refused with `extract/invalid-markdown-structure — the first body line must be an H1 title`.) + +No boilerplate. No mandatory twelve-part template to fill out just to capture a thought. Yet it is already a typed node in the graph, joined to `spec:orders.order-management`. Its question has an **entry address**, an address for one keyed entry inside a Spec: `spec:orders.refund-order.api-contract#question.idempotencyHeader`. Another Spec's prose can cite that address, and `sdp validate` reports one that does not resolve; a register, test, or page outside the Specs checks its addresses with address resolution (recipe 25). The floor tells the author what comes next: the empty `## Contract` holds the Spec at `idea` until the contract has its first entry, and once it does, the blocking question holds it below `defined` until someone settles it. + +### Why the Spec is the prompt + +This is the sense of the title. Because one `Spec` is enriched in place from rough idea to implementable contract, and never migrates across tools, everything an implementer needs about it sits behind one ID in one graph. Feeding an agent that Spec's graph context *is* the implementation prompt: + +```sh +npm run --silent sdp:q -- 'return g.specContext("spec:consumers.reader")' --json ``` -No boilerplate. No mandatory 12-section template to fill out just to capture a thought. Yet it is already a typed node in the graph, joined to `spec:orders.create-order`, carrying a keyed blocking question (`spec:orders.create-order.api-contract#question.idempotencyHeader`) that the validator knows will hold this Spec below `defined` until settled. +`g.specContext` returns the Spec's typed sections, its stated and derived readiness with any floor failures, its relations in both directions with their claims, the code units that realize it or answer to it (with file and line), its verifiers and oracle, and the source line of each keyed entry. There is no second brief to write and keep in sync: "implement `spec:…`" is the whole instruction, and the real work moves to maturing and reviewing the Spec. + +That only works if Specs stay lean. Principle P4 of the [founding principles](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md) carries the corollary: **significance governs detail, not a tier.** A Spec carries detail only where the work is architecturally significant or genuinely novel; the Nth CRUD endpoint earns a reference, not a re-derivation. Its motto is *"Enrich what matters; reference what is standard; pad nothing."* That is why readiness is a **floor to clear**, never a **quota to fill**: a fixed template pressures every author, and AI authors most of all, to fill sections the work never needed, and padding is noise the next prompt has to read. --- -## 3. The Epistemic Boundary: Why `claim` Separation Changes Everything +## 3. The Graph Is Derived, and Git Is the Event Log + +### `graph = f(repo)` + +Nobody edits the graph. `sdp build` reads every carrier and source anchor and derives one flat document: an array of nodes, an array of edges, and a location table of source lines, with no nesting and nothing hand-maintained ([`spec:extraction.derive-graph`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/derive-graph.sdp.md)). Here is one real `Primitive` node from the Protocol's own graph, complete. Fittingly, it is the Spec that states the regenerability law: + +```json +{ + "id": "spec:extraction.regenerability", + "nodeType": "Primitive", + "claim": "declared", + "specKind": "rule", + "altitude": "feature", + "readiness": "ready", + "title": "Generated artifacts are disposable projections", + "file": "specs/extraction/regenerability.sdp.md", + "sections": { + "intent": { + "outcome": "Keep the repository canonical while allowing every graph and projection to be rebuilt safely." + }, + "behavior": { + "rules": [ + "Generated artifacts are disposable: deleting them and rebuilding from the same committed repository produces the same bytes.", + "Consumers read the graph or link to source locations recorded in it; they never re-parse source or keep a parallel model.", + "The graph is one JSON document queried in memory." + ] + } + }, + "deliveryFacts": ["implemented", "has-verifier"] +} +``` + +And these are the five edges that touch it. Hierarchy, membership, and bindings are all just edges, and every edge carries its `claim`: + +```json +[ + { "from": "spec:decisions.studio-html-surface", "type": "dependsOn", "to": "spec:extraction.regenerability", "claim": "declared", + "reason": "disposable HTML can be regenerated from the graph instead of committed as authored source" }, + { "from": "spec:extraction.regenerability", "type": "refines", "to": "spec:extraction.determinism", "claim": "declared" }, + { "from": "spec:extraction.regenerability", "type": "belongsTo", "to": "pack:self-hosting-v1", "claim": "declared" }, + { "from": "impl:protocol.regenerability", "type": "satisfies", "to": "spec:extraction.regenerability", "claim": "anchored" }, + { "from": "test:protocol.regenerability", "type": "verifies", "to": "spec:extraction.regenerability", "claim": "anchored" } +] +``` + +The `refines` and `dependsOn` edges are authored relations (`declared`). The `belongsTo` edge is computed from the Pack manifest and inherits the manifest's `declared` claim. The `satisfies` and `verifies` edges come from a code anchor and a test anchor (`anchored`), and they are why the node's `deliveryFacts` read `implemented` and `has-verifier`. Reproduce the excerpt with: + +```sh +npm run --silent sdp:q -- 'const id = "spec:extraction.regenerability"; return { node: graph.nodes.find((n) => n.id === id), edges: graph.edges.filter((e) => e.from === id || e.to === id) }' --json +``` + +Derivation is deterministic: stable ordering, no timestamps, `graph = f(repo)` and `view = f(graph)`. The regenerability invariant follows: **delete `generated/` and rebuild, and you get the same bytes** ([`spec:extraction.regenerability`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/regenerability.sdp.md)). Every validator, human view, and agent query reads this one graph, so none of them can disagree with another about what the repository says. + +### Git is the event log + +The graph holds only the current state of the repository at one commit. It keeps no deleted or superseded "ghost" nodes, no change history, and no audit tables. When a Spec is removed, it is gone from the graph; to read what it used to say, check out an earlier commit and regenerate. "Who changed what, and when" is `git log` and `git blame`, never graph state. + +There is one narrow exception, and it holds because it is current intent rather than history. A decision record may declare `supersedes` another decision record while both are present and the older one still explains something a reader needs. Once it no longer does, the older record is deleted and git holds its history. The Protocol's own registry shows this: the decision that kept source comments out of the graph (MD-35) was replaced by the comment form (MD-36) and then deleted; [`DECISIONS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/DECISIONS.md) keeps a `deleted` row naming the commit that holds its last text. + +--- + +## 4. The Epistemic Boundary: Why `claim` Separation Changes Everything Why don't coding agents trust architecture tools? Because most tools mix together three completely different kinds of statements: @@ -106,57 +197,74 @@ Why don't coding agents trust architecture tools? Because most tools mix togethe Once those three collapse into a single "dependency" edge, a single bad heuristic poisons the graph. -The Protocol enforces a strict epistemic boundary ([`01-founding-principles-and-invariants.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md), P9 & P10; [`spec:extraction.claim-taxonomy`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/claim-taxonomy.sdp.md)). Every node and every edge in the graph carries a **`claim`** field with one of three values, and **they are never collapsed**: +The graph keeps a strict epistemic boundary (a working name; see Principles P9 and P10 in [`01-founding-principles-and-invariants.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md), and [`spec:extraction.claim-taxonomy`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/claim-taxonomy.sdp.md)). A **`claim`** records a fact's epistemic status, and its three values are **never collapsed**. Every node and edge in the curated graph carries `declared` or `anchored`. The third value, `inferred`, names mechanical structure: the file-level imports that `sdp build --imports` writes beside the graph. That input is kept apart from the curated graph, feeds curation advice, and never derives a curated edge, a binding, or a delivery fact. | `claim` value | Who asserts it | What it means | Authority | |---|---|---|---| -| **`declared`** | Human author in a `Spec` or `Pack` carrier | Authoritative delivery intent: relations (`refines`, `dependsOn`, `constrainedBy`, `decidedBy`, `verifies`, `supersedes`), stated readiness, and section content | **Authoritative intent** | -| **`anchored`** | Human author via an in-code `anchor` | Authoritative binding from source code or tests into the graph (`satisfies`, `references`, `verifies`, `models`, `memberOf`, `uses`) | **Authoritative binding** (carries no intent) | -| **`inferred`** | Static analyzer (e.g., `sdp build --imports`) | Mechanical code structure (file-level imports used for curation advice) | **Advisory only** — never confers delivery facts, never triggers validator errors, never silently promoted | +| **`declared`** | Human author in a `Spec` or `Pack` carrier | Delivery intent: relations (`refines`, `dependsOn`, `constrainedBy`, `decidedBy`, `verifies`, `supersedes`), stated readiness, section content, and Pack membership (`belongsTo`, computed from the manifest) | **Authoritative intent** | +| **`anchored`** | Human author via an in-code `anchor` | A binding from source code or tests into the graph (`satisfies`, `references`, `verifies`, `models`, `memberOf`, `uses`) | **Authoritative binding** (carries no intent) | +| **`inferred`** | Static analyzer (the TypeScript imports adapter behind `sdp build --imports`) | Mechanical code structure (file-level imports), held outside the curated graph | **Advisory only** — feeds curation advice; never derives a curated edge, binding, or delivery fact; never triggers a validator error; never silently promoted | ### Stated Readiness vs. Derived Delivery Facts -Because `declared` intent and `anchored` bindings are kept separate, **design maturity** (`readiness`) and **code realization** (`deliveryFacts`) live on orthogonal axes ([`spec:extraction.delivery-facts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/delivery-facts.sdp.md)): +Because `declared` intent and `anchored` bindings are kept separate, **design maturity** (`readiness`) and **code realization** (`deliveryFacts`) are independent coordinates (readiness is separate from delivery facts, D1; [`spec:extraction.delivery-facts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/delivery-facts.sdp.md)): + +- **Author-stated `readiness` (`idea → scoped → defined → ready`):** What the author states about the maturity of the design, checked against the cumulative [**readiness floor**](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/readiness-floor.sdp.md). The graph also computes **`derivedReadiness`**, the highest rung whose cumulative floor clauses all pass, and renders it beside `statedReadiness`; it never overwrites the author's statement. You cannot state `defined` while a `[blocking]` open question remains open, and you cannot state `ready` while a `dependsOn`, `refines`, `constrainedBy`, or `decidedBy` target sits below `defined`. +- **Derived `deliveryFacts`:** Computed deterministically from resolving `anchored` edges—**never authored by hand**. `implemented` and `has-verifier` are derived today; `observed`, the runtime liveness fact, is designed for and not yet derived. + - `implemented` is conferred when at least one anchored `satisfies` edge from a code anchor resolves to that Spec. It never propagates through `refines` from parent to child or child to parent. + - `has-verifier` is conferred when a resolving `specTest` anchor verifies the Spec directly, or when a verifying `example` Spec is itself backed by a resolving `specTest` anchor (an **enabled verifier**). It says a verifier binding exists, not that the test passed. + +Every route to hand-authoring a delivery fact is refused, each with its own finding: + +- Writing `implemented: true` in a Spec's frontmatter is refused at extraction (`extract/reserved-property`). +- Writing a derived edge such as `satisfies` under `relations` is refused at extraction (`extract/invalid-frontmatter`). +- Smuggling a delivery fact into a section fails `honesty/authoring-shape`. +- A graph whose stated delivery facts disagree with its bindings, from any graph producer, fails `honesty/delivery-facts`. + +Look at the two payoff queries that fall out of keeping those coordinates separate. Each is a filter over the reader's `g.specs()`; the shipped recipes, runnable `sdp q` bodies cataloged in [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md) and each executed as written by the test suite, wrap these expressions with counts and audit rows. -- **Author-stated `readiness` (`idea → scoped → defined → ready`):** What the author states about the maturity of the design, checked against the cumulative [**readiness floor**](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/readiness-floor.sdp.md) (`derivedReadiness`). You cannot state `defined` while a `[blocking]` open question remains open; you cannot state `ready` while a `dependsOn`, `refines`, `constrainedBy`, or `decidedBy` target sits below `defined` ([`spec:validation.typed-dependency-floor`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/typed-dependency-floor.sdp.md)). -- **Derived `deliveryFacts` (`implemented`, `has-verifier`, `observed`):** Computed deterministically from resolving `anchored` edges—**never authored by hand**. Writing `implemented: true` in a Spec file is rejected as an honesty violation (`honesty/authored-delivery-fact`). - - `implemented` is conferred when at least one resolving code anchor declares `satisfies` to that Spec. It never propagates through `refines` from parent to child or child to parent. - - `has-verifier` is conferred when a resolving `specTest` anchor verifies the Spec directly, or when a verifying `example` Spec is itself backed by a resolving `specTest` anchor (an **enabled verifier**). +1. **The operational build backlog (recipe 1):** Specs whose design is settled, whose typed dependencies are at least `defined`, and whose author has stated `ready`, waiting to be built. Ready `example` and `decision` Specs are excluded and counted, because examples carry verification evidence and decision records are ratified rather than built (the example realization posture, MD-24, and the decision readiness posture, MD-26). -Look at the two payoff queries that fall out of keeping those axes separate: + ```js + g.specs().filter((s) => + s.statedReadiness === "ready" && + !s.deliveryFacts.includes("implemented") && + s.specKind !== "example" && + s.specKind !== "decision") + ``` -1. **The Operational Build Backlog ([Recipe 1](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md)):** - \[ - \text{statedReadiness} = \texttt{ready} \;\land\; \texttt{implemented} \notin \text{deliveryFacts} \;\land\; \text{specKind} \notin \{\texttt{example}, \texttt{decision}\} - \] - These are the Specs whose design is settled, whose dependencies are at least `defined`, and whose human author has stated `ready`—waiting to be built. -2. **The Drift Alarm ([Recipe 2](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md)):** - \[ - \texttt{implemented} \in \text{deliveryFacts} \;\land\; \text{statedReadiness} \neq \texttt{ready} - \] - This is code that ran ahead of its design: an implementation anchor claims to realize a Spec whose design has not been stated `ready`. +2. **The drift alarm (recipe 2):** Code that ran ahead of its design: a code anchor binds an implementation to a Spec whose author has not stated `ready`. + + ```js + g.specs().filter((s) => + s.deliveryFacts.includes("implemented") && + s.statedReadiness !== "ready") + ``` + +Neither query is a CI failure. They are standing questions anyone can ask the graph at any commit. --- -## 4. Conformance and Honesty Checks (Plus Corpus Rules) +## 5. Conformance and Honesty Checks (Plus Corpus Rules) When you run `sdp validate`, every carrier is extracted into the one graph and checked by two validator families ([`spec:validation.two-check-families`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/two-check-families.sdp.md)): - **Conformance checks** ask: *Is this model well-formed against the Protocol?* - - Do all relation targets and anchor targets resolve (`conformance/referential-integrity`, with edit-distance suggestions)? - - Are all IDs unique across `.sdp.md`, `.sdp.ts`, `.sdp.gherkin`, and source anchors (`conformance/duplicate-ids`)? - - Are `claim` values, descriptors, and structural anchor edges (`memberOf`, `uses`, `layer`, `context`) valid (`conformance/claim-separation`, `conformance/structural-anchors`)? - - Do `spec:` mentions and entry addresses (`spec:id#design.key`, `#ui.key`, `#question.key`, `#rule.key`) written in prose resolve and have a backing declared relation (`conformance/prose-mentions`)? + - Do all relation targets and anchor targets resolve (`conformance/referential-integrity`, with a nearest-ID suggestion)? + - Are all IDs unique across `.sdp.md`, `.sdp.ts`, `.sdp.gherkin`, and source anchors? The extractor reports each duplicate site (`extract/duplicate-id`) and keeps those carriers out of the graph; `conformance/duplicate-ids` is the backstop for any other graph producer. + - Do `claim` values, node types, edge types, and descriptors stay inside their closed sets (`conformance/claim-separation`)? + - Are the structural edges `memberOf` and `uses`, and the design-reference edge `references`, well-formed (`conformance/structural-anchors`)? `layer` and `context` are attributes of a `component:` anchor rather than edges; a `layer` outside its closed set, or either attribute on a non-component anchor, is refused at extraction. + - Do `spec:` mentions and entry addresses (`spec:id#design.key`, `#ui.key`, `#question.key`, `#rule.key`) written in prose resolve (`conformance/prose-mentions`)? A mention or entry address that does not resolve is an error. A resolving mention between two Specs where neither declares a relation to the other is an informative warning, reported once per pair. - **Honesty checks** ask: *Is this model pretending to be something it isn't?* - - Did someone try to hand-author a derived edge (`satisfies`, `belongsTo`) or a delivery fact (`implemented`, `has-verifier`) inside a Spec (`honesty/authoring-shape`, `honesty/authored-delivery-fact`)? + - Did someone hand-author a delivery fact or a derived edge? (The refusals listed in the previous section: `extract/reserved-property` and `extract/invalid-frontmatter` at extraction, `honesty/authoring-shape` and `honesty/delivery-facts` over the graph.) - Does every Spec clear the cumulative structural floor for the `readiness` rung its author stated (`honesty/readiness-floor`)? - - Are `ready` Specs missing an enabled verifier surfaced visibly as informative `honesty/gaps` warnings without blocking the build? + - Are `ready` Specs (excluding `decision` Specs, under the decision readiness posture, MD-26) that have no resolving verifier surfaced as informative `honesty/gaps` warnings, never blocking the build? -And when an adopting team has project-specific architectural rules—for instance, "domain components must never use edge components" or "every question must name a decider"—they run **corpus rules** on the same validation path via `sdp validate --rules` ([`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md), introduced in [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). Corpus findings are reported in their own section, never corrupt Protocol readiness floors, and fail CI when they report an error. +And when an adopting team has project-specific architectural rules—for instance, "domain components must never use edge components" or "every question must name a decider"—they run **corpus rules** on the same validation path via `sdp validate --rules` ([`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md), introduced in [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). Corpus findings are reported in their own section, never move a readiness floor or change a Protocol finding, and make `sdp validate` exit nonzero when they report an error. --- -## 5. Self-Hosting by the Numbers +## 6. Self-Hosting by the Numbers The Protocol does not have a separate PRD. It **self-hosts** ([`spec:protocol.self-hosting`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/protocol/self-hosting.sdp.md)): every rule of its model, extractor, validators, projections, and agent surface is authored in its own `.sdp.md` and `.sdp.gherkin` carriers under [`specs/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs), bound to its own TypeScript engine under [`src/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src) and test suite under [`test/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/test), and certified on every commit by `npm run check`. @@ -167,38 +275,75 @@ At commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tr | **Authored `Spec`s** | **276** | `110 example` · `71 behavior` · `41 decision` · `35 rule` · `12 model` · `5 contract` · `1 workflow` · `1 constraint` | | **Stated `readiness`** | **276** | `245 ready` · `17 defined` · `3 scoped` · `11 idea` | | **Authored `Pack`s** | **6** | `self-hosting-v1` (276) · `design-management-v1` (24) · `spec-studio-v1` (18) · `corpus-checks-v1` (13) · `design-iteration-v1` (11) · `obsidian-vault-v1` (5) | -| **Source & Test Bindings** | **339** | `160 CodeNode` (`impl:`, `api:`, `component:` across 14 components and 4 layers) · `179 Anchor` (`test:` and `oracle:`) | -| **Derived Graph Size** | **621 nodes · 1,624 edges** | `1,063 declared` edges · `561 anchored` edges · `1,774` entry & relation source locations (schema `0.10.0`) | -| **Derived Delivery Facts** | **94 `implemented` · 205 `has-verifier`** | Operational build backlog (recipe 1): **0** · Drift alarm (recipe 2): **0** · Design backlog (recipe 33): **17** | +| **Source & test bindings** | **339** | `160 CodeNode` (`146 impl:`, `14 component:` across 14 components and 4 layers; code anchors also admit `api:`) · `179 Anchor` (`172 test:`, `7 oracle:`) | +| **Derived graph size** | **621 nodes · 1,624 edges** | `1,063 declared` edges · `561 anchored` edges · `1,774` entry and relation source locations (schema `0.10.0`) | +| **Derived delivery facts** | **94 `implemented` · 205 `has-verifier`** | Operational build backlog (recipe 1): **0** · Drift alarm (recipe 2): **0** · Design backlog (recipe 33): **17** | + +Re-derive the breakdowns with: + +```sh +npm run --silent sdp:q -- 'const tally = (xs, f) => xs.reduce((a, x) => ((a[f(x)] = (a[f(x)] || 0) + 1), a), {}); return { kinds: tally(g.specs(), (s) => s.specKind), readiness: tally(g.specs(), (s) => s.statedReadiness), nodeTypes: tally(graph.nodes, (n) => n.nodeType), bindingNamespaces: tally(graph.nodes.filter((n) => n.nodeType === "CodeNode" || n.nodeType === "Anchor"), (n) => n.id.split(":")[0]), edgeClaims: tally(graph.edges, (e) => e.claim), locations: graph.locations.length }' --json +``` + +The self-hosting `validate` run prints `0 errors · 6 warnings`, and all six are intentional, pinned by the self-hosting oracle: five `conformance/prose-mentions` warnings, where one Spec names another (three times in an open question's `links:` field, twice in rule text) without a declared relation between them, and one `honesty/gaps` warning on `spec:model.spec-sections`, an owner-ratified `ready` Spec with no resolving verifier. -Meanwhile, the first external adopter, `libar-platform`, models a six-layer transactional domain platform as **243 Specs across six Packs**, using the Protocol to run token-budgeted design passes, enforce context isolation through `sdp validate --rules`, and publish a 251-page static HTML **Spec Studio** workbench ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). +Meanwhile, the first adopter, [`libar-platform`](https://github.com/libar-dev/libar-platform) (a separate repository in the `libar-dev` organization), models a Convex transactional domain platform as **243 Specs across six Packs**, most of it designed ahead of its code. It runs design passes whose reading is composed within a 100,000-UTF-16-unit context bundle, checks its own architecture and policy rules through `sdp validate --rules`, and publishes 251 static **Spec Studio** pages. The Protocol work that made that possible is [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); the adopter's side is [`libar-platform#13`](https://github.com/libar-dev/libar-platform/pull/13). -![The Spec Studio Pack page for pack:corpus-checks-v1](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/protocol-pack.png) +The Spec Studio renders the same graph for people. Here is the Protocol's own `pack:corpus-checks-v1`, the Pack that carries corpus rules: + +![The Spec Studio Pack page for pack:corpus-checks-v1: a plain-language summary, the Pack's framing, and one collapsed card per member](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/protocol-pack.png) + +*The Pack page for `pack:corpus-checks-v1`, published by `sdp studio` and opened from `file://` with no network. It leads with a summary the graph computes, then the Pack's framing, then one card per member with its rung, floor, questions, and bindings. Captured for [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33).* --- -## 6. Try It Yourself in 60 Seconds +## 7. Scope, Non-Goals, and How Adopters Install Today + +Some things the Protocol deliberately is not ([`00` §5, non-goals](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md)): + +- **Not a workflow state machine.** It adopts the delivery nouns (phase, iteration, milestone, release) as descriptive vocabulary and rejects the gates. "In sprint / in review / in QA" status stays in your issue tracker, linked through a separate index. +- **Not an embedded RBAC or governance engine.** Change approval lives in git and CI (pull request review, CODEOWNERS). A Spec names no owner, and an open question's `decider:` names who settles it, never an authorization. +- **Not an intent inferer.** Structure is extracted; intent is always authored. Inference assists and never asserts. +- **Not a replacement for adjacent tools.** It is the membrane that links issue trackers, design tools, LikeC4, and OpenAPI, not a substitute for them. +- **No separate PRD.** The jobs live in [`jtbd-stories/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/jtbd-stories) and the laws live in [`specs/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs). -Clone the repository and query the live self-hosting graph through `sdp q`—the evaluation sink that derives the graph in memory and runs any async JavaScript body you give it: +And where it stands today: + +- **Install from a reviewed commit.** The package version is `0.0.0` and no npm prerelease is published. An adopter pins `"@libar-dev/software-delivery-protocol": "github:libar-dev/software-delivery-protocol#"` to a commit it reviewed; the package's `prepare` script builds the CLI, subpath exports, and shipped recipes during the git install, and `sdp --version` prints the pinned commit. The [README](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/README.md) covers the npm and pnpm build approvals. +- **Designed in, deferred.** The `observed` delivery fact (runtime liveness) and the MCP surface for user-facing apps (the MCP-deferred decision, D6) are designed for and not built. +- **Verdicts stay in CI.** `has-verifier` means a verifier binding exists. Pass, fail, skip, and quarantine never enter the graph. + +--- + +## 8. Try It Yourself in 60 Seconds + +Clone the repository at the commit these numbers were measured at, and query the self-hosting graph through `sdp q`—the evaluation sink that derives the graph in memory and runs any async JavaScript body you give it: ```sh git clone https://github.com/libar-dev/software-delivery-protocol.git cd software-delivery-protocol -npm ci +git checkout 5a0b22c2d9ead98c900b60ed6209342fce2cbef6 +npm ci # the prepare script builds dist/ -# 1. Validate the 276-Spec self-hosting corpus +# 1. Validate the 276-Spec self-hosting corpus (0 errors · 6 warnings) npm run --silent sdp -- validate . --exclude explorations --exclude examples --exclude test/fixtures/import/parity -# 2. Run Recipe 1 (the operational build backlog) and Recipe 2 (the drift alarm) +# 2. Run recipe 1 (the operational build backlog) and recipe 2 (the drift alarm) npm run --silent sdp:q -- "$(cat dist/recipes/01-the-build-backlog.js)" --json npm run --silent sdp:q -- "$(cat dist/recipes/02-the-drift-alarm.js)" --json -# 3. Publish the static HTML Spec Studio and open generated/spec-studio/index.html +# 3. Publish the static HTML Spec Studio, then open generated/spec-studio/index.html npm run --silent sdp -- studio . --exclude explorations --exclude examples --exclude test/fixtures/import/parity ``` +The three `--exclude` flags keep the exploration exhibits, the separate worked example, and the carrier-parity fixtures (which deliberately duplicate IDs) out of the self-hosting root; the `sdp:q` script supplies them for you. + --- ## Up Next in the Series -In **[Part 2: From Rough Idea to Verified Code](./02-walkthrough-intent-to-verified-code.md)**, we open the hood on `examples/checkout-v1` and the Protocol's own source code to walk through the complete engineering loop: authoring `.sdp.md` and `.sdp.gherkin` carriers, binding code with constant-form and `@sdp*` comment-form anchors, generating typed step and space contracts, writing an expected-outcome `specOracle`, and deliberately breaking the graph to watch the conformance and honesty checks fire. +In **Part 2: From Rough Idea to Verified Code** ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/ARTICLES_COMMIT/docs/articles/02-walkthrough-intent-to-verified-code.md)), we open `examples/checkout-v1` and the Protocol's own source code to walk the complete engineering loop: authoring `.sdp.md` carriers (and where `.sdp.gherkin` fits), binding code with constant-form and `@sdp*` comment-form anchors, generating typed step and space contracts, writing an expected-outcome `specOracle`, and deliberately breaking the graph to watch the extraction, conformance, honesty, and contract checks fire. + +--- + +*About this article: Part 1 of 8 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every measurement are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the numbers at a later commit.* diff --git a/docs/articles/02-walkthrough-intent-to-verified-code.md b/docs/articles/02-walkthrough-intent-to-verified-code.md index 1c5c328a0..51fd9fb88 100644 --- a/docs/articles/02-walkthrough-intent-to-verified-code.md +++ b/docs/articles/02-walkthrough-intent-to-verified-code.md @@ -1,8 +1,8 @@ # From Rough Idea to Verified Code: A Hands-On Tour of the Libar Software Delivery Protocol -> *Series: Introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (Part 2 of 8) · All links and measurements pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)).* +*Part 2 of 8 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* -In [Part 1](./01-the-spec-is-the-prompt-is-the-design.md), we introduced the core thesis of the **Libar Software Delivery Protocol**: model the delivery lifecycle as instances of one enrichable primitive (`Spec`), bind code and tests with source `anchor`s, derive **one regenerable graph**, and check **conformance and honesty** in CI. +In Part 1 ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/ARTICLES_COMMIT/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), we introduced the core thesis of the **Libar Software Delivery Protocol**: model the delivery lifecycle as instances of one enrichable primitive (`Spec`), bind code and tests with source `anchor`s, derive **one regenerable graph**, and check **conformance and honesty** in CI. That sounds clean in principle. What does it actually look like in a text editor and a terminal? @@ -10,41 +10,82 @@ In this post, we will trace a single bounded context—**Order Management** ([`e --- +## 0. Setup + +Every command in this tour runs from a checkout of the repository at the commit the outputs were measured at: + +```sh +git clone https://github.com/libar-dev/software-delivery-protocol.git +cd software-delivery-protocol +git checkout 5a0b22c2d9ead98c900b60ed6209342fce2cbef6 +npm ci # the prepare script builds dist/ +``` + +We run the CLI as `npm run --silent sdp -- …`, never as a bare `sdp`. Inside its own checkout the package is not linked into `node_modules/.bin`, so a bare `sdp` resolves to nothing, or, on macOS, to Apple's unrelated `/usr/bin/sdp`. (In an adopter repository that installs the package, `npx sdp` or `pnpm exec sdp` works.) `examples/checkout-v1` is its own extraction root, so the commands below pass it as the root and need none of the three fixture `--exclude` flags the repository root uses. + +--- + ## 1. The Worked Example: 11 Specs, 1 Pack, 5 Anchors -The repository ships a complete, self-contained bounded context under [`examples/checkout-v1`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md). It models Order Management (`pack:checkout-v1`) using all eight `Spec` kinds (`behavior`, `workflow`, `example`, `rule`, `constraint`, `model`, `decision`, `contract`) and all four readiness rungs (`idea`, `scoped`, `defined`, `ready`): +The repository ships a complete, self-contained bounded context under [`examples/checkout-v1`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md). It models Order Management (`pack:checkout-v1`) using all eight `Spec` kinds (`behavior`, `workflow`, `example`, `rule`, `constraint`, `model`, `decision`, `contract`) and all four readiness rungs (`idea`, `scoped`, `defined`, `ready`). Each carrier's `kind · altitude · stated readiness`: ```text examples/checkout-v1/ specs/ checkout.pack.sdp.md # Pack manifest (pack:checkout-v1) - orders/order-management.sdp.md # behavior · epic · scoped - orders/order-placement-flow.sdp.md # workflow · feature · scoped - orders/create-order.sdp.md # behavior · feature · defined (owns example space) - orders/create-order-valid-cart.sdp.md # example · story · ready (enabled verifier) - orders/create-order-invalid-cart.sdp.md # example · story · defined (declared only) - orders/create-order-api-contract.sdp.md # contract · story · idea (blocking question) - orders/order-total-rule.sdp.md # rule · story · ready - orders/order-inventory-rule.sdp.md # rule · story · ready - orders/order-latency-constraint.sdp.md # constraint · feature · defined - orders/order-model.sdp.md # model · feature · defined - decisions/order-lifecycle.sdp.md # decision · feature · defined + orders/order-management.sdp.md # behavior · epic · defined + orders/order-placement-flow.sdp.md # workflow · feature · scoped + orders/create-order.sdp.md # behavior · feature · defined (owns the example space) + orders/create-order-valid-cart.sdp.md # example · story · ready (enabled verifier) + orders/create-order-invalid-cart.sdp.md # example · story · defined (declared only) + orders/create-order-api-contract.sdp.md # contract · story · idea (blocking question) + orders/order-total-rule.sdp.md # rule · story · defined + orders/order-inventory-rule.sdp.md # rule · story · defined + orders/order-latency-constraint.sdp.md # constraint · story · defined + orders/order-model.sdp.md # model · story · defined + decisions/order-lifecycle.sdp.md # decision · feature · defined src/orders/ create-order.use-case.ts # impl:orders.create-order-use-case + impl:orders.order-total create-order.route.ts # api:orders.post test/orders/ create-order.valid-cart.test.ts # test:orders.create-order.valid-cart create-order.oracle.ts # oracle:orders.create-order - orders.create-order.valid-cart.test.generated.ts # committed adopted registrar (MD-31) + orders.create-order.valid-cart.test.generated.ts # committed adopted registrar ``` -When you run `sdp build examples/checkout-v1`, the extractor parses the `.sdp.md` carriers and the TypeScript anchors and prints: +Build the example's graph: + +```sh +npm run --silent sdp -- build examples/checkout-v1 +``` ```text 11 specs · 1 packs · 5 anchors → 17 nodes · 32 edges (0 errors, 0 warnings) ``` -Let's inspect the pieces that produce those 17 nodes and 32 edges. +### Your first graph question: the drift alarm + +Before opening a single file, ask the graph which Specs have code ahead of their design. That is recipe 2, the drift alarm (`implemented ∧ ¬ready`): + +```sh +npm run --silent sdp -- q "$(cat dist/recipes/02-the-drift-alarm.js)" --root examples/checkout-v1 --json +``` + +```json +{ + "total": 2, + "alarms": [ + { "id": "spec:orders.create-order", "statedReadiness": "defined", "floorReached": "ready", "firstUnmetClause": null, "implementationBindings": 2 }, + { "id": "spec:orders.order-total-rule", "statedReadiness": "defined", "floorReached": "ready", "firstUnmetClause": null, "implementationBindings": 1 } + ] +} +``` + +Both Specs carry `implemented`, because code anchors bind to them through `satisfies`, while their authors state only `defined`. Their structural floor already reaches `ready` (`firstUnmetClause: null`), so nothing structural stands in the way. Yet `npm run --silent sdp -- validate examples/checkout-v1` reports `0 errors · 0 warnings`. + +That is the point. The drift alarm is a graph query, not a CI failure. The [worked dialogue in `CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md) already speaks this way: asked "Anything dishonest in the pack?", its agent reports a drift alarm on an order rule—"an anchor binds code to it, but the design isn't mature"—as a fact to act on, not a broken build. Reaching `ready` takes the floor cleared **plus a human's `declared` statement**. Here the floor is cleared, so each Spec waits only on a person who reads it in a Design Review and states `ready`. That a review happened is never recorded in the graph or checked; doing so would turn the Protocol into a workflow gate. + +Now let's inspect the pieces that produce those 17 nodes and 32 edges. --- @@ -52,7 +93,7 @@ Let's inspect the pieces that produce those 17 nodes and 32 edges. Open [`examples/checkout-v1/specs/orders/create-order.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/specs/orders/create-order.sdp.md): -```markdown +````markdown --- id: spec:orders.create-order kind: behavior @@ -82,19 +123,19 @@ Then an order is created with total {total:number} And the order contains the original cart lines And order creation is rejected because {reason:"empty cart"|"out of stock"} ``` -``` +```` Notice three things about this carrier: -1. **The YAML frontmatter is the closed `envelope` ([`spec:carrier.envelope-contract`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/envelope-contract.sdp.md)).** It carries `id`, `kind`, `altitude`, `readiness`, and `relations`. Nothing else is permitted in frontmatter—every derived property (`implemented`, `hasVerifier`, `claim`) is refused if authored by hand. -2. **Sections carry content; relations carry links ([content-only sections, MD-10](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/content-only-sections.sdp.md)).** `create-order` links to its parent (`refines: spec:orders.order-management`), its latency NFR (`constrainedBy: spec:orders.order-latency-constraint`), and the shaping ADR (`decidedBy: spec:decisions.order-lifecycle`) in `relations`, never buried inside bullet text. +1. **The YAML frontmatter is the closed `envelope` ([`spec:carrier.envelope-contract`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/envelope-contract.sdp.md)).** It carries `id`, `kind`, `altitude`, `readiness`, and `relations`; the H1 supplies the title. Nothing else is permitted in frontmatter: a derived key such as `implemented` or `deliveryFacts` is refused at extraction (`extract/reserved-property`), and a derived edge such as `satisfies` under `relations` is refused as well (`extract/invalid-frontmatter`). +2. **Sections carry content; relations carry links ([content-only sections, MD-10](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/content-only-sections.sdp.md)).** `create-order` links to its parent (`refines: spec:orders.order-management`), its latency NFR (`constrainedBy: spec:orders.order-latency-constraint`), and the shaping decision record (`decidedBy: spec:decisions.order-lifecycle`) in `relations`, never buried inside bullet text. 3. **The `gwt-vocabulary` fence defines a typed `example space` ([`spec:carrier.slot-notation`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/slot-notation.sdp.md)).** Each `{slot:type}` placeholder—`{n:number}`, `{availability:"in stock"|"out of stock"}`, `{reason:"empty cart"|"out of stock"}`—declares the shared parameter vocabulary that child examples will bind. ### Binding a Point in the Space -Now look at the child example [`examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md): +Now look at the child example [`examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md). In an `example` Spec, one `gwt` fence closes the `## Intent` section and binds the point: -```markdown +````markdown --- id: spec:orders.create-order.valid-cart kind: example @@ -124,11 +165,11 @@ And the order contains the original cart lines - The order result contains a stable id. - The returned total matches the authored total — the spec's value, never the test's. -``` +```` Under [the point-per-example law (MD-17)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/point-per-example.sdp.md), each `example` Spec binds one concrete point in its parent's example space (`n: 2, q: 1, price: 50, availability: "in stock" → total: 100`). Under the **concreteness law** ([`spec:validation.kind-evidence`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/kind-evidence.sdp.md)), if any slot used in a step is left unbound (for instance, `{n}` instead of `{n: 2}`), the example is capped below `defined` and `sdp build` withholds its step contract. -> **Prefer Gherkin for behavior and example Specs?** Under [the Gherkin carrier option (MD-27)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/gherkin-carrier-option.sdp.md) and [the `.sdp.gherkin` extension law (MD-28)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/sdp-gherkin-extension.sdp.md), any `behavior` or `example` Spec ID can instead use a graph-aware `.sdp.gherkin` file as its one canonical surface, producing the exact same graph nodes and generated contracts. +> **Prefer Gherkin for behavior and example Specs?** Under [the Gherkin carrier option (MD-27)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/gherkin-carrier-option.sdp.md) and [the `.sdp.gherkin` extension (MD-28)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/sdp-gherkin-extension.sdp.md), any `behavior` or `example` Spec ID can instead use a graph-aware `.sdp.gherkin` file as its one canonical surface. For the content both carriers can express, it produces the exact same graph nodes and generated contracts. The Gherkin carrier has no form for open questions, their fields, or relation reasons, so a Spec that needs one stays in Markdown ([`spec:carrier.gherkin-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/gherkin-authoring.sdp.md)). The Protocol's own reader Spec, [`spec:consumers.reader`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/reader.sdp.gherkin), is authored this way. --- @@ -136,10 +177,12 @@ Under [the point-per-example law (MD-17)](https://github.com/libar-dev/software- How does source code connect to `spec:orders.create-order`? Through an in-code **anchor** ([`spec:model.anchors`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/anchors.sdp.md)). -An anchor is a human-authored binding assertion in a `.ts` or `.tsx` file. It never carries Spec prose, readiness, or status. It can be written in **two representations** that feed the exact same closed envelope: +An anchor is a human-authored binding assertion in a `.ts` or `.tsx` file. It never carries Spec prose, readiness, or status. It can be written in **two representations** that feed the exact same closed envelope. + +Both rest on **Principle P6, identity is the universal join key** ([founding principles](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md)). Specs and code link by string IDs such as `"spec:orders.create-order"`, never by TypeScript imports between code and Specs. That is why a refactor that moves or renames files never breaks the trace, why the generated contracts never import product code, and why the comment form below needs no import at all. ### Representation A: The Constant Form -When your code may import `@libar-dev/software-delivery-protocol` (or its zero-dependency subpath `@libar-dev/software-delivery-protocol/anchors` for browser and edge runtimes), you write a top-level constant, as in [`examples/checkout-v1/src/orders/create-order.use-case.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/src/orders/create-order.use-case.ts): +When your code may import `@libar-dev/software-delivery-protocol` (or its zero-dependency subpath `@libar-dev/software-delivery-protocol/anchors`), you write a top-level constant, as in [`examples/checkout-v1/src/orders/create-order.use-case.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/src/orders/create-order.use-case.ts): ```ts import { codeAnchor, codeAnchorId, ref } from "@libar-dev/software-delivery-protocol"; @@ -173,12 +216,12 @@ You can write a top-level `/** … */` comment using reserved camelCase `@sdp*` */ ``` -Look closely at the distinction between `@sdpSatisfies` and `@sdpReferences` ([MD-37](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md)): +Look closely at the distinction between `@sdpSatisfies` and `@sdpReferences` (the binding-grain ruling, [MD-37](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md)): - **`@sdpSatisfies` (`satisfies`):** This unit (together with any sibling units satisfying the same Spec) **realizes** the Spec's promises. It confers the `implemented` delivery fact on `spec:consumers.census-page`. - **`@sdpReferences` (`references`):** This unit **answers to** a design or decision (`spec:decisions.architectural-annotation`) without claiming to realize it—or realizes only part of a Spec whose remaining promises are not yet built. It puts a `references` edge in the graph so `blastRadius` and impact queries traverse it, **without conferring `implemented` and without tripping the drift alarm**. -If you write a stub or follow an ADR, you use `references`. You never have to lie with `satisfies` just to get traceability. +If you write a stub or follow a decision record, you use `references`. You never have to lie with `satisfies` just to get traceability. --- @@ -186,10 +229,10 @@ If you write a stub or follow an ADR, you use `references`. You never have to li How do we verify `spec:orders.create-order.valid-cart` so that if someone changes `{total: 100}` to `{total: 120}` in the Markdown Spec, the test suite fails immediately—without brittle Cucumber regexes? -When `sdp build examples/checkout-v1` runs, it emits **derived TypeScript contracts** under `generated/contracts/` and a **registrar** beside the test ([`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md)): +When `npm run --silent sdp -- build examples/checkout-v1` runs, it emits **derived TypeScript contracts** under `generated/contracts/` (one step contract per example and one space contract for the parent) and a **registrar** beside the test ([`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md)). Because the authored test imports that registrar, it is committed and byte-checked rather than ignored ([the adopted-registrars ruling, MD-31](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/adopted-registrars-committed.sdp.md)). 1. **The Space Contract (`orders.create-order.space.ts`):** Exports `CreateOrderConditions` (`{ n: number; q: number; price: number; availability: "in stock" | "out of stock" }`), every child's bound point, and the `CreateOrderOutcome` discriminated union derived from the parent's `Then` steps. -2. **The Expected-Outcome Oracle ([`test/orders/create-order.oracle.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.oracle.ts)):** Models the expected outcome across the entire example space, bound by `specOracle`: +2. **The Expected-Outcome Oracle ([`test/orders/create-order.oracle.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.oracle.ts)):** Models the expected outcome across the entire example space, bound by `specOracle`. Where the Spec states no outcome, it returns `unspecified` rather than guessing: ```ts export const createOrderOracle = specOracle({ @@ -223,7 +266,7 @@ export function expected(conditions: Partial): CreateOrde } ``` -3. **The Bound Test ([`test/orders/create-order.valid-cart.test.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.valid-cart.test.ts)):** Anchors `test:orders.create-order.valid-cart` and activates the generated registrar with `createWorld`, `invoke`, `observe`, `expected`, and `assertions`: +3. **The Bound Test ([`test/orders/create-order.valid-cart.test.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.valid-cart.test.ts)):** Anchors `test:orders.create-order.valid-cart` and activates the generated registrar: ```ts export const createOrderValidCartTest = specTest({ @@ -235,6 +278,14 @@ export const createOrderValidCartTest = specTest({ registerValidCart({ createWorld, invoke, observe, expected, assertions }); ``` +The registrar takes five adapter functions, all defined in that test file except `expected`: + +- `createWorld(point)` builds a fresh world from the generated point, the Spec's own bound values. +- `invoke(world)` runs the system under test, here `createOrderFromCart`. +- `observe(world)` reads what the system did, typed as a `CreateOrderOutcome`. +- `expected(point)` is the oracle above. +- `assertions(world)` adds checks the outcome union does not carry, such as the stable order id. + The generated registrar compares three things at runtime: - **The Spec's authored outcome** (`{ total: 100 }` from the `.sdp.md` file), - **The Oracle's expected outcome** (`2 * 1 * 50 = 100` from `expected(point)`), and @@ -242,30 +293,53 @@ The generated registrar compares three things at runtime: If the Markdown Spec, the oracle, or the implementation drifts from either of the other two, the test fails and names the exact step that diverged. -Meanwhile, `spec:orders.create-order.invalid-cart` is authored at `defined` and declares `verifies: spec:orders.create-order`, but has no `specTest` anchor yet. When you run Recipe 10 (declared versus enabled verifiers) against `examples/checkout-v1`: +Meanwhile, `spec:orders.create-order.invalid-cart` is authored at `defined` and declares `verifies: spec:orders.create-order`, but has no `specTest` anchor yet. Run recipe 10 (declared versus enabled verifiers) against the example: ```sh -node dist/cli/sdp.js q "$(cat dist/recipes/10-declared-versus-enabled-verifiers.js)" --root examples/checkout-v1 --json +npm run --silent sdp -- q "$(cat dist/recipes/10-declared-versus-enabled-verifiers.js)" --root examples/checkout-v1 --json +``` + +```json +{ + "total": 2, + "withDeclaredOnly": 1, + "rows": [ + { + "id": "spec:orders.create-order", + "declared": ["spec:orders.create-order.invalid-cart", "spec:orders.create-order.valid-cart"], + "enabled": ["spec:orders.create-order.valid-cart"] + }, + { + "id": "spec:orders.create-order.valid-cart", + "declared": [], + "enabled": ["test:orders.create-order.valid-cart"] + } + ] +} ``` -The graph reports both `valid-cart` and `invalid-cart` under `declaredExampleVerifiers`, but only `valid-cart` as `enabled: true`. Because `invalid-cart` states `defined` (not `ready`), it sits quietly as honest incomplete trace data without a false warning ([the unbound-example posture, `spec:validation.unbound-example-posture`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/unbound-example-posture.sdp.md)). +The `spec:orders.create-order` row lists both examples under `declared` and only `valid-cart` under `enabled`, and the top-level `withDeclaredOnly: 1` counts the one Spec with a declared verifier that is not enabled. Because `invalid-cart` states `defined` (not `ready`), it sits quietly as honest incomplete trace data without a false warning ([the unbound-example posture, `spec:validation.unbound-example-posture`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/unbound-example-posture.sdp.md)). An unbound example that states `ready` would draw a `conformance/verifies-linkage` warning. --- -## 5. Break It on Purpose: Watching the Honesty Checks Fire +## 5. Break It on Purpose: Watching Extraction, Conformance, Honesty, and Contract Checks Fire -The fastest way to appreciate the validators is to trip them in `examples/checkout-v1` (revert each edit when done): +The fastest way to appreciate the checks is to trip them in `examples/checkout-v1`. Run each experiment from the repository root, then revert the edit (`git checkout -- examples/checkout-v1`) and rerun `npm run generate:example` to restore the generated files. All paths below are under `examples/checkout-v1/specs/orders/`. -| Experiment | What you edit | What happens when you run `sdp validate` or `vitest` | -|---|---|---| -| **1. Misspell a section heading** | In `order-total-rule.sdp.md`, change `## Rule` to `## Rul` | Exits `1` with `extract/unrecognized-heading` and suggests `Did you mean "## Rule"?` | -| **2. Dangle a relation target** | In `create-order.sdp.md`, change `refines: spec:orders.order-management` to `spec:orders.order-managment` | Exits `1` with `conformance/referential-integrity` and suggests `spec:orders.order-management` (edit distance $\le 2$) | -| **3. State readiness you haven't earned** | In `create-order.sdp.md` (stated `defined`), add `### Open questions` with `- [blocking] Wait?` | Exits `1` with `honesty/readiness-floor`, naming unmet clause `no-blocking-open-questions` | -| **4. Leave a parameter slot unbound** | In `create-order-valid-cart.sdp.md`, change `{n: 2}` to `{n}` | Exits `1` with `honesty/readiness-floor` (`kind-evidence-complete`) and withholds the step contract | -| **5. Drift a Spec value from code** | In `create-order-valid-cart.sdp.md`, change `{total: 100}` to `{total: 120}`, rebuild, and run `npx vitest --run examples/checkout-v1/test` | The bound test fails immediately because the generated contract supplies the Spec's `{total: 120}` against the oracle and implementation's `100` | +| Experiment | What you edit | Run | What happens | +|---|---|---|---| +| **1. Misspell a section heading** (extraction) | In `order-total-rule.sdp.md`, change `## Rule` to `## Rul` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `extract/unrecognized-heading` (`heading "Rul" is not recognized; did you mean "Rule"?`), plus two `extract/invalid-markdown-structure` errors (`Intent entries require a field name`), because the rule bullets beneath the unrecognized heading now fall under `## Intent` | +| **2. Dangle a relation target** (conformance) | In `create-order.sdp.md`, change `refines: spec:orders.order-management` to `refines: spec:orders.order-managment` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `conformance/referential-integrity` (`points to missing target "spec:orders.order-managment". Did you mean "spec:orders.order-management"?`); the suggestion is the unique nearest ID within edit distance ≤ 2 | +| **3. State readiness you haven't earned** (honesty) | In `create-order.sdp.md` (stated `defined`), add `### Open questions` with `- [blocking] Wait?` at the end of `## Intent`, before `## Example space` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `honesty/readiness-floor`, naming the unmet clause `no-blocking-open-questions` | +| **4. Leave a parameter slot unbound** (honesty) | In `create-order-valid-cart.sdp.md`, change `{n: 2}` to `{n}` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `honesty/readiness-floor`, naming the unmet clause `kind-evidence-complete`, and writes two contract modules instead of three: the example's step contract is withheld | +| **5. Drift a Spec value from code** (contract) | In `create-order-valid-cart.sdp.md`, change `{total: 100}` to `{total: 120}` | `npm run generate:example && npx vitest --run examples/checkout-v1/test` | The bound test fails at `Then an order is created with total 120` with `Spec Then parameters do not match the oracle payload` and `$.total: expected 120, actual 100`: the regenerated contract supplies the Spec's value, and the oracle and implementation still say `100` | --- ## Up Next in the Series -We have seen how Specs and anchors enter the repo and how the extractor and validators keep them honest. In **[Part 3: Stop Building 30-Verb CLI Walls for Coding Agents](./03-stop-building-verb-walls-for-coding-agents.md)**, we look at what happens *downstream* of the graph: how AI coding agents script `sdp q` across 43 runnable recipes, how design passes fit 240-Spec corpora into context bundles, and how humans review the same graph in the static HTML **Spec Studio**. +We have seen how Specs and anchors enter the repo and how the extractor and validators keep them honest. In **Part 3: Stop Building 30-Verb CLI Walls for Coding Agents** ([read it here](./03-stop-building-verb-walls-for-coding-agents.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/ARTICLES_COMMIT/docs/articles/03-stop-building-verb-walls-for-coding-agents.md)), we look at what happens *downstream* of the graph: how AI coding agents script `sdp q` across 43 runnable recipes, how design passes fit a 243-Spec adopter corpus into context bundles, and how humans review the same graph in the static HTML **Spec Studio**. + +--- + +*About this article: Part 2 of 8 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every command output are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the outputs at a later commit.* diff --git a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md index 9a26030f9..1baaf6282 100644 --- a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md +++ b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md @@ -1,12 +1,12 @@ # Stop Building 30-Verb CLI Walls for Coding Agents: Scripting the Delivery Graph with `sdp q` and Spec Studio -> *Series: Introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (Part 3 of 8) · All links and measurements pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)).* +*Part 3 of 8 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* When teams build CLI tools or MCP servers for AI coding agents, they almost always fall into the same trap: they build a **verb wall**. First, they add `cli list-specs`. Then an agent needs to filter by readiness, so they add `--readiness`. Then an agent needs to see which tests verify a spec, so they add `cli show-verifiers`. Soon there are 30 subcommands and 80 flags. Yet the first time an agent asks a composite question—"show me every `defined` Spec in this Pack that has no blocking questions, together with the files that reference it"—no single verb answers. The agent makes 15 sequential tool calls, blows its context window on verbose CLI output, and stitches the join together in its own scratchpad. -In the **Libar Software Delivery Protocol**, we took the opposite path ([the agent-surface-scripts-graph decision, D5](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-surface-scripts-graph.sdp.md); [the agent front door, MD-22](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-front-door.sdp.md)): +In the **Libar Software Delivery Protocol**, we took the opposite path ([the agent surface scripts the graph decision, D5](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-surface-scripts-graph.sdp.md); [the agent front door, MD-22](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-front-door.sdp.md)): > **Don't give coding agents a wall of verbs. Give them a visible, typed graph and one evaluation sink they can script.** @@ -16,26 +16,36 @@ In the **Libar Software Delivery Protocol**, we took the opposite path ([the age Before designing [`spec:consumers.agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.sdp.md), we asked a simple question: *What is an agent holding in its hand when a session starts?* -Almost never a `Spec` ID. In practice, an agent arrives holding one of three things—what the Protocol calls the **demand map**: +Never the Spec ID it is looking for. An agent arrives holding one of three things—what the Protocol calls the **demand map**: 1. **A concept string** from a user prompt ("where do we enforce the readiness floor?"). 2. **A file path** it is about to read or edit (`src/projections/census.ts`). 3. **A changeset** of modified files from `git diff` (`["src/reader/reader.ts", "src/cli/q-command.ts"]`). -So the Protocol's typed [`reader`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/reader.sdp.md) (`g`) freezes **only** the decoded graph joins (`specs()`, `packs()`, `specContext(id)`, `packContext(id)`) plus those three entry adapters: +So the Protocol's typed reader, `g` ([`spec:consumers.reader`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/reader.sdp.gherkin), itself authored in the Protocol's Gherkin carrier), freezes a deliberately small surface. Four members expose the graph as decoded data: -- `g.findByConcept(term)` — searches titles, narratives, section content, coined camelCase keys, and model terms. -- `g.byFile(path)` — returns every `CodeNode` or `Anchor` recorded at `path` (including zero-import `@sdp*` comment-form anchors) and the `Spec`s reachable from it. -- `g.blastRadius(files)` — computes the file-level impact over the curated graph: directly impacted `Spec`s and `Pack`s, the one-hop `atRisk` neighborhood with each connecting edge's type and `claim`, changed identity-only `unlinked` units, and—crucially—**`coverageUnknown`**, the honest list of changed files the graph records nothing at. +- `g.graph` — the raw graph schema, every node and edge, for any question the other members do not shape. +- `g.specs()` and `g.packs()` — one summary per Spec (its coordinates, stated and derived readiness, delivery facts, and Packs, already computed) and one per Pack (its title, framing, and carrier file). +- `g.findings()` — the validation report's findings, the same ones `sdp validate` prints. + +Two members assemble per-node context: `g.specContext(id)` (one Spec with its sections, its relations in both directions with each edge's claim decoded, its bindings and verifiers, its design columns, the floor clauses it fails, and the findings that name it) and `g.packContext(id)` (one Pack as a review unit: framing, members in authored order, and its design and boundary). + +The last three are the demand map's entry adapters: + +- `g.findByConcept(term)` — a deterministic, case-insensitive substring match over IDs, titles, binding labels, Pack framing, Spec narratives, and section content, naming the field each match was found in. +- `g.byFile(path)` — every node the graph records at `path` (a Spec or Pack authored in that carrier, or a `CodeNode` or `Anchor` declared there, including zero-import `@sdp*` comment-form anchors), plus every Spec reachable from them. +- `g.blastRadius(files)` — the file-level impact of a changeset over the curated graph: the directly impacted Specs and Packs, the one-hop `atRisk` neighborhood with each connecting edge's type and claim, changed code units bound to no Spec (`unlinked`), and, crucially, **`coverageUnknown`**: the honest list of changed files at which the graph records nothing. + +`blastRadius` deliberately never consults an import graph. The Protocol's two-surface rule (a corollary of [Principle P10](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md)) keeps mechanical structure inferred and apart: `sdp build --imports` can write a TypeScript adapter's file-level imports to `generated/imports.json` as external, inferred input, and the curation report (recipe 40, [`spec:consumers.impact-graph.curation-assist`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/impact-graph.curation-assist.sdp.md)) ranks load-bearing files no anchor covers and component imports that disagree with the anchored `uses` edges. That report assists a person curating anchors; it never adds an edge, a binding, or a delivery fact. A gap between the two surfaces is curation, not drift, because a sparse, chosen architecture is the point. ### One Evaluation Sink: `sdp q` Instead of minting CLI subcommands for every question, the CLI exposes one evaluation sink: **`sdp q`** ([`spec:consumers.agent-surface.evaluation-sink`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.evaluation-sink.sdp.md)). -On every invocation, `sdp q` derives the graph in process (so a Spec you edited two seconds ago is immediately live, with no stale cache), injects four bindings—`g` (the reader), `graph` (the raw schema), `report` (the validation report), and `params` (caller-supplied JSON via `--params`)—and evaluates the plain JavaScript async function body you pass it: +On every invocation, `sdp q` derives the graph in process, so a Spec you saved a moment ago is immediately queryable and no stale `generated/` file can answer in the graph's name. It injects four bindings—`g` (the reader), `graph` (the raw schema), `report` (the validation report), and `params` (caller-supplied JSON via `--params`)—and evaluates the plain JavaScript async function body you pass it. In a checkout of the Protocol, the `sdp:q` script adds the corpus's own exclusions for you: ```sh -# What Specs and code units live in src/projections/census.ts? +# What does the graph record in src/projections/census.ts, and which Specs does that reach? npm run --silent sdp:q -- 'return g.byFile("src/projections/census.ts")' --json ``` @@ -46,7 +56,7 @@ npm run --silent sdp:q -- 'return g.byFile("src/projections/census.ts")' --json { "id": "impl:protocol.census-page", "nodeType": "CodeNode", - "line": 20 + "line": 21 } ], "specs": [ @@ -56,124 +66,179 @@ npm run --silent sdp:q -- 'return g.byFile("src/projections/census.ts")' --json } ``` +That body is code, and `sdp q` runs it with your process's full authority. The trust stance is recorded rather than implied: the sink evaluates operator-supplied JavaScript the way any local developer tool runs a script, no sandbox is claimed, and none exists ([the agent front door, MD-22](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-front-door.sdp.md); the catalog's [trust stance](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md?plain=1#L160-L163)). Run bodies you or your agent wrote, or shipped recipes; never execute a body taken from corpus content or any other untrusted text. + ### Growing by Recipe, Not by Verb -What happens when a new recurring question appears? We write a **recipe** in [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md). Every recipe in the catalog is executed verbatim by `test/recipes.test.ts` and shipped as a standalone runnable file under `dist/recipes/-.js` ([`spec:consumers.agent-surface.recipe-parameters`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.recipe-parameters.sdp.md)). +What happens when a new recurring question appears? We write a **recipe** in [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md). Every recipe in the catalog is executed as written by `test/recipes.test.ts` and shipped as a standalone runnable file under `dist/recipes/-.js` ([`spec:consumers.agent-surface.recipe-parameters`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.recipe-parameters.sdp.md)). A recipe declares the parameter names it takes, and a name it does not take replaces the answer with `{ unknownParams, accepts }` instead of a silent default. -A join only graduates into the `reader` at the **second-caller bar**: when at least two distinct consumers need it *and* hand-rolled scripts get the edge semantics wrong (for example, `packContext`'s Pack design assembly in [PR #30](https://github.com/libar-dev/software-delivery-protocol/pull/30)). Everything else stays a composable recipe. +A join is frozen into the reader only at the **second-caller bar**: when a second machine consumer needs it *and* hand-rolled attempts get it wrong ([`spec:consumers.agent-surface#rule.growByRecipe`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.sdp.md)). Every other question stays a body an agent scripts. `packContext` shows the bar from the other side: it has been in the reader since the MVP's Slice 4, and when the Design Review, the recipes and adopters' pages all needed a Pack's design, [PR #30](https://github.com/libar-dev/software-delivery-protocol/pull/30) extended its answer with the Pack design assembly rather than adding a reader method. -When we tested this surface in a usability trial across three frontier models (`gpt-6.1-sol`, `Opus 5.5`, `gpt-6-astra`; see [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31)), all three models answered 13 out of 13 delivery questions from the skills and `sdp q` alone without opening a single `.sdp.md` file—and their friction points directly shaped summary-first recipe outputs (`detail: true`), unknown-parameter diagnostics (`{ unknownParams, accepts }`), and rule-level source lines. +We have tested this surface on agents. After PR #30, three agents of two model families (`gpt-6.1-sol`, `Opus 5.5`, `gpt-6-astra`) answered thirteen delivery questions from the skills and `sdp q` alone, and all three answered every question correctly. All three also stumbled in the same places: answers too large for a tool's output, misses that named no reason, two meanings of "next rung", and rules with no address or line. [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31) answered each of those: summary-first outputs for the largest answers (recipes 4 and 29, and later recipe 35, with `detail: true` for the full rows), explained misses, both next rungs, and rule keys with recorded lines. A re-run on `gpt-6.1-sol` answered 13 of 13 again but took 23 calls against the first trial's 20, and one of its own batched calls overflowed; the reading skill's task-to-recipe sheet ([PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32), [`spec:consumers.reading-on-ramp`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/reading-on-ramp.sdp.md)) is the answer to that. --- -## 2. Iterating on a Large Design Pass After Pass +## 2. Iterating on a Large Design, Pass After Pass + +The real test of an AI-native delivery system is not a 10-Spec toy example. It is our first adopter, [`libar-platform`](https://github.com/libar-dev/libar-platform), which models a Convex transactional domain platform across **243 Specs in six capability Packs**, most of it design that will stay ahead of its code for months ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); the adopter's side is [libar-platform#13](https://github.com/libar-dev/libar-platform/pull/13)). + +Design at that scale happens in **design passes**: one round in which an agent or a person reads a Pack (or a list of Specs), finds the gaps, and proposes changes, and the next round starts from what that one changed. A pass reads three things beside the Specs themselves. A member's **footing** is every Spec it rests on through its typed dependencies, with the rung each one states and the floor each one reaches (recipe 21). A **pinned declaration** is a keyed Design entry whose value opens with a code span, stating one signature, type, validator, or table that a builder and a test author both hold to (recipe 24). And an **entry address**, such as `spec:#design.`, names one keyed entry so a pass can cite it exactly. + +Four problems hit immediately at that scale, and the iteration loop answers each on the one graph: -The real test of an AI-native delivery system is not a 10-Spec toy example. It is our first adopter, `libar-platform`, which models a Convex transactional domain platform across **243 Specs in six capability Packs**—most of it designed months ahead of its code ([PR #26](https://github.com/libar-dev/software-delivery-protocol/pull/26), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). +### 1. Fitting a Pack into a Model's Context Budget (recipe 36) -At that scale, four problems hit immediately—and the Protocol's iteration loop solves each one on the one graph: +Hand-assembling a pass's reading material (the Pack design, member sections, open questions, footing, pinned declarations, and examples) overflows a model's context. **The context bundle** ([`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md), recipe 36) composes it on demand within a hard budget, `100,000` by default. -### 1. Fitting a Pack into a Model's Context Budget (`recipe 36`) -When an agent runs a design pass over a capability Pack, hand-assembling the Pack design, member sections, open questions, dependency footing, pinned declarations, and examples overflows the context window (`pack:history-view` in `libar-platform` was nearly 300,000 UTF-16 code units). +The budget is counted in **UTF-16 code units**: the `.length` a JavaScript body reads, measured on the compact JSON of the answer. That count is deterministic on every machine and binds the Protocol to no model's tokenizer; the bundle reports a `tokenEstimate` beside it, labeled as an estimate (code units divided by four). -**The Context Bundle ([`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md), recipe 36)** composes a pass's reading material on demand within a hard character budget (default `100,000` UTF-16 code units). It stores each question and pinned declaration once, and if the Pack exceeds the budget, it applies a deterministic **seven-step cut order**—trimming boundary details to IDs, footing details to IDs, narratives, example steps, non-blocking questions, non-declaration section entries, and finally (when opted in with `declarations: "addresses"`) replacing pinned declarations with their entry addresses (`spec:id#design.key`) so the agent can look up only the signatures it needs. It never returns a partial member list. On `libar-platform`'s `pack:history-view`, `declarations: "addresses"` brings a 299k-unit Pack cleanly down to **93,806 units**. +A bundle carries each question and each pinned declaration in one place only. If the scope still exceeds the budget, it 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 narratives, example step text, non-blocking question text, and section values other than pinned declarations. Only when the caller passes `declarations: "addresses"` does a seventh cut replace pinned declarations with their entry addresses, so the agent looks up only the signatures it needs. Member IDs, stated rungs, floors, blocking questions, and entry addresses are never cut, and the bundle never returns a partial member list: when even the protected content does not fit, the answer is `budgetExceeded: true` with the minimum length it needs. + +On `libar-platform`, `pack:history-view` first needed at least 299,106 code units. PR #33 redesigned the bundle's shape (each member's file named once, cut entries reduced to an address or a line, a new footing cut) and added the opt-in declaration cut; with `declarations: "addresses"` the same Pack now fits in **93,806 of 100,000** units. ```sh npm run --silent sdp:q -- "$(cat dist/recipes/36-the-context-bundle.js)" \ --params '{"pack":"pack:design-iteration-v1","declarations":"addresses"}' --json ``` -### 2. Comparing Design Passes at Entry Grain (`recipe 35`) -`git diff` shows changed lines; it does not tell you which Design keys changed, which open questions closed, or which readiness rungs moved since the last design pass. **The Design Diff ([`spec:consumers.design-diff`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/design-diff.sdp.md), recipe 35)** takes a base `graph.json` via `--params base=@path` and diffs the curated graph by Spec, section, and entry key, keeping code-binding changes separate from intent changes. +On the Protocol's own corpus, that eleven-member Pack fits in 87,185 units with nothing left out. Asked for `pack:self-hosting-v1`, all 276 of the Protocol's Specs, with declarations kept whole, the recipe answers `budgetExceeded: true` with a `protectedLength` of 409,404 rather than a bundle that silently drops members. + +### 2. Comparing Design Passes at Entry Grain (recipe 35) + +`git diff` shows changed lines; it does not tell you which Design keys changed, which open questions closed, or which readiness rungs moved since the last pass. **The design diff** ([`spec:consumers.design-diff`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/design-diff.sdp.md), recipe 35) takes the graph `sdp build` wrote at the base commit via `--params base=@path` and diffs the curated graph by Spec, section, and entry key, keeping code-binding changes separate from intent changes. + +### 3. Handing Changes Back Safely (recipe 38) -### 3. Handing Changes Back Safely (`recipe 38`) -Views never patch source files directly ([`spec:consumers.edit-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/edit-model.sdp.md)). Instead, a design pass returns a structured **Change Proposal** ([`spec:consumers.intent-composition.proposal`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/intent-composition.proposal.sdp.md))—a JSON document validated against [`schemas/sdp-proposal-1.schema.json`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/schemas/sdp-proposal-1.schema.json)—where each item targets a Spec section or entry address (`spec:id#rule.key`, `#design.key`, `#question.key`). Before an agent applies the proposal as an ordinary source edit, **the Proposal Check (recipe 38)** verifies whether any target scope moved since the proposal's base commit; any scope that moved is routed to a human. +Views never patch source files directly ([`spec:consumers.edit-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/edit-model.sdp.md)). Instead, a design pass returns a structured **change proposal** ([`spec:consumers.intent-composition.proposal`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/intent-composition.proposal.sdp.md)): a JSON document on the shipped schema [`schemas/sdp-proposal-1.schema.json`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/schemas/sdp-proposal-1.schema.json), where each change targets a Spec or an entry address (`#design.`, `#ui.`, `#question.`, `#rule.`). -### 4. Running Project-Specific Architecture & Policy Rules (`sdp validate --rules`) -In [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33), `libar-platform` retired the graph-side half of its custom Python checker (`check.py`) into `sdp validate --rules`. Because any `sdp q` body returning `{ findings }` is a valid rules file, shipped recipes like **the Architecture Walk ([`spec:validation.architecture-constraints`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/architecture-constraints.sdp.md), recipe 42)** run directly inside `sdp validate`: +**The proposal check** (recipe 38) takes the proposal and the graph written at the proposal's base commit, and reports, change by change, whether each target scope has moved since that base. It only reports: it applies nothing and writes no carrier. The caller that applies the proposal sends any change whose scope moved to a person, applies the others as ordinary source edits, and lands them only from a worktree on which `sdp validate` reports no error ([`spec:consumers.intent-composition#rule.changedScope`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/intent-composition.sdp.md)). + +### 4. Running Project-Specific Architecture and Policy Rules (`sdp validate --rules`) + +In PR #33, `libar-platform` retired the graph-side half of its custom Python checker, `check.py`, into `sdp validate --rules` ([`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md)). A rules file is a `sdp q` body that returns `{ findings }`, where each finding is well-formed: a `validatorId` (the rule's own kebab-case ID), a `subjectId`, a `severity` of `error` or `warning`, and a `message`, with an optional `file` and `line`. A broken rule, or a finding missing one of those fields, fails the run rather than passing as clean. + +So a shipped recipe whose answer carries findings is already a rules file. **The architecture walk** ([`spec:validation.architecture-constraints`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/architecture-constraints.sdp.md), recipe 42) checks forbidden layer directions and isolated contexts against anchored `uses` edges. Here it forbids the `edge` layer from using the `domain` layer, at warning severity: ```sh -node dist/cli/sdp.js validate . \ +npm run --silent sdp -- validate . \ --exclude explorations --exclude examples --exclude test/fixtures/import/parity \ --rules dist/recipes/42-the-architecture-walk.js \ - --params '{"forbidden":[{"from":"domain","to":"edge"}]}' + --params '{"forbidden":[{"from":"edge","to":"domain"}],"severity":"warning"}' ``` +Standard output keeps the Protocol's summary line untouched and tallies the corpus's own rules on a line of their own: + ```text validate: 0 errors · 6 warnings (conformance + honesty over the one graph) -corpus rules: 0 errors · 0 warnings · 0 of 1 files failed (the corpus's own rules) +corpus rules: 0 errors · 7 warnings · 0 of 1 files failed (the corpus's own rules) ``` +On standard error, the Protocol's six findings come first (the corpus's five intentional prose-mention warnings and one pending verifier gap, both pinned by its own test oracle). Then each rules file prints a header and its findings: + +```text +corpus rules dist/recipes/42-the-architecture-walk.js +[warning] forbidden-layer — component:protocol.cli uses component:protocol.graph: layer edge uses layer domain (component:protocol.cli to component:protocol.graph), a forbidden pair +[warning] forbidden-layer — component:protocol.cli uses component:protocol.model: layer edge uses layer domain (component:protocol.cli to component:protocol.model), a forbidden pair +…five more for the CLI and projections components +``` + +Drop `"severity":"warning"` and the same seven findings are errors: the corpus line reads `7 errors` and `sdp validate` exits `1`. The Protocol's own architecture does not forbid that direction, so this is a demonstration, not a defect; forbid a direction no edge takes, such as `domain` to `infrastructure`, and the corpus line reads `0 errors · 0 warnings`. + --- ## 3. Spec Studio: The Static HTML Workbench for Human Reviewers -Agents love `sdp q` and JSON. Humans reviewing a 20-member capability Pack do not want to read JSON, and once a Pack grows rich with design columns, open questions, and code bindings, a flat Markdown table becomes 13 columns wide. +Agents are well served by `sdp q` and JSON. Humans reviewing a 20-member capability Pack do not want to read JSON, and the frozen Markdown Design Review's Pack member table already runs 13 columns wide. -In [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33), we shipped Phase 0 of the **Spec Studio** ([`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md); [the Studio renders derived HTML, MD-41](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-html-surface.sdp.md); [built from custom elements, MD-42](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-web-components.sdp.md)). +In PR #33, we shipped phase 0 of the **Spec Studio** ([`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md); [the Studio renders derived HTML, MD-41](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-html-surface.sdp.md); [the Studio is built from custom elements, MD-42](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-web-components.sdp.md)). -Run one command: +Run one command from a checkout: ```sh -node dist/cli/sdp.js studio . --exclude explorations --exclude examples --exclude test/fixtures/import/parity +npm run --silent sdp -- studio . --exclude explorations --exclude examples --exclude test/fixtures/import/parity ``` -`sdp studio` publishes a deterministic, zero-network static HTML site under `generated/spec-studio/` that opens directly from `file://` in any browser or hosts on any static preview server: +`sdp studio` publishes a deterministic static HTML page set under `generated/spec-studio/`. It is never committed, makes no network request when opened from `file://`, and also works from any static preview server: + +- **No second graph in the browser.** Every join, claim, delivery fact, derived readiness, floor failure, and finding on a page was computed once by the reader during `sdp studio`. Six framework-free custom elements add filtering, open-all and close-all, copy buttons, search, and a build stamp without recomputing graph logic. +- **Readable Pack pages ([`spec:consumers.spec-studio.pack-page`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.pack-page.sdp.md)).** Each Pack page leads with a plain-language summary of where the design stands, then one collapsible card per member in authored reading order (its stated rung beside the floor it reaches, and its stated next rung with either `nothing blocks` or how many floor clauses are still unmet), then the Pack's four-part boundary: `Rests on`, `Rested on by`, `Verified by`, `Superseded by`. + +![The libar-platform history-view Pack page in Spec Studio: a plain-language summary, the Pack framing, and collapsible member cards](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-pack.png) + +*The `libar-platform` Pack page for `pack:history-view`: the summary the graph computes (19 members, 283 design entries and 169 pinned declarations, 53 open questions and none blocking), the framing, then the member cards.* -- **No second graph in the browser.** Every join, claim, delivery fact, readiness floor, and finding on the page was computed once by the `reader` during `sdp studio`. Six framework-free Web Components add filtering, collapsible cards, address-copy buttons, and instant search without recomputing graph logic. -- **Readable Pack pages ([`spec:consumers.spec-studio.pack-page`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.pack-page.sdp.md)).** Each Pack page leads with a plain-language summary of where the design stands, followed by collapsible member cards in authored reading order and the Pack's four-part boundary (`Rests on`, `Rested on by`, `Verified by`, `Superseded by`): +- **Addressable entries and open questions ([`spec:consumers.spec-studio.spec-page.core`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.spec-page.core.sdp.md)).** Every keyed Design entry (`#design.`), UI entry (`#ui.`), rule (`#rule.`), and open question (`#question.`) has a deep link and a one-click copy action for its full entry address. Each open question also shows the four optional fields only open questions carry: its `decider`, its `links`, its `provisional` answer, and its `trigger`. -![The platform's history-view Pack page in Spec Studio](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-pack.png) +![Open questions on a Spec Studio Spec page, each showing its blocking flag, its key with a copy button, its recorded line, and its text, followed by the Spec's keyed workflow rules with their lines](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-question.png) -- **Addressable open questions and section entries ([`spec:consumers.spec-studio.spec-page.core`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.spec-page.core.sdp.md)).** Every keyed Design entry (`#design.`), UI entry (`#ui.`), rule (`#rule.`), and open question (`#question.`) has a deep link and a one-click address copy action, displaying its `decider`, `links`, `provisional` answer, and `trigger`: +*The open questions on a `libar-platform` Spec page, opened at its entry address `#question.verifyPassUnderPause`. These questions carry no optional fields; a question that does shows each one on its own labelled line.* -![A Spec page's open questions with keyed addresses and fields in Spec Studio](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-question.png) +- **Shared concept search ([`spec:consumers.spec-studio.search`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.search.sdp.md)).** The search page bundles the same matcher that powers `g.findByConcept` in `sdp q` (shipped for browsers as the package's `./concept-match` subpath), and adds the keys authors coin, such as `verifyPassUnderPause`, and the file paths the graph records: -- **Shared concept search ([`spec:consumers.spec-studio.search`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.search.sdp.md)).** The browser search page bundles the exact same `./concept-match` matcher that powers `g.findByConcept` in `sdp q`, indexing Spec prose, coined camelCase keys (`verifyPassUnderPause`), pinned declarations, and file paths: +![Spec Studio search for OD-157: one matching Spec, with the fields searched listed and the matching open question shown under it with its section and line](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-search.png) -![Searching for decision register ID OD-157 in Spec Studio](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-search.png) +*Searching the `libar-platform` page set for `OD-157`, an identifier the adopter cites in its own Spec prose: one Spec matches, and the entry that mentions it is listed beneath with its line.* + +All three screenshots were captured from `libar-platform`'s branch during PR #33, as [the screenshot notes](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/README.md) record; publish the page set yourself to see the current pages. --- -## 4. Three Mandatory Skills: How Agents Learn the Workflow +## 4. Three Skills: How Agents Learn the Workflow A powerful graph surface only helps if coding agents actually use it instead of falling back to `grep` and file scanning. -The Protocol ships three repository-owned agent skills under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills) (included in the npm package so every adopter gets them automatically): +The Protocol ships three agent skills under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills). In the Protocol's own repository they are mandatory: [`AGENTS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/AGENTS.md) requires an agent to load the matching skill before any corpus question, Spec authoring, or delivery-session routing. They also ship in the npm package, at `node_modules/@libar-dev/software-delivery-protocol/.agents/skills/`, for an adopter to point its agents at: -1. **[`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md)** — Teaches the graph shape, the task-to-recipe lookup table, the wrap-and-filter idiom, and the **anti-anecdote rule**: *the derived graph outranks any summary or cached note; query the graph first, read files last*. -2. **[`sdp-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md)** — Teaches the closed `.sdp.md` and `.sdp.gherkin` grammars, where unsettled truths live (blocking questions, `trigger:` deferrals, `provisional:` leans), how to choose honestly between `satisfies` and `references`, and when to slim implemented design detail past the **distillation boundary**. -3. **[`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md)** — Routes delivery sessions across five advisory work shapes (*Capture*, *Design*, *Implement*, *Review*, *Close/Slim*), complete with tripwires and a re-measurable handoff template—without ever turning those shapes into a workflow state machine. +1. **[`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md)** — teaches the graph's shape, the task-to-recipe sheet, the wrap-and-filter idiom (run a shipped recipe inside your own body and filter its answer, rather than editing the recipe), and the **anti-anecdote rule**: *the derived graph outranks any summary or cached note; query the graph first, read files last*. +2. **[`sdp-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md)** — teaches the closed `.sdp.md` and `.sdp.gherkin` grammars, where unsettled truths live (blocking questions, `trigger:` deferrals, `provisional:` leans), how to choose honestly between `satisfies` and `references` when binding code, and how much to write. +3. **[`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md)** — routes delivery sessions across five advisory work shapes (*Capture / refine*, *Design*, *Implement*, *Review*, *Close / slim*), with a re-measurable handoff template and **tripwires**: signs that work has slipped out of its shape, such as writing code during a design session or stating a rung for the author during a review. Its *Close / slim* shape names the **distillation boundary** ([`spec:model.enrichment-lifecycle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/enrichment-lifecycle.sdp.md)): a Design entry may leave its Spec only once the Spec is implemented with a verifier and a bound artifact carries the entry. None of the shapes is a workflow state machine; every preflight is advisory. --- ## 5. Try the Agent Surface and Spec Studio -From a checkout of [`libar-dev/software-delivery-protocol`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) after `npm ci`: +From a checkout of [`libar-dev/software-delivery-protocol`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6): ```sh +git clone https://github.com/libar-dev/software-delivery-protocol.git +cd software-delivery-protocol +git checkout 5a0b22c2d9ead98c900b60ed6209342fce2cbef6 +npm ci # also builds dist/ and dist/recipes/ + # 1. Inspect the Pack design summary for pack:spec-studio-v1 (Recipe 29) npm run --silent sdp:q -- "$(cat dist/recipes/29-pack-design.js)" \ --params '{"pack":"pack:spec-studio-v1"}' --json -# 2. Check all 43 anchored uses edges across layers and contexts (Recipe 32) +# 2. List the anchored uses edges that cross a layer or context, tallied by direction (Recipe 32) npm run --silent sdp:q -- "$(cat dist/recipes/32-architecture-crossings.js)" --json -# 3. Search for a family of keyed Design entries by prefix (Recipe 41) +# 3. List every Design and UI entry whose key starts with a prefix (Recipe 41) npm run --silent sdp:q -- "$(cat dist/recipes/41-the-key-family-register.js)" \ --params '{"prefixes":["bundle","rule"]}' --json -# 4. Compare planned component designs against realized component: anchors (Recipe 43) +# 4. Compare planned component designs against realized component: anchors (Recipe 43). +# On the Protocol's own corpus this returns empty totals (designs: 0), because every +# component is already realized in code; it is for adopters planning components ahead of code. npm run --silent sdp:q -- "$(cat dist/recipes/43-planned-against-realized-components.js)" --json + +# 5. Publish the Spec Studio, then open generated/spec-studio/index.html +npm run --silent sdp -- studio . --exclude explorations --exclude examples --exclude test/fixtures/import/parity ``` +Never invoke a bare `sdp` inside this checkout: `npm run --silent sdp -- …` runs the package's own build, and `npm run --silent sdp:q -- …` adds the three exclusions that keep exploration exhibits, the separate worked example, and carrier-parity fixtures out of the self-hosting root. An adopter that installed the package runs `npx sdp …` from its own root instead. + --- ## Where the Series Goes Next -With the launch manifesto ([Part 1](./01-the-spec-is-the-prompt-is-the-design.md)), the hands-on code walkthrough ([Part 2](./02-walkthrough-intent-to-verified-code.md)), and the agent surface & Spec Studio tour (Part 3) in place, the upcoming installments in the [Media Plan](./README.md) dive into specific architectural stories from the Protocol's construction: +This post closes the opening trio. **Part 1**, *The Spec Is the Prompt Is the Design* ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/ARTICLES_COMMIT/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), made the case for one graph. **Part 2**, *From Rough Idea to Verified Code* ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/ARTICLES_COMMIT/docs/articles/02-walkthrough-intent-to-verified-code.md)), walked the loop from a Spec to a verified test. The next five installments tell specific stories from the Protocol's construction: - **Part 4:** *Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition* - **Part 5:** *What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Annotation Reversal, and Honest Code Bindings* - **Part 6:** *Executable Specifications Without Cucumber Glue: Typed Example Spaces, Generated Contracts, and Expected-Outcome Oracles* -- **Part 7:** *Designing a 240-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale* -- **Part 8:** *Auditing 276 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh* +- **Part 7:** *Designing a 243-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale* +- **Part 8:** *Auditing 258 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh* + +--- + +*About this article: Part 3 of 8 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every command output are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the outputs at a later commit.* diff --git a/docs/articles/README.md b/docs/articles/README.md index eeabf34d6..af0cf44bf 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -18,9 +18,9 @@ When AI coding agents enter that environment, the fragmentation compounds. Agent ### The four-part slogan Every article in the series reinforces one or more clauses of the founding slogan from the [concept overview](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/README.md): -1. **Specs are code** — One authored truth-primitive (`Spec`) enriched in place across three independent descriptors (`kind` × `altitude` × `readiness`), authored in Markdown (`.sdp.md`) by default, TypeScript (`.sdp.ts`), or graph-aware Gherkin (`.sdp.gherkin`). +1. **Specs are code** — One authored truth-primitive (`Spec`) enriched in place across three independent descriptors (`kind` × `altitude` × `readiness`), authored in Markdown (`.sdp.md`) by default, with TypeScript (`.sdp.ts`) as a lawful per-ID option and graph-aware Gherkin (`.sdp.gherkin`) as a per-ID option for `behavior` and `example` Specs only (the Gherkin carrier option, MD-27). 2. **The graph is derived** — One flat, deterministic, regenerable graph (`graph = f(repo)`) serves as the sole read model for validators, human views, and coding agents. Delete `generated/` and rebuild it byte-for-byte. -3. **The `claim` stays honest** — Every fact carries its epistemic status (`declared` human intent, `anchored` source bindings, or `inferred` mechanical structure) and the three are never collapsed. Delivery facts (`implemented`, `has-verifier`, `observed`) are derived from resolving edges, never hand-authored. +3. **The `claim` stays honest** — Every fact carries its epistemic status (`declared` human intent, `anchored` source bindings, or `inferred` mechanical structure) and the three are never collapsed. The delivery facts `implemented` and `has-verifier` are derived today from resolving edges, never hand-authored; `observed` is designed into the model but not yet derived. 4. **Git is the event log** — History and prior states live in git commits, not in a second database or ticket status machine. --- @@ -30,15 +30,59 @@ Every article in the series reinforces one or more clauses of the founding sloga | Audience segment | What hurts them today | What hooks them in the Protocol | Primary articles | |---|---|---|---| | **Staff / Principal Engineers & Software Architects** | Architecture diagrams and ADRs rot the week after a design review; no way to verify whether code still honors the design or which decisions shape a module. | One primitive with named coordinates; zero-import `@sdp*` comment-form anchors (`satisfies` vs. `references`, `role`, `layer`, `context`); deterministic conformance + honesty checks in CI. | Articles 1, 2, 5, 7 | -| **AI / Coding-Agent Builders & Power Users** | `AGENTS.md` files bloat into thousands of lines of prose; MCP tools and CLI "verb walls" force agents to make 20+ calls or grep raw files and guess. | "Agents script the graph, not a verb wall": `sdp q` + `createReader`, 43 runnable recipes, token-budgeted context bundles, and structured change proposals. | Articles 1, 3, 7, 8 | -| **Engineering Leaders & Technical Founders** | Process theatre—mandatory sprint gates, Jira status choreography, and "100% spec coverage" quotas that produce AI-generated boilerplate nobody reads. | "Adopt the nouns, reject the gates" (MD-2); readiness floors instead of quotas; `ready ∧ ¬implemented` (build backlog) and `implemented ∧ ¬ready` (drift alarm) as live graph queries. | Articles 1, 4, 7 | -| **BDD & Testing Practitioners** | Cucumber `.feature` files drift from step regexes, duplicate domain logic, and turn non-behavioral NFRs or domain models into awkward Given/When/Then prose. | Carrier-independent executable specs: typed `gwt-vocabulary` example spaces, generated step and space contracts, committed registrars, and `specOracle` expected-outcome functions. | Articles 2, 4, 6 | +| **AI / Coding-Agent Builders & Power Users** | `AGENTS.md` files bloat into thousands of lines of prose; MCP tools and CLI "verb walls" force agents to make 20+ calls or grep raw files and guess. | "Agents script the graph, not a verb wall": `sdp q` + `createReader`, 43 runnable recipes, context bundles composed within a budget counted in UTF-16 code units, and structured change proposals. | Articles 1, 3, 7, 8 | +| **Engineering Leaders & Technical Founders** | Process theatre—mandatory sprint gates, Jira status choreography, and "100% spec coverage" quotas that produce AI-generated boilerplate nobody reads. | "Adopt the nouns, reject the gates" (MD-2); readiness floors instead of quotas; the build backlog (`ready` Specs without `implemented`, excluding `example` and `decision` Specs) and the drift alarm (`implemented ∧ ¬ready`) as live graph queries. | Articles 1, 4, 7 | +| **BDD & Testing Practitioners** | Cucumber `.feature` files drift from step regexes, duplicate domain logic, and turn non-behavioral NFRs or domain models into awkward Given/When/Then prose. | Carrier-independent executable specs: typed `gwt-vocabulary` example spaces, generated step and space contracts, committed adopted registrars (MD-31), and `specOracle` expected-outcome functions. | Articles 2, 4, 6 | ### Channel & format rollout - **Long-form canonical posts** — Publish from the Markdown manuscripts in `docs/articles/` to the project blog, Substack, or dev.to, with commit-pinned links to the public repository (`libar-dev/software-delivery-protocol`) and `try-it` blocks readers can run in 60 seconds. -- **Hacker News / Lobsters launches** — Lead with Articles 1, 3, and 5. Title for technical substance and contrarian clarity (e.g., why we deleted a 24-tag annotation registry, why we built one JS evaluation sink instead of 30 CLI subcommands, why we ran a 4-way carrier competition to choose Markdown). +- **Tables on other platforms** — Substack and Medium do not render Markdown tables. When cross-posting there, convert each table in an article (and in this playbook) to a bulleted list, one bullet per row, or to a rendered image with the same content as its alt text. dev.to and GitHub render the tables as written. +- **Hacker News / Lobsters launches** — Lead with Articles 1, 3, and 5. Title for technical substance and contrarian clarity (e.g., why we refused to carry over Gen 1's 24-tag registry, why we built one JS evaluation sink instead of 30 CLI subcommands, why we ran a 4-way carrier competition to choose Markdown). - **Visual & interactive companions** — Pair Article 3 and Article 7 with the static HTML **Spec Studio** screenshots from [`reviews/assets/pr33`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33) and live static-host previews generated by `sdp studio`. -- **Podcast / Audio briefings (NotebookLM)** — Feed the 24 core sources plus Articles 1–3 into NotebookLM to produce an audio overview for commuters and architecture-review prep. +- **Podcast / Audio briefings (NotebookLM)** — Feed [the 24 core sources](#the-24-core-sources-pinned-to-5a0b22c) plus Articles 1–3 into NotebookLM to produce an audio overview for commuters and architecture-review prep. + +### The 24 Core Sources (Pinned to `5a0b22c`) + +
+The repository files the series draws on, each pinned to commit 5a0b22c (expand) + +**Vocabulary and principles** + +1. [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md) — the ratified glossary +2. [`docs/concept/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/README.md) — the concept overview and the four-part slogan +3. [`docs/concept/00-vision-scope-and-mvp-boundary.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md) — vision, scope, and non-goals +4. [`docs/concept/01-founding-principles-and-invariants.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md) — principles P1 to P10 +5. [`docs/concept/04-authoring-and-binding.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/04-authoring-and-binding.md) — authoring and binding +6. [`docs/concept/06-consumers-and-projections.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/06-consumers-and-projections.md) — consumers and the two surfaces +7. [`docs/concept/DECISIONS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/DECISIONS.md) — the decision registry + +**Carrying Specs** + +8. [`specs/model/core-model.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/core-model.sdp.md) — the one primitive and its descriptors +9. [`specs/decisions/carrier-ruling.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/carrier-ruling.sdp.md) — the carrier ruling (MD-18) +10. [`specs/decisions/anchor-comment-form.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md) — the comment form (MD-36) +11. [`specs/decisions/anchor-binding-grain.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md) — bindings are optional, plural, and may reference a design (MD-37) +12. [`specs/decisions/architectural-annotation.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/architectural-annotation.sdp.md) — architectural significance is annotated where it is realized (MD-38) +13. [`specs/extraction/executable-contracts.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md) — generated step and space contracts +14. [`specs/consumers/agent-surface.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.sdp.md) — the agent surface and the demand map +15. [`specs/consumers/context-bundle.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md) — the context bundle +16. [`specs/validation/corpus-rules.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md) — corpus rules under `sdp validate --rules` +17. [`specs/consumers/spec-studio.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md) — the Spec Studio + +**Agent on-ramps and recipes** + +18. [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md) — the runnable recipe catalog +19. [`.agents/skills/sdp-agent-surface/SKILL.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md) — the reading skill +20. [`.agents/skills/sdp-authoring/SKILL.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md) — the authoring skill +21. [`.agents/skills/sdp-sessions/SKILL.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md) — the sessions skill + +**Worked example, lineage, and review** + +22. [`examples/checkout-v1/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md) — the worked MVP example +23. [`docs/lineage/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage/README.md) — the restored v0 design documents and the Gen 1 annotation scout report +24. [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md) — the whole-corpus refresh record + +
--- @@ -48,26 +92,26 @@ Every article in the series reinforces one or more clauses of the founding sloga |---|---|---|---|---| | **01** | **[The Spec Is the Prompt Is the Design: Why AI-Native Software Delivery Needs One Graph, Not Another Ticket Tracker](./01-the-spec-is-the-prompt-is-the-design.md)** | **Ready in this PR** | The launch manifesto: why delivery truth fractures across tools, why AI agents hallucinate structure without a graph, and how one primitive (`Spec`), three claims (`declared` / `anchored` / `inferred`), and two check families (conformance + honesty) replace workflow gates. | [`docs/concept/00`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md), [`01`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md), [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md), [`spec:model.core-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/core-model.sdp.md), [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1) | | **02** | **[From Rough Idea to Verified Code: A Hands-On Tour of the Libar Software Delivery Protocol](./02-walkthrough-intent-to-verified-code.md)** | **Ready in this PR** | The step-by-step engineer's tour through `pack:checkout-v1` and the self-hosted engine: authoring `.sdp.md` and `.sdp.gherkin` Specs, clearing readiness floors, binding code with constant-form and `@sdp*` comment-form anchors, generating step/space contracts, binding a `specOracle`, and tripping the honesty checks on purpose. | [`examples/checkout-v1/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md), [`docs/concept/04`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/04-authoring-and-binding.md), [`sdp-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md), [PR #3](https://github.com/libar-dev/software-delivery-protocol/pull/3), [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29) | -| **03** | **[Stop Building 30-Verb CLI Walls for Coding Agents: Scripting the Delivery Graph with `sdp q` and Spec Studio](./03-stop-building-verb-walls-for-coding-agents.md)** | **Ready in this PR** | How coding agents and human reviewers consume the same derived graph: the demand map (`findByConcept`, `byFile`, `blastRadius`), the `sdp q` evaluation sink, growing by recipe instead of CLI verbs, scaling to a 240-Spec adopter with context bundles and corpus rules, and browsing the static HTML Spec Studio. | [`docs/concept/06`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/06-consumers-and-projections.md), [`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md), [`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | +| **03** | **[Stop Building 30-Verb CLI Walls for Coding Agents: Scripting the Delivery Graph with `sdp q` and Spec Studio](./03-stop-building-verb-walls-for-coding-agents.md)** | **Ready in this PR** | How coding agents and human reviewers consume the same derived graph: the demand map (`findByConcept`, `byFile`, `blastRadius`), the `sdp q` evaluation sink, growing by recipe instead of CLI verbs, scaling to a 243-Spec adopter with context bundles and corpus rules, and browsing the static HTML Spec Studio. | [`docs/concept/06`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/06-consumers-and-projections.md), [`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md), [`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | | **04** | **Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition** | Planned | Why settling words (`claim` vs. provenance, `altitude` vs. abstraction, `readiness floor` vs. profile) settled the architecture, and how a falsifiable four-entrant competition (Markdown vs. custom grammar vs. Gherkin fork vs. TSX) ruled `.sdp.md` as default while preserving per-ID universality. | [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md), [`DECISIONS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/DECISIONS.md), [`spec:decisions.carrier-ruling`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/carrier-ruling.sdp.md), [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1), [PR #8](https://github.com/libar-dev/software-delivery-protocol/pull/8), [PR #19](https://github.com/libar-dev/software-delivery-protocol/pull/19) | | **05** | **What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Annotation Reversal, and Honest Code Bindings** | Planned | A candid post-mortem on Gen 1 (`@libar-dev/architect`), how an evidence-based scout report overturned our own early Gen 2 refusal of doc-comment tags, and why distinguishing `satisfies` (collective realization) from `references` ("answers to") solved the stub-coverage lie. | [`docs/lineage/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage/README.md), [`docs/agent-surface/architecture.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/architecture.md), [`spec:decisions.anchor-comment-form`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md), [`spec:decisions.anchor-binding-grain`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md), [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29) | | **06** | **Executable Specifications Without Cucumber Glue: Typed Example Spaces, Generated Contracts, and Expected-Outcome Oracles** | Planned | Why traditional BDD rots at the regex boundary, and how the Protocol compiles `gwt-vocabulary` example spaces and bound points into TypeScript union contracts, deterministic registrars, and `specOracle` functions where `unspecified` is a first-class answer. | [`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md), [`spec:validation.oracle-target-eligibility`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/oracle-target-eligibility.sdp.md), [PR #16](https://github.com/libar-dev/software-delivery-protocol/pull/16), [PR #19](https://github.com/libar-dev/software-delivery-protocol/pull/19) | -| **07** | **Designing a 240-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale** | Planned | How the first external adopter (`libar-platform`) pressured the Protocol when designing six capability Packs months ahead of code: entry addresses (`#design.`, `#question.`, `#rule.`), pinned declarations, 100k-unit context bundles, planned components, and `sdp validate --rules`. | [`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md), [`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md), [`spec:model.planned-architecture`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/planned-architecture.sdp.md), [PR #26](https://github.com/libar-dev/software-delivery-protocol/pull/26), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | -| **08** | **Auditing 276 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh** | Planned | How a solo developer + multi-model agent teams audited the entire self-hosted corpus against the engine: splitting into 13 units, running Fidelity vs. Quality lenses across model families, cross-verifying 595 findings (refuting 59), executing 18 parallel worktree lanes, and pinning the self-hosting oracle. | [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md), [`specs/self-hosting.pack.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/self-hosting.pack.sdp.md), [`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md), [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32) | +| **07** | **Designing a 243-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale** | Planned | How the first external adopter (`libar-platform`) pressured the Protocol when designing six capability Packs months ahead of code: entry addresses (`#design.`, `#question.`, `#rule.`), pinned declarations, 100k-unit context bundles, planned components, and `sdp validate --rules`. | [`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md), [`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md), [`spec:model.planned-architecture`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/planned-architecture.sdp.md), [PR #26](https://github.com/libar-dev/software-delivery-protocol/pull/26), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | +| **08** | **Auditing 258 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh** | Planned | How a solo developer + multi-model agent teams audited the entire self-hosted corpus (258 Specs at PR #32's base, `46f0280`) against the engine: splitting into 13 units, running Fidelity vs. Quality lenses across model families, cross-verifying 595 findings (refuting 59), executing 18 parallel worktree lanes, and pinning the self-hosting oracle. | [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md), [`specs/self-hosting.pack.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/self-hosting.pack.sdp.md), [`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md), [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32) | ### Detailed outlines for Planned Articles (04–08) #### Article 04: *Shared Language Is a Performance Feature* - **Hook:** Why most "AI prompt engineering" is compensating for an unratified domain vocabulary—and how ambiguous words like *status*, *abstraction*, and *provenance* silently corrupt both human design reviews and subagent reasoning. -- **Section 1 — Define first, name last:** The governing rubric from [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md): adopt established industry nouns, reject process state-machine gates (the adopt-the-nouns decision, MD-2), and require every coined word to name a genuine epistemic difference. -- **Section 2 — Seven renames that changed the engine:** Walkthrough of the term ledger table (`claim` replacing provenance, `anchor` replacing marker, `altitude` replacing abstraction, `section` replacing facet, `readiness floor` replacing profile, `Pack` replacing SpecPack). +- **Section 1 — Define first, name last:** The governing rubric from [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md): adopt established industry nouns, reject process state-machine gates (adopt the nouns, reject the gates, MD-2), and keep a coined word only for a genuine differentiator, where the word must name the difference it marks. +- **Section 2 — Six renames that changed the engine:** Walkthrough of the rejected-terms ledger in `CONTEXT.md` (`claim` replacing provenance, `anchor` replacing marker, `altitude` replacing abstraction, `section` replacing facet, `readiness floor` replacing readiness profile, `Pack` replacing SpecPack). - **Section 3 — The four-way carrier competition:** How [PR #8](https://github.com/libar-dev/software-delivery-protocol/pull/8) judged F2 Markdown, C2 custom grammar, Gherkin fork, and TSX on falsifiable scorecards—and why C2's permanent ecosystem tax (parser, formatter, LSP, zero LLM training distribution) lost to `.sdp.md` + per-ID `.sdp.gherkin` and `.sdp.ts` options (the carrier ruling, MD-18, and the Gherkin carrier option, MD-27). #### Article 05: *What We Learned Rebuilding `@libar-dev/architect`* -- **Hook:** Our first-generation tool (`@libar-dev/architect`) worked in production across 349 patterns—and taught us every trap of source-first architecture graphs. +- **Hook:** Our first-generation tool (`@libar-dev/architect`) worked in production, and Gen 1's own self-hosted graph held 349 patterns—and it taught us every trap of source-first architecture graphs. - **Section 1 — The sins we refused to carry over:** Sprawling tag registries, workflow lifecycle gates (`candidate → roadmap → active → completed`), and hand-authored delivery status. - **Section 2 — When git archaeology corrects memory:** How the [annotation scout report](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage) inspected Gen 1's commit history and discovered that the "50 tags shrank to 26" story was formal-spec cleanup, while the real failures were duplicate identities, fixture scan leaks, and overloaded `implements` edges. -- **Section 3 — The annotation reversal ([PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29)):** How the first adopter (`libar-platform`) proved why runtime code that cannot import the Protocol package needed the `@sdp*` comment form (MD-36), optional plural bindings and the non-conferring `references` edge (MD-37), and `role`, `layer`, and `context` on anchors (MD-38). +- **Section 3 — The annotation reversal ([PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29)):** How the first adopter (`libar-platform`) proved why runtime code that cannot import the Protocol package needed the `@sdp*` comment form (MD-36), optional plural bindings and the non-conferring `references` edge (MD-37), and architectural attributes where code is realized (MD-38): a `role` on a code anchor, and a `layer` and `context` on `component:` anchors only. #### Article 06: *Executable Specifications Without Cucumber Glue* - **Hook:** Everyone loves readable scenarios until a one-word phrasing change breaks 40 regex step definitions—or worse, passes silently while testing the wrong value. @@ -75,17 +119,17 @@ Every article in the series reinforces one or more clauses of the founding sloga - **Section 2 — Authored → generated, never the reverse:** How committed adopted registrars (MD-31) wire `createWorld`, `invoke`, `observe`, and `expected` without the generated code ever importing product code. - **Section 3 — The `specOracle` and `unspecified`:** Why expected-outcome semantics belong in a typed oracle beside the tests—and why returning `unspecified` for unmodeled regions surfaces honest coverage gaps without false gates. -#### Article 07: *Designing a 240-Spec Platform Ahead of Its Code* -- **Hook:** What happens when an adopter models a six-layer transactional platform in 243 Specs before writing layers 3 through 6? Every toy assumption in your spec tool breaks—and forces the real design-at-scale loop. +#### Article 07: *Designing a 243-Spec Platform Ahead of Its Code* +- **Hook:** What happens when an adopter models a transactional platform in 243 Specs and states the components of its layers 3 to 6 as designs before their code exists? Every toy assumption in your spec tool breaks—and forces the real design-at-scale loop. - **Section 1 — Fine-grained addressing without sub-Spec explosion:** Entry addresses (`spec:#design.`, `#ui.`, `#question.`, `#rule.`), pinned declarations (recipe 24), and the key-family register (recipe 41). - **Section 2 — Fitting a capability Pack into an LLM's context window:** How recipe 36 (the context bundle) uses a deterministic seven-step cut order and `declarations: "addresses"` to fit `pack:history-view` into 93,806 of 100,000 UTF-16 code units ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). - **Section 3 — Corpus rules and planned architecture:** Running project-specific policies and the architecture walk (recipe 42) via `sdp validate --rules`, and comparing component design Specs against realized `component:` anchors with recipe 43. -#### Article 08: *Auditing 276 Self-Hosted Specs with Cross-Family AI Reviewers* -- **Hook:** When your repository self-hosts 276 Specs, 339 anchors, and 43 recipes, how do you verify that the Specs, the engine, and the docs actually agree? +#### Article 08: *Auditing 258 Self-Hosted Specs with Cross-Family AI Reviewers* +- **Hook:** When your repository self-hosts 258 Specs and 40 recipes (measured at `46f0280`, the merge of PR #31 and the base of [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32)), how do you verify that the Specs, the engine, and the docs actually agree? - **Section 1 — Two lenses, two model families:** The methodology behind [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md) ([PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32)): pairing a Fidelity lens (code + tests vs. Spec rules) with a Quality lens (cross-Spec consistency, vocabulary, readiness honesty), then cross-verifying every finding with the opposite model family. - **Section 2 — What the 595 findings caught (and the 59 they got wrong):** Real bugs caught—from Gherkin reserved tag heads (`@status.ready`) passing as decoration to a collective-vs-per-unit `satisfies` ambiguity—plus why cross-verification was essential to refute false blockers. -- **Section 3 — The distillation boundary and living hygiene:** Using slimming candidates (recipe 39) and the file-level curation report (recipe 40) to keep Specs lean after implementation. +- **Section 3 — The distillation boundary and living hygiene:** Two recipes with different jobs. Slimming candidates (recipe 39) lists the pinned declarations of implemented, verified Specs that bound code may now carry, so a person can slim a Spec past the distillation boundary. The curation report (recipe 40) curates anchors rather than Specs: over the file-level imports `sdp build --imports` writes, it ranks load-bearing files no anchor covers and imports that disagree with anchored `uses` edges. --- @@ -101,7 +145,7 @@ Every article in `docs/articles/` must conform to the repository's ratified lang - Lead decision references with meaning first, code in parentheses: e.g., *the carrier ruling (MD-18)*, *the agent front door (MD-22)*, *the comment form (MD-36)*. 2. **Temporal-guard compliance (`npm run check:temporal`).** - `check-temporal.mjs` sweeps every tracked file outside the historical genres (`plans/`, `reviews/`, `explorations/`, `docs/lineage/`, `docs/concept/DECISIONS.md`). - - Never include raw calendar dates (`YYYY-MM-DD`), numbered plan paths (`plans/`), or session/wave/fold tokens in `docs/articles/*.md`. When linking to a numbered plan or dated lineage file inside `docs/articles/*.md`, either link to its pull request (`.../pull/33`), its parent directory (`.../tree/5a0b22c.../plans`), or percent-encode the leading digit in the URL path (`.../plans/%345-design-at-the-adopters-scale.md`). + - Never include raw calendar dates (`YYYY-MM-DD`), numbered plan paths (`plans/`), or session/wave/fold tokens in `docs/articles/*.md`. To point a reader at a numbered plan or a dated lineage file, link to the pull request that carried it (`.../pull/33`) or to its parent directory (`.../tree/5a0b22c.../docs/lineage`), never to the file itself and never through an encoded path that only hides the token from the check. 3. **Every corpus count is derived, dated to a commit, and paired with its command.** - Never quote a number without naming the commit it was measured at and giving the reader the `sdp validate` or `sdp q` command to re-derive it. @@ -111,11 +155,13 @@ Every article in `docs/articles/` must conform to the repository's ratified lang Because this pull request stays open and is periodically updated as `main` evolves, refreshing the series after merging or rebasing onto a new `main` commit takes four deterministic steps: -### Step 1: Build and run the green gate +### Step 1: Install and build ```sh -npm ci && npm run build +npm ci # installs and builds dist/, including dist/recipes/ ``` +The commands below use `npm run --silent sdp:q -- …`, which adds the corpus's three exclusions; `pnpm --silent sdp:q …` is equivalent where pnpm is installed. Never run a bare `sdp` in this checkout. + ### Step 2: Measure the design diff from the last pinned commit Generate the base graph at the previously pinned commit (currently `5a0b22c2d9ead98c900b60ed6209342fce2cbef6`) in a temporary worktree and run recipe 35 (the design diff) to see every Spec, Pack, section, entry key, and binding that changed: @@ -124,20 +170,22 @@ BASE_SHA="5a0b22c2d9ead98c900b60ed6209342fce2cbef6" git worktree add --detach .sdp-base "$BASE_SHA" && ln -s "$PWD/node_modules" .sdp-base/node_modules npm --prefix .sdp-base run build node .sdp-base/dist/cli/sdp.js build .sdp-base --exclude explorations --exclude examples --exclude test/fixtures/import/parity -pnpm --silent sdp:q "$(cat dist/recipes/35-the-design-diff.js)" --params base=@.sdp-base/generated/graph.json --params '{"detail":true}' --json +npm run --silent sdp:q -- "$(cat dist/recipes/35-the-design-diff.js)" --params base=@.sdp-base/generated/graph.json --params '{"detail":true}' --json git worktree remove --force .sdp-base ``` ### Step 3: Re-derive the headline corpus metrics at the new `HEAD` ```sh -# 1. Overall graph, readiness, kind, and delivery-fact census -pnpm --silent sdp:q 'return { +# 1. Overall graph, node-type, claim, readiness, kind, and delivery-fact census +npm run --silent sdp:q -- 'return { commit: "HEAD", schemaVersion: graph.schemaVersion, specs: g.specs().length, packs: g.packs().length, nodes: graph.nodes.length, + nodeTypes: graph.nodes.reduce((a, n) => ((a[n.nodeType] = (a[n.nodeType] || 0) + 1), a), {}), edges: graph.edges.length, + edgeClaims: graph.edges.reduce((a, e) => ((a[e.claim] = (a[e.claim] || 0) + 1), a), {}), locations: graph.locations.length, readiness: g.specs().reduce((a, s) => ((a[s.statedReadiness] = (a[s.statedReadiness] || 0) + 1), a), {}), kinds: g.specs().reduce((a, s) => ((a[s.specKind] = (a[s.specKind] || 0) + 1), a), {}), @@ -149,11 +197,15 @@ pnpm --silent sdp:q 'return { }' --json # 2. Operational build backlog (recipe 1) and drift alarm (recipe 2) -pnpm --silent sdp:q "$(cat dist/recipes/01-the-build-backlog.js)" --json -pnpm --silent sdp:q "$(cat dist/recipes/02-the-drift-alarm.js)" --json +npm run --silent sdp:q -- "$(cat dist/recipes/01-the-build-backlog.js)" --json +npm run --silent sdp:q -- "$(cat dist/recipes/02-the-drift-alarm.js)" --json + +# 3. Open-question register (recipe 20) and design backlog (recipe 33) +npm run --silent sdp:q -- "$(cat dist/recipes/20-open-question-register.js)" --json +npm run --silent sdp:q -- "$(cat dist/recipes/33-the-design-backlog.js)" --json -# 3. Pack design summaries across all Packs (recipe 29) -pnpm --silent sdp:q ' +# 4. Pack design summaries across all Packs (recipe 29) +npm run --silent sdp:q -- ' const out = []; for (const p of g.packs()) { const r = await (async (params) => { '"$(cat dist/recipes/29-pack-design.js)"' })({ pack: p.id }); @@ -171,7 +223,7 @@ Measured at `5a0b22c2d9ead98c900b60ed6209342fce2cbef6` (re-run the commands abov | Schema version | `0.10.0` | `graph.schemaVersion` | | Authored `Spec`s | `276` (`245 ready`, `17 defined`, `3 scoped`, `11 idea`) | `g.specs()` | | Authored `Pack`s | `6` (`self-hosting-v1`, `design-management-v1`, `spec-studio-v1`, `corpus-checks-v1`, `design-iteration-v1`, `obsidian-vault-v1`) | `g.packs()` | -| Total nodes / edges / locations | `621` nodes (`276 Primitive`, `6 Pack`, `160 CodeNode`, `179 Anchor`) · `1,624` edges (`1,063 declared`, `561 anchored`) · `1,774` locations | `graph.nodes`, `graph.edges`, `graph.locations` | +| Total nodes / edges / locations | `621` nodes (`276 Primitive`, `6 Pack`, `160 CodeNode`, `179 Anchor`) · `1,624` edges (`1,063 declared`, `561 anchored`) · `1,774` locations | `nodes` and `nodeTypes`, `edges` and `edgeClaims`, `locations` in census body 1 | | Delivery facts | `94 implemented` · `205 has-verifier` · `0 observed` | `s.deliveryFacts` | | Operational build backlog | `0` (`110` ready examples and `41` ready decisions audited and excluded) | Recipe 1 (`01-the-build-backlog.js`) | | Drift alarm (`implemented ∧ ¬ready`) | `0` | Recipe 2 (`02-the-drift-alarm.js`) | @@ -180,3 +232,5 @@ Measured at `5a0b22c2d9ead98c900b60ed6209342fce2cbef6` (re-run the commands abov | Shipped recipes | `43` runnable bodies under `dist/recipes/` | `docs/agent-surface/recipes.md` | After updating any changed counts or commit links, stage the files and run `npm run check` to verify that `check:temporal`, `lint`, `format:check`, `test`, and `preflight` all exit `0`. + +That gate covers less of this directory than it seems. `check:temporal` does sweep `docs/articles/` for dates, numbered plan paths, and session tokens, but `.prettierignore` excludes `docs/**`, so `format:check` never reads these articles, and `lint` reads only code. Nothing in the gate catches an unbalanced code fence or a broken link here. Before publishing, check both yourself, with a throwaway script you do not commit: every fence must close with a backtick run at least as long as the one that opened it (a Markdown example that contains its own fences needs a four-backtick outer fence), and every relative link and every commit-pinned GitHub link must resolve. From 7f8677e114c31d3ef59a8b9d78e3bb642e3f4968 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 00:49:47 +0000 Subject: [PATCH 03/36] docs(articles): pin sibling-article links to 58a8202 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Darko Mijić --- docs/articles/01-the-spec-is-the-prompt-is-the-design.md | 2 +- docs/articles/02-walkthrough-intent-to-verified-code.md | 4 ++-- .../articles/03-stop-building-verb-walls-for-coding-agents.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md index b93a64572..c91532464 100644 --- a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -342,7 +342,7 @@ The three `--exclude` flags keep the exploration exhibits, the separate worked e ## Up Next in the Series -In **Part 2: From Rough Idea to Verified Code** ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/ARTICLES_COMMIT/docs/articles/02-walkthrough-intent-to-verified-code.md)), we open `examples/checkout-v1` and the Protocol's own source code to walk the complete engineering loop: authoring `.sdp.md` carriers (and where `.sdp.gherkin` fits), binding code with constant-form and `@sdp*` comment-form anchors, generating typed step and space contracts, writing an expected-outcome `specOracle`, and deliberately breaking the graph to watch the extraction, conformance, honesty, and contract checks fire. +In **Part 2: From Rough Idea to Verified Code** ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/02-walkthrough-intent-to-verified-code.md)), we open `examples/checkout-v1` and the Protocol's own source code to walk the complete engineering loop: authoring `.sdp.md` carriers (and where `.sdp.gherkin` fits), binding code with constant-form and `@sdp*` comment-form anchors, generating typed step and space contracts, writing an expected-outcome `specOracle`, and deliberately breaking the graph to watch the extraction, conformance, honesty, and contract checks fire. --- diff --git a/docs/articles/02-walkthrough-intent-to-verified-code.md b/docs/articles/02-walkthrough-intent-to-verified-code.md index 51fd9fb88..493f8e8d7 100644 --- a/docs/articles/02-walkthrough-intent-to-verified-code.md +++ b/docs/articles/02-walkthrough-intent-to-verified-code.md @@ -2,7 +2,7 @@ *Part 2 of 8 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* -In Part 1 ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/ARTICLES_COMMIT/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), we introduced the core thesis of the **Libar Software Delivery Protocol**: model the delivery lifecycle as instances of one enrichable primitive (`Spec`), bind code and tests with source `anchor`s, derive **one regenerable graph**, and check **conformance and honesty** in CI. +In Part 1 ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), we introduced the core thesis of the **Libar Software Delivery Protocol**: model the delivery lifecycle as instances of one enrichable primitive (`Spec`), bind code and tests with source `anchor`s, derive **one regenerable graph**, and check **conformance and honesty** in CI. That sounds clean in principle. What does it actually look like in a text editor and a terminal? @@ -338,7 +338,7 @@ The fastest way to appreciate the checks is to trip them in `examples/checkout-v ## Up Next in the Series -We have seen how Specs and anchors enter the repo and how the extractor and validators keep them honest. In **Part 3: Stop Building 30-Verb CLI Walls for Coding Agents** ([read it here](./03-stop-building-verb-walls-for-coding-agents.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/ARTICLES_COMMIT/docs/articles/03-stop-building-verb-walls-for-coding-agents.md)), we look at what happens *downstream* of the graph: how AI coding agents script `sdp q` across 43 runnable recipes, how design passes fit a 243-Spec adopter corpus into context bundles, and how humans review the same graph in the static HTML **Spec Studio**. +We have seen how Specs and anchors enter the repo and how the extractor and validators keep them honest. In **Part 3: Stop Building 30-Verb CLI Walls for Coding Agents** ([read it here](./03-stop-building-verb-walls-for-coding-agents.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/03-stop-building-verb-walls-for-coding-agents.md)), we look at what happens *downstream* of the graph: how AI coding agents script `sdp q` across 43 runnable recipes, how design passes fit a 243-Spec adopter corpus into context bundles, and how humans review the same graph in the static HTML **Spec Studio**. --- diff --git a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md index 1baaf6282..b700930f6 100644 --- a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md +++ b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md @@ -231,7 +231,7 @@ Never invoke a bare `sdp` inside this checkout: `npm run --silent sdp -- …` ru ## Where the Series Goes Next -This post closes the opening trio. **Part 1**, *The Spec Is the Prompt Is the Design* ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/ARTICLES_COMMIT/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), made the case for one graph. **Part 2**, *From Rough Idea to Verified Code* ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/ARTICLES_COMMIT/docs/articles/02-walkthrough-intent-to-verified-code.md)), walked the loop from a Spec to a verified test. The next five installments tell specific stories from the Protocol's construction: +This post closes the opening trio. **Part 1**, *The Spec Is the Prompt Is the Design* ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), made the case for one graph. **Part 2**, *From Rough Idea to Verified Code* ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/02-walkthrough-intent-to-verified-code.md)), walked the loop from a Spec to a verified test. The next five installments tell specific stories from the Protocol's construction: - **Part 4:** *Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition* - **Part 5:** *What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Annotation Reversal, and Honest Code Bindings* From 48d8f495959c2a8adc6ffab4069d4ab60afbf716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Thu, 22 Oct 2026 03:12:39 +0200 Subject: [PATCH 04/36] docs(articles): fold the parallel six-reviewer findings Verify each finding of the parallel review of e1d2f59 against HEAD and the engine, and fix the ones the earlier fold left standing: series numbering, verbatim excerpts with elision markers, the registrar's two checks, the comment-form story, collective satisfies, break-it results, registry names for decisions, the playbook's base worktree outside the repository, the census query behind each baseline row, and the planned outlines' attributions and figures. Co-Authored-By: Claude Opus 5.5 --- ...01-the-spec-is-the-prompt-is-the-design.md | 26 ++--- .../02-walkthrough-intent-to-verified-code.md | 61 +++++++----- ...p-building-verb-walls-for-coding-agents.md | 30 +++--- docs/articles/README.md | 95 ++++++++++--------- 4 files changed, 117 insertions(+), 95 deletions(-) diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md index c91532464..e8d6ebc6a 100644 --- a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -1,10 +1,10 @@ # The Spec Is the Prompt Is the Design: Why AI-Native Software Delivery Needs One Graph, Not Another Ticket Tracker -*Part 1 of 8 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* +*Part 1 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* Every software team knows the decay curve of documentation. -At the start of a project, intent is sharp. You write an architecture doc, sketch a few sequence diagrams, record a couple of ADRs, file tickets in your issue tracker, and write tests in your repo. Six weeks later, a customer edge case forces a design change during a pull request. The code changes. The test changes. The architecture doc, the ADR, and the ticket do not. Within a quarter, your documentation is a museum of what you *meant* to build three sprints ago. +At the start of a project, intent is sharp. You write an architecture doc, sketch a few sequence diagrams, record a couple of architecture decision records (ADRs), file tickets in your issue tracker, and write tests in your repo. Six weeks later, a customer edge case forces a design change during a pull request. The code changes. The test changes. The architecture doc, the ADR, and the ticket do not. Within a quarter, your documentation is a museum of what you *meant* to build three sprints ago. Now drop AI coding agents into that repository. @@ -22,7 +22,7 @@ This article takes those four clauses in turn, then measures the Protocol agains ## 1. A Second Take, Front-Loaded with First Principles -The Protocol is not our first attempt at this problem. Its predecessor, [`@libar-dev/architect`](https://github.com/libar-dev/architect), ran in production; its own self-hosted graph held 349 patterns (227 from TypeScript, 122 from Gherkin), as the [annotation scout report in `docs/lineage/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage) measured. It proved three things that worked: keeping annotations in source beside the code, deriving a single immutable graph as the read model for both documentation and agents, and giving agents a scriptable graph handle instead of raw files. +The Protocol is not our first attempt at this problem. Its predecessor, [`@libar-dev/architect`](https://github.com/libar-dev/architect), ran in production and taught us where the problem bites. At the commit we later audited, its own repository's graph held 349 patterns (227 from TypeScript, 122 from Gherkin), as the lineage scout report in [`docs/lineage/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage) measured ([PR #28](https://github.com/libar-dev/software-delivery-protocol/pull/28) carried it). It proved three things that worked: keeping design metadata in source, beside the code, deriving a single immutable graph as the read model for both documentation and agents, and giving agents a scriptable graph handle instead of raw files. It also taught us what fails in production: @@ -30,13 +30,13 @@ It also taught us what fails in production: - **Process state-machine gates.** Encoding ticket statuses (`candidate → roadmap → active → completed`) inside source files turns your compiler into a workflow bureaucracy. - **Hand-authored delivery status.** If a human or an agent types `status: completed` into a spec file, that string will eventually lie. -We built the Libar Software Delivery Protocol ([PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1)) as a clean-sheet second generation—zero backward compatibility, zero old sins carried over—around **three meta-levels** ([the executable meta-model decision, MD-1](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/executable-meta-model.sdp.md)): +We designed the Libar Software Delivery Protocol as a clean-sheet second generation (its concept foundation landed in [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1), the engine in [PR #2](https://github.com/libar-dev/software-delivery-protocol/pull/2))—zero backward compatibility, zero old sins carried over—around **three meta-levels** ([the executable meta-model decision, MD-1](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/executable-meta-model.sdp.md)): | Meta-level | What lives here | Role | |---|---|---| | **1. The Protocol** | The `Spec` primitive, descriptors, relations, and validators *as typed code* | Defines the conformance contract | | **2. The Authored Model** | A project's `Spec`s, `Pack`s, and source `anchor`s committed in the repo | Conforms to the contract (checked in CI, never workflow-gated) | -| **3. Derived Facts** | Machine truth (delivery facts such as `implemented` and `has-verifier`) and projections | Computed from the graph, **never authored by hand** | +| **3. Derived Facts** | Machine truth (delivery facts such as `implemented` and `has-verifier`) | Computed from the graph, **never authored by hand** | And we bound the entire system with **two permanent honesty guardrails** ([`00-vision-scope-and-mvp-boundary.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md)): @@ -223,7 +223,7 @@ Every route to hand-authoring a delivery fact is refused, each with its own find Look at the two payoff queries that fall out of keeping those coordinates separate. Each is a filter over the reader's `g.specs()`; the shipped recipes, runnable `sdp q` bodies cataloged in [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md) and each executed as written by the test suite, wrap these expressions with counts and audit rows. -1. **The operational build backlog (recipe 1):** Specs whose design is settled, whose typed dependencies are at least `defined`, and whose author has stated `ready`, waiting to be built. Ready `example` and `decision` Specs are excluded and counted, because examples carry verification evidence and decision records are ratified rather than built (the example realization posture, MD-24, and the decision readiness posture, MD-26). +1. **The operational build backlog (recipe 1):** Specs whose author has stated `ready` and that no code anchor yet realizes, waiting to be built. The readiness floor already admits `ready` only once every typed dependency states at least `defined`, so the recipe does not filter on dependencies itself. Ready `example` and `decision` Specs are excluded and counted, because examples carry verification evidence and decision records are ratified rather than built (the example realization posture, MD-24, and the decision readiness posture, MD-26). ```js g.specs().filter((s) => @@ -266,7 +266,7 @@ And when an adopting team has project-specific architectural rules—for instanc ## 6. Self-Hosting by the Numbers -The Protocol does not have a separate PRD. It **self-hosts** ([`spec:protocol.self-hosting`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/protocol/self-hosting.sdp.md)): every rule of its model, extractor, validators, projections, and agent surface is authored in its own `.sdp.md` and `.sdp.gherkin` carriers under [`specs/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs), bound to its own TypeScript engine under [`src/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src) and test suite under [`test/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/test), and certified on every commit by `npm run check`. +The Protocol does not have a separate PRD. It **self-hosts** ([`spec:protocol.self-hosting`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/protocol/self-hosting.sdp.md)): every rule of its model, extractor, validators, projections, and agent surface is authored in its own `.sdp.md` and `.sdp.gherkin` carriers under [`specs/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs), bound to its own TypeScript engine under [`src/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src) and test suite under [`test/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/test), and checked by `npm run check` in CI on every push and pull request. At commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (re-run the queries below rather than quoting these numbers across commits), the Protocol's own self-hosting graph measures: @@ -279,21 +279,21 @@ At commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tr | **Derived graph size** | **621 nodes · 1,624 edges** | `1,063 declared` edges · `561 anchored` edges · `1,774` entry and relation source locations (schema `0.10.0`) | | **Derived delivery facts** | **94 `implemented` · 205 `has-verifier`** | Operational build backlog (recipe 1): **0** · Drift alarm (recipe 2): **0** · Design backlog (recipe 33): **17** | -Re-derive the breakdowns with: +Re-derive the table (except the three recipe counts, which are recipes 1, 2, and 33) with: ```sh -npm run --silent sdp:q -- 'const tally = (xs, f) => xs.reduce((a, x) => ((a[f(x)] = (a[f(x)] || 0) + 1), a), {}); return { kinds: tally(g.specs(), (s) => s.specKind), readiness: tally(g.specs(), (s) => s.statedReadiness), nodeTypes: tally(graph.nodes, (n) => n.nodeType), bindingNamespaces: tally(graph.nodes.filter((n) => n.nodeType === "CodeNode" || n.nodeType === "Anchor"), (n) => n.id.split(":")[0]), edgeClaims: tally(graph.edges, (e) => e.claim), locations: graph.locations.length }' --json +npm run --silent sdp:q -- 'const tally = (xs, f) => xs.reduce((a, x) => ((a[f(x)] = (a[f(x)] || 0) + 1), a), {}); return { kinds: tally(g.specs(), (s) => s.specKind), readiness: tally(g.specs(), (s) => s.statedReadiness), nodeTypes: tally(graph.nodes, (n) => n.nodeType), bindingNamespaces: tally(graph.nodes.filter((n) => n.nodeType === "CodeNode" || n.nodeType === "Anchor"), (n) => n.id.split(":")[0]), edgeClaims: tally(graph.edges, (e) => e.claim), locations: graph.locations.length, schema: graph.schemaVersion, packs: graph.nodes.filter((n) => n.nodeType === "Pack").map((p) => [p.id, p.members.length]), layers: tally(graph.nodes.filter((n) => n.id.startsWith("component:")), (n) => n.layer), implemented: g.specs().filter((s) => s.deliveryFacts.includes("implemented")).length, hasVerifier: g.specs().filter((s) => s.deliveryFacts.includes("has-verifier")).length }' --json ``` The self-hosting `validate` run prints `0 errors · 6 warnings`, and all six are intentional, pinned by the self-hosting oracle: five `conformance/prose-mentions` warnings, where one Spec names another (three times in an open question's `links:` field, twice in rule text) without a declared relation between them, and one `honesty/gaps` warning on `spec:model.spec-sections`, an owner-ratified `ready` Spec with no resolving verifier. -Meanwhile, the first adopter, [`libar-platform`](https://github.com/libar-dev/libar-platform) (a separate repository in the `libar-dev` organization), models a Convex transactional domain platform as **243 Specs across six Packs**, most of it designed ahead of its code. It runs design passes whose reading is composed within a 100,000-UTF-16-unit context bundle, checks its own architecture and policy rules through `sdp validate --rules`, and publishes 251 static **Spec Studio** pages. The Protocol work that made that possible is [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); the adopter's side is [`libar-platform#13`](https://github.com/libar-dev/libar-platform/pull/13). +Meanwhile, the first adopter, [`libar-platform`](https://github.com/libar-dev/libar-platform) (a separate repository in the `libar-dev` organization), models a Convex transactional domain platform as **243 Specs across six Packs**, most of it designed ahead of its code. It runs design passes whose reading is composed within a 100,000-UTF-16-unit context bundle, checks its own architecture and policy rules through `sdp validate --rules`, and publishes 251 static **Spec Studio** pages. Those adopter figures were measured at the adopter's own pin and recorded in [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33), the Protocol work that made them possible; the adopter's side is [`libar-platform#13`](https://github.com/libar-dev/libar-platform/pull/13). The Spec Studio renders the same graph for people. Here is the Protocol's own `pack:corpus-checks-v1`, the Pack that carries corpus rules: ![The Spec Studio Pack page for pack:corpus-checks-v1: a plain-language summary, the Pack's framing, and one collapsed card per member](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/protocol-pack.png) -*The Pack page for `pack:corpus-checks-v1`, published by `sdp studio` and opened from `file://` with no network. It leads with a summary the graph computes, then the Pack's framing, then one card per member with its rung, floor, questions, and bindings. Captured for [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33).* +*The Pack page for `pack:corpus-checks-v1`, published by `sdp studio` and opened from `file://` with no network. It leads with a summary the graph computes, then the Pack's framing, then one card per member with its rung, floor, questions, and bindings. Captured at commit `30f196a` for [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33), so its counts can differ slightly from the table above.* --- @@ -317,7 +317,7 @@ And where it stands today: ## 8. Try It Yourself in 60 Seconds -Clone the repository at the commit these numbers were measured at, and query the self-hosting graph through `sdp q`—the evaluation sink that derives the graph in memory and runs any async JavaScript body you give it: +With Node 20 or later, clone the repository at the commit these numbers were measured at, and query the self-hosting graph through `sdp q`—the evaluation sink that derives the graph in process and runs any async JavaScript body you give it: ```sh git clone https://github.com/libar-dev/software-delivery-protocol.git @@ -346,4 +346,4 @@ In **Part 2: From Rough Idea to Verified Code** ([read it here](./02-walkthrough --- -*About this article: Part 1 of 8 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every measurement are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the numbers at a later commit.* +*About this article: Part 1 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every measurement are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the numbers at a later commit.* diff --git a/docs/articles/02-walkthrough-intent-to-verified-code.md b/docs/articles/02-walkthrough-intent-to-verified-code.md index 493f8e8d7..00efec3af 100644 --- a/docs/articles/02-walkthrough-intent-to-verified-code.md +++ b/docs/articles/02-walkthrough-intent-to-verified-code.md @@ -1,6 +1,6 @@ # From Rough Idea to Verified Code: A Hands-On Tour of the Libar Software Delivery Protocol -*Part 2 of 8 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* +*Part 2 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* In Part 1 ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), we introduced the core thesis of the **Libar Software Delivery Protocol**: model the delivery lifecycle as instances of one enrichable primitive (`Spec`), bind code and tests with source `anchor`s, derive **one regenerable graph**, and check **conformance and honesty** in CI. @@ -61,6 +61,8 @@ npm run --silent sdp -- build examples/checkout-v1 ```text 11 specs · 1 packs · 5 anchors → 17 nodes · 32 edges (0 errors, 0 warnings) +Wrote /examples/checkout-v1/generated/graph.json +Wrote /examples/checkout-v1/generated/contracts (3 modules) ``` ### Your first graph question: the drift alarm @@ -167,9 +169,9 @@ And the order contains the original cart lines - The returned total matches the authored total — the spec's value, never the test's. ```` -Under [the point-per-example law (MD-17)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/point-per-example.sdp.md), each `example` Spec binds one concrete point in its parent's example space (`n: 2, q: 1, price: 50, availability: "in stock" → total: 100`). Under the **concreteness law** ([`spec:validation.kind-evidence`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/kind-evidence.sdp.md)), if any slot used in a step is left unbound (for instance, `{n}` instead of `{n: 2}`), the example is capped below `defined` and `sdp build` withholds its step contract. +Under [the point-per-example law (MD-17)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/point-per-example.sdp.md), each `example` Spec binds one concrete point in its parent's example space (`n: 2, q: 1, price: 50, availability: "in stock" → total: 100`). Under the **concreteness law** ([`spec:validation.kind-evidence`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/kind-evidence.sdp.md)), if any slot used in a step is left unbound (for instance, `{n}` instead of `{n: 2}`), the example is capped below `defined` and `sdp build` withholds its step contract and its registrar. -> **Prefer Gherkin for behavior and example Specs?** Under [the Gherkin carrier option (MD-27)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/gherkin-carrier-option.sdp.md) and [the `.sdp.gherkin` extension (MD-28)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/sdp-gherkin-extension.sdp.md), any `behavior` or `example` Spec ID can instead use a graph-aware `.sdp.gherkin` file as its one canonical surface. For the content both carriers can express, it produces the exact same graph nodes and generated contracts. The Gherkin carrier has no form for open questions, their fields, or relation reasons, so a Spec that needs one stays in Markdown ([`spec:carrier.gherkin-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/gherkin-authoring.sdp.md)). The Protocol's own reader Spec, [`spec:consumers.reader`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/reader.sdp.gherkin), is authored this way. +> **Prefer Gherkin for behavior and example Specs?** Under [the Gherkin carrier option (MD-27)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/gherkin-carrier-option.sdp.md) and [the `.sdp.gherkin` extension (MD-28)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/sdp-gherkin-extension.sdp.md), any `behavior` or `example` Spec ID can instead use a graph-aware `.sdp.gherkin` file as its one canonical surface. A parity example proves that a Markdown twin and its Gherkin twin derive equal graphs and equal contracts ([`spec:carrier.gherkin-authoring.contract-parity`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/gherkin-authoring.contract-parity.sdp.md)). The Gherkin carrier has no form for open questions, their fields, or relation reasons, so a Spec that needs one stays in Markdown ([`spec:carrier.gherkin-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/gherkin-authoring.sdp.md)). The Protocol's own reader Spec, [`spec:consumers.reader`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/reader.sdp.gherkin), is authored this way. --- @@ -182,17 +184,21 @@ An anchor is a human-authored binding assertion in a `.ts` or `.tsx` file. It ne Both rest on **Principle P6, identity is the universal join key** ([founding principles](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md)). Specs and code link by string IDs such as `"spec:orders.create-order"`, never by TypeScript imports between code and Specs. That is why a refactor that moves or renames files never breaks the trace, why the generated contracts never import product code, and why the comment form below needs no import at all. ### Representation A: The Constant Form -When your code may import `@libar-dev/software-delivery-protocol` (or its zero-dependency subpath `@libar-dev/software-delivery-protocol/anchors`), you write a top-level constant, as in [`examples/checkout-v1/src/orders/create-order.use-case.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/src/orders/create-order.use-case.ts): +When your code may import `@libar-dev/software-delivery-protocol` (or its zero-dependency subpath `@libar-dev/software-delivery-protocol/anchors`, which loads no `node:*` module), you write a top-level constant, as in [`examples/checkout-v1/src/orders/create-order.use-case.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/src/orders/create-order.use-case.ts#L23-L35): ```ts import { codeAnchor, codeAnchorId, ref } from "@libar-dev/software-delivery-protocol"; +// … (lines 3–21: the cart and order types) … + export const createOrderUseCaseAnchor = codeAnchor({ id: codeAnchorId("impl:orders.create-order-use-case"), label: "createOrderFromCart", satisfies: ref("spec:orders.create-order"), }); +// A second binding in the same file: anchors bind per spec, never per file (MD-8) — the total +// reduction below is where the order-total rule is implemented. export const orderTotalRuleAnchor = codeAnchor({ id: codeAnchorId("impl:orders.order-total"), label: "cart-math order total", @@ -201,9 +207,9 @@ export const orderTotalRuleAnchor = codeAnchor({ ``` ### Representation B: The Comment Form (Zero Runtime Imports) -What if your production runtime is strict about zero external imports? In [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29), the first adopter's experience led to [the comment-form ruling (MD-36)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md), [the binding-grain ruling (MD-37)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md), and [the architectural-annotation ruling (MD-38)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/architectural-annotation.sdp.md). +What if your runtime code cannot import the package? The first adopter's could not, so every one of its code anchors sat in a test or a harness, and asking the graph about a source file returned nothing. In [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29), that experience led to three rulings: [the comment form (MD-36)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md), [bindings are optional, plural, and may reference a design (MD-37)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md), and [architectural significance is annotated where it is realized (MD-38)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/architectural-annotation.sdp.md). -You can write a top-level `/** … */` comment using reserved camelCase `@sdp*` tags with **zero imports**. Here is a real anchor from the Protocol's own census projection in [`src/projections/census.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src/projections/census.ts): +You can write a top-level `/** … */` comment using reserved camelCase `@sdp*` tags with **zero imports**. Here is a real anchor from the Protocol's own census projection in [`src/projections/census.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src/projections/census.ts#L21-L28): ```ts /** @@ -216,12 +222,12 @@ You can write a top-level `/** … */` comment using reserved camelCase `@sdp*` */ ``` -Look closely at the distinction between `@sdpSatisfies` and `@sdpReferences` (the binding-grain ruling, [MD-37](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md)): +Look closely at the distinction between `@sdpSatisfies` and `@sdpReferences` (the ruling that [bindings are optional, plural, and may reference a design, MD-37](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md)): -- **`@sdpSatisfies` (`satisfies`):** This unit (together with any sibling units satisfying the same Spec) **realizes** the Spec's promises. It confers the `implemented` delivery fact on `spec:consumers.census-page`. +- **`@sdpSatisfies` (`satisfies`):** This unit **realizes** the Spec's promises together with every other unit that satisfies the same Spec; here a second unit, the `sdp census` command in [`src/cli/census-command.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src/cli/census-command.ts#L64-L70), completes the set. The set confers the `implemented` delivery fact on `spec:consumers.census-page`. - **`@sdpReferences` (`references`):** This unit **answers to** a design or decision (`spec:decisions.architectural-annotation`) without claiming to realize it—or realizes only part of a Spec whose remaining promises are not yet built. It puts a `references` edge in the graph so `blastRadius` and impact queries traverse it, **without conferring `implemented` and without tripping the drift alarm**. -If you write a stub or follow a decision record, you use `references`. You never have to lie with `satisfies` just to get traceability. +If you write a stub or follow a decision record, you use `references`; code never satisfies a decision Spec, because a ruling is not something code realizes. You never have to lie with `satisfies` just to get traceability. --- @@ -229,10 +235,12 @@ If you write a stub or follow a decision record, you use `references`. You never How do we verify `spec:orders.create-order.valid-cart` so that if someone changes `{total: 100}` to `{total: 120}` in the Markdown Spec, the test suite fails immediately—without brittle Cucumber regexes? -When `npm run --silent sdp -- build examples/checkout-v1` runs, it emits **derived TypeScript contracts** under `generated/contracts/` (one step contract per example and one space contract for the parent) and a **registrar** beside the test ([`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md)). Because the authored test imports that registrar, it is committed and byte-checked rather than ignored ([the adopted-registrars ruling, MD-31](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/adopted-registrars-committed.sdp.md)). +When `npm run --silent sdp -- build examples/checkout-v1` runs, it emits **derived TypeScript contracts** under `generated/contracts/` (one step contract per example and one space contract for the parent) and a **registrar** beside the test ([`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md)). Because the authored test imports that registrar, it is committed and byte-checked rather than ignored (the ruling that [adopted registrars are committed, MD-31](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/adopted-registrars-committed.sdp.md)). + +Three files meet at test time. Only the first is generated; the oracle and the test are written by hand: 1. **The Space Contract (`orders.create-order.space.ts`):** Exports `CreateOrderConditions` (`{ n: number; q: number; price: number; availability: "in stock" | "out of stock" }`), every child's bound point, and the `CreateOrderOutcome` discriminated union derived from the parent's `Then` steps. -2. **The Expected-Outcome Oracle ([`test/orders/create-order.oracle.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.oracle.ts)):** Models the expected outcome across the entire example space, bound by `specOracle`. Where the Spec states no outcome, it returns `unspecified` rather than guessing: +2. **The Expected-Outcome Oracle ([`test/orders/create-order.oracle.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.oracle.ts#L12-L48)):** Models the expected outcome across the entire example space, bound by `specOracle`. Where the Spec states no outcome, it returns `unspecified` rather than guessing: ```ts export const createOrderOracle = specOracle({ @@ -241,6 +249,7 @@ export const createOrderOracle = specOracle({ models: ref("spec:orders.create-order"), }); +// … (lines 18–21: a comment on the generated input and return types) … export function expected(conditions: Partial): CreateOrderOutcome { if ( conditions.n === undefined || @@ -250,15 +259,19 @@ export function expected(conditions: Partial): CreateOrde ) { return unspecified; } + if (conditions.n === 0) { return { kind: "order creation is rejected because {reason}", reason: "empty cart" }; } + if (conditions.availability === "out of stock") { return { kind: "order creation is rejected because {reason}", reason: "out of stock" }; } + if (conditions.q <= 0 || conditions.price <= 0) { return unspecified; } + return { kind: "an order is created with total {total}", total: conditions.n * conditions.q * conditions.price, @@ -266,7 +279,7 @@ export function expected(conditions: Partial): CreateOrde } ``` -3. **The Bound Test ([`test/orders/create-order.valid-cart.test.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.valid-cart.test.ts)):** Anchors `test:orders.create-order.valid-cart` and activates the generated registrar: +3. **The Bound Test ([`test/orders/create-order.valid-cart.test.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.valid-cart.test.ts#L13-L58)):** Anchors `test:orders.create-order.valid-cart` and activates the generated registrar: ```ts export const createOrderValidCartTest = specTest({ @@ -275,6 +288,8 @@ export const createOrderValidCartTest = specTest({ verifies: ref("spec:orders.create-order.valid-cart"), }); +// … (lines 19–56: the World type and the four adapters listed below) … + registerValidCart({ createWorld, invoke, observe, expected, assertions }); ``` @@ -286,12 +301,12 @@ The registrar takes five adapter functions, all defined in that test file except - `expected(point)` is the oracle above. - `assertions(world)` adds checks the outcome union does not carry, such as the stable order id. -The generated registrar compares three things at runtime: -- **The Spec's authored outcome** (`{ total: 100 }` from the `.sdp.md` file), -- **The Oracle's expected outcome** (`2 * 1 * 50 = 100` from `expected(point)`), and -- **The System Under Test's observed outcome** (`world.order.total` from `createOrderFromCart`). +At the matching `Then` step, the generated registrar makes two checks, both against the oracle ([`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md)): + +1. **The Spec's authored values against the oracle:** `{ total: 100 }` from the `.sdp.md` file against `2 * 1 * 50 = 100` from `expected(point)`. +2. **The observed outcome against the oracle:** `world.order.total` from `createOrderFromCart`, read through `observe(world)`, against the same oracle outcome. -If the Markdown Spec, the oracle, or the implementation drifts from either of the other two, the test fails and names the exact step that diverged. +If the Markdown Spec, the oracle, or the implementation drifts, one of the two checks fails and names the step that diverged. Passing the second check alone is not enough: a changed Spec value must fail the test even when the code and the oracle still agree. Meanwhile, `spec:orders.create-order.invalid-cart` is authored at `defined` and declares `verifies: spec:orders.create-order`, but has no `specTest` anchor yet. Run recipe 10 (declared versus enabled verifiers) against the example: @@ -322,24 +337,24 @@ The `spec:orders.create-order` row lists both examples under `declared` and only --- -## 5. Break It on Purpose: Watching Extraction, Conformance, Honesty, and Contract Checks Fire +## 5. Break It on Purpose: Watching the Checks Fire The fastest way to appreciate the checks is to trip them in `examples/checkout-v1`. Run each experiment from the repository root, then revert the edit (`git checkout -- examples/checkout-v1`) and rerun `npm run generate:example` to restore the generated files. All paths below are under `examples/checkout-v1/specs/orders/`. | Experiment | What you edit | Run | What happens | |---|---|---|---| -| **1. Misspell a section heading** (extraction) | In `order-total-rule.sdp.md`, change `## Rule` to `## Rul` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `extract/unrecognized-heading` (`heading "Rul" is not recognized; did you mean "Rule"?`), plus two `extract/invalid-markdown-structure` errors (`Intent entries require a field name`), because the rule bullets beneath the unrecognized heading now fall under `## Intent` | +| **1. Misspell a section heading** (extraction) | In `order-total-rule.sdp.md`, change `## Rule` to `## Rul` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `extract/unrecognized-heading` (`heading "Rul" is not recognized; did you mean "Rule"?`), plus two `extract/invalid-markdown-structure` errors (`Intent entries require a field name`), because the rule bullets beneath the unrecognized heading now fall under `## Intent`. The Spec drops out of the graph (10 specs) and `graph.json` is not written | | **2. Dangle a relation target** (conformance) | In `create-order.sdp.md`, change `refines: spec:orders.order-management` to `refines: spec:orders.order-managment` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `conformance/referential-integrity` (`points to missing target "spec:orders.order-managment". Did you mean "spec:orders.order-management"?`); the suggestion is the unique nearest ID within edit distance ≤ 2 | -| **3. State readiness you haven't earned** (honesty) | In `create-order.sdp.md` (stated `defined`), add `### Open questions` with `- [blocking] Wait?` at the end of `## Intent`, before `## Example space` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `honesty/readiness-floor`, naming the unmet clause `no-blocking-open-questions` | -| **4. Leave a parameter slot unbound** (honesty) | In `create-order-valid-cart.sdp.md`, change `{n: 2}` to `{n}` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `honesty/readiness-floor`, naming the unmet clause `kind-evidence-complete`, and writes two contract modules instead of three: the example's step contract is withheld | +| **3. Keep a rung the Spec no longer earns** (honesty) | In `create-order.sdp.md` (stated `defined`), add `### Open questions` with `- [blocking] Wait?` at the end of `## Intent`, before `## Example space` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `honesty/readiness-floor`, naming the unmet clause `no-blocking-open-questions` | +| **4. Leave a parameter slot unbound** (honesty) | In `create-order-valid-cart.sdp.md`, change `{n: 2}` to `{n}` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `honesty/readiness-floor`, naming the unmet clause `kind-evidence-complete`, and writes two contract modules instead of three: the example's step contract and its registrar are withheld. `sdp build` alone exits `0` with the same two modules, because `sdp build` fails only on extraction errors and the readiness floor is an `sdp validate` check | | **5. Drift a Spec value from code** (contract) | In `create-order-valid-cart.sdp.md`, change `{total: 100}` to `{total: 120}` | `npm run generate:example && npx vitest --run examples/checkout-v1/test` | The bound test fails at `Then an order is created with total 120` with `Spec Then parameters do not match the oracle payload` and `$.total: expected 120, actual 100`: the regenerated contract supplies the Spec's value, and the oracle and implementation still say `100` | --- ## Up Next in the Series -We have seen how Specs and anchors enter the repo and how the extractor and validators keep them honest. In **Part 3: Stop Building 30-Verb CLI Walls for Coding Agents** ([read it here](./03-stop-building-verb-walls-for-coding-agents.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/03-stop-building-verb-walls-for-coding-agents.md)), we look at what happens *downstream* of the graph: how AI coding agents script `sdp q` across 43 runnable recipes, how design passes fit a 243-Spec adopter corpus into context bundles, and how humans review the same graph in the static HTML **Spec Studio**. +We have seen how Specs and anchors enter the repo and how the extractor and validators keep them honest. In **Part 3: Stop Building 30-Verb CLI Walls for Coding Agents** ([read it here](./03-stop-building-verb-walls-for-coding-agents.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/03-stop-building-verb-walls-for-coding-agents.md)), we look at what happens *downstream* of the graph: how AI coding agents script `sdp q` across 43 runnable recipes, how a design pass fits one capability Pack of the first adopter's 243-Spec corpus into a context bundle, and how humans review the same graph in the static HTML **Spec Studio**. --- -*About this article: Part 2 of 8 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every command output are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the outputs at a later commit.* +*About this article: Part 2 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every command output are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the outputs at a later commit.* diff --git a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md index b700930f6..4c496d52c 100644 --- a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md +++ b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md @@ -1,12 +1,12 @@ # Stop Building 30-Verb CLI Walls for Coding Agents: Scripting the Delivery Graph with `sdp q` and Spec Studio -*Part 3 of 8 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* +*Part 3 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* -When teams build CLI tools or MCP servers for AI coding agents, they almost always fall into the same trap: they build a **verb wall**. +When teams build CLI tools or MCP servers for AI coding agents, they often fall into the same trap: they build a **verb wall**. First, they add `cli list-specs`. Then an agent needs to filter by readiness, so they add `--readiness`. Then an agent needs to see which tests verify a spec, so they add `cli show-verifiers`. Soon there are 30 subcommands and 80 flags. Yet the first time an agent asks a composite question—"show me every `defined` Spec in this Pack that has no blocking questions, together with the files that reference it"—no single verb answers. The agent makes 15 sequential tool calls, blows its context window on verbose CLI output, and stitches the join together in its own scratchpad. -In the **Libar Software Delivery Protocol**, we took the opposite path ([the agent surface scripts the graph decision, D5](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-surface-scripts-graph.sdp.md); [the agent front door, MD-22](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-front-door.sdp.md)): +In the **Libar Software Delivery Protocol**, we took the opposite path ([the agent surface scripts the graph decision (D5)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-surface-scripts-graph.sdp.md); [the agent front door (MD-22)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-front-door.sdp.md)): > **Don't give coding agents a wall of verbs. Give them a visible, typed graph and one evaluation sink they can script.** @@ -32,9 +32,9 @@ Two members assemble per-node context: `g.specContext(id)` (one Spec with its se The last three are the demand map's entry adapters: -- `g.findByConcept(term)` — a deterministic, case-insensitive substring match over IDs, titles, binding labels, Pack framing, Spec narratives, and section content, naming the field each match was found in. +- `g.findByConcept(term)` — a deterministic, case-insensitive substring match over IDs, titles, binding labels, Pack framing, Spec narratives, and section values (section keys only in the `model` section), naming the field each match was found in. - `g.byFile(path)` — every node the graph records at `path` (a Spec or Pack authored in that carrier, or a `CodeNode` or `Anchor` declared there, including zero-import `@sdp*` comment-form anchors), plus every Spec reachable from them. -- `g.blastRadius(files)` — the file-level impact of a changeset over the curated graph: the directly impacted Specs and Packs, the one-hop `atRisk` neighborhood with each connecting edge's type and claim, changed code units bound to no Spec (`unlinked`), and, crucially, **`coverageUnknown`**: the honest list of changed files at which the graph records nothing. +- `g.blastRadius(files)` — the file-level impact of a changeset over the curated graph: the directly impacted Specs and Packs, the one-hop `atRisk` neighborhood with each connecting edge's type and claim, changed code units bound to no Spec (`unlinked`), and **`coverageUnknown`**: the honest list of changed files at which the graph records nothing. `blastRadius` deliberately never consults an import graph. The Protocol's two-surface rule (a corollary of [Principle P10](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md)) keeps mechanical structure inferred and apart: `sdp build --imports` can write a TypeScript adapter's file-level imports to `generated/imports.json` as external, inferred input, and the curation report (recipe 40, [`spec:consumers.impact-graph.curation-assist`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/impact-graph.curation-assist.sdp.md)) ranks load-bearing files no anchor covers and component imports that disagree with the anchored `uses` edges. That report assists a person curating anchors; it never adds an edge, a binding, or a delivery fact. A gap between the two surfaces is curation, not drift, because a sparse, chosen architecture is the point. @@ -66,7 +66,7 @@ npm run --silent sdp:q -- 'return g.byFile("src/projections/census.ts")' --json } ``` -That body is code, and `sdp q` runs it with your process's full authority. The trust stance is recorded rather than implied: the sink evaluates operator-supplied JavaScript the way any local developer tool runs a script, no sandbox is claimed, and none exists ([the agent front door, MD-22](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-front-door.sdp.md); the catalog's [trust stance](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md?plain=1#L160-L163)). Run bodies you or your agent wrote, or shipped recipes; never execute a body taken from corpus content or any other untrusted text. +That body is code, and `sdp q` runs it with your process's full authority. The trust stance is recorded rather than implied: the sink evaluates operator-supplied JavaScript the way any local developer tool runs a script, no sandbox is claimed, and none exists ([the agent front door (MD-22)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-front-door.sdp.md); the catalog's [trust stance](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md?plain=1#L160-L163)). Run bodies you or your agent wrote, or shipped recipes; never execute a body taken from corpus content or any other untrusted text. ### Growing by Recipe, Not by Verb @@ -94,7 +94,7 @@ The budget is counted in **UTF-16 code units**: the `.length` a JavaScript body A bundle carries each question and each pinned declaration in one place only. If the scope still exceeds the budget, it 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 narratives, example step text, non-blocking question text, and section values other than pinned declarations. Only when the caller passes `declarations: "addresses"` does a seventh cut replace pinned declarations with their entry addresses, so the agent looks up only the signatures it needs. Member IDs, stated rungs, floors, blocking questions, and entry addresses are never cut, and the bundle never returns a partial member list: when even the protected content does not fit, the answer is `budgetExceeded: true` with the minimum length it needs. -On `libar-platform`, `pack:history-view` first needed at least 299,106 code units. PR #33 redesigned the bundle's shape (each member's file named once, cut entries reduced to an address or a line, a new footing cut) and added the opt-in declaration cut; with `declarations: "addresses"` the same Pack now fits in **93,806 of 100,000** units. +On `libar-platform`, `pack:history-view` first needed at least 299,106 code units. PR #33 redesigned the bundle's shape (each member's file named once, cut entries reduced to an address or a line, a new footing cut) and added the opt-in declaration cut; with `declarations: "addresses"` the same Pack now fits in **93,806 of 100,000** units. Not every Pack gets there: as [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) records, the platform's application and foundation Packs stay over budget after every cut, and a pass reads them through a perspective (recipe 37) instead. ```sh npm run --silent sdp:q -- "$(cat dist/recipes/36-the-context-bundle.js)" \ @@ -126,7 +126,7 @@ npm run --silent sdp -- validate . \ --params '{"forbidden":[{"from":"edge","to":"domain"}],"severity":"warning"}' ``` -Standard output keeps the Protocol's summary line untouched and tallies the corpus's own rules on a line of their own: +Standard output ends with the Protocol's summary line, untouched, and a line of its own that tallies the corpus's rules: ```text validate: 0 errors · 6 warnings (conformance + honesty over the one graph) @@ -150,7 +150,7 @@ Drop `"severity":"warning"` and the same seven findings are errors: the corpus l Agents are well served by `sdp q` and JSON. Humans reviewing a 20-member capability Pack do not want to read JSON, and the frozen Markdown Design Review's Pack member table already runs 13 columns wide. -In PR #33, we shipped phase 0 of the **Spec Studio** ([`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md); [the Studio renders derived HTML, MD-41](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-html-surface.sdp.md); [the Studio is built from custom elements, MD-42](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-web-components.sdp.md)). +In PR #33, we shipped phase 0 of the **Spec Studio** ([`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md); [the Studio renders derived HTML (MD-41)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-html-surface.sdp.md); [the Studio is built from custom elements (MD-42)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-web-components.sdp.md)). Run one command from a checkout: @@ -177,7 +177,7 @@ npm run --silent sdp -- studio . --exclude explorations --exclude examples --exc ![Spec Studio search for OD-157: one matching Spec, with the fields searched listed and the matching open question shown under it with its section and line](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-search.png) -*Searching the `libar-platform` page set for `OD-157`, an identifier the adopter cites in its own Spec prose: one Spec matches, and the entry that mentions it is listed beneath with its line.* +*Searching the `libar-platform` page set for `OD-157`, an id from the adopter's own decision register that the adopter had written into the text of an open question: one Spec matches, in its Intent section, and the question `verifyPassUnderPause` is listed beneath with its line.* All three screenshots were captured from `libar-platform`'s branch during PR #33, as [the screenshot notes](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/README.md) record; publish the page set yourself to see the current pages. @@ -189,7 +189,7 @@ A powerful graph surface only helps if coding agents actually use it instead of The Protocol ships three agent skills under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills). In the Protocol's own repository they are mandatory: [`AGENTS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/AGENTS.md) requires an agent to load the matching skill before any corpus question, Spec authoring, or delivery-session routing. They also ship in the npm package, at `node_modules/@libar-dev/software-delivery-protocol/.agents/skills/`, for an adopter to point its agents at: -1. **[`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md)** — teaches the graph's shape, the task-to-recipe sheet, the wrap-and-filter idiom (run a shipped recipe inside your own body and filter its answer, rather than editing the recipe), and the **anti-anecdote rule**: *the derived graph outranks any summary or cached note; query the graph first, read files last*. +1. **[`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md)** — teaches the graph's shape, the task-to-recipe sheet, the wrap-and-filter idiom (run a shipped recipe inside your own body and filter its answer, rather than editing the recipe), and the **anti-anecdote rule**: *"The derived graph outranks this skill. It also outranks any summary you cached earlier in a session and any paraphrase in any document."* 2. **[`sdp-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md)** — teaches the closed `.sdp.md` and `.sdp.gherkin` grammars, where unsettled truths live (blocking questions, `trigger:` deferrals, `provisional:` leans), how to choose honestly between `satisfies` and `references` when binding code, and how much to write. 3. **[`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md)** — routes delivery sessions across five advisory work shapes (*Capture / refine*, *Design*, *Implement*, *Review*, *Close / slim*), with a re-measurable handoff template and **tripwires**: signs that work has slipped out of its shape, such as writing code during a design session or stating a rung for the author during a review. Its *Close / slim* shape names the **distillation boundary** ([`spec:model.enrichment-lifecycle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/enrichment-lifecycle.sdp.md)): a Design entry may leave its Spec only once the Spec is implemented with a verifier and a bound artifact carries the entry. None of the shapes is a workflow state machine; every preflight is advisory. @@ -217,8 +217,8 @@ npm run --silent sdp:q -- "$(cat dist/recipes/41-the-key-family-register.js)" \ --params '{"prefixes":["bundle","rule"]}' --json # 4. Compare planned component designs against realized component: anchors (Recipe 43). -# On the Protocol's own corpus this returns empty totals (designs: 0), because every -# component is already realized in code; it is for adopters planning components ahead of code. +# On the Protocol's own corpus every total is 0: no Spec here states a component design +# (a Design `id` naming a component:). It serves adopters planning components ahead of code. npm run --silent sdp:q -- "$(cat dist/recipes/43-planned-against-realized-components.js)" --json # 5. Publish the Spec Studio, then open generated/spec-studio/index.html @@ -234,11 +234,11 @@ Never invoke a bare `sdp` inside this checkout: `npm run --silent sdp -- …` ru This post closes the opening trio. **Part 1**, *The Spec Is the Prompt Is the Design* ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), made the case for one graph. **Part 2**, *From Rough Idea to Verified Code* ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/02-walkthrough-intent-to-verified-code.md)), walked the loop from a Spec to a verified test. The next five installments tell specific stories from the Protocol's construction: - **Part 4:** *Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition* -- **Part 5:** *What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Annotation Reversal, and Honest Code Bindings* +- **Part 5:** *What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Comment-Form Reversal, and Honest Code Bindings* - **Part 6:** *Executable Specifications Without Cucumber Glue: Typed Example Spaces, Generated Contracts, and Expected-Outcome Oracles* - **Part 7:** *Designing a 243-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale* - **Part 8:** *Auditing 258 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh* --- -*About this article: Part 3 of 8 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every command output are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the outputs at a later commit.* +*About this article: Part 3 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every command output are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the outputs at a later commit.* diff --git a/docs/articles/README.md b/docs/articles/README.md index af0cf44bf..98ad844c9 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -32,7 +32,7 @@ Every article in the series reinforces one or more clauses of the founding sloga | **Staff / Principal Engineers & Software Architects** | Architecture diagrams and ADRs rot the week after a design review; no way to verify whether code still honors the design or which decisions shape a module. | One primitive with named coordinates; zero-import `@sdp*` comment-form anchors (`satisfies` vs. `references`, `role`, `layer`, `context`); deterministic conformance + honesty checks in CI. | Articles 1, 2, 5, 7 | | **AI / Coding-Agent Builders & Power Users** | `AGENTS.md` files bloat into thousands of lines of prose; MCP tools and CLI "verb walls" force agents to make 20+ calls or grep raw files and guess. | "Agents script the graph, not a verb wall": `sdp q` + `createReader`, 43 runnable recipes, context bundles composed within a budget counted in UTF-16 code units, and structured change proposals. | Articles 1, 3, 7, 8 | | **Engineering Leaders & Technical Founders** | Process theatre—mandatory sprint gates, Jira status choreography, and "100% spec coverage" quotas that produce AI-generated boilerplate nobody reads. | "Adopt the nouns, reject the gates" (MD-2); readiness floors instead of quotas; the build backlog (`ready` Specs without `implemented`, excluding `example` and `decision` Specs) and the drift alarm (`implemented ∧ ¬ready`) as live graph queries. | Articles 1, 4, 7 | -| **BDD & Testing Practitioners** | Cucumber `.feature` files drift from step regexes, duplicate domain logic, and turn non-behavioral NFRs or domain models into awkward Given/When/Then prose. | Carrier-independent executable specs: typed `gwt-vocabulary` example spaces, generated step and space contracts, committed adopted registrars (MD-31), and `specOracle` expected-outcome functions. | Articles 2, 4, 6 | +| **BDD & Testing Practitioners** | Cucumber `.feature` files drift from step regexes, duplicate domain logic, and turn non-behavioral NFRs or domain models into awkward Given/When/Then prose. | Carrier-independent executable specs: typed `gwt-vocabulary` example spaces, generated step and space contracts, generated registrars committed once a test adopts them (adopted registrars are committed, MD-31), and authored `specOracle` expected-outcome functions. | Articles 2, 4, 6 | ### Channel & format rollout - **Long-form canonical posts** — Publish from the Markdown manuscripts in `docs/articles/` to the project blog, Substack, or dev.to, with commit-pinned links to the public repository (`libar-dev/software-delivery-protocol`) and `try-it` blocks readers can run in 60 seconds. @@ -91,44 +91,44 @@ Every article in the series reinforces one or more clauses of the founding sloga | # | Title & file | Status | Core angle | Key repo sources & PRs | |---|---|---|---|---| | **01** | **[The Spec Is the Prompt Is the Design: Why AI-Native Software Delivery Needs One Graph, Not Another Ticket Tracker](./01-the-spec-is-the-prompt-is-the-design.md)** | **Ready in this PR** | The launch manifesto: why delivery truth fractures across tools, why AI agents hallucinate structure without a graph, and how one primitive (`Spec`), three claims (`declared` / `anchored` / `inferred`), and two check families (conformance + honesty) replace workflow gates. | [`docs/concept/00`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md), [`01`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md), [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md), [`spec:model.core-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/core-model.sdp.md), [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1) | -| **02** | **[From Rough Idea to Verified Code: A Hands-On Tour of the Libar Software Delivery Protocol](./02-walkthrough-intent-to-verified-code.md)** | **Ready in this PR** | The step-by-step engineer's tour through `pack:checkout-v1` and the self-hosted engine: authoring `.sdp.md` and `.sdp.gherkin` Specs, clearing readiness floors, binding code with constant-form and `@sdp*` comment-form anchors, generating step/space contracts, binding a `specOracle`, and tripping the honesty checks on purpose. | [`examples/checkout-v1/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md), [`docs/concept/04`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/04-authoring-and-binding.md), [`sdp-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md), [PR #3](https://github.com/libar-dev/software-delivery-protocol/pull/3), [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29) | -| **03** | **[Stop Building 30-Verb CLI Walls for Coding Agents: Scripting the Delivery Graph with `sdp q` and Spec Studio](./03-stop-building-verb-walls-for-coding-agents.md)** | **Ready in this PR** | How coding agents and human reviewers consume the same derived graph: the demand map (`findByConcept`, `byFile`, `blastRadius`), the `sdp q` evaluation sink, growing by recipe instead of CLI verbs, scaling to a 243-Spec adopter with context bundles and corpus rules, and browsing the static HTML Spec Studio. | [`docs/concept/06`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/06-consumers-and-projections.md), [`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md), [`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | -| **04** | **Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition** | Planned | Why settling words (`claim` vs. provenance, `altitude` vs. abstraction, `readiness floor` vs. profile) settled the architecture, and how a falsifiable four-entrant competition (Markdown vs. custom grammar vs. Gherkin fork vs. TSX) ruled `.sdp.md` as default while preserving per-ID universality. | [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md), [`DECISIONS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/DECISIONS.md), [`spec:decisions.carrier-ruling`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/carrier-ruling.sdp.md), [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1), [PR #8](https://github.com/libar-dev/software-delivery-protocol/pull/8), [PR #19](https://github.com/libar-dev/software-delivery-protocol/pull/19) | -| **05** | **What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Annotation Reversal, and Honest Code Bindings** | Planned | A candid post-mortem on Gen 1 (`@libar-dev/architect`), how an evidence-based scout report overturned our own early Gen 2 refusal of doc-comment tags, and why distinguishing `satisfies` (collective realization) from `references` ("answers to") solved the stub-coverage lie. | [`docs/lineage/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage/README.md), [`docs/agent-surface/architecture.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/architecture.md), [`spec:decisions.anchor-comment-form`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md), [`spec:decisions.anchor-binding-grain`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md), [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29) | -| **06** | **Executable Specifications Without Cucumber Glue: Typed Example Spaces, Generated Contracts, and Expected-Outcome Oracles** | Planned | Why traditional BDD rots at the regex boundary, and how the Protocol compiles `gwt-vocabulary` example spaces and bound points into TypeScript union contracts, deterministic registrars, and `specOracle` functions where `unspecified` is a first-class answer. | [`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md), [`spec:validation.oracle-target-eligibility`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/oracle-target-eligibility.sdp.md), [PR #16](https://github.com/libar-dev/software-delivery-protocol/pull/16), [PR #19](https://github.com/libar-dev/software-delivery-protocol/pull/19) | -| **07** | **Designing a 243-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale** | Planned | How the first external adopter (`libar-platform`) pressured the Protocol when designing six capability Packs months ahead of code: entry addresses (`#design.`, `#question.`, `#rule.`), pinned declarations, 100k-unit context bundles, planned components, and `sdp validate --rules`. | [`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md), [`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md), [`spec:model.planned-architecture`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/planned-architecture.sdp.md), [PR #26](https://github.com/libar-dev/software-delivery-protocol/pull/26), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | -| **08** | **Auditing 258 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh** | Planned | How a solo developer + multi-model agent teams audited the entire self-hosted corpus (258 Specs at PR #32's base, `46f0280`) against the engine: splitting into 13 units, running Fidelity vs. Quality lenses across model families, cross-verifying 595 findings (refuting 59), executing 18 parallel worktree lanes, and pinning the self-hosting oracle. | [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md), [`specs/self-hosting.pack.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/self-hosting.pack.sdp.md), [`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md), [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32) | +| **02** | **[From Rough Idea to Verified Code: A Hands-On Tour of the Libar Software Delivery Protocol](./02-walkthrough-intent-to-verified-code.md)** | **Ready in this PR** | The step-by-step engineer's tour through `pack:checkout-v1` and the self-hosted engine: authoring `.sdp.md` Specs (with a sidebar on the `.sdp.gherkin` option), clearing readiness floors, binding code with constant-form and `@sdp*` comment-form anchors, generating step/space contracts, binding an authored `specOracle`, and tripping the checks on purpose. | [`examples/checkout-v1/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md), [`docs/concept/04`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/04-authoring-and-binding.md), [`sdp-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md), [PR #3](https://github.com/libar-dev/software-delivery-protocol/pull/3), [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29) | +| **03** | **[Stop Building 30-Verb CLI Walls for Coding Agents: Scripting the Delivery Graph with `sdp q` and Spec Studio](./03-stop-building-verb-walls-for-coding-agents.md)** | **Ready in this PR** | How coding agents and human reviewers consume the same derived graph: the demand map (`findByConcept`, `byFile`, `blastRadius`), the `sdp q` evaluation sink, growing by recipe instead of CLI verbs, scaling to a 243-Spec adopter with context bundles and corpus rules, and browsing the static HTML Spec Studio. | [`docs/concept/06`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/06-consumers-and-projections.md), [`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md), [`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md), [PR #14](https://github.com/libar-dev/software-delivery-protocol/pull/14), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | +| **04** | **Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition** | Planned | Why settling the words first (`claim` vs. provenance, `altitude` vs. abstraction, `readiness floor` vs. readiness profile) gave the engine a design to be built on, and how a four-entrant carrier competition (Markdown vs. custom grammar vs. Gherkin fork vs. TSX), closed on exhibits, scorecards, and probes, ruled `.sdp.md` the default while keeping one canonical surface per ID. | [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md), [`DECISIONS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/DECISIONS.md), [`spec:decisions.carrier-ruling`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/carrier-ruling.sdp.md), [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1), PRs [#4](https://github.com/libar-dev/software-delivery-protocol/pull/4) to [#7](https://github.com/libar-dev/software-delivery-protocol/pull/7), [PR #8](https://github.com/libar-dev/software-delivery-protocol/pull/8), [PR #17](https://github.com/libar-dev/software-delivery-protocol/pull/17), [PR #19](https://github.com/libar-dev/software-delivery-protocol/pull/19) | +| **05** | **What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Comment-Form Reversal, and Honest Code Bindings** | Planned | A candid post-mortem on Gen 1 (`@libar-dev/architect`), how an evidence-based scout report overturned our own Gen 2 refusal of identity tags in doc comments, and why separating `satisfies` (code that realizes a Spec) from `references` (code that answers to a design without realizing it) stopped a stub from reading as `implemented`. | [`docs/lineage/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage/README.md), [`docs/agent-surface/architecture.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/architecture.md), [`spec:decisions.anchor-comment-form`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md), [`spec:decisions.anchor-binding-grain`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md), [PR #28](https://github.com/libar-dev/software-delivery-protocol/pull/28), [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29), [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32) | +| **06** | **Executable Specifications Without Cucumber Glue: Typed Example Spaces, Generated Contracts, and Expected-Outcome Oracles** | Planned | Why traditional BDD rots at the regex boundary, and how the Protocol generates TypeScript step and space contracts and deterministic registrars from `gwt-vocabulary` example spaces and bound points, then checks each bound example against an authored `specOracle` whose answers include `unspecified`. | [`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md), [`spec:validation.oracle-target-eligibility`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/oracle-target-eligibility.sdp.md), [PR #3](https://github.com/libar-dev/software-delivery-protocol/pull/3), [PR #19](https://github.com/libar-dev/software-delivery-protocol/pull/19), [PR #21](https://github.com/libar-dev/software-delivery-protocol/pull/21) | +| **07** | **Designing a 243-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale** | Planned | How the first adopter (`libar-platform`) pressured the Protocol while designing six Packs months ahead of their code: entry addresses (`#design.`, `#ui.`, `#question.`, `#rule.`), pinned declarations, context bundles within a default budget of 100,000 UTF-16 code units, planned components, and `sdp validate --rules`. | [`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md), [`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md), [`spec:model.planned-architecture`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/planned-architecture.sdp.md), [PR #26](https://github.com/libar-dev/software-delivery-protocol/pull/26), [PR #27](https://github.com/libar-dev/software-delivery-protocol/pull/27), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | +| **08** | **Auditing 258 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh** | Planned | How one developer and multi-model agent teams audited the entire self-hosted corpus (258 Specs at PR #32's base, `46f0280`) against the engine: splitting it into 13 units, running Fidelity and Quality lenses across model families, cross-verifying 595 findings (refuting 59), applying them in 18 lanes, each in its own worktree, and re-transcribing the self-hosting oracle from the carriers. | [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md), [`specs/self-hosting.pack.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/self-hosting.pack.sdp.md), [`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md), [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32) | ### Detailed outlines for Planned Articles (04–08) #### Article 04: *Shared Language Is a Performance Feature* -- **Hook:** Why most "AI prompt engineering" is compensating for an unratified domain vocabulary—and how ambiguous words like *status*, *abstraction*, and *provenance* silently corrupt both human design reviews and subagent reasoning. +- **Hook:** An unratified domain vocabulary costs every reader, human or agent: ambiguous words like *status*, *abstraction*, and *provenance* silently corrupt both human design reviews and subagent reasoning. - **Section 1 — Define first, name last:** The governing rubric from [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md): adopt established industry nouns, reject process state-machine gates (adopt the nouns, reject the gates, MD-2), and keep a coined word only for a genuine differentiator, where the word must name the difference it marks. -- **Section 2 — Six renames that changed the engine:** Walkthrough of the rejected-terms ledger in `CONTEXT.md` (`claim` replacing provenance, `anchor` replacing marker, `altitude` replacing abstraction, `section` replacing facet, `readiness floor` replacing readiness profile, `Pack` replacing SpecPack). -- **Section 3 — The four-way carrier competition:** How [PR #8](https://github.com/libar-dev/software-delivery-protocol/pull/8) judged F2 Markdown, C2 custom grammar, Gherkin fork, and TSX on falsifiable scorecards—and why C2's permanent ecosystem tax (parser, formatter, LSP, zero LLM training distribution) lost to `.sdp.md` + per-ID `.sdp.gherkin` and `.sdp.ts` options (the carrier ruling, MD-18, and the Gherkin carrier option, MD-27). +- **Section 2 — Seven renames the engine was built on:** The rename table in [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1)'s description, settled before any engine code: `claim` replacing provenance, `anchor` replacing marker, `altitude` replacing abstraction, `section` replacing facet, `readiness floor` replacing readiness profile, `Pack` replacing SpecPack, and `reader` and `impact graph` replacing handle and mechanical substrate. `CONTEXT.md` keeps the rejected names in its ledger; one came back with a narrower job, since `marker` now names only the bracket prefix of an open question or keyed entry, never the binding. +- **Section 3 — The four-way carrier competition:** Four entrants, each with its own exhibit PR ([#4](https://github.com/libar-dev/software-delivery-protocol/pull/4) to [#7](https://github.com/libar-dev/software-delivery-protocol/pull/7)): F2 Markdown and the C2 custom grammar as full exhibits judged on their recorded scorecards in [PR #8](https://github.com/libar-dev/software-delivery-protocol/pull/8), the Gherkin fork and typed markup (TSX) closed earlier on falsifiable probes. Why C2's permanent ecosystem tax (parser, formatter, highlighting, editor integrations, LSP, and a grammar every agent must be taught) lost to `.sdp.md` with the TS DSL as a lawful per-ID option (the carrier ruling, MD-18), and how a graph-aware `.sdp.gherkin` option for behavior and example Specs arrived later ([PR #17](https://github.com/libar-dev/software-delivery-protocol/pull/17); the Gherkin carrier option, MD-27). #### Article 05: *What We Learned Rebuilding `@libar-dev/architect`* - **Hook:** Our first-generation tool (`@libar-dev/architect`) worked in production, and Gen 1's own self-hosted graph held 349 patterns—and it taught us every trap of source-first architecture graphs. - **Section 1 — The sins we refused to carry over:** Sprawling tag registries, workflow lifecycle gates (`candidate → roadmap → active → completed`), and hand-authored delivery status. -- **Section 2 — When git archaeology corrects memory:** How the [annotation scout report](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage) inspected Gen 1's commit history and discovered that the "50 tags shrank to 26" story was formal-spec cleanup, while the real failures were duplicate identities, fixture scan leaks, and overloaded `implements` edges. -- **Section 3 — The annotation reversal ([PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29)):** How the first adopter (`libar-platform`) proved why runtime code that cannot import the Protocol package needed the `@sdp*` comment form (MD-36), optional plural bindings and the non-conferring `references` edge (MD-37), and architectural attributes where code is realized (MD-38): a `role` on a code anchor, and a `layer` and `context` on `component:` anchors only. +- **Section 2 — When git archaeology corrects memory:** How the lineage scout report (carried by [PR #28](https://github.com/libar-dev/software-delivery-protocol/pull/28)) read Gen 1's commit history and found that the "50 tags shrank to 26" story was a cleanup of the formal vocabulary, not of a shipped runtime registry, and that what actually drifted was two Gherkin files sharing one identity, CLI fixtures leaking into the production scan, and misread tag syntax. Its tag inventory, not the history, showed the overloaded `implements`, used both for code realizing a Spec and for a feature file pointing at production code. +- **Section 3 — The comment-form reversal ([PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29)):** Three rulings, each grounded in the scout, the original design, and the owner's direction to move closer to it. The `@sdp*` comment form (the comment form, MD-36) answered the first adopter (`libar-platform`), whose runtime code cannot import the Protocol package, so every one of its code anchors sat in a test. Optional plural bindings and the non-conferring `references` edge (bindings are optional, plural, and may reference a design, MD-37) answered its stub problem: the only way to name a design from code was to claim to realize it. Architectural attributes where code is realized (architectural significance is annotated where it is realized, MD-38) came from the original design and the owner, not the adopter: a `role` on any code anchor, and a `layer` and `context` on `component:` anchors only. The collective reading of `satisfies`, where the satisfying units together realize every promise of the Spec, was made explicit later, as ruling R01 of the whole-corpus refresh ([PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32)). #### Article 06: *Executable Specifications Without Cucumber Glue* -- **Hook:** Everyone loves readable scenarios until a one-word phrasing change breaks 40 regex step definitions—or worse, passes silently while testing the wrong value. +- **Hook:** Everyone loves readable scenarios until a one-word phrasing change breaks a regex step definition—or worse, passes silently while testing the wrong value. - **Section 1 — Carrier-independent executability:** How `sdp build` turns `gwt-vocabulary` example spaces and bound points (`{n: 2}`, `{total: 100}`) into typed TypeScript step and space contracts under `generated/contracts/`. -- **Section 2 — Authored → generated, never the reverse:** How committed adopted registrars (MD-31) wire `createWorld`, `invoke`, `observe`, and `expected` without the generated code ever importing product code. -- **Section 3 — The `specOracle` and `unspecified`:** Why expected-outcome semantics belong in a typed oracle beside the tests—and why returning `unspecified` for unmodeled regions surfaces honest coverage gaps without false gates. +- **Section 2 — Authored → generated, never the reverse:** How the generated registrar, written beside its test as a `*.test.generated.ts` file and committed once that test imports it (adopted registrars are committed, MD-31), takes four required adapters (`createWorld`, `invoke`, `observe`, `expected`) and an optional fifth (`assertions`), without the generated code ever importing authored code. +- **Section 3 — The `specOracle` and `unspecified`:** Why expected-outcome semantics belong in an authored, typed oracle beside the tests—and why an oracle that answers `unspecified` for an unmodeled region reports a coverage gap rather than a failure. One caveat belongs in the article: a bound executable example whose point lands on `unspecified` still turns its test red, because no authored Then may silently witness a gap. #### Article 07: *Designing a 243-Spec Platform Ahead of Its Code* -- **Hook:** What happens when an adopter models a transactional platform in 243 Specs and states the components of its layers 3 to 6 as designs before their code exists? Every toy assumption in your spec tool breaks—and forces the real design-at-scale loop. -- **Section 1 — Fine-grained addressing without sub-Spec explosion:** Entry addresses (`spec:#design.`, `#ui.`, `#question.`, `#rule.`), pinned declarations (recipe 24), and the key-family register (recipe 41). -- **Section 2 — Fitting a capability Pack into an LLM's context window:** How recipe 36 (the context bundle) uses a deterministic seven-step cut order and `declarations: "addresses"` to fit `pack:history-view` into 93,806 of 100,000 UTF-16 code units ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). +- **Hook:** What happens when an adopter models a transactional platform in 243 Specs (measured at its pin, per [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)) across seven layers, 0 to 6, and states the components of layers 3 to 6 as designs before their code exists? The assumptions a small example never tests start to break, and the design-at-scale loop has to be built. +- **Section 1 — Addressing one entry without splitting the Spec:** Entry addresses (`spec:#design.`, `#ui.`, `#question.`, `#rule.`, from [PR #27](https://github.com/libar-dev/software-delivery-protocol/pull/27)), pinned declarations (recipe 24), and the key-family register (recipe 41). The limit belongs in the article too: an entry address is admitted in prose, where it must resolve, and refused in every ID slot (checked mentions, MD-40), so a relation still targets a whole Spec. +- **Section 2 — Fitting a Pack into a model's context budget:** Recipe 36 (the context bundle) cuts in a deterministic seven-step order. At first `pack:history-view` needed at least 299,106 UTF-16 code units against the default budget of 100,000; [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)'s compact bundle shape plus the `declarations: "addresses"` opt-in brought it to 93,806. Two of the platform's Packs still exceed the budget after every cut, and are read through a perspective (recipe 37) until then. - **Section 3 — Corpus rules and planned architecture:** Running project-specific policies and the architecture walk (recipe 42) via `sdp validate --rules`, and comparing component design Specs against realized `component:` anchors with recipe 43. #### Article 08: *Auditing 258 Self-Hosted Specs with Cross-Family AI Reviewers* - **Hook:** When your repository self-hosts 258 Specs and 40 recipes (measured at `46f0280`, the merge of PR #31 and the base of [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32)), how do you verify that the Specs, the engine, and the docs actually agree? - **Section 1 — Two lenses, two model families:** The methodology behind [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md) ([PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32)): pairing a Fidelity lens (code + tests vs. Spec rules) with a Quality lens (cross-Spec consistency, vocabulary, readiness honesty), then cross-verifying every finding with the opposite model family. -- **Section 2 — What the 595 findings caught (and the 59 they got wrong):** Real bugs caught—from Gherkin reserved tag heads (`@status.ready`) passing as decoration to a collective-vs-per-unit `satisfies` ambiguity—plus why cross-verification was essential to refute false blockers. +- **Section 2 — What the 595 findings caught (and the 59 that were refuted):** Code that broke its own promises, such as Gherkin status lookalikes (`@status.ready`) passing as decoration; two homes of the law that disagreed, such as the authoring skill reading `satisfies` collectively while a Spec read it per unit (ruled as R01); and why cross-verification by the other model family was needed to refute false blockers. - **Section 3 — The distillation boundary and living hygiene:** Two recipes with different jobs. Slimming candidates (recipe 39) lists the pinned declarations of implemented, verified Specs that bound code may now carry, so a person can slim a Spec past the distillation boundary. The curation report (recipe 40) curates anchors rather than Specs: over the file-level imports `sdp build --imports` writes, it ranks load-bearing files no anchor covers and imports that disagree with anchored `uses` edges. --- @@ -144,10 +144,13 @@ Every article in `docs/articles/` must conform to the repository's ratified lang - State **"deterministically validated," never "provably correct."** - Lead decision references with meaning first, code in parentheses: e.g., *the carrier ruling (MD-18)*, *the agent front door (MD-22)*, *the comment form (MD-36)*. 2. **Temporal-guard compliance (`npm run check:temporal`).** - - `check-temporal.mjs` sweeps every tracked file outside the historical genres (`plans/`, `reviews/`, `explorations/`, `docs/lineage/`, `docs/concept/DECISIONS.md`). - - Never include raw calendar dates (`YYYY-MM-DD`), numbered plan paths (`plans/`), or session/wave/fold tokens in `docs/articles/*.md`. To point a reader at a numbered plan or a dated lineage file, link to the pull request that carried it (`.../pull/33`) or to its parent directory (`.../tree/5a0b22c.../docs/lineage`), never to the file itself and never through an encoded path that only hides the token from the check. -3. **Every corpus count is derived, dated to a commit, and paired with its command.** - - Never quote a number without naming the commit it was measured at and giving the reader the `sdp validate` or `sdp q` command to re-derive it. + - `check-temporal.mjs` sweeps every tracked file and every untracked file `.gitignore` does not exclude, except the temporal-by-genre artifacts (`plans/`, `reviews/`, `explorations/`, `docs/lineage/`, `docs/concept/DECISIONS.md`, the tracked drafts, plans, and evidence under `.omo/`) and the machine-generated `package-lock.json`. + - Never include raw calendar dates (`YYYY-MM-DD`), numbered plan paths (`plans/`), or session/wave/fold tokens in `docs/articles/*.md`. To point a reader at a numbered plan or a dated lineage file, link to the pull request that carried it (`.../pull/33`), never to the file itself, and never encode a path to slip it past the check. +3. **Every count is pinned and re-derivable.** + - A count of the Protocol's own corpus names the commit it was measured at, and a reader can re-derive it with an `sdp validate` or `sdp q` command, given beside the number or in this playbook. + - An adopter figure or a historical figure cites the pull request or review that measured it. +4. **Links survive publication.** + - Inside the repository, link sibling articles relatively (`./02-….md`); at publication, rewrite each relative link to the published URL or to a commit-pinned GitHub URL, since a relative link breaks off the repository. --- @@ -163,15 +166,16 @@ npm ci # installs and builds dist/, including dist/recipes/ The commands below use `npm run --silent sdp:q -- …`, which adds the corpus's three exclusions; `pnpm --silent sdp:q …` is equivalent where pnpm is installed. Never run a bare `sdp` in this checkout. ### Step 2: Measure the design diff from the last pinned commit -Generate the base graph at the previously pinned commit (currently `5a0b22c2d9ead98c900b60ed6209342fce2cbef6`) in a temporary worktree and run recipe 35 (the design diff) to see every Spec, Pack, section, entry key, and binding that changed: +Generate the base graph at the previously pinned commit (currently `5a0b22c2d9ead98c900b60ed6209342fce2cbef6`) in a temporary worktree outside the repository and run recipe 35 (the design diff) to see every Spec, Pack, section, entry key, and binding that changed. Keep the worktree outside: inside the checkout it is an untracked directory the gate trips over, and `check:temporal` fails on it. ```sh BASE_SHA="5a0b22c2d9ead98c900b60ed6209342fce2cbef6" -git worktree add --detach .sdp-base "$BASE_SHA" && ln -s "$PWD/node_modules" .sdp-base/node_modules -npm --prefix .sdp-base run build -node .sdp-base/dist/cli/sdp.js build .sdp-base --exclude explorations --exclude examples --exclude test/fixtures/import/parity -npm run --silent sdp:q -- "$(cat dist/recipes/35-the-design-diff.js)" --params base=@.sdp-base/generated/graph.json --params '{"detail":true}' --json -git worktree remove --force .sdp-base +BASE_DIR="$(mktemp -d)/base" +git worktree add --detach "$BASE_DIR" "$BASE_SHA" && ln -s "$PWD/node_modules" "$BASE_DIR/node_modules" +npm --prefix "$BASE_DIR" run build +node "$BASE_DIR/dist/cli/sdp.js" build "$BASE_DIR" --exclude explorations --exclude examples --exclude test/fixtures/import/parity +npm run --silent sdp:q -- "$(cat dist/recipes/35-the-design-diff.js)" --params "base=@$BASE_DIR/generated/graph.json" --params '{"detail":true}' --json +git worktree remove --force "$BASE_DIR" ``` ### Step 3: Re-derive the headline corpus metrics at the new `HEAD` @@ -181,7 +185,7 @@ npm run --silent sdp:q -- 'return { commit: "HEAD", schemaVersion: graph.schemaVersion, specs: g.specs().length, - packs: g.packs().length, + packs: g.packs().map((p) => p.id), nodes: graph.nodes.length, nodeTypes: graph.nodes.reduce((a, n) => ((a[n.nodeType] = (a[n.nodeType] || 0) + 1), a), {}), edges: graph.edges.length, @@ -213,24 +217,27 @@ npm run --silent sdp:q -- ' } return out; ' --json + +# 5. Shipped recipe bodies +ls dist/recipes/*.js | wc -l ``` ### Step 4: Baseline snapshot at `5a0b22c` (for comparison on next refresh) Measured at `5a0b22c2d9ead98c900b60ed6209342fce2cbef6` (re-run the commands above rather than inheriting): -| Metric | Value at `5a0b22c` | Source query | +| Metric | Value at `5a0b22c` | Source in Step 3 (command · output field) | |---|---|---| -| Schema version | `0.10.0` | `graph.schemaVersion` | -| Authored `Spec`s | `276` (`245 ready`, `17 defined`, `3 scoped`, `11 idea`) | `g.specs()` | -| Authored `Pack`s | `6` (`self-hosting-v1`, `design-management-v1`, `spec-studio-v1`, `corpus-checks-v1`, `design-iteration-v1`, `obsidian-vault-v1`) | `g.packs()` | -| Total nodes / edges / locations | `621` nodes (`276 Primitive`, `6 Pack`, `160 CodeNode`, `179 Anchor`) · `1,624` edges (`1,063 declared`, `561 anchored`) · `1,774` locations | `nodes` and `nodeTypes`, `edges` and `edgeClaims`, `locations` in census body 1 | -| Delivery facts | `94 implemented` · `205 has-verifier` · `0 observed` | `s.deliveryFacts` | -| Operational build backlog | `0` (`110` ready examples and `41` ready decisions audited and excluded) | Recipe 1 (`01-the-build-backlog.js`) | -| Drift alarm (`implemented ∧ ¬ready`) | `0` | Recipe 2 (`02-the-drift-alarm.js`) | -| Design backlog (`defined ∧ ¬implemented`) | `17` Specs | Recipe 33 (`33-the-design-backlog.js`) | -| Open questions | `43` across `24` Specs (`16` blocking: `8` deferrals with triggers, `8` holds; `27` non-blocking) | Recipe 20 (`20-open-question-register.js`) | -| Shipped recipes | `43` runnable bodies under `dist/recipes/` | `docs/agent-surface/recipes.md` | - -After updating any changed counts or commit links, stage the files and run `npm run check` to verify that `check:temporal`, `lint`, `format:check`, `test`, and `preflight` all exit `0`. +| Schema version | `0.10.0` | 1 · `schemaVersion` | +| Authored `Spec`s | `276` (`245 ready`, `17 defined`, `3 scoped`, `11 idea`) | 1 · `specs`, `readiness` | +| Authored `Pack`s | `6` (`self-hosting-v1`, `design-management-v1`, `spec-studio-v1`, `corpus-checks-v1`, `design-iteration-v1`, `obsidian-vault-v1`) | 1 · `packs` | +| Total nodes / edges / locations | `621` nodes (`276 Primitive`, `6 Pack`, `160 CodeNode`, `179 Anchor`) · `1,624` edges (`1,063 declared`, `561 anchored`) · `1,774` locations | 1 · `nodes`, `nodeTypes`, `edges`, `edgeClaims`, `locations` | +| Delivery facts | `94 implemented` · `205 has-verifier` · `0 observed` | 1 · `deliveryFacts` | +| Operational build backlog | `0` (`110` ready examples and `41` ready decisions audited and excluded) | 2 · recipe 1's `total`, `excludedReadyExamples`, `excludedReadyDecisions` | +| Drift alarm (`implemented ∧ ¬ready`) | `0` | 2 · recipe 2's `total` | +| Design backlog (`defined` Specs no code realizes) | `17` Specs | 3 · recipe 33's `totals.specs` | +| Open questions | `43` across `24` Specs (`16` blocking: `8` deferrals with triggers, `8` holds; `27` non-blocking) | 3 · recipe 20's `totals` (`questions`, `specs`, `blocking`, `deferrals`, `holds`, `nonBlocking`) | +| Shipped recipes | `43` runnable bodies under `dist/recipes/` | 5 · the file count | + +After updating any changed counts or commit links, stage the files (`preflight` fails on an untracked file `.gitignore` does not exclude) and run the full green gate, `npm run check`: thirteen steps from `check:temporal` to `preflight`, all of which must exit `0`. That gate covers less of this directory than it seems. `check:temporal` does sweep `docs/articles/` for dates, numbered plan paths, and session tokens, but `.prettierignore` excludes `docs/**`, so `format:check` never reads these articles, and `lint` reads only code. Nothing in the gate catches an unbalanced code fence or a broken link here. Before publishing, check both yourself, with a throwaway script you do not commit: every fence must close with a backtick run at least as long as the one that opened it (a Markdown example that contains its own fences needs a four-backtick outer fence), and every relative link and every commit-pinned GitHub link must resolve. From 20cf20d601b20c5e763eb3845c1795851040584b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Thu, 22 Oct 2026 03:12:49 +0200 Subject: [PATCH 05/36] docs(articles): pin sibling-article links to 48d8f49 Co-Authored-By: Claude Opus 5.5 --- docs/articles/01-the-spec-is-the-prompt-is-the-design.md | 2 +- docs/articles/02-walkthrough-intent-to-verified-code.md | 4 ++-- .../articles/03-stop-building-verb-walls-for-coding-agents.md | 2 +- docs/articles/README.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md index e8d6ebc6a..7bf7f4582 100644 --- a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -342,7 +342,7 @@ The three `--exclude` flags keep the exploration exhibits, the separate worked e ## Up Next in the Series -In **Part 2: From Rough Idea to Verified Code** ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/02-walkthrough-intent-to-verified-code.md)), we open `examples/checkout-v1` and the Protocol's own source code to walk the complete engineering loop: authoring `.sdp.md` carriers (and where `.sdp.gherkin` fits), binding code with constant-form and `@sdp*` comment-form anchors, generating typed step and space contracts, writing an expected-outcome `specOracle`, and deliberately breaking the graph to watch the extraction, conformance, honesty, and contract checks fire. +In **Part 2: From Rough Idea to Verified Code** ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/48d8f495959c2a8adc6ffab4069d4ab60afbf716/docs/articles/02-walkthrough-intent-to-verified-code.md)), we open `examples/checkout-v1` and the Protocol's own source code to walk the complete engineering loop: authoring `.sdp.md` carriers (and where `.sdp.gherkin` fits), binding code with constant-form and `@sdp*` comment-form anchors, generating typed step and space contracts, writing an expected-outcome `specOracle`, and deliberately breaking the graph to watch the extraction, conformance, honesty, and contract checks fire. --- diff --git a/docs/articles/02-walkthrough-intent-to-verified-code.md b/docs/articles/02-walkthrough-intent-to-verified-code.md index 00efec3af..1f9772d91 100644 --- a/docs/articles/02-walkthrough-intent-to-verified-code.md +++ b/docs/articles/02-walkthrough-intent-to-verified-code.md @@ -2,7 +2,7 @@ *Part 2 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* -In Part 1 ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), we introduced the core thesis of the **Libar Software Delivery Protocol**: model the delivery lifecycle as instances of one enrichable primitive (`Spec`), bind code and tests with source `anchor`s, derive **one regenerable graph**, and check **conformance and honesty** in CI. +In Part 1 ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/48d8f495959c2a8adc6ffab4069d4ab60afbf716/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), we introduced the core thesis of the **Libar Software Delivery Protocol**: model the delivery lifecycle as instances of one enrichable primitive (`Spec`), bind code and tests with source `anchor`s, derive **one regenerable graph**, and check **conformance and honesty** in CI. That sounds clean in principle. What does it actually look like in a text editor and a terminal? @@ -353,7 +353,7 @@ The fastest way to appreciate the checks is to trip them in `examples/checkout-v ## Up Next in the Series -We have seen how Specs and anchors enter the repo and how the extractor and validators keep them honest. In **Part 3: Stop Building 30-Verb CLI Walls for Coding Agents** ([read it here](./03-stop-building-verb-walls-for-coding-agents.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/03-stop-building-verb-walls-for-coding-agents.md)), we look at what happens *downstream* of the graph: how AI coding agents script `sdp q` across 43 runnable recipes, how a design pass fits one capability Pack of the first adopter's 243-Spec corpus into a context bundle, and how humans review the same graph in the static HTML **Spec Studio**. +We have seen how Specs and anchors enter the repo and how the extractor and validators keep them honest. In **Part 3: Stop Building 30-Verb CLI Walls for Coding Agents** ([read it here](./03-stop-building-verb-walls-for-coding-agents.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/48d8f495959c2a8adc6ffab4069d4ab60afbf716/docs/articles/03-stop-building-verb-walls-for-coding-agents.md)), we look at what happens *downstream* of the graph: how AI coding agents script `sdp q` across 43 runnable recipes, how a design pass fits one capability Pack of the first adopter's 243-Spec corpus into a context bundle, and how humans review the same graph in the static HTML **Spec Studio**. --- diff --git a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md index 4c496d52c..49e1274a4 100644 --- a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md +++ b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md @@ -231,7 +231,7 @@ Never invoke a bare `sdp` inside this checkout: `npm run --silent sdp -- …` ru ## Where the Series Goes Next -This post closes the opening trio. **Part 1**, *The Spec Is the Prompt Is the Design* ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), made the case for one graph. **Part 2**, *From Rough Idea to Verified Code* ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/58a82023ec9c66784f09ccbb5be941a0beda39a0/docs/articles/02-walkthrough-intent-to-verified-code.md)), walked the loop from a Spec to a verified test. The next five installments tell specific stories from the Protocol's construction: +This post closes the opening trio. **Part 1**, *The Spec Is the Prompt Is the Design* ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/48d8f495959c2a8adc6ffab4069d4ab60afbf716/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), made the case for one graph. **Part 2**, *From Rough Idea to Verified Code* ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/48d8f495959c2a8adc6ffab4069d4ab60afbf716/docs/articles/02-walkthrough-intent-to-verified-code.md)), walked the loop from a Spec to a verified test. The next five installments tell specific stories from the Protocol's construction: - **Part 4:** *Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition* - **Part 5:** *What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Comment-Form Reversal, and Honest Code Bindings* diff --git a/docs/articles/README.md b/docs/articles/README.md index 98ad844c9..31e21b8dc 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -79,7 +79,7 @@ Every article in the series reinforces one or more clauses of the founding sloga **Worked example, lineage, and review** 22. [`examples/checkout-v1/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md) — the worked MVP example -23. [`docs/lineage/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage/README.md) — the restored v0 design documents and the Gen 1 annotation scout report +23. [`docs/lineage/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage/README.md) — the restored v0 design documents and the Gen 1 lineage scout report 24. [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md) — the whole-corpus refresh record From 2ad259bca57171b8e1c26f6db0253a7014bacecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Sat, 31 Oct 2026 15:21:40 +0100 Subject: [PATCH 06/36] docs(articles): publish the projection narrative Owner working rules: commit, push, update PR #34, and make article evidence regenerable and CI-checked. Source baseline: b5e0a8be (main, PR #36). Branch base: f9dfa0c8. Arc Pack: none; docs/articles is the living editorial record. Expand the series to five full drafts and five outlined follow-ups. Add the Studio/UX and future-AI narratives, regenerate article evidence, and feature the series from the repository README. Condense the annotation context and archive the detailed scout. --- .github/workflows/ci.yml | 13 + README.md | 19 + ...01-the-spec-is-the-prompt-is-the-design.md | 369 +++++----------- docs/articles/02-one-graph-many-views.md | 150 +++++++ .../02-walkthrough-intent-to-verified-code.md | 360 ---------------- ...p-building-verb-walls-for-coding-agents.md | 244 ----------- .../03-walkthrough-intent-to-verified-code.md | 289 +++++++++++++ ...p-building-verb-walls-for-coding-agents.md | 148 +++++++ docs/articles/05-ai-can-shape-the-reading.md | 82 ++++ docs/articles/README.md | 401 +++++++++--------- docs/articles/sync-evidence.mjs | 240 +++++++++++ docs/lineage/README.md | 2 +- docs/lineage/annotation-context.md | 40 ++ ...=> archive-annotation-scout-2026-10-04.md} | 0 package.json | 1 + 15 files changed, 1290 insertions(+), 1068 deletions(-) create mode 100644 docs/articles/02-one-graph-many-views.md delete mode 100644 docs/articles/02-walkthrough-intent-to-verified-code.md delete mode 100644 docs/articles/03-stop-building-verb-walls-for-coding-agents.md create mode 100644 docs/articles/03-walkthrough-intent-to-verified-code.md create mode 100644 docs/articles/04-stop-building-verb-walls-for-coding-agents.md create mode 100644 docs/articles/05-ai-can-shape-the-reading.md create mode 100644 docs/articles/sync-evidence.mjs create mode 100644 docs/lineage/annotation-context.md rename docs/lineage/{annotation-scout-2026-10-04.md => archive-annotation-scout-2026-10-04.md} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b09c43cc3..82d148325 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4 @@ -28,6 +30,17 @@ jobs: SDP_CORPUS_COMMIT: ${{ github.sha }} run: npm run check + - name: Check article evidence + shell: bash + run: | + BASE_SHA="$(node docs/articles/sync-evidence.mjs --print-pin)" + BASE_DIR="${{ runner.temp }}/article-evidence" + git worktree add --detach "$BASE_DIR" "$BASE_SHA" + ln -s "$PWD/node_modules" "$BASE_DIR/node_modules" + npm --prefix "$BASE_DIR" run build + npm run check:articles -- "$BASE_DIR" + git worktree remove --force "$BASE_DIR" + - name: Upload run evidence if: always() uses: actions/upload-artifact@v4 diff --git a/README.md b/README.md index 5514d6bf8..685043e7d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,25 @@ Libar Software Delivery Protocol is a typed, executable, self-validating meta-mo delivery: author `Spec` documents in the repository, derive one graph, and check conformance and honesty. +## Read the product story + +The [article series and publication plan](docs/articles/README.md) are the guided introduction to +the Protocol, its human review experience and its AI direction. They are developed in the open in +[PR #34](https://github.com/libar-dev/software-delivery-protocol/pull/34). + +Start with [the spec is the prompt is the design](docs/articles/01-the-spec-is-the-prompt-is-the-design.md), +then read [one graph, many views](docs/articles/02-one-graph-many-views.md) to see why HTML, +Markdown, Mermaid and other projections can change without becoming competing sources of truth. +The [hands-on walkthrough](docs/articles/03-walkthrough-intent-to-verified-code.md) follows intent +into verified code. [The agent-surface article](docs/articles/04-stop-building-verb-walls-for-coding-agents.md) +explains `sdp q`, and [the AI direction](docs/articles/05-ai-can-shape-the-reading.md) separates +current agent workflows from future presentation assistance. + +The [pre-code seed design](docs/lineage/v0-design/README.md) carries the original product +experience. The [short annotation context](docs/lineage/annotation-context.md) explains why source +bindings are critical to useful projections. For the first UX/UI design pass, use the compact +article packet, its designer's brief and a Studio page set generated from the same source pin. + **Carrier rule:** Specs and Packs default to Markdown; the TS DSL survives as import source and a lawful per-ID option, while behavior and example Specs may use Gherkin canonically per ID (the carrier ruling, MD-18, the Pack syntax ruling, MD-25, and the Gherkin carrier option, MD-27). diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md index 7bf7f4582..d998e0d0b 100644 --- a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -1,349 +1,184 @@ -# The Spec Is the Prompt Is the Design: Why AI-Native Software Delivery Needs One Graph, Not Another Ticket Tracker +# The spec is the prompt is the design: why AI-native delivery needs one graph *Part 1 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* -Every software team knows the decay curve of documentation. +At the start of a project, intent is sharp. You write an architecture document, record a few decisions, file tickets and write tests. Then a customer edge case forces a design change during a pull request. The code and test change. The architecture document, decision record and ticket do not. -At the start of a project, intent is sharp. You write an architecture doc, sketch a few sequence diagrams, record a couple of architecture decision records (ADRs), file tickets in your issue tracker, and write tests in your repo. Six weeks later, a customer edge case forces a design change during a pull request. The code changes. The test changes. The architecture doc, the ADR, and the ticket do not. Within a quarter, your documentation is a museum of what you *meant* to build three sprints ago. +An AI coding agent inherits that fragmentation. It can write a function quickly, but it cannot tell an early idea from a settled contract, an implementation shortcut from a ratified decision, or a verifier binding from a passing test. It reads raw files and stale prose, then guesses at the missing structure. -Now drop AI coding agents into that repository. +[Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753) (`@libar-dev/software-delivery-protocol`, CLI `sdp`) addresses that missing structure. It is an executable, self-validating meta-model of the software-delivery process. -An LLM can write a function in seconds, but when you ask it to change an architectural seam—"update order creation so stock reservation respects our latency constraint"—how does it know what the system guarantees? It greps raw source files, reads stale Markdown, and **hallucinates structure**. It cannot tell an aspirational idea from a settled contract, an implementation shortcut from a ratified decision, or a stub from a verified feature—because **no typed model of the delivery lifecycle exists in the repository for it to reason over**. +Its premise has four parts: -[**Libar Software Delivery Protocol**](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (`@libar-dev/software-delivery-protocol`, CLI `sdp`) is our answer: an **executable, self-validating meta-model of the software-delivery process**. +> Specs are code; the graph is derived; the `claim` stays honest; git is the event log. -Its premise fits in one sentence: +This article explains those parts and what they make possible. -> **Specs are code; the graph is derived; the `claim` stays honest; git is the event log.** +## One Spec, enriched in place -This article takes those four clauses in turn, then measures the Protocol against its own corpus. +Delivery tools often make an idea change artifact types as it matures. A note becomes a requirement, then a design document, then tickets, then acceptance criteria. Each migration loses context and breaks traceability. ---- - -## 1. A Second Take, Front-Loaded with First Principles - -The Protocol is not our first attempt at this problem. Its predecessor, [`@libar-dev/architect`](https://github.com/libar-dev/architect), ran in production and taught us where the problem bites. At the commit we later audited, its own repository's graph held 349 patterns (227 from TypeScript, 122 from Gherkin), as the lineage scout report in [`docs/lineage/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage) measured ([PR #28](https://github.com/libar-dev/software-delivery-protocol/pull/28) carried it). It proved three things that worked: keeping design metadata in source, beside the code, deriving a single immutable graph as the read model for both documentation and agents, and giving agents a scriptable graph handle instead of raw files. - -It also taught us what fails in production: - -- **Sprawling tag registries.** When every metadata wish becomes a new `@architect-*` tag across TypeScript and Gherkin, the formal taxonomy and the live parser inevitably drift apart. -- **Process state-machine gates.** Encoding ticket statuses (`candidate → roadmap → active → completed`) inside source files turns your compiler into a workflow bureaucracy. -- **Hand-authored delivery status.** If a human or an agent types `status: completed` into a spec file, that string will eventually lie. - -We designed the Libar Software Delivery Protocol as a clean-sheet second generation (its concept foundation landed in [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1), the engine in [PR #2](https://github.com/libar-dev/software-delivery-protocol/pull/2))—zero backward compatibility, zero old sins carried over—around **three meta-levels** ([the executable meta-model decision, MD-1](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/executable-meta-model.sdp.md)): - -| Meta-level | What lives here | Role | -|---|---|---| -| **1. The Protocol** | The `Spec` primitive, descriptors, relations, and validators *as typed code* | Defines the conformance contract | -| **2. The Authored Model** | A project's `Spec`s, `Pack`s, and source `anchor`s committed in the repo | Conforms to the contract (checked in CI, never workflow-gated) | -| **3. Derived Facts** | Machine truth (delivery facts such as `implemented` and `has-verifier`) | Computed from the graph, **never authored by hand** | - -And we bound the entire system with **two permanent honesty guardrails** ([`00-vision-scope-and-mvp-boundary.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md)): - -1. **Checks police conformance and honesty—never content quality, never workflow.** Validators check whether your model is well-formed and whether it is pretending to have evidence it lacks. They never judge whether your design is "good," and they never enforce sprint or ticket state machines ([adopt the nouns, reject the gates, MD-2](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/adopt-the-nouns.sdp.md)). -2. **We claim "deterministically validated," never "provably correct."** Conformance to a typed meta-model is mechanically verifiable; real-world business correctness requires human judgment and runtime evidence. - ---- - -## 2. One Enrichable Primitive: `Spec` - -In traditional delivery tooling, an idea changes artifact types as it matures. It starts as a sticky note or Epic, gets rewritten into a PRD, gets copied into an ADR or RFC, gets sliced into Jira Stories, and finally gets re-typed into Gherkin `.feature` files and unit tests. Every migration between artifact types loses context and breaks traceability. - -In the Protocol, there is **one authored truth-primitive: `Spec`** ([one primitive, named coordinates, MD-4](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/one-primitive.sdp.md); [`spec:model.core-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/core-model.sdp.md)). - -A `Spec` keeps its stable, namespaced identity (`spec:orders.create-order`) from its first two-line capture to production realization. Instead of migrating it to a new artifact type, you **enrich it in place**, by adding detail to its typed sections or by refining it into child `Spec`s. There are **eight typed sections**, one concern each: `intent`, `behavior`, `constraints`, `model`, `design`, `decision`, `verification`, and `ui` ([`spec:model.spec-sections`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/spec-sections.sdp.md)). In the Markdown carrier you will also see `## Rule`, `## Workflow`, `## Contract`, and `## Example space` headings; those are carrier forms of the one `behavior` section, and they reify into it ([`spec:validation.kind-evidence`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/kind-evidence.sdp.md), [`spec:carrier.markdown-body-grammar`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/markdown-body-grammar.sdp.md)). - -Every `Spec` is positioned by **three independent descriptors**: - -- **`kind`** *(the category of truth — a true subtype that determines required evidence)*: - `behavior` (Use Case / Behavior) · `workflow` (Workflow) · `example` (Example / Scenario) · `rule` (Business Rule) · `constraint` (Constraint / NFR) · `model` (Domain Model) · `decision` (Decision Record) · `contract` (Contract). -- **`altitude`** *(size and scope)*: - `epic → feature → story`. -- **`readiness`** *(author-stated design maturity, checked against a structural floor)*: - `idea → scoped → defined → ready`. - -Familiar delivery nouns—Use Case, NFR, Decision Record, Contract, Epic, Feature, Story, Scenario—are **named coordinates on this single primitive**, not separate schemas or tools. An **Executable Spec** is likewise not a type or a rung: it is an `example` Spec that carries the `has-verifier` delivery fact. - -Because a child's readiness statement is its own, parent and child can stand on different rungs. In the worked example the next article tours, the `epic`-altitude `behavior` Spec `spec:orders.order-management` states `defined`, as does the `feature` beneath it, while the `story`-altitude `example` `spec:orders.create-order.valid-cart` already states `ready` and has a bound verifier. The epic could honestly sit at `scoped`, because the example refines the `defined` feature, not the epic. There is one coupling: the `ready` floor requires every `refines`, `dependsOn`, `constrainedBy`, and `decidedBy` target to state at least `defined` (`spec:validation.readiness-floor#rule.readyFloor`, [`spec:validation.typed-dependency-floor`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/typed-dependency-floor.sdp.md)). So a `ready` Spec never refines a `scoped` parent directly. - -Alongside `Spec`, two other authored constructs enter the repository, and **neither states system truth of its own**: - -- **`Pack`** (`pack:checkout-v1`, [`spec:model.pack-aggregate`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/pack-aggregate.sdp.md)): a review and reading grouping over `Spec`s. A `Spec` can belong to multiple `Pack`s. -- **`anchor`** ([`spec:model.anchors`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/anchors.sdp.md)): a pointer in source code or tests that binds a code unit (`impl:`, `api:`, `component:`), a test (`test:`), or an expected-outcome oracle (`oracle:`) to `Spec` IDs. An anchor carries identity, bindings, and optional architectural structure—never behavior, rationale, or readiness. - -### A Spec at its first rung - -Suppose the team behind the worked example (`examples/checkout-v1`, Order Management) starts thinking about refunds. One command scaffolds an honest `idea`-rung Spec in the default Markdown carrier (`.sdp.md`): - -```sh -npm run --silent sdp -- new spec examples/checkout-v1/specs/orders/refund-order-api-contract.sdp.md \ - --id spec:orders.refund-order.api-contract --kind contract --altitude story \ - --title "HTTP contract for refunding an order" \ - --outcome "Define the HTTP request and response payload for POST /orders/{id}/refunds." -``` - -`sdp new spec` writes the envelope with `readiness: idea` and `relations: {}`, the `# Title`, an `## Intent` section holding the `- outcome:` line, and the kind's typed heading, here a bare `## Contract`, with no invented content. The author then edits the file in place: one `refines` relation to the Order Management epic, and one keyed blocking question with a decider and a provisional answer: - -```markdown ---- -id: spec:orders.refund-order.api-contract -kind: contract -altitude: story -readiness: idea -relations: - refines: spec:orders.order-management ---- -# HTTP contract for refunding an order - -## Intent +The Protocol has one authored truth-primitive, `Spec`. A Spec keeps its stable, namespaced identity, such as `spec:orders.create-order`, from its first capture through implementation. You enrich it with typed sections or refine it into child Specs. You do not convert it into a different artifact ([`spec:model.core-model`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/model/core-model.sdp.md)). -- outcome: Define the HTTP request and response payload for POST /orders/{id}/refunds. +Three independent descriptors position every Spec: -### Open questions +- `kind` names the category of truth: `behavior`, `workflow`, `example`, `rule`, `constraint`, `model`, `decision` or `contract`. +- `altitude` names size and scope: `epic`, `feature` or `story`. +- `readiness` is the author's stated design maturity: `idea`, `scoped`, `defined` or `ready`. -- [blocking #idempotencyHeader] Should the endpoint require an Idempotency-Key header on every refund request? - - decider: owner - - provisional: Require an Idempotency-Key header on every refund request. +Use Case, NFR, Decision Record, Epic, Story and Scenario are named coordinates on this one shape, not separate schemas. An Executable Spec is an `example` with a verifier, not another type or a readiness rung. -## Contract -``` - -Dropped into `examples/checkout-v1/specs/orders/`, that file validates cleanly: `npm run --silent sdp -- validate examples/checkout-v1` reports `12 specs · 1 packs · 5 anchors → 18 nodes · 33 edges` and `validate: 0 errors · 0 warnings`. (The title must follow the closing `---` directly; a blank line between them is refused with `extract/invalid-markdown-structure — the first body line must be an H1 title`.) - -No boilerplate. No mandatory twelve-part template to fill out just to capture a thought. Yet it is already a typed node in the graph, joined to `spec:orders.order-management`. Its question has an **entry address**, an address for one keyed entry inside a Spec: `spec:orders.refund-order.api-contract#question.idempotencyHeader`. Another Spec's prose can cite that address, and `sdp validate` reports one that does not resolve; a register, test, or page outside the Specs checks its addresses with address resolution (recipe 25). The floor tells the author what comes next: the empty `## Contract` holds the Spec at `idea` until the contract has its first entry, and once it does, the blocking question holds it below `defined` until someone settles it. +A Pack groups Specs for reading and review without stating system truth of its own. An anchor binds a code unit, test or expected-outcome oracle into the graph. Anchors carry identity and optional structure, never behavior, rationale or readiness. ### Why the Spec is the prompt -This is the sense of the title. Because one `Spec` is enriched in place from rough idea to implementable contract, and never migrates across tools, everything an implementer needs about it sits behind one ID in one graph. Feeding an agent that Spec's graph context *is* the implementation prompt: +A Spec can carry the intent, rules, examples, constraints, decisions, design entries and open questions needed to implement it. Its graph context also includes relations, source bindings, verifier bindings and the structural floor behind its stated readiness: ```sh npm run --silent sdp:q -- 'return g.specContext("spec:consumers.reader")' --json ``` -`g.specContext` returns the Spec's typed sections, its stated and derived readiness with any floor failures, its relations in both directions with their claims, the code units that realize it or answer to it (with file and line), its verifiers and oracle, and the source line of each keyed entry. There is no second brief to write and keep in sync: "implement `spec:…`" is the whole instruction, and the real work moves to maturing and reviewing the Spec. - -That only works if Specs stay lean. Principle P4 of the [founding principles](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md) carries the corollary: **significance governs detail, not a tier.** A Spec carries detail only where the work is architecturally significant or genuinely novel; the Nth CRUD endpoint earns a reference, not a re-derivation. Its motto is *"Enrich what matters; reference what is standard; pad nothing."* That is why readiness is a **floor to clear**, never a **quota to fill**: a fixed template pressures every author, and AI authors most of all, to fill sections the work never needed, and padding is noise the next prompt has to read. - ---- - -## 3. The Graph Is Derived, and Git Is the Event Log - -### `graph = f(repo)` - -Nobody edits the graph. `sdp build` reads every carrier and source anchor and derives one flat document: an array of nodes, an array of edges, and a location table of source lines, with no nesting and nothing hand-maintained ([`spec:extraction.derive-graph`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/derive-graph.sdp.md)). Here is one real `Primitive` node from the Protocol's own graph, complete. Fittingly, it is the Spec that states the regenerability law: - -```json -{ - "id": "spec:extraction.regenerability", - "nodeType": "Primitive", - "claim": "declared", - "specKind": "rule", - "altitude": "feature", - "readiness": "ready", - "title": "Generated artifacts are disposable projections", - "file": "specs/extraction/regenerability.sdp.md", - "sections": { - "intent": { - "outcome": "Keep the repository canonical while allowing every graph and projection to be rebuilt safely." - }, - "behavior": { - "rules": [ - "Generated artifacts are disposable: deleting them and rebuilding from the same committed repository produces the same bytes.", - "Consumers read the graph or link to source locations recorded in it; they never re-parse source or keep a parallel model.", - "The graph is one JSON document queried in memory." - ] - } - }, - "deliveryFacts": ["implemented", "has-verifier"] -} -``` - -And these are the five edges that touch it. Hierarchy, membership, and bindings are all just edges, and every edge carries its `claim`: - -```json -[ - { "from": "spec:decisions.studio-html-surface", "type": "dependsOn", "to": "spec:extraction.regenerability", "claim": "declared", - "reason": "disposable HTML can be regenerated from the graph instead of committed as authored source" }, - { "from": "spec:extraction.regenerability", "type": "refines", "to": "spec:extraction.determinism", "claim": "declared" }, - { "from": "spec:extraction.regenerability", "type": "belongsTo", "to": "pack:self-hosting-v1", "claim": "declared" }, - { "from": "impl:protocol.regenerability", "type": "satisfies", "to": "spec:extraction.regenerability", "claim": "anchored" }, - { "from": "test:protocol.regenerability", "type": "verifies", "to": "spec:extraction.regenerability", "claim": "anchored" } -] -``` - -The `refines` and `dependsOn` edges are authored relations (`declared`). The `belongsTo` edge is computed from the Pack manifest and inherits the manifest's `declared` claim. The `satisfies` and `verifies` edges come from a code anchor and a test anchor (`anchored`), and they are why the node's `deliveryFacts` read `implemented` and `has-verifier`. Reproduce the excerpt with: - -```sh -npm run --silent sdp:q -- 'const id = "spec:extraction.regenerability"; return { node: graph.nodes.find((n) => n.id === id), edges: graph.edges.filter((e) => e.from === id || e.to === id) }' --json -``` - -Derivation is deterministic: stable ordering, no timestamps, `graph = f(repo)` and `view = f(graph)`. The regenerability invariant follows: **delete `generated/` and rebuild, and you get the same bytes** ([`spec:extraction.regenerability`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/regenerability.sdp.md)). Every validator, human view, and agent query reads this one graph, so none of them can disagree with another about what the repository says. - -### Git is the event log +"Implement `spec:…`" can therefore be a precise instruction. The agent reads the same authored intent and bindings that a reviewer sees. The work shifts from rewriting briefs for different tools to improving the Spec itself. -The graph holds only the current state of the repository at one commit. It keeps no deleted or superseded "ghost" nodes, no change history, and no audit tables. When a Spec is removed, it is gone from the graph; to read what it used to say, check out an earlier commit and regenerate. "Who changed what, and when" is `git log` and `git blame`, never graph state. +That only works when authors avoid padding. The Protocol's readiness floor is a minimum structural requirement, not a quota of sections. A familiar CRUD endpoint may need little detail. A novel business rule may need much more. Checks can tell whether a stated rung clears its floor. They cannot decide whether the design is good. -There is one narrow exception, and it holds because it is current intent rather than history. A decision record may declare `supersedes` another decision record while both are present and the older one still explains something a reader needs. Once it no longer does, the older record is deleted and git holds its history. The Protocol's own registry shows this: the decision that kept source comments out of the graph (MD-35) was replaced by the comment form (MD-36) and then deleted; [`DECISIONS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/DECISIONS.md) keeps a `deleted` row naming the commit that holds its last text. +## The graph is derived ---- - -## 4. The Epistemic Boundary: Why `claim` Separation Changes Everything - -Why don't coding agents trust architecture tools? Because most tools mix together three completely different kinds of statements: - -1. What a human *intended* the system to do. -2. What a human *bound* in code or tests. -3. What a static-analysis heuristic *guessed* from import statements. +Nobody authors the graph. `sdp build` reads the carriers and source anchors and derives one flat, typed, deterministic document: nodes, edges and a location table. Validators, recipes and projections consume that one read model rather than parsing source into private alternatives ([`spec:extraction.derive-graph`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/extraction/derive-graph.sdp.md)). -Once those three collapse into a single "dependency" edge, a single bad heuristic poisons the graph. +Two edges can point at the same Spec and mean different things: -The graph keeps a strict epistemic boundary (a working name; see Principles P9 and P10 in [`01-founding-principles-and-invariants.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md), and [`spec:extraction.claim-taxonomy`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/claim-taxonomy.sdp.md)). A **`claim`** records a fact's epistemic status, and its three values are **never collapsed**. Every node and edge in the curated graph carries `declared` or `anchored`. The third value, `inferred`, names mechanical structure: the file-level imports that `sdp build --imports` writes beside the graph. That input is kept apart from the curated graph, feeds curation advice, and never derives a curated edge, a binding, or a delivery fact. +- `refines` from one Spec to another is authored intent with a `declared` claim. +- `satisfies` from a code unit to a Spec comes from an anchor and has an `anchored` claim. +- `belongsTo` is computed from a Pack manifest and inherits the manifest's `declared` claim. -| `claim` value | Who asserts it | What it means | Authority | -|---|---|---|---| -| **`declared`** | Human author in a `Spec` or `Pack` carrier | Delivery intent: relations (`refines`, `dependsOn`, `constrainedBy`, `decidedBy`, `verifies`, `supersedes`), stated readiness, section content, and Pack membership (`belongsTo`, computed from the manifest) | **Authoritative intent** | -| **`anchored`** | Human author via an in-code `anchor` | A binding from source code or tests into the graph (`satisfies`, `references`, `verifies`, `models`, `memberOf`, `uses`) | **Authoritative binding** (carries no intent) | -| **`inferred`** | Static analyzer (the TypeScript imports adapter behind `sdp build --imports`) | Mechanical code structure (file-level imports), held outside the curated graph | **Advisory only** — feeds curation advice; never derives a curated edge, binding, or delivery fact; never triggers a validator error; never silently promoted | +The distinction is not bookkeeping. It lets a consumer answer "what did the authors intend?" separately from "what source did they bind?" and "what did static analysis infer?" -### Stated Readiness vs. Derived Delivery Facts +The graph holds current state at one commit. Git holds prior states, deleted Specs and the answers to "who changed what?" and "when?" A decision may declare `supersedes` while the older record still explains something a reader needs. That is current intent, not a second history store. -Because `declared` intent and `anchored` bindings are kept separate, **design maturity** (`readiness`) and **code realization** (`deliveryFacts`) are independent coordinates (readiness is separate from delivery facts, D1; [`spec:extraction.delivery-facts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/delivery-facts.sdp.md)): +## The claim stays honest -- **Author-stated `readiness` (`idea → scoped → defined → ready`):** What the author states about the maturity of the design, checked against the cumulative [**readiness floor**](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/readiness-floor.sdp.md). The graph also computes **`derivedReadiness`**, the highest rung whose cumulative floor clauses all pass, and renders it beside `statedReadiness`; it never overwrites the author's statement. You cannot state `defined` while a `[blocking]` open question remains open, and you cannot state `ready` while a `dependsOn`, `refines`, `constrainedBy`, or `decidedBy` target sits below `defined`. -- **Derived `deliveryFacts`:** Computed deterministically from resolving `anchored` edges—**never authored by hand**. `implemented` and `has-verifier` are derived today; `observed`, the runtime liveness fact, is designed for and not yet derived. - - `implemented` is conferred when at least one anchored `satisfies` edge from a code anchor resolves to that Spec. It never propagates through `refines` from parent to child or child to parent. - - `has-verifier` is conferred when a resolving `specTest` anchor verifies the Spec directly, or when a verifying `example` Spec is itself backed by a resolving `specTest` anchor (an **enabled verifier**). It says a verifier binding exists, not that the test passed. +Every node and edge has one of three claims: -Every route to hand-authoring a delivery fact is refused, each with its own finding: +| Claim | Source | Authority | +|---|---|---| +| `declared` | Human-authored Spec or Pack content | Authoritative intent | +| `anchored` | Human-authored source or test anchor | Authoritative binding, no intent | +| `inferred` | Mechanical analysis | Advisory structure, never silently promoted | -- Writing `implemented: true` in a Spec's frontmatter is refused at extraction (`extract/reserved-property`). -- Writing a derived edge such as `satisfies` under `relations` is refused at extraction (`extract/invalid-frontmatter`). -- Smuggling a delivery fact into a section fails `honesty/authoring-shape`. -- A graph whose stated delivery facts disagree with its bindings, from any graph producer, fails `honesty/delivery-facts`. +The current curated graph contains declared and anchored content. File-level imports supplied through `sdp build --imports` remain separate inferred input for curation advice. They do not mint curated edges or delivery facts ([`spec:extraction.claim-taxonomy`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/extraction/claim-taxonomy.sdp.md)). -Look at the two payoff queries that fall out of keeping those coordinates separate. Each is a filter over the reader's `g.specs()`; the shipped recipes, runnable `sdp q` bodies cataloged in [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md) and each executed as written by the test suite, wrap these expressions with counts and audit rows. +Readiness and realization stay independent too: -1. **The operational build backlog (recipe 1):** Specs whose author has stated `ready` and that no code anchor yet realizes, waiting to be built. The readiness floor already admits `ready` only once every typed dependency states at least `defined`, so the recipe does not filter on dependencies itself. Ready `example` and `decision` Specs are excluded and counted, because examples carry verification evidence and decision records are ratified rather than built (the example realization posture, MD-24, and the decision readiness posture, MD-26). +- Stated readiness is an author's position, checked against a structural floor. +- Derived readiness is the highest rung whose floor clauses pass. It appears beside the stated rung and never replaces it. +- `implemented` means at least one resolving `satisfies` edge binds code directly to the Spec. +- `has-verifier` means a resolving verifier binding exists. It does not mean the test passed. +- `observed` would record runtime liveness. It is designed for and not derived today. - ```js - g.specs().filter((s) => - s.statedReadiness === "ready" && - !s.deliveryFacts.includes("implemented") && - s.specKind !== "example" && - s.specKind !== "decision") - ``` +Two useful queries fall out of this separation. -2. **The drift alarm (recipe 2):** Code that ran ahead of its design: a code anchor binds an implementation to a Spec whose author has not stated `ready`. +The operational build backlog finds ready Specs that no code realizes, excluding examples and decision records for their audited realization postures: - ```js - g.specs().filter((s) => - s.deliveryFacts.includes("implemented") && - s.statedReadiness !== "ready") - ``` +```js +g.specs().filter((s) => + s.statedReadiness === "ready" && + !s.deliveryFacts.includes("implemented") && + s.specKind !== "example" && + s.specKind !== "decision") +``` -Neither query is a CI failure. They are standing questions anyone can ask the graph at any commit. +The drift alarm finds code ahead of stated design maturity: ---- +```js +g.specs().filter((s) => + s.deliveryFacts.includes("implemented") && + s.statedReadiness !== "ready") +``` -## 5. Conformance and Honesty Checks (Plus Corpus Rules) +Neither query is a CI failure. They are standing questions over current repository state. -When you run `sdp validate`, every carrier is extracted into the one graph and checked by two validator families ([`spec:validation.two-check-families`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/two-check-families.sdp.md)): +## Checks police conformance and honesty -- **Conformance checks** ask: *Is this model well-formed against the Protocol?* - - Do all relation targets and anchor targets resolve (`conformance/referential-integrity`, with a nearest-ID suggestion)? - - Are all IDs unique across `.sdp.md`, `.sdp.ts`, `.sdp.gherkin`, and source anchors? The extractor reports each duplicate site (`extract/duplicate-id`) and keeps those carriers out of the graph; `conformance/duplicate-ids` is the backstop for any other graph producer. - - Do `claim` values, node types, edge types, and descriptors stay inside their closed sets (`conformance/claim-separation`)? - - Are the structural edges `memberOf` and `uses`, and the design-reference edge `references`, well-formed (`conformance/structural-anchors`)? `layer` and `context` are attributes of a `component:` anchor rather than edges; a `layer` outside its closed set, or either attribute on a non-component anchor, is refused at extraction. - - Do `spec:` mentions and entry addresses (`spec:id#design.key`, `#ui.key`, `#question.key`, `#rule.key`) written in prose resolve (`conformance/prose-mentions`)? A mention or entry address that does not resolve is an error. A resolving mention between two Specs where neither declares a relation to the other is an informative warning, reported once per pair. -- **Honesty checks** ask: *Is this model pretending to be something it isn't?* - - Did someone hand-author a delivery fact or a derived edge? (The refusals listed in the previous section: `extract/reserved-property` and `extract/invalid-frontmatter` at extraction, `honesty/authoring-shape` and `honesty/delivery-facts` over the graph.) - - Does every Spec clear the cumulative structural floor for the `readiness` rung its author stated (`honesty/readiness-floor`)? - - Are `ready` Specs (excluding `decision` Specs, under the decision readiness posture, MD-26) that have no resolving verifier surfaced as informative `honesty/gaps` warnings, never blocking the build? +`sdp validate` runs two check families over the one graph. -And when an adopting team has project-specific architectural rules—for instance, "domain components must never use edge components" or "every question must name a decider"—they run **corpus rules** on the same validation path via `sdp validate --rules` ([`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md), introduced in [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)). Corpus findings are reported in their own section, never move a readiness floor or change a Protocol finding, and make `sdp validate` exit nonzero when they report an error. +Conformance checks ask whether the model is well-formed. They catch unresolved relations, duplicate IDs, invalid claims and malformed structural bindings. Honesty checks ask whether the model is pretending. They reject hand-authored delivery facts and readiness statements that do not clear their floors. Informative gaps, such as a ready Spec without a verifier, remain visible without becoming mandatory workflow gates. ---- +A team can add corpus rules through `sdp validate --rules`. Those findings remain distinct from the Protocol's two families. -## 6. Self-Hosting by the Numbers +The permanent guardrail is worth stating exactly: checks police conformance and honesty, never content quality and never workflow. The strongest claim is "deterministically validated," never "provably correct." -The Protocol does not have a separate PRD. It **self-hosts** ([`spec:protocol.self-hosting`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/protocol/self-hosting.sdp.md)): every rule of its model, extractor, validators, projections, and agent surface is authored in its own `.sdp.md` and `.sdp.gherkin` carriers under [`specs/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs), bound to its own TypeScript engine under [`src/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src) and test suite under [`test/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/test), and checked by `npm run check` in CI on every push and pull request. +## One graph feeds different consumers -At commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (re-run the queries below rather than quoting these numbers across commits), the Protocol's own self-hosting graph measures: +A graph is useful only if people and agents can read it in forms that fit their work. -| Dimension | Count at `5a0b22c` | Breakdown | -|---|---|---| -| **Authored `Spec`s** | **276** | `110 example` · `71 behavior` · `41 decision` · `35 rule` · `12 model` · `5 contract` · `1 workflow` · `1 constraint` | -| **Stated `readiness`** | **276** | `245 ready` · `17 defined` · `3 scoped` · `11 idea` | -| **Authored `Pack`s** | **6** | `self-hosting-v1` (276) · `design-management-v1` (24) · `spec-studio-v1` (18) · `corpus-checks-v1` (13) · `design-iteration-v1` (11) · `obsidian-vault-v1` (5) | -| **Source & test bindings** | **339** | `160 CodeNode` (`146 impl:`, `14 component:` across 14 components and 4 layers; code anchors also admit `api:`) · `179 Anchor` (`172 test:`, `7 oracle:`) | -| **Derived graph size** | **621 nodes · 1,624 edges** | `1,063 declared` edges · `561 anchored` edges · `1,774` entry and relation source locations (schema `0.10.0`) | -| **Derived delivery facts** | **94 `implemented` · 205 `has-verifier`** | Operational build backlog (recipe 1): **0** · Drift alarm (recipe 2): **0** · Design backlog (recipe 33): **17** | +Agents script the graph through `sdp q` and the exported typed reader. People can read Markdown Design Reviews, census reports, Mermaid diagrams, Gherkin-shaped views, the static HTML Spec Studio or generated Obsidian notes. These are projections, disposable and regenerable rather than rival stores of delivery status. -Re-derive the table (except the three recipe counts, which are recipes 1, 2, and 33) with: +The Spec Studio makes the human side concrete. A reviewer can explore a Pack, inspect open questions and architecture, compare a design with a base, read supplied run evidence and gather requests as scoped intent. An agent applies accepted changes as ordinary source edits. The view never becomes canonical. -```sh -npm run --silent sdp:q -- 'const tally = (xs, f) => xs.reduce((a, x) => ((a[f(x)] = (a[f(x)] || 0) + 1), a), {}); return { kinds: tally(g.specs(), (s) => s.specKind), readiness: tally(g.specs(), (s) => s.statedReadiness), nodeTypes: tally(graph.nodes, (n) => n.nodeType), bindingNamespaces: tally(graph.nodes.filter((n) => n.nodeType === "CodeNode" || n.nodeType === "Anchor"), (n) => n.id.split(":")[0]), edgeClaims: tally(graph.edges, (e) => e.claim), locations: graph.locations.length, schema: graph.schemaVersion, packs: graph.nodes.filter((n) => n.nodeType === "Pack").map((p) => [p.id, p.members.length]), layers: tally(graph.nodes.filter((n) => n.id.startsWith("component:")), (n) => n.layer), implemented: g.specs().filter((s) => s.deliveryFacts.includes("implemented")).length, hasVerifier: g.specs().filter((s) => s.deliveryFacts.includes("has-verifier")).length }' --json -``` +[Part 2](./02-one-graph-many-views.md) develops that human-projection story and its implications for UX/UI design. [Part 4](./04-stop-building-verb-walls-for-coding-agents.md) takes the agent side. -The self-hosting `validate` run prints `0 errors · 6 warnings`, and all six are intentional, pinned by the self-hosting oracle: five `conformance/prose-mentions` warnings, where one Spec names another (three times in an open question's `links:` field, twice in rule text) without a declared relation between them, and one `honesty/gaps` warning on `spec:model.spec-sections`, an owner-ratified `ready` Spec with no resolving verifier. +## The Protocol measures itself -Meanwhile, the first adopter, [`libar-platform`](https://github.com/libar-dev/libar-platform) (a separate repository in the `libar-dev` organization), models a Convex transactional domain platform as **243 Specs across six Packs**, most of it designed ahead of its code. It runs design passes whose reading is composed within a 100,000-UTF-16-unit context bundle, checks its own architecture and policy rules through `sdp validate --rules`, and publishes 251 static **Spec Studio** pages. Those adopter figures were measured at the adopter's own pin and recorded in [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33), the Protocol work that made them possible; the adopter's side is [`libar-platform#13`](https://github.com/libar-dev/libar-platform/pull/13). +The Protocol has no separate PRD. It authors its own rules as Specs, binds its engine and tests to them, derives its own graph and checks that graph in CI ([`spec:protocol.self-hosting`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/protocol/self-hosting.sdp.md)). -The Spec Studio renders the same graph for people. Here is the Protocol's own `pack:corpus-checks-v1`, the Pack that carries corpus rules: +At commit `b5e0a8be`, its self-hosting graph reports: -![The Spec Studio Pack page for pack:corpus-checks-v1: a plain-language summary, the Pack's framing, and one collapsed card per member](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/protocol-pack.png) + +| Measure | Value | +|---|---| +| Authored Specs | 456: 447 `ready`, 4 `scoped`, 5 `idea` | +| Packs | 9 | +| Source and test bindings | 241 CodeNodes and 366 Anchors | +| Graph | 1,072 nodes, 2,978 edges, 3,120 locations | +| Delivery facts | 166 `implemented`, 397 `has-verifier`, 0 `observed` | +| Operational build backlog | 2 | +| Drift alarm | 0 | +| Runnable recipes | 55 | + -*The Pack page for `pack:corpus-checks-v1`, published by `sdp studio` and opened from `file://` with no network. It leads with a summary the graph computes, then the Pack's framing, then one card per member with its rung, floor, questions, and bindings. Captured at commit `30f196a` for [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33), so its counts can differ slightly from the table above.* +The self-hosting validation run reports 0 errors and 3 intentional `honesty/gaps` warnings, each a ready Spec without a resolving verifier. Re-run the commands below rather than carrying these numbers to another commit. ---- +The first adopter has also used the Protocol to design a much larger platform ahead of implementation. That story, including context bundles in parts, design slices and corpus rules, belongs to Part 9. -## 7. Scope, Non-Goals, and How Adopters Install Today +## What the Protocol does not try to be -Some things the Protocol deliberately is not ([`00` §5, non-goals](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md)): +The Protocol is not a ticket state machine. It adopts delivery nouns such as phase, iteration and release as descriptive vocabulary and projections, not mandatory gates. -- **Not a workflow state machine.** It adopts the delivery nouns (phase, iteration, milestone, release) as descriptive vocabulary and rejects the gates. "In sprint / in review / in QA" status stays in your issue tracker, linked through a separate index. -- **Not an embedded RBAC or governance engine.** Change approval lives in git and CI (pull request review, CODEOWNERS). A Spec names no owner, and an open question's `decider:` names who settles it, never an authorization. -- **Not an intent inferer.** Structure is extracted; intent is always authored. Inference assists and never asserts. -- **Not a replacement for adjacent tools.** It is the membrane that links issue trackers, design tools, LikeC4, and OpenAPI, not a substitute for them. -- **No separate PRD.** The jobs live in [`jtbd-stories/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/jtbd-stories) and the laws live in [`specs/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs). +It is not an embedded permissions system. Approval belongs in git and CI. -And where it stands today: +It does not infer intent. Mechanical structure can assist curation, but a person authors intent and bindings. -- **Install from a reviewed commit.** The package version is `0.0.0` and no npm prerelease is published. An adopter pins `"@libar-dev/software-delivery-protocol": "github:libar-dev/software-delivery-protocol#"` to a commit it reviewed; the package's `prepare` script builds the CLI, subpath exports, and shipped recipes during the git install, and `sdp --version` prints the pinned commit. The [README](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/README.md) covers the npm and pnpm build approvals. -- **Designed in, deferred.** The `observed` delivery fact (runtime liveness) and the MCP surface for user-facing apps (the MCP-deferred decision, D6) are designed for and not built. -- **Verdicts stay in CI.** `has-verifier` means a verifier binding exists. Pass, fail, skip, and quarantine never enter the graph. +It does not replace every adjacent tool. It links to issue trackers, design tools, API formats and evidence systems. ---- +At the pinned commit, installation is from a reviewed git commit rather than an npm release. The package version is `0.0.0`; `sdp --version` names the build commit. -## 8. Try It Yourself in 60 Seconds +## Try it -With Node 20 or later, clone the repository at the commit these numbers were measured at, and query the self-hosting graph through `sdp q`—the evaluation sink that derives the graph in process and runs any async JavaScript body you give it: +With Node 20 or later: ```sh git clone https://github.com/libar-dev/software-delivery-protocol.git cd software-delivery-protocol -git checkout 5a0b22c2d9ead98c900b60ed6209342fce2cbef6 -npm ci # the prepare script builds dist/ +git checkout b5e0a8bea1e3bedd48c616f96c9928e955792753 +npm ci -# 1. Validate the 276-Spec self-hosting corpus (0 errors · 6 warnings) -npm run --silent sdp -- validate . --exclude explorations --exclude examples --exclude test/fixtures/import/parity +npm run --silent sdp -- validate . \ + --exclude explorations --exclude examples --exclude test/fixtures/import/parity -# 2. Run recipe 1 (the operational build backlog) and recipe 2 (the drift alarm) npm run --silent sdp:q -- "$(cat dist/recipes/01-the-build-backlog.js)" --json npm run --silent sdp:q -- "$(cat dist/recipes/02-the-drift-alarm.js)" --json -# 3. Publish the static HTML Spec Studio, then open generated/spec-studio/index.html -npm run --silent sdp -- studio . --exclude explorations --exclude examples --exclude test/fixtures/import/parity +npm run --silent sdp -- studio . \ + --exclude explorations --exclude examples --exclude test/fixtures/import/parity ``` -The three `--exclude` flags keep the exploration exhibits, the separate worked example, and the carrier-parity fixtures (which deliberately duplicate IDs) out of the self-hosting root; the `sdp:q` script supplies them for you. - ---- +Open `generated/spec-studio/index.html`. The `sdp:q` script supplies the repository's three exclusions itself; the explicit CLI commands include them. -## Up Next in the Series +## Up next -In **Part 2: From Rough Idea to Verified Code** ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/48d8f495959c2a8adc6ffab4069d4ab60afbf716/docs/articles/02-walkthrough-intent-to-verified-code.md)), we open `examples/checkout-v1` and the Protocol's own source code to walk the complete engineering loop: authoring `.sdp.md` carriers (and where `.sdp.gherkin` fits), binding code with constant-form and `@sdp*` comment-form anchors, generating typed step and space contracts, writing an expected-outcome `specOracle`, and deliberately breaking the graph to watch the extraction, conformance, honesty, and contract checks fire. +[Part 2: One graph, many views](./02-one-graph-many-views.md) follows a reviewer through the Studio and asks the question that matters for the first UX/UI design pass: what can change freely when every interface is a projection? --- -*About this article: Part 1 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every measurement are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the numbers at a later commit.* +*About this article: Part 1 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753). Repository links and measurements are pinned to commit `b5e0a8be`; re-run the commands rather than quoting the numbers at a later commit.* diff --git a/docs/articles/02-one-graph-many-views.md b/docs/articles/02-one-graph-many-views.md new file mode 100644 index 000000000..09d8808bc --- /dev/null +++ b/docs/articles/02-one-graph-many-views.md @@ -0,0 +1,150 @@ +# One graph, many views: the interface is allowed to change + +*Part 2 in a series introducing the Libar Software Delivery Protocol. Links are pinned to one commit; see the footer.* + +A product manager opens a pull-request preview on a phone. The design under review has no implementation yet. They read the Pack's framing, inspect a blocking question, and open the Spec that carries it. A dependency points to an existing component, so they follow it to see what the new design would touch. A base graph shows what changed since the last review. They want a different example and a clearer constraint, so they gather both requests as scoped intent and hand the resulting proposal to an agent. + +The agent edits the carriers. Git records the change. The conformance and honesty checks run. A new graph produces a new page set. + +Nothing in that workflow requires the interface to be a database, an editor, or the one canonical way of reading a design. The interface is a projection. We can replace it without replacing the truth it presents. + +That freedom is the subject of this article. + +## The seed was a workbench, not a documentation page + +The original design, written before the implementation, described a generated HTML Spec Studio where people could explore Specs, architecture, tests and evidence. Its central scenario was not "read a requirement online." It was a person understanding a system, finding gaps, proposing a change and returning that change to the repository ([the seed vision](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/v0-design/00-vision-and-product.md), [the seed Studio design](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/v0-design/07-spec-studio-and-projections.md)). + +Some representations in that design have since been rejected. TypeScript is no longer the default carrier. JSON patches and codemods have given way to scoped intent and ordinary source edits. Test verdicts no longer enter the graph as readiness. The vocabulary has been ratified and narrowed. + +The product idea survived those changes: one delivery model should support reading and review experiences that raw carriers and flat documents cannot provide. + +The current Studio is an early realization of that idea, not its final visual design. Its pages, diagrams and interaction patterns give us working material to learn from. They are not a template that the first UX/UI designer must preserve. + +## One record, several readings + +The repository holds authored Specs, Packs and source anchors. The extractor derives one graph. Validators and consumers read that graph. A projection turns it into an artifact for a particular consumer ([`spec:consumers.projections-model`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/projections-model.sdp.md)). + +At the pinned commit, the package publishes five projection roots: + +| Projection | Consumer and reading | +|---|---| +| Design Review | A Spec or Pack rendered in context as Markdown, with the relations and bindings needed for a readiness decision | +| Census | Taxonomy, readiness, bindings and findings as a compact report | +| Mermaid | Relationship and architecture diagrams with their source available for inspection | +| Gherkin | A familiar behavioral reading of the graph | +| Spec Studio | A static HTML workbench for exploration, review and composing intent | + +The graph also serves `sdp q`, CLI answers and typed contracts. An Obsidian vault provides generated notes and Bases beside a person's own notes. These are different readings, not competing repositories of delivery status. + +This breadth is intentional. We have not settled the human experience by shipping several output forms. We have made it cheap to discover which forms earn their place. + +The article's diagram evidence comes from the same mechanism. This Mermaid source is emitted by `sdp mermaid` from the checkout roadmap Pack, not redrawn for publication: + + +```mermaid +flowchart LR + n_007000610063006b003a0063006800650063006b006f00750074002d0072006f00610064006d00610070["Checkout roadmap pack:checkout-roadmap Pack"] + n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d006f00640065006c["Order-management domain vocabulary spec:orders.order-model Primitive"] + n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d0070006c006100630065006d0065006e0074002d0066006c006f0077["Order placement flow spec:orders.order-placement-flow Primitive"] + n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d006d006f00640065006c -->|"belongsTo [declared]"| n_007000610063006b003a0063006800650063006b006f00750074002d0072006f00610064006d00610070 + n_0073007000650063003a006f00720064006500720073002e006f0072006400650072002d0070006c006100630065006d0065006e0074002d0066006c006f0077 -->|"belongsTo [declared]"| n_007000610063006b003a0063006800650063006b006f00750074002d0072006f00610064006d00610070 +``` + + +The rule that matters is not "everything must look the same." It is "no view may quietly become another source of truth." + +## What the current Studio already does + +`sdp studio` publishes a static page set that works from `file://` or a static host. It needs no application server. Every join, delivery fact, readiness floor and finding is computed during the build. The browser does not assemble a second graph ([`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/spec-studio.sdp.md)). + +A reader can move through five lenses: Packs, capabilities, architecture, tests and evidence. Other pages collect the roadmap, the owner queue, open questions, decisions, declarations, the design backlog and the glossary. A build can also include a design diff from a supplied base graph and run evidence from supplied CTRF reports. A Pack can be exported as one self-contained HTML file. + +For a pull-request reviewer, the important detail is the stamp. Publishing metadata names the corpus commit and engine after the page set is built. The page set itself remains a deterministic function of its graph and named inputs ([`spec:consumers.spec-studio.sharing`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/spec-studio.sharing.sdp.md)). + +This is already enough to test a real review workflow. It is not evidence that five lenses, the current navigation or today's information density are the right long-term answers. Those are UX questions. + +## Honesty has to be visible + +A flexible interface still has obligations to the model. The important ones are not visual conventions. They are rules against misleading the reader. + +**Intent, bindings and mechanical structure need different meanings.** A `declared` relation is authored intent. An `anchored` relation is a human binding from code or tests. `inferred` structure is machine-derived and advisory. A view may use words, placement or typography to make that difference legible. It must not merge the three into an undifferentiated web of connections. + +**A binding is not a verdict.** `implemented` means a resolving `satisfies` binding exists. `has-verifier` means a resolving verifier exists. Neither means the code works or a test passed. Supplied run records can appear beside those bindings, labeled with their input and commit. They remain external run evidence, not new delivery facts. + +**Readiness and realization are independent.** An author states readiness. The graph derives the highest structural floor reached and the delivery facts. A useful review page lets a person see all three without turning them into a single progress score. + +**Absence needs words.** `coverage-unknown`, a missing verifier and an evidence lens with no tracked observations say different things. An empty panel with no explanation forces the reader to guess which absence the system means. + +**A drawing needs a text equivalent.** Studio diagrams retain words that state every box and edge. When a drawing exceeds its bound, it folds and the text remains. This is an accessibility rule and an honesty rule: visual compression must not remove meaning. + +These constraints leave many visual choices open. They rule out interface shortcuts that would invent certainty. + +## The view composes intent, not edits + +The seed design used a patch panel. The current design has a composing panel instead. + +A person can gather requests while reading, scoped to a Spec, an entry, a Pack or an open question. The panel produces a change proposal and its Markdown memo when it has a base and a reason. Anything the proposal contract cannot carry stays as scoped-intent text. The page sends nothing over the network and opens no pull request ([`spec:consumers.spec-studio.intent-panel`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/spec-studio.intent-panel.sdp.md)). + +An agent receives that hand-off and edits ordinary source. Git records the result. The same checks apply to that edit as to one typed directly into a carrier. + +The distinction matters for UX. The person is not manipulating a shadow copy of the graph. They are explaining what they want changed, with enough scope for another reader, human or agent, to act on it. + +## A compact brief for the first UX/UI design pass + +Use the current Studio as a working prototype and the seed design as product context. Do not start by restyling today's pages. Start with the review jobs. + +The fixed ground: + +- The repository is canonical and the graph is the sole read model. +- A projection is disposable and regenerable. No interface state becomes delivery truth. +- Claims, stated readiness, derived readiness and delivery facts stay distinct. +- Outside inputs and their commits stay named wherever they appear. +- Unknowns and missing evidence remain visible. +- Diagrams keep complete text equivalents. +- Changes leave as scoped intent. Source edits and git history happen outside the view. +- Conformance and honesty checks never judge visual quality or design quality. + +The open design space: + +- Navigation, page structure, visual hierarchy, typography and interaction patterns. +- Which projections deserve to survive, merge or disappear. +- How a person moves between a Pack overview, one Spec, an entry and its source. +- How desktop and phone review differ. +- How much information belongs in the first reading and what belongs behind disclosure. +- How a future AI assistant can steer a reading without obscuring its basis. + +The first design pass should answer these questions with prototypes: + +1. What can a reviewer accomplish from a stamped link without opening the repository? +2. What is the fastest route from "what changed?" to "is this design ready?" for one Pack? +3. How should a reader distinguish intent, code bindings, inferred structure and supplied run evidence without learning the implementation schema first? +4. How do missing verification, missing observations and unknown coverage look different? +5. When is a diagram better than prose, and how does its text equivalent read on a phone? +6. How does a person gather several related requests and understand exactly what will be handed to an agent? +7. Which current pages solve real review jobs, and which only expose data because it is available? + +The deliverable should be a small set of task-based experiences, not a comprehensive design system for every page the generator can emit. + +## Try the material + +From a checkout at the pinned commit: + +```sh +git clone https://github.com/libar-dev/software-delivery-protocol.git +cd software-delivery-protocol +git checkout b5e0a8bea1e3bedd48c616f96c9928e955792753 +npm ci + +npm run --silent sdp -- generate . \ + --exclude explorations --exclude examples --exclude test/fixtures/import/parity +``` + +Open `generated/spec-studio/index.html`. Compare it with `generated/design-review/`, `generated/census/`, `generated/mermaid/` and `generated/gherkin/`. They are different answers over one graph, not five versions of the truth to reconcile. + +## Up next + +Part 3, [From rough idea to verified code](./03-walkthrough-intent-to-verified-code.md), leaves the reading surfaces and follows one Spec through authoring, source bindings, generated contracts and executable verification. + +--- + +*About this article: Part 2 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753). Repository links are pinned to commit `b5e0a8be`. The seed documents explain the original product experience; current Specs and source determine what is built now.* diff --git a/docs/articles/02-walkthrough-intent-to-verified-code.md b/docs/articles/02-walkthrough-intent-to-verified-code.md deleted file mode 100644 index 1f9772d91..000000000 --- a/docs/articles/02-walkthrough-intent-to-verified-code.md +++ /dev/null @@ -1,360 +0,0 @@ -# From Rough Idea to Verified Code: A Hands-On Tour of the Libar Software Delivery Protocol - -*Part 2 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* - -In Part 1 ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/48d8f495959c2a8adc6ffab4069d4ab60afbf716/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), we introduced the core thesis of the **Libar Software Delivery Protocol**: model the delivery lifecycle as instances of one enrichable primitive (`Spec`), bind code and tests with source `anchor`s, derive **one regenerable graph**, and check **conformance and honesty** in CI. - -That sounds clean in principle. What does it actually look like in a text editor and a terminal? - -In this post, we will trace a single bounded context—**Order Management** ([`examples/checkout-v1`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1)), alongside real comment-form bindings from the Protocol's own engine—from an initial `.sdp.md` Spec all the way to generated TypeScript contracts, an expected-outcome oracle, and deterministic CI checks. - ---- - -## 0. Setup - -Every command in this tour runs from a checkout of the repository at the commit the outputs were measured at: - -```sh -git clone https://github.com/libar-dev/software-delivery-protocol.git -cd software-delivery-protocol -git checkout 5a0b22c2d9ead98c900b60ed6209342fce2cbef6 -npm ci # the prepare script builds dist/ -``` - -We run the CLI as `npm run --silent sdp -- …`, never as a bare `sdp`. Inside its own checkout the package is not linked into `node_modules/.bin`, so a bare `sdp` resolves to nothing, or, on macOS, to Apple's unrelated `/usr/bin/sdp`. (In an adopter repository that installs the package, `npx sdp` or `pnpm exec sdp` works.) `examples/checkout-v1` is its own extraction root, so the commands below pass it as the root and need none of the three fixture `--exclude` flags the repository root uses. - ---- - -## 1. The Worked Example: 11 Specs, 1 Pack, 5 Anchors - -The repository ships a complete, self-contained bounded context under [`examples/checkout-v1`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md). It models Order Management (`pack:checkout-v1`) using all eight `Spec` kinds (`behavior`, `workflow`, `example`, `rule`, `constraint`, `model`, `decision`, `contract`) and all four readiness rungs (`idea`, `scoped`, `defined`, `ready`). Each carrier's `kind · altitude · stated readiness`: - -```text -examples/checkout-v1/ - specs/ - checkout.pack.sdp.md # Pack manifest (pack:checkout-v1) - orders/order-management.sdp.md # behavior · epic · defined - orders/order-placement-flow.sdp.md # workflow · feature · scoped - orders/create-order.sdp.md # behavior · feature · defined (owns the example space) - orders/create-order-valid-cart.sdp.md # example · story · ready (enabled verifier) - orders/create-order-invalid-cart.sdp.md # example · story · defined (declared only) - orders/create-order-api-contract.sdp.md # contract · story · idea (blocking question) - orders/order-total-rule.sdp.md # rule · story · defined - orders/order-inventory-rule.sdp.md # rule · story · defined - orders/order-latency-constraint.sdp.md # constraint · story · defined - orders/order-model.sdp.md # model · story · defined - decisions/order-lifecycle.sdp.md # decision · feature · defined - src/orders/ - create-order.use-case.ts # impl:orders.create-order-use-case + impl:orders.order-total - create-order.route.ts # api:orders.post - test/orders/ - create-order.valid-cart.test.ts # test:orders.create-order.valid-cart - create-order.oracle.ts # oracle:orders.create-order - orders.create-order.valid-cart.test.generated.ts # committed adopted registrar -``` - -Build the example's graph: - -```sh -npm run --silent sdp -- build examples/checkout-v1 -``` - -```text -11 specs · 1 packs · 5 anchors → 17 nodes · 32 edges (0 errors, 0 warnings) -Wrote /examples/checkout-v1/generated/graph.json -Wrote /examples/checkout-v1/generated/contracts (3 modules) -``` - -### Your first graph question: the drift alarm - -Before opening a single file, ask the graph which Specs have code ahead of their design. That is recipe 2, the drift alarm (`implemented ∧ ¬ready`): - -```sh -npm run --silent sdp -- q "$(cat dist/recipes/02-the-drift-alarm.js)" --root examples/checkout-v1 --json -``` - -```json -{ - "total": 2, - "alarms": [ - { "id": "spec:orders.create-order", "statedReadiness": "defined", "floorReached": "ready", "firstUnmetClause": null, "implementationBindings": 2 }, - { "id": "spec:orders.order-total-rule", "statedReadiness": "defined", "floorReached": "ready", "firstUnmetClause": null, "implementationBindings": 1 } - ] -} -``` - -Both Specs carry `implemented`, because code anchors bind to them through `satisfies`, while their authors state only `defined`. Their structural floor already reaches `ready` (`firstUnmetClause: null`), so nothing structural stands in the way. Yet `npm run --silent sdp -- validate examples/checkout-v1` reports `0 errors · 0 warnings`. - -That is the point. The drift alarm is a graph query, not a CI failure. The [worked dialogue in `CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md) already speaks this way: asked "Anything dishonest in the pack?", its agent reports a drift alarm on an order rule—"an anchor binds code to it, but the design isn't mature"—as a fact to act on, not a broken build. Reaching `ready` takes the floor cleared **plus a human's `declared` statement**. Here the floor is cleared, so each Spec waits only on a person who reads it in a Design Review and states `ready`. That a review happened is never recorded in the graph or checked; doing so would turn the Protocol into a workflow gate. - -Now let's inspect the pieces that produce those 17 nodes and 32 edges. - ---- - -## 2. Authoring Intent: The Parent Spec and Its Typed Example Space - -Open [`examples/checkout-v1/specs/orders/create-order.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/specs/orders/create-order.sdp.md): - -````markdown ---- -id: spec:orders.create-order -kind: behavior -altitude: feature -readiness: defined -relations: - refines: spec:orders.order-management - constrainedBy: spec:orders.order-latency-constraint - decidedBy: spec:decisions.order-lifecycle ---- -# Customer creates an order - -## Intent - -- actor: customer -- outcome: Turn a valid cart into an order. -- value: Customers can complete purchases without the example modeling the rest of checkout. - -## Example space - -```gwt-vocabulary -Given a customer has a cart with {n:number} line items -And every line item has quantity {q:number} and unit price {price:number} -And every cart item is {availability:"in stock"|"out of stock"} -When the customer submits the cart for order creation -Then an order is created with total {total:number} -And the order contains the original cart lines -And order creation is rejected because {reason:"empty cart"|"out of stock"} -``` -```` - -Notice three things about this carrier: - -1. **The YAML frontmatter is the closed `envelope` ([`spec:carrier.envelope-contract`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/envelope-contract.sdp.md)).** It carries `id`, `kind`, `altitude`, `readiness`, and `relations`; the H1 supplies the title. Nothing else is permitted in frontmatter: a derived key such as `implemented` or `deliveryFacts` is refused at extraction (`extract/reserved-property`), and a derived edge such as `satisfies` under `relations` is refused as well (`extract/invalid-frontmatter`). -2. **Sections carry content; relations carry links ([content-only sections, MD-10](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/content-only-sections.sdp.md)).** `create-order` links to its parent (`refines: spec:orders.order-management`), its latency NFR (`constrainedBy: spec:orders.order-latency-constraint`), and the shaping decision record (`decidedBy: spec:decisions.order-lifecycle`) in `relations`, never buried inside bullet text. -3. **The `gwt-vocabulary` fence defines a typed `example space` ([`spec:carrier.slot-notation`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/slot-notation.sdp.md)).** Each `{slot:type}` placeholder—`{n:number}`, `{availability:"in stock"|"out of stock"}`, `{reason:"empty cart"|"out of stock"}`—declares the shared parameter vocabulary that child examples will bind. - -### Binding a Point in the Space - -Now look at the child example [`examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md). In an `example` Spec, one `gwt` fence closes the `## Intent` section and binds the point: - -````markdown ---- -id: spec:orders.create-order.valid-cart -kind: example -altitude: story -readiness: ready -relations: - refines: spec:orders.create-order - verifies: spec:orders.create-order ---- -# Valid cart creates an order - -## Intent - -- outcome: Show that a valid cart can become an order. -- value: The authored example demonstrates the happy path for create-order. - -```gwt -Given a customer has a cart with {n: 2} line items -And every line item has quantity {q: 1} and unit price {price: 50} -And every cart item is {availability: "in stock"} -When the customer submits the cart for order creation -Then an order is created with total {total: 100} -And the order contains the original cart lines -``` - -## Verification — executable - -- The order result contains a stable id. -- The returned total matches the authored total — the spec's value, never the test's. -```` - -Under [the point-per-example law (MD-17)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/point-per-example.sdp.md), each `example` Spec binds one concrete point in its parent's example space (`n: 2, q: 1, price: 50, availability: "in stock" → total: 100`). Under the **concreteness law** ([`spec:validation.kind-evidence`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/kind-evidence.sdp.md)), if any slot used in a step is left unbound (for instance, `{n}` instead of `{n: 2}`), the example is capped below `defined` and `sdp build` withholds its step contract and its registrar. - -> **Prefer Gherkin for behavior and example Specs?** Under [the Gherkin carrier option (MD-27)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/gherkin-carrier-option.sdp.md) and [the `.sdp.gherkin` extension (MD-28)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/sdp-gherkin-extension.sdp.md), any `behavior` or `example` Spec ID can instead use a graph-aware `.sdp.gherkin` file as its one canonical surface. A parity example proves that a Markdown twin and its Gherkin twin derive equal graphs and equal contracts ([`spec:carrier.gherkin-authoring.contract-parity`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/gherkin-authoring.contract-parity.sdp.md)). The Gherkin carrier has no form for open questions, their fields, or relation reasons, so a Spec that needs one stays in Markdown ([`spec:carrier.gherkin-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/carrier/gherkin-authoring.sdp.md)). The Protocol's own reader Spec, [`spec:consumers.reader`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/reader.sdp.gherkin), is authored this way. - ---- - -## 3. Binding Source Code: Constant Form and Comment Form - -How does source code connect to `spec:orders.create-order`? Through an in-code **anchor** ([`spec:model.anchors`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/anchors.sdp.md)). - -An anchor is a human-authored binding assertion in a `.ts` or `.tsx` file. It never carries Spec prose, readiness, or status. It can be written in **two representations** that feed the exact same closed envelope. - -Both rest on **Principle P6, identity is the universal join key** ([founding principles](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md)). Specs and code link by string IDs such as `"spec:orders.create-order"`, never by TypeScript imports between code and Specs. That is why a refactor that moves or renames files never breaks the trace, why the generated contracts never import product code, and why the comment form below needs no import at all. - -### Representation A: The Constant Form -When your code may import `@libar-dev/software-delivery-protocol` (or its zero-dependency subpath `@libar-dev/software-delivery-protocol/anchors`, which loads no `node:*` module), you write a top-level constant, as in [`examples/checkout-v1/src/orders/create-order.use-case.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/src/orders/create-order.use-case.ts#L23-L35): - -```ts -import { codeAnchor, codeAnchorId, ref } from "@libar-dev/software-delivery-protocol"; - -// … (lines 3–21: the cart and order types) … - -export const createOrderUseCaseAnchor = codeAnchor({ - id: codeAnchorId("impl:orders.create-order-use-case"), - label: "createOrderFromCart", - satisfies: ref("spec:orders.create-order"), -}); - -// A second binding in the same file: anchors bind per spec, never per file (MD-8) — the total -// reduction below is where the order-total rule is implemented. -export const orderTotalRuleAnchor = codeAnchor({ - id: codeAnchorId("impl:orders.order-total"), - label: "cart-math order total", - satisfies: ref("spec:orders.order-total-rule"), -}); -``` - -### Representation B: The Comment Form (Zero Runtime Imports) -What if your runtime code cannot import the package? The first adopter's could not, so every one of its code anchors sat in a test or a harness, and asking the graph about a source file returned nothing. In [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29), that experience led to three rulings: [the comment form (MD-36)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md), [bindings are optional, plural, and may reference a design (MD-37)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md), and [architectural significance is annotated where it is realized (MD-38)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/architectural-annotation.sdp.md). - -You can write a top-level `/** … */` comment using reserved camelCase `@sdp*` tags with **zero imports**. Here is a real anchor from the Protocol's own census projection in [`src/projections/census.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src/projections/census.ts#L21-L28): - -```ts -/** - * @sdpAnchor impl:protocol.census-page - * @sdpLabel renders the derived census and runtime taxonomy projection - * @sdpSatisfies spec:consumers.census-page - * @sdpComponent component:protocol.projections - * @sdpReferences spec:decisions.architectural-annotation - * @sdpRole projection - */ -``` - -Look closely at the distinction between `@sdpSatisfies` and `@sdpReferences` (the ruling that [bindings are optional, plural, and may reference a design, MD-37](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md)): - -- **`@sdpSatisfies` (`satisfies`):** This unit **realizes** the Spec's promises together with every other unit that satisfies the same Spec; here a second unit, the `sdp census` command in [`src/cli/census-command.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/src/cli/census-command.ts#L64-L70), completes the set. The set confers the `implemented` delivery fact on `spec:consumers.census-page`. -- **`@sdpReferences` (`references`):** This unit **answers to** a design or decision (`spec:decisions.architectural-annotation`) without claiming to realize it—or realizes only part of a Spec whose remaining promises are not yet built. It puts a `references` edge in the graph so `blastRadius` and impact queries traverse it, **without conferring `implemented` and without tripping the drift alarm**. - -If you write a stub or follow a decision record, you use `references`; code never satisfies a decision Spec, because a ruling is not something code realizes. You never have to lie with `satisfies` just to get traceability. - ---- - -## 4. The Executable Half: Generated Contracts, Registrars, and Oracles - -How do we verify `spec:orders.create-order.valid-cart` so that if someone changes `{total: 100}` to `{total: 120}` in the Markdown Spec, the test suite fails immediately—without brittle Cucumber regexes? - -When `npm run --silent sdp -- build examples/checkout-v1` runs, it emits **derived TypeScript contracts** under `generated/contracts/` (one step contract per example and one space contract for the parent) and a **registrar** beside the test ([`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md)). Because the authored test imports that registrar, it is committed and byte-checked rather than ignored (the ruling that [adopted registrars are committed, MD-31](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/adopted-registrars-committed.sdp.md)). - -Three files meet at test time. Only the first is generated; the oracle and the test are written by hand: - -1. **The Space Contract (`orders.create-order.space.ts`):** Exports `CreateOrderConditions` (`{ n: number; q: number; price: number; availability: "in stock" | "out of stock" }`), every child's bound point, and the `CreateOrderOutcome` discriminated union derived from the parent's `Then` steps. -2. **The Expected-Outcome Oracle ([`test/orders/create-order.oracle.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.oracle.ts#L12-L48)):** Models the expected outcome across the entire example space, bound by `specOracle`. Where the Spec states no outcome, it returns `unspecified` rather than guessing: - -```ts -export const createOrderOracle = specOracle({ - id: oracleAnchorId("oracle:orders.create-order"), - label: "expected create-order outcome over the example space", - models: ref("spec:orders.create-order"), -}); - -// … (lines 18–21: a comment on the generated input and return types) … -export function expected(conditions: Partial): CreateOrderOutcome { - if ( - conditions.n === undefined || - conditions.q === undefined || - conditions.price === undefined || - conditions.availability === undefined - ) { - return unspecified; - } - - if (conditions.n === 0) { - return { kind: "order creation is rejected because {reason}", reason: "empty cart" }; - } - - if (conditions.availability === "out of stock") { - return { kind: "order creation is rejected because {reason}", reason: "out of stock" }; - } - - if (conditions.q <= 0 || conditions.price <= 0) { - return unspecified; - } - - return { - kind: "an order is created with total {total}", - total: conditions.n * conditions.q * conditions.price, - }; -} -``` - -3. **The Bound Test ([`test/orders/create-order.valid-cart.test.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/test/orders/create-order.valid-cart.test.ts#L13-L58)):** Anchors `test:orders.create-order.valid-cart` and activates the generated registrar: - -```ts -export const createOrderValidCartTest = specTest({ - id: testAnchorId("test:orders.create-order.valid-cart"), - label: "valid cart verifies the create-order happy path", - verifies: ref("spec:orders.create-order.valid-cart"), -}); - -// … (lines 19–56: the World type and the four adapters listed below) … - -registerValidCart({ createWorld, invoke, observe, expected, assertions }); -``` - -The registrar takes five adapter functions, all defined in that test file except `expected`: - -- `createWorld(point)` builds a fresh world from the generated point, the Spec's own bound values. -- `invoke(world)` runs the system under test, here `createOrderFromCart`. -- `observe(world)` reads what the system did, typed as a `CreateOrderOutcome`. -- `expected(point)` is the oracle above. -- `assertions(world)` adds checks the outcome union does not carry, such as the stable order id. - -At the matching `Then` step, the generated registrar makes two checks, both against the oracle ([`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md)): - -1. **The Spec's authored values against the oracle:** `{ total: 100 }` from the `.sdp.md` file against `2 * 1 * 50 = 100` from `expected(point)`. -2. **The observed outcome against the oracle:** `world.order.total` from `createOrderFromCart`, read through `observe(world)`, against the same oracle outcome. - -If the Markdown Spec, the oracle, or the implementation drifts, one of the two checks fails and names the step that diverged. Passing the second check alone is not enough: a changed Spec value must fail the test even when the code and the oracle still agree. - -Meanwhile, `spec:orders.create-order.invalid-cart` is authored at `defined` and declares `verifies: spec:orders.create-order`, but has no `specTest` anchor yet. Run recipe 10 (declared versus enabled verifiers) against the example: - -```sh -npm run --silent sdp -- q "$(cat dist/recipes/10-declared-versus-enabled-verifiers.js)" --root examples/checkout-v1 --json -``` - -```json -{ - "total": 2, - "withDeclaredOnly": 1, - "rows": [ - { - "id": "spec:orders.create-order", - "declared": ["spec:orders.create-order.invalid-cart", "spec:orders.create-order.valid-cart"], - "enabled": ["spec:orders.create-order.valid-cart"] - }, - { - "id": "spec:orders.create-order.valid-cart", - "declared": [], - "enabled": ["test:orders.create-order.valid-cart"] - } - ] -} -``` - -The `spec:orders.create-order` row lists both examples under `declared` and only `valid-cart` under `enabled`, and the top-level `withDeclaredOnly: 1` counts the one Spec with a declared verifier that is not enabled. Because `invalid-cart` states `defined` (not `ready`), it sits quietly as honest incomplete trace data without a false warning ([the unbound-example posture, `spec:validation.unbound-example-posture`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/unbound-example-posture.sdp.md)). An unbound example that states `ready` would draw a `conformance/verifies-linkage` warning. - ---- - -## 5. Break It on Purpose: Watching the Checks Fire - -The fastest way to appreciate the checks is to trip them in `examples/checkout-v1`. Run each experiment from the repository root, then revert the edit (`git checkout -- examples/checkout-v1`) and rerun `npm run generate:example` to restore the generated files. All paths below are under `examples/checkout-v1/specs/orders/`. - -| Experiment | What you edit | Run | What happens | -|---|---|---|---| -| **1. Misspell a section heading** (extraction) | In `order-total-rule.sdp.md`, change `## Rule` to `## Rul` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `extract/unrecognized-heading` (`heading "Rul" is not recognized; did you mean "Rule"?`), plus two `extract/invalid-markdown-structure` errors (`Intent entries require a field name`), because the rule bullets beneath the unrecognized heading now fall under `## Intent`. The Spec drops out of the graph (10 specs) and `graph.json` is not written | -| **2. Dangle a relation target** (conformance) | In `create-order.sdp.md`, change `refines: spec:orders.order-management` to `refines: spec:orders.order-managment` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `conformance/referential-integrity` (`points to missing target "spec:orders.order-managment". Did you mean "spec:orders.order-management"?`); the suggestion is the unique nearest ID within edit distance ≤ 2 | -| **3. Keep a rung the Spec no longer earns** (honesty) | In `create-order.sdp.md` (stated `defined`), add `### Open questions` with `- [blocking] Wait?` at the end of `## Intent`, before `## Example space` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `honesty/readiness-floor`, naming the unmet clause `no-blocking-open-questions` | -| **4. Leave a parameter slot unbound** (honesty) | In `create-order-valid-cart.sdp.md`, change `{n: 2}` to `{n}` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits `1` with `honesty/readiness-floor`, naming the unmet clause `kind-evidence-complete`, and writes two contract modules instead of three: the example's step contract and its registrar are withheld. `sdp build` alone exits `0` with the same two modules, because `sdp build` fails only on extraction errors and the readiness floor is an `sdp validate` check | -| **5. Drift a Spec value from code** (contract) | In `create-order-valid-cart.sdp.md`, change `{total: 100}` to `{total: 120}` | `npm run generate:example && npx vitest --run examples/checkout-v1/test` | The bound test fails at `Then an order is created with total 120` with `Spec Then parameters do not match the oracle payload` and `$.total: expected 120, actual 100`: the regenerated contract supplies the Spec's value, and the oracle and implementation still say `100` | - ---- - -## Up Next in the Series - -We have seen how Specs and anchors enter the repo and how the extractor and validators keep them honest. In **Part 3: Stop Building 30-Verb CLI Walls for Coding Agents** ([read it here](./03-stop-building-verb-walls-for-coding-agents.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/48d8f495959c2a8adc6ffab4069d4ab60afbf716/docs/articles/03-stop-building-verb-walls-for-coding-agents.md)), we look at what happens *downstream* of the graph: how AI coding agents script `sdp q` across 43 runnable recipes, how a design pass fits one capability Pack of the first adopter's 243-Spec corpus into a context bundle, and how humans review the same graph in the static HTML **Spec Studio**. - ---- - -*About this article: Part 2 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every command output are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the outputs at a later commit.* diff --git a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md b/docs/articles/03-stop-building-verb-walls-for-coding-agents.md deleted file mode 100644 index 49e1274a4..000000000 --- a/docs/articles/03-stop-building-verb-walls-for-coding-agents.md +++ /dev/null @@ -1,244 +0,0 @@ -# Stop Building 30-Verb CLI Walls for Coding Agents: Scripting the Delivery Graph with `sdp q` and Spec Studio - -*Part 3 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* - -When teams build CLI tools or MCP servers for AI coding agents, they often fall into the same trap: they build a **verb wall**. - -First, they add `cli list-specs`. Then an agent needs to filter by readiness, so they add `--readiness`. Then an agent needs to see which tests verify a spec, so they add `cli show-verifiers`. Soon there are 30 subcommands and 80 flags. Yet the first time an agent asks a composite question—"show me every `defined` Spec in this Pack that has no blocking questions, together with the files that reference it"—no single verb answers. The agent makes 15 sequential tool calls, blows its context window on verbose CLI output, and stitches the join together in its own scratchpad. - -In the **Libar Software Delivery Protocol**, we took the opposite path ([the agent surface scripts the graph decision (D5)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-surface-scripts-graph.sdp.md); [the agent front door (MD-22)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-front-door.sdp.md)): - -> **Don't give coding agents a wall of verbs. Give them a visible, typed graph and one evaluation sink they can script.** - ---- - -## 1. The Demand Map: How Coding Agents Actually Arrive - -Before designing [`spec:consumers.agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.sdp.md), we asked a simple question: *What is an agent holding in its hand when a session starts?* - -Never the Spec ID it is looking for. An agent arrives holding one of three things—what the Protocol calls the **demand map**: - -1. **A concept string** from a user prompt ("where do we enforce the readiness floor?"). -2. **A file path** it is about to read or edit (`src/projections/census.ts`). -3. **A changeset** of modified files from `git diff` (`["src/reader/reader.ts", "src/cli/q-command.ts"]`). - -So the Protocol's typed reader, `g` ([`spec:consumers.reader`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/reader.sdp.gherkin), itself authored in the Protocol's Gherkin carrier), freezes a deliberately small surface. Four members expose the graph as decoded data: - -- `g.graph` — the raw graph schema, every node and edge, for any question the other members do not shape. -- `g.specs()` and `g.packs()` — one summary per Spec (its coordinates, stated and derived readiness, delivery facts, and Packs, already computed) and one per Pack (its title, framing, and carrier file). -- `g.findings()` — the validation report's findings, the same ones `sdp validate` prints. - -Two members assemble per-node context: `g.specContext(id)` (one Spec with its sections, its relations in both directions with each edge's claim decoded, its bindings and verifiers, its design columns, the floor clauses it fails, and the findings that name it) and `g.packContext(id)` (one Pack as a review unit: framing, members in authored order, and its design and boundary). - -The last three are the demand map's entry adapters: - -- `g.findByConcept(term)` — a deterministic, case-insensitive substring match over IDs, titles, binding labels, Pack framing, Spec narratives, and section values (section keys only in the `model` section), naming the field each match was found in. -- `g.byFile(path)` — every node the graph records at `path` (a Spec or Pack authored in that carrier, or a `CodeNode` or `Anchor` declared there, including zero-import `@sdp*` comment-form anchors), plus every Spec reachable from them. -- `g.blastRadius(files)` — the file-level impact of a changeset over the curated graph: the directly impacted Specs and Packs, the one-hop `atRisk` neighborhood with each connecting edge's type and claim, changed code units bound to no Spec (`unlinked`), and **`coverageUnknown`**: the honest list of changed files at which the graph records nothing. - -`blastRadius` deliberately never consults an import graph. The Protocol's two-surface rule (a corollary of [Principle P10](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md)) keeps mechanical structure inferred and apart: `sdp build --imports` can write a TypeScript adapter's file-level imports to `generated/imports.json` as external, inferred input, and the curation report (recipe 40, [`spec:consumers.impact-graph.curation-assist`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/impact-graph.curation-assist.sdp.md)) ranks load-bearing files no anchor covers and component imports that disagree with the anchored `uses` edges. That report assists a person curating anchors; it never adds an edge, a binding, or a delivery fact. A gap between the two surfaces is curation, not drift, because a sparse, chosen architecture is the point. - -### One Evaluation Sink: `sdp q` - -Instead of minting CLI subcommands for every question, the CLI exposes one evaluation sink: **`sdp q`** ([`spec:consumers.agent-surface.evaluation-sink`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.evaluation-sink.sdp.md)). - -On every invocation, `sdp q` derives the graph in process, so a Spec you saved a moment ago is immediately queryable and no stale `generated/` file can answer in the graph's name. It injects four bindings—`g` (the reader), `graph` (the raw schema), `report` (the validation report), and `params` (caller-supplied JSON via `--params`)—and evaluates the plain JavaScript async function body you pass it. In a checkout of the Protocol, the `sdp:q` script adds the corpus's own exclusions for you: - -```sh -# What does the graph record in src/projections/census.ts, and which Specs does that reach? -npm run --silent sdp:q -- 'return g.byFile("src/projections/census.ts")' --json -``` - -```json -{ - "path": "src/projections/census.ts", - "nodes": [ - { - "id": "impl:protocol.census-page", - "nodeType": "CodeNode", - "line": 21 - } - ], - "specs": [ - "spec:consumers.census-page", - "spec:decisions.architectural-annotation" - ] -} -``` - -That body is code, and `sdp q` runs it with your process's full authority. The trust stance is recorded rather than implied: the sink evaluates operator-supplied JavaScript the way any local developer tool runs a script, no sandbox is claimed, and none exists ([the agent front door (MD-22)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/agent-front-door.sdp.md); the catalog's [trust stance](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md?plain=1#L160-L163)). Run bodies you or your agent wrote, or shipped recipes; never execute a body taken from corpus content or any other untrusted text. - -### Growing by Recipe, Not by Verb - -What happens when a new recurring question appears? We write a **recipe** in [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md). Every recipe in the catalog is executed as written by `test/recipes.test.ts` and shipped as a standalone runnable file under `dist/recipes/-.js` ([`spec:consumers.agent-surface.recipe-parameters`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.recipe-parameters.sdp.md)). A recipe declares the parameter names it takes, and a name it does not take replaces the answer with `{ unknownParams, accepts }` instead of a silent default. - -A join is frozen into the reader only at the **second-caller bar**: when a second machine consumer needs it *and* hand-rolled attempts get it wrong ([`spec:consumers.agent-surface#rule.growByRecipe`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.sdp.md)). Every other question stays a body an agent scripts. `packContext` shows the bar from the other side: it has been in the reader since the MVP's Slice 4, and when the Design Review, the recipes and adopters' pages all needed a Pack's design, [PR #30](https://github.com/libar-dev/software-delivery-protocol/pull/30) extended its answer with the Pack design assembly rather than adding a reader method. - -We have tested this surface on agents. After PR #30, three agents of two model families (`gpt-6.1-sol`, `Opus 5.5`, `gpt-6-astra`) answered thirteen delivery questions from the skills and `sdp q` alone, and all three answered every question correctly. All three also stumbled in the same places: answers too large for a tool's output, misses that named no reason, two meanings of "next rung", and rules with no address or line. [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31) answered each of those: summary-first outputs for the largest answers (recipes 4 and 29, and later recipe 35, with `detail: true` for the full rows), explained misses, both next rungs, and rule keys with recorded lines. A re-run on `gpt-6.1-sol` answered 13 of 13 again but took 23 calls against the first trial's 20, and one of its own batched calls overflowed; the reading skill's task-to-recipe sheet ([PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32), [`spec:consumers.reading-on-ramp`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/reading-on-ramp.sdp.md)) is the answer to that. - ---- - -## 2. Iterating on a Large Design, Pass After Pass - -The real test of an AI-native delivery system is not a 10-Spec toy example. It is our first adopter, [`libar-platform`](https://github.com/libar-dev/libar-platform), which models a Convex transactional domain platform across **243 Specs in six capability Packs**, most of it design that will stay ahead of its code for months ([PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); the adopter's side is [libar-platform#13](https://github.com/libar-dev/libar-platform/pull/13)). - -Design at that scale happens in **design passes**: one round in which an agent or a person reads a Pack (or a list of Specs), finds the gaps, and proposes changes, and the next round starts from what that one changed. A pass reads three things beside the Specs themselves. A member's **footing** is every Spec it rests on through its typed dependencies, with the rung each one states and the floor each one reaches (recipe 21). A **pinned declaration** is a keyed Design entry whose value opens with a code span, stating one signature, type, validator, or table that a builder and a test author both hold to (recipe 24). And an **entry address**, such as `spec:#design.`, names one keyed entry so a pass can cite it exactly. - -Four problems hit immediately at that scale, and the iteration loop answers each on the one graph: - -### 1. Fitting a Pack into a Model's Context Budget (recipe 36) - -Hand-assembling a pass's reading material (the Pack design, member sections, open questions, footing, pinned declarations, and examples) overflows a model's context. **The context bundle** ([`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md), recipe 36) composes it on demand within a hard budget, `100,000` by default. - -The budget is counted in **UTF-16 code units**: the `.length` a JavaScript body reads, measured on the compact JSON of the answer. That count is deterministic on every machine and binds the Protocol to no model's tokenizer; the bundle reports a `tokenEstimate` beside it, labeled as an estimate (code units divided by four). - -A bundle carries each question and each pinned declaration in one place only. If the scope still exceeds the budget, it 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 narratives, example step text, non-blocking question text, and section values other than pinned declarations. Only when the caller passes `declarations: "addresses"` does a seventh cut replace pinned declarations with their entry addresses, so the agent looks up only the signatures it needs. Member IDs, stated rungs, floors, blocking questions, and entry addresses are never cut, and the bundle never returns a partial member list: when even the protected content does not fit, the answer is `budgetExceeded: true` with the minimum length it needs. - -On `libar-platform`, `pack:history-view` first needed at least 299,106 code units. PR #33 redesigned the bundle's shape (each member's file named once, cut entries reduced to an address or a line, a new footing cut) and added the opt-in declaration cut; with `declarations: "addresses"` the same Pack now fits in **93,806 of 100,000** units. Not every Pack gets there: as [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) records, the platform's application and foundation Packs stay over budget after every cut, and a pass reads them through a perspective (recipe 37) instead. - -```sh -npm run --silent sdp:q -- "$(cat dist/recipes/36-the-context-bundle.js)" \ - --params '{"pack":"pack:design-iteration-v1","declarations":"addresses"}' --json -``` - -On the Protocol's own corpus, that eleven-member Pack fits in 87,185 units with nothing left out. Asked for `pack:self-hosting-v1`, all 276 of the Protocol's Specs, with declarations kept whole, the recipe answers `budgetExceeded: true` with a `protectedLength` of 409,404 rather than a bundle that silently drops members. - -### 2. Comparing Design Passes at Entry Grain (recipe 35) - -`git diff` shows changed lines; it does not tell you which Design keys changed, which open questions closed, or which readiness rungs moved since the last pass. **The design diff** ([`spec:consumers.design-diff`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/design-diff.sdp.md), recipe 35) takes the graph `sdp build` wrote at the base commit via `--params base=@path` and diffs the curated graph by Spec, section, and entry key, keeping code-binding changes separate from intent changes. - -### 3. Handing Changes Back Safely (recipe 38) - -Views never patch source files directly ([`spec:consumers.edit-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/edit-model.sdp.md)). Instead, a design pass returns a structured **change proposal** ([`spec:consumers.intent-composition.proposal`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/intent-composition.proposal.sdp.md)): a JSON document on the shipped schema [`schemas/sdp-proposal-1.schema.json`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/schemas/sdp-proposal-1.schema.json), where each change targets a Spec or an entry address (`#design.`, `#ui.`, `#question.`, `#rule.`). - -**The proposal check** (recipe 38) takes the proposal and the graph written at the proposal's base commit, and reports, change by change, whether each target scope has moved since that base. It only reports: it applies nothing and writes no carrier. The caller that applies the proposal sends any change whose scope moved to a person, applies the others as ordinary source edits, and lands them only from a worktree on which `sdp validate` reports no error ([`spec:consumers.intent-composition#rule.changedScope`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/intent-composition.sdp.md)). - -### 4. Running Project-Specific Architecture and Policy Rules (`sdp validate --rules`) - -In PR #33, `libar-platform` retired the graph-side half of its custom Python checker, `check.py`, into `sdp validate --rules` ([`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md)). A rules file is a `sdp q` body that returns `{ findings }`, where each finding is well-formed: a `validatorId` (the rule's own kebab-case ID), a `subjectId`, a `severity` of `error` or `warning`, and a `message`, with an optional `file` and `line`. A broken rule, or a finding missing one of those fields, fails the run rather than passing as clean. - -So a shipped recipe whose answer carries findings is already a rules file. **The architecture walk** ([`spec:validation.architecture-constraints`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/architecture-constraints.sdp.md), recipe 42) checks forbidden layer directions and isolated contexts against anchored `uses` edges. Here it forbids the `edge` layer from using the `domain` layer, at warning severity: - -```sh -npm run --silent sdp -- validate . \ - --exclude explorations --exclude examples --exclude test/fixtures/import/parity \ - --rules dist/recipes/42-the-architecture-walk.js \ - --params '{"forbidden":[{"from":"edge","to":"domain"}],"severity":"warning"}' -``` - -Standard output ends with the Protocol's summary line, untouched, and a line of its own that tallies the corpus's rules: - -```text -validate: 0 errors · 6 warnings (conformance + honesty over the one graph) -corpus rules: 0 errors · 7 warnings · 0 of 1 files failed (the corpus's own rules) -``` - -On standard error, the Protocol's six findings come first (the corpus's five intentional prose-mention warnings and one pending verifier gap, both pinned by its own test oracle). Then each rules file prints a header and its findings: - -```text -corpus rules dist/recipes/42-the-architecture-walk.js -[warning] forbidden-layer — component:protocol.cli uses component:protocol.graph: layer edge uses layer domain (component:protocol.cli to component:protocol.graph), a forbidden pair -[warning] forbidden-layer — component:protocol.cli uses component:protocol.model: layer edge uses layer domain (component:protocol.cli to component:protocol.model), a forbidden pair -…five more for the CLI and projections components -``` - -Drop `"severity":"warning"` and the same seven findings are errors: the corpus line reads `7 errors` and `sdp validate` exits `1`. The Protocol's own architecture does not forbid that direction, so this is a demonstration, not a defect; forbid a direction no edge takes, such as `domain` to `infrastructure`, and the corpus line reads `0 errors · 0 warnings`. - ---- - -## 3. Spec Studio: The Static HTML Workbench for Human Reviewers - -Agents are well served by `sdp q` and JSON. Humans reviewing a 20-member capability Pack do not want to read JSON, and the frozen Markdown Design Review's Pack member table already runs 13 columns wide. - -In PR #33, we shipped phase 0 of the **Spec Studio** ([`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md); [the Studio renders derived HTML (MD-41)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-html-surface.sdp.md); [the Studio is built from custom elements (MD-42)](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/studio-web-components.sdp.md)). - -Run one command from a checkout: - -```sh -npm run --silent sdp -- studio . --exclude explorations --exclude examples --exclude test/fixtures/import/parity -``` - -`sdp studio` publishes a deterministic static HTML page set under `generated/spec-studio/`. It is never committed, makes no network request when opened from `file://`, and also works from any static preview server: - -- **No second graph in the browser.** Every join, claim, delivery fact, derived readiness, floor failure, and finding on a page was computed once by the reader during `sdp studio`. Six framework-free custom elements add filtering, open-all and close-all, copy buttons, search, and a build stamp without recomputing graph logic. -- **Readable Pack pages ([`spec:consumers.spec-studio.pack-page`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.pack-page.sdp.md)).** Each Pack page leads with a plain-language summary of where the design stands, then one collapsible card per member in authored reading order (its stated rung beside the floor it reaches, and its stated next rung with either `nothing blocks` or how many floor clauses are still unmet), then the Pack's four-part boundary: `Rests on`, `Rested on by`, `Verified by`, `Superseded by`. - -![The libar-platform history-view Pack page in Spec Studio: a plain-language summary, the Pack framing, and collapsible member cards](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-pack.png) - -*The `libar-platform` Pack page for `pack:history-view`: the summary the graph computes (19 members, 283 design entries and 169 pinned declarations, 53 open questions and none blocking), the framing, then the member cards.* - -- **Addressable entries and open questions ([`spec:consumers.spec-studio.spec-page.core`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.spec-page.core.sdp.md)).** Every keyed Design entry (`#design.`), UI entry (`#ui.`), rule (`#rule.`), and open question (`#question.`) has a deep link and a one-click copy action for its full entry address. Each open question also shows the four optional fields only open questions carry: its `decider`, its `links`, its `provisional` answer, and its `trigger`. - -![Open questions on a Spec Studio Spec page, each showing its blocking flag, its key with a copy button, its recorded line, and its text, followed by the Spec's keyed workflow rules with their lines](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-question.png) - -*The open questions on a `libar-platform` Spec page, opened at its entry address `#question.verifyPassUnderPause`. These questions carry no optional fields; a question that does shows each one on its own labelled line.* - -- **Shared concept search ([`spec:consumers.spec-studio.search`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.search.sdp.md)).** The search page bundles the same matcher that powers `g.findByConcept` in `sdp q` (shipped for browsers as the package's `./concept-match` subpath), and adds the keys authors coin, such as `verifyPassUnderPause`, and the file paths the graph records: - -![Spec Studio search for OD-157: one matching Spec, with the fields searched listed and the matching open question shown under it with its section and line](https://raw.githubusercontent.com/libar-dev/software-delivery-protocol/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/platform-search.png) - -*Searching the `libar-platform` page set for `OD-157`, an id from the adopter's own decision register that the adopter had written into the text of an open question: one Spec matches, in its Intent section, and the question `verifyPassUnderPause` is listed beneath with its line.* - -All three screenshots were captured from `libar-platform`'s branch during PR #33, as [the screenshot notes](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33/README.md) record; publish the page set yourself to see the current pages. - ---- - -## 4. Three Skills: How Agents Learn the Workflow - -A powerful graph surface only helps if coding agents actually use it instead of falling back to `grep` and file scanning. - -The Protocol ships three agent skills under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills). In the Protocol's own repository they are mandatory: [`AGENTS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/AGENTS.md) requires an agent to load the matching skill before any corpus question, Spec authoring, or delivery-session routing. They also ship in the npm package, at `node_modules/@libar-dev/software-delivery-protocol/.agents/skills/`, for an adopter to point its agents at: - -1. **[`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md)** — teaches the graph's shape, the task-to-recipe sheet, the wrap-and-filter idiom (run a shipped recipe inside your own body and filter its answer, rather than editing the recipe), and the **anti-anecdote rule**: *"The derived graph outranks this skill. It also outranks any summary you cached earlier in a session and any paraphrase in any document."* -2. **[`sdp-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md)** — teaches the closed `.sdp.md` and `.sdp.gherkin` grammars, where unsettled truths live (blocking questions, `trigger:` deferrals, `provisional:` leans), how to choose honestly between `satisfies` and `references` when binding code, and how much to write. -3. **[`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md)** — routes delivery sessions across five advisory work shapes (*Capture / refine*, *Design*, *Implement*, *Review*, *Close / slim*), with a re-measurable handoff template and **tripwires**: signs that work has slipped out of its shape, such as writing code during a design session or stating a rung for the author during a review. Its *Close / slim* shape names the **distillation boundary** ([`spec:model.enrichment-lifecycle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/enrichment-lifecycle.sdp.md)): a Design entry may leave its Spec only once the Spec is implemented with a verifier and a bound artifact carries the entry. None of the shapes is a workflow state machine; every preflight is advisory. - ---- - -## 5. Try the Agent Surface and Spec Studio - -From a checkout of [`libar-dev/software-delivery-protocol`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6): - -```sh -git clone https://github.com/libar-dev/software-delivery-protocol.git -cd software-delivery-protocol -git checkout 5a0b22c2d9ead98c900b60ed6209342fce2cbef6 -npm ci # also builds dist/ and dist/recipes/ - -# 1. Inspect the Pack design summary for pack:spec-studio-v1 (Recipe 29) -npm run --silent sdp:q -- "$(cat dist/recipes/29-pack-design.js)" \ - --params '{"pack":"pack:spec-studio-v1"}' --json - -# 2. List the anchored uses edges that cross a layer or context, tallied by direction (Recipe 32) -npm run --silent sdp:q -- "$(cat dist/recipes/32-architecture-crossings.js)" --json - -# 3. List every Design and UI entry whose key starts with a prefix (Recipe 41) -npm run --silent sdp:q -- "$(cat dist/recipes/41-the-key-family-register.js)" \ - --params '{"prefixes":["bundle","rule"]}' --json - -# 4. Compare planned component designs against realized component: anchors (Recipe 43). -# On the Protocol's own corpus every total is 0: no Spec here states a component design -# (a Design `id` naming a component:). It serves adopters planning components ahead of code. -npm run --silent sdp:q -- "$(cat dist/recipes/43-planned-against-realized-components.js)" --json - -# 5. Publish the Spec Studio, then open generated/spec-studio/index.html -npm run --silent sdp -- studio . --exclude explorations --exclude examples --exclude test/fixtures/import/parity -``` - -Never invoke a bare `sdp` inside this checkout: `npm run --silent sdp -- …` runs the package's own build, and `npm run --silent sdp:q -- …` adds the three exclusions that keep exploration exhibits, the separate worked example, and carrier-parity fixtures out of the self-hosting root. An adopter that installed the package runs `npx sdp …` from its own root instead. - ---- - -## Where the Series Goes Next - -This post closes the opening trio. **Part 1**, *The Spec Is the Prompt Is the Design* ([read it here](./01-the-spec-is-the-prompt-is-the-design.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/48d8f495959c2a8adc6ffab4069d4ab60afbf716/docs/articles/01-the-spec-is-the-prompt-is-the-design.md)), made the case for one graph. **Part 2**, *From Rough Idea to Verified Code* ([read it here](./02-walkthrough-intent-to-verified-code.md), or [on GitHub](https://github.com/libar-dev/software-delivery-protocol/blob/48d8f495959c2a8adc6ffab4069d4ab60afbf716/docs/articles/02-walkthrough-intent-to-verified-code.md)), walked the loop from a Spec to a verified test. The next five installments tell specific stories from the Protocol's construction: - -- **Part 4:** *Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition* -- **Part 5:** *What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Comment-Form Reversal, and Honest Code Bindings* -- **Part 6:** *Executable Specifications Without Cucumber Glue: Typed Example Spaces, Generated Contracts, and Expected-Outcome Oracles* -- **Part 7:** *Designing a 243-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale* -- **Part 8:** *Auditing 258 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh* - ---- - -*About this article: Part 3 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6). Repository links and every command output are pinned to commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33); re-run the commands rather than quoting the outputs at a later commit.* diff --git a/docs/articles/03-walkthrough-intent-to-verified-code.md b/docs/articles/03-walkthrough-intent-to-verified-code.md new file mode 100644 index 000000000..3b64a8e93 --- /dev/null +++ b/docs/articles/03-walkthrough-intent-to-verified-code.md @@ -0,0 +1,289 @@ +# From rough idea to verified code: a hands-on tour of the Protocol + +*Part 3 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* + +[Part 1](./01-the-spec-is-the-prompt-is-the-design.md) introduced one enrichable `Spec`, source anchors and the derived graph. [Part 2](./02-one-graph-many-views.md) showed how people read that graph through replaceable projections. + +This article follows the engineering loop in a text editor and a terminal. We will use Order Management in [`examples/checkout-v1`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/examples/checkout-v1), then inspect a comment-form binding in the Protocol's own engine. + +## Setup + +Every command runs from a checkout at the pinned commit: + +```sh +git clone https://github.com/libar-dev/software-delivery-protocol.git +cd software-delivery-protocol +git checkout b5e0a8bea1e3bedd48c616f96c9928e955792753 +npm ci +``` + +Inside the Protocol's own checkout, run the CLI as `npm run --silent sdp -- `. Do not invoke a bare `sdp`, which can select macOS's unrelated binary. An adopter that installed the package uses `npx sdp` or `pnpm exec sdp` instead. + +`examples/checkout-v1` is its own extraction root, so these commands need none of the exclusions used for the Protocol's self-hosting corpus. + +## The worked example + +The example models Order Management with all eight Spec kinds and all four readiness rungs. At the pinned commit it contains 11 Specs, two Packs and five anchors: + +```text +examples/checkout-v1/ + specs/ + checkout.pack.sdp.md # pack:checkout-v1 + roadmap.pack.sdp.md # pack:checkout-roadmap + orders/order-management.sdp.md # behavior · epic · defined + orders/order-placement-flow.sdp.md # workflow · feature · scoped + orders/create-order.sdp.md # behavior · feature · defined + orders/create-order-valid-cart.sdp.md # example · story · ready + orders/create-order-invalid-cart.sdp.md # example · story · defined + orders/create-order-api-contract.sdp.md # contract · story · idea + orders/order-total-rule.sdp.md # rule · story · defined + orders/order-inventory-rule.sdp.md # rule · story · defined + orders/order-latency-constraint.sdp.md # constraint · story · defined + orders/order-model.sdp.md # model · story · defined + decisions/order-lifecycle.sdp.md # decision · feature · defined + src/orders/ + create-order.use-case.ts # two implementation anchors + create-order.route.ts # one API anchor + test/orders/ + create-order.valid-cart.test.ts # one test anchor + create-order.oracle.ts # one oracle anchor + orders.create-order.valid-cart.test.generated.ts +``` + +Build its graph: + +```sh +npm run --silent sdp -- build examples/checkout-v1 +``` + + +```text +11 specs · 2 packs · 5 anchors → 18 nodes · 34 edges (0 errors, 0 warnings) +Wrote /examples/checkout-v1/generated/graph.json +Wrote /examples/checkout-v1/generated/contracts (3 modules) +``` + + +Before opening a file, ask which Specs have code ahead of their stated design maturity. Recipe 2 answers the drift alarm, `implemented ∧ ¬ready`: + +```sh +npm run --silent sdp -- q "$(cat dist/recipes/02-the-drift-alarm.js)" \ + --root examples/checkout-v1 --json +``` + + +```json +{ + "total": 2, + "alarms": [ + { + "id": "spec:orders.create-order", + "statedReadiness": "defined", + "floorReached": "ready", + "implementationBindings": 2 + }, + { + "id": "spec:orders.order-total-rule", + "statedReadiness": "defined", + "floorReached": "ready", + "implementationBindings": 1 + } + ] +} +``` + + +Both Specs have implementation bindings and state only `defined`. Their structural floors reach `ready`, so they wait only for an author's readiness statement. Validation still reports 0 errors and 0 warnings. The drift alarm is a query, not a workflow gate. + +## Author the parent and its example space + +Open [`create-order.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/examples/checkout-v1/specs/orders/create-order.sdp.md): + +````markdown +--- +id: spec:orders.create-order +kind: behavior +altitude: feature +readiness: defined +relations: + refines: spec:orders.order-management + constrainedBy: spec:orders.order-latency-constraint + decidedBy: spec:decisions.order-lifecycle +--- +# Customer creates an order + +## Intent + +- actor: customer +- outcome: Turn a valid cart into an order. +- value: Customers can complete purchases without the example modeling the rest of checkout. + +## Example space + +```gwt-vocabulary +Given a customer has a cart with {n:number} line items +And every line item has quantity {q:number} and unit price {price:number} +And every cart item is {availability:"in stock"|"out of stock"} +When the customer submits the cart for order creation +Then an order is created with total {total:number} +And the order contains the original cart lines +And order creation is rejected because {reason:"empty cart"|"out of stock"} +``` +```` + +The YAML frontmatter is the closed envelope. It carries identity, the three descriptors and relations. A hand-authored `implemented` property or a derived `satisfies` relation is refused. + +Sections carry content and relations carry linkage. The parent names its own refinement, constraint and shaping decision in the envelope rather than hiding those relations in prose. + +The `gwt-vocabulary` fence declares the typed example space its child examples share. Each `{slot:type}` placeholder becomes part of the generated contract. + +### Bind one point + +The child example [`create-order-valid-cart.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md) refines and verifies its parent: + +````markdown +--- +id: spec:orders.create-order.valid-cart +kind: example +altitude: story +readiness: ready +relations: + refines: spec:orders.create-order + verifies: spec:orders.create-order +--- +# Valid cart creates an order + +## Intent + +- outcome: Show that a valid cart can become an order. + +```gwt +Given a customer has a cart with {n: 2} line items +And every line item has quantity {q: 1} and unit price {price: 50} +And every cart item is {availability: "in stock"} +When the customer submits the cart for order creation +Then an order is created with total {total: 100} +And the order contains the original cart lines +``` + +## Verification — executable + +- The order result contains a stable id. +- The returned total matches the authored total. +```` + +Under the point-per-example law, one example binds one concrete point. Under the concreteness law, leaving a used slot unbound, such as `{n}` instead of `{n: 2}`, caps the example below `defined` and withholds its step contract and registrar. + +A behavior or example Spec may use `.sdp.gherkin` instead of Markdown as its one canonical carrier. Markdown remains the default and supports authoring features the Gherkin carrier does not, including open questions and relation reasons. [Part 8's outline](./README.md#article-08-executable-specifications-without-cucumber-glue) takes the carrier and executable-contract design further. + +## Bind source code + +An anchor is a human-authored binding in a `.ts` or `.tsx` file. It carries no Spec prose, readiness or delivery status. Two representations feed the same closed envelope. + +The constant form works when source may import the package or its zero-dependency `/anchors` subpath: + + +```ts +import { codeAnchor, codeAnchorId, ref } from "@libar-dev/software-delivery-protocol"; + +// … + +export const createOrderUseCaseAnchor = codeAnchor({ + id: codeAnchorId("impl:orders.create-order-use-case"), + label: "createOrderFromCart", + satisfies: ref("spec:orders.create-order"), +}); +``` + + +The comment form needs no runtime import. This anchor comes from the Protocol's own [`src/projections/census.ts`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/src/projections/census.ts): + + +```ts +/** + * @sdpAnchor impl:protocol.census-page + * @sdpLabel renders the derived census and runtime taxonomy projection + * @sdpSatisfies spec:consumers.census-page, spec:model.structural-patterns + * @sdpComponent component:protocol.projections + * @sdpReferences spec:decisions.architectural-annotation + * @sdpRole projection + */ +``` + + +`satisfies` and `references` have different meanings: + +- `satisfies` says this unit realizes the Spec's promises together with any other satisfying units. A resolving edge confers `implemented`. +- `references` says this unit answers to a design without claiming to realize it. It supports traceability and impact queries, confers no delivery fact and does not trip the drift alarm. + +A stub or a unit following a decision can therefore name that design without pretending to implement it. + +## Generate the executable half + +`sdp build` emits a step contract for each example and a space contract for the parent. It also emits a registrar beside the test. The authored test imports and activates that registrar, so the adopted registrar is committed and byte-checked. Generated code never imports authored semantics. + +Three pieces meet at test time. + +**The space contract** types the example space. For create-order it includes `{ n: number; q: number; price: number; availability: "in stock" | "out of stock" }`, every child's bound point and the outcome union derived from the parent's Then vocabulary. + +**The expected-outcome oracle** is authored beside the tests and bound with `specOracle`. It returns `unspecified` where the Spec states no outcome: + + +```ts +export function expected(conditions: Partial): CreateOrderOutcome { + if ( + conditions.n === undefined || + conditions.q === undefined || + conditions.price === undefined || + conditions.availability === undefined + ) { + return unspecified; + } + + if (conditions.n === 0) { + return { kind: "order creation is rejected because {reason}", reason: "empty cart" }; + } + + if (conditions.availability === "out of stock") { + return { kind: "order creation is rejected because {reason}", reason: "out of stock" }; + } + + if (conditions.q <= 0 || conditions.price <= 0) { + return unspecified; + } + + return { + kind: "an order is created with total {total}", + total: conditions.n * conditions.q * conditions.price, + }; +} +``` + + +**The bound test** anchors `test:orders.create-order.valid-cart` and activates the registrar with `createWorld`, `invoke`, `observe`, `expected` and optional `assertions` adapters. + +At the matching Then step, the registrar checks both the Spec's authored values and the observed outcome against the oracle. If the Markdown changes `{total: 100}` to `{total: 120}` while the oracle and code still say 100, the test fails on the changed Spec value. Agreement between code and oracle alone is not enough. + +The second example, `invalid-cart`, declares `verifies` but has no test anchor. Recipe 10 shows it as declared but not enabled. Its `defined` rung makes that an honest incomplete trace, not a warning. An unbound example stating `ready` would draw a verifies-linkage warning. + +## Break it on purpose + +Run each experiment, then restore the example with `git checkout -- examples/checkout-v1` and rerun `npm run generate:example`. + +| Experiment | Edit | Run | Expected result | +|---|---|---|---| +| Misspell a section heading | Change `## Rule` to `## Rul` in `order-total-rule.sdp.md` | `npm run --silent sdp -- validate examples/checkout-v1` | Exits 1 with `extract/unrecognized-heading` and follow-on structure errors. The Spec drops out of the graph and `graph.json` is not written. | +| Dangle a relation | Misspell `spec:orders.order-management` in `create-order.sdp.md` | The same validate command | Exits 1 with `conformance/referential-integrity` and a nearest-ID suggestion. | +| Keep an unearned rung | Add a blocking open question to the stated-`defined` create-order Spec | The same validate command | Exits 1 with `honesty/readiness-floor`, naming `no-blocking-open-questions`. | +| Leave a slot unbound | Change `{n: 2}` to `{n}` in the valid-cart example | The same validate command | Exits 1 with `honesty/readiness-floor`; the example's contract and registrar are withheld. | +| Drift a Spec value | Change `{total: 100}` to `{total: 120}` | `npm run generate:example && npx vitest --run examples/checkout-v1/test` | The bound test fails because the Spec's value no longer matches the oracle. | + +These experiments show the boundary between extraction, conformance, honesty and executable checks. They also show why none of them needs to judge sprint state or design taste. + +## Up next + +[Part 4: Stop building 30-verb CLI walls for coding agents](./04-stop-building-verb-walls-for-coding-agents.md) follows the graph downstream: the demand map, `sdp q`, recipes and the evidence from agents using that surface. + +--- + +*About this article: Part 3 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753). Repository links and command outputs are pinned to commit `b5e0a8be`; re-run the commands rather than quoting the outputs at a later commit.* diff --git a/docs/articles/04-stop-building-verb-walls-for-coding-agents.md b/docs/articles/04-stop-building-verb-walls-for-coding-agents.md new file mode 100644 index 000000000..e7fb0e00a --- /dev/null +++ b/docs/articles/04-stop-building-verb-walls-for-coding-agents.md @@ -0,0 +1,148 @@ +# Stop building 30-verb CLI walls for coding agents + +*Part 4 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* + +A CLI gains `list-specs`. Then it gains a readiness filter, a command for verifiers, another for relations and flags for every join. The agent still receives a question no command answers: "show every `defined` Spec in this Pack with no blocking questions and the files that reference it." + +The agent makes a sequence of calls, receives more output than its context can hold and joins the results in scratch space. The tool has exposed many operations and little of the underlying model. + +The Libar Software Delivery Protocol takes another path ([the agent front door, MD-22](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/decisions/agent-front-door.sdp.md)): + +> Give coding agents a visible, typed graph and one evaluation sink they can script. + +## Start with what the agent holds + +Before designing [`spec:consumers.agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/agent-surface.sdp.md), we asked what an agent has when a session starts. Usually it does not have the Spec ID it needs. It has one of three things, the demand map: + +1. A concept string from a prompt, such as "where do we enforce the readiness floor?" +2. A file path it is about to read or edit. +3. A changeset from `git diff`. + +The typed reader freezes a small set of entry adapters for those arrivals: + +- `g.findByConcept(term)` searches IDs, titles, binding labels, Pack framing, narratives and section values, and names the matching field. +- `g.byFile(path)` returns every node recorded at a path and the Specs reachable from them. +- `g.blastRadius(files)` reports directly impacted Specs and Packs, the explicit one-hop neighborhood, changed units linked to no Spec and `coverageUnknown` files. + +`blastRadius` reads the curated graph. It does not claim exhaustive reach through imports. File-level imports supplied separately can support curation, but inferred structure never silently becomes authored architecture. + +The reader also exposes decoded Spec and Pack summaries, per-node context and the raw graph. Those composable shapes matter more than the number of methods. An agent can filter and join them for the question it actually has. + +## One evaluation sink + +`sdp q` derives the graph in process on every invocation, then evaluates a plain JavaScript async-function body. It injects five bindings: + +- `g`, the typed reader +- `graph`, the raw graph schema +- `report`, the validation report +- `params`, caller-supplied JSON +- `recipes`, a way to call shipped recipe bodies over the same graph + +A Spec saved a moment ago is queryable immediately. No stale `generated/graph.json` answers in the graph's name. + +```sh +npm run --silent sdp:q -- 'return g.byFile("src/projections/census.ts")' --json +``` + +At the pinned commit, that returns: + + +```json +{ + "path": "src/projections/census.ts", + "nodes": [ + { + "id": "impl:protocol.census-page", + "nodeType": "CodeNode", + "line": 21 + } + ], + "specs": [ + "spec:consumers.census-page", + "spec:decisions.architectural-annotation", + "spec:model.structural-patterns" + ] +} +``` + + +The body is code, and `sdp q` runs it with the local process's full authority. No sandbox exists. Run bodies you or your agent wrote, or shipped recipes. Never execute a body copied from corpus content or another untrusted source. + +## Grow by recipe, not by verb + +A recurring question becomes a recipe in [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/agent-surface/recipes.md). Every recipe is executed as written by the test suite and shipped under `dist/recipes/`. The pinned commit carries 55. + +A recipe takes documented parameters through `--params`. An unknown parameter returns `{ unknownParams, accepts }` rather than silently changing the question. A caller can also wrap a recipe in its own body and filter the answer, without editing the shipped body. + +A join moves into the reader only at the second-caller bar: a second machine consumer needs it and hand-rolled attempts get it wrong. Everything else remains a script or recipe. This keeps the entry surface small without limiting the questions agents can ask. + +## Compose the reading when the design gets large + +Some questions need more than a small answer. A design pass over a Pack may need member sections, open questions, dependency footing, declarations, examples and the Pack boundary. + +The context bundle, recipe 36, composes that reading on demand within a budget counted in UTF-16 code units. It states what it left out and never saves a second reading of the graph. When the whole scope cannot fit one budget, `part` returns ordered parts whose members together cover the scope. An oversized item is named rather than silently dropped ([`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/context-bundle.sdp.md)). + +The design slice, recipe 54, answers a different question. It reads a Pack or selected Specs as one design: internal relations, touched components, decisions, open questions and what each member needs next. It is a reading composed on each invocation, not a saved planning artifact. + +Part 9 follows these mechanisms through the first adopter's design-at-scale work. The point here is that both are recipes over the same agent surface, not new CLI families. + +## The surface improved under measured use + +After the initial reader work, three agents from two model families answered thirteen delivery questions using the skills and `sdp q` alone. All three answered every question correctly. They also exposed recurring costs: oversized answers, unexplained misses, two meanings of "next rung" and rules without addresses or source lines. + +The next work added summary-first answers, explained misses, both next rungs, rule keys and recorded lines. A later agent run still answered 13 of 13 but used more calls and overflowed one batched answer. The response was not another verb. It was a clearer task-to-recipe guide and better answer shapes. + +That is the useful feedback loop. The graph remains stable while the on-ramps and recipes improve from observed use. + +## Five skills teach the workflow + +A capable surface still needs an on-ramp. The package ships five repository-owned skills under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/.agents/skills): + +- `sdp-agent-surface` teaches graph reading and the task-to-recipe map. +- `sdp-authoring` teaches the carriers, readiness floors, relations and bindings. +- `sdp-sessions` routes advisory delivery work shapes without making them workflow gates. +- `sdp-studio` covers publishing, reading and sharing the Studio and receiving composed intent. +- `sdp-vault` covers the generated Obsidian notes and the hand-off back to source. + +Their standing rule is the anti-anecdote rule: the derived graph outranks a skill, a cached summary or a paraphrase. The skill teaches how to ask. The graph answers. + +## People read the same graph differently + +This article is about the agent surface, but the Protocol does not require people to consume JSON. The Studio, Design Review, census, Mermaid and Gherkin projections read the same graph for human tasks. + +The Studio's browser code does not build a second graph. Its joins and derived values come from the reader during publication. Its composing panel hands wanted changes back as scoped intent rather than mutating source. + +[Part 2](./02-one-graph-many-views.md) gives that side of the system its own article. The two surfaces matter together: an agent can inspect the same Spec and bindings a reviewer just saw, without translating between two delivery databases. + +## Try the agent surface + +From a checkout at the pinned commit: + +```sh +git clone https://github.com/libar-dev/software-delivery-protocol.git +cd software-delivery-protocol +git checkout b5e0a8bea1e3bedd48c616f96c9928e955792753 +npm ci + +# Find a concept. +npm run --silent sdp:q -- "$(cat dist/recipes/06-where-is-this-concept.js)" \ + --params '{"term":"readiness floor"}' --json + +# Read the owner's advisory roadmap. +npm run --silent sdp:q -- "$(cat dist/recipes/53-the-roadmap.js)" \ + --params '{"pack":"pack:roadmap"}' --json + +# Read one Pack as a design, with summary rows. +npm run --silent sdp:q -- "$(cat dist/recipes/54-the-design-slice.js)" \ + --params '{"pack":"pack:spec-studio-v1"}' --json +``` + +In this source checkout, `npm run --silent sdp:q --` supplies the corpus's fixture exclusions. An adopter invokes the installed `sdp q` with its own root and exclusions. + +## Up next + +[Part 5: AI can shape the reading, but it cannot rewrite the record](./05-ai-can-shape-the-reading.md) separates what agents already do from a future AI presentation layer, including the reproducibility question that layer must answer. + +--- + +*About this article: Part 4 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753). Repository links and command outputs are pinned to commit `b5e0a8be`; re-run the commands rather than quoting them at a later commit.* diff --git a/docs/articles/05-ai-can-shape-the-reading.md b/docs/articles/05-ai-can-shape-the-reading.md new file mode 100644 index 000000000..c942d5f0d --- /dev/null +++ b/docs/articles/05-ai-can-shape-the-reading.md @@ -0,0 +1,82 @@ +# AI can shape the reading, but it cannot rewrite the record + +*Part 5 in a series introducing the Libar Software Delivery Protocol. This is a direction article, not a description of shipped AI presentation features. Links are pinned to one commit; see the footer.* + +The Protocol already uses AI agents in two distinct roles. An agent can read the delivery graph and answer questions. It can also receive scoped intent and edit source. A third role belongs to the product's longer-term direction: AI can help decide how a person reads the graph. + +That might mean selecting a useful projection, narrowing a Pack to one concern, explaining a difficult relation, or adapting a review to the reader's context. The appeal is not a universal interface generated on every visit. It is presentation that fits the question while the underlying record stays unchanged. + +This role needs the same honesty discipline as the other two. It also introduces a real design tension: today's projections are deterministic, while language-model output can vary. + +The series should discuss that tension directly. + +## Three roles, three different trust boundaries + +**AI as a reader is built.** Through `sdp q`, an agent scripts the typed graph. Recipes answer recurring questions. A context bundle composes the reading for a design pass within a stated budget, in parts when the scope needs them. Nothing in this path asks the model to invent the model. + +**AI as an editor is built as a hand-off.** A person or agent composes scoped intent. The Studio's composing panel can produce a change proposal and memo. An agent checks the proposal against its base and applies accepted changes as ordinary source edits. Git and the conformance and honesty checks see the same edit they would see from a person. + +**AI as a presentation assistant is a future direction.** The current package does not use an LLM to generate Studio pages, select a lens for a reader, summarize a Pack or rearrange a review. The MCP surface for user-facing apps remains deferred. Runtime observation also remains deferred, and supplied run evidence is not runtime observation. + +Keeping the roles separate prevents a useful reading aid from acquiring authority it never earned. + +## What AI may shape + +A presentation assistant could work at several levels. + +It could choose among deterministic artifacts. Given a reviewer's question, it might open a design slice, a capability map, a Mermaid diagram or a Spec's source context. The assistant chooses the reading; the existing projection supplies the content. + +It could compose a new deterministic projection through code. An agent proposes a projection and its checks. Once reviewed and built, that projection reads the graph like any other. AI helped write the generator, not the answer on each render. + +It could produce explanatory prose or a bespoke arrangement beside a deterministic projection. A summary might explain why two Specs conflict or translate a readiness floor failure into plainer language. This is where the fine-tuning and presentation finesse become valuable, and where the current guarantees need careful extension. + +All three patterns are options to explore. None is a ratified product design today. + +## The determinism question cannot be waved away + +Today, `graph = f(repo)` and a shipped projection is a pure, disposable, regenerable function of the graph. The same graph and named inputs produce the same bytes. That property makes staleness falsifiable and lets the repository's checks compare regenerated output. + +An LLM-written summary does not automatically inherit that property. Model versions, decoding choices and prompt context can change the words even when the graph has not changed. + +A mature design must decide where that variation belongs. It could cache generated prose as a named artifact, record its model and source commit, and present it beside the deterministic view. It could restrict AI to selecting deterministic content. It could define a separate explanatory projection with a different reproducibility contract. These are design questions, not implementation details to hide. + +The wrong answer is to call every generated sentence a projection and assume the existing guarantee still applies. + +The same caution applies to layout. An adaptive interface may reorder information, but it must not drop a blocking question because the summary seemed complete, turn an inferred edge into an authoritative one, or hide supplied evidence whose commit does not match the page's stamp. + +## The record keeps its boundaries + +Whatever presentation mechanism emerges, several boundaries remain useful tests for it. + +- A model may explain a `declared` relation, but it may not present its own inference as `declared` intent. +- A model may summarize a binding, but `implemented` still means a resolving `satisfies` edge and `has-verifier` still means a resolving verifier exists. +- A model may discuss supplied run results, but it may not turn them into `observed` or imply that a verifier passed when no run record says so. +- A model may recommend a readiness statement. Only an author states readiness, and the structural floor still checks that statement. +- A model may propose a source edit. A view does not become canonical by displaying that proposal. +- A model may omit detail for readability. It must preserve the scope and what was left out, as a context bundle does. + +These rules do not guarantee that a summary is good. They make its claims inspectable. + +## A better goal than "AI-generated UI" + +The seed design treated AI context as a first-class projection and the Studio as a place to explore and refine a living model ([the seed projection design](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/v0-design/07-spec-studio-and-projections.md)). The current system has made the graph, agent surface, Studio and intent hand-off real. + +The next useful step is not a button that generates an entire dashboard. It is a set of measured experiments: + +1. Can an assistant select the right existing reading for a review task more reliably than a reader navigating alone? +2. Does a generated explanation help a reviewer understand a Pack without making them less likely to inspect its Specs? +3. Can a compact, reader-specific projection preserve every claim and omission needed for an honest decision? +4. Which generated artifacts need a model, prompt and source-commit record to remain reviewable? +5. When does adaptive presentation save effort, and when does a stable page structure make review easier? + +The first UX/UI design pass can explore these questions without committing the product to AI-rendered pages. The deterministic Studio remains a baseline and a fallback. + +The strategic value is flexibility. A strong graph and an explicit edit model let us replace weak projections, test specialized ones and add AI assistance where it proves useful. The interface can keep changing. The record does not change with it. + +## Up next + +Part 6, *Shared language is a performance feature*, returns to the construction of the Protocol: why precise terms reduced ambiguity for people and agents, and how the carrier competition chose Markdown without closing every authoring option. + +--- + +*About this article: Part 5 in a series introducing the [Libar Software Delivery Protocol](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753). Repository links are pinned to commit `b5e0a8be`. Current capabilities are distinguished from design options throughout; the AI presentation direction is not shipped functionality.* diff --git a/docs/articles/README.md b/docs/articles/README.md index 31e21b8dc..4f44a478d 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -1,198 +1,192 @@ -# Media Plan & Living Series Playbook — Libar Software Delivery Protocol +# Media plan and living series playbook -> **Pinned baseline:** commit [`5a0b22c`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6) (`5a0b22c2d9ead98c900b60ed6209342fce2cbef6`), the merge of [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33). -> **Posture:** This directory lives on a standing, unmerged pull request that is periodically refreshed as `main` advances. Every corpus number in this series is derived from the graph at the pinned commit—re-run the queries in [The Living-PR Refresh Playbook](#5-the-living-pr-refresh-playbook) rather than inheriting counts across updates. +> **Pinned source baseline:** commit [`b5e0a8be`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753), the merge of [PR #36](https://github.com/libar-dev/software-delivery-protocol/pull/36). +> **Posture:** This directory lives on a standing pull request refreshed as `main` advances. Current behavior, seed-design intent and future directions must read as three different things. Re-run every measurement at a new pin rather than inheriting it. The source baseline may be ahead of the article branch. Evidence verification therefore uses a separate checkout of the pinned source until the branch contains it. ---- +## 1. The series argument -## 1. Narrative Positioning & Core Thesis +Software teams keep intent in tickets and product documents, architecture in diagrams and wikis, decisions in ADRs, acceptance criteria in test files and run results in CI. The links between them are usually prose. AI coding agents inherit that fragmentation and must guess at structure. -### The problem we lead with -Software teams have more tools than ever, yet delivery truth is more fragmented than ever. Intent lives in tickets and product docs, architecture lives on whiteboards and stale wikis, decisions live in scattered ADR files, acceptance criteria live in `.feature` files, and verification lives in CI logs. Nothing joins them except rot-prone prose. Within weeks of implementation, documentation drifts from code. +Libar Software Delivery Protocol (`@libar-dev/software-delivery-protocol`, CLI `sdp`) models delivery as typed repository content, derives one graph and generates consumer-specific readings from it. Its founding slogan is: -When AI coding agents enter that environment, the fragmentation compounds. Agents read raw source files and stale Markdown, guess at boundaries, and hallucinate structure because **no typed model of the system exists in the repository for them to reason over**. +> Specs are code; the graph is derived; the `claim` stays honest; git is the event log. -### The thesis -**Libar Software Delivery Protocol** (`@libar-dev/software-delivery-protocol`, CLI `sdp`) is a programmatic, AI-native software-delivery system built on one bet: **the spec is the prompt is the design**. Instead of syncing external trackers or bolting a workflow state machine onto git, you model the whole delivery lifecycle as typed code in the repository, derive **one regenerable graph** from it, check **conformance and honesty** in CI, and generate every human- and AI-facing artifact off that graph. +The series has to carry four consequences, not just the engine behind them: -### The four-part slogan -Every article in the series reinforces one or more clauses of the founding slogan from the [concept overview](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/README.md): +1. **One Spec, enriched in place.** Eight kinds, three altitudes and four stated readiness rungs cover delivery intent without migrating an idea through artifact types. +2. **Authored intent and derived facts stay separate.** `declared`, `anchored` and `inferred` claims are never collapsed. `implemented` and `has-verifier` are derived from bindings; `observed` remains deferred. +3. **Projections are cheap to replace.** Markdown, Mermaid, Gherkin, HTML, CLI answers and generated notes are readings, not competing stores. The current Studio is working material for UX learning, not a finished interface. +4. **AI has explicit trust boundaries.** Agents already read the graph and apply scoped intent. AI-assisted presentation is a future direction that must preserve those boundaries and address non-determinism honestly. -1. **Specs are code** — One authored truth-primitive (`Spec`) enriched in place across three independent descriptors (`kind` × `altitude` × `readiness`), authored in Markdown (`.sdp.md`) by default, with TypeScript (`.sdp.ts`) as a lawful per-ID option and graph-aware Gherkin (`.sdp.gherkin`) as a per-ID option for `behavior` and `example` Specs only (the Gherkin carrier option, MD-27). -2. **The graph is derived** — One flat, deterministic, regenerable graph (`graph = f(repo)`) serves as the sole read model for validators, human views, and coding agents. Delete `generated/` and rebuild it byte-for-byte. -3. **The `claim` stays honest** — Every fact carries its epistemic status (`declared` human intent, `anchored` source bindings, or `inferred` mechanical structure) and the three are never collapsed. The delivery facts `implemented` and `has-verifier` are derived today from resolving edges, never hand-authored; `observed` is designed into the model but not yet derived. -4. **Git is the event log** — History and prior states live in git commits, not in a second database or ticket status machine. +The pre-code seed design in [`docs/lineage/v0-design/`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/v0-design) is primary context for the product experience. It is historical evidence, not current law. Current Specs decide what is built and ratified. ---- +## 2. Audiences and distribution -## 2. Target Audiences & Distribution Strategy +| Audience | Problem to lead with | Protocol story | Main articles | +|---|---|---|---| +| Staff engineers and architects | Design and code drift apart; impact and ownership are hard to reconstruct | Stable Spec identities, source anchors, claim-aware graph, architecture and design slices | 1, 3, 7, 9 | +| AI and coding-agent builders | Verb walls and raw-file search waste calls and context | `sdp q`, typed reader, recipes, context bundles and explicit edit hand-offs | 1, 4, 5, 9 | +| Engineering leaders and technical founders | Process theater and hand-authored status create false confidence | Readiness floors instead of quotas, live backlog and drift queries, evidence with named limits | 1, 2, 5, 6 | +| Product, QA and design reviewers | The repository holds the design but the useful review job is hard to perform | Stamped Studio previews, task-based projections, open questions, run evidence and scoped intent | 2, 3, 5, 9 | +| UX/UI designers | The product has many possible readings and no settled visual language | One graph, disposable projections, visible honesty, current Studio as prototype | 2 and 5, with 1 as orientation | +| BDD practitioners | Scenario text and step regexes drift; non-behavioral truth fits Gherkin poorly | Typed example spaces, generated contracts, oracles and lawful carriers | 3 and 8 | + +### Channels + +- Publish long-form manuscripts from `docs/articles/` to the project blog, dev.to or another canonical home. Keep commit-pinned repository links and runnable try-it blocks. +- Convert Markdown tables to lists or accessible images on platforms that do not render tables. +- Lead technical launches with Articles 1, 4 and 7. Lead design and product discussions with Article 2. +- Generate the articles' Markdown tables, source excerpts, CLI output and Mermaid blocks at the publication pin. Screenshots are optional UX review material, not the primary evidence and never a substitute for regenerable content. +- Use compact versions of Articles 1, 2 and 5 as the first UX/UI design packet, together with the Studio page set generated at the source pin. Article 2 already contains the compact design brief. The generated pages let the designer inspect the current interface without relying on screenshots. The packet does not need to wait for public serialization. +- For audio or NotebookLM, supply the five full manuscripts and the reading path below, not the whole repository. + +## 3. Source reading path + +### Current vocabulary and jobs + +1. [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/CONTEXT.md), the ratified language. +2. [`docs/concept/00`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/concept/00-vision-scope-and-mvp-boundary.md) and [`01`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/concept/01-founding-principles-and-invariants.md), scope and principles. +3. [`jtbd-stories/05`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/jtbd-stories/05-see-and-share.md) and [`06`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/jtbd-stories/06-edit-through-the-lens.md), the human reading and edit jobs. + +### Seed design and annotation evidence + +4. [`docs/lineage/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/README.md), the lineage guide and annotation-scout pointer. +5. Seed [`00`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/v0-design/00-vision-and-product.md), [`01`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/v0-design/01-core-primitives.md) and [`02`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/v0-design/02-system-architecture.md), the original model and canonical/derived split. +6. Seed [`07`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/v0-design/07-spec-studio-and-projections.md) and [`08`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/v0-design/08-delivery-evidence-and-tooling.md), the original human experience, AI slices and evidence ambition. +7. The [short annotation context](../lineage/annotation-context.md), with the archived scout behind it and the rulings carried by [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29). It explains why complete annotation exclusion was an overcorrection and why source bindings are critical to projections. + +### Current carrying Specs and on-ramps -| Audience segment | What hurts them today | What hooks them in the Protocol | Primary articles | +8. [`spec:model.core-model`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/model/core-model.sdp.md) and [`spec:consumers.projections-model`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/projections-model.sdp.md). +9. [`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/spec-studio.sdp.md), its lenses, sharing and intent-panel Specs in the same directory. +10. [`spec:consumers.agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/agent-surface.sdp.md), [`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/context-bundle.sdp.md) and [`spec:consumers.design-slice`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/design-slice.sdp.md). +11. The five skills under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/.agents/skills) and the [recipe catalog](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/agent-surface/recipes.md). +12. [`examples/checkout-v1`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/examples/checkout-v1), the worked engineering loop. + +For any corpus fact, query the graph before reading a carrier. The source list is reading material for prose and design context, not permission to bypass the graph for measurements. + +## 4. The ten-part series + +| # | Title and file | Status | Job in the series | |---|---|---|---| -| **Staff / Principal Engineers & Software Architects** | Architecture diagrams and ADRs rot the week after a design review; no way to verify whether code still honors the design or which decisions shape a module. | One primitive with named coordinates; zero-import `@sdp*` comment-form anchors (`satisfies` vs. `references`, `role`, `layer`, `context`); deterministic conformance + honesty checks in CI. | Articles 1, 2, 5, 7 | -| **AI / Coding-Agent Builders & Power Users** | `AGENTS.md` files bloat into thousands of lines of prose; MCP tools and CLI "verb walls" force agents to make 20+ calls or grep raw files and guess. | "Agents script the graph, not a verb wall": `sdp q` + `createReader`, 43 runnable recipes, context bundles composed within a budget counted in UTF-16 code units, and structured change proposals. | Articles 1, 3, 7, 8 | -| **Engineering Leaders & Technical Founders** | Process theatre—mandatory sprint gates, Jira status choreography, and "100% spec coverage" quotas that produce AI-generated boilerplate nobody reads. | "Adopt the nouns, reject the gates" (MD-2); readiness floors instead of quotas; the build backlog (`ready` Specs without `implemented`, excluding `example` and `decision` Specs) and the drift alarm (`implemented ∧ ¬ready`) as live graph queries. | Articles 1, 4, 7 | -| **BDD & Testing Practitioners** | Cucumber `.feature` files drift from step regexes, duplicate domain logic, and turn non-behavioral NFRs or domain models into awkward Given/When/Then prose. | Carrier-independent executable specs: typed `gwt-vocabulary` example spaces, generated step and space contracts, generated registrars committed once a test adopts them (adopted registrars are committed, MD-31), and authored `specOracle` expected-outcome functions. | Articles 2, 4, 6 | - -### Channel & format rollout -- **Long-form canonical posts** — Publish from the Markdown manuscripts in `docs/articles/` to the project blog, Substack, or dev.to, with commit-pinned links to the public repository (`libar-dev/software-delivery-protocol`) and `try-it` blocks readers can run in 60 seconds. -- **Tables on other platforms** — Substack and Medium do not render Markdown tables. When cross-posting there, convert each table in an article (and in this playbook) to a bulleted list, one bullet per row, or to a rendered image with the same content as its alt text. dev.to and GitHub render the tables as written. -- **Hacker News / Lobsters launches** — Lead with Articles 1, 3, and 5. Title for technical substance and contrarian clarity (e.g., why we refused to carry over Gen 1's 24-tag registry, why we built one JS evaluation sink instead of 30 CLI subcommands, why we ran a 4-way carrier competition to choose Markdown). -- **Visual & interactive companions** — Pair Article 3 and Article 7 with the static HTML **Spec Studio** screenshots from [`reviews/assets/pr33`](https://github.com/libar-dev/software-delivery-protocol/tree/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/assets/pr33) and live static-host previews generated by `sdp studio`. -- **Podcast / Audio briefings (NotebookLM)** — Feed [the 24 core sources](#the-24-core-sources-pinned-to-5a0b22c) plus Articles 1–3 into NotebookLM to produce an audio overview for commuters and architecture-review prep. - -### The 24 Core Sources (Pinned to `5a0b22c`) - -
-The repository files the series draws on, each pinned to commit 5a0b22c (expand) - -**Vocabulary and principles** - -1. [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md) — the ratified glossary -2. [`docs/concept/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/README.md) — the concept overview and the four-part slogan -3. [`docs/concept/00-vision-scope-and-mvp-boundary.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md) — vision, scope, and non-goals -4. [`docs/concept/01-founding-principles-and-invariants.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md) — principles P1 to P10 -5. [`docs/concept/04-authoring-and-binding.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/04-authoring-and-binding.md) — authoring and binding -6. [`docs/concept/06-consumers-and-projections.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/06-consumers-and-projections.md) — consumers and the two surfaces -7. [`docs/concept/DECISIONS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/DECISIONS.md) — the decision registry - -**Carrying Specs** - -8. [`specs/model/core-model.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/core-model.sdp.md) — the one primitive and its descriptors -9. [`specs/decisions/carrier-ruling.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/carrier-ruling.sdp.md) — the carrier ruling (MD-18) -10. [`specs/decisions/anchor-comment-form.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md) — the comment form (MD-36) -11. [`specs/decisions/anchor-binding-grain.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md) — bindings are optional, plural, and may reference a design (MD-37) -12. [`specs/decisions/architectural-annotation.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/architectural-annotation.sdp.md) — architectural significance is annotated where it is realized (MD-38) -13. [`specs/extraction/executable-contracts.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md) — generated step and space contracts -14. [`specs/consumers/agent-surface.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/agent-surface.sdp.md) — the agent surface and the demand map -15. [`specs/consumers/context-bundle.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md) — the context bundle -16. [`specs/validation/corpus-rules.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md) — corpus rules under `sdp validate --rules` -17. [`specs/consumers/spec-studio.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md) — the Spec Studio - -**Agent on-ramps and recipes** - -18. [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/recipes.md) — the runnable recipe catalog -19. [`.agents/skills/sdp-agent-surface/SKILL.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md) — the reading skill -20. [`.agents/skills/sdp-authoring/SKILL.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md) — the authoring skill -21. [`.agents/skills/sdp-sessions/SKILL.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md) — the sessions skill - -**Worked example, lineage, and review** - -22. [`examples/checkout-v1/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md) — the worked MVP example -23. [`docs/lineage/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage/README.md) — the restored v0 design documents and the Gen 1 lineage scout report -24. [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md) — the whole-corpus refresh record - -
- ---- - -## 3. The 8-Part Article Series Plan - -| # | Title & file | Status | Core angle | Key repo sources & PRs | -|---|---|---|---|---| -| **01** | **[The Spec Is the Prompt Is the Design: Why AI-Native Software Delivery Needs One Graph, Not Another Ticket Tracker](./01-the-spec-is-the-prompt-is-the-design.md)** | **Ready in this PR** | The launch manifesto: why delivery truth fractures across tools, why AI agents hallucinate structure without a graph, and how one primitive (`Spec`), three claims (`declared` / `anchored` / `inferred`), and two check families (conformance + honesty) replace workflow gates. | [`docs/concept/00`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/00-vision-scope-and-mvp-boundary.md), [`01`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/01-founding-principles-and-invariants.md), [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md), [`spec:model.core-model`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/core-model.sdp.md), [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1) | -| **02** | **[From Rough Idea to Verified Code: A Hands-On Tour of the Libar Software Delivery Protocol](./02-walkthrough-intent-to-verified-code.md)** | **Ready in this PR** | The step-by-step engineer's tour through `pack:checkout-v1` and the self-hosted engine: authoring `.sdp.md` Specs (with a sidebar on the `.sdp.gherkin` option), clearing readiness floors, binding code with constant-form and `@sdp*` comment-form anchors, generating step/space contracts, binding an authored `specOracle`, and tripping the checks on purpose. | [`examples/checkout-v1/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/examples/checkout-v1/README.md), [`docs/concept/04`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/04-authoring-and-binding.md), [`sdp-authoring`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-authoring/SKILL.md), [PR #3](https://github.com/libar-dev/software-delivery-protocol/pull/3), [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29) | -| **03** | **[Stop Building 30-Verb CLI Walls for Coding Agents: Scripting the Delivery Graph with `sdp q` and Spec Studio](./03-stop-building-verb-walls-for-coding-agents.md)** | **Ready in this PR** | How coding agents and human reviewers consume the same derived graph: the demand map (`findByConcept`, `byFile`, `blastRadius`), the `sdp q` evaluation sink, growing by recipe instead of CLI verbs, scaling to a 243-Spec adopter with context bundles and corpus rules, and browsing the static HTML Spec Studio. | [`docs/concept/06`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/06-consumers-and-projections.md), [`sdp-agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-agent-surface/SKILL.md), [`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/spec-studio.sdp.md), [PR #14](https://github.com/libar-dev/software-delivery-protocol/pull/14), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | -| **04** | **Shared Language Is a Performance Feature: Ubiquitous Language, Rejecting Workflow Gates, and the Four-Way Carrier Competition** | Planned | Why settling the words first (`claim` vs. provenance, `altitude` vs. abstraction, `readiness floor` vs. readiness profile) gave the engine a design to be built on, and how a four-entrant carrier competition (Markdown vs. custom grammar vs. Gherkin fork vs. TSX), closed on exhibits, scorecards, and probes, ruled `.sdp.md` the default while keeping one canonical surface per ID. | [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md), [`DECISIONS.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/concept/DECISIONS.md), [`spec:decisions.carrier-ruling`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/carrier-ruling.sdp.md), [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1), PRs [#4](https://github.com/libar-dev/software-delivery-protocol/pull/4) to [#7](https://github.com/libar-dev/software-delivery-protocol/pull/7), [PR #8](https://github.com/libar-dev/software-delivery-protocol/pull/8), [PR #17](https://github.com/libar-dev/software-delivery-protocol/pull/17), [PR #19](https://github.com/libar-dev/software-delivery-protocol/pull/19) | -| **05** | **What We Learned Rebuilding `@libar-dev/architect`: Git Archaeology, the Comment-Form Reversal, and Honest Code Bindings** | Planned | A candid post-mortem on Gen 1 (`@libar-dev/architect`), how an evidence-based scout report overturned our own Gen 2 refusal of identity tags in doc comments, and why separating `satisfies` (code that realizes a Spec) from `references` (code that answers to a design without realizing it) stopped a stub from reading as `implemented`. | [`docs/lineage/README.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/lineage/README.md), [`docs/agent-surface/architecture.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/docs/agent-surface/architecture.md), [`spec:decisions.anchor-comment-form`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-comment-form.sdp.md), [`spec:decisions.anchor-binding-grain`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/decisions/anchor-binding-grain.sdp.md), [PR #28](https://github.com/libar-dev/software-delivery-protocol/pull/28), [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29), [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32) | -| **06** | **Executable Specifications Without Cucumber Glue: Typed Example Spaces, Generated Contracts, and Expected-Outcome Oracles** | Planned | Why traditional BDD rots at the regex boundary, and how the Protocol generates TypeScript step and space contracts and deterministic registrars from `gwt-vocabulary` example spaces and bound points, then checks each bound example against an authored `specOracle` whose answers include `unspecified`. | [`spec:extraction.executable-contracts`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/executable-contracts.sdp.md), [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/extraction/runnable-modules.sdp.md), [`spec:validation.oracle-target-eligibility`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/oracle-target-eligibility.sdp.md), [PR #3](https://github.com/libar-dev/software-delivery-protocol/pull/3), [PR #19](https://github.com/libar-dev/software-delivery-protocol/pull/19), [PR #21](https://github.com/libar-dev/software-delivery-protocol/pull/21) | -| **07** | **Designing a 243-Spec Platform Ahead of Its Code: Context Bundles, Entry Addresses, and Corpus Rules at Scale** | Planned | How the first adopter (`libar-platform`) pressured the Protocol while designing six Packs months ahead of their code: entry addresses (`#design.`, `#ui.`, `#question.`, `#rule.`), pinned declarations, context bundles within a default budget of 100,000 UTF-16 code units, planned components, and `sdp validate --rules`. | [`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/consumers/context-bundle.sdp.md), [`spec:validation.corpus-rules`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/validation/corpus-rules.sdp.md), [`spec:model.planned-architecture`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/model/planned-architecture.sdp.md), [PR #26](https://github.com/libar-dev/software-delivery-protocol/pull/26), [PR #27](https://github.com/libar-dev/software-delivery-protocol/pull/27), [PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31), [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33) | -| **08** | **Auditing 258 Self-Hosted Specs with Cross-Family AI Reviewers: Inside the 595-Finding Whole-Corpus Refresh** | Planned | How one developer and multi-model agent teams audited the entire self-hosted corpus (258 Specs at PR #32's base, `46f0280`) against the engine: splitting it into 13 units, running Fidelity and Quality lenses across model families, cross-verifying 595 findings (refuting 59), applying them in 18 lanes, each in its own worktree, and re-transcribing the self-hosting oracle from the carriers. | [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md), [`specs/self-hosting.pack.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/specs/self-hosting.pack.sdp.md), [`sdp-sessions`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/.agents/skills/sdp-sessions/SKILL.md), [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32) | - -### Detailed outlines for Planned Articles (04–08) - -#### Article 04: *Shared Language Is a Performance Feature* -- **Hook:** An unratified domain vocabulary costs every reader, human or agent: ambiguous words like *status*, *abstraction*, and *provenance* silently corrupt both human design reviews and subagent reasoning. -- **Section 1 — Define first, name last:** The governing rubric from [`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md): adopt established industry nouns, reject process state-machine gates (adopt the nouns, reject the gates, MD-2), and keep a coined word only for a genuine differentiator, where the word must name the difference it marks. -- **Section 2 — Seven renames the engine was built on:** The rename table in [PR #1](https://github.com/libar-dev/software-delivery-protocol/pull/1)'s description, settled before any engine code: `claim` replacing provenance, `anchor` replacing marker, `altitude` replacing abstraction, `section` replacing facet, `readiness floor` replacing readiness profile, `Pack` replacing SpecPack, and `reader` and `impact graph` replacing handle and mechanical substrate. `CONTEXT.md` keeps the rejected names in its ledger; one came back with a narrower job, since `marker` now names only the bracket prefix of an open question or keyed entry, never the binding. -- **Section 3 — The four-way carrier competition:** Four entrants, each with its own exhibit PR ([#4](https://github.com/libar-dev/software-delivery-protocol/pull/4) to [#7](https://github.com/libar-dev/software-delivery-protocol/pull/7)): F2 Markdown and the C2 custom grammar as full exhibits judged on their recorded scorecards in [PR #8](https://github.com/libar-dev/software-delivery-protocol/pull/8), the Gherkin fork and typed markup (TSX) closed earlier on falsifiable probes. Why C2's permanent ecosystem tax (parser, formatter, highlighting, editor integrations, LSP, and a grammar every agent must be taught) lost to `.sdp.md` with the TS DSL as a lawful per-ID option (the carrier ruling, MD-18), and how a graph-aware `.sdp.gherkin` option for behavior and example Specs arrived later ([PR #17](https://github.com/libar-dev/software-delivery-protocol/pull/17); the Gherkin carrier option, MD-27). - -#### Article 05: *What We Learned Rebuilding `@libar-dev/architect`* -- **Hook:** Our first-generation tool (`@libar-dev/architect`) worked in production, and Gen 1's own self-hosted graph held 349 patterns—and it taught us every trap of source-first architecture graphs. -- **Section 1 — The sins we refused to carry over:** Sprawling tag registries, workflow lifecycle gates (`candidate → roadmap → active → completed`), and hand-authored delivery status. -- **Section 2 — When git archaeology corrects memory:** How the lineage scout report (carried by [PR #28](https://github.com/libar-dev/software-delivery-protocol/pull/28)) read Gen 1's commit history and found that the "50 tags shrank to 26" story was a cleanup of the formal vocabulary, not of a shipped runtime registry, and that what actually drifted was two Gherkin files sharing one identity, CLI fixtures leaking into the production scan, and misread tag syntax. Its tag inventory, not the history, showed the overloaded `implements`, used both for code realizing a Spec and for a feature file pointing at production code. -- **Section 3 — The comment-form reversal ([PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29)):** Three rulings, each grounded in the scout, the original design, and the owner's direction to move closer to it. The `@sdp*` comment form (the comment form, MD-36) answered the first adopter (`libar-platform`), whose runtime code cannot import the Protocol package, so every one of its code anchors sat in a test. Optional plural bindings and the non-conferring `references` edge (bindings are optional, plural, and may reference a design, MD-37) answered its stub problem: the only way to name a design from code was to claim to realize it. Architectural attributes where code is realized (architectural significance is annotated where it is realized, MD-38) came from the original design and the owner, not the adopter: a `role` on any code anchor, and a `layer` and `context` on `component:` anchors only. The collective reading of `satisfies`, where the satisfying units together realize every promise of the Spec, was made explicit later, as ruling R01 of the whole-corpus refresh ([PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32)). - -#### Article 06: *Executable Specifications Without Cucumber Glue* -- **Hook:** Everyone loves readable scenarios until a one-word phrasing change breaks a regex step definition—or worse, passes silently while testing the wrong value. -- **Section 1 — Carrier-independent executability:** How `sdp build` turns `gwt-vocabulary` example spaces and bound points (`{n: 2}`, `{total: 100}`) into typed TypeScript step and space contracts under `generated/contracts/`. -- **Section 2 — Authored → generated, never the reverse:** How the generated registrar, written beside its test as a `*.test.generated.ts` file and committed once that test imports it (adopted registrars are committed, MD-31), takes four required adapters (`createWorld`, `invoke`, `observe`, `expected`) and an optional fifth (`assertions`), without the generated code ever importing authored code. -- **Section 3 — The `specOracle` and `unspecified`:** Why expected-outcome semantics belong in an authored, typed oracle beside the tests—and why an oracle that answers `unspecified` for an unmodeled region reports a coverage gap rather than a failure. One caveat belongs in the article: a bound executable example whose point lands on `unspecified` still turns its test red, because no authored Then may silently witness a gap. - -#### Article 07: *Designing a 243-Spec Platform Ahead of Its Code* -- **Hook:** What happens when an adopter models a transactional platform in 243 Specs (measured at its pin, per [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)) across seven layers, 0 to 6, and states the components of layers 3 to 6 as designs before their code exists? The assumptions a small example never tests start to break, and the design-at-scale loop has to be built. -- **Section 1 — Addressing one entry without splitting the Spec:** Entry addresses (`spec:#design.`, `#ui.`, `#question.`, `#rule.`, from [PR #27](https://github.com/libar-dev/software-delivery-protocol/pull/27)), pinned declarations (recipe 24), and the key-family register (recipe 41). The limit belongs in the article too: an entry address is admitted in prose, where it must resolve, and refused in every ID slot (checked mentions, MD-40), so a relation still targets a whole Spec. -- **Section 2 — Fitting a Pack into a model's context budget:** Recipe 36 (the context bundle) cuts in a deterministic seven-step order. At first `pack:history-view` needed at least 299,106 UTF-16 code units against the default budget of 100,000; [PR #33](https://github.com/libar-dev/software-delivery-protocol/pull/33)'s compact bundle shape plus the `declarations: "addresses"` opt-in brought it to 93,806. Two of the platform's Packs still exceed the budget after every cut, and are read through a perspective (recipe 37) until then. -- **Section 3 — Corpus rules and planned architecture:** Running project-specific policies and the architecture walk (recipe 42) via `sdp validate --rules`, and comparing component design Specs against realized `component:` anchors with recipe 43. - -#### Article 08: *Auditing 258 Self-Hosted Specs with Cross-Family AI Reviewers* -- **Hook:** When your repository self-hosts 258 Specs and 40 recipes (measured at `46f0280`, the merge of PR #31 and the base of [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32)), how do you verify that the Specs, the engine, and the docs actually agree? -- **Section 1 — Two lenses, two model families:** The methodology behind [`reviews/26-whole-corpus-refresh.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/reviews/26-whole-corpus-refresh.md) ([PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32)): pairing a Fidelity lens (code + tests vs. Spec rules) with a Quality lens (cross-Spec consistency, vocabulary, readiness honesty), then cross-verifying every finding with the opposite model family. -- **Section 2 — What the 595 findings caught (and the 59 that were refuted):** Code that broke its own promises, such as Gherkin status lookalikes (`@status.ready`) passing as decoration; two homes of the law that disagreed, such as the authoring skill reading `satisfies` collectively while a Spec read it per unit (ruled as R01); and why cross-verification by the other model family was needed to refute false blockers. -- **Section 3 — The distillation boundary and living hygiene:** Two recipes with different jobs. Slimming candidates (recipe 39) lists the pinned declarations of implemented, verified Specs that bound code may now carry, so a person can slim a Spec past the distillation boundary. The curation report (recipe 40) curates anchors rather than Specs: over the file-level imports `sdp build --imports` writes, it ranks load-bearing files no anchor covers and imports that disagree with anchored `uses` edges. - ---- - -## 4. Editorial House Rules & Guardrails - -Every article in `docs/articles/` must conform to the repository's ratified language and automated checks: - -1. **Ratified terminology only ([`CONTEXT.md`](https://github.com/libar-dev/software-delivery-protocol/blob/5a0b22c2d9ead98c900b60ed6209342fce2cbef6/CONTEXT.md)).** - - Use **`Spec`**, **`Pack`**, **`section`**, **`anchor`** (constant form / comment form), **`claim`** (`declared` / `anchored` / `inferred`), **`altitude`** (`epic` / `feature` / `story`), **`readiness`** (`idea` / `scoped` / `defined` / `ready`), **`readiness floor`**, **`delivery fact`** (`implemented` / `has-verifier` / `observed`). - - Readiness is **stated** or **asserted**, never "claimed" ("claim" is reserved for the `claim` taxonomy). - - The Protocol **defines a contract** and instances **conform**; never write that the Protocol "governs" or "polices" a project ("checks police conformance and honesty" is the sole guardrail phrasing). - - State **"deterministically validated," never "provably correct."** - - Lead decision references with meaning first, code in parentheses: e.g., *the carrier ruling (MD-18)*, *the agent front door (MD-22)*, *the comment form (MD-36)*. -2. **Temporal-guard compliance (`npm run check:temporal`).** - - `check-temporal.mjs` sweeps every tracked file and every untracked file `.gitignore` does not exclude, except the temporal-by-genre artifacts (`plans/`, `reviews/`, `explorations/`, `docs/lineage/`, `docs/concept/DECISIONS.md`, the tracked drafts, plans, and evidence under `.omo/`) and the machine-generated `package-lock.json`. - - Never include raw calendar dates (`YYYY-MM-DD`), numbered plan paths (`plans/`), or session/wave/fold tokens in `docs/articles/*.md`. To point a reader at a numbered plan or a dated lineage file, link to the pull request that carried it (`.../pull/33`), never to the file itself, and never encode a path to slip it past the check. -3. **Every count is pinned and re-derivable.** - - A count of the Protocol's own corpus names the commit it was measured at, and a reader can re-derive it with an `sdp validate` or `sdp q` command, given beside the number or in this playbook. - - An adopter figure or a historical figure cites the pull request or review that measured it. -4. **Links survive publication.** - - Inside the repository, link sibling articles relatively (`./02-….md`); at publication, rewrite each relative link to the published URL or to a commit-pinned GitHub URL, since a relative link breaks off the repository. - ---- - -## 5. The Living-PR Refresh Playbook - -Because this pull request stays open and is periodically updated as `main` evolves, refreshing the series after merging or rebasing onto a new `main` commit takes four deterministic steps: +| 01 | [The spec is the prompt is the design](./01-the-spec-is-the-prompt-is-the-design.md) | Full draft | The problem, one Spec, one graph, claim separation and the two check families | +| 02 | [One graph, many views: the interface is allowed to change](./02-one-graph-many-views.md) | Full draft and UX/UI input | The seed's human scenario updated to current mechanics, the projection family, Studio honesty and a compact designer's brief | +| 03 | [From rough idea to verified code](./03-walkthrough-intent-to-verified-code.md) | Full draft | The engineering loop through the worked example, bindings, contracts, oracles and deliberate failures | +| 04 | [Stop building 30-verb CLI walls for coding agents](./04-stop-building-verb-walls-for-coding-agents.md) | Full draft | Demand map, `sdp q`, recipes, bundles in parts, design slices and measured agent use | +| 05 | [AI can shape the reading, but it cannot rewrite the record](./05-ai-can-shape-the-reading.md) | Full direction draft | Built AI roles versus future presentation assistance, including the deterministic-projection tension | +| 06 | Shared language is a performance feature | Detailed outline | Vocabulary decisions, readiness wording and the four-way carrier competition | +| 07 | Excluding annotations was the mistake: what three generations taught us | Detailed outline | Gen 1, the pre-code seed and the current Protocol; the return of source bindings and what projections need from them | +| 08 | Executable specifications without Cucumber glue | Detailed outline | The design behind typed example spaces, contracts and expected-outcome oracles, without repeating Part 3's tutorial | +| 09 | Designing a large platform ahead of its code | Detailed outline | The first adopter, entry addresses, bundles in parts, design slices, corpus rules and roadmap readings | +| 10 | Auditing your own corpus with cross-family AI reviewers | Detailed outline | A transferable review method, with the historical whole-corpus audit as evidence rather than the headline | + +### Article 06: *Shared language is a performance feature* + +- Open on the cost of ambiguous words such as `status`, `abstraction` and `provenance` for people and agents. +- Explain the governing rubric in `CONTEXT.md`: adopt established delivery nouns, reject process gates, coin a term only for a real difference. +- Use the seed's "readiness is a claim" as the concrete collision. Today readiness is stated, while `claim` names epistemic status. The wording change records a model decision. +- Cover the rename ledger and why a rejected name may return with a narrower job, as `marker` did for carrier syntax. +- Tell the four-way carrier competition through its exhibits and probes. Explain why Markdown won the default, why TypeScript remains lawful per ID and why graph-aware Gherkin returned for behavior and example Specs. +- Keep one canonical carrier per ID as the closing rule. + +### Article 07: *Excluding annotations was the mistake* + +- Start with the apparent contradiction: Gen 1's tag sprawl was a real failure, but removing source annotations altogether cut off the bindings that make architecture, traceability and impact projections valuable. +- Use three generations rather than a simple old-versus-new story. Gen 1 proved production value and taxonomy drift. The pre-code seed restored structured source markers to the design. The early current Protocol overcorrected by refusing JSDoc identity tags and restricting bindings. +- Let the annotation scout supply the evidence: formal vocabulary cleanup was not the same as a shipped runtime registry shrinking; multiple extractors do not require multiple read models; source bindings feed generated documentation. +- Explain why adoption made the cost visible. Runtime code that could not import the package had to bind in tests, and source-file queries missed the actual units. +- Cover the comment-form return, optional plural bindings, `satisfies` versus `references`, and architectural `role`, `layer` and `context` where they are realized. +- Connect the ruling to Article 2: a projection cannot show a component, realization or design reference the graph does not hold. Anchors are identity and structure, never Spec prose, and that narrowness is what makes them safe to project. +- Include what did not return: sprawling tags, workflow statuses in code and hand-authored delivery facts. + +### Article 08: *Executable specifications without Cucumber glue* + +- Lead with regex rot and with the deeper problem of tests verifying values that no longer match the authored example. +- Explain carrier independence: Markdown and Gherkin can derive the same example space and contracts. +- Focus on the authored/generated direction, point-per-example, bound points and the outcome union. Point back to Part 3 for registrar mechanics. +- Explain the oracle's job and `unspecified`, including why an authored Then that lands on `unspecified` still fails rather than silently witnessing a gap. +- Use the Protocol's own reader Spec in `.sdp.gherkin` as the self-hosting example. +- Keep the boundary clear: `has-verifier` records a binding, supplied run evidence records results, and `observed` remains a separate deferred fact. + +### Article 09: *Designing a large platform ahead of its code* + +- Use the first adopter's historical 243-Spec pin as the opening scale example, not as a current count. +- Show why design ahead of code needs entry addresses, pinned declarations and planned components rather than fabricated implementation bindings. +- Follow one design pass through a context bundle. Explain deterministic cuts, complete scopes and ordered parts. Do not repeat the retired claim that a bundle never returns a subset of members. +- Add the design slice as the whole-Pack reading and the design diff as the comparison between passes. +- Explain change proposals against a named base and corpus rules that remain separate from Protocol findings. +- Close with capability maps, design slices and the priority roadmap as current readings of planned work, with recipe 53 as the live source of next work. + +### Article 10: *Auditing your own corpus with cross-family AI reviewers* + +- Lead with the method, not the finding count: one lens checks Specs against code and tests, another checks cross-Spec consistency and language, and a different model family verifies each finding. +- Use the historical 258-Spec, 595-finding audit as the evidence at its own pin. Include the 59 refuted findings so review does not become a volume contest. +- Show what the method caught: code that broke promises, two homes of one law and stale authoring guidance. +- Explain why self-hosting makes drift testable and why the self-hosting oracle must be re-transcribed rather than inherited. +- Close with slimming candidates and curation advice as different recurring hygiene tasks. + +## 5. Editorial house rules + +1. Use ratified terminology from `CONTEXT.md`. Readiness is stated, never "claimed." The Protocol defines a contract and instances conform. Claim "deterministically validated," never "provably correct." +2. Label time and status. Current realization comes from the pinned graph and source. The seed design is pre-code evidence and direction. AI-assisted presentation is a future direction, not shipped functionality. +3. Do not present today's Studio layout as settled UX. It is a working projection used to learn which readings help. Conversely, do not treat an open visual question as permission to change graph law in prose. +4. Keep AI roles distinct: reading the graph, applying scoped intent and assisting presentation. If an AI-generated view varies, do not silently attribute the deterministic projection guarantee to it. +5. Derive every current count at the named commit. Cite historical adopter and audit figures to the pull request or review that measured them. +6. Run `npm run check:temporal`. Articles must not contain raw calendar dates, numbered plan paths or session/wave/fold tokens. Link the pull request that carried historical material instead of a dated lineage file. +7. Link sibling articles relatively. At publication, rewrite relative links to public URLs or commit-pinned repository URLs. Refresh sibling links after renumbering. +8. Use sentence-case headings and plain language. Cut repeated definitions; cross-link to the article that owns the detailed treatment. +9. Check code fences and links manually. The standard formatter ignores `docs/**`, and the green gate does not prove that article links resolve. +10. Regenerate article evidence at the publication pin and run its check mode. Screenshots are optional UX review material. Name the commit beside any screenshot or shared Pack export. + +## 6. Refresh playbook + +A refresh measures both numbers and qualitative claims. A design diff alone cannot tell you that the series now describes an old Studio phase or misses a new recipe parameter. ### Step 1: Install and build + ```sh -npm ci # installs and builds dist/, including dist/recipes/ +npm ci ``` -The commands below use `npm run --silent sdp:q -- …`, which adds the corpus's three exclusions; `pnpm --silent sdp:q …` is equivalent where pnpm is installed. Never run a bare `sdp` in this checkout. +Use `npm run --silent sdp:q --` or `pnpm --silent sdp:q` in this checkout. Never invoke a bare `sdp`. -### Step 2: Measure the design diff from the last pinned commit -Generate the base graph at the previously pinned commit (currently `5a0b22c2d9ead98c900b60ed6209342fce2cbef6`) in a temporary worktree outside the repository and run recipe 35 (the design diff) to see every Spec, Pack, section, entry key, and binding that changed. Keep the worktree outside: inside the checkout it is an untracked directory the gate trips over, and `check:temporal` fails on it. +### Step 2: Diff against the previous pin + +Generate the base graph in a temporary worktree outside the repository. The current previous pin is `b5e0a8bea1e3bedd48c616f96c9928e955792753`. ```sh -BASE_SHA="5a0b22c2d9ead98c900b60ed6209342fce2cbef6" +BASE_SHA="b5e0a8bea1e3bedd48c616f96c9928e955792753" BASE_DIR="$(mktemp -d)/base" git worktree add --detach "$BASE_DIR" "$BASE_SHA" && ln -s "$PWD/node_modules" "$BASE_DIR/node_modules" npm --prefix "$BASE_DIR" run build -node "$BASE_DIR/dist/cli/sdp.js" build "$BASE_DIR" --exclude explorations --exclude examples --exclude test/fixtures/import/parity -npm run --silent sdp:q -- "$(cat dist/recipes/35-the-design-diff.js)" --params "base=@$BASE_DIR/generated/graph.json" --params '{"detail":true}' --json +node "$BASE_DIR/dist/cli/sdp.js" build "$BASE_DIR" \ + --exclude explorations --exclude examples --exclude test/fixtures/import/parity +npm run --silent sdp:q -- "$(cat dist/recipes/35-the-design-diff.js)" \ + --params "base=@$BASE_DIR/generated/graph.json" --params '{"detail":true}' --json git worktree remove --force "$BASE_DIR" ``` -### Step 3: Re-derive the headline corpus metrics at the new `HEAD` +Read the roadmap at the new head too: + +```sh +npm run --silent sdp:q -- "$(cat dist/recipes/53-the-roadmap.js)" \ + --params '{"pack":"pack:roadmap"}' --json +``` + +### Step 3: Re-derive corpus metrics + ```sh -# 1. Overall graph, node-type, claim, readiness, kind, and delivery-fact census -npm run --silent sdp:q -- 'return { - commit: "HEAD", +npm run --silent sdp:q -- 'const tally = (xs, f) => xs.reduce((a, x) => ((a[f(x)] = (a[f(x)] || 0) + 1), a), {}); return { schemaVersion: graph.schemaVersion, specs: g.specs().length, - packs: g.packs().map((p) => p.id), + packs: graph.nodes.filter((n) => n.nodeType === "Pack").map((p) => ({ id: p.id, members: p.members.length })), nodes: graph.nodes.length, - nodeTypes: graph.nodes.reduce((a, n) => ((a[n.nodeType] = (a[n.nodeType] || 0) + 1), a), {}), + nodeTypes: tally(graph.nodes, (n) => n.nodeType), edges: graph.edges.length, - edgeClaims: graph.edges.reduce((a, e) => ((a[e.claim] = (a[e.claim] || 0) + 1), a), {}), + edgeClaims: tally(graph.edges, (e) => e.claim), locations: graph.locations.length, - readiness: g.specs().reduce((a, s) => ((a[s.statedReadiness] = (a[s.statedReadiness] || 0) + 1), a), {}), - kinds: g.specs().reduce((a, s) => ((a[s.specKind] = (a[s.specKind] || 0) + 1), a), {}), + readiness: tally(g.specs(), (s) => s.statedReadiness), + kinds: tally(g.specs(), (s) => s.specKind), deliveryFacts: { implemented: g.specs().filter((s) => s.deliveryFacts.includes("implemented")).length, hasVerifier: g.specs().filter((s) => s.deliveryFacts.includes("has-verifier")).length, @@ -200,44 +194,59 @@ npm run --silent sdp:q -- 'return { } }' --json -# 2. Operational build backlog (recipe 1) and drift alarm (recipe 2) npm run --silent sdp:q -- "$(cat dist/recipes/01-the-build-backlog.js)" --json npm run --silent sdp:q -- "$(cat dist/recipes/02-the-drift-alarm.js)" --json - -# 3. Open-question register (recipe 20) and design backlog (recipe 33) npm run --silent sdp:q -- "$(cat dist/recipes/20-open-question-register.js)" --json npm run --silent sdp:q -- "$(cat dist/recipes/33-the-design-backlog.js)" --json +ls dist/recipes/*.js | wc -l +``` -# 4. Pack design summaries across all Packs (recipe 29) -npm run --silent sdp:q -- ' - const out = []; - for (const p of g.packs()) { - const r = await (async (params) => { '"$(cat dist/recipes/29-pack-design.js)"' })({ pack: p.id }); - out.push({ id: r.id, totals: r.totals }); - } - return out; -' --json +Baseline measured at `b5e0a8be`: -# 5. Shipped recipe bodies -ls dist/recipes/*.js | wc -l + +| Metric | Value | +|---|---| +| Schema | `0.12.0` | +| Specs and readiness | 456: 447 `ready`, 4 `scoped`, 5 `idea` | +| Packs | 9: `adopter-speed-v1`, `spec-studio-v1`, `corpus-checks-v1`, `design-iteration-v1`, `design-management-v1`, `obsidian-vault-v1`, `roadmap`, `roadmaps-and-slices-v1`, `self-hosting-v1` | +| Nodes, edges and locations | 1,072 nodes (456 Primitive, 9 Pack, 241 CodeNode, 366 Anchor); 2,978 edges (1,929 declared, 1,049 anchored); 3,120 locations | +| Delivery facts | 166 `implemented`, 397 `has-verifier`, 0 `observed` | +| Build backlog | 2, with 232 ready examples and 47 ready decisions audited and excluded | +| Drift alarm | 0 | +| Design backlog | 0 | +| Open questions | 16 across 12 Specs: 10 blocking (6 deferrals, 4 holds), 6 non-blocking | +| Validation | 0 errors and 3 intentional `honesty/gaps` warnings | +| Shipped recipes | 55 | + + +### Step 4: Regenerate the article evidence + +Marked evidence blocks are generated, not maintained by hand. The sync reads the pinned commit from this playbook, verifies that the source checkout matches it, runs the real CLI and extracts the actual source. It covers graph-derived tables, build and query output, source anchors, the oracle and Mermaid. + +```sh +# SOURCE_DIR is a built checkout of the commit named as the source baseline. +node docs/articles/sync-evidence.mjs "$SOURCE_DIR" + +# Check mode leaves the manuscripts unchanged and fails when any block differs. +node docs/articles/sync-evidence.mjs --check "$SOURCE_DIR" ``` -### Step 4: Baseline snapshot at `5a0b22c` (for comparison on next refresh) -Measured at `5a0b22c2d9ead98c900b60ed6209342fce2cbef6` (re-run the commands above rather than inheriting): - -| Metric | Value at `5a0b22c` | Source in Step 3 (command · output field) | -|---|---|---| -| Schema version | `0.10.0` | 1 · `schemaVersion` | -| Authored `Spec`s | `276` (`245 ready`, `17 defined`, `3 scoped`, `11 idea`) | 1 · `specs`, `readiness` | -| Authored `Pack`s | `6` (`self-hosting-v1`, `design-management-v1`, `spec-studio-v1`, `corpus-checks-v1`, `design-iteration-v1`, `obsidian-vault-v1`) | 1 · `packs` | -| Total nodes / edges / locations | `621` nodes (`276 Primitive`, `6 Pack`, `160 CodeNode`, `179 Anchor`) · `1,624` edges (`1,063 declared`, `561 anchored`) · `1,774` locations | 1 · `nodes`, `nodeTypes`, `edges`, `edgeClaims`, `locations` | -| Delivery facts | `94 implemented` · `205 has-verifier` · `0 observed` | 1 · `deliveryFacts` | -| Operational build backlog | `0` (`110` ready examples and `41` ready decisions audited and excluded) | 2 · recipe 1's `total`, `excludedReadyExamples`, `excludedReadyDecisions` | -| Drift alarm (`implemented ∧ ¬ready`) | `0` | 2 · recipe 2's `total` | -| Design backlog (`defined` Specs no code realizes) | `17` Specs | 3 · recipe 33's `totals.specs` | -| Open questions | `43` across `24` Specs (`16` blocking: `8` deferrals with triggers, `8` holds; `27` non-blocking) | 3 · recipe 20's `totals` (`questions`, `specs`, `blocking`, `deferrals`, `holds`, `nonBlocking`) | -| Shipped recipes | `43` runnable bodies under `dist/recipes/` | 5 · the file count | - -After updating any changed counts or commit links, stage the files (`preflight` fails on an untracked file `.gitignore` does not exclude) and run the full green gate, `npm run check`: thirteen steps from `check:temporal` to `preflight`, all of which must exit `0`. - -That gate covers less of this directory than it seems. `check:temporal` does sweep `docs/articles/` for dates, numbered plan paths, and session tokens, but `.prettierignore` excludes `docs/**`, so `format:check` never reads these articles, and `lint` reads only code. Nothing in the gate catches an unbalanced code fence or a broken link here. Before publishing, check both yourself, with a throwaway script you do not commit: every fence must close with a backtick run at least as long as the one that opened it (a Markdown example that contains its own fences needs a four-backtick outer fence), and every relative link and every commit-pinned GitHub link must resolve. +When the article branch itself is at the baseline commit, omit `$SOURCE_DIR`. Add a marked block and its generator whenever a manuscript gains a reusable output excerpt. Keep elision explicit, as in the constant-anchor excerpt. Do not paste a modified version of source or command output and call it an example. + +Mermaid stays live source. Markdown tables come from graph queries. Screenshots remain optional supplements for discussing visual design, never the only record of behavior. + +### Step 5: Check version-sensitive prose + +- Recount recipes and skills. The current counts are 55 and five. +- Check `sdp q` bindings. The current sink injects `g`, `graph`, `report`, `params` and `recipes`. +- Re-read the Studio's carrying Specs and publish a current page set. Check lenses, roadmap, registers, queue, diff, runs, export and composing panel rather than saying "phase 0" from memory. +- Check context-bundle parameters and behavior. `part` now returns ordered parts; never restore the old blanket statement that partial member lists are refused. +- Re-run the worked example. At this pin it reports 11 Specs, two Packs, five anchors, 18 nodes and 34 edges. +- Keep run evidence, `has-verifier` and `observed` distinct. Supplied CTRF results do not derive runtime observation. +- Review command coverage. Any new reusable output belongs in a generated evidence block. +- Check every sibling link after renumbering, every commit-pinned path, every code fence and every image. +- Record which articles need conceptual changes, not only which tables need new counts. This is the feedback loop between the repository and the series. + +### Step 6: Verify + +Run `npm run check:articles -- "$SOURCE_DIR"` and `npm run check` after staging new article files so preflight can see them. CI runs the same article check in a built worktree of the pinned source with full git history, so a manuscript branch behind the pin cannot merge stale evidence green. Then manually inspect the rendered Markdown, relative links and commit-pinned links. The main gate does not check those links or all Markdown fence structure. diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs new file mode 100644 index 000000000..9b1a6eae2 --- /dev/null +++ b/docs/articles/sync-evidence.mjs @@ -0,0 +1,240 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const articlesDir = path.dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = path.resolve(articlesDir, "../.."); +const playbook = readFileSync(path.join(articlesDir, "README.md"), "utf8"); +const pinnedCommit = playbook.match(/Pinned source baseline:.*tree\/([0-9a-f]{40})/)?.[1]; + +if (!pinnedCommit) { + throw new Error("The publication plan does not name a full pinned source commit."); +} + +if (process.argv.includes("--print-pin")) { + console.log(pinnedCommit); + process.exit(0); +} + +const check = process.argv.includes("--check"); +const sourceArgument = process.argv.slice(2).find((argument) => argument !== "--check"); +const sourceRoot = path.resolve(sourceArgument ?? repositoryRoot); +const realSourceRoot = realpathSync(sourceRoot); +const sourceCommit = execFileSync("git", ["-C", sourceRoot, "rev-parse", "HEAD"], { + encoding: "utf8", +}).trim(); + +if (sourceCommit !== pinnedCommit) { + throw new Error( + `Article baseline ${pinnedCommit} does not match source checkout ${sourceCommit}. ` + + "Pass a checkout of the pinned commit as the source path.", + ); +} + +if (!existsSync(path.join(sourceRoot, "dist/cli/sdp.js"))) { + throw new Error(`Build the source checkout first: npm --prefix ${sourceRoot} run build`); +} + +const exclusions = [ + "--exclude", + "explorations", + "--exclude", + "examples", + "--exclude", + "test/fixtures/import/parity", +]; + +function runCli(args) { + const result = spawnSync(process.execPath, [path.join(sourceRoot, "dist/cli/sdp.js"), ...args], { + cwd: sourceRoot, + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, + }); + const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; + if (result.status !== 0) { + throw new Error(`sdp ${args.join(" ")} failed:\n${output}`); + } + return output.replaceAll(realSourceRoot, "").replaceAll(sourceRoot, "").trim(); +} + +function query(body, root = sourceRoot, selfHosting = true) { + const args = ["q", body, "--root", root, "--json", ...(selfHosting ? exclusions : [])]; + return JSON.parse(runCli(args)); +} + +function recipe(number, parameters = {}, root = sourceRoot, selfHosting = true) { + return query(`return await recipes(${number}, ${JSON.stringify(parameters)})`, root, selfHosting); +} + +function sourceText(relativePath) { + return readFileSync(path.join(sourceRoot, relativePath), "utf8"); +} + +function selfHostingCensus() { + const specs = query(`return { + schemaVersion: graph.schemaVersion, + specs: g.specs().length, + packs: graph.nodes.filter((n) => n.nodeType === "Pack").map((p) => ({ id: p.id, members: p.members.length })), + nodes: graph.nodes.length, + nodeTypes: graph.nodes.reduce((a, n) => ((a[n.nodeType] = (a[n.nodeType] || 0) + 1), a), {}), + edges: graph.edges.length, + edgeClaims: graph.edges.reduce((a, e) => ((a[e.claim] = (a[e.claim] || 0) + 1), a), {}), + locations: graph.locations.length, + readiness: g.specs().reduce((a, s) => ((a[s.statedReadiness] = (a[s.statedReadiness] || 0) + 1), a), {}), + deliveryFacts: { + implemented: g.specs().filter((s) => s.deliveryFacts.includes("implemented")).length, + hasVerifier: g.specs().filter((s) => s.deliveryFacts.includes("has-verifier")).length, + observed: g.specs().filter((s) => s.deliveryFacts.includes("observed")).length + } + }`); + return { + ...specs, + backlog: recipe(1), + drift: recipe(2), + designBacklog: recipe(33), + questions: recipe(20).totals, + recipes: readdirSync(path.join(sourceRoot, "dist/recipes")).filter((file) => file.endsWith(".js")).length, + }; +} + +function articleCensus() { + const census = selfHostingCensus(); + return `| Measure | Value | +|---|---| +| Authored Specs | ${census.specs}: ${census.readiness.ready ?? 0} \`ready\`, ${census.readiness.scoped ?? 0} \`scoped\`, ${census.readiness.idea ?? 0} \`idea\` | +| Packs | ${census.packs.length} | +| Source and test bindings | ${census.nodeTypes.CodeNode ?? 0} CodeNodes and ${census.nodeTypes.Anchor ?? 0} Anchors | +| Graph | ${census.nodes.toLocaleString("en-US")} nodes, ${census.edges.toLocaleString("en-US")} edges, ${census.locations.toLocaleString("en-US")} locations | +| Delivery facts | ${census.deliveryFacts.implemented} \`implemented\`, ${census.deliveryFacts.hasVerifier} \`has-verifier\`, ${census.deliveryFacts.observed} \`observed\` | +| Operational build backlog | ${census.backlog.total} | +| Drift alarm | ${census.drift.total} | +| Runnable recipes | ${census.recipes} |`; +} + +function baselineMetrics() { + const census = selfHostingCensus(); + const validation = runCli(["validate", ".", ...exclusions]); + const validationMatch = validation.match(/validate: (\d+) errors · (\d+) warnings/); + if (!validationMatch) { + throw new Error("Could not read the self-hosting validation summary."); + } + const packNames = census.packs.map((pack) => `\`${pack.id.replace(/^pack:/, "")}\``).join(", "); + return `| Metric | Value | +|---|---| +| Schema | \`${census.schemaVersion}\` | +| Specs and readiness | ${census.specs}: ${census.readiness.ready ?? 0} \`ready\`, ${census.readiness.scoped ?? 0} \`scoped\`, ${census.readiness.idea ?? 0} \`idea\` | +| Packs | ${census.packs.length}: ${packNames} | +| Nodes, edges and locations | ${census.nodes.toLocaleString("en-US")} nodes (${census.nodeTypes.Primitive} Primitive, ${census.nodeTypes.Pack} Pack, ${census.nodeTypes.CodeNode} CodeNode, ${census.nodeTypes.Anchor} Anchor); ${census.edges.toLocaleString("en-US")} edges (${census.edgeClaims.declared.toLocaleString("en-US")} declared, ${census.edgeClaims.anchored.toLocaleString("en-US")} anchored); ${census.locations.toLocaleString("en-US")} locations | +| Delivery facts | ${census.deliveryFacts.implemented} \`implemented\`, ${census.deliveryFacts.hasVerifier} \`has-verifier\`, ${census.deliveryFacts.observed} \`observed\` | +| Build backlog | ${census.backlog.total}, with ${census.backlog.excludedReadyExamples} ready examples and ${census.backlog.excludedReadyDecisions} ready decisions audited and excluded | +| Drift alarm | ${census.drift.total} | +| Design backlog | ${census.designBacklog.totals.specs} | +| Open questions | ${census.questions.questions} across ${census.questions.specs} Specs: ${census.questions.blocking} blocking (${census.questions.deferrals} deferrals, ${census.questions.holds} holds), ${census.questions.nonBlocking} non-blocking | +| Validation | ${validationMatch[1]} errors and ${validationMatch[2]} intentional \`honesty/gaps\` warnings | +| Shipped recipes | ${census.recipes} |`; +} + +function exampleBuild() { + return runCli(["build", "examples/checkout-v1"]) + .split("\n") + .filter((line) => /^\d+ specs/.test(line) || line.startsWith("Wrote ")) + .join("\n"); +} + +function exampleDrift() { + const answer = recipe(2, {}, path.join(sourceRoot, "examples/checkout-v1"), false); + return JSON.stringify( + { + total: answer.total, + alarms: answer.alarms.map((alarm) => ({ + id: alarm.id, + statedReadiness: alarm.statedReadiness, + floorReached: alarm.floorReached, + implementationBindings: alarm.implementationBindings, + })), + }, + null, + 2, + ); +} + +function constantAnchor() { + const source = sourceText("examples/checkout-v1/src/orders/create-order.use-case.ts"); + const importLine = source.split("\n")[0]; + const anchor = source.match(/export const createOrderUseCaseAnchor = codeAnchor\(\{[\s\S]*?\n\}\);/)?.[0]; + if (!anchor) throw new Error("Could not locate the create-order constant anchor."); + return `${importLine}\n\n// …\n\n${anchor}`; +} + +function commentAnchor() { + const source = sourceText("src/projections/census.ts"); + const anchorIndex = source.indexOf("@sdpAnchor impl:protocol.census-page"); + const start = source.lastIndexOf("/**", anchorIndex); + const end = source.indexOf("*/", anchorIndex); + if (anchorIndex < 0 || start < 0 || end < 0) throw new Error("Could not locate the census comment anchor."); + return source.slice(start, end + 2); +} + +function oracle() { + const source = sourceText("examples/checkout-v1/test/orders/create-order.oracle.ts"); + const start = source.indexOf("export function expected("); + const end = source.indexOf("\n}", start); + if (start < 0 || end < 0) throw new Error("Could not locate the create-order oracle."); + return source.slice(start, end + 2); +} + +function byFile() { + return JSON.stringify(query(`return g.byFile("src/projections/census.ts")`), null, 2); +} + +function roadmapMermaid() { + runCli(["mermaid", "examples/checkout-v1"]); + const page = sourceText("examples/checkout-v1/generated/mermaid/pack/checkout-roadmap.md"); + const match = page.match(/```mermaid\n([\s\S]*?)\n```/); + if (!match) throw new Error("Could not read the generated checkout-roadmap Mermaid block."); + return match[1]; +} + +const blocks = [ + { id: "self-hosting-census", file: "01-the-spec-is-the-prompt-is-the-design.md", language: null, generate: articleCensus }, + { id: "roadmap-mermaid", file: "02-one-graph-many-views.md", language: "mermaid", generate: roadmapMermaid }, + { id: "example-build", file: "03-walkthrough-intent-to-verified-code.md", language: "text", generate: exampleBuild }, + { id: "example-drift", file: "03-walkthrough-intent-to-verified-code.md", language: "json", generate: exampleDrift }, + { id: "constant-anchor", file: "03-walkthrough-intent-to-verified-code.md", language: "ts", generate: constantAnchor }, + { id: "comment-anchor", file: "03-walkthrough-intent-to-verified-code.md", language: "ts", generate: commentAnchor }, + { id: "oracle", file: "03-walkthrough-intent-to-verified-code.md", language: "ts", generate: oracle }, + { id: "by-file", file: "04-stop-building-verb-walls-for-coding-agents.md", language: "json", generate: byFile }, + { id: "baseline-metrics", file: "README.md", language: null, generate: baselineMetrics }, +]; + +const drift = []; +for (const block of blocks) { + const filePath = path.join(articlesDir, block.file); + const original = readFileSync(filePath, "utf8"); + const opening = ``; + const closing = ``; + const pattern = new RegExp(`${opening.replaceAll("-", "\\-")}\\n[\\s\\S]*?\\n${closing.replaceAll("-", "\\-")}`); + if (!pattern.test(original)) { + throw new Error(`${block.file} is missing the ${block.id} evidence markers.`); + } + const body = block.generate().trim(); + const replacement = block.language + ? `${opening}\n\`\`\`${block.language}\n${body}\n\`\`\`\n${closing}` + : `${opening}\n${body}\n${closing}`; + const updated = original.replace(pattern, replacement); + if (updated === original) continue; + if (check) { + drift.push(`${block.file}: ${block.id}`); + } else { + writeFileSync(filePath, updated); + console.log(`Updated ${block.file}: ${block.id}`); + } +} + +if (drift.length > 0) { + console.error(`Article evidence is stale:\n${drift.join("\n")}`); + process.exitCode = 1; +} else { + console.log(`${check ? "Checked" : "Synchronized"} ${blocks.length} evidence blocks at ${pinnedCommit}.`); +} diff --git a/docs/lineage/README.md b/docs/lineage/README.md index a97b972ab..cc9413b7c 100644 --- a/docs/lineage/README.md +++ b/docs/lineage/README.md @@ -1 +1 @@ -This folder holds the v0 design documents restored verbatim with lineage headers and the dated annotation scout, `annotation-scout-2026-10-04.md`, written as evidence for the owner's anchor decisions. Both are historical evidence, never intended truth. The v0 concept set was superseded 2026-06-06 by the reboot and restored from `ed5f6ad^` for the executable-verification design review, `reviews/14`. Intended truth lives in `specs/`, ratified decision Specs under `specs/decisions/`, with their names in `docs/concept/DECISIONS.md`, and current realization in `src/`; on any disagreement this folder is the stale side. Restored documents enter with a lineage header naming the superseding commit; the named scout is the authored-report exception already present here. The pre-ratification vocabulary files (`docs/concept/ubiquitous-language.md`, `docs/concept/GLOSSARY.md`) were deliberately not restored: the first was renamed to root `CONTEXT.md` (live), the second was a post-reboot intermediate it superseded. +This folder holds the v0 design documents restored verbatim with lineage headers and the annotation evidence written for the owner's anchor decisions. Start with [the short annotation context](./annotation-context.md). The [archived annotation scout](./archive-annotation-scout-2026-10-04.md) retains the detailed evidence temporarily and may be removed later. All of this is historical evidence, never intended truth. The v0 concept set was superseded 2026-06-06 by the reboot and restored from `ed5f6ad^` for the executable-verification design review, `reviews/14`. Intended truth lives in `specs/`, ratified decision Specs under `specs/decisions/`, with their names in `docs/concept/DECISIONS.md`, and current realization in `src/`; on any disagreement this folder is the stale side. Restored documents enter with a lineage header naming the superseding commit. The pre-ratification vocabulary files (`docs/concept/ubiquitous-language.md`, `docs/concept/GLOSSARY.md`) were deliberately not restored: the first was renamed to root `CONTEXT.md` (live), the second was a post-reboot intermediate it superseded. diff --git a/docs/lineage/annotation-context.md b/docs/lineage/annotation-context.md new file mode 100644 index 000000000..9ed78eea6 --- /dev/null +++ b/docs/lineage/annotation-context.md @@ -0,0 +1,40 @@ +# Annotation context + +This is the short orientation to the annotation decisions. It is historical and design context, not current law. The carrying Specs under `specs/` and the vocabulary in `CONTEXT.md` remain authoritative. The detailed scout is retained temporarily as [archive material](./archive-annotation-scout-2026-10-04.md). + +## The lesson + +The first generation, `@libar-dev/architect`, proved that source annotations could join code, behavior, decisions and generated documentation in one graph. It also showed the cost of a broad tag registry: the formal vocabulary, parser, checks and guidance drifted apart. + +The pre-code seed design kept structured source markers in the model. The current Protocol's early design then overcorrected. It refused JSDoc identity tags and restricted source bindings too narrowly. Complete annotation exclusion was a mistake: without source bindings, projections cannot show which units realize a Spec, which tests verify it, how components relate or which design existing code follows. + +The correction was not a return to the old tag registry. It was a small binding model with two representations and one graph. + +## What an anchor contributes + +An anchor is a human-written pointer in source code or tests. It binds identity and optional structure into the graph. It does not carry behavior, rationale, readiness, workflow status or delivery facts. + +The graph needs those bindings for several kinds of projections: + +- `satisfies` identifies code that realizes a Spec and confers `implemented` when the edge resolves. +- `verifies` identifies a test or example that verifies a Spec and can confer `has-verifier`. +- `models` binds an expected-outcome oracle to an example space. +- `references` identifies code that answers to a design without claiming to realize it. It confers no delivery fact. +- `memberOf` and `uses` record curated architectural structure between code units. +- `role`, and `layer` and `context` on component anchors, describe the unit's architectural place. + +These bindings let a projection answer from repository evidence rather than reconstructing architecture from filenames, imports or prose. A binding still does not prove behavior. `implemented` is not liveness, and `has-verifier` is not a passing test. + +## The current representations + +The constant form uses a statically extracted top-level builder call. The comment form uses reserved `@sdp*` tags in a top-level block and needs no runtime import. Both feed the same closed anchor envelope and derive the same graph model. + +Bindings are optional and plural. A unit may name only structure, reference an unrealized design, or realize several Specs. Code that follows a decision uses `references`, not `satisfies`. + +Declared intent in Specs, anchored bindings in source and inferred mechanical structure keep their distinct claims. Multiple extractors and representations do not imply multiple read models. + +## What stayed excluded + +The old process metadata did not return. Source anchors do not carry ticket status, hand-authored completion or a second readiness model. Git records history. CI supplies run results separately. The graph derives delivery facts from resolving edges. + +The detailed evidence behind the correction lives in the archived scout and [PR #29](https://github.com/libar-dev/software-delivery-protocol/pull/29). Current law lives in `spec:model.anchors`, `spec:extraction.claim-taxonomy`, `spec:decisions.anchor-comment-form`, `spec:decisions.anchor-binding-grain` and `spec:decisions.architectural-annotation`. diff --git a/docs/lineage/annotation-scout-2026-10-04.md b/docs/lineage/archive-annotation-scout-2026-10-04.md similarity index 100% rename from docs/lineage/annotation-scout-2026-10-04.md rename to docs/lineage/archive-annotation-scout-2026-10-04.md diff --git a/package.json b/package.json index 6de740c47..47a1d89d3 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "format": "prettier --write .", "format:check": "prettier --check .", "check:temporal": "node ./check-temporal.mjs", + "check:articles": "node docs/articles/sync-evidence.mjs --check", "generate:example": "node ./projection-suite.mjs examples/checkout-v1", "generate:self-hosting": "node ./projection-suite.mjs . --exclude explorations --exclude examples --exclude test/fixtures/import/parity", "check:self-hosting": "node ./projection-suite.mjs . --exclude explorations --exclude examples --exclude test/fixtures/import/parity --check-clean", From e7d4342fd397782e28db2e8e1a36866245e2850c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Sat, 31 Oct 2026 15:28:22 +0100 Subject: [PATCH 07/36] docs(articles): clarify pinned-source verification --- docs/articles/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/README.md b/docs/articles/README.md index 4f44a478d..2660fb4ff 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -1,7 +1,7 @@ # Media plan and living series playbook > **Pinned source baseline:** commit [`b5e0a8be`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753), the merge of [PR #36](https://github.com/libar-dev/software-delivery-protocol/pull/36). -> **Posture:** This directory lives on a standing pull request refreshed as `main` advances. Current behavior, seed-design intent and future directions must read as three different things. Re-run every measurement at a new pin rather than inheriting it. The source baseline may be ahead of the article branch. Evidence verification therefore uses a separate checkout of the pinned source until the branch contains it. +> **Posture:** This directory lives on a standing pull request refreshed as `main` advances. Current behavior, seed-design intent and future directions must read as three different things. Re-run every measurement at a new pin rather than inheriting it. Evidence verification always uses a separate checkout of the exact source pin, so manuscript commits cannot change the answers being verified. CI creates that checkout with full git history. ## 1. The series argument From 14883657d9541bf7af862aa60894d417110084db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Sat, 31 Oct 2026 15:49:04 +0100 Subject: [PATCH 08/36] docs(articles): address publication review feedback --- docs/articles/03-walkthrough-intent-to-verified-code.md | 2 +- .../04-stop-building-verb-walls-for-coding-agents.md | 2 +- docs/articles/README.md | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/articles/03-walkthrough-intent-to-verified-code.md b/docs/articles/03-walkthrough-intent-to-verified-code.md index 3b64a8e93..cd9b8ed0a 100644 --- a/docs/articles/03-walkthrough-intent-to-verified-code.md +++ b/docs/articles/03-walkthrough-intent-to-verified-code.md @@ -268,7 +268,7 @@ The second example, `invalid-cart`, declares `verifies` but has no test anchor. ## Break it on purpose -Run each experiment, then restore the example with `git checkout -- examples/checkout-v1` and rerun `npm run generate:example`. +Run each experiment in a disposable worktree. If you edit your checkout directly, restore only the files changed by that experiment and rerun `npm run generate:example`. | Experiment | Edit | Run | Expected result | |---|---|---|---| diff --git a/docs/articles/04-stop-building-verb-walls-for-coding-agents.md b/docs/articles/04-stop-building-verb-walls-for-coding-agents.md index e7fb0e00a..55b90354c 100644 --- a/docs/articles/04-stop-building-verb-walls-for-coding-agents.md +++ b/docs/articles/04-stop-building-verb-walls-for-coding-agents.md @@ -20,7 +20,7 @@ Before designing [`spec:consumers.agent-surface`](https://github.com/libar-dev/s The typed reader freezes a small set of entry adapters for those arrivals: -- `g.findByConcept(term)` searches IDs, titles, binding labels, Pack framing, narratives and section values, and names the matching field. +- `g.findByConcept(term)` searches IDs, titles, anchor labels, Pack framing, narratives and section values, and names the matching field. - `g.byFile(path)` returns every node recorded at a path and the Specs reachable from them. - `g.blastRadius(files)` reports directly impacted Specs and Packs, the explicit one-hop neighborhood, changed units linked to no Spec and `coverageUnknown` files. diff --git a/docs/articles/README.md b/docs/articles/README.md index 2660fb4ff..b980b1653 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -1,7 +1,7 @@ # Media plan and living series playbook > **Pinned source baseline:** commit [`b5e0a8be`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753), the merge of [PR #36](https://github.com/libar-dev/software-delivery-protocol/pull/36). -> **Posture:** This directory lives on a standing pull request refreshed as `main` advances. Current behavior, seed-design intent and future directions must read as three different things. Re-run every measurement at a new pin rather than inheriting it. Evidence verification always uses a separate checkout of the exact source pin, so manuscript commits cannot change the answers being verified. CI creates that checkout with full git history. +> **Posture:** This directory lives on a standing pull request refreshed as `main` advances. Current behavior, seed-design intent and future directions must read as three different things. Re-run Protocol-corpus measurements at a new pin rather than inheriting them; adopter and historical counts keep their cited measurement sources. Evidence verification always uses a separate checkout of the exact source pin, so manuscript commits cannot change the answers being verified. CI creates that checkout with full git history. ## 1. The series argument @@ -155,15 +155,17 @@ Use `npm run --silent sdp:q --` or `pnpm --silent sdp:q` in this checkout. Never Generate the base graph in a temporary worktree outside the repository. The current previous pin is `b5e0a8bea1e3bedd48c616f96c9928e955792753`. ```sh +set -euo pipefail BASE_SHA="b5e0a8bea1e3bedd48c616f96c9928e955792753" BASE_DIR="$(mktemp -d)/base" -git worktree add --detach "$BASE_DIR" "$BASE_SHA" && ln -s "$PWD/node_modules" "$BASE_DIR/node_modules" +trap 'git worktree remove --force "$BASE_DIR" 2>/dev/null || true' EXIT +git worktree add --detach "$BASE_DIR" "$BASE_SHA" +ln -s "$PWD/node_modules" "$BASE_DIR/node_modules" npm --prefix "$BASE_DIR" run build node "$BASE_DIR/dist/cli/sdp.js" build "$BASE_DIR" \ --exclude explorations --exclude examples --exclude test/fixtures/import/parity npm run --silent sdp:q -- "$(cat dist/recipes/35-the-design-diff.js)" \ --params "base=@$BASE_DIR/generated/graph.json" --params '{"detail":true}' --json -git worktree remove --force "$BASE_DIR" ``` Read the roadmap at the new head too: @@ -176,6 +178,7 @@ npm run --silent sdp:q -- "$(cat dist/recipes/53-the-roadmap.js)" \ ### Step 3: Re-derive corpus metrics ```sh +set -euo pipefail npm run --silent sdp:q -- 'const tally = (xs, f) => xs.reduce((a, x) => ((a[f(x)] = (a[f(x)] || 0) + 1), a), {}); return { schemaVersion: graph.schemaVersion, specs: g.specs().length, From 39eec16733d46ce806445960910af6fce70d8adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Sat, 31 Oct 2026 15:57:00 +0100 Subject: [PATCH 09/36] docs(articles): move series maintenance onto main --- docs/articles/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/README.md b/docs/articles/README.md index b980b1653..c7d42bacd 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -1,7 +1,7 @@ # Media plan and living series playbook > **Pinned source baseline:** commit [`b5e0a8be`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753), the merge of [PR #36](https://github.com/libar-dev/software-delivery-protocol/pull/36). -> **Posture:** This directory lives on a standing pull request refreshed as `main` advances. Current behavior, seed-design intent and future directions must read as three different things. Re-run Protocol-corpus measurements at a new pin rather than inheriting them; adopter and historical counts keep their cited measurement sources. Evidence verification always uses a separate checkout of the exact source pin, so manuscript commits cannot change the answers being verified. CI creates that checkout with full git history. +> **Posture:** This directory is maintained on `main` through focused refresh PRs as the Protocol advances. The opening arc used a standing PR for review, but no permanent media diff is needed. Current behavior, seed-design intent and future directions must read as three different things. Re-run Protocol-corpus measurements at a new pin rather than inheriting them; adopter and historical counts keep their cited measurement sources. Evidence verification always uses a separate checkout of the exact source pin, so manuscript commits cannot change the answers being verified. CI creates that checkout with full git history. ## 1. The series argument From 9aa986a6a801e450a7736cca5d5fa7c825785437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Sat, 31 Oct 2026 16:07:55 +0100 Subject: [PATCH 10/36] fix(articles): reject dirty evidence sources --- docs/articles/README.md | 2 +- docs/articles/sync-evidence.mjs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/articles/README.md b/docs/articles/README.md index c7d42bacd..0970347ee 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -224,7 +224,7 @@ Baseline measured at `b5e0a8be`: ### Step 4: Regenerate the article evidence -Marked evidence blocks are generated, not maintained by hand. The sync reads the pinned commit from this playbook, verifies that the source checkout matches it, runs the real CLI and extracts the actual source. It covers graph-derived tables, build and query output, source anchors, the oracle and Mermaid. +Marked evidence blocks are generated, not maintained by hand. The sync reads the pinned commit from this playbook, verifies that the source checkout matches it and is clean, then runs the real CLI and extracts the actual source. It refuses changed or untracked source paths before any generation. It covers graph-derived tables, build and query output, source anchors, the oracle and Mermaid. ```sh # SOURCE_DIR is a built checkout of the commit named as the source baseline. diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index 9b1a6eae2..bb9d00793 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -32,6 +32,16 @@ if (sourceCommit !== pinnedCommit) { ); } +const sourceStatus = execFileSync("git", ["-C", sourceRoot, "status", "--porcelain"], { + encoding: "utf8", +}).trim(); +if (sourceStatus) { + throw new Error( + "The pinned source checkout must be clean before generating article evidence. " + + `Found changed or untracked paths:\n${sourceStatus}`, + ); +} + if (!existsSync(path.join(sourceRoot, "dist/cli/sdp.js"))) { throw new Error(`Build the source checkout first: npm --prefix ${sourceRoot} run build`); } From 8c5d1942d6827d60e2cda10276699612d212d302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Sat, 31 Oct 2026 16:19:52 +0100 Subject: [PATCH 11/36] fix(articles): verify the evidence CLI build --- .../03-walkthrough-intent-to-verified-code.md | 2 +- .../04-stop-building-verb-walls-for-coding-agents.md | 4 ++-- docs/articles/README.md | 2 +- docs/articles/sync-evidence.mjs | 12 ++++++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/articles/03-walkthrough-intent-to-verified-code.md b/docs/articles/03-walkthrough-intent-to-verified-code.md index cd9b8ed0a..9d1e31406 100644 --- a/docs/articles/03-walkthrough-intent-to-verified-code.md +++ b/docs/articles/03-walkthrough-intent-to-verified-code.md @@ -268,7 +268,7 @@ The second example, `invalid-cart`, declares `verifies` but has no test anchor. ## Break it on purpose -Run each experiment in a disposable worktree. If you edit your checkout directly, restore only the files changed by that experiment and rerun `npm run generate:example`. +Run each experiment in a disposable worktree. If an experiment file already has local changes, do not edit it in your checkout. Otherwise, restore only the files changed by that experiment and rerun `npm run generate:example`. | Experiment | Edit | Run | Expected result | |---|---|---|---| diff --git a/docs/articles/04-stop-building-verb-walls-for-coding-agents.md b/docs/articles/04-stop-building-verb-walls-for-coding-agents.md index 55b90354c..1db733529 100644 --- a/docs/articles/04-stop-building-verb-walls-for-coding-agents.md +++ b/docs/articles/04-stop-building-verb-walls-for-coding-agents.md @@ -88,9 +88,9 @@ Part 9 follows these mechanisms through the first adopter's design-at-scale work ## The surface improved under measured use -After the initial reader work, three agents from two model families answered thirteen delivery questions using the skills and `sdp q` alone. All three answered every question correctly. They also exposed recurring costs: oversized answers, unexplained misses, two meanings of "next rung" and rules without addresses or source lines. +After [PR #30](https://github.com/libar-dev/software-delivery-protocol/pull/30), three agents from two model families answered thirteen delivery questions using the skills and `sdp q` alone. All three answered every question correctly. They also exposed recurring costs: oversized answers, unexplained misses, two meanings of "next rung" and rules without addresses or source lines. -The next work added summary-first answers, explained misses, both next rungs, rule keys and recorded lines. A later agent run still answered 13 of 13 but used more calls and overflowed one batched answer. The response was not another verb. It was a clearer task-to-recipe guide and better answer shapes. +[PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31) added summary-first answers, explained misses, both next rungs, rule keys and recorded lines. A re-run on `gpt-6.1-sol`, recorded in [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32), again answered 13 of 13 but took 23 calls against the first trial's 20 and overflowed one batched answer. The response was not another verb. It was a clearer task-to-recipe guide and better answer shapes. That is the useful feedback loop. The graph remains stable while the on-ramps and recipes improve from observed use. diff --git a/docs/articles/README.md b/docs/articles/README.md index 0970347ee..6499f81d1 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -224,7 +224,7 @@ Baseline measured at `b5e0a8be`: ### Step 4: Regenerate the article evidence -Marked evidence blocks are generated, not maintained by hand. The sync reads the pinned commit from this playbook, verifies that the source checkout matches it and is clean, then runs the real CLI and extracts the actual source. It refuses changed or untracked source paths before any generation. It covers graph-derived tables, build and query output, source anchors, the oracle and Mermaid. +Marked evidence blocks are generated, not maintained by hand. The sync reads the pinned commit from this playbook, verifies that the source checkout matches it and is clean, and checks the CLI build metadata against the same commit before running anything. It refuses changed or untracked source paths and stale builds before any generation. It covers graph-derived tables, build and query output, source anchors, the oracle and Mermaid. ```sh # SOURCE_DIR is a built checkout of the commit named as the source baseline. diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index bb9d00793..35b323325 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -46,6 +46,18 @@ if (!existsSync(path.join(sourceRoot, "dist/cli/sdp.js"))) { throw new Error(`Build the source checkout first: npm --prefix ${sourceRoot} run build`); } +const buildInfoPath = path.join(sourceRoot, "dist/cli/build-info.json"); +if (!existsSync(buildInfoPath)) { + throw new Error(`Build metadata is missing at ${buildInfoPath}; rebuild the pinned source checkout.`); +} +const buildCommit = JSON.parse(readFileSync(buildInfoPath, "utf8")).commit; +if (buildCommit !== pinnedCommit) { + throw new Error( + `Article baseline ${pinnedCommit} does not match the CLI build ${buildCommit ?? "unknown"}. ` + + "Rebuild the pinned source checkout before generating evidence.", + ); +} + const exclusions = [ "--exclude", "explorations", From 230b0d28850ee7d927098595a2b7bfe5e6b79ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Sat, 31 Oct 2026 16:27:28 +0100 Subject: [PATCH 12/36] fix(articles): allow dependency links in clean sources --- docs/articles/sync-evidence.mjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index 35b323325..54bf3026e 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -32,9 +32,11 @@ if (sourceCommit !== pinnedCommit) { ); } -const sourceStatus = execFileSync("git", ["-C", sourceRoot, "status", "--porcelain"], { - encoding: "utf8", -}).trim(); +const sourceStatus = execFileSync( + "git", + ["-C", sourceRoot, "status", "--porcelain", "--", ".", ":(exclude)node_modules"], + { encoding: "utf8" }, +).trim(); if (sourceStatus) { throw new Error( "The pinned source checkout must be clean before generating article evidence. " + From 2a38d152e04fe82496de996b8006f667c518447f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 17:52:33 +0100 Subject: [PATCH 13/36] fix(articles): close the evidence check's review gaps - Insert each generated block through a replacer function, so `$$`, `$&`, `` $` `` and `$'` in a body are written literally. Before, sync wrote an altered body and check re-derived the same alteration and passed. - Install the pin's own locked dependencies in CI instead of linking the head's node_modules, so a dependency upgrade on main cannot break or change the pin's build. - Count self-hosting validation warnings by rule, and refuse to label a rule outside the two AGENTS.md documents as intentional. - Point prose at the generated blocks instead of copying their values; the baseline metrics also count the repository-owned skills. - Export the block replacement and the validation summary for tests, and run the CLI only as the main module, comparing real paths so a symlinked checkout still runs the check instead of exiting 0 having checked nothing. Bring the script under Prettier. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- .prettierignore | 3 + ...01-the-spec-is-the-prompt-is-the-design.md | 2 +- .../03-walkthrough-intent-to-verified-code.md | 4 +- ...p-building-verb-walls-for-coding-agents.md | 6 +- docs/articles/README.md | 11 +- docs/articles/sync-evidence.d.mts | 7 + docs/articles/sync-evidence.mjs | 489 +++++++++++------- test/article-evidence.test.ts | 95 ++++ 9 files changed, 414 insertions(+), 205 deletions(-) create mode 100644 docs/articles/sync-evidence.d.mts create mode 100644 test/article-evidence.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82d148325..230bc9618 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: BASE_SHA="$(node docs/articles/sync-evidence.mjs --print-pin)" BASE_DIR="${{ runner.temp }}/article-evidence" git worktree add --detach "$BASE_DIR" "$BASE_SHA" - ln -s "$PWD/node_modules" "$BASE_DIR/node_modules" + npm --prefix "$BASE_DIR" ci --ignore-scripts npm --prefix "$BASE_DIR" run build npm run check:articles -- "$BASE_DIR" git worktree remove --force "$BASE_DIR" diff --git a/.prettierignore b/.prettierignore index 313fb66bc..127c29003 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,9 @@ AGENTS.md CLAUDE.md CONTEXT.md docs/** +!docs/articles/ +!docs/articles/sync-evidence.mjs +!docs/articles/sync-evidence.d.mts jtbd-stories/** plans/** .sisyphus/** diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md index d998e0d0b..1da95c4bf 100644 --- a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -137,7 +137,7 @@ At commit `b5e0a8be`, its self-hosting graph reports: | Runnable recipes | 55 | -The self-hosting validation run reports 0 errors and 3 intentional `honesty/gaps` warnings, each a ready Spec without a resolving verifier. Re-run the commands below rather than carrying these numbers to another commit. +The [generated baseline metrics](./README.md#step-3-re-derive-corpus-metrics) report self-hosting validation errors and warnings by rule. A `honesty/gaps` warning names a ready Spec without a resolving verifier. Re-run the commands below rather than carrying these numbers to another commit. The first adopter has also used the Protocol to design a much larger platform ahead of implementation. That story, including context bundles in parts, design slices and corpus rules, belongs to Part 9. diff --git a/docs/articles/03-walkthrough-intent-to-verified-code.md b/docs/articles/03-walkthrough-intent-to-verified-code.md index 9d1e31406..3c8ed59cf 100644 --- a/docs/articles/03-walkthrough-intent-to-verified-code.md +++ b/docs/articles/03-walkthrough-intent-to-verified-code.md @@ -23,7 +23,7 @@ Inside the Protocol's own checkout, run the CLI as `npm run --silent sdp -- -Both Specs have implementation bindings and state only `defined`. Their structural floors reach `ready`, so they wait only for an author's readiness statement. Validation still reports 0 errors and 0 warnings. The drift alarm is a query, not a workflow gate. +The Specs listed by the generated drift answer have implementation bindings and state only `defined`. Their structural floors reach `ready`, so they wait only for an author's readiness statement. The generated build output above carries the validation totals. The drift alarm is a query, not a workflow gate. ## Author the parent and its example space diff --git a/docs/articles/04-stop-building-verb-walls-for-coding-agents.md b/docs/articles/04-stop-building-verb-walls-for-coding-agents.md index 1db733529..ae58f4c5f 100644 --- a/docs/articles/04-stop-building-verb-walls-for-coding-agents.md +++ b/docs/articles/04-stop-building-verb-walls-for-coding-agents.md @@ -70,7 +70,7 @@ The body is code, and `sdp q` runs it with the local process's full authority. N ## Grow by recipe, not by verb -A recurring question becomes a recipe in [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/agent-surface/recipes.md). Every recipe is executed as written by the test suite and shipped under `dist/recipes/`. The pinned commit carries 55. +A recurring question becomes a recipe in [`docs/agent-surface/recipes.md`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/agent-surface/recipes.md). Every recipe is executed as written by the test suite and shipped under `dist/recipes/`. The [generated baseline metrics](./README.md#step-3-re-derive-corpus-metrics) carry the recipe count at the pin. A recipe takes documented parameters through `--params`. An unknown parameter returns `{ unknownParams, accepts }` rather than silently changing the question. A caller can also wrap a recipe in its own body and filter the answer, without editing the shipped body. @@ -94,9 +94,9 @@ After [PR #30](https://github.com/libar-dev/software-delivery-protocol/pull/30), That is the useful feedback loop. The graph remains stable while the on-ramps and recipes improve from observed use. -## Five skills teach the workflow +## Repository-owned skills teach the workflow -A capable surface still needs an on-ramp. The package ships five repository-owned skills under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/.agents/skills): +A capable surface still needs an on-ramp. The package ships repository-owned skills, counted in the [generated baseline metrics](./README.md#step-3-re-derive-corpus-metrics), under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/.agents/skills): - `sdp-agent-surface` teaches graph reading and the task-to-recipe map. - `sdp-authoring` teaches the carriers, readiness floors, relations and bindings. diff --git a/docs/articles/README.md b/docs/articles/README.md index 6499f81d1..8d3cd1645 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -60,7 +60,7 @@ The pre-code seed design in [`docs/lineage/v0-design/`](https://github.com/libar 8. [`spec:model.core-model`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/model/core-model.sdp.md) and [`spec:consumers.projections-model`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/projections-model.sdp.md). 9. [`spec:consumers.spec-studio`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/spec-studio.sdp.md), its lenses, sharing and intent-panel Specs in the same directory. 10. [`spec:consumers.agent-surface`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/agent-surface.sdp.md), [`spec:consumers.context-bundle`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/context-bundle.sdp.md) and [`spec:consumers.design-slice`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/design-slice.sdp.md). -11. The five skills under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/.agents/skills) and the [recipe catalog](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/agent-surface/recipes.md). +11. The repository-owned skills under [`.agents/skills/`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/.agents/skills) and the [recipe catalog](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/agent-surface/recipes.md). 12. [`examples/checkout-v1`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/examples/checkout-v1), the worked engineering loop. For any corpus fact, query the graph before reading a carrier. The source list is reading material for prose and design context, not permission to bypass the graph for measurements. @@ -160,7 +160,7 @@ BASE_SHA="b5e0a8bea1e3bedd48c616f96c9928e955792753" BASE_DIR="$(mktemp -d)/base" trap 'git worktree remove --force "$BASE_DIR" 2>/dev/null || true' EXIT git worktree add --detach "$BASE_DIR" "$BASE_SHA" -ln -s "$PWD/node_modules" "$BASE_DIR/node_modules" +npm --prefix "$BASE_DIR" ci --ignore-scripts npm --prefix "$BASE_DIR" run build node "$BASE_DIR/dist/cli/sdp.js" build "$BASE_DIR" \ --exclude explorations --exclude examples --exclude test/fixtures/import/parity @@ -218,8 +218,9 @@ Baseline measured at `b5e0a8be`: | Drift alarm | 0 | | Design backlog | 0 | | Open questions | 16 across 12 Specs: 10 blocking (6 deferrals, 4 holds), 6 non-blocking | -| Validation | 0 errors and 3 intentional `honesty/gaps` warnings | +| Validation | 0 errors and 3 intentional warnings (3 `honesty/gaps`, 0 `conformance/prose-mentions`) | | Shipped recipes | 55 | +| Repository-owned skills | 5 | ### Step 4: Regenerate the article evidence @@ -240,11 +241,11 @@ Mermaid stays live source. Markdown tables come from graph queries. Screenshots ### Step 5: Check version-sensitive prose -- Recount recipes and skills. The current counts are 55 and five. +- Recount recipes and skills in the [generated baseline metrics](#step-3-re-derive-corpus-metrics). - Check `sdp q` bindings. The current sink injects `g`, `graph`, `report`, `params` and `recipes`. - Re-read the Studio's carrying Specs and publish a current page set. Check lenses, roadmap, registers, queue, diff, runs, export and composing panel rather than saying "phase 0" from memory. - Check context-bundle parameters and behavior. `part` now returns ordered parts; never restore the old blanket statement that partial member lists are refused. -- Re-run the worked example. At this pin it reports 11 Specs, two Packs, five anchors, 18 nodes and 34 edges. +- Re-run the worked example and refresh the [generated build output in Part 3](./03-walkthrough-intent-to-verified-code.md#the-worked-example). - Keep run evidence, `has-verifier` and `observed` distinct. Supplied CTRF results do not derive runtime observation. - Review command coverage. Any new reusable output belongs in a generated evidence block. - Check every sibling link after renumbering, every commit-pinned path, every code fence and every image. diff --git a/docs/articles/sync-evidence.d.mts b/docs/articles/sync-evidence.d.mts new file mode 100644 index 000000000..5b849f755 --- /dev/null +++ b/docs/articles/sync-evidence.d.mts @@ -0,0 +1,7 @@ +export function replaceEvidenceBlock( + original: string, + block: { readonly id: string; readonly file: string; readonly language: string | null }, + generated: string, +): string; + +export function summarizeValidation(output: string): string; diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index 54bf3026e..d9df8666e 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -3,100 +3,151 @@ import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } fr import path from "node:path"; import { fileURLToPath } from "node:url"; -const articlesDir = path.dirname(fileURLToPath(import.meta.url)); -const repositoryRoot = path.resolve(articlesDir, "../.."); -const playbook = readFileSync(path.join(articlesDir, "README.md"), "utf8"); -const pinnedCommit = playbook.match(/Pinned source baseline:.*tree\/([0-9a-f]{40})/)?.[1]; - -if (!pinnedCommit) { - throw new Error("The publication plan does not name a full pinned source commit."); +export function replaceEvidenceBlock(original, block, generated) { + const opening = ``; + const closing = ``; + const pattern = new RegExp( + `${opening.replaceAll("-", "\\-")}\\n[\\s\\S]*?\\n${closing.replaceAll("-", "\\-")}`, + ); + if (!pattern.test(original)) { + throw new Error(`${block.file} is missing the ${block.id} evidence markers.`); + } + const body = generated.trim(); + const replacement = block.language + ? `${opening}\n\`\`\`${block.language}\n${body}\n\`\`\`\n${closing}` + : `${opening}\n${body}\n${closing}`; + return original.replace(pattern, () => replacement); } -if (process.argv.includes("--print-pin")) { - console.log(pinnedCommit); - process.exit(0); +export function summarizeValidation(output) { + const summary = output.match(/validate: (\d+) errors · (\d+) warnings/); + if (!summary) { + throw new Error("Could not read the self-hosting validation summary."); + } + // AGENTS.md documents exactly these intentional self-hosting warning rules. + const counts = new Map([ + ["honesty/gaps", 0], + ["conformance/prose-mentions", 0], + ]); + const warnings = [...output.matchAll(/\[warning\] (\S+) —/g)]; + for (const [, rule] of warnings) { + if (!counts.has(rule)) { + throw new Error(`Unexpected self-hosting warning rule: ${rule}`); + } + counts.set(rule, counts.get(rule) + 1); + } + if (warnings.length !== Number(summary[2])) { + throw new Error("Warning breakdown does not match the self-hosting validation summary."); + } + const breakdown = [...counts].map(([rule, count]) => `${count} \`${rule}\``).join(", "); + return `${summary[1]} errors and ${summary[2]} intentional warnings (${breakdown})`; } -const check = process.argv.includes("--check"); -const sourceArgument = process.argv.slice(2).find((argument) => argument !== "--check"); -const sourceRoot = path.resolve(sourceArgument ?? repositoryRoot); -const realSourceRoot = realpathSync(sourceRoot); -const sourceCommit = execFileSync("git", ["-C", sourceRoot, "rev-parse", "HEAD"], { - encoding: "utf8", -}).trim(); +function main() { + const articlesDir = path.dirname(fileURLToPath(import.meta.url)); + const repositoryRoot = path.resolve(articlesDir, "../.."); + const playbook = readFileSync(path.join(articlesDir, "README.md"), "utf8"); + const pinnedCommit = playbook.match(/Pinned source baseline:.*tree\/([0-9a-f]{40})/)?.[1]; -if (sourceCommit !== pinnedCommit) { - throw new Error( - `Article baseline ${pinnedCommit} does not match source checkout ${sourceCommit}. ` + - "Pass a checkout of the pinned commit as the source path.", - ); -} + if (!pinnedCommit) { + throw new Error("The publication plan does not name a full pinned source commit."); + } -const sourceStatus = execFileSync( - "git", - ["-C", sourceRoot, "status", "--porcelain", "--", ".", ":(exclude)node_modules"], - { encoding: "utf8" }, -).trim(); -if (sourceStatus) { - throw new Error( - "The pinned source checkout must be clean before generating article evidence. " + - `Found changed or untracked paths:\n${sourceStatus}`, - ); -} + if (process.argv.includes("--print-pin")) { + console.log(pinnedCommit); + process.exit(0); + } -if (!existsSync(path.join(sourceRoot, "dist/cli/sdp.js"))) { - throw new Error(`Build the source checkout first: npm --prefix ${sourceRoot} run build`); -} + const check = process.argv.includes("--check"); + const sourceArgument = process.argv.slice(2).find((argument) => argument !== "--check"); + const sourceRoot = path.resolve(sourceArgument ?? repositoryRoot); + const realSourceRoot = realpathSync(sourceRoot); + const sourceCommit = execFileSync("git", ["-C", sourceRoot, "rev-parse", "HEAD"], { + encoding: "utf8", + }).trim(); -const buildInfoPath = path.join(sourceRoot, "dist/cli/build-info.json"); -if (!existsSync(buildInfoPath)) { - throw new Error(`Build metadata is missing at ${buildInfoPath}; rebuild the pinned source checkout.`); -} -const buildCommit = JSON.parse(readFileSync(buildInfoPath, "utf8")).commit; -if (buildCommit !== pinnedCommit) { - throw new Error( - `Article baseline ${pinnedCommit} does not match the CLI build ${buildCommit ?? "unknown"}. ` + - "Rebuild the pinned source checkout before generating evidence.", - ); -} + if (sourceCommit !== pinnedCommit) { + throw new Error( + `Article baseline ${pinnedCommit} does not match source checkout ${sourceCommit}. ` + + "Pass a checkout of the pinned commit as the source path.", + ); + } -const exclusions = [ - "--exclude", - "explorations", - "--exclude", - "examples", - "--exclude", - "test/fixtures/import/parity", -]; + const sourceStatus = execFileSync( + "git", + ["-C", sourceRoot, "status", "--porcelain", "--", ".", ":(exclude)node_modules"], + { encoding: "utf8" }, + ).trim(); + if (sourceStatus) { + throw new Error( + "The pinned source checkout must be clean before generating article evidence. " + + `Found changed or untracked paths:\n${sourceStatus}`, + ); + } -function runCli(args) { - const result = spawnSync(process.execPath, [path.join(sourceRoot, "dist/cli/sdp.js"), ...args], { - cwd: sourceRoot, - encoding: "utf8", - maxBuffer: 20 * 1024 * 1024, - }); - const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; - if (result.status !== 0) { - throw new Error(`sdp ${args.join(" ")} failed:\n${output}`); + if (!existsSync(path.join(sourceRoot, "dist/cli/sdp.js"))) { + throw new Error(`Build the source checkout first: npm --prefix ${sourceRoot} run build`); } - return output.replaceAll(realSourceRoot, "").replaceAll(sourceRoot, "").trim(); -} -function query(body, root = sourceRoot, selfHosting = true) { - const args = ["q", body, "--root", root, "--json", ...(selfHosting ? exclusions : [])]; - return JSON.parse(runCli(args)); -} + const buildInfoPath = path.join(sourceRoot, "dist/cli/build-info.json"); + if (!existsSync(buildInfoPath)) { + throw new Error( + `Build metadata is missing at ${buildInfoPath}; rebuild the pinned source checkout.`, + ); + } + const buildCommit = JSON.parse(readFileSync(buildInfoPath, "utf8")).commit; + if (buildCommit !== pinnedCommit) { + throw new Error( + `Article baseline ${pinnedCommit} does not match the CLI build ${buildCommit ?? "unknown"}. ` + + "Rebuild the pinned source checkout before generating evidence.", + ); + } -function recipe(number, parameters = {}, root = sourceRoot, selfHosting = true) { - return query(`return await recipes(${number}, ${JSON.stringify(parameters)})`, root, selfHosting); -} + const exclusions = [ + "--exclude", + "explorations", + "--exclude", + "examples", + "--exclude", + "test/fixtures/import/parity", + ]; -function sourceText(relativePath) { - return readFileSync(path.join(sourceRoot, relativePath), "utf8"); -} + function runCli(args) { + const result = spawnSync( + process.execPath, + [path.join(sourceRoot, "dist/cli/sdp.js"), ...args], + { + cwd: sourceRoot, + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, + }, + ); + const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; + if (result.status !== 0) { + throw new Error(`sdp ${args.join(" ")} failed:\n${output}`); + } + return output.replaceAll(realSourceRoot, "").replaceAll(sourceRoot, "").trim(); + } + + function query(body, root = sourceRoot, selfHosting = true) { + const args = ["q", body, "--root", root, "--json", ...(selfHosting ? exclusions : [])]; + return JSON.parse(runCli(args)); + } -function selfHostingCensus() { - const specs = query(`return { + function recipe(number, parameters = {}, root = sourceRoot, selfHosting = true) { + return query( + `return await recipes(${number}, ${JSON.stringify(parameters)})`, + root, + selfHosting, + ); + } + + function sourceText(relativePath) { + return readFileSync(path.join(sourceRoot, relativePath), "utf8"); + } + + function selfHostingCensus() { + const specs = query(`return { schemaVersion: graph.schemaVersion, specs: g.specs().length, packs: graph.nodes.filter((n) => n.nodeType === "Pack").map((p) => ({ id: p.id, members: p.members.length })), @@ -112,19 +163,21 @@ function selfHostingCensus() { observed: g.specs().filter((s) => s.deliveryFacts.includes("observed")).length } }`); - return { - ...specs, - backlog: recipe(1), - drift: recipe(2), - designBacklog: recipe(33), - questions: recipe(20).totals, - recipes: readdirSync(path.join(sourceRoot, "dist/recipes")).filter((file) => file.endsWith(".js")).length, - }; -} + return { + ...specs, + backlog: recipe(1), + drift: recipe(2), + designBacklog: recipe(33), + questions: recipe(20).totals, + recipes: readdirSync(path.join(sourceRoot, "dist/recipes")).filter((file) => + file.endsWith(".js"), + ).length, + }; + } -function articleCensus() { - const census = selfHostingCensus(); - return `| Measure | Value | + function articleCensus() { + const census = selfHostingCensus(); + return `| Measure | Value | |---|---| | Authored Specs | ${census.specs}: ${census.readiness.ready ?? 0} \`ready\`, ${census.readiness.scoped ?? 0} \`scoped\`, ${census.readiness.idea ?? 0} \`idea\` | | Packs | ${census.packs.length} | @@ -134,17 +187,21 @@ function articleCensus() { | Operational build backlog | ${census.backlog.total} | | Drift alarm | ${census.drift.total} | | Runnable recipes | ${census.recipes} |`; -} - -function baselineMetrics() { - const census = selfHostingCensus(); - const validation = runCli(["validate", ".", ...exclusions]); - const validationMatch = validation.match(/validate: (\d+) errors · (\d+) warnings/); - if (!validationMatch) { - throw new Error("Could not read the self-hosting validation summary."); } - const packNames = census.packs.map((pack) => `\`${pack.id.replace(/^pack:/, "")}\``).join(", "); - return `| Metric | Value | + + function baselineMetrics() { + const census = selfHostingCensus(); + const validation = runCli(["validate", ".", ...exclusions]); + const validationSummary = summarizeValidation(validation); + const skills = readdirSync(path.join(sourceRoot, ".agents/skills"), { + withFileTypes: true, + }).filter( + (entry) => + entry.isDirectory() && + existsSync(path.join(sourceRoot, ".agents/skills", entry.name, "SKILL.md")), + ).length; + const packNames = census.packs.map((pack) => `\`${pack.id.replace(/^pack:/, "")}\``).join(", "); + return `| Metric | Value | |---|---| | Schema | \`${census.schemaVersion}\` | | Specs and readiness | ${census.specs}: ${census.readiness.ready ?? 0} \`ready\`, ${census.readiness.scoped ?? 0} \`scoped\`, ${census.readiness.idea ?? 0} \`idea\` | @@ -155,110 +212,156 @@ function baselineMetrics() { | Drift alarm | ${census.drift.total} | | Design backlog | ${census.designBacklog.totals.specs} | | Open questions | ${census.questions.questions} across ${census.questions.specs} Specs: ${census.questions.blocking} blocking (${census.questions.deferrals} deferrals, ${census.questions.holds} holds), ${census.questions.nonBlocking} non-blocking | -| Validation | ${validationMatch[1]} errors and ${validationMatch[2]} intentional \`honesty/gaps\` warnings | -| Shipped recipes | ${census.recipes} |`; -} +| Validation | ${validationSummary} | +| Shipped recipes | ${census.recipes} | +| Repository-owned skills | ${skills} |`; + } -function exampleBuild() { - return runCli(["build", "examples/checkout-v1"]) - .split("\n") - .filter((line) => /^\d+ specs/.test(line) || line.startsWith("Wrote ")) - .join("\n"); -} + function exampleBuild() { + return runCli(["build", "examples/checkout-v1"]) + .split("\n") + .filter((line) => /^\d+ specs/.test(line) || line.startsWith("Wrote ")) + .join("\n"); + } -function exampleDrift() { - const answer = recipe(2, {}, path.join(sourceRoot, "examples/checkout-v1"), false); - return JSON.stringify( - { - total: answer.total, - alarms: answer.alarms.map((alarm) => ({ - id: alarm.id, - statedReadiness: alarm.statedReadiness, - floorReached: alarm.floorReached, - implementationBindings: alarm.implementationBindings, - })), - }, - null, - 2, - ); -} + function exampleDrift() { + const answer = recipe(2, {}, path.join(sourceRoot, "examples/checkout-v1"), false); + return JSON.stringify( + { + total: answer.total, + alarms: answer.alarms.map((alarm) => ({ + id: alarm.id, + statedReadiness: alarm.statedReadiness, + floorReached: alarm.floorReached, + implementationBindings: alarm.implementationBindings, + })), + }, + null, + 2, + ); + } -function constantAnchor() { - const source = sourceText("examples/checkout-v1/src/orders/create-order.use-case.ts"); - const importLine = source.split("\n")[0]; - const anchor = source.match(/export const createOrderUseCaseAnchor = codeAnchor\(\{[\s\S]*?\n\}\);/)?.[0]; - if (!anchor) throw new Error("Could not locate the create-order constant anchor."); - return `${importLine}\n\n// …\n\n${anchor}`; -} + function constantAnchor() { + const source = sourceText("examples/checkout-v1/src/orders/create-order.use-case.ts"); + const importLine = source.split("\n")[0]; + const anchor = source.match( + /export const createOrderUseCaseAnchor = codeAnchor\(\{[\s\S]*?\n\}\);/, + )?.[0]; + if (!anchor) throw new Error("Could not locate the create-order constant anchor."); + return `${importLine}\n\n// …\n\n${anchor}`; + } -function commentAnchor() { - const source = sourceText("src/projections/census.ts"); - const anchorIndex = source.indexOf("@sdpAnchor impl:protocol.census-page"); - const start = source.lastIndexOf("/**", anchorIndex); - const end = source.indexOf("*/", anchorIndex); - if (anchorIndex < 0 || start < 0 || end < 0) throw new Error("Could not locate the census comment anchor."); - return source.slice(start, end + 2); -} + function commentAnchor() { + const source = sourceText("src/projections/census.ts"); + const anchorIndex = source.indexOf("@sdpAnchor impl:protocol.census-page"); + const start = source.lastIndexOf("/**", anchorIndex); + const end = source.indexOf("*/", anchorIndex); + if (anchorIndex < 0 || start < 0 || end < 0) + throw new Error("Could not locate the census comment anchor."); + return source.slice(start, end + 2); + } -function oracle() { - const source = sourceText("examples/checkout-v1/test/orders/create-order.oracle.ts"); - const start = source.indexOf("export function expected("); - const end = source.indexOf("\n}", start); - if (start < 0 || end < 0) throw new Error("Could not locate the create-order oracle."); - return source.slice(start, end + 2); -} + function oracle() { + const source = sourceText("examples/checkout-v1/test/orders/create-order.oracle.ts"); + const start = source.indexOf("export function expected("); + const end = source.indexOf("\n}", start); + if (start < 0 || end < 0) throw new Error("Could not locate the create-order oracle."); + return source.slice(start, end + 2); + } -function byFile() { - return JSON.stringify(query(`return g.byFile("src/projections/census.ts")`), null, 2); -} + function byFile() { + return JSON.stringify(query(`return g.byFile("src/projections/census.ts")`), null, 2); + } -function roadmapMermaid() { - runCli(["mermaid", "examples/checkout-v1"]); - const page = sourceText("examples/checkout-v1/generated/mermaid/pack/checkout-roadmap.md"); - const match = page.match(/```mermaid\n([\s\S]*?)\n```/); - if (!match) throw new Error("Could not read the generated checkout-roadmap Mermaid block."); - return match[1]; -} + function roadmapMermaid() { + runCli(["mermaid", "examples/checkout-v1"]); + const page = sourceText("examples/checkout-v1/generated/mermaid/pack/checkout-roadmap.md"); + const match = page.match(/```mermaid\n([\s\S]*?)\n```/); + if (!match) throw new Error("Could not read the generated checkout-roadmap Mermaid block."); + return match[1]; + } -const blocks = [ - { id: "self-hosting-census", file: "01-the-spec-is-the-prompt-is-the-design.md", language: null, generate: articleCensus }, - { id: "roadmap-mermaid", file: "02-one-graph-many-views.md", language: "mermaid", generate: roadmapMermaid }, - { id: "example-build", file: "03-walkthrough-intent-to-verified-code.md", language: "text", generate: exampleBuild }, - { id: "example-drift", file: "03-walkthrough-intent-to-verified-code.md", language: "json", generate: exampleDrift }, - { id: "constant-anchor", file: "03-walkthrough-intent-to-verified-code.md", language: "ts", generate: constantAnchor }, - { id: "comment-anchor", file: "03-walkthrough-intent-to-verified-code.md", language: "ts", generate: commentAnchor }, - { id: "oracle", file: "03-walkthrough-intent-to-verified-code.md", language: "ts", generate: oracle }, - { id: "by-file", file: "04-stop-building-verb-walls-for-coding-agents.md", language: "json", generate: byFile }, - { id: "baseline-metrics", file: "README.md", language: null, generate: baselineMetrics }, -]; + const blocks = [ + { + id: "self-hosting-census", + file: "01-the-spec-is-the-prompt-is-the-design.md", + language: null, + generate: articleCensus, + }, + { + id: "roadmap-mermaid", + file: "02-one-graph-many-views.md", + language: "mermaid", + generate: roadmapMermaid, + }, + { + id: "example-build", + file: "03-walkthrough-intent-to-verified-code.md", + language: "text", + generate: exampleBuild, + }, + { + id: "example-drift", + file: "03-walkthrough-intent-to-verified-code.md", + language: "json", + generate: exampleDrift, + }, + { + id: "constant-anchor", + file: "03-walkthrough-intent-to-verified-code.md", + language: "ts", + generate: constantAnchor, + }, + { + id: "comment-anchor", + file: "03-walkthrough-intent-to-verified-code.md", + language: "ts", + generate: commentAnchor, + }, + { + id: "oracle", + file: "03-walkthrough-intent-to-verified-code.md", + language: "ts", + generate: oracle, + }, + { + id: "by-file", + file: "04-stop-building-verb-walls-for-coding-agents.md", + language: "json", + generate: byFile, + }, + { id: "baseline-metrics", file: "README.md", language: null, generate: baselineMetrics }, + ]; -const drift = []; -for (const block of blocks) { - const filePath = path.join(articlesDir, block.file); - const original = readFileSync(filePath, "utf8"); - const opening = ``; - const closing = ``; - const pattern = new RegExp(`${opening.replaceAll("-", "\\-")}\\n[\\s\\S]*?\\n${closing.replaceAll("-", "\\-")}`); - if (!pattern.test(original)) { - throw new Error(`${block.file} is missing the ${block.id} evidence markers.`); + const drift = []; + for (const block of blocks) { + const filePath = path.join(articlesDir, block.file); + const original = readFileSync(filePath, "utf8"); + const updated = replaceEvidenceBlock(original, block, block.generate()); + if (updated === original) continue; + if (check) { + drift.push(`${block.file}: ${block.id}`); + } else { + writeFileSync(filePath, updated); + console.log(`Updated ${block.file}: ${block.id}`); + } } - const body = block.generate().trim(); - const replacement = block.language - ? `${opening}\n\`\`\`${block.language}\n${body}\n\`\`\`\n${closing}` - : `${opening}\n${body}\n${closing}`; - const updated = original.replace(pattern, replacement); - if (updated === original) continue; - if (check) { - drift.push(`${block.file}: ${block.id}`); + + if (drift.length > 0) { + console.error(`Article evidence is stale:\n${drift.join("\n")}`); + process.exitCode = 1; } else { - writeFileSync(filePath, updated); - console.log(`Updated ${block.file}: ${block.id}`); + console.log( + `${check ? "Checked" : "Synchronized"} ${blocks.length} evidence blocks at ${pinnedCommit}.`, + ); } } -if (drift.length > 0) { - console.error(`Article evidence is stale:\n${drift.join("\n")}`); - process.exitCode = 1; -} else { - console.log(`${check ? "Checked" : "Synchronized"} ${blocks.length} evidence blocks at ${pinnedCommit}.`); +// Node resolves the main module through symlinks, so compare real paths: a symlinked checkout +// path must still run the check rather than exit 0 having checked nothing. +if ( + process.argv[1] && + realpathSync(path.resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url)) +) { + main(); } diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts new file mode 100644 index 000000000..3a2155ef6 --- /dev/null +++ b/test/article-evidence.test.ts @@ -0,0 +1,95 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, symlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +import { replaceEvidenceBlock, summarizeValidation } from "../docs/articles/sync-evidence.mjs"; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +describe("article evidence CLI entry", () => { + it("runs when invoked through a symlinked checkout path", () => { + const linkParent = mkdtempSync(path.join(tmpdir(), "sdp-article-link-")); + try { + const linkedRoot = path.join(linkParent, "checkout"); + symlinkSync(repositoryRoot, linkedRoot, "dir"); + const result = spawnSync( + process.execPath, + [path.join(linkedRoot, "docs/articles/sync-evidence.mjs"), "--print-pin"], + { encoding: "utf8" }, + ); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toMatch(/^[0-9a-f]{40}$/u); + } finally { + rmSync(linkParent, { recursive: true, force: true }); + } + }); +}); + +// Import the CLI's helpers directly: an import must not validate a checkout or regenerate files. +describe("article evidence replacement", () => { + it.each([null, "text"])("preserves replacement tokens literally with language %s", (language) => { + const opening = ""; + const closing = ""; + const original = `Before\n${opening}\nold evidence\n${closing}\nAfter\n`; + const body = "Literal $$, $&, $`, and $' must survive."; + const block = { id: "probe", file: "article.md", language }; + const expectedBody = language === null ? body : `\`\`\`text\n${body}\n\`\`\``; + const expected = `Before\n${opening}\n${expectedBody}\n${closing}\nAfter\n`; + + const updated = replaceEvidenceBlock(original, block, body); + expect(updated).toBe(expected); + expect(replaceEvidenceBlock(updated, block, body)).toBe(expected); + }); + + it("refuses a missing evidence block", () => { + expect(() => + replaceEvidenceBlock( + "No markers", + { id: "probe", file: "article.md", language: null }, + "body", + ), + ).toThrow("article.md is missing the probe evidence markers"); + }); +}); + +describe("article validation evidence", () => { + it("counts both documented warning rules, including located diagnostics", () => { + expect( + summarizeValidation( + "validate: 0 errors · 3 warnings\n" + + "[warning] honesty/gaps — missing verifier\n" + + "[warning] honesty/gaps — another missing verifier\n" + + "specs/probe.sdp.md:9 — [warning] conformance/prose-mentions — missing relation\n", + ), + ).toBe( + "0 errors and 3 intentional warnings (2 `honesty/gaps`, 1 `conformance/prose-mentions`)", + ); + }); + + it("reports zero counts for documented rules without warnings", () => { + expect(summarizeValidation("validate: 0 errors · 0 warnings")).toBe( + "0 errors and 0 intentional warnings (0 `honesty/gaps`, 0 `conformance/prose-mentions`)", + ); + }); + + it("refuses to label an undocumented warning rule intentional", () => { + expect(() => + summarizeValidation( + "validate: 0 errors · 1 warnings\n[warning] honesty/orphans — unlinked\n", + ), + ).toThrow("Unexpected self-hosting warning rule: honesty/orphans"); + }); + + it("refuses unparsed warnings and missing summaries", () => { + expect(() => summarizeValidation("validate: 0 errors · 1 warnings\nunknown format")).toThrow( + "Warning breakdown does not match", + ); + expect(() => summarizeValidation("No summary")).toThrow( + "Could not read the self-hosting validation summary", + ); + }); +}); From 2dc6a1a2c146faaef6ad2092eaa7fe2c2acf3968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 18:26:02 +0100 Subject: [PATCH 14/36] docs(articles): apply the whole-repository review's article corrections Apply the eight confirmed docs findings against the series from the review in draft PR #38, each verified by a second model family: - L10-4, L11-02: the shipped-projections decision (MD-32) freezes Design Review, census, Mermaid and Gherkin; only Studio pages are open to survive, merge or disappear. - L10-5: example-space coverage and file-impact coverage-unknown are different absences. - L10-7: Part 1 no longer says the Protocol links to issue trackers and design tools; it separates CTRF run evidence, the idea-stage reference capture and undesigned links. - L10-8: the preview's design diff compares against the pull request's base. - L10-11: Part 2 points prototypes at the Studio's manual acceptance tasks without claiming anyone completed them. - L9-F7: the source checkout's Node range comes from the test toolchain, apart from the CLI's Node >=20 floor. - L9-F6: the evidence sync refuses a self-hosting warning rule other than honesty/gaps at the pin, so a new prose-mentions pair cannot be labelled intentional. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- .../01-the-spec-is-the-prompt-is-the-design.md | 4 ++-- docs/articles/02-one-graph-many-views.md | 13 ++++++++----- .../03-walkthrough-intent-to-verified-code.md | 2 +- docs/articles/05-ai-can-shape-the-reading.md | 2 +- docs/articles/README.md | 4 +++- docs/articles/sync-evidence.mjs | 10 +++++++++- 7 files changed, 26 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 685043e7d..afb307f04 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ the Protocol, its human review experience and its AI direction. They are develop [PR #34](https://github.com/libar-dev/software-delivery-protocol/pull/34). Start with [the spec is the prompt is the design](docs/articles/01-the-spec-is-the-prompt-is-the-design.md), -then read [one graph, many views](docs/articles/02-one-graph-many-views.md) to see why HTML, -Markdown, Mermaid and other projections can change without becoming competing sources of truth. +then read [one graph, many views](docs/articles/02-one-graph-many-views.md) for the projection +family and the decision boundaries around changing it. The [hands-on walkthrough](docs/articles/03-walkthrough-intent-to-verified-code.md) follows intent into verified code. [The agent-surface article](docs/articles/04-stop-building-verb-walls-for-coding-agents.md) explains `sdp q`, and [the AI direction](docs/articles/05-ai-can-shape-the-reading.md) separates diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md index 1da95c4bf..291337458 100644 --- a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -149,13 +149,13 @@ It is not an embedded permissions system. Approval belongs in git and CI. It does not infer intent. Mechanical structure can assist curation, but a person authors intent and bindings. -It does not replace every adjacent tool. It links to issue trackers, design tools, API formats and evidence systems. +The aim is to connect adjacent tools rather than replace them. Today the Studio reads supplied CTRF test reports as run evidence. An interface and API reference remains an [idea-stage capture](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/reference-projection.sdp.md). Dedicated integrations with issue trackers and design tools are not designed yet; generic Design and UI prose can mention them without providing that experience. At the pinned commit, installation is from a reviewed git commit rather than an npm release. The package version is `0.0.0`; `sdp --version` names the build commit. ## Try it -With Node 20 or later: +For this source checkout, use Node `^20.19.0 || >=22.12.0`, the installed test toolchain's declared requirement. The installed CLI separately declares Node `>=20`: ```sh git clone https://github.com/libar-dev/software-delivery-protocol.git diff --git a/docs/articles/02-one-graph-many-views.md b/docs/articles/02-one-graph-many-views.md index 09d8808bc..b13d3540c 100644 --- a/docs/articles/02-one-graph-many-views.md +++ b/docs/articles/02-one-graph-many-views.md @@ -2,11 +2,11 @@ *Part 2 in a series introducing the Libar Software Delivery Protocol. Links are pinned to one commit; see the footer.* -A product manager opens a pull-request preview on a phone. The design under review has no implementation yet. They read the Pack's framing, inspect a blocking question, and open the Spec that carries it. A dependency points to an existing component, so they follow it to see what the new design would touch. A base graph shows what changed since the last review. They want a different example and a clearer constraint, so they gather both requests as scoped intent and hand the resulting proposal to an agent. +A product manager opens a pull-request preview on a phone. The design under review has no implementation yet. They read the Pack's framing, inspect a blocking question, and open the Spec that carries it. A dependency points to an existing component, so they follow it to see what the new design would touch. A base graph shows what changed in the design against the pull request's base commit. They want a different example and a clearer constraint, so they gather both requests as scoped intent and hand the resulting proposal to an agent. The agent edits the carriers. Git records the change. The conformance and honesty checks run. A new graph produces a new page set. -Nothing in that workflow requires the interface to be a database, an editor, or the one canonical way of reading a design. The interface is a projection. We can replace it without replacing the truth it presents. +Nothing in that workflow requires the interface to be a database, an editor, or the one canonical way of reading a design. The interface is a projection. We can replace the Studio interface under its carrying Specs without replacing the truth it presents. That freedom is the subject of this article. @@ -73,7 +73,7 @@ A flexible interface still has obligations to the model. The important ones are **Readiness and realization are independent.** An author states readiness. The graph derives the highest structural floor reached and the delivery facts. A useful review page lets a person see all three without turning them into a single progress score. -**Absence needs words.** `coverage-unknown`, a missing verifier and an evidence lens with no tracked observations say different things. An empty panel with no explanation forces the reader to guess which absence the system means. +**Absence needs words.** An example-space value no example binds, a slot whose coverage is not measurable, a missing verifier and an evidence lens with no tracked observations say different things. `coverage-unknown` names a separate file-impact blind spot: a changed file the graph records nothing at. An empty panel with no explanation forces the reader to guess which absence the system means. **A drawing needs a text equivalent.** Studio diagrams retain words that state every box and edge. When a drawing exceeds its bound, it folds and the text remains. This is an accessibility rule and an honesty rule: visual compression must not remove meaning. @@ -97,6 +97,7 @@ The fixed ground: - The repository is canonical and the graph is the sole read model. - A projection is disposable and regenerable. No interface state becomes delivery truth. +- The [shipped-projections decision](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/decisions/shipped-projections-frozen.sdp.md) (MD-32) freezes Design Review, census, Mermaid and Gherkin. Reshaping one needs a refining or superseding decision. The Studio can evolve under its own Specs. - Claims, stated readiness, derived readiness and delivery facts stay distinct. - Outside inputs and their commits stay named wherever they appear. - Unknowns and missing evidence remain visible. @@ -107,18 +108,20 @@ The fixed ground: The open design space: - Navigation, page structure, visual hierarchy, typography and interaction patterns. -- Which projections deserve to survive, merge or disappear. +- Which Studio pages deserve to survive, merge or disappear. - How a person moves between a Pack overview, one Spec, an entry and its source. - How desktop and phone review differ. - How much information belongs in the first reading and what belongs behind disclosure. - How a future AI assistant can steer a reading without obscuring its basis. +Start from the Studio's [manual acceptance tasks](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/consumers/spec-studio.sdp.md#verification--manual) and test prototypes against them. These are specified acceptance criteria, not evidence that a person has completed them. + The first design pass should answer these questions with prototypes: 1. What can a reviewer accomplish from a stamped link without opening the repository? 2. What is the fastest route from "what changed?" to "is this design ready?" for one Pack? 3. How should a reader distinguish intent, code bindings, inferred structure and supplied run evidence without learning the implementation schema first? -4. How do missing verification, missing observations and unknown coverage look different? +4. How do missing verification, missing observations, unmeasurable slot coverage and example-space values with no example look different? 5. When is a diagram better than prose, and how does its text equivalent read on a phone? 6. How does a person gather several related requests and understand exactly what will be handed to an agent? 7. Which current pages solve real review jobs, and which only expose data because it is available? diff --git a/docs/articles/03-walkthrough-intent-to-verified-code.md b/docs/articles/03-walkthrough-intent-to-verified-code.md index 3c8ed59cf..c8d41c833 100644 --- a/docs/articles/03-walkthrough-intent-to-verified-code.md +++ b/docs/articles/03-walkthrough-intent-to-verified-code.md @@ -8,7 +8,7 @@ This article follows the engineering loop in a text editor and a terminal. We wi ## Setup -Every command runs from a checkout at the pinned commit: +Every command runs from a checkout at the pinned commit. Use Node `^20.19.0 || >=22.12.0` for the installed test toolchain: ```sh git clone https://github.com/libar-dev/software-delivery-protocol.git diff --git a/docs/articles/05-ai-can-shape-the-reading.md b/docs/articles/05-ai-can-shape-the-reading.md index c942d5f0d..5a4812891 100644 --- a/docs/articles/05-ai-can-shape-the-reading.md +++ b/docs/articles/05-ai-can-shape-the-reading.md @@ -71,7 +71,7 @@ The next useful step is not a button that generates an entire dashboard. It is a The first UX/UI design pass can explore these questions without committing the product to AI-rendered pages. The deterministic Studio remains a baseline and a fallback. -The strategic value is flexibility. A strong graph and an explicit edit model let us replace weak projections, test specialized ones and add AI assistance where it proves useful. The interface can keep changing. The record does not change with it. +The strategic value is flexibility within explicit contracts. We can test additional readings and explore AI assistance while preserving the current deterministic surfaces. Reshaping Design Review, census, Mermaid or Gherkin requires a decision that refines or supersedes the [shipped-projections decision](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/decisions/shipped-projections-frozen.sdp.md) (MD-32); Studio changes follow its own carrying Specs. The interface can keep changing. The record does not change with it. ## Up next diff --git a/docs/articles/README.md b/docs/articles/README.md index 8d3cd1645..9716f1c62 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -15,7 +15,7 @@ The series has to carry four consequences, not just the engine behind them: 1. **One Spec, enriched in place.** Eight kinds, three altitudes and four stated readiness rungs cover delivery intent without migrating an idea through artifact types. 2. **Authored intent and derived facts stay separate.** `declared`, `anchored` and `inferred` claims are never collapsed. `implemented` and `has-verifier` are derived from bindings; `observed` remains deferred. -3. **Projections are cheap to replace.** Markdown, Mermaid, Gherkin, HTML, CLI answers and generated notes are readings, not competing stores. The current Studio is working material for UX learning, not a finished interface. +3. **Projections are regenerable readings.** Markdown, Mermaid, Gherkin, HTML, CLI answers and generated notes are readings, not competing stores. Reshaping Design Review, census, Mermaid or Gherkin requires a decision that refines or supersedes the [shipped-projections decision](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/decisions/shipped-projections-frozen.sdp.md) (MD-32). The Studio can evolve under its own Specs and remains working material for UX learning. 4. **AI has explicit trust boundaries.** Agents already read the graph and apply scoped intent. AI-assisted presentation is a future direction that must preserve those boundaries and address non-determinism honestly. The pre-code seed design in [`docs/lineage/v0-design/`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/docs/lineage/v0-design) is primary context for the product experience. It is historical evidence, not current law. Current Specs decide what is built and ratified. @@ -144,6 +144,8 @@ A refresh measures both numbers and qualitative claims. A design diff alone cann ### Step 1: Install and build +Use Node `^20.19.0 || >=22.12.0` for the installed test toolchain. This source-development requirement is separate from the CLI's declared Node `>=20` runtime floor. + ```sh npm ci ``` diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index d9df8666e..91c75aa1a 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -24,7 +24,7 @@ export function summarizeValidation(output) { if (!summary) { throw new Error("Could not read the self-hosting validation summary."); } - // AGENTS.md documents exactly these intentional self-hosting warning rules. + // Recognize both diagnostic rules; baselineMetrics checks which are expected at the pin. const counts = new Map([ ["honesty/gaps", 0], ["conformance/prose-mentions", 0], @@ -192,6 +192,14 @@ function main() { function baselineMetrics() { const census = selfHostingCensus(); const validation = runCli(["validate", ".", ...exclusions]); + const warningRules = query(`return [...new Set(report.findings + .filter((finding) => finding.severity === "warning") + .map((finding) => finding.validatorId))]`); + for (const rule of warningRules) { + if (rule !== "honesty/gaps") { + throw new Error(`Unexpected self-hosting warning rule: ${rule}`); + } + } const validationSummary = summarizeValidation(validation); const skills = readdirSync(path.join(sourceRoot, ".agents/skills"), { withFileTypes: true, From 4286c3881ccb552c91a58488004a32af3bd8d88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 22:56:53 +0100 Subject: [PATCH 15/36] docs(articles): scope open projection forms to the studio and new readings - L34-c-1: Part 2 said the human experience was unsettled right under the table of the five projection roots, and Part 3 called the projections replaceable. Part 2 now names the shipped-projections decision (MD-32), which settles Design Review, census, Mermaid and Gherkin, and leaves only the Studio and new readings open; Part 3 says regenerable. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/articles/02-one-graph-many-views.md | 2 +- docs/articles/03-walkthrough-intent-to-verified-code.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/02-one-graph-many-views.md b/docs/articles/02-one-graph-many-views.md index b13d3540c..cb7c541e3 100644 --- a/docs/articles/02-one-graph-many-views.md +++ b/docs/articles/02-one-graph-many-views.md @@ -36,7 +36,7 @@ At the pinned commit, the package publishes five projection roots: The graph also serves `sdp q`, CLI answers and typed contracts. An Obsidian vault provides generated notes and Bases beside a person's own notes. These are different readings, not competing repositories of delivery status. -This breadth is intentional. We have not settled the human experience by shipping several output forms. We have made it cheap to discover which forms earn their place. +This breadth is intentional, but not every form is open. The [shipped-projections decision](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/decisions/shipped-projections-frozen.sdp.md) (MD-32) settles Design Review, census, Mermaid and Gherkin as read models. The Studio and any new reading are where we learn which forms earn their place. The article's diagram evidence comes from the same mechanism. This Mermaid source is emitted by `sdp mermaid` from the checkout roadmap Pack, not redrawn for publication: diff --git a/docs/articles/03-walkthrough-intent-to-verified-code.md b/docs/articles/03-walkthrough-intent-to-verified-code.md index c8d41c833..002d07f7e 100644 --- a/docs/articles/03-walkthrough-intent-to-verified-code.md +++ b/docs/articles/03-walkthrough-intent-to-verified-code.md @@ -2,7 +2,7 @@ *Part 3 in a series introducing the Libar Software Delivery Protocol. Links and measurements are pinned to one commit; see the footer.* -[Part 1](./01-the-spec-is-the-prompt-is-the-design.md) introduced one enrichable `Spec`, source anchors and the derived graph. [Part 2](./02-one-graph-many-views.md) showed how people read that graph through replaceable projections. +[Part 1](./01-the-spec-is-the-prompt-is-the-design.md) introduced one enrichable `Spec`, source anchors and the derived graph. [Part 2](./02-one-graph-many-views.md) showed how people read that graph through regenerable projections. This article follows the engineering loop in a text editor and a terminal. We will use Order Management in [`examples/checkout-v1`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753/examples/checkout-v1), then inspect a comment-form binding in the Protocol's own engine. From 91aa3b46130f710ff428ec66cbb9b194e8cd9939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 22:57:11 +0100 Subject: [PATCH 16/36] docs(articles): cite the agent-trial re-run to the pull request that recorded it - L34-c-3: Part 4 said PR #32 recorded the gpt-6.1-sol re-run (13 of 13, 23 calls against 20, one overflow). PR #31's acceptance records it; PR #32 carried the response, the task-to-recipe sheet in sdp-agent-surface and the impacted Packs in recipe 4's summary. Part 4 now cites each to its own pull request. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/articles/04-stop-building-verb-walls-for-coding-agents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/04-stop-building-verb-walls-for-coding-agents.md b/docs/articles/04-stop-building-verb-walls-for-coding-agents.md index ae58f4c5f..cd83ffebb 100644 --- a/docs/articles/04-stop-building-verb-walls-for-coding-agents.md +++ b/docs/articles/04-stop-building-verb-walls-for-coding-agents.md @@ -90,7 +90,7 @@ Part 9 follows these mechanisms through the first adopter's design-at-scale work After [PR #30](https://github.com/libar-dev/software-delivery-protocol/pull/30), three agents from two model families answered thirteen delivery questions using the skills and `sdp q` alone. All three answered every question correctly. They also exposed recurring costs: oversized answers, unexplained misses, two meanings of "next rung" and rules without addresses or source lines. -[PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31) added summary-first answers, explained misses, both next rungs, rule keys and recorded lines. A re-run on `gpt-6.1-sol`, recorded in [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32), again answered 13 of 13 but took 23 calls against the first trial's 20 and overflowed one batched answer. The response was not another verb. It was a clearer task-to-recipe guide and better answer shapes. +[PR #31](https://github.com/libar-dev/software-delivery-protocol/pull/31) added summary-first answers, explained misses, both next rungs, rule keys and recorded lines. Its acceptance re-run on `gpt-6.1-sol` again answered 13 of 13 but took 23 calls against the first trial's 20 and overflowed one batched answer. The response, in [PR #32](https://github.com/libar-dev/software-delivery-protocol/pull/32), was not another verb. It was a task-to-recipe sheet in the `sdp-agent-surface` skill and better answer shapes, such as the impacted Packs in recipe 4's summary. That is the useful feedback loop. The graph remains stable while the on-ramps and recipes improve from observed use. From aae0c4691caf84b305ac08b6fab6d924eb257621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 22:57:20 +0100 Subject: [PATCH 17/36] docs(articles): describe the three claim examples as edges touching a spec - L34-c-4: Part 1 introduced three edges as "two edges can point at the same Spec", but belongsTo runs from a Spec to its Pack (706 Primitive->Pack edges at the pin). The lead-in now says edges that touch the same Spec can carry different claims, and the belongsTo bullet names its direction. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/articles/01-the-spec-is-the-prompt-is-the-design.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md index 291337458..a95a0ec74 100644 --- a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -46,11 +46,11 @@ That only works when authors avoid padding. The Protocol's readiness floor is a Nobody authors the graph. `sdp build` reads the carriers and source anchors and derives one flat, typed, deterministic document: nodes, edges and a location table. Validators, recipes and projections consume that one read model rather than parsing source into private alternatives ([`spec:extraction.derive-graph`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/extraction/derive-graph.sdp.md)). -Two edges can point at the same Spec and mean different things: +Edges that touch the same Spec can carry different claims: - `refines` from one Spec to another is authored intent with a `declared` claim. - `satisfies` from a code unit to a Spec comes from an anchor and has an `anchored` claim. -- `belongsTo` is computed from a Pack manifest and inherits the manifest's `declared` claim. +- `belongsTo` from a Spec to its Pack is computed from the Pack manifest and inherits the manifest's `declared` claim. The distinction is not bookkeeping. It lets a consumer answer "what did the authors intend?" separately from "what source did they bind?" and "what did static analysis infer?" From 207e155a7443fe63fbdb838cef0b391ee80d87d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 22:57:45 +0100 Subject: [PATCH 18/36] docs(articles): state the node range the whole test toolchain declares - L34-c-5: Parts 1 and 3 and the playbook gave Vite's and Rolldown's range `^20.19.0 || >=22.12.0` as the test toolchain's requirement, but the pinned Vitest 4.1.10 declares `^20.0.0 || ^22.0.0 || >=24.0.0` and excludes Node 23. All three sites now state the intersection `^20.19.0 || ^22.12.0 || >=24.0.0` and name the three packages. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/articles/01-the-spec-is-the-prompt-is-the-design.md | 2 +- docs/articles/03-walkthrough-intent-to-verified-code.md | 2 +- docs/articles/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md index a95a0ec74..1175bc515 100644 --- a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -155,7 +155,7 @@ At the pinned commit, installation is from a reviewed git commit rather than an ## Try it -For this source checkout, use Node `^20.19.0 || >=22.12.0`, the installed test toolchain's declared requirement. The installed CLI separately declares Node `>=20`: +For this source checkout, use Node `^20.19.0 || ^22.12.0 || >=24.0.0`, where the ranges declared by the installed test toolchain (Vitest, Vite and Rolldown) overlap. The installed CLI separately declares Node `>=20`: ```sh git clone https://github.com/libar-dev/software-delivery-protocol.git diff --git a/docs/articles/03-walkthrough-intent-to-verified-code.md b/docs/articles/03-walkthrough-intent-to-verified-code.md index 002d07f7e..b900f0045 100644 --- a/docs/articles/03-walkthrough-intent-to-verified-code.md +++ b/docs/articles/03-walkthrough-intent-to-verified-code.md @@ -8,7 +8,7 @@ This article follows the engineering loop in a text editor and a terminal. We wi ## Setup -Every command runs from a checkout at the pinned commit. Use Node `^20.19.0 || >=22.12.0` for the installed test toolchain: +Every command runs from a checkout at the pinned commit. Use Node `^20.19.0 || ^22.12.0 || >=24.0.0`, where the ranges declared by the installed test toolchain (Vitest, Vite and Rolldown) overlap: ```sh git clone https://github.com/libar-dev/software-delivery-protocol.git diff --git a/docs/articles/README.md b/docs/articles/README.md index 9716f1c62..17bc0e01e 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -144,7 +144,7 @@ A refresh measures both numbers and qualitative claims. A design diff alone cann ### Step 1: Install and build -Use Node `^20.19.0 || >=22.12.0` for the installed test toolchain. This source-development requirement is separate from the CLI's declared Node `>=20` runtime floor. +Use Node `^20.19.0 || ^22.12.0 || >=24.0.0`, where the ranges declared by the installed test toolchain (Vitest, Vite and Rolldown) overlap. This source-development requirement is separate from the CLI's declared Node `>=20` runtime floor. ```sh npm ci From 2283d5bb3ecf663b0cf2a60c5352f32a5e503189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 22:58:24 +0100 Subject: [PATCH 19/36] docs(articles): label article 08's unspecified rule and its pinned exception - L34-g-2: the Article 08 outline said an authored Then landing on `unspecified` still fails, as behavior at the pin. The pinned engine passes a bound example whose Then key is itself `unspecified` (PR #38's L1-02). The outline now states the rule as spec:extraction.runnable-modules' rule, names that exception at the source baseline, and asks the writer to re-check at the article's own pin. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/articles/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/README.md b/docs/articles/README.md index 17bc0e01e..4cbed52f7 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -104,7 +104,7 @@ For any corpus fact, query the graph before reading a carrier. The source list i - Lead with regex rot and with the deeper problem of tests verifying values that no longer match the authored example. - Explain carrier independence: Markdown and Gherkin can derive the same example space and contracts. - Focus on the authored/generated direction, point-per-example, bound points and the outcome union. Point back to Part 3 for registrar mechanics. -- Explain the oracle's job and `unspecified`, including why an authored Then that lands on `unspecified` still fails rather than silently witnessing a gap. +- Explain the oracle's job and `unspecified`, including why [`spec:extraction.runnable-modules`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/extraction/runnable-modules.sdp.md) makes a bound example whose oracle selects `unspecified` fail rather than silently witness a gap. The engine at the source baseline has one exception: it still passes such an example when its Then step's own contract key is `unspecified`, a gap the whole-repository review in [PR #38](https://github.com/libar-dev/software-delivery-protocol/pull/38) records as L1-02. Re-check the rule at the article's own pin before stating it without that exception. - Use the Protocol's own reader Spec in `.sdp.gherkin` as the self-hosting example. - Keep the boundary clear: `has-verifier` records a binding, supplied run evidence records results, and `observed` remains a separate deferred fact. From dc2fab0295d02f2f06270f013bfee2cae2e66eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 22:58:57 +0100 Subject: [PATCH 20/36] docs(lineage): keep an example's declared verifies apart from the test anchor's - GAP4-1: the annotation context listed `verifies` among anchor bindings as identifying "a test or example". Only a test anchor binds `verifies` (anchored); an example states its own `verifies` in its carrier (declared). The list now names the test anchor, and a separate paragraph says an example's declared relation confers `has-verifier` only when a resolving test anchor binds the example, as an enabled verifier. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/lineage/annotation-context.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/lineage/annotation-context.md b/docs/lineage/annotation-context.md index 9ed78eea6..c72502661 100644 --- a/docs/lineage/annotation-context.md +++ b/docs/lineage/annotation-context.md @@ -17,7 +17,7 @@ An anchor is a human-written pointer in source code or tests. It binds identity The graph needs those bindings for several kinds of projections: - `satisfies` identifies code that realizes a Spec and confers `implemented` when the edge resolves. -- `verifies` identifies a test or example that verifies a Spec and can confer `has-verifier`. +- `verifies`, on a test anchor, identifies a test that verifies a Spec. The edge is `anchored` and can confer `has-verifier`. - `models` binds an expected-outcome oracle to an example space. - `references` identifies code that answers to a design without claiming to realize it. It confers no delivery fact. - `memberOf` and `uses` record curated architectural structure between code units. @@ -25,6 +25,8 @@ The graph needs those bindings for several kinds of projections: These bindings let a projection answer from repository evidence rather than reconstructing architecture from filenames, imports or prose. A binding still does not prove behavior. `implemented` is not liveness, and `has-verifier` is not a passing test. +An example verifies differently. An example Spec states its own `verifies` relation in its carrier, with the `declared` claim, not through an anchor. That relation confers `has-verifier` on its target only when a resolving test anchor binds the example itself, which makes the example an enabled verifier. + ## The current representations The constant form uses a statically extracted top-level builder call. The comment form uses reserved `@sdp*` tags in a top-level block and needs no runtime import. Both feed the same closed anchor envelope and derive the same graph model. From af3aa1c0073a2dfae4d4dbbc5fefd99565951579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 22:59:05 +0100 Subject: [PATCH 21/36] docs(readme): point readers at the series' refresh model, not its opening pr - N-c-7: the root README said the articles "are developed in the open in PR #34", which names a merged pull request as the venue once #34 lands. It now says the series opened in PR #34 and is maintained on main through focused refresh PRs, as the playbook's posture and the PR's publication model state. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index afb307f04..05e983c85 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,9 @@ honesty. ## Read the product story The [article series and publication plan](docs/articles/README.md) are the guided introduction to -the Protocol, its human review experience and its AI direction. They are developed in the open in -[PR #34](https://github.com/libar-dev/software-delivery-protocol/pull/34). +the Protocol, its human review experience and its AI direction. The series opened in +[PR #34](https://github.com/libar-dev/software-delivery-protocol/pull/34) and is maintained on +`main` through focused refresh PRs. Start with [the spec is the prompt is the design](docs/articles/01-the-spec-is-the-prompt-is-the-design.md), then read [one graph, many views](docs/articles/02-one-graph-many-views.md) for the projection From efc5f7d93bde6cb27df82daa703f346241206cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 23:01:27 +0100 Subject: [PATCH 22/36] fix(articles): derive checked evidence from a disposable pinned build Y1 on r2/Y1, based on 2dc6a1a2. Keep the article source pinned to b5e0a8be, work only in this lane, prove each fix by regression and reversion checks, and do not push. This lane changes no Pack. - K34-c-1, K34-c-3, S-K34-1: materialize the pin from git, install its locked dependencies and build it; keep supplied checkout outputs intact. - K34-c-2: refuse missing, duplicate, unknown, misplaced, nested and fenced evidence markers across the articles before generating evidence. - GAP12-1, K34-c-5: parse options and forwarded -- strictly, require an explicit source repository and document the disposable worktree writes. - K34-c-4: use one honesty/gaps-only warning policy and test source, dependency, build and output isolation instead of trusting metadata. - K34-c-6: generate both measurement labels inside their evidence blocks. - S-L34-3, L34-c-6: generate both Part 3 carrier excerpts from the pin. - L34-c-7: serialize the full drift recipe answer, including its clause. - L34-c-2: describe build diagnostics separately from validation checks. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 8 +- ...01-the-spec-is-the-prompt-is-the-design.md | 4 +- .../03-walkthrough-intent-to-verified-code.md | 11 +- docs/articles/README.md | 18 +- docs/articles/sync-evidence.d.mts | 25 ++ docs/articles/sync-evidence.mjs | 321 +++++++++++------- test/article-evidence.test.ts | 298 +++++++++++++++- 7 files changed, 531 insertions(+), 154 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 230bc9618..c6dd99e24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,13 +33,7 @@ jobs: - name: Check article evidence shell: bash run: | - BASE_SHA="$(node docs/articles/sync-evidence.mjs --print-pin)" - BASE_DIR="${{ runner.temp }}/article-evidence" - git worktree add --detach "$BASE_DIR" "$BASE_SHA" - npm --prefix "$BASE_DIR" ci --ignore-scripts - npm --prefix "$BASE_DIR" run build - npm run check:articles -- "$BASE_DIR" - git worktree remove --force "$BASE_DIR" + npm run check:articles -- "$GITHUB_WORKSPACE" - name: Upload run evidence if: always() diff --git a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md index 291337458..8308dc882 100644 --- a/docs/articles/01-the-spec-is-the-prompt-is-the-design.md +++ b/docs/articles/01-the-spec-is-the-prompt-is-the-design.md @@ -122,9 +122,9 @@ The Spec Studio makes the human side concrete. A reviewer can explore a Pack, in The Protocol has no separate PRD. It authors its own rules as Specs, binds its engine and tests to them, derives its own graph and checks that graph in CI ([`spec:protocol.self-hosting`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/specs/protocol/self-hosting.sdp.md)). -At commit `b5e0a8be`, its self-hosting graph reports: - +Measured at commit `b5e0a8bea1e3bedd48c616f96c9928e955792753`: + | Measure | Value | |---|---| | Authored Specs | 456: 447 `ready`, 4 `scoped`, 5 `idea` | diff --git a/docs/articles/03-walkthrough-intent-to-verified-code.md b/docs/articles/03-walkthrough-intent-to-verified-code.md index c8d41c833..c909c54ae 100644 --- a/docs/articles/03-walkthrough-intent-to-verified-code.md +++ b/docs/articles/03-walkthrough-intent-to-verified-code.md @@ -80,12 +80,14 @@ npm run --silent sdp -- q "$(cat dist/recipes/02-the-drift-alarm.js)" \ "id": "spec:orders.create-order", "statedReadiness": "defined", "floorReached": "ready", + "firstUnmetClause": null, "implementationBindings": 2 }, { "id": "spec:orders.order-total-rule", "statedReadiness": "defined", "floorReached": "ready", + "firstUnmetClause": null, "implementationBindings": 1 } ] @@ -93,12 +95,13 @@ npm run --silent sdp -- q "$(cat dist/recipes/02-the-drift-alarm.js)" \ ``` -The Specs listed by the generated drift answer have implementation bindings and state only `defined`. Their structural floors reach `ready`, so they wait only for an author's readiness statement. The generated build output above carries the validation totals. The drift alarm is a query, not a workflow gate. +The Specs listed by the generated drift answer have implementation bindings and state only `defined`. Their structural floors reach `ready`, so they wait only for an author's readiness statement. The build output above counts extraction and contract-generation diagnostics; `sdp validate examples/checkout-v1` runs the conformance and honesty checks. The drift alarm is a query, not a workflow gate. ## Author the parent and its example space Open [`create-order.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/examples/checkout-v1/specs/orders/create-order.sdp.md): + ````markdown --- id: spec:orders.create-order @@ -130,6 +133,7 @@ And the order contains the original cart lines And order creation is rejected because {reason:"empty cart"|"out of stock"} ``` ```` + The YAML frontmatter is the closed envelope. It carries identity, the three descriptors and relations. A hand-authored `implemented` property or a derived `satisfies` relation is refused. @@ -141,6 +145,7 @@ The `gwt-vocabulary` fence declares the typed example space its child examples s The child example [`create-order-valid-cart.sdp.md`](https://github.com/libar-dev/software-delivery-protocol/blob/b5e0a8bea1e3bedd48c616f96c9928e955792753/examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md) refines and verifies its parent: + ````markdown --- id: spec:orders.create-order.valid-cart @@ -156,6 +161,7 @@ relations: ## Intent - outcome: Show that a valid cart can become an order. +- value: The authored example demonstrates the happy path for create-order. ```gwt Given a customer has a cart with {n: 2} line items @@ -169,8 +175,9 @@ And the order contains the original cart lines ## Verification — executable - The order result contains a stable id. -- The returned total matches the authored total. +- The returned total matches the authored total — the spec's value, never the test's. ```` + Under the point-per-example law, one example binds one concrete point. Under the concreteness law, leaving a used slot unbound, such as `{n}` instead of `{n: 2}`, caps the example below `defined` and withholds its step contract and registrar. diff --git a/docs/articles/README.md b/docs/articles/README.md index 9716f1c62..53c19c052 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -1,7 +1,7 @@ # Media plan and living series playbook > **Pinned source baseline:** commit [`b5e0a8be`](https://github.com/libar-dev/software-delivery-protocol/tree/b5e0a8bea1e3bedd48c616f96c9928e955792753), the merge of [PR #36](https://github.com/libar-dev/software-delivery-protocol/pull/36). -> **Posture:** This directory is maintained on `main` through focused refresh PRs as the Protocol advances. The opening arc used a standing PR for review, but no permanent media diff is needed. Current behavior, seed-design intent and future directions must read as three different things. Re-run Protocol-corpus measurements at a new pin rather than inheriting them; adopter and historical counts keep their cited measurement sources. Evidence verification always uses a separate checkout of the exact source pin, so manuscript commits cannot change the answers being verified. CI creates that checkout with full git history. +> **Posture:** This directory is maintained on `main` through focused refresh PRs as the Protocol advances. The opening arc used a standing PR for review, but no permanent media diff is needed. Current behavior, seed-design intent and future directions must read as three different things. Re-run Protocol-corpus measurements at a new pin rather than inheriting them; adopter and historical counts keep their cited measurement sources. Evidence verification always uses a separate checkout of the exact source pin, so manuscript commits cannot change the answers being verified. The sync creates and removes that disposable checkout in both local and CI runs; its writes are described in Step 4. ## 1. The series argument @@ -206,9 +206,9 @@ npm run --silent sdp:q -- "$(cat dist/recipes/33-the-design-backlog.js)" --json ls dist/recipes/*.js | wc -l ``` -Baseline measured at `b5e0a8be`: - +Measured at commit `b5e0a8bea1e3bedd48c616f96c9928e955792753`: + | Metric | Value | |---|---| | Schema | `0.12.0` | @@ -220,24 +220,26 @@ Baseline measured at `b5e0a8be`: | Drift alarm | 0 | | Design backlog | 0 | | Open questions | 16 across 12 Specs: 10 blocking (6 deferrals, 4 holds), 6 non-blocking | -| Validation | 0 errors and 3 intentional warnings (3 `honesty/gaps`, 0 `conformance/prose-mentions`) | +| Validation | 0 errors and 3 intentional warnings (3 `honesty/gaps`) | | Shipped recipes | 55 | | Repository-owned skills | 5 | ### Step 4: Regenerate the article evidence -Marked evidence blocks are generated, not maintained by hand. The sync reads the pinned commit from this playbook, verifies that the source checkout matches it and is clean, and checks the CLI build metadata against the same commit before running anything. It refuses changed or untracked source paths and stale builds before any generation. It covers graph-derived tables, build and query output, source anchors, the oracle and Mermaid. +Marked evidence blocks are generated, not maintained by hand. The sync reads this playbook's pin and creates a disposable worktree from that commit, installs dependencies with `npm ci --ignore-scripts`, and builds its CLI before collecting evidence. The supplied repository provides git objects only: its working files, installed dependencies and existing build are not used. It covers graph-derived tables, build and query output, carrier excerpts, source anchors, the oracle and Mermaid. Self-hosting validation accepts only `honesty/gaps` warnings; every other warning rule fails the sync. ```sh -# SOURCE_DIR is a built checkout of the commit named as the source baseline. +# SOURCE_DIR is a repository checkout containing the pinned commit. node docs/articles/sync-evidence.mjs "$SOURCE_DIR" # Check mode leaves the manuscripts unchanged and fails when any block differs. node docs/articles/sync-evidence.mjs --check "$SOURCE_DIR" ``` -When the article branch itself is at the baseline commit, omit `$SOURCE_DIR`. Add a marked block and its generator whenever a manuscript gains a reusable output excerpt. Keep elision explicit, as in the constant-anchor excerpt. Do not paste a modified version of source or command output and call it an example. +Both commands create a temporary worktree under the article checkout's `.scratch/`, install dependencies, build `dist/`, and regenerate `generated/`, test registrars and the example's generated outputs there. They remove that worktree on success or failure; they do not overwrite outputs in `$SOURCE_DIR`. Installing dependencies may populate npm's cache and needs registry access or a populated cache. Sync mode also writes the marked manuscript blocks. Check mode writes no manuscript. + +Always pass the source repository explicitly. Add a marked block and its generator whenever a manuscript gains a reusable output excerpt. Missing, duplicate, unknown, nested or fenced markers fail the check. Keep elision explicit, as in the constant-anchor excerpt. Do not paste a modified version of source or command output and call it an example. Mermaid stays live source. Markdown tables come from graph queries. Screenshots remain optional supplements for discussing visual design, never the only record of behavior. @@ -255,4 +257,4 @@ Mermaid stays live source. Markdown tables come from graph queries. Screenshots ### Step 6: Verify -Run `npm run check:articles -- "$SOURCE_DIR"` and `npm run check` after staging new article files so preflight can see them. CI runs the same article check in a built worktree of the pinned source with full git history, so a manuscript branch behind the pin cannot merge stale evidence green. Then manually inspect the rendered Markdown, relative links and commit-pinned links. The main gate does not check those links or all Markdown fence structure. +Run `npm run check:articles -- "$SOURCE_DIR"` and `npm run check` after staging new article files so preflight can see them. CI runs the same article check with full git history, using its disposable build of the pinned source, so a manuscript branch behind the pin cannot merge stale evidence green. Then manually inspect the rendered Markdown, relative links and commit-pinned links. The main gate does not check those links or all Markdown fence structure. diff --git a/docs/articles/sync-evidence.d.mts b/docs/articles/sync-evidence.d.mts index 5b849f755..1dc6bfcd6 100644 --- a/docs/articles/sync-evidence.d.mts +++ b/docs/articles/sync-evidence.d.mts @@ -5,3 +5,28 @@ export function replaceEvidenceBlock( ): string; export function summarizeValidation(output: string): string; + +export function evidenceMarkers( + original: string, + file: string, +): Map; +export function validateEvidenceMarkers( + documents: ReadonlyMap, + blocks: readonly { id: string; file: string }[], +): void; +export function parseArguments(args: string[]): { + check: boolean; + printPin: boolean; + source: string | undefined; +}; +export function withPinnedSource( + repository: string, + commit: string, + scratchRoot: string, + action: (source: string) => T, +): T; +export function measurementLabel(commit: string, body: string): string; +export function evidenceBlocks( + sourceRoot: string, + pinnedCommit: string, +): { id: string; file: string; language: string | null; generate: () => string }[]; diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index 91c75aa1a..d34df4e66 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -1,22 +1,136 @@ import { execFileSync, spawnSync } from "node:child_process"; -import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + realpathSync, + rmSync, + writeFileSync, +} from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -export function replaceEvidenceBlock(original, block, generated) { - const opening = ``; - const closing = ``; - const pattern = new RegExp( - `${opening.replaceAll("-", "\\-")}\\n[\\s\\S]*?\\n${closing.replaceAll("-", "\\-")}`, - ); - if (!pattern.test(original)) { - throw new Error(`${block.file} is missing the ${block.id} evidence markers.`); +// Markers are structural lines, never examples inside a Markdown fence. +export function evidenceMarkers(original, file) { + const blocks = new Map(); + let active; + let fence; + let offset = 0; + for (const line of original.split("\n")) { + const marker = /^$/.exec(line); + if (/\n${content}\n`; + return original.slice(0, range.start) + replacement + original.slice(range.end); +} + +export function parseArguments(args) { + let check = false; + let printPin = false; + let source; + let separator = false; + const usage = "Usage: sync-evidence.mjs [--check] [--] SOURCE_REPOSITORY | --print-pin"; + for (const argument of args) { + if (argument === "--" && !separator) separator = true; + else if (argument === "--check" && !separator && !check) check = true; + else if (argument === "--print-pin" && !separator && !printPin) printPin = true; + else if (argument.startsWith("-")) + throw new Error(`Unknown or repeated option: ${argument}\n${usage}`); + else if (source !== undefined) + throw new Error(`Only one source repository is allowed.\n${usage}`); + else source = argument; + } + if (printPin ? check || source !== undefined : !source) throw new Error(usage); + return { check, printPin, source }; +} + +// The supplied checkout is only an object source. Its files, ignored outputs and installed +// dependencies never participate in evidence. All writes happen in a disposable worktree. +export function withPinnedSource(repository, commit, scratchRoot, action) { + const root = realpathSync(repository); + const top = execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { + encoding: "utf8", + }).trim(); + if (realpathSync(top) !== root) + throw new Error("Pass the source repository root, not a subdirectory."); + mkdirSync(scratchRoot, { recursive: true }); + const temporary = mkdtempSync(path.join(scratchRoot, "article-evidence-")); + const source = path.join(temporary, "source"); + let added = false; + try { + execFileSync("git", ["-C", root, "worktree", "add", "--detach", source, commit], { + stdio: "pipe", + }); + added = true; + execFileSync("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], { + cwd: source, + stdio: "pipe", + }); + execFileSync("npm", ["run", "build"], { cwd: source, stdio: "pipe" }); + return action(source); + } finally { + try { + if (added) + execFileSync("git", ["-C", root, "worktree", "remove", "--force", source], { + stdio: "pipe", + }); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + } +} + +export function measurementLabel(commit, body) { + return `Measured at commit \`${commit}\`:\n\n${body}`; } export function summarizeValidation(output) { @@ -24,11 +138,7 @@ export function summarizeValidation(output) { if (!summary) { throw new Error("Could not read the self-hosting validation summary."); } - // Recognize both diagnostic rules; baselineMetrics checks which are expected at the pin. - const counts = new Map([ - ["honesty/gaps", 0], - ["conformance/prose-mentions", 0], - ]); + const counts = new Map([["honesty/gaps", 0]]); const warnings = [...output.matchAll(/\[warning\] (\S+) —/g)]; for (const [, rule] of warnings) { if (!counts.has(rule)) { @@ -43,66 +153,8 @@ export function summarizeValidation(output) { return `${summary[1]} errors and ${summary[2]} intentional warnings (${breakdown})`; } -function main() { - const articlesDir = path.dirname(fileURLToPath(import.meta.url)); - const repositoryRoot = path.resolve(articlesDir, "../.."); - const playbook = readFileSync(path.join(articlesDir, "README.md"), "utf8"); - const pinnedCommit = playbook.match(/Pinned source baseline:.*tree\/([0-9a-f]{40})/)?.[1]; - - if (!pinnedCommit) { - throw new Error("The publication plan does not name a full pinned source commit."); - } - - if (process.argv.includes("--print-pin")) { - console.log(pinnedCommit); - process.exit(0); - } - - const check = process.argv.includes("--check"); - const sourceArgument = process.argv.slice(2).find((argument) => argument !== "--check"); - const sourceRoot = path.resolve(sourceArgument ?? repositoryRoot); +export function evidenceBlocks(sourceRoot, pinnedCommit) { const realSourceRoot = realpathSync(sourceRoot); - const sourceCommit = execFileSync("git", ["-C", sourceRoot, "rev-parse", "HEAD"], { - encoding: "utf8", - }).trim(); - - if (sourceCommit !== pinnedCommit) { - throw new Error( - `Article baseline ${pinnedCommit} does not match source checkout ${sourceCommit}. ` + - "Pass a checkout of the pinned commit as the source path.", - ); - } - - const sourceStatus = execFileSync( - "git", - ["-C", sourceRoot, "status", "--porcelain", "--", ".", ":(exclude)node_modules"], - { encoding: "utf8" }, - ).trim(); - if (sourceStatus) { - throw new Error( - "The pinned source checkout must be clean before generating article evidence. " + - `Found changed or untracked paths:\n${sourceStatus}`, - ); - } - - if (!existsSync(path.join(sourceRoot, "dist/cli/sdp.js"))) { - throw new Error(`Build the source checkout first: npm --prefix ${sourceRoot} run build`); - } - - const buildInfoPath = path.join(sourceRoot, "dist/cli/build-info.json"); - if (!existsSync(buildInfoPath)) { - throw new Error( - `Build metadata is missing at ${buildInfoPath}; rebuild the pinned source checkout.`, - ); - } - const buildCommit = JSON.parse(readFileSync(buildInfoPath, "utf8")).commit; - if (buildCommit !== pinnedCommit) { - throw new Error( - `Article baseline ${pinnedCommit} does not match the CLI build ${buildCommit ?? "unknown"}. ` + - "Rebuild the pinned source checkout before generating evidence.", - ); - } - const exclusions = [ "--exclude", "explorations", @@ -192,14 +244,6 @@ function main() { function baselineMetrics() { const census = selfHostingCensus(); const validation = runCli(["validate", ".", ...exclusions]); - const warningRules = query(`return [...new Set(report.findings - .filter((finding) => finding.severity === "warning") - .map((finding) => finding.validatorId))]`); - for (const rule of warningRules) { - if (rule !== "honesty/gaps") { - throw new Error(`Unexpected self-hosting warning rule: ${rule}`); - } - } const validationSummary = summarizeValidation(validation); const skills = readdirSync(path.join(sourceRoot, ".agents/skills"), { withFileTypes: true, @@ -234,19 +278,7 @@ function main() { function exampleDrift() { const answer = recipe(2, {}, path.join(sourceRoot, "examples/checkout-v1"), false); - return JSON.stringify( - { - total: answer.total, - alarms: answer.alarms.map((alarm) => ({ - id: alarm.id, - statedReadiness: alarm.statedReadiness, - floorReached: alarm.floorReached, - implementationBindings: alarm.implementationBindings, - })), - }, - null, - 2, - ); + return JSON.stringify(answer, null, 2); } function constantAnchor() { @@ -294,7 +326,7 @@ function main() { id: "self-hosting-census", file: "01-the-spec-is-the-prompt-is-the-design.md", language: null, - generate: articleCensus, + generate: () => measurementLabel(pinnedCommit, articleCensus()), }, { id: "roadmap-mermaid", @@ -308,6 +340,19 @@ function main() { language: "text", generate: exampleBuild, }, + { + id: "create-order-carrier", + file: "03-walkthrough-intent-to-verified-code.md", + language: "markdown", + generate: () => sourceText("examples/checkout-v1/specs/orders/create-order.sdp.md"), + }, + { + id: "valid-cart-carrier", + file: "03-walkthrough-intent-to-verified-code.md", + language: "markdown", + generate: () => + sourceText("examples/checkout-v1/specs/orders/create-order-valid-cart.sdp.md"), + }, { id: "example-drift", file: "03-walkthrough-intent-to-verified-code.md", @@ -338,31 +383,69 @@ function main() { language: "json", generate: byFile, }, - { id: "baseline-metrics", file: "README.md", language: null, generate: baselineMetrics }, + { + id: "baseline-metrics", + file: "README.md", + language: null, + generate: () => measurementLabel(pinnedCommit, baselineMetrics()), + }, ]; - const drift = []; - for (const block of blocks) { - const filePath = path.join(articlesDir, block.file); - const original = readFileSync(filePath, "utf8"); - const updated = replaceEvidenceBlock(original, block, block.generate()); - if (updated === original) continue; - if (check) { - drift.push(`${block.file}: ${block.id}`); - } else { - writeFileSync(filePath, updated); - console.log(`Updated ${block.file}: ${block.id}`); - } + return blocks; +} + +function main() { + const { check, printPin, source: sourceArgument } = parseArguments(process.argv.slice(2)); + const articlesDir = path.dirname(fileURLToPath(import.meta.url)); + const repositoryRoot = path.resolve(articlesDir, "../.."); + const playbook = readFileSync(path.join(articlesDir, "README.md"), "utf8"); + const pinnedCommit = playbook.match(/Pinned source baseline:.*tree\/([0-9a-f]{40})/)?.[1]; + + if (!pinnedCommit) { + throw new Error("The publication plan does not name a full pinned source commit."); } - if (drift.length > 0) { - console.error(`Article evidence is stale:\n${drift.join("\n")}`); - process.exitCode = 1; - } else { - console.log( - `${check ? "Checked" : "Synchronized"} ${blocks.length} evidence blocks at ${pinnedCommit}.`, - ); + if (printPin) { + console.log(pinnedCommit); + return; } + + const documents = new Map( + readdirSync(articlesDir) + .filter((name) => name.endsWith(".md")) + .map((name) => [name, readFileSync(path.join(articlesDir, name), "utf8")]), + ); + validateEvidenceMarkers(documents, evidenceBlocks(repositoryRoot, pinnedCommit)); + return withPinnedSource( + sourceArgument, + pinnedCommit, + path.join(repositoryRoot, ".scratch"), + (sourceRoot) => { + const blocks = evidenceBlocks(sourceRoot, pinnedCommit); + const drift = []; + for (const block of blocks) { + const filePath = path.join(articlesDir, block.file); + const original = readFileSync(filePath, "utf8"); + const updated = replaceEvidenceBlock(original, block, block.generate()); + if (updated === original) continue; + if (check) { + drift.push(`${block.file}: ${block.id}`); + } else { + writeFileSync(filePath, updated); + console.log(`Updated ${block.file}: ${block.id}`); + } + } + + if (drift.length > 0) { + console.error(`Article evidence is stale:\n${drift.join("\n")}`); + process.exitCode = 1; + } else { + console.log( + `${check ? "Checked" : "Synchronized"} ${blocks.length} evidence blocks at ${pinnedCommit}.`, + ); + } + }, + ); } // Node resolves the main module through symlinks, so compare real paths: a symlinked checkout diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index 3a2155ef6..df6dfc5c6 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -1,12 +1,30 @@ -import { spawnSync } from "node:child_process"; -import { mkdtempSync, rmSync, symlinkSync } from "node:fs"; +import { execFileSync, spawnSync } from "node:child_process"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; -import { replaceEvidenceBlock, summarizeValidation } from "../docs/articles/sync-evidence.mjs"; +import { + evidenceBlocks, + evidenceMarkers, + measurementLabel, + parseArguments, + replaceEvidenceBlock, + summarizeValidation, + validateEvidenceMarkers, + withPinnedSource, +} from "../docs/articles/sync-evidence.mjs"; const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); @@ -57,32 +75,31 @@ describe("article evidence replacement", () => { }); describe("article validation evidence", () => { - it("counts both documented warning rules, including located diagnostics", () => { + it("counts only honesty gaps, including located diagnostics", () => { expect( summarizeValidation( "validate: 0 errors · 3 warnings\n" + "[warning] honesty/gaps — missing verifier\n" + "[warning] honesty/gaps — another missing verifier\n" + - "specs/probe.sdp.md:9 — [warning] conformance/prose-mentions — missing relation\n", + "specs/probe.sdp.md:9 — [warning] honesty/gaps — missing verifier\n", ), - ).toBe( - "0 errors and 3 intentional warnings (2 `honesty/gaps`, 1 `conformance/prose-mentions`)", - ); + ).toBe("0 errors and 3 intentional warnings (3 `honesty/gaps`)"); }); it("reports zero counts for documented rules without warnings", () => { expect(summarizeValidation("validate: 0 errors · 0 warnings")).toBe( - "0 errors and 0 intentional warnings (0 `honesty/gaps`, 0 `conformance/prose-mentions`)", + "0 errors and 0 intentional warnings (0 `honesty/gaps`)", ); }); - it("refuses to label an undocumented warning rule intentional", () => { - expect(() => - summarizeValidation( - "validate: 0 errors · 1 warnings\n[warning] honesty/orphans — unlinked\n", - ), - ).toThrow("Unexpected self-hosting warning rule: honesty/orphans"); - }); + it.each(["honesty/orphans", "conformance/prose-mentions"])( + "refuses to label %s intentional", + (rule) => { + expect(() => + summarizeValidation(`validate: 0 errors · 1 warnings\n[warning] ${rule} — unlinked\n`), + ).toThrow(`Unexpected self-hosting warning rule: ${rule}`); + }, + ); it("refuses unparsed warnings and missing summaries", () => { expect(() => summarizeValidation("validate: 0 errors · 1 warnings\nunknown format")).toThrow( @@ -93,3 +110,252 @@ describe("article validation evidence", () => { ); }); }); + +const probeBlock = { id: "probe", file: "article.md", language: null }; +const probeText = "\nbody\n"; + +describe("article evidence structure", () => { + it.each([ + ["duplicate", `${probeText}\n${probeText}`], + ["fenced", `\`\`\`\`markdown\n${probeText}\n\`\`\`\``], + ["tilde fenced", `~~~\n${probeText}\n~~~`], + ["shadowed", `\`\`\`\`\n${probeText}\n\`\`\`\`\n${probeText}`], + ["nested", "\n" + probeText + "\n"], + ["unclosed", "\nbody"], + ["unmatched", ""], + ["inline", `prefix ${probeText}`], + ])("refuses %s markers", (_name, text) => { + expect(() => replaceEvidenceBlock(text, probeBlock, "new body")).toThrow(); + }); + + it("refuses unknown, misplaced and missing blocks across every article", () => { + expect(() => { + validateEvidenceMarkers( + new Map([ + ["article.md", probeText], + ["extra.md", probeText.replaceAll("probe", "unknown")], + ]), + [probeBlock], + ); + }).toThrow("unknown evidence block"); + expect(() => { + validateEvidenceMarkers( + new Map([ + ["article.md", probeText], + ["extra.md", probeText], + ]), + [probeBlock], + ); + }).toThrow("duplicate evidence block"); + expect(() => { + validateEvidenceMarkers(new Map([["extra.md", probeText]]), [probeBlock]); + }).toThrow("misplaced evidence block"); + expect(() => { + validateEvidenceMarkers(new Map(), [probeBlock]); + }).toThrow("missing"); + }); + + it("preserves nested carrier fences and detects edits to both carrier excerpts", () => { + const blocks = evidenceBlocks(repositoryRoot, "pin"); + for (const [id, file] of [ + ["create-order-carrier", "create-order.sdp.md"], + ["valid-cart-carrier", "create-order-valid-cart.sdp.md"], + ] as const) { + const block = blocks.find((entry) => entry.id === id); + expect(block, id).toBeDefined(); + if (!block) throw new Error(id); + const source = readFileSync( + path.join(repositoryRoot, "examples/checkout-v1/specs/orders", file), + "utf8", + ).trim(); + expect(block.generate()).toBe(source + "\n"); + const original = readFileSync(path.join(repositoryRoot, "docs/articles", block.file), "utf8"); + expect(replaceEvidenceBlock(original, block, source)).toBe(original); + const range = evidenceMarkers(original, block.file).get(id); + if (!range) throw new Error(id); + const changed = + original.slice(0, range.start) + + original.slice(range.start, range.end).replace("readiness:", "altered:") + + original.slice(range.end); + expect(replaceEvidenceBlock(changed, block, block.generate())).not.toBe(changed); + expect(original.slice(range.start, range.end)).toContain("````markdown"); + } + }); + + it("renders the entire drift recipe answer including firstUnmetClause", () => { + const expected = execFileSync( + process.execPath, + [ + path.join(repositoryRoot, "dist/cli/sdp.js"), + "q", + "return recipes(2)", + "--root", + "examples/checkout-v1", + "--json", + ], + { cwd: repositoryRoot, encoding: "utf8" }, + ); + const block = evidenceBlocks(repositoryRoot, "pin").find( + (entry) => entry.id === "example-drift", + ); + if (!block) throw new Error("Missing drift generator"); + expect(JSON.parse(block.generate())).toEqual(JSON.parse(expected)); + expect(block.generate()).toContain('"firstUnmetClause"'); + }); + + it("moves measurement labels with the pin inside both generated blocks", () => { + expect(measurementLabel("new-pin", "table")).toBe("Measured at commit `new-pin`:\n\ntable"); + for (const id of ["self-hosting-census", "baseline-metrics"]) { + const block = evidenceBlocks(repositoryRoot, "new-pin").find((entry) => entry.id === id); + if (!block) throw new Error(id); + if (id === "self-hosting-census") { + expect(block.generate()).toMatch(/^Measured at commit `new-pin`:/); + } + const original = readFileSync(path.join(repositoryRoot, "docs/articles", block.file), "utf8"); + const range = evidenceMarkers(original, block.file).get(id); + if (!range) throw new Error(id); + expect(original.slice(range.start, range.end)).toMatch(/Measured at commit `[0-9a-f]{40}`/); + const updated = replaceEvidenceBlock(original, block, measurementLabel("new-pin", "table")); + expect(updated.slice(range.start)).toContain("Measured at commit `new-pin`"); + } + }, 60_000); + + it("describes build diagnostics without calling them validation totals", () => { + const article = readFileSync( + path.join(repositoryRoot, "docs/articles/03-walkthrough-intent-to-verified-code.md"), + "utf8", + ); + expect(article).not.toContain("build output above carries the validation totals"); + expect(article).toContain("extraction and contract-generation diagnostics"); + }); +}); + +describe("article evidence arguments", () => { + it("accepts the forwarded pnpm separator", () => { + expect(parseArguments(["--check", "--", "/source"])).toEqual({ + check: true, + printPin: false, + source: "/source", + }); + }); + + it.each([ + ["/source", "--chek"], + ["--verbose", "/source"], + ["one", "two"], + ["--print-pin", "--chek"], + [], + ["--check"], + ["--print-pin", "/source"], + ])("refuses invalid arguments %j before touching manuscripts", (...args) => { + const article = path.join(repositoryRoot, "docs/articles/README.md"); + const before = readFileSync(article, "utf8"); + const result = spawnSync( + process.execPath, + [path.join(repositoryRoot, "docs/articles/sync-evidence.mjs"), ...args], + { encoding: "utf8" }, + ); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("Usage:"); + expect(readFileSync(article, "utf8")).toBe(before); + }); + + it("documents an explicit source repository and disposable writes", () => { + const playbook = readFileSync(path.join(repositoryRoot, "docs/articles/README.md"), "utf8"); + expect(playbook).not.toContain("omit `$SOURCE_DIR`"); + expect(playbook).toContain("Always pass the source repository explicitly"); + expect(playbook).toContain("Both commands create a temporary worktree"); + }); +}); + +// Small real git repositories exercise materialization, npm's lockfile install and the build +// without reinstalling the Protocol's full dependency tree for every regression probe. +describe("pinned evidence source", () => { + it.each(["source", "build", "outputs"])("uses only the pin and isolates %s", (focus) => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-pin-")); + const repo = path.join(temporary, "repository"); + const scratch = path.join(temporary, "scratch"); + mkdirSync(repo); + const git = (...args: string[]) => + execFileSync("git", ["-C", repo, ...args], { encoding: "utf8" }).trim(); + const read = (root: string, file: string) => readFileSync(path.join(root, file), "utf8"); + try { + git("init"); + writeFileSync( + path.join(repo, "package.json"), + JSON.stringify({ + name: "pin-probe", + version: "1.0.0", + scripts: { build: "node build.cjs" }, + }), + ); + writeFileSync( + path.join(repo, "package-lock.json"), + JSON.stringify({ + name: "pin-probe", + version: "1.0.0", + lockfileVersion: 3, + packages: { "": { name: "pin-probe", version: "1.0.0" } }, + }), + ); + writeFileSync(path.join(repo, "source.txt"), "pinned source"); + writeFileSync( + path.join(repo, ".gitignore"), + "*.test.generated.ts\ndist/\nnode_modules/\ngenerated/\n", + ); + writeFileSync( + path.join(repo, "build.cjs"), + "const fs = require('node:fs'); fs.mkdirSync('dist'); fs.writeFileSync('dist/result', fs.readFileSync('source.txt'));\n", + ); + git("add", "package.json", "package-lock.json", "source.txt", ".gitignore", "build.cjs"); + git( + "-c", + "user.name=Test", + "-c", + "user.email=test@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "-m", + "pin", + ); + const commit = git("rev-parse", "HEAD"); + git("update-index", "--skip-worktree", "source.txt"); + writeFileSync(path.join(repo, "source.txt"), "hidden edit"); + writeFileSync(path.join(repo, "phantom.test.generated.ts"), "/** @sdpAnchor impl:phantom */"); + writeFileSync(path.join(repo, "untracked.txt"), "hidden untracked"); + git("config", "status.showUntrackedFiles", "no"); + for (const dir of ["dist", "node_modules", "generated"]) mkdirSync(path.join(repo, dir)); + writeFileSync(path.join(repo, "dist/result"), "stale build"); + writeFileSync(path.join(repo, "node_modules/foreign.txt"), "foreign dependency"); + writeFileSync(path.join(repo, "generated/sentinel"), "keep output"); + expect(git("status", "--porcelain")).toBe(""); + let materialized = ""; + withPinnedSource(repo, commit, scratch, (source) => { + materialized = source; + if (focus === "source") { + expect(read(source, "source.txt")).toBe("pinned source"); + expect(existsSync(path.join(source, "phantom.test.generated.ts"))).toBe(false); + expect(existsSync(path.join(source, "untracked.txt"))).toBe(false); + expect(existsSync(path.join(source, "node_modules/foreign.txt"))).toBe(false); + } + if (focus === "build") expect(read(source, "dist/result")).toBe("pinned source"); + mkdirSync(path.join(source, "generated"), { recursive: true }); + writeFileSync(path.join(source, "generated/sentinel"), "new output"); + }); + expect(read(repo, "generated/sentinel")).toBe("keep output"); + expect(existsSync(materialized)).toBe(false); + expect(read(repo, "dist/result")).toBe("stale build"); + expect(read(repo, "source.txt")).toBe("hidden edit"); + expect(() => + withPinnedSource(repo, commit, scratch, () => { + throw new Error("probe failure"); + }), + ).toThrow("probe failure"); + expect(readdirSync(scratch)).toEqual([]); + expect(git("worktree", "list", "--porcelain").match(/^worktree /gm)).toHaveLength(1); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }); +}); From 37e9a6f4f1081f6474467accfd014558b294eb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 23:32:19 +0100 Subject: [PATCH 23/36] fix(articles): materialize evidence from unmodified git blobs - K34-c-1: read raw pinned blobs with replacement objects disabled so sparse checkout and smudge filters cannot alter evidence. - K34-c-4: test the CLI isolation call against a repository without the pin; bypassing materialization now fails the test. - S-L34-3, L34-c-6: test carrier generation with independent fixtures so later carrier edits do not require refreshing pinned articles. - S-K34-1: document temporary worktree registration and keep generated output in the disposable source. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/articles/README.md | 4 +- docs/articles/sync-evidence.mjs | 72 +++++-- test/article-evidence.test.ts | 328 ++++++++++++++++++++------------ 3 files changed, 266 insertions(+), 138 deletions(-) diff --git a/docs/articles/README.md b/docs/articles/README.md index 53c19c052..857cba934 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -227,7 +227,7 @@ Measured at commit `b5e0a8bea1e3bedd48c616f96c9928e955792753`: ### Step 4: Regenerate the article evidence -Marked evidence blocks are generated, not maintained by hand. The sync reads this playbook's pin and creates a disposable worktree from that commit, installs dependencies with `npm ci --ignore-scripts`, and builds its CLI before collecting evidence. The supplied repository provides git objects only: its working files, installed dependencies and existing build are not used. It covers graph-derived tables, build and query output, carrier excerpts, source anchors, the oracle and Mermaid. Self-hosting validation accepts only `honesty/gaps` warnings; every other warning rule fails the sync. +Marked evidence blocks are generated, not maintained by hand. The sync reads this playbook's pin and fills a disposable worktree from that commit's raw Git blobs, with replacement objects disabled. It installs dependencies with `npm ci --ignore-scripts` and builds the CLI before collecting evidence. The supplied repository provides git objects only: its working files, sparse-checkout settings, attribute filters, installed dependencies and existing build are not used. It covers graph-derived tables, build and query output, carrier excerpts, source anchors, the oracle and Mermaid. Self-hosting validation accepts only `honesty/gaps` warnings; every other warning rule fails the sync. ```sh # SOURCE_DIR is a repository checkout containing the pinned commit. @@ -237,7 +237,7 @@ node docs/articles/sync-evidence.mjs "$SOURCE_DIR" node docs/articles/sync-evidence.mjs --check "$SOURCE_DIR" ``` -Both commands create a temporary worktree under the article checkout's `.scratch/`, install dependencies, build `dist/`, and regenerate `generated/`, test registrars and the example's generated outputs there. They remove that worktree on success or failure; they do not overwrite outputs in `$SOURCE_DIR`. Installing dependencies may populate npm's cache and needs registry access or a populated cache. Sync mode also writes the marked manuscript blocks. Check mode writes no manuscript. +Both commands create a temporary worktree under the article checkout's `.scratch/`, install dependencies, build `dist/`, and regenerate `generated/`, test registrars and the example's generated outputs there. They temporarily register the worktree in `$SOURCE_DIR`'s Git metadata and remove it on success or failure; they do not overwrite outputs in `$SOURCE_DIR`. Installing dependencies may populate npm's cache and needs registry access or a populated cache. Sync mode also writes the marked manuscript blocks. Check mode writes no manuscript. Always pass the source repository explicitly. Add a marked block and its generator whenever a manuscript gains a reusable output excerpt. Missing, duplicate, unknown, nested or fenced markers fail the check. Keep elision explicit, as in the constant-anchor excerpt. Do not paste a modified version of source or command output and call it an example. diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index d34df4e66..470146ae5 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -7,6 +7,7 @@ import { readdirSync, realpathSync, rmSync, + symlinkSync, writeFileSync, } from "node:fs"; import path from "node:path"; @@ -93,13 +94,61 @@ export function parseArguments(args) { return { check, printPin, source }; } -// The supplied checkout is only an object source. Its files, ignored outputs and installed -// dependencies never participate in evidence. All writes happen in a disposable worktree. +// Read raw blobs rather than checking out files: sparse patterns, attributes and filter +// drivers belong to the supplied repository, not the pin. Preserve binary bytes and modes. +function materializeTree(git, commit, source) { + const entries = git(["ls-tree", "-r", "-z", commit]) + .toString("utf8") + .split("\0") + .filter(Boolean) + .map((entry) => { + const tab = entry.indexOf("\t"); + const [mode, type, oid] = entry.slice(0, tab).split(" "); + const file = entry.slice(tab + 1); + if (type !== "blob" || !["100644", "100755", "120000"].includes(mode)) + throw new Error(`Unsupported pinned tree entry: ${mode} ${file}`); + return { mode, oid, file }; + }); + const blobs = git(["cat-file", "--batch"], entries.map(({ oid }) => oid + "\n").join("")); + let offset = 0; + for (const { mode, oid, file } of entries) { + const newline = blobs.indexOf(10, offset); + const [actualOid, type, sizeText] = blobs.subarray(offset, newline).toString("utf8").split(" "); + const size = Number(sizeText); + const start = newline + 1; + const end = start + size; + if ( + newline < offset || + actualOid !== oid || + type !== "blob" || + !Number.isSafeInteger(size) || + size < 0 || + end >= blobs.length || + blobs[end] !== 10 + ) + throw new Error(`Could not read pinned blob: ${file}`); + const target = path.join(source, file); + mkdirSync(path.dirname(target), { recursive: true }); + const content = blobs.subarray(start, end); + if (mode === "120000") symlinkSync(content, target); + else writeFileSync(target, content, { mode: mode === "100755" ? 0o755 : 0o644 }); + offset = end + 1; + } +} + +// The supplied checkout provides objects only. Its files, checkout settings, ignored outputs +// and dependencies never participate. Worktree registration is temporary git metadata. export function withPinnedSource(repository, commit, scratchRoot, action) { const root = realpathSync(repository); - const top = execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { - encoding: "utf8", - }).trim(); + const env = { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" }; + const git = (args, input) => + execFileSync("git", ["--no-replace-objects", "-C", root, ...args], { + env, + input, + stdio: "pipe", + maxBuffer: 128 * 1024 * 1024, + }); + const top = git(["rev-parse", "--show-toplevel"]).toString("utf8").trim(); if (realpathSync(top) !== root) throw new Error("Pass the source repository root, not a subdirectory."); mkdirSync(scratchRoot, { recursive: true }); @@ -107,22 +156,19 @@ export function withPinnedSource(repository, commit, scratchRoot, action) { const source = path.join(temporary, "source"); let added = false; try { - execFileSync("git", ["-C", root, "worktree", "add", "--detach", source, commit], { - stdio: "pipe", - }); + git(["worktree", "add", "--detach", "--no-checkout", source, commit]); added = true; + materializeTree(git, commit, source); execFileSync("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], { cwd: source, + env, stdio: "pipe", }); - execFileSync("npm", ["run", "build"], { cwd: source, stdio: "pipe" }); + execFileSync("npm", ["run", "build"], { cwd: source, env, stdio: "pipe" }); return action(source); } finally { try { - if (added) - execFileSync("git", ["-C", root, "worktree", "remove", "--force", source], { - stdio: "pipe", - }); + if (added) git(["worktree", "remove", "--force", source]); } finally { rmSync(temporary, { recursive: true, force: true }); } diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index df6dfc5c6..94e93a79d 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -1,11 +1,14 @@ import { execFileSync, spawnSync } from "node:child_process"; import { + chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, + readlinkSync, rmSync, + statSync, symlinkSync, writeFileSync, } from "node:fs"; @@ -45,6 +48,29 @@ describe("article evidence CLI entry", () => { rmSync(linkParent, { recursive: true, force: true }); } }); + + it("refuses a repository without the pin before executing its CLI", () => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-unpinned-")); + const sentinel = path.join(temporary, "executed"); + try { + execFileSync("git", ["init", temporary], { stdio: "pipe" }); + mkdirSync(path.join(temporary, "dist/cli"), { recursive: true }); + writeFileSync( + path.join(temporary, "dist/cli/sdp.js"), + `require("node:fs").writeFileSync(${JSON.stringify(sentinel)}, "executed");`, + ); + const result = spawnSync( + process.execPath, + [path.join(repositoryRoot, "docs/articles/sync-evidence.mjs"), "--check", temporary], + { encoding: "utf8" }, + ); + expect(existsSync(sentinel)).toBe(false); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("invalid reference"); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }); }); // Import the CLI's helpers directly: an import must not validate a checkout or regenerate files. @@ -155,30 +181,44 @@ describe("article evidence structure", () => { }).toThrow("missing"); }); - it("preserves nested carrier fences and detects edits to both carrier excerpts", () => { - const blocks = evidenceBlocks(repositoryRoot, "pin"); - for (const [id, file] of [ - ["create-order-carrier", "create-order.sdp.md"], - ["valid-cart-carrier", "create-order-valid-cart.sdp.md"], - ] as const) { - const block = blocks.find((entry) => entry.id === id); - expect(block, id).toBeDefined(); - if (!block) throw new Error(id); - const source = readFileSync( - path.join(repositoryRoot, "examples/checkout-v1/specs/orders", file), - "utf8", - ).trim(); - expect(block.generate()).toBe(source + "\n"); - const original = readFileSync(path.join(repositoryRoot, "docs/articles", block.file), "utf8"); - expect(replaceEvidenceBlock(original, block, source)).toBe(original); - const range = evidenceMarkers(original, block.file).get(id); - if (!range) throw new Error(id); - const changed = - original.slice(0, range.start) + - original.slice(range.start, range.end).replace("readiness:", "altered:") + - original.slice(range.end); - expect(replaceEvidenceBlock(changed, block, block.generate())).not.toBe(changed); - expect(original.slice(range.start, range.end)).toContain("````markdown"); + it("preserves nested carrier fences and detects edits independently of HEAD", () => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-carriers-")); + try { + const orders = path.join(temporary, "examples/checkout-v1/specs/orders"); + mkdirSync(orders, { recursive: true }); + const blocks = evidenceBlocks(temporary, "pin"); + for (const [id, file] of [ + ["create-order-carrier", "create-order.sdp.md"], + ["valid-cart-carrier", "create-order-valid-cart.sdp.md"], + ] as const) { + // Deliberately differs from the published pin: future carrier edits must not + // require an article refresh. check:articles owns equality at the pin. + const source = + `---\nreadiness: ready\n---\nA later carrier edit: ${id}\n\n` + + "```gwt\nGiven a changed example\n```\n"; + writeFileSync(path.join(orders, file), source); + const block = blocks.find((entry) => entry.id === id); + expect(block, id).toBeDefined(); + if (!block) throw new Error(id); + expect(block.generate()).toBe(source); + const original = readFileSync( + path.join(repositoryRoot, "docs/articles", block.file), + "utf8", + ); + const rendered = replaceEvidenceBlock(original, block, source); + expect(replaceEvidenceBlock(rendered, block, source)).toBe(rendered); + const range = evidenceMarkers(rendered, block.file).get(id); + if (!range) throw new Error(id); + const changed = + rendered.slice(0, range.start) + + rendered.slice(range.start, range.end).replace("readiness:", "altered:") + + rendered.slice(range.end); + expect(replaceEvidenceBlock(changed, block, block.generate())).toBe(rendered); + expect(rendered.slice(range.start, range.end)).toContain("````markdown"); + expect(rendered.slice(range.start, range.end)).toContain("```gwt"); + } + } finally { + rmSync(temporary, { recursive: true, force: true }); } }); @@ -226,7 +266,6 @@ describe("article evidence structure", () => { "utf8", ); expect(article).not.toContain("build output above carries the validation totals"); - expect(article).toContain("extraction and contract-generation diagnostics"); }); }); @@ -239,15 +278,17 @@ describe("article evidence arguments", () => { }); }); - it.each([ - ["/source", "--chek"], - ["--verbose", "/source"], - ["one", "two"], - ["--print-pin", "--chek"], - [], - ["--check"], - ["--print-pin", "/source"], - ])("refuses invalid arguments %j before touching manuscripts", (...args) => { + it.each( + [ + ["/source", "--chek"], + ["--verbose", "/source"], + ["one", "two"], + ["--print-pin", "--chek"], + [], + ["--check"], + ["--print-pin", "/source"], + ].map((args) => [args]), + )("refuses invalid arguments %j before touching manuscripts", (args) => { const article = path.join(repositoryRoot, "docs/articles/README.md"); const before = readFileSync(article, "utf8"); const result = spawnSync( @@ -260,102 +301,143 @@ describe("article evidence arguments", () => { expect(readFileSync(article, "utf8")).toBe(before); }); - it("documents an explicit source repository and disposable writes", () => { + it("does not offer an implicit source repository", () => { const playbook = readFileSync(path.join(repositoryRoot, "docs/articles/README.md"), "utf8"); expect(playbook).not.toContain("omit `$SOURCE_DIR`"); - expect(playbook).toContain("Always pass the source repository explicitly"); - expect(playbook).toContain("Both commands create a temporary worktree"); }); }); // Small real git repositories exercise materialization, npm's lockfile install and the build // without reinstalling the Protocol's full dependency tree for every regression probe. describe("pinned evidence source", () => { - it.each(["source", "build", "outputs"])("uses only the pin and isolates %s", (focus) => { - const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-pin-")); - const repo = path.join(temporary, "repository"); - const scratch = path.join(temporary, "scratch"); - mkdirSync(repo); - const git = (...args: string[]) => - execFileSync("git", ["-C", repo, ...args], { encoding: "utf8" }).trim(); - const read = (root: string, file: string) => readFileSync(path.join(root, file), "utf8"); - try { - git("init"); - writeFileSync( - path.join(repo, "package.json"), - JSON.stringify({ - name: "pin-probe", - version: "1.0.0", - scripts: { build: "node build.cjs" }, - }), - ); - writeFileSync( - path.join(repo, "package-lock.json"), - JSON.stringify({ - name: "pin-probe", - version: "1.0.0", - lockfileVersion: 3, - packages: { "": { name: "pin-probe", version: "1.0.0" } }, - }), - ); - writeFileSync(path.join(repo, "source.txt"), "pinned source"); - writeFileSync( - path.join(repo, ".gitignore"), - "*.test.generated.ts\ndist/\nnode_modules/\ngenerated/\n", - ); - writeFileSync( - path.join(repo, "build.cjs"), - "const fs = require('node:fs'); fs.mkdirSync('dist'); fs.writeFileSync('dist/result', fs.readFileSync('source.txt'));\n", - ); - git("add", "package.json", "package-lock.json", "source.txt", ".gitignore", "build.cjs"); - git( - "-c", - "user.name=Test", - "-c", - "user.email=test@example.invalid", - "-c", - "commit.gpgsign=false", - "commit", - "-m", - "pin", - ); - const commit = git("rev-parse", "HEAD"); - git("update-index", "--skip-worktree", "source.txt"); - writeFileSync(path.join(repo, "source.txt"), "hidden edit"); - writeFileSync(path.join(repo, "phantom.test.generated.ts"), "/** @sdpAnchor impl:phantom */"); - writeFileSync(path.join(repo, "untracked.txt"), "hidden untracked"); - git("config", "status.showUntrackedFiles", "no"); - for (const dir of ["dist", "node_modules", "generated"]) mkdirSync(path.join(repo, dir)); - writeFileSync(path.join(repo, "dist/result"), "stale build"); - writeFileSync(path.join(repo, "node_modules/foreign.txt"), "foreign dependency"); - writeFileSync(path.join(repo, "generated/sentinel"), "keep output"); - expect(git("status", "--porcelain")).toBe(""); - let materialized = ""; - withPinnedSource(repo, commit, scratch, (source) => { - materialized = source; - if (focus === "source") { - expect(read(source, "source.txt")).toBe("pinned source"); - expect(existsSync(path.join(source, "phantom.test.generated.ts"))).toBe(false); - expect(existsSync(path.join(source, "untracked.txt"))).toBe(false); - expect(existsSync(path.join(source, "node_modules/foreign.txt"))).toBe(false); + it.each(["source", "build", "outputs", "sparse checkout", "replace ref", "smudge filter"])( + "uses only the pin and isolates %s", + (focus) => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-pin-")); + const repo = path.join(temporary, "repository"); + const scratch = path.join(temporary, "scratch"); + mkdirSync(repo); + const git = (...args: string[]) => + execFileSync("git", ["-C", repo, ...args], { encoding: "utf8" }).trim(); + const read = (root: string, file: string) => readFileSync(path.join(root, file), "utf8"); + try { + git("init"); + writeFileSync( + path.join(repo, "package.json"), + JSON.stringify({ + name: "pin-probe", + version: "1.0.0", + scripts: { build: "node build.cjs" }, + }), + ); + writeFileSync( + path.join(repo, "package-lock.json"), + JSON.stringify({ + name: "pin-probe", + version: "1.0.0", + lockfileVersion: 3, + packages: { "": { name: "pin-probe", version: "1.0.0" } }, + }), + ); + writeFileSync(path.join(repo, "source.txt"), "pinned source"); + writeFileSync(path.join(repo, "omitted.txt"), "always present"); + writeFileSync(path.join(repo, "binary"), Buffer.from([0, 255, 10, 13])); + chmodSync(path.join(repo, "binary"), 0o755); + symlinkSync("source.txt", path.join(repo, "source-link")); + writeFileSync( + path.join(repo, ".gitignore"), + "*.test.generated.ts\ndist/\nnode_modules/\ngenerated/\n", + ); + writeFileSync( + path.join(repo, "build.cjs"), + "const fs = require('node:fs'); fs.mkdirSync('dist'); fs.writeFileSync('dist/result', fs.readFileSync('source.txt'));\n", + ); + git( + "add", + "package.json", + "package-lock.json", + "source.txt", + "omitted.txt", + "binary", + "source-link", + ".gitignore", + "build.cjs", + ); + git( + "-c", + "user.name=Test", + "-c", + "user.email=test@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "-m", + "pin", + ); + const commit = git("rev-parse", "HEAD"); + git("update-index", "--skip-worktree", "source.txt"); + writeFileSync(path.join(repo, "source.txt"), "hidden edit"); + writeFileSync( + path.join(repo, "phantom.test.generated.ts"), + "/** @sdpAnchor impl:phantom */", + ); + writeFileSync(path.join(repo, "untracked.txt"), "hidden untracked"); + git("config", "status.showUntrackedFiles", "no"); + for (const dir of ["dist", "node_modules", "generated"]) mkdirSync(path.join(repo, dir)); + writeFileSync(path.join(repo, "dist/result"), "stale build"); + writeFileSync(path.join(repo, "node_modules/foreign.txt"), "foreign dependency"); + writeFileSync(path.join(repo, "generated/sentinel"), "keep output"); + expect(git("status", "--porcelain")).toBe(""); + if (focus === "sparse checkout") { + git("sparse-checkout", "set", "--no-cone", "/*", "!/omitted.txt"); + expect(existsSync(path.join(repo, "omitted.txt"))).toBe(false); } - if (focus === "build") expect(read(source, "dist/result")).toBe("pinned source"); - mkdirSync(path.join(source, "generated"), { recursive: true }); - writeFileSync(path.join(source, "generated/sentinel"), "new output"); - }); - expect(read(repo, "generated/sentinel")).toBe("keep output"); - expect(existsSync(materialized)).toBe(false); - expect(read(repo, "dist/result")).toBe("stale build"); - expect(read(repo, "source.txt")).toBe("hidden edit"); - expect(() => - withPinnedSource(repo, commit, scratch, () => { - throw new Error("probe failure"); - }), - ).toThrow("probe failure"); - expect(readdirSync(scratch)).toEqual([]); - expect(git("worktree", "list", "--porcelain").match(/^worktree /gm)).toHaveLength(1); - } finally { - rmSync(temporary, { recursive: true, force: true }); - } - }); + if (focus === "replace ref") { + const original = git("rev-parse", `${commit}:source.txt`); + const replacement = execFileSync("git", ["-C", repo, "hash-object", "-w", "--stdin"], { + input: "replaced source", + encoding: "utf8", + }).trim(); + git("replace", original, replacement); + expect(git("show", `${commit}:source.txt`)).toBe("replaced source"); + } + if (focus === "smudge filter") { + writeFileSync(path.join(repo, ".git/info/attributes"), "source.txt filter=probe\n"); + git("config", "filter.probe.smudge", "sed s/pinned/filtered/"); + git("config", "filter.probe.required", "true"); + } + let materialized = ""; + withPinnedSource(repo, commit, scratch, (source) => { + materialized = source; + if (focus !== "build" && focus !== "outputs") + expect(read(source, "source.txt")).toBe("pinned source"); + expect(read(source, "omitted.txt")).toBe("always present"); + expect(readFileSync(path.join(source, "binary"))).toEqual(Buffer.from([0, 255, 10, 13])); + expect(statSync(path.join(source, "binary")).mode & 0o111).toBe(0o111); + expect(readlinkSync(path.join(source, "source-link"))).toBe("source.txt"); + if (focus === "source") { + expect(existsSync(path.join(source, "phantom.test.generated.ts"))).toBe(false); + expect(existsSync(path.join(source, "untracked.txt"))).toBe(false); + expect(existsSync(path.join(source, "node_modules/foreign.txt"))).toBe(false); + } + if (focus === "build") expect(read(source, "dist/result")).toBe("pinned source"); + mkdirSync(path.join(source, "generated"), { recursive: true }); + writeFileSync(path.join(source, "generated/sentinel"), "new output"); + }); + expect(read(repo, "generated/sentinel")).toBe("keep output"); + expect(existsSync(materialized)).toBe(false); + expect(read(repo, "dist/result")).toBe("stale build"); + expect(read(repo, "source.txt")).toBe("hidden edit"); + expect(() => + withPinnedSource(repo, commit, scratch, () => { + throw new Error("probe failure"); + }), + ).toThrow("probe failure"); + expect(readdirSync(scratch)).toEqual([]); + expect(git("worktree", "list", "--porcelain").match(/^worktree /gm)).toHaveLength(1); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }, + ); }); From 8fc1802b98173aadf5f8e2e6a9a53689cf7f2881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Tue, 3 Nov 2026 23:50:56 +0100 Subject: [PATCH 24/36] test(articles): prove generation uses the materialized pin K34-c-4 (round 3; also K34-c-1 and K34-c-3): exercise the real CLI with a toy pinned build and a different supplied CLI. The sentinel proves generation reads the materialized root; both bypass mutations now fail. Check the owner executable bit so the materialization test also passes under umask 077. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- test/article-evidence.test.ts | 83 ++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index 94e93a79d..7eaf30a83 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -1,6 +1,7 @@ import { execFileSync, spawnSync } from "node:child_process"; import { chmodSync, + copyFileSync, existsSync, mkdirSync, mkdtempSync, @@ -71,6 +72,86 @@ describe("article evidence CLI entry", () => { rmSync(temporary, { recursive: true, force: true }); } }); + + it("generates evidence from the materialized pin, never the supplied checkout", () => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-main-")); + const articles = path.join(temporary, "articles/docs/articles"); + const repo = path.join(temporary, "repository"); + const sentinel = path.join(temporary, "executed"); + const git = (...args: string[]) => + execFileSync("git", ["-C", repo, ...args], { encoding: "utf8" }).trim(); + // Stop at the first generator's CLI call: its origin is the observation under test. + const cli = (origin: string) => + `require("node:fs").appendFileSync(${JSON.stringify(sentinel)}, ${JSON.stringify(origin + "\n")}); process.exit(1);`; + try { + mkdirSync(articles, { recursive: true }); + mkdirSync(repo); + git("init", "-q"); + writeFileSync( + path.join(repo, "package.json"), + JSON.stringify({ + name: "pin-probe", + version: "1.0.0", + scripts: { build: "node build.cjs" }, + }), + ); + writeFileSync( + path.join(repo, "package-lock.json"), + JSON.stringify({ + name: "pin-probe", + version: "1.0.0", + lockfileVersion: 3, + packages: { "": { name: "pin-probe", version: "1.0.0" } }, + }), + ); + writeFileSync( + path.join(repo, "build.cjs"), + `const fs = require("node:fs"); fs.mkdirSync("dist/cli", { recursive: true }); fs.writeFileSync("dist/cli/sdp.js", ${JSON.stringify(cli("materialized"))});`, + ); + writeFileSync(path.join(repo, ".gitignore"), "dist/\n"); + git("add", "package.json", "package-lock.json", "build.cjs", ".gitignore"); + git( + "-c", + "user.name=Test", + "-c", + "user.email=test@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "-qm", + "pin", + ); + const pin = git("rev-parse", "HEAD"); + mkdirSync(path.join(repo, "dist/cli"), { recursive: true }); + writeFileSync(path.join(repo, "dist/cli/sdp.js"), cli("supplied")); + + // Run the real entry point with a toy pin, independent of HEAD's engine and corpus. + copyFileSync( + path.join(repositoryRoot, "docs/articles/sync-evidence.mjs"), + path.join(articles, "sync-evidence.mjs"), + ); + const documents = new Map([["README.md", `Pinned source baseline: x/tree/${pin}\n`]]); + for (const block of evidenceBlocks(repo, pin)) { + documents.set( + block.file, + (documents.get(block.file) ?? "") + + `\nold\n\n`, + ); + } + for (const [file, text] of documents) writeFileSync(path.join(articles, file), text); + const result = spawnSync( + process.execPath, + [path.join(articles, "sync-evidence.mjs"), "--check", repo], + { encoding: "utf8" }, + ); + expect(result.error).toBeUndefined(); + expect(result.status).toBe(1); + expect(readFileSync(sentinel, "utf8")).toBe("materialized\n"); + expect(git("worktree", "list", "--porcelain").match(/^worktree /gm)).toHaveLength(1); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }); }); // Import the CLI's helpers directly: an import must not validate a checkout or regenerate files. @@ -413,7 +494,7 @@ describe("pinned evidence source", () => { expect(read(source, "source.txt")).toBe("pinned source"); expect(read(source, "omitted.txt")).toBe("always present"); expect(readFileSync(path.join(source, "binary"))).toEqual(Buffer.from([0, 255, 10, 13])); - expect(statSync(path.join(source, "binary")).mode & 0o111).toBe(0o111); + expect(statSync(path.join(source, "binary")).mode & 0o100).toBe(0o100); expect(readlinkSync(path.join(source, "source-link"))).toBe("source.txt"); if (focus === "source") { expect(existsSync(path.join(source, "phantom.test.generated.ts"))).toBe(false); From 5a3de4e0fdb62a45941b92a2bffb3e39fd97233c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 00:16:38 +0100 Subject: [PATCH 25/36] docs(agents): describe the lineage folder's two authored annotation notes - VR2-T-1: the handbook's docs/lineage row named only the restored v0 concept set. The folder also holds two authored notes with no lineage header: the short annotation context and the dated research scout kept under an archive- name. The row now names both, says each states its own status, and keeps all of it as evidence, never intended truth. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 1af0c26dd..82db9e227 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,7 +63,7 @@ Progressive disclosure — start at the top, follow the pointers down. | `src/` | **the engine** — `model` (Spec/descriptors/pack/anchors) · `extract` · `graph` · `validate` · `reader` (agent surface) · `projections` (Design Review, census, Mermaid, Gherkin and Studio) · `cli` (`sdp build` · `validate` · `view` · `census` · `mermaid` · `gherkin` · `studio` · `vault` · `stamp` · `generate` · `import` · `new spec` · `q`) · `import` (carrier conversion and import analysis) · `testing` (the registrar runtime exported through `./testing`) · `runner` / `codegen` / `notation` / `adapters` | when implementing or verifying **current engine** behavior | | `examples/checkout-v1` | **the worked MVP example** (Markdown carriers; originally the TS DSL tracer bullet) — specs, anchors, untracked `generated/` (regenerated in-pipeline); walkthrough in its README | when proving the loop end-to-end | | `explorations/` | **evidence only** (carrier exhibits, executable-example findings) — mapping evidence for design; **never promote spike code into product** | when judging design evidence; not a source tree to ship | -| `docs/lineage/` | restored historical design documents — the v0 concept set, verbatim with lineage headers; evidence for design reviews, never intended truth | when a review needs superseded design on the table rather than in git history | +| `docs/lineage/` | historical design evidence — the v0 concept set, restored verbatim with lineage headers, plus two authored annotation notes: the short annotation context (`annotation-context.md`) and the dated research scout, kept temporarily under an `archive-` name; each note states its own status; all of it is evidence for design reviews, never intended truth | when a review needs superseded design, or the background to the annotation decisions, on the table rather than in git history | | `pack:roadmap` + recipe 53 | **the next work** — the owner's advisory priority over upcoming capabilities, each entry placed by the graph as to design, to state, to build or built; the open arc's record is its draft pull request | before writing code | | `npm run check` | **the green gate** — `check:temporal` → `lint` → `format:check` → `build` → `generate:self-hosting` → `generate:example` → `typecheck` → `typecheck:examples` → `test` → `check:self-hosting-gates` → `check:self-hosting` → `check:example` → `preflight` | before claiming green / after engine edits | | `plans/`, `reviews/` | **frozen history** of earlier arcs: plans, session reviews, prompts and assets; read for provenance; nothing is added | rarely | From 5b37938424e1e88ef8899d929f4dd8507ad8a1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 00:18:40 +0100 Subject: [PATCH 26/36] fix(articles): bound evidence delimiter scans - claude-side-T-6: replace polynomial anchor and Mermaid regexes with fixed delimiter searches and bound pin parsing to one line. Add adversarial subprocess tests; all three time out without the fix. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/articles/sync-evidence.mjs | 30 ++++++++--- test/article-evidence.test.ts | 89 +++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 8 deletions(-) diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index 470146ae5..614f8b2d9 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -330,10 +330,12 @@ export function evidenceBlocks(sourceRoot, pinnedCommit) { function constantAnchor() { const source = sourceText("examples/checkout-v1/src/orders/create-order.use-case.ts"); const importLine = source.split("\n")[0]; - const anchor = source.match( - /export const createOrderUseCaseAnchor = codeAnchor\(\{[\s\S]*?\n\}\);/, - )?.[0]; - if (!anchor) throw new Error("Could not locate the create-order constant anchor."); + const opening = "export const createOrderUseCaseAnchor = codeAnchor({"; + const closing = "\n});"; + const start = source.indexOf(opening); + const end = start < 0 ? -1 : source.indexOf(closing, start + opening.length); + if (end < 0) throw new Error("Could not locate the create-order constant anchor."); + const anchor = source.slice(start, end + closing.length); return `${importLine}\n\n// …\n\n${anchor}`; } @@ -362,9 +364,11 @@ export function evidenceBlocks(sourceRoot, pinnedCommit) { function roadmapMermaid() { runCli(["mermaid", "examples/checkout-v1"]); const page = sourceText("examples/checkout-v1/generated/mermaid/pack/checkout-roadmap.md"); - const match = page.match(/```mermaid\n([\s\S]*?)\n```/); - if (!match) throw new Error("Could not read the generated checkout-roadmap Mermaid block."); - return match[1]; + const opening = "```mermaid\n"; + const start = page.indexOf(opening); + const end = start < 0 ? -1 : page.indexOf("\n```", start + opening.length); + if (end < 0) throw new Error("Could not read the generated checkout-roadmap Mermaid block."); + return page.slice(start + opening.length, end); } const blocks = [ @@ -445,7 +449,17 @@ function main() { const articlesDir = path.dirname(fileURLToPath(import.meta.url)); const repositoryRoot = path.resolve(articlesDir, "../.."); const playbook = readFileSync(path.join(articlesDir, "README.md"), "utf8"); - const pinnedCommit = playbook.match(/Pinned source baseline:.*tree\/([0-9a-f]{40})/)?.[1]; + // Bound the link search on each candidate line; repeated incomplete labels must not + // cause rescans of the rest of the playbook. Accept the playbook's quoted bold label + // and the plain label used by small source fixtures. + const pinnedCommit = playbook + .split("\n") + .map((line) => + /^(?:> \*\*)?Pinned source baseline:[^\r\n]{0,200}tree\/([0-9a-f]{40})(?![0-9a-f])/.exec( + line, + ), + ) + .find((match) => match)?.[1]; if (!pinnedCommit) { throw new Error("The publication plan does not name a full pinned source commit."); diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index 7eaf30a83..185749d0b 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -32,6 +32,95 @@ import { const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +describe("article evidence delimiter scans", () => { + const pin = "a".repeat(40); + const cases = [ + { + id: "pin", + file: "README.md", + prefix: "Pinned source baseline:x", + complete: `> **Pinned source baseline:** commit [pin](https://example.test/tree/${pin}).\n`, + expected: pin, + error: "The publication plan does not name a full pinned source commit.", + }, + { + id: "constant-anchor", + file: "examples/checkout-v1/src/orders/create-order.use-case.ts", + prefix: "export const createOrderUseCaseAnchor = codeAnchor({", + complete: + 'import { codeAnchor } from "./anchors";\n// earlier delimiter\n});\n' + + 'export const createOrderUseCaseAnchor = codeAnchor({\n id: "impl:probe",\n});\ntrailing text', + expected: + 'import { codeAnchor } from "./anchors";\n\n// …\n\n' + + 'export const createOrderUseCaseAnchor = codeAnchor({\n id: "impl:probe",\n});', + error: "Could not locate the create-order constant anchor.", + }, + { + id: "roadmap-mermaid", + file: "examples/checkout-v1/generated/mermaid/pack/checkout-roadmap.md", + prefix: "```mermaid\nx", + complete: "Earlier fence\n```\n\n```mermaid\ngraph TD\n A --> B\n```\ntrailing text", + expected: "graph TD\n A --> B", + error: "Could not read the generated checkout-roadmap Mermaid block.", + }, + ]; + + for (const probe of cases) { + it.each(["complete", "missing opening", "missing closing", "adversarial"])( + `${probe.id}: handles %s input within the subprocess deadline`, + (scenario) => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-scan-")); + try { + const input = + scenario === "complete" + ? probe.complete + : scenario === "missing opening" + ? "unrelated text\n});\n```\n" + : scenario === "missing closing" + ? probe.prefix + : probe.prefix.repeat(100_000) + + (probe.id === "pin" ? `\n${probe.complete}` : ""); + const file = path.join(temporary, probe.file); + mkdirSync(path.dirname(file), { recursive: true }); + writeFileSync(file, input); + // The Mermaid generator runs the CLI first. Keep this probe about parsing its + // generated page, without invoking a full graph build on adversarial text. + mkdirSync(path.join(temporary, "dist/cli"), { recursive: true }); + writeFileSync(path.join(temporary, "dist/cli/sdp.js"), ""); + const script = path.join(repositoryRoot, "docs/articles/sync-evidence.mjs"); + const copiedScript = path.join(temporary, "sync-evidence.mjs"); + copyFileSync(script, copiedScript); + const args = + probe.id === "pin" + ? [copiedScript, "--print-pin"] + : [ + "--input-type=module", + "--eval", + `import { evidenceBlocks } from ${JSON.stringify(copiedScript)}; + console.log(evidenceBlocks(${JSON.stringify(temporary)}, "pin") + .find((block) => block.id === ${JSON.stringify(probe.id)}).generate());`, + ]; + // Synchronous regex work cannot be interrupted by Vitest's own timeout. A + // generous child deadline catches polynomial scans without a tight benchmark. + const result = spawnSync(process.execPath, args, { + encoding: "utf8", + timeout: 5_000, + }); + expect(result.error, `${probe.id}: ${scenario}`).toBeUndefined(); + const succeeds = + scenario === "complete" || (scenario === "adversarial" && probe.id === "pin"); + expect(result.status).toBe(succeeds ? 0 : 1); + if (succeeds) expect(result.stdout.trim()).toBe(probe.expected); + else expect(result.stderr).toContain(probe.error); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }, + 15_000, + ); + } +}); + describe("article evidence CLI entry", () => { it("runs when invoked through a symlinked checkout path", () => { const linkParent = mkdtempSync(path.join(tmpdir(), "sdp-article-link-")); From 6eeceb664f1e0a53d9dab4ac79f26713ba1c3189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 00:29:17 +0100 Subject: [PATCH 27/36] fix(articles): bound markdown fence delimiter parsing - claude-side-T-6: remove the remaining quadratic fence suffix match; slice the suffix after matching the delimiter run. Cover both fence characters with CR and Unicode line separators under child deadlines, and recognize CRLF closing fences. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/articles/sync-evidence.mjs | 5 +++-- test/article-evidence.test.ts | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index 614f8b2d9..b69a8674c 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -35,9 +35,10 @@ export function evidenceMarkers(original, file) { active = undefined; } } - const delimiter = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line); + const delimiter = /^ {0,3}(`{3,}|~{3,})/.exec(line); if (delimiter) { - const [, run, rest] = delimiter; + const run = delimiter[1]; + const rest = line.slice(delimiter[0].length); if (!fence) fence = run; else if (run[0] === fence[0] && run.length >= fence.length && !rest.trim()) fence = undefined; } diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index 185749d0b..f2a7086a3 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -33,6 +33,36 @@ import { const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); describe("article evidence delimiter scans", () => { + it.each([ + ["backtick", "`", "carriage return", "\r"], + ["backtick", "`", "line separator", "\u2028"], + ["backtick", "`", "paragraph separator", "\u2029"], + ["tilde", "~", "carriage return", "\r"], + ["tilde", "~", "line separator", "\u2028"], + ["tilde", "~", "paragraph separator", "\u2029"], + ])( + "%s fence (%s) followed by %s completes within the subprocess deadline", + (_name, delimiter, _tailName, tail) => { + const script = path.join(repositoryRoot, "docs/articles/sync-evidence.mjs"); + const result = spawnSync( + process.execPath, + [ + "--input-type=module", + "--eval", + `import { evidenceMarkers } from ${JSON.stringify(script)}; + const article = "# Article\\n\\n" + ${JSON.stringify(delimiter)}.repeat(100_000) + + ${JSON.stringify(tail)} + "\\n\\nprose\\n"; + console.log(JSON.stringify([...evidenceMarkers(article, "article.md")]));`, + ], + { encoding: "utf8", timeout: 5_000 }, + ); + expect(result.error).toBeUndefined(); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("[]"); + }, + 15_000, + ); + const pin = "a".repeat(40); const cases = [ { @@ -351,6 +381,13 @@ describe("article evidence structure", () => { }).toThrow("missing"); }); + it.each(["```", "~~~"])("recognizes markers after a CRLF %s closing fence", (fence) => { + const original = `${fence}text\nexample\n${fence}\r\n${probeText}`; + const range = evidenceMarkers(original, "article.md").get("probe"); + expect(range).toBeDefined(); + expect(original.slice(range?.start, range?.end)).toBe(probeText); + }); + it("preserves nested carrier fences and detects edits independently of HEAD", () => { const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-carriers-")); try { From 43d6390118e836029d5ff48e3d3cf7023073be0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 01:55:18 +0100 Subject: [PATCH 28/36] fix(articles): honor operand separators and markdown fence whitespace - F-sweep2-4: accept dash-prefixed source operands after -- while rejecting extra operands. - F-sweep2-5: close fences only on CommonMark spaces and tabs, with CRLF support. Co-Authored-By: Claude Opus 5.5 --- docs/articles/sync-evidence.mjs | 4 ++-- test/article-evidence.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index b69a8674c..3ca1ffe29 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -40,7 +40,7 @@ export function evidenceMarkers(original, file) { const run = delimiter[1]; const rest = line.slice(delimiter[0].length); if (!fence) fence = run; - else if (run[0] === fence[0] && run.length >= fence.length && !rest.trim()) fence = undefined; + else if (run[0] === fence[0] && run.length >= fence.length && /^[ \t]*\r?$/.test(rest)) fence = undefined; } offset += line.length + 1; } @@ -85,7 +85,7 @@ export function parseArguments(args) { if (argument === "--" && !separator) separator = true; else if (argument === "--check" && !separator && !check) check = true; else if (argument === "--print-pin" && !separator && !printPin) printPin = true; - else if (argument.startsWith("-")) + else if (!separator && argument.startsWith("-")) throw new Error(`Unknown or repeated option: ${argument}\n${usage}`); else if (source !== undefined) throw new Error(`Only one source repository is allowed.\n${usage}`); diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index f2a7086a3..3514f2a6c 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -388,6 +388,27 @@ describe("article evidence structure", () => { expect(original.slice(range?.start, range?.end)).toBe(probeText); }); + for (const fence of ["```", "~~~"]) { + it.each(["\u2028", "\u2029", "\u00a0", "\v", "\f"])( + `keeps ${fence} open with a non-CommonMark closing tail %j`, + (tail) => { + expect(() => + evidenceMarkers(`${fence}\ncode\n${fence}${tail}\n${probeText}`, "article.md"), + ).toThrow("malformed or fenced evidence marker"); + }, + ); + it.each(["", " ", "\t", " \t\r"])( + `closes ${fence} with a CommonMark closing tail %j`, + (tail) => { + expect( + evidenceMarkers(`${fence}\ncode\n${fence}${tail}\n${probeText}`, "article.md").has( + "probe", + ), + ).toBe(true); + }, + ); + } + it("preserves nested carrier fences and detects edits independently of HEAD", () => { const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-carriers-")); try { @@ -485,6 +506,15 @@ describe("article evidence arguments", () => { }); }); + it.each(["-checkout", "--check", "--"])("accepts %s as an operand after --", (source) => { + expect(parseArguments(["--check", "--", source])).toEqual({ + check: true, + printPin: false, + source, + }); + expect(() => parseArguments(["--check", "--", source, "second"])).toThrow("Only one source"); + }); + it.each( [ ["/source", "--chek"], From 4c61484512d218ec72ce4bc519d025f94d87edcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 01:55:56 +0100 Subject: [PATCH 29/36] fix(articles): stream pinned blobs directly to files - F-sweep2-6: materialize each regular blob through a file descriptor instead of buffering the entire tree. A 144 MiB fixture exceeds the former aggregate limit. Co-Authored-By: Claude Opus 5.5 --- docs/articles/sync-evidence.mjs | 38 ++++++++++------------- test/article-evidence.test.ts | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index 3ca1ffe29..fd9bf3f5d 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -1,8 +1,10 @@ import { execFileSync, spawnSync } from "node:child_process"; import { + closeSync, existsSync, mkdirSync, mkdtempSync, + openSync, readFileSync, readdirSync, realpathSync, @@ -110,30 +112,20 @@ function materializeTree(git, commit, source) { throw new Error(`Unsupported pinned tree entry: ${mode} ${file}`); return { mode, oid, file }; }); - const blobs = git(["cat-file", "--batch"], entries.map(({ oid }) => oid + "\n").join("")); - let offset = 0; for (const { mode, oid, file } of entries) { - const newline = blobs.indexOf(10, offset); - const [actualOid, type, sizeText] = blobs.subarray(offset, newline).toString("utf8").split(" "); - const size = Number(sizeText); - const start = newline + 1; - const end = start + size; - if ( - newline < offset || - actualOid !== oid || - type !== "blob" || - !Number.isSafeInteger(size) || - size < 0 || - end >= blobs.length || - blobs[end] !== 10 - ) - throw new Error(`Could not read pinned blob: ${file}`); const target = path.join(source, file); mkdirSync(path.dirname(target), { recursive: true }); - const content = blobs.subarray(start, end); - if (mode === "120000") symlinkSync(content, target); - else writeFileSync(target, content, { mode: mode === "100755" ? 0o755 : 0o644 }); - offset = end + 1; + if (mode === "120000") symlinkSync(git(["cat-file", "blob", oid]), target); + else { + // Direct stdout to the file: neither the tree nor a single large blob needs + // to fit in execFileSync's output buffer. + const output = openSync(target, "w", mode === "100755" ? 0o755 : 0o644); + try { + git(["cat-file", "blob", oid], undefined, output); + } finally { + closeSync(output); + } + } } } @@ -142,11 +134,11 @@ function materializeTree(git, commit, source) { export function withPinnedSource(repository, commit, scratchRoot, action) { const root = realpathSync(repository); const env = { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" }; - const git = (args, input) => + const git = (args, input, output = "pipe") => execFileSync("git", ["--no-replace-objects", "-C", root, ...args], { env, input, - stdio: "pipe", + stdio: ["pipe", output, "pipe"], maxBuffer: 128 * 1024 * 1024, }); const top = git(["rev-parse", "--show-toplevel"]).toString("utf8").trim(); diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index 3514f2a6c..8b20bd235 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -547,6 +547,59 @@ describe("article evidence arguments", () => { // Small real git repositories exercise materialization, npm's lockfile install and the build // without reinstalling the Protocol's full dependency tree for every regression probe. describe("pinned evidence source", () => { + it("materializes blobs totaling more than the former batch buffer", () => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-large-")); + const repo = path.join(temporary, "repository"); + mkdirSync(repo); + const git = (...args: string[]) => + execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: "pipe" }).trim(); + try { + git("init", "-q"); + writeFileSync( + path.join(repo, "package.json"), + JSON.stringify({ name: "large-probe", version: "1.0.0", scripts: { build: "node -e ''" } }), + ); + writeFileSync( + path.join(repo, "package-lock.json"), + JSON.stringify({ + name: "large-probe", + version: "1.0.0", + lockfileVersion: 3, + packages: { "": { name: "large-probe", version: "1.0.0" } }, + }), + ); + const size = 48 * 1024 * 1024; + for (const file of ["a.bin", "b.bin", "c.bin"]) + writeFileSync(path.join(repo, file), Buffer.alloc(size, 42)); + git("add", "package.json", "package-lock.json", "a.bin", "b.bin", "c.bin"); + git( + "-c", + "user.name=Test", + "-c", + "user.email=test@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "-qm", + "pin", + ); + withPinnedSource( + repo, + git("rev-parse", "HEAD"), + path.join(temporary, "scratch"), + (source) => { + for (const file of ["a.bin", "b.bin", "c.bin"]) { + expect(statSync(path.join(source, file)).size).toBe(size); + expect(readFileSync(path.join(source, file)).equals(Buffer.alloc(size, 42))).toBe(true); + } + }, + ); + expect(git("worktree", "list", "--porcelain").match(/^worktree /gm)).toHaveLength(1); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }, 30_000); + it.each(["source", "build", "outputs", "sparse checkout", "replace ref", "smudge filter"])( "uses only the pin and isolates %s", (focus) => { From 5efd7312dba0b91f10830279aadf073c6088ed3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 01:58:11 +0100 Subject: [PATCH 30/36] fix(articles): clean up interrupted evidence builds - F-pr34-claude-1: supervise the pinned build outside the checkout, stop its process group on SIGINT or SIGTERM, and remove its temporary registration. Verify cancellation and a concurrent temporal gate. Co-Authored-By: Claude Opus 5.5 --- docs/articles/README.md | 2 +- docs/articles/sync-evidence.mjs | 63 +++++++++++++- test/article-evidence.test.ts | 142 +++++++++++++++++++++++++++++++- 3 files changed, 201 insertions(+), 6 deletions(-) diff --git a/docs/articles/README.md b/docs/articles/README.md index 468400891..c854ad94a 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -237,7 +237,7 @@ node docs/articles/sync-evidence.mjs "$SOURCE_DIR" node docs/articles/sync-evidence.mjs --check "$SOURCE_DIR" ``` -Both commands create a temporary worktree under the article checkout's `.scratch/`, install dependencies, build `dist/`, and regenerate `generated/`, test registrars and the example's generated outputs there. They temporarily register the worktree in `$SOURCE_DIR`'s Git metadata and remove it on success or failure; they do not overwrite outputs in `$SOURCE_DIR`. Installing dependencies may populate npm's cache and needs registry access or a populated cache. Sync mode also writes the marked manuscript blocks. Check mode writes no manuscript. +Both commands create a worktree in the system temporary directory (keep `TMPDIR` outside either checkout), install dependencies, build `dist/`, and regenerate `generated/`, test registrars and the example's generated outputs there. They temporarily register the worktree in `$SOURCE_DIR`'s Git metadata and remove it on success, failure, SIGINT or SIGTERM; they do not overwrite outputs in `$SOURCE_DIR`. Installing dependencies may populate npm's cache and needs registry access or a populated cache. Sync mode also writes the marked manuscript blocks. Check mode writes no manuscript. On interruption, the supervising process stops the worker and its child processes before removing the worktree. SIGKILL and machine failure cannot run cleanup; remove any surviving worktree with `git worktree remove --force ` from the source repository. Always pass the source repository explicitly. Add a marked block and its generator whenever a manuscript gains a reusable output excerpt. Missing, duplicate, unknown, nested or fenced markers fail the check. Keep elision explicit, as in the constant-anchor excerpt. Do not paste a modified version of source or command output and call it an example. diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index fd9bf3f5d..8f174d386 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -1,4 +1,4 @@ -import { execFileSync, spawnSync } from "node:child_process"; +import { execFileSync, fork, spawnSync } from "node:child_process"; import { closeSync, existsSync, @@ -12,6 +12,7 @@ import { symlinkSync, writeFileSync, } from "node:fs"; +import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -437,7 +438,58 @@ export function evidenceBlocks(sourceRoot, pinnedCommit) { return blocks; } -function main() { +// Keep cancellation in a responsive parent while the worker runs synchronous git, +// npm and evidence commands. A separate process group includes npm's descendants. +async function supervise(repository) { + const temporary = realpathSync(mkdtempSync(path.join(tmpdir(), "article-evidence-"))); + let interrupted; + const child = fork(fileURLToPath(import.meta.url), process.argv.slice(2), { + detached: process.platform !== "win32", + stdio: ["inherit", "inherit", "inherit", "ipc"], + env: { ...process.env, SDP_ARTICLE_WORKER_ROOT: temporary }, + }); + const stop = (signal) => { + interrupted ??= signal; + if (!child.pid) return; + if (process.platform === "win32") { + spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }); + } else { + try { + process.kill(-child.pid, "SIGKILL"); + } catch (error) { + if (error.code !== "ESRCH") throw error; + } + } + }; + const onInterrupt = () => stop("SIGINT"); + const onTerminate = () => stop("SIGTERM"); + process.on("SIGINT", onInterrupt); + process.on("SIGTERM", onTerminate); + try { + const code = await new Promise((resolve, reject) => { + child.once("error", reject); + child.once("close", (code) => resolve(code)); + }); + process.exitCode = interrupted === "SIGINT" ? 130 : interrupted === "SIGTERM" ? 143 : (code ?? 1); + } finally { + try { + // A killed worker cannot run its finally. Remove only this invocation's + // registrations, after its process group has stopped writing. + for (const name of readdirSync(temporary)) { + const source = path.join(temporary, name, "source"); + if (existsSync(source)) { + execFileSync("git", ["-C", repository, "worktree", "remove", "--force", source], { stdio: "pipe" }); + } + } + rmSync(temporary, { recursive: true, force: true }); + } finally { + process.off("SIGINT", onInterrupt); + process.off("SIGTERM", onTerminate); + } + } +} + +async function main() { const { check, printPin, source: sourceArgument } = parseArguments(process.argv.slice(2)); const articlesDir = path.dirname(fileURLToPath(import.meta.url)); const repositoryRoot = path.resolve(articlesDir, "../.."); @@ -469,10 +521,13 @@ function main() { .map((name) => [name, readFileSync(path.join(articlesDir, name), "utf8")]), ); validateEvidenceMarkers(documents, evidenceBlocks(repositoryRoot, pinnedCommit)); + if (!process.send || !process.env.SDP_ARTICLE_WORKER_ROOT) { + return supervise(sourceArgument); + } return withPinnedSource( sourceArgument, pinnedCommit, - path.join(repositoryRoot, ".scratch"), + process.env.SDP_ARTICLE_WORKER_ROOT, (sourceRoot) => { const blocks = evidenceBlocks(sourceRoot, pinnedCommit); const drift = []; @@ -507,5 +562,5 @@ if ( process.argv[1] && realpathSync(path.resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url)) ) { - main(); + await main(); } diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index 8b20bd235..58a0c6cdc 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -1,4 +1,4 @@ -import { execFileSync, spawnSync } from "node:child_process"; +import { execFileSync, spawn, spawnSync } from "node:child_process"; import { chmodSync, copyFileSync, @@ -192,6 +192,146 @@ describe("article evidence CLI entry", () => { } }); + it.each(["SIGINT", "SIGTERM"] as const)( + "stops descendants and removes the temporary registration on %s while gates can run", + async (signal) => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-cancel-")); + const repo = path.join(temporary, "repository"); + const articles = path.join(repo, "docs/articles"); + const started = path.join(temporary, "started.json"); + const git = (...args: string[]) => + execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: "pipe" }).trim(); + let child: ReturnType | undefined; + let workerSource: string | undefined; + let descendant: number | undefined; + try { + mkdirSync(articles, { recursive: true }); + git("init", "-q"); + writeFileSync( + path.join(repo, "package.json"), + JSON.stringify({ + name: "cancel-probe", + version: "1.0.0", + scripts: { build: "node build.cjs" }, + }), + ); + writeFileSync( + path.join(repo, "package-lock.json"), + JSON.stringify({ + name: "cancel-probe", + version: "1.0.0", + lockfileVersion: 3, + packages: { "": { name: "cancel-probe", version: "1.0.0" } }, + }), + ); + // npm -> shell -> build -> long-lived descendant. Killing only npm leaks it. + writeFileSync( + path.join(repo, "build.cjs"), + `const {spawn} = require("node:child_process"); + const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], {stdio: "inherit"}); + require("node:fs").writeFileSync(${JSON.stringify(started)}, JSON.stringify({source: process.cwd(), pid: child.pid})); + setInterval(() => {}, 1000);`, + ); + git("add", "package.json", "package-lock.json", "build.cjs"); + git( + "-c", + "user.name=Test", + "-c", + "user.email=test@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "-qm", + "pin", + ); + const pin = git("rev-parse", "HEAD"); + copyFileSync( + path.join(repositoryRoot, "docs/articles/sync-evidence.mjs"), + path.join(articles, "sync-evidence.mjs"), + ); + copyFileSync( + path.join(repositoryRoot, "check-temporal.mjs"), + path.join(repo, "check-temporal.mjs"), + ); + const documents = new Map([["README.md", `Pinned source baseline: x/tree/${pin}\n`]]); + for (const block of evidenceBlocks(repo, pin)) + documents.set( + block.file, + (documents.get(block.file) ?? "") + + `\nold\n\n`, + ); + for (const [file, text] of documents) writeFileSync(path.join(articles, file), text); + const before = git("status", "--porcelain", "--untracked-files=all"); + child = spawn( + process.execPath, + [path.join(articles, "sync-evidence.mjs"), "--check", repo], + { stdio: "pipe", detached: process.platform !== "win32" }, + ); + let stderr = ""; + child.stderr?.on("data", (data: Buffer) => { + stderr += data.toString(); + }); + const exited = new Promise((resolve, reject) => { + child?.once("error", reject); + child?.once("close", resolve); + }); + await expect.poll(() => existsSync(started), { timeout: 15_000 }).toBe(true); + const startedData = JSON.parse(readFileSync(started, "utf8")) as { + source: string; + pid: number; + }; + workerSource = startedData.source; + descendant = startedData.pid; + // A concurrent repository gate must not enumerate the disposable source. + const gate = spawnSync(process.execPath, [path.join(repo, "check-temporal.mjs")], { + encoding: "utf8", + }); + child.kill(signal); + await expect + .poll(() => child?.exitCode !== null || child?.signalCode !== null, { timeout: 5_000 }) + .toBe(true); + expect.soft(await exited, stderr).toBe(signal === "SIGINT" ? 130 : 143); + expect.soft(gate.status, gate.stdout + gate.stderr).toBe(0); + expect.soft(path.relative(repo, workerSource).startsWith(".." + path.sep)).toBe(true); + expect.soft(git("status", "--porcelain", "--untracked-files=all")).toBe(before); + expect.soft(existsSync(workerSource)).toBe(false); + expect(git("worktree", "list", "--porcelain").match(/^worktree /gm)).toHaveLength(1); + await expect + .poll( + () => { + try { + process.kill(startedData.pid, 0); + return true; + } catch { + return false; + } + }, + { timeout: 5_000 }, + ) + .toBe(false); + } finally { + if (child?.pid && process.platform !== "win32") { + try { + process.kill(-child.pid, "SIGKILL"); + } catch { + /* already stopped */ + } + } else if (child?.exitCode === null && child.signalCode === null) child.kill("SIGKILL"); + if (descendant) { + try { + process.kill(descendant, "SIGKILL"); + } catch { + /* already stopped */ + } + } + if (workerSource && existsSync(workerSource)) + spawnSync("git", ["-C", repo, "worktree", "remove", "--force", workerSource]); + rmSync(temporary, { recursive: true, force: true }); + } + }, + 30_000, + ); + it("generates evidence from the materialized pin, never the supplied checkout", () => { const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-main-")); const articles = path.join(temporary, "articles/docs/articles"); From 0389c5605e6f40c7c14ccbe2d407d6498de5069f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 02:01:29 +0100 Subject: [PATCH 31/36] fix(articles): keep refresh guidance and tests independent of head state - F-sweep2-2: give install-and-build tests a 30-second timeout without changing the global timeout. - F-sweep2-3: render controlled empty and nonempty drift answers without requiring a field or alarm from the current corpus. - F-pr34-claude-2: read the runtime range from package.json engines.node and recheck prose during refresh. - F-pr34-claude-3: distinguish repository-path link checks from manual fragment, URL and rendering review. - F-pr34-claude-1: check removal of the whole temporary directory and compare canonical source paths. Co-Authored-By: Claude Opus 5.5 --- docs/articles/README.md | 5 ++- docs/articles/sync-evidence.mjs | 10 +++-- test/article-evidence.test.ts | 73 +++++++++++++++++++++++---------- 3 files changed, 61 insertions(+), 27 deletions(-) diff --git a/docs/articles/README.md b/docs/articles/README.md index c854ad94a..b5e5edbb2 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -144,7 +144,7 @@ A refresh measures both numbers and qualitative claims. A design diff alone cann ### Step 1: Install and build -Use Node `^20.19.0 || ^22.12.0 || >=24.0.0`, where the ranges declared by the installed test toolchain (Vitest, Vite and Rolldown) overlap. This source-development requirement is separate from the CLI's declared Node `>=20` runtime floor. +Use Node `^20.19.0 || ^22.12.0 || >=24.0.0`, where the ranges declared by the installed test toolchain (Vitest, Vite and Rolldown) overlap. This source-development requirement is separate from the CLI's runtime range, declared in the checkout's `package.json` under `engines.node`. ```sh npm ci @@ -245,6 +245,7 @@ Mermaid stays live source. Markdown tables come from graph queries. Screenshots ### Step 5: Check version-sensitive prose +- Recheck runtime and engines statements outside evidence blocks, including Step 1 and Part 1, against the new pin's `package.json` and installed toolchain. - Recount recipes and skills in the [generated baseline metrics](#step-3-re-derive-corpus-metrics). - Check `sdp q` bindings. The current sink injects `g`, `graph`, `report`, `params` and `recipes`. - Re-read the Studio's carrying Specs and publish a current page set. Check lenses, roadmap, registers, queue, diff, runs, export and composing panel rather than saying "phase 0" from memory. @@ -257,4 +258,4 @@ Mermaid stays live source. Markdown tables come from graph queries. Screenshots ### Step 6: Verify -Run `npm run check:articles -- "$SOURCE_DIR"` and `npm run check` after staging new article files so preflight can see them. CI runs the same article check with full git history, using its disposable build of the pinned source, so a manuscript branch behind the pin cannot merge stale evidence green. Then manually inspect the rendered Markdown, relative links and commit-pinned links. The main gate does not check those links or all Markdown fence structure. +Run `npm run check:articles -- "$SOURCE_DIR"` and `npm run check` after staging new article files so preflight can see them. CI runs the same article check with full git history, using its disposable build of the pinned source, so a manuscript branch behind the pin cannot merge stale evidence green. Check relative link target paths with the repository's Markdown link gate when available; it covers article paths under `docs/`. Manually inspect the rendered Markdown, link fragments, commit-pinned URLs and code fence structure, which a repository-path check does not establish. diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index 8f174d386..784a2ad0b 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -43,7 +43,8 @@ export function evidenceMarkers(original, file) { const run = delimiter[1]; const rest = line.slice(delimiter[0].length); if (!fence) fence = run; - else if (run[0] === fence[0] && run.length >= fence.length && /^[ \t]*\r?$/.test(rest)) fence = undefined; + else if (run[0] === fence[0] && run.length >= fence.length && /^[ \t]*\r?$/.test(rest)) + fence = undefined; } offset += line.length + 1; } @@ -470,7 +471,8 @@ async function supervise(repository) { child.once("error", reject); child.once("close", (code) => resolve(code)); }); - process.exitCode = interrupted === "SIGINT" ? 130 : interrupted === "SIGTERM" ? 143 : (code ?? 1); + process.exitCode = + interrupted === "SIGINT" ? 130 : interrupted === "SIGTERM" ? 143 : (code ?? 1); } finally { try { // A killed worker cannot run its finally. Remove only this invocation's @@ -478,7 +480,9 @@ async function supervise(repository) { for (const name of readdirSync(temporary)) { const source = path.join(temporary, name, "source"); if (existsSync(source)) { - execFileSync("git", ["-C", repository, "worktree", "remove", "--force", source], { stdio: "pipe" }); + execFileSync("git", ["-C", repository, "worktree", "remove", "--force", source], { + stdio: "pipe", + }); } } rmSync(temporary, { recursive: true, force: true }); diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index 58a0c6cdc..c24034ef2 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -8,6 +8,7 @@ import { readFileSync, readdirSync, readlinkSync, + realpathSync, rmSync, statSync, symlinkSync, @@ -288,13 +289,16 @@ describe("article evidence CLI entry", () => { }); child.kill(signal); await expect - .poll(() => child?.exitCode !== null || child?.signalCode !== null, { timeout: 5_000 }) + .poll(() => child?.exitCode !== null || child.signalCode !== null, { timeout: 5_000 }) .toBe(true); expect.soft(await exited, stderr).toBe(signal === "SIGINT" ? 130 : 143); expect.soft(gate.status, gate.stdout + gate.stderr).toBe(0); - expect.soft(path.relative(repo, workerSource).startsWith(".." + path.sep)).toBe(true); + expect + .soft(path.relative(realpathSync(repo), workerSource).startsWith(".." + path.sep)) + .toBe(true); expect.soft(git("status", "--porcelain", "--untracked-files=all")).toBe(before); expect.soft(existsSync(workerSource)).toBe(false); + expect(existsSync(path.dirname(path.dirname(workerSource)))).toBe(false); expect(git("worktree", "list", "--porcelain").match(/^worktree /gm)).toHaveLength(1); await expect .poll( @@ -410,7 +414,7 @@ describe("article evidence CLI entry", () => { } finally { rmSync(temporary, { recursive: true, force: true }); } - }); + }, 30_000); }); // Import the CLI's helpers directly: an import must not validate a checkout or regenerate files. @@ -590,25 +594,30 @@ describe("article evidence structure", () => { } }); - it("renders the entire drift recipe answer including firstUnmetClause", () => { - const expected = execFileSync( - process.execPath, - [ - path.join(repositoryRoot, "dist/cli/sdp.js"), - "q", - "return recipes(2)", - "--root", - "examples/checkout-v1", - "--json", - ], - { cwd: repositoryRoot, encoding: "utf8" }, - ); - const block = evidenceBlocks(repositoryRoot, "pin").find( - (entry) => entry.id === "example-drift", - ); - if (!block) throw new Error("Missing drift generator"); - expect(JSON.parse(block.generate())).toEqual(JSON.parse(expected)); - expect(block.generate()).toContain('"firstUnmetClause"'); + it.each([ + [ + "nonempty", + { + total: 1, + alarms: [{ id: "spec:probe.drift", futureDetail: { clause: "not-ready" } }], + extra: "preserve me", + }, + ], + ["empty", { total: 0, alarms: [] }], + ])("renders the entire %s drift recipe answer independently of HEAD", (_name, answer) => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-drift-")); + try { + mkdirSync(path.join(temporary, "dist/cli"), { recursive: true }); + writeFileSync( + path.join(temporary, "dist/cli/sdp.js"), + `console.log(${JSON.stringify(JSON.stringify(answer))});`, + ); + const block = evidenceBlocks(temporary, "pin").find((entry) => entry.id === "example-drift"); + if (!block) throw new Error("Missing drift generator"); + expect(block.generate()).toBe(JSON.stringify(answer, null, 2)); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } }); it("moves measurement labels with the pin inside both generated blocks", () => { @@ -678,6 +687,25 @@ describe("article evidence arguments", () => { expect(readFileSync(article, "utf8")).toBe(before); }); + it("ties refresh runtime guidance to checkout engines and rechecks pinned prose", () => { + const playbook = readFileSync(path.join(repositoryRoot, "docs/articles/README.md"), "utf8"); + const install = playbook.split("### Step 1:")[1]?.split("### Step 2:")[0]; + expect(install).toContain("`package.json`"); + expect(install).toContain("`engines.node`"); + expect(install).not.toContain("CLI's declared Node `>=20`"); + expect(playbook).toContain( + "runtime and engines statements outside evidence blocks, including Step 1 and Part 1", + ); + }); + + it("distinguishes repository-path link checks from manual publication review", () => { + const playbook = readFileSync(path.join(repositoryRoot, "docs/articles/README.md"), "utf8"); + const verify = playbook.split("### Step 6:")[1]; + expect(verify).not.toContain("main gate does not check those links"); + expect(verify).toContain("Markdown link gate"); + expect(verify).toContain("link fragments, commit-pinned URLs and code fence structure"); + }); + it("does not offer an implicit source repository", () => { const playbook = readFileSync(path.join(repositoryRoot, "docs/articles/README.md"), "utf8"); expect(playbook).not.toContain("omit `$SOURCE_DIR`"); @@ -869,5 +897,6 @@ describe("pinned evidence source", () => { rmSync(temporary, { recursive: true, force: true }); } }, + 30_000, ); }); From 7b37251407f3ed32b38da59067af480fcc9c9c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 02:29:19 +0100 Subject: [PATCH 32/36] fix(articles): recover interrupted worktrees and batch pinned blob reads - F-pr34-claude-1: make cancellation idempotent, tolerate vanished worker groups, and remove partial or locked worktrees before pruning. Refuse temporary roots inside either checkout and exercise npm signal forwarding. - F-sweep2-6: spool one git blob batch and copy bounded chunks, preserving object, type, size and terminator checks without an aggregate buffer cap or a process per blob. Keep the 144 MiB regression. - F-sweep2-5: label invisible fence tails explicitly in test output. - F-pr34-claude-2, F-pr34-claude-3: name the Markdown link gate and avoid pinning incidental playbook phrasing in its regression tests. Review fixes for PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/articles/README.md | 4 +- docs/articles/sync-evidence.d.mts | 7 + docs/articles/sync-evidence.mjs | 123 ++++++++++++---- test/article-evidence.test.ts | 233 +++++++++++++++++++++++++++--- 4 files changed, 312 insertions(+), 55 deletions(-) diff --git a/docs/articles/README.md b/docs/articles/README.md index b5e5edbb2..3eddce7d0 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -237,7 +237,7 @@ node docs/articles/sync-evidence.mjs "$SOURCE_DIR" node docs/articles/sync-evidence.mjs --check "$SOURCE_DIR" ``` -Both commands create a worktree in the system temporary directory (keep `TMPDIR` outside either checkout), install dependencies, build `dist/`, and regenerate `generated/`, test registrars and the example's generated outputs there. They temporarily register the worktree in `$SOURCE_DIR`'s Git metadata and remove it on success, failure, SIGINT or SIGTERM; they do not overwrite outputs in `$SOURCE_DIR`. Installing dependencies may populate npm's cache and needs registry access or a populated cache. Sync mode also writes the marked manuscript blocks. Check mode writes no manuscript. On interruption, the supervising process stops the worker and its child processes before removing the worktree. SIGKILL and machine failure cannot run cleanup; remove any surviving worktree with `git worktree remove --force ` from the source repository. +Both commands create a worktree in the system temporary directory (they refuse a `TMPDIR` inside either checkout), install dependencies, build `dist/`, and regenerate `generated/`, test registrars and the example's generated outputs there. They temporarily register the worktree in `$SOURCE_DIR`'s Git metadata and remove it on success, failure, SIGINT or SIGTERM; they do not overwrite outputs in `$SOURCE_DIR`. Installing dependencies may populate npm's cache and needs registry access or a populated cache. Sync mode also writes the marked manuscript blocks. Check mode writes no manuscript. On interruption, the supervising process stops the worker and its child processes before removing the worktree. SIGKILL and machine failure cannot run cleanup; remove any surviving worktree with `git worktree remove --force ` from the source repository. Always pass the source repository explicitly. Add a marked block and its generator whenever a manuscript gains a reusable output excerpt. Missing, duplicate, unknown, nested or fenced markers fail the check. Keep elision explicit, as in the constant-anchor excerpt. Do not paste a modified version of source or command output and call it an example. @@ -258,4 +258,4 @@ Mermaid stays live source. Markdown tables come from graph queries. Screenshots ### Step 6: Verify -Run `npm run check:articles -- "$SOURCE_DIR"` and `npm run check` after staging new article files so preflight can see them. CI runs the same article check with full git history, using its disposable build of the pinned source, so a manuscript branch behind the pin cannot merge stale evidence green. Check relative link target paths with the repository's Markdown link gate when available; it covers article paths under `docs/`. Manually inspect the rendered Markdown, link fragments, commit-pinned URLs and code fence structure, which a repository-path check does not establish. +Run `npm run check:articles -- "$SOURCE_DIR"` and `npm run check` after staging new article files so preflight can see them. CI runs the same article check with full git history, using its disposable build of the pinned source, so a manuscript branch behind the pin cannot merge stale evidence green. On checkouts with `test/markdown-links.test.ts`, `npm run check` runs that Markdown link gate: it checks that relative link target paths exist, including article paths under `docs/`. Manually inspect the rendered Markdown, link fragments, commit-pinned URLs and code fence structure, which a repository-path check does not establish. diff --git a/docs/articles/sync-evidence.d.mts b/docs/articles/sync-evidence.d.mts index 1dc6bfcd6..faaf3a907 100644 --- a/docs/articles/sync-evidence.d.mts +++ b/docs/articles/sync-evidence.d.mts @@ -30,3 +30,10 @@ export function evidenceBlocks( sourceRoot: string, pinnedCommit: string, ): { id: string; file: string; language: string | null; generate: () => string }[]; + +export function cleanupPinnedSource(repository: string, temporary: string): void; +export function materializeTree( + git: (args: string[], input?: string, output?: number) => Buffer, + commit: string, + source: string, +): void; diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index 784a2ad0b..d5c2720eb 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -6,11 +6,13 @@ import { mkdtempSync, openSync, readFileSync, + readSync, readdirSync, realpathSync, rmSync, symlinkSync, writeFileSync, + writeSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -101,7 +103,7 @@ export function parseArguments(args) { // Read raw blobs rather than checking out files: sparse patterns, attributes and filter // drivers belong to the supplied repository, not the pin. Preserve binary bytes and modes. -function materializeTree(git, commit, source) { +export function materializeTree(git, commit, source) { const entries = git(["ls-tree", "-r", "-z", commit]) .toString("utf8") .split("\0") @@ -114,20 +116,83 @@ function materializeTree(git, commit, source) { throw new Error(`Unsupported pinned tree entry: ${mode} ${file}`); return { mode, oid, file }; }); - for (const { mode, oid, file } of entries) { - const target = path.join(source, file); - mkdirSync(path.dirname(target), { recursive: true }); - if (mode === "120000") symlinkSync(git(["cat-file", "blob", oid]), target); - else { - // Direct stdout to the file: neither the tree nor a single large blob needs - // to fit in execFileSync's output buffer. - const output = openSync(target, "w", mode === "100755" ? 0o755 : 0o644); + // Spool one batch outside the source tree; neither total nor individual blob size + // is bounded by execFileSync's output buffer. Read bodies in bounded chunks. + const batchPath = path.join(path.dirname(source), "blobs.batch"); + const batch = openSync(batchPath, "w+"); + try { + git(["cat-file", "--batch"], entries.map(({ oid }) => `${oid}\n`).join(""), batch); + let offset = 0; + const buffer = Buffer.alloc(64 * 1024); + const read = (length) => { + const count = readSync(batch, buffer, 0, length, offset); + if (count === 0) throw new Error("Truncated pinned blob batch."); + offset += count; + return buffer.subarray(0, count); + }; + for (const { mode, oid, file } of entries) { + const count = readSync(batch, buffer, 0, 256, offset); + const newline = buffer.subarray(0, count).indexOf(10); + if (newline < 0) throw new Error(`Invalid pinned blob header: ${file}`); + const header = buffer.subarray(0, newline).toString("ascii"); + const match = /^([0-9a-f]+) blob (0|[1-9][0-9]*)$/.exec(header); + const size = Number(match?.[2]); + if (match?.[1] !== oid || !Number.isSafeInteger(size)) + throw new Error(`Invalid pinned blob header: ${file}`); + offset += newline + 1; + const target = path.join(source, file); + mkdirSync(path.dirname(target), { recursive: true }); + const link = mode === "120000" ? Buffer.alloc(size) : undefined; + const output = link ? undefined : openSync(target, "w", mode === "100755" ? 0o755 : 0o644); try { - git(["cat-file", "blob", oid], undefined, output); + let copied = 0; + while (copied < size) { + const chunk = read(Math.min(buffer.length, size - copied)); + if (link) chunk.copy(link, copied); + else { + let written = 0; + while (written < chunk.length) + written += writeSync(output, chunk, written, chunk.length - written); + } + copied += chunk.length; + } } finally { - closeSync(output); + if (output !== undefined) closeSync(output); } + if (read(1)[0] !== 10) throw new Error(`Invalid pinned blob terminator: ${file}`); + if (link) symlinkSync(link, target); } + if (readSync(batch, buffer, 0, 1, offset) !== 0) + throw new Error("Unexpected trailing pinned blob data."); + } finally { + closeSync(batch); + rmSync(batchPath, { force: true }); + } +} + +// An interrupt can leave a locked registration or remove its .git file first. +// Attempt every cleanup step, including pruning after the directory is gone. +export function cleanupPinnedSource(repository, temporary) { + const source = path.join(temporary, "source"); + const removed = spawnSync( + "git", + ["-C", repository, "worktree", "remove", "--force", "--force", source], + { + stdio: "ignore", + }, + ); + // An initializing registration can be locked even before .git exists. Unlock + // this invocation's entry so pruning can remove it after the fallback deletion. + if (removed.status !== 0) + spawnSync("git", ["-C", repository, "worktree", "unlock", source], { stdio: "ignore" }); + try { + rmSync(temporary, { recursive: true, force: true }); + } catch (error) { + console.error(`Could not remove temporary evidence directory ${temporary}: ${error.message}`); + } finally { + spawnSync("git", ["-C", repository, "worktree", "prune", "--expire", "now"], { + stdio: "ignore", + }); } } @@ -149,10 +214,8 @@ export function withPinnedSource(repository, commit, scratchRoot, action) { mkdirSync(scratchRoot, { recursive: true }); const temporary = mkdtempSync(path.join(scratchRoot, "article-evidence-")); const source = path.join(temporary, "source"); - let added = false; try { git(["worktree", "add", "--detach", "--no-checkout", source, commit]); - added = true; materializeTree(git, commit, source); execFileSync("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], { cwd: source, @@ -162,11 +225,7 @@ export function withPinnedSource(repository, commit, scratchRoot, action) { execFileSync("npm", ["run", "build"], { cwd: source, env, stdio: "pipe" }); return action(source); } finally { - try { - if (added) git(["worktree", "remove", "--force", source]); - } finally { - rmSync(temporary, { recursive: true, force: true }); - } + cleanupPinnedSource(root, temporary); } } @@ -441,8 +500,14 @@ export function evidenceBlocks(sourceRoot, pinnedCommit) { // Keep cancellation in a responsive parent while the worker runs synchronous git, // npm and evidence commands. A separate process group includes npm's descendants. -async function supervise(repository) { - const temporary = realpathSync(mkdtempSync(path.join(tmpdir(), "article-evidence-"))); +async function supervise(repository, repositoryRoot) { + const scratchRoot = realpathSync(tmpdir()); + for (const root of [repository, repositoryRoot]) { + const relative = path.relative(realpathSync(root), scratchRoot); + if (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) + throw new Error("TMPDIR must be outside the article checkout and the source repository."); + } + const temporary = realpathSync(mkdtempSync(path.join(scratchRoot, "article-evidence-"))); let interrupted; const child = fork(fileURLToPath(import.meta.url), process.argv.slice(2), { detached: process.platform !== "win32", @@ -450,7 +515,8 @@ async function supervise(repository) { env: { ...process.env, SDP_ARTICLE_WORKER_ROOT: temporary }, }); const stop = (signal) => { - interrupted ??= signal; + if (interrupted) return; + interrupted = signal; if (!child.pid) return; if (process.platform === "win32") { spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }); @@ -458,7 +524,8 @@ async function supervise(repository) { try { process.kill(-child.pid, "SIGKILL"); } catch (error) { - if (error.code !== "ESRCH") throw error; + if (error.code !== "ESRCH" && error.code !== "EPERM") + console.error(`Could not stop evidence worker: ${error.message}`); } } }; @@ -477,14 +544,8 @@ async function supervise(repository) { try { // A killed worker cannot run its finally. Remove only this invocation's // registrations, after its process group has stopped writing. - for (const name of readdirSync(temporary)) { - const source = path.join(temporary, name, "source"); - if (existsSync(source)) { - execFileSync("git", ["-C", repository, "worktree", "remove", "--force", source], { - stdio: "pipe", - }); - } - } + for (const name of readdirSync(temporary)) + cleanupPinnedSource(repository, path.join(temporary, name)); rmSync(temporary, { recursive: true, force: true }); } finally { process.off("SIGINT", onInterrupt); @@ -526,7 +587,7 @@ async function main() { ); validateEvidenceMarkers(documents, evidenceBlocks(repositoryRoot, pinnedCommit)); if (!process.send || !process.env.SDP_ARTICLE_WORKER_ROOT) { - return supervise(sourceArgument); + return supervise(sourceArgument, repositoryRoot); } return withPinnedSource( sourceArgument, diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index c24034ef2..1c629c349 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -21,8 +21,10 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { + cleanupPinnedSource, evidenceBlocks, evidenceMarkers, + materializeTree, measurementLabel, parseArguments, replaceEvidenceBlock, @@ -193,9 +195,45 @@ describe("article evidence CLI entry", () => { } }); - it.each(["SIGINT", "SIGTERM"] as const)( - "stops descendants and removes the temporary registration on %s while gates can run", - async (signal) => { + it.each(["articles", "source"])("refuses TMPDIR inside the %s checkout", (location) => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-tmpdir-")); + const articles = path.join(temporary, "articles/docs/articles"); + const repo = path.join(temporary, "source"); + try { + mkdirSync(articles, { recursive: true }); + mkdirSync(repo); + for (const name of readdirSync(path.join(repositoryRoot, "docs/articles"))) { + if (name.endsWith(".md") || name === "sync-evidence.mjs") + copyFileSync(path.join(repositoryRoot, "docs/articles", name), path.join(articles, name)); + } + const scratch = path.join(temporary, location, "tmp"); + mkdirSync(scratch); + const result = spawnSync( + process.execPath, + [path.join(articles, "sync-evidence.mjs"), "--check", repo], + { + encoding: "utf8", + env: { ...process.env, TMPDIR: scratch, TEMP: scratch, TMP: scratch }, + }, + ); + expect(result.status).toBe(1); + expect(result.stderr).toContain("TMPDIR must be outside"); + expect(readdirSync(scratch)).toEqual([]); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }); + + it.each([ + ["direct", "SIGINT"], + ["direct", "SIGTERM"], + ["npm", "SIGINT"], + ["npm", "SIGTERM"], + ["repeated signal", "SIGINT"], + ["worker group EPERM", "SIGINT"], + ] as const)( + "%s stops descendants and removes the temporary registration on %s while gates can run", + async (invocation, signal) => { const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-cancel-")); const repo = path.join(temporary, "repository"); const articles = path.join(repo, "docs/articles"); @@ -213,7 +251,10 @@ describe("article evidence CLI entry", () => { JSON.stringify({ name: "cancel-probe", version: "1.0.0", - scripts: { build: "node build.cjs" }, + scripts: { + build: "node build.cjs", + "check:articles": "node docs/articles/sync-evidence.mjs --check", + }, }), ); writeFileSync( @@ -262,11 +303,49 @@ describe("article evidence CLI entry", () => { `\nold\n\n`, ); for (const [file, text] of documents) writeFileSync(path.join(articles, file), text); + // Model macOS returning EPERM for an unreaped worker group, and a wrapper + // delivering its forwarded signal before the first handler has returned. + const hook = path.join(temporary, "signal-hook.mjs"); + const killAttempts = path.join(temporary, "kill-attempts"); + const injectSignal = + invocation === "repeated signal" || invocation === "worker group EPERM"; + if (injectSignal) + writeFileSync( + hook, + ` + import {appendFileSync} from "node:fs"; + const kill = process.kill.bind(process); + let killed = false; + process.kill = (pid, signal) => { + if (pid >= 0 || signal !== "SIGKILL") return kill(pid, signal); + appendFileSync(${JSON.stringify(killAttempts)}, "kill\\n"); + if (!killed) { + killed = true; + const result = kill(pid, signal); + if (${JSON.stringify(invocation)} !== "worker group EPERM") return result; + } + throw Object.assign(new Error("kill EPERM"), {code: "EPERM"}); + }; + if (${JSON.stringify(invocation)} === "repeated signal") { + const on = process.on.bind(process); + process.on = (event, listener) => on(event, event === "SIGINT" ? () => { + listener(); listener(); + } : listener); + } + `, + ); const before = git("status", "--porcelain", "--untracked-files=all"); child = spawn( - process.execPath, - [path.join(articles, "sync-evidence.mjs"), "--check", repo], - { stdio: "pipe", detached: process.platform !== "win32" }, + invocation === "npm" ? "npm" : process.execPath, + invocation === "npm" + ? ["run", "--silent", "check:articles", "--", repo] + : [ + ...(injectSignal ? ["--import", hook] : []), + path.join(articles, "sync-evidence.mjs"), + "--check", + repo, + ], + { cwd: repo, stdio: "pipe", detached: process.platform !== "win32" }, ); let stderr = ""; child.stderr?.on("data", (data: Buffer) => { @@ -287,11 +366,15 @@ describe("article evidence CLI entry", () => { const gate = spawnSync(process.execPath, [path.join(repo, "check-temporal.mjs")], { encoding: "utf8", }); - child.kill(signal); + // Ctrl-C reaches npm and the supervisor; npm forwards it a second time. + if (invocation === "npm" && child.pid && process.platform !== "win32") + process.kill(-child.pid, signal); + else child.kill(signal); await expect .poll(() => child?.exitCode !== null || child.signalCode !== null, { timeout: 5_000 }) .toBe(true); expect.soft(await exited, stderr).toBe(signal === "SIGINT" ? 130 : 143); + if (injectSignal) expect(readFileSync(killAttempts, "utf8")).toBe("kill\n"); expect.soft(gate.status, gate.stdout + gate.stderr).toBe(0); expect .soft(path.relative(realpathSync(repo), workerSource).startsWith(".." + path.sep)) @@ -328,8 +411,10 @@ describe("article evidence CLI entry", () => { /* already stopped */ } } - if (workerSource && existsSync(workerSource)) - spawnSync("git", ["-C", repo, "worktree", "remove", "--force", workerSource]); + if (workerSource) { + cleanupPinnedSource(repo, path.dirname(workerSource)); + rmSync(path.dirname(path.dirname(workerSource)), { recursive: true, force: true }); + } rmSync(temporary, { recursive: true, force: true }); } }, @@ -533,14 +618,17 @@ describe("article evidence structure", () => { }); for (const fence of ["```", "~~~"]) { - it.each(["\u2028", "\u2029", "\u00a0", "\v", "\f"])( - `keeps ${fence} open with a non-CommonMark closing tail %j`, - (tail) => { - expect(() => - evidenceMarkers(`${fence}\ncode\n${fence}${tail}\n${probeText}`, "article.md"), - ).toThrow("malformed or fenced evidence marker"); - }, - ); + it.each([ + ["U+2028", "\u2028"], + ["U+2029", "\u2029"], + ["NBSP", "\u00a0"], + ["vertical tab", "\v"], + ["form feed", "\f"], + ])(`keeps ${fence} open with a non-CommonMark closing tail %s`, (_label, tail) => { + expect(() => + evidenceMarkers(`${fence}\ncode\n${fence}${tail}\n${probeText}`, "article.md"), + ).toThrow("malformed or fenced evidence marker"); + }); it.each(["", " ", "\t", " \t\r"])( `closes ${fence} with a CommonMark closing tail %j`, (tail) => { @@ -693,9 +781,6 @@ describe("article evidence arguments", () => { expect(install).toContain("`package.json`"); expect(install).toContain("`engines.node`"); expect(install).not.toContain("CLI's declared Node `>=20`"); - expect(playbook).toContain( - "runtime and engines statements outside evidence blocks, including Step 1 and Part 1", - ); }); it("distinguishes repository-path link checks from manual publication review", () => { @@ -703,7 +788,6 @@ describe("article evidence arguments", () => { const verify = playbook.split("### Step 6:")[1]; expect(verify).not.toContain("main gate does not check those links"); expect(verify).toContain("Markdown link gate"); - expect(verify).toContain("link fragments, commit-pinned URLs and code fence structure"); }); it("does not offer an implicit source repository", () => { @@ -715,6 +799,111 @@ describe("article evidence arguments", () => { // Small real git repositories exercise materialization, npm's lockfile install and the build // without reinstalling the Protocol's full dependency tree for every regression probe. describe("pinned evidence source", () => { + it.each(["missing .git", "locked as initializing", "missing .git while locked"])( + "cleans up a worktree %s", + (state) => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-cleanup-")); + const repo = path.join(temporary, "repository"); + const disposable = path.join(temporary, "disposable"); + const source = path.join(disposable, "source"); + const git = (...args: string[]) => + execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: "pipe" }).trim(); + try { + mkdirSync(repo); + git("init", "-q"); + git( + "-c", + "user.name=Test", + "-c", + "user.email=test@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-qm", + "pin", + ); + git("worktree", "add", "--detach", source, "HEAD"); + if (state !== "missing .git") git("worktree", "lock", "--reason", "initializing", source); + if (state !== "locked as initializing") rmSync(path.join(source, ".git")); + cleanupPinnedSource(repo, disposable); + expect(existsSync(disposable)).toBe(false); + expect(git("worktree", "list", "--porcelain").match(/^worktree /gm)).toHaveLength(1); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }, + ); + + it("materializes many blobs with one batch process", () => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-batch-")); + const repo = path.join(temporary, "repository"); + const source = path.join(temporary, "source"); + const git = (args: string[], input?: string, output?: number) => + execFileSync("git", ["-C", repo, ...args], { + input, + stdio: ["pipe", output ?? "pipe", "pipe"], + }); + try { + mkdirSync(repo); + mkdirSync(source); + git(["init", "-q"]); + for (let i = 0; i < 80; i++) + writeFileSync(path.join(repo, `${String(i)}.bin`), Buffer.from([i, 0, 255, 10])); + git(["add", "--all"]); + const tree = git(["write-tree"]).toString().trim(); + const calls: string[][] = []; + materializeTree( + (args, input, output) => { + calls.push(args); + return git(args, input, output); + }, + tree, + source, + ); + for (let i = 0; i < 80; i++) + expect(readFileSync(path.join(source, `${String(i)}.bin`))).toEqual( + Buffer.from([i, 0, 255, 10]), + ); + expect(calls.filter((args) => args[0] === "cat-file")).toEqual([["cat-file", "--batch"]]); + expect(readdirSync(temporary).sort()).toEqual(["repository", "source"]); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }); + + it.each([ + ["oid", `${"b".repeat(40)} blob 1\nx\n`], + ["type", `${"a".repeat(40)} tree 1\nx\n`], + ["size", `${"a".repeat(40)} blob -1\nx\n`], + ["unsafe size", `${"a".repeat(40)} blob 9007199254740992\nx\n`], + ["truncated header", "unfinished"], + ["truncated body", `${"a".repeat(40)} blob 9\nx`], + ["terminator", `${"a".repeat(40)} blob 1\nxx`], + ["trailing data", `${"a".repeat(40)} blob 1\nx\nextra`], + ])("refuses a batch with invalid %s and removes its spool", (_name, batch) => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-batch-invalid-")); + const source = path.join(temporary, "source"); + try { + mkdirSync(source); + expect(() => { + materializeTree( + (args, _input, output) => { + if (args[0] === "ls-tree") return Buffer.from(`100644 blob ${"a".repeat(40)}\tfile\0`); + if (output === undefined) throw new Error("Expected a batch output descriptor"); + writeFileSync(output, batch); + return Buffer.alloc(0); + }, + "pin", + source, + ); + }).toThrow(/pinned blob/); + expect(existsSync(path.join(temporary, "blobs.batch"))).toBe(false); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }); + it("materializes blobs totaling more than the former batch buffer", () => { const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-large-")); const repo = path.join(temporary, "repository"); From 35c3f072df5d066e81f44570510ccfec44e7ca29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 02:53:09 +0100 Subject: [PATCH 33/36] fix(articles): preserve unrelated worktree registrations during cleanup F-pr34-claude-1: replace repository-wide pruning with removal of only the disposable worktree registration, including interrupted and locked entries. Preserve repairable worktrees and their staged contents. Install interrupt handlers before creating temporary directories or starting the worker. Cover both startup boundaries, clarify detached-worker behavior after SIGKILL, and distinguish forwarding coverage from deterministic signal regression tests. Second review pass over PRs #34, #38 and #39; the diff is reviewed by the other model family. Co-Authored-By: Claude Opus 5.5 --- docs/articles/README.md | 2 +- docs/articles/sync-evidence.mjs | 93 ++++++++++++----- test/article-evidence.test.ts | 170 +++++++++++++++++++++++++------- 3 files changed, 204 insertions(+), 61 deletions(-) diff --git a/docs/articles/README.md b/docs/articles/README.md index 3eddce7d0..81c7143ba 100644 --- a/docs/articles/README.md +++ b/docs/articles/README.md @@ -237,7 +237,7 @@ node docs/articles/sync-evidence.mjs "$SOURCE_DIR" node docs/articles/sync-evidence.mjs --check "$SOURCE_DIR" ``` -Both commands create a worktree in the system temporary directory (they refuse a `TMPDIR` inside either checkout), install dependencies, build `dist/`, and regenerate `generated/`, test registrars and the example's generated outputs there. They temporarily register the worktree in `$SOURCE_DIR`'s Git metadata and remove it on success, failure, SIGINT or SIGTERM; they do not overwrite outputs in `$SOURCE_DIR`. Installing dependencies may populate npm's cache and needs registry access or a populated cache. Sync mode also writes the marked manuscript blocks. Check mode writes no manuscript. On interruption, the supervising process stops the worker and its child processes before removing the worktree. SIGKILL and machine failure cannot run cleanup; remove any surviving worktree with `git worktree remove --force ` from the source repository. +Both commands create a worktree in the system temporary directory (they refuse a `TMPDIR` inside either checkout), install dependencies, build `dist/`, and regenerate `generated/`, test registrars and the example's generated outputs there. They temporarily register the worktree in `$SOURCE_DIR`'s Git metadata and remove only their own directory and registration on success, failure, SIGINT or SIGTERM; they do not overwrite outputs in `$SOURCE_DIR`. Installing dependencies may populate npm's cache and needs registry access or a populated cache. Sync mode also writes the marked manuscript blocks. Check mode writes no manuscript. On interruption, the supervising process stops the worker and its child processes before removing the worktree. SIGKILL of the supervisor leaves the detached worker running: it finishes and cleans up its own worktree, and sync mode can still write manuscript blocks. Wait for that worker to stop before removing anything. After it stops, or after a machine failure, remove any surviving worktree with `git worktree remove --force ` from the source repository. Always pass the source repository explicitly. Add a marked block and its generator whenever a manuscript gains a reusable output excerpt. Missing, duplicate, unknown, nested or fenced markers fail the check. Keep elision explicit, as in the constant-anchor excerpt. Do not paste a modified version of source or command output and call it an example. diff --git a/docs/articles/sync-evidence.mjs b/docs/articles/sync-evidence.mjs index d5c2720eb..eaa58816f 100644 --- a/docs/articles/sync-evidence.mjs +++ b/docs/articles/sync-evidence.mjs @@ -170,10 +170,20 @@ export function materializeTree(git, commit, source) { } } +// Resolve aliases even when an interrupted cleanup already removed the leaf. +function realpathWithMissingTail(file) { + try { + return realpathSync(file); + } catch (error) { + if (error.code !== "ENOENT") throw error; + return path.join(realpathWithMissingTail(path.dirname(file)), path.basename(file)); + } +} + // An interrupt can leave a locked registration or remove its .git file first. -// Attempt every cleanup step, including pruning after the directory is gone. +// Never prune the repository: other missing worktrees may still be recoverable. export function cleanupPinnedSource(repository, temporary) { - const source = path.join(temporary, "source"); + const source = realpathWithMissingTail(path.resolve(temporary, "source")); const removed = spawnSync( "git", ["-C", repository, "worktree", "remove", "--force", "--force", source], @@ -181,18 +191,35 @@ export function cleanupPinnedSource(repository, temporary) { stdio: "ignore", }, ); - // An initializing registration can be locked even before .git exists. Unlock - // this invocation's entry so pruning can remove it after the fallback deletion. - if (removed.status !== 0) - spawnSync("git", ["-C", repository, "worktree", "unlock", source], { stdio: "ignore" }); try { rmSync(temporary, { recursive: true, force: true }); } catch (error) { console.error(`Could not remove temporary evidence directory ${temporary}: ${error.message}`); - } finally { - spawnSync("git", ["-C", repository, "worktree", "prune", "--expire", "now"], { - stdio: "ignore", - }); + } + if (removed.status !== 0) { + try { + const common = execFileSync( + "git", + ["-C", repository, "rev-parse", "--path-format=absolute", "--git-common-dir"], + { encoding: "utf8", stdio: "pipe" }, + ).trim(); + const registrations = path.join(common, "worktrees"); + if (existsSync(registrations)) { + for (const name of readdirSync(registrations)) { + const entry = path.join(registrations, name); + try { + const gitdir = readFileSync(path.join(entry, "gitdir"), "utf8").trim(); + if (realpathWithMissingTail(gitdir) === path.join(source, ".git")) + rmSync(entry, { recursive: true, force: true }); + } catch (error) { + // A concurrent worktree removal may have already removed this entry. + if (error.code !== "ENOENT") throw error; + } + } + } + } catch (error) { + console.error(`Could not remove evidence worktree registration ${source}: ${error.message}`); + } } } @@ -507,17 +534,11 @@ async function supervise(repository, repositoryRoot) { if (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) throw new Error("TMPDIR must be outside the article checkout and the source repository."); } - const temporary = realpathSync(mkdtempSync(path.join(scratchRoot, "article-evidence-"))); + let temporary; + let child; let interrupted; - const child = fork(fileURLToPath(import.meta.url), process.argv.slice(2), { - detached: process.platform !== "win32", - stdio: ["inherit", "inherit", "inherit", "ipc"], - env: { ...process.env, SDP_ARTICLE_WORKER_ROOT: temporary }, - }); - const stop = (signal) => { - if (interrupted) return; - interrupted = signal; - if (!child.pid) return; + const stopChild = () => { + if (!child?.pid) return; if (process.platform === "win32") { spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }); } else { @@ -529,24 +550,42 @@ async function supervise(repository, repositoryRoot) { } } }; + const stop = (signal) => { + if (interrupted) return; + interrupted = signal; + stopChild(); + }; const onInterrupt = () => stop("SIGINT"); const onTerminate = () => stop("SIGTERM"); process.on("SIGINT", onInterrupt); process.on("SIGTERM", onTerminate); try { - const code = await new Promise((resolve, reject) => { - child.once("error", reject); - child.once("close", (code) => resolve(code)); - }); + temporary = realpathSync(mkdtempSync(path.join(scratchRoot, "article-evidence-"))); + let code; + if (!interrupted) { + child = fork(fileURLToPath(import.meta.url), process.argv.slice(2), { + detached: process.platform !== "win32", + stdio: ["inherit", "inherit", "inherit", "ipc"], + env: { ...process.env, SDP_ARTICLE_WORKER_ROOT: temporary }, + }); + // A signal during fork is remembered until the worker's pid is available. + if (interrupted) stopChild(); + code = await new Promise((resolve, reject) => { + child.once("error", reject); + child.once("close", (code) => resolve(code)); + }); + } process.exitCode = interrupted === "SIGINT" ? 130 : interrupted === "SIGTERM" ? 143 : (code ?? 1); } finally { try { // A killed worker cannot run its finally. Remove only this invocation's // registrations, after its process group has stopped writing. - for (const name of readdirSync(temporary)) - cleanupPinnedSource(repository, path.join(temporary, name)); - rmSync(temporary, { recursive: true, force: true }); + if (temporary) { + for (const name of readdirSync(temporary)) + cleanupPinnedSource(repository, path.join(temporary, name)); + rmSync(temporary, { recursive: true, force: true }); + } } finally { process.off("SIGINT", onInterrupt); process.off("SIGTERM", onTerminate); diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index 1c629c349..50c046c99 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -9,6 +9,7 @@ import { readdirSync, readlinkSync, realpathSync, + renameSync, rmSync, statSync, symlinkSync, @@ -224,6 +225,81 @@ describe("article evidence CLI entry", () => { } }); + it.each([ + ["temporary directory creation", "SIGINT"], + ["worker fork", "SIGTERM"], + ] as const)("cleans up an interrupt during %s", (stage, signal) => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-startup-")); + const articles = path.join(temporary, "articles/docs/articles"); + const scratch = path.join(temporary, "scratch"); + const workerPid = path.join(temporary, "worker-pid"); + try { + mkdirSync(articles, { recursive: true }); + mkdirSync(scratch); + for (const name of readdirSync(path.join(repositoryRoot, "docs/articles"))) { + if (name.endsWith(".md") || name === "sync-evidence.mjs") + copyFileSync(path.join(repositoryRoot, "docs/articles", name), path.join(articles, name)); + } + const hook = path.join(temporary, "startup-hook.mjs"); + writeFileSync( + hook, + ` + import fs from "node:fs"; + import cp from "node:child_process"; + import {syncBuiltinESMExports} from "node:module"; + if (!process.send) { + const interrupt = () => { + // Model delivery at the exact startup boundary, including the default action. + if (!process.emit(${JSON.stringify(signal)})) process.exit(${signal === "SIGINT" ? "130" : "143"}); + }; + if (${JSON.stringify(stage)} === "temporary directory creation") { + const mkdtemp = fs.mkdtempSync; + fs.mkdtempSync = (...args) => { const dir = mkdtemp(...args); interrupt(); return dir; }; + } else { + const fork = cp.fork; + cp.fork = (...args) => { + const child = fork(...args); + fs.writeFileSync(${JSON.stringify(workerPid)}, String(child.pid)); + interrupt(); + return child; + }; + } + syncBuiltinESMExports(); + } + `, + ); + const result = spawnSync( + process.execPath, + [ + "--import", + hook, + path.join(articles, "sync-evidence.mjs"), + "--check", + path.join(temporary, "articles"), + ], + { + encoding: "utf8", + env: { ...process.env, TMPDIR: scratch, TEMP: scratch, TMP: scratch }, + timeout: 5_000, + }, + ); + expect(result.status, result.stderr).toBe(signal === "SIGINT" ? 130 : 143); + expect(readdirSync(scratch)).toEqual([]); + } finally { + if (existsSync(workerPid)) { + try { + const pid = Number(readFileSync(workerPid, "utf8")); + process.kill(process.platform === "win32" ? pid : -pid, "SIGKILL"); + } catch { + /* already stopped */ + } + } + rmSync(temporary, { recursive: true, force: true }); + } + }); + + // The npm cases exercise actual forwarding. The hook cases deterministically + // reproduce the repeated-signal and EPERM regressions that npm may not trigger. it.each([ ["direct", "SIGINT"], ["direct", "SIGTERM"], @@ -799,41 +875,69 @@ describe("article evidence arguments", () => { // Small real git repositories exercise materialization, npm's lockfile install and the build // without reinstalling the Protocol's full dependency tree for every regression probe. describe("pinned evidence source", () => { - it.each(["missing .git", "locked as initializing", "missing .git while locked"])( - "cleans up a worktree %s", - (state) => { - const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-cleanup-")); - const repo = path.join(temporary, "repository"); - const disposable = path.join(temporary, "disposable"); - const source = path.join(disposable, "source"); - const git = (...args: string[]) => - execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: "pipe" }).trim(); - try { - mkdirSync(repo); - git("init", "-q"); - git( - "-c", - "user.name=Test", - "-c", - "user.email=test@example.invalid", - "-c", - "commit.gpgsign=false", - "commit", - "--allow-empty", - "-qm", - "pin", - ); + it.each([ + "healthy", + "missing .git", + "locked as initializing", + "missing .git while locked", + "missing directory", + "never registered", + ])("cleans up a worktree %s without pruning unrelated registrations", (state) => { + const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-cleanup-")); + const repo = path.join(temporary, "repository"); + const disposable = path.join(temporary, "disposable"); + const source = path.join(disposable, "source"); + const git = (...args: string[]) => + execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: "pipe" }).trim(); + try { + mkdirSync(repo); + git("init", "-q"); + git( + "-c", + "user.name=Test", + "-c", + "user.email=test@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-qm", + "pin", + ); + // A moved worktree is prunable until repaired, but its index still owns staged work. + const original = path.join(temporary, "other"); + const moved = path.join(temporary, "moved"); + git("worktree", "add", "-b", "other", original, "HEAD"); + writeFileSync(path.join(original, "staged.txt"), "keep staged contents\n"); + git("-C", original, "add", "staged.txt"); + renameSync(original, moved); + const before = git("worktree", "list", "--porcelain"); + expect(before).toContain("prunable"); + // Exercise --git-common-dir from a linked checkout through a path alias. + const caller = path.join(temporary, "caller"); + const alias = path.join(temporary, "alias"); + git("worktree", "add", "--detach", caller, "HEAD"); + symlinkSync(temporary, alias, "dir"); + if (state !== "never registered") { git("worktree", "add", "--detach", source, "HEAD"); - if (state !== "missing .git") git("worktree", "lock", "--reason", "initializing", source); - if (state !== "locked as initializing") rmSync(path.join(source, ".git")); - cleanupPinnedSource(repo, disposable); - expect(existsSync(disposable)).toBe(false); - expect(git("worktree", "list", "--porcelain").match(/^worktree /gm)).toHaveLength(1); - } finally { - rmSync(temporary, { recursive: true, force: true }); + if (state.includes("locked")) git("worktree", "lock", "--reason", "initializing", source); + if (state.startsWith("missing .git")) rmSync(path.join(source, ".git")); + if (state === "missing directory") rmSync(source, { recursive: true }); } - }, - ); + cleanupPinnedSource(caller, path.join(alias, "disposable")); + expect(existsSync(disposable)).toBe(false); + const after = git("worktree", "list", "--porcelain"); + expect(after.split("\n\n")).toContain( + before.split("\n\n").find((entry) => entry.includes("branch refs/heads/other")), + ); + expect(after.match(/^worktree /gm)).toHaveLength(3); + git("worktree", "repair", moved); + expect(git("-C", moved, "status", "--short")).toBe("A staged.txt"); + expect(git("-C", moved, "show", ":staged.txt")).toBe("keep staged contents"); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + }); it("materializes many blobs with one batch process", () => { const temporary = mkdtempSync(path.join(tmpdir(), "sdp-article-batch-")); From d80749ef66aecd7ec40308c2d4a9c9cee8290db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 03:39:48 +0100 Subject: [PATCH 34/36] test(articles): accept npm's own report of a forwarded signal The npm cases of the interrupt test sent SIGINT or SIGTERM to npm's process group and expected npm to exit 130 or 143. npm 11 does, but on CI's Linux runner (Node 20, npm 10) npm itself ends by the same signal, so its close event reports no exit code and both npm cases failed while every cleanup assertion passed. The npm cases now accept either form of the one signal they sent; the direct cases still require the sync's own exit codes 130 and 143. Co-Authored-By: Claude Opus 5.5 --- test/article-evidence.test.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/test/article-evidence.test.ts b/test/article-evidence.test.ts index 50c046c99..01f5068ed 100644 --- a/test/article-evidence.test.ts +++ b/test/article-evidence.test.ts @@ -427,10 +427,14 @@ describe("article evidence CLI entry", () => { child.stderr?.on("data", (data: Buffer) => { stderr += data.toString(); }); - const exited = new Promise((resolve, reject) => { - child?.once("error", reject); - child?.once("close", resolve); - }); + const exited = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>( + (resolve, reject) => { + child?.once("error", reject); + child?.once("close", (code, closeSignal) => { + resolve({ code, signal: closeSignal }); + }); + }, + ); await expect.poll(() => existsSync(started), { timeout: 15_000 }).toBe(true); const startedData = JSON.parse(readFileSync(started, "utf8")) as { source: string; @@ -449,7 +453,19 @@ describe("article evidence CLI entry", () => { await expect .poll(() => child?.exitCode !== null || child.signalCode !== null, { timeout: 5_000 }) .toBe(true); - expect.soft(await exited, stderr).toBe(signal === "SIGINT" ? 130 : 143); + const outcome = await exited; + const signalExit = signal === "SIGINT" ? 130 : 143; + // The sync itself must exit 128 + n. npm reports its own end differently by + // version: npm 11 exits 128 + n, while npm 10 (Node 20's) dies from the same + // signal, so the wrapper case accepts either form of that one signal. + if (invocation === "npm") + expect + .soft( + outcome.code === signalExit || (outcome.code === null && outcome.signal === signal), + `${JSON.stringify(outcome)}\n${stderr}`, + ) + .toBe(true); + else expect.soft(outcome.code, stderr).toBe(signalExit); if (injectSignal) expect(readFileSync(killAttempts, "utf8")).toBe("kill\n"); expect.soft(gate.status, gate.stdout + gate.stderr).toBe(0); expect From ba2816653f3e1cda0a28b512f9f0c2e10b41e00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 04:01:41 +0100 Subject: [PATCH 35/36] test: give the default-root view test a budget that holds on CI `views the self-hosting corpus from the default repository root` builds and views a copy of the whole self-hosting corpus twice. CI's runner took 15.1 s and 15.5 s on two recent runs, past its 15 s budget, and failed the gate. The budget is now 120 s, the same change PR #40 makes to this test, so the two branches merge cleanly in either order. Co-Authored-By: Claude Opus 5.5 --- test/cli.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli.test.ts b/test/cli.test.ts index e18971304..daae128ff 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -343,7 +343,7 @@ describe("sdp cli", () => { cwd.mockRestore(); rmSync(root, { recursive: true, force: true }); } - }, 15_000); + }, 120_000); it("renders contracts warnings through the one formatter and counts them in the summary", () => { const root = mkdtempSync(join(tmpdir(), "sdp-contracts-warning-")); From 1e2d1bf4c8ae6b960cf09cd295e4539220ca45aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Wed, 4 Nov 2026 04:02:06 +0100 Subject: [PATCH 36/36] test: explain the default-root view test's budget as PR #40 does Adds the same two-line comment PR #40 writes above this budget, so the two branches make the identical change and merge without a conflict in either order. Co-Authored-By: Claude Opus 5.5 --- test/cli.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/cli.test.ts b/test/cli.test.ts index daae128ff..7882034be 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -343,6 +343,8 @@ describe("sdp cli", () => { cwd.mockRestore(); rmSync(root, { recursive: true, force: true }); } + // A budget that holds on a loaded machine: the test copies and extracts the whole corpus + // and renders it twice. }, 120_000); it("renders contracts warnings through the one formatter and counts them in the summary", () => {