Skip to content

MVP slices 1–5: the full loop — extract, bind, check, read, polish#3

Open
darko-mijic wants to merge 20 commits into
mainfrom
feature/anchors
Open

MVP slices 1–5: the full loop — extract, bind, check, read, polish#3
darko-mijic wants to merge 20 commits into
mainfrom
feature/anchors

Conversation

@darko-mijic

Copy link
Copy Markdown
Contributor

The MVP loop, end to end: Slices 1–5 on the Phase 0 foundation

What this PR is

Phase 0 (already on main) made the Protocol itself typed code: the Spec primitive, its three
descriptors (kind · altitude · readiness), the relation set, and the validators — plus a worked
example that typechecks. What it could not yet do was anything useful: there was no way to turn
authored specs into a graph, no binding between specs and code, no CI gate over the graph, and nothing
generated to read.

This PR builds the rest of the MVP — all five vertical slices from the roadmap
(docs/concept/07) — so the loop the concept
promises actually runs:

author delivery intent as typed Specs → bind implementing code and tests with anchors →
derive the one graph → check conformance + honesty → read the generated Design Review —
deterministically, gated in CI.

Concretely, from the repo root:

npm run build
node ./dist/cli/sdp.js view examples/checkout-v1 --check-clean
9 specs · 1 packs · 3 anchors → 13 nodes · 25 edges (0 errors, 0 warnings)
specs/orders/create-order-invalid-cart.sdp.ts — [warning] conformance/verifies-linkage —
Example "spec:orders.create-order.invalid-cart" declares verifies → "spec:orders.create-order"
but is not an enabled verifier — no test anchor binds it …
validate: 0 errors · 1 warnings (conformance + honesty over the one graph)
Wrote examples/checkout-v1/generated/design-review (11 pages)

That single warning is deliberate, and it is the project's honesty posture in one line: invalid-cart
declares that it verifies its parent, but no test anchor binds it yet — so the graph says the
spec↔test trace is incomplete instead of pretending. A surfaced absence is informative, never a gate
(the exit code stays 0).

Why it is shaped this way

Two disciplines from the concept docs drove the order of work:

  • Thin vertical slices, each end-to-end on the example. No slice landed as infrastructure-only;
    each one had to extract / bind / check / render the real checkout-v1 example before it counted as
    done. The slice roadmap is docs/concept/07 §1; the per-session plans are in plans/.
  • Tracer-bullet authoring. The example specs and anchored code came first, so the DSL and the
    extractor were forced to be usable before they were finished. When the example didn't extract, the
    extractor was wrong — not the example.

One structural decision from the decision log shaped the architecture more than any other: one
validation path (MD-14)
. Validators consume the extractor's output and nothing else — source →
static reification → graph → checks. The Phase-0 AuthoredModel stand-in harness is deleted on this
branch; validateGraph is the sole validation seam. The truth the checks see is what source
statically states, never what it happens to evaluate to on import.

What landed, slice by slice

Slice Delivers Where
1 the ts-morph extractor: discovers *.sdp.ts files, statically reifies specs and packs (no execution), derives nodes + declared edges, writes generated/graph.json src/extract/
2 generic anchors: codeAnchor (impl: / api: / component:) and specTest bindings extracted from implementation and test files → satisfies / verifies edges with claim: "anchored", and the derived delivery facts (implemented / has-verifier) src/extract/anchors.ts, src/graph/delivery-facts.ts
3 the conformance + honesty checks re-keyed onto the graph, as the CI gate: referential integrity · duplicate IDs · honest readiness against the floor · orphans · verifies linkage · authoring-shape honesty src/validate/
4 the agent surface: the reader (a thin typed loader — joins and claim decode done once, entry adapters + file-level impact with explicit coverage-unknown items) and the Design Review (a pure, regenerable projection: index + one page per spec/pack) src/reader/, src/projections/design-review.ts
5 polish: the CLI surface resolved (sdp build · validate · view, each subsuming the previous stage), one diagnostic rendering rule (every finding renders location-first from structured fields, failures stay to one line), the documented example walkthrough, and the clean-repo determinism test src/cli/sdp.ts, examples/checkout-v1/README.md

The trust model ships whole

The MVP's claim is not "a spec format" — it is that the graph can be trusted. Every piece of that
posture is in this PR:

  • The claim taxonomy is never collapsed. Every edge carries declared (human intent),
    anchored (a human binding from code), or inferred (machine-derived — designed-in, decoded by
    every consumer, deliberately shipping empty until the impact graph produces it). An example's own
    verifies is declared; the test anchor's is anchored; consumers can always tell which is which.
  • Delivery facts are derived, never authored. implemented and has-verifier are computed from
    resolving anchor edges. Hand-authoring one is rejected twice: the closed section types refuse it at
    tsc time, and the authoring-shape honesty check refuses it at validate time even when smuggled
    past the types.
  • Readiness is stated, then checked. The author states a rung; the readiness floor verifies it is
    structurally earned (kind-conditional evidence, blocking open questions, resolution clauses). The
    Design Review renders stated beside derived and the banner fires only in the dishonest direction.
  • Binding, never liveness. The views render "Implementation binding: present · Verifier binding:
    present · Runtime observation: not tracked" — the graph records that a binding exists, never that
    code is live or tests passed. Pass/fail belongs to CI; liveness belongs to the deferred observed
    fact.
  • Determinism is checked, not promised. --check-clean runs the full pipeline twice independently
    and fails on a single divergent byte; deleting generated/ and rebuilding is byte-identical; so is
    the pipeline run from a copy at a different absolute path. All three are pinned in the test suite,
    and npm run check:example gates CI on the first.

Hardened, not just landed

The commit history reads slice → adversarial review pass → hardening, five times over. A few of the
catches worth knowing about, because they show what the reviews were for:

  • The branded-ID flavor layering was unsound. Stacking a second __brand key with disjoint unit
    types reduces the intersection to never — so tsc was enforcing nothing for the flavored anchor
    ids. Rebuilt as a proper two-layer brand, with cross-flavor assignment pins so it cannot silently
    regress.
  • Parse-broken spec files now fail loudly on both surfaces (extraction summary and validate),
    instead of being silently skipped — a missing file must never look like a clean graph.
  • Shadowed and default-import bindings resolve honestly in anchor extraction, so a local variable
    named like the builder cannot mint a binding.
  • The example-verifier decode fails closed: one shared predicate decides "enabled verifier" for
    both the reader's decode and the derived facts, so the two surfaces cannot drift apart.
  • The floor's evidence clauses are mutation-tested: fixtures pin that each clause actually flips
    the verdict, not merely that green inputs stay green.

The should-fail fixture corpus grew accordingly: test/fixtures/extract/ now covers ~25 invalid or
edge-case repos (dangling refs, duplicate IDs, hand-authored delivery facts, non-static envelopes,
wrong builders, misplaced anchors, namespace imports, parse errors, …), each pinned to its exact
diagnostic.

Docs kept in lock-step

Two repair commits keep the concept docs honest against the landed surface: the DSL fragments, the
graph-schema sample, and the acceptance criteria in docs/concept/ now match the code that exists,
and the JTBD stories were re-aligned to the ratified language (readiness is stated, never promoted;
the cut list is named by the MVP boundary). The worked example gained a full walkthrough —
examples/checkout-v1/README.md — including a "break it on
purpose" section that trips each check deliberately.

Verification

  • npm run check (the full CI gate) is green: temporal-language check → typecheck (src + examples) →
    lint → format → tests → build → example determinism.
  • 187 tests across 14 files, including the golden correctness oracle
    (test/fixtures/checkout-v1/expected-graph.json + the golden Design Review tree) kept distinct from
    the determinism self-checks.
  • The acceptance criteria of record (docs/concept/07 §6) are each pinned by a named test: the
    blocking-open-question honesty failure, drops-no-sections extraction, the api: anchor extraction,
    and coverage-unknown as acceptance.

Deliberately not in this PR

The cut list (docs/concept/07 §3) stands: no runtime observation or test-result ingestion (verification
in the graph is structural — a linked, enabled verifier exists), no Gherkin surface, no patch-back
loop (the edit path is intent → agent → git → checks), no MCP surface, no symbol-level impact graph,
no incremental builds. Each is recorded with rationale and a designed-in seam, so none requires
refactoring the core when it earns its way in.

extract({root}) reifies *.sdp.ts files by pure AST reading (no type checker, no
import following — MD-14's static reification) into Primitive/Pack nodes,
declared edges, and derived belongsTo (claim "declared"; no 4th claim), with
two-tier degradation: envelope failures are hard errors per spec, section
content drops property-by-property with warnings (L3). serialize.ts owns every
output byte (sorted nodes/edges, pinned key order, authored-order sections);
sdp build [root] [--check-clean] writes <root>/generated/graph.json
all-or-nothing. The checkout-v1 example is the first real input: model.ts hand
assembly deleted, tracer-bullet test re-pointed to extractor output (zero
findings, no dropped sections), golden oracle committed (prettier-ignored: the
serializer owns those bytes) plus the distinct determinism self-check. Schema
grows additively to 0.2.0; Finding gains optional file/line; seven on-disk
corpora pin the five extraction finding ids, duplicate-id exclusion, and the
emitted dangling edge; three reserved fixture names activate.
…ot, stale artifacts

Three valid findings fixed. (1) The CLI entry guard suffix-matched argv[1]
against /dist/cli/sdp.js, but Node keeps npm's .bin/sdp symlink path in
argv[1], so the installed binary exited 0 silently; isCliEntrypoint now
realpath-compares argv[1] against the module's own file (fail closed),
unit-tested through a real symlink and verified against the built binary.
(2) Committed hard-error corpora poisoned the documented default root —
sdp build from the repo root swept test/fixtures/extract/*.sdp.ts and
failed; discovery stays config-free (P3), so corpora are committed defused
as *.sdp.ts.txt and materialized into temp dirs under their real names by
test/helpers/extract-corpus.ts (the dead tsconfig/eslint corpus exclusions
drop out); a regression test pins the repo root as a clean default root.
(3) A failed build left the previous graph.json readable as current; hard
errors and --check-clean divergence now remove the prior artifact, and the
success path writes temp-then-rename. Plan 06 done-record carries the
adversarial-pass record.
codeAnchor replaces anchorImplementation over impl/api/component (MD-8,
folded into the builder doc-comment); the extractor sweeps source files
for anchor constants by import binding, derives CodeNode/Anchor binding
nodes (file + line) and anchored satisfies/verifies edges, and computes
the first delivery facts (implemented / has-verifier, enabled-verifier
gated per 02 §2 — binding, never liveness). The example gains H10's api
anchor; schema 0.3.0; golden regenerated (13 nodes, 25 edges, zero
findings); eight new defused corpora pin the anchor finding outcomes,
including the new extract/misplaced-authoring warning.
…id slots only, executable verifier

Three valid findings, all drift repair forced by the base (L2 / MD-10 / 04 §2):
(1) unknown top-level spec/pack properties no longer vanish silently — derived
graph vocabulary (delivery facts, claim, edge fields) is an envelope-tier hard
error (extract/reserved-property, the extraction-layer twin of authoring-shape
honesty) and anything else outside the authored shape warns-and-drops
(extract/unrecognized-property); (2) id builders unwrap in id slots only — a
ref() in section content drops loudly (sections carry content, relations carry
linkage — MD-10); (3) the valid-cart verifier anchor gained its executable
runner test beside it (04 §2) and the example's tests run in the suite (golden
diff: the anchor's binding line only). A validation re-review pressed the
raw-id-shaped-string case; resolved as the documented prose-is-prose boundary
(closing it would police prose content, MD-1 guardrail 1), pinned by the
id-shaped-string-content corpus. Five new defused corpora; 87 tests green.
validateGraph over the one graph is the sole validation seam (MD-14
executed): the ten 05 $2 validators, the active ready floor clauses,
sdp validate = build + checks, AuthoredModel retired, and the CI gate
on the example (0 errors, 1 standing warning).

Includes the post-review hardening (Codex adversarial pass): the
delivery-fact derivation rule shared via src/graph/delivery-facts.ts
(one derivation path, resolving edge-contract rows), the new
honesty/delivery-facts check (stated facts must equal recomputation;
gaps reads the recomputed facts), and descriptor conformance (unratified
specKind/altitude/readiness fail closed; the floor evaluator is total).
…ndpoints

The 03 $1 edge contract's kind-typed endpoint rows were stated but
unenforced: claim-separation now errors on constrainedBy targets outside
rule/constraint (per 02 $6's 'a rule / NFR / policy spec' — MD-16
contemplates rule-kind targets), decidedBy targets that are not
decision-kind, and supersedes endpoints (either end) that are not
decision-kind. Rows evaluate only on resolving, ratified-kind endpoints
(dangling stays referential integrity's finding, unratified the
descriptor check's); the declared-verifies-from-an-example row stays the
informative verifies-linkage warning. Pinned by DSL-fixture should-fail
and should-pass tests.
The consumer half of the MVP story: createReader(graph) is the one decode
path (joins, claim decode, recomputed delivery facts, derived readiness,
findings — once at construction; frozen surface: findByConcept, byFile,
blastRadius with coverage-unknown, specContext, packContext); the Design
Review renders off the reader only (Markdown, golden-pinned); sdp view =
validate + render, view directory owned wholesale. deriveReadiness rides
the same floor table (MD-13); the inferred set decided empty with its
consumer; graph golden byte-identical — this slice only reads.
…test-anchor rule

Two findings from the Slice-4 adversarial review, both valid:

- One resolving-test-anchor rule, never three. checkVerifiesLinkage and
  both reader decode sites accepted an anchored verifies edge without
  requiring its source to resolve to an Anchor node, while
  computeDeliveryFacts (correctly) did - so a foreign producer's
  off-contract edge could suppress the missing-test warning and decode
  an example as enabled. The rule is now one exported predicate
  (isResolvingTestAnchorVerify, in delivery-facts.ts) consumed by the
  derivation, the verifies-linkage check, and the reader - the surfaces
  can no longer disagree (fail closed). Regressions pin it in
  validators.test.ts and reader.test.ts; both verified to fail pre-fix.

- Anchor docs reconciled to the landed binding contract. 04 §2, 03 §1,
  07 §4, and jtbd-02 advertised component/implements (and a plural
  satisfies) the extractor hard-rejects; they now state the landed
  contract (id · optional label · one satisfies/verifies target) and
  name component/implements/handles/emits explicitly ASPIRATIONAL,
  pointing at the inline-vs-centralized open question (07 §4) - the
  R1/R3 pattern: the code was the more faithful artifact.
…documented example, the clean-repo determinism test (plan 10)
… cited; attribute each determinism gate

Two documentation findings from the Slice-5 review, both verified against
the gates (check:temporal, format:check):

- The second-caller bar is now named at 06 §3, where the freeze rule is
  stated and where 00 §4 / 07 §1+§3 / AGENTS.md point. The term had
  graduated from plan vocabulary into the base with citations to a
  section that stated the rule but never the name - the only naming
  site (06 §4, "applied to writes") presupposed it.

- The example README's determinism paragraph attributes each property
  to its actual gate: check:example gates the twice-run --check-clean
  self-check over the example; delete-generated/-and-rerun and the
  different-absolute-path (clean-repo) property are pinned in the test
  suite. It previously credited check:example with delete-and-rerun and
  omitted the location-independence property Slice 5 itself added.

Review otherwise clean: all four break-it experiments re-executed and
reproduced as documented, the exactly-once location rendering grep-
verified across src/, no surviving "maybe" about explain/search, and
the full gate green at 157 tests.
…s fail loudly on both surfaces; the namespace import form extracts; every missing envelope field reports in one pass; a repeated carrier property is loud; discovery skips dot-directories
…gates the reader's enabled decode and the Design Review's test-anchor rendering; the readiness ladder derives from SPEC_READINESS
…he parse errors covered; an error boundary keeps failures past discovery to one line, exit 1, stale artifacts removed (the injection seam, never a flag)
…nd array-carrier pins; real graph-flip assertion; id-brand pins; the empty typecheck stub deleted
…ded-ID DSL fragments, suffix-alone discovery, the schema sample, the acceptance criteria of record
…er promoted; the verifier gap reads 'designed, stated done, unverified'; the Founding Principle is stated once (concept README) and only pointed at; the cut list is named by the MVP, not by 'now'
…nd branded layer — one __brand key with disjoint unit types reduces to never, so tsc enforced nothing for the flavored ids; cross-flavor pins added; doc-comment deixis dropped
… rule — one predicate decides conferral for decode and derived facts; the builder's id contract restated statically; opaque envelope entries never double-report as missing; the duplicate-property guard reaches every object tier; shadowed and default-import bindings resolved honestly; CLI recovery never re-throws, temp twins removed, the checks ride the one-line law; residual deixis dropped
… names its ratified rungs; the wrong-builder diagnostic reads as one sentence; the discovery skip-list boundary recorded as a named deferral; the unknown-option line rides the one-line failure prefix
…orce:true swallows only ENOENT there, so generated-as-a-file raised ENOTDIR out of the one-line law; success-path removals stay raw so a stale temp tree is never renamed into place
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant