Spec-driven development — the Ascend harness

How intended behavior is written down, verified, and enforced across the Ascend stack. The harness keeps two states permanently distinct — the canon (what the product should do: backbone → specs → acceptance + evals) and the build (what the code is) — and measures one against the other mechanically, at every stage, on every push. Its operating rule: ship with gaps, never ship with divergences.

01

What the system is for

Development at agent speed produces more behavior than review at human speed can hold in memory. Behavior gets lost in a rework without anyone deciding to lose it; numbers drift from the models that define them; a change reaches into a contract nobody intended to touch. The harness closes that class of failure by making intended behavior a versioned, machine-checkable artifact — and by classifying every mismatch between canon and code as exactly one of two things:

Gap

Absence — tracked, may ship

The canon has it; the code doesn't yet. Every gap is recorded in a coverage ledger and dispositioned — built now, deferred, or consciously cut. A dispositioned gap ships: it is the burndown. An un-dispositioned gap blocks the merge — silent absence is precisely the failure the harness exists to stop.

Divergence

Contradiction — blocked, never ships

The code asserts something the canon forbids: a failing must-pass scenario, a number that contradicts the live system, a diff inside a protected contract. Divergences are walls — continuous integration blocks them unconditionally, and no disposition waves one through.

One more reading exists on the evidence axis: UNRESOLVED — the checker itself cannot run (an unreachable oracle, an unresolvable pin). It never counts as a pass: the merge verdict fails closed on evidence it cannot check.

Why the enforcer is continuous integration
The gates re-derive their verdict from the repository on every push. They hold no conversation history, accumulate no assumptions, and make no judgment calls — people decide dispositions in the speccing session; the gates enforce exactly what was decided, identically, every time. A long working session drifts as its context compacts; a stateless check cannot.
02

Identifiers and naming — every layer citable, one grammar

The chain is machine-walkable only when every node has a stable, greppable name — so any line of code resolves up to the job it serves and back down to the scenario that checks it. One grammar runs top to bottom. Case carries meaning: UPPERCASE filenames are harness artifacts (SPEC.md, EVALS.md, COVERAGE.md, JTBD.md); lowercase reserved names are OKF bundle files (index.md, log.md); everything else is kebab-case.

LayerIdentifierExampleRegistered where
Actorthree-letter codeBOR · LEN · LIQ · HUBthe actor table atop JTBD.md
Job (JTBD)J-<ACTOR>-<NN>J-BOR-02one entry per job in JTBD.md
SpecS<NNN> — monotonic, never reusedS002the SPECS.md ledger (hub root)
Acceptance filecapability slugacceptance/borrow.featureinside the spec folder
Scenarioimmutable tag @S<NNN>-AC-<NN>@S002-AC-12the tag line above the scenario
Eval caseEV-<TOPIC>-<N>S002/EV-HF-2EVALS.md
Coverage rowcapability slug + cited scenarioCOVERAGE.md
GateG0–G5G4the gate table · scripts/
Decisiondated ADR2026-07-16-….mdthe decisions bundle

Spec folders and the ID ledger

A spec lives at <spoke>/specs/S<NNN>-<slug>/. The slug says what it specs; the ID gives it a citation handle; the date lives in SPEC.md frontmatter and git history, not the path — paths are identity, history is time. A folder is never copied to -v2; it evolves in place through re-baseline, and its history is its git log.

SPECS.md at the hub root is the allocator and the tombstone record — an append-only table, one row per spec. Allocating an ID means appending the next row in the same PR that creates the folder: two authors grabbing the same number collide as a textual merge conflict, which is precisely the race detector. Rows are never deleted — a retired spec keeps its row so an ID can never rebind, and a rename appends an alias row. G0 checks every folder against the ledger and every ledger row against the tree.

SPEC.md carries the joins

id: S002 # must match the folder prefix (G0) slug: borrow-onboarding jtbd: [J-BOR-01, J-BOR-02] # the only spec→backbone edge genre: preservation # forward | preservation | hybrid date: 2026-07-01 # authored; re-baselines append sources: … # SHA-pinned; twin in sources.lock

The JTBD registry (knowledge/backbone/JTBD.md) holds the actor table and one entry per job, linking to backbone sections — and carries no spec pointers. The spec→job edge lives only in the spec's jtbd:; job coverage is a set-difference G1 computes, never a hand-kept column.

Three naming rules the gates enforce

One file per artifact kind. A legacy lowercase name (spec.md) is accepted with a deprecation warning only when the uppercase form is absent; the two coexisting, or a case-fold collision, is a violation — a platform-dependent source of truth is worse than either name.

Identity in the path, time in git. No dates and no -v2 in folder names; a spec is one folder that evolves in place.

The hub holds only what is shared. knowledge/, .claude/, scripts/, the workflows. Each spoke holds only its own specs, decisions, and mounted code. Specs never live in the hub; shared canon never lives in a spoke.

03

Components — where each lives, who ships it, and why each exists

Five layers, nested like a Russian doll: each wraps the next, is derived from the one above, and cites it explicitly — so the chain is machine-walkable in both directions, from any line of code up to the job it serves and back. The gates are not a layer of their own: they sit on the boundaries between layers, checking every crossing.

knowledge/backbone/hub · product + strategy

the everything-doc · money models + supervisory procedures as referenced canon · the JTBD registry

G1 · backbone-coverage
<spoke>/specs/S<NNN>-<slug>/SPEC.mdspec author + spoke owner

one ID'd folder per unit of work (the slug says what it specs; the date lives in frontmatter and git) — its jobs (jtbd:), its genre, its SHA-pinned sources

G0 · spec-lintG0b · sources-driftG2 · COVERAGE + scenario xref
acceptance/ · EVALS.md · COVERAGE.mdthe speccing session

Gherkin scenarios, one Feature per file · quantitative oracle cases · the gap ledger · sources.lock, the pin twin

G3 · acceptance-runnerG4 · eval-oracle
tests + step definitionsengineers · code repos

step definitions bind each scenario phrase to real actions; spec-local harnesses where they enforce the spec

G5 · core-untouchable
the running codethe build — the IS state

Why each layer, given the one above it

The backbone — given nothing

Without one agreed statement of intent, every spec re-derives (and re-disputes) first principles. The backbone settles what the product is once, so everything below can cite it instead of re-arguing it.

Specs — given the backbone

The backbone states intent for the whole product at once — too broad to review, pin, or change-control per unit of work. A spec cuts it into a dated, owned, pinned unit that a pull request can actually be held against.

Acceptance — given a spec

A spec is still prose, and two readers can "pass" the same sentence with different behavior. Scenarios give every criterion a binary verdict — given this, when that, then exactly this — so agreement is checkable, not assumed.

EVALS — given acceptance

Scenarios verify behavior but treat numbers as text: a scenario can pass with the wrong haircut inside it. EVALS make each quantity falsifiable against the oracle, with a tolerance — catching what pass/fail steps cannot.

COVERAGE — given acceptance and EVALS

Scenarios and evals only test what someone remembered to write; both are silent about what is missing. COVERAGE fixes the denominator — every behavior gets a row — so absence becomes visible and must be dispositioned, never silent.

Tests — given acceptance

A scenario is executable in principle only. Step definitions bind each phrase to real actions against the app; without the binding, the canon is prose and nothing fails when the code disagrees.

The gates — given all of it

Everything above is discipline, and discipline decays: sessions drift, people forget. The gates re-check the whole chain mechanically on every push and refuse the merge — the system stops depending on anyone's memory.

The citations are the structure: a scenario is cross-referenced by its coverage row; a spec's jtbd: cites registry jobs; the registry links the backbone's sections; eval cases cite the running system. Numeric sources — the models, the spreadsheets, the contracts — remain the system of record: cited into specs by pinned reference, never paraphrased into prose, never retired.

04

Correctness — the gates, and the three fields

Every stage boundary carries a gap face and a divergence face, and each is guarded by a named gate. Gap-gates measure coverage completeness and block only un-dispositioned absence; divergence-gates detect contradiction and block it outright.

GGateBoundaryClassThe failure it prevents
G0spec-lint — genre: / sources: / pass-definition hard, rest warnspec structureDivergencea spec nobody can trace or verify
G0bsources-drift — the pin twin vs the moving worldpinned ↔ liveDivergencea green gate proven against sources that have since moved
G1backbone-coverage — set-difference: registry jobs vs specs' jtbd:backbone → specGapa job silently never specced
G2COVERAGE + scenario cross-referencespec → acceptanceGapa rework silently deleting shipped behavior
G3acceptance-runner — must-pass scenarios executeacceptance → codeDivergencecode contradicting a sanctioned scenario
G4eval-oracle — numeric invariants vs the running systemevals → codeDivergencea copied constant canonizing a number the live system refutes; double-counted value
G5core-untouchable — anchored paths, renames rejected, recorded owner approvaldiff ↔ contract canonDivergencea change mutating a sacrosanct contract

One machine, three fields

Every change decomposes into per-behavior transitions — create, keep, modify, remove — so one process covers all three kinds of work. The field label sets the review focus, not the machinery:

FieldMeaningThe ledger it keeps
Greenfieldnet-new capabilityforward — every criterion covered, deferred, or cut
Brownfieldrework of shipped behaviorpreservation — every current behavior covered, intentionally removed, or deferred; never silently absent
Bluefieldrework + net-new in one changehybrid — both ledgers in one file, routed per row by provenance

Three rules that keep the chain honest

Provenance on every criterion. Each acceptance criterion carries its origin — current behavior, onchain rule, backbone intent, or a decided change — so inherited behavior and new product decisions never blur.

Numbers come from the oracle. Numeric claims are read from the running system, never copied from source constants — a copied constant can canonize a number the deployed system quietly computes differently.

Core is extended, never edited. The protocol owner marks sacrosanct contract paths; any diff touching one — renames included — blocks until a recorded owner approval. New behavior around core arrives as a new module, not an edit.

05

The development process, end to end

What a unit of work looks like once the harness is in place — from the backbone to a merged pull request.

1

Pick the entry point

Entry differs by field. Greenfield opens from the backbone: a job-to-be-done in the registry, or a new one, registered first. Brownfield opens from the shipped surface: current behavior is inventoried — file:line-cited — before anything else is decided. Bluefield opens from both. A speccing session — the spoke owner plus whoever ships the change, an agent driving — walks the job (and, for brownfield, the inventory) against the backbone sections and models it cites.

2

Write the canon

The session produces the spec folder: SPEC.md (its jobs, its genre, its pinned sources) → acceptance scenarios → eval cases → coverage rows. People make every disposition in the room; a decision not yet taken is tagged pending — it blocks the eventual merge, not the spec.

3

Build against it

Code and tests land in the mounted repo, step definitions binding scenarios to the implementation. The spec is the contract; the code is the response.

4

Burn the ledger

Coverage rows move to their final dispositions as the work lands. Deferred rows remain on the books as tracked gaps; nothing is allowed to be silently absent.

5

Submit the pull request

The gates run. Gap-gates block un-dispositioned absence; divergence-gates block contradiction. A green run means one thing: the code matches the canon, or every mismatch is on the books. Merge follows; if pinned sources moved intentionally, the re-baseline is recorded as part of it — the chain never silently outruns its pins.

What changes per field

The five steps run unchanged for all three fields; what differs is where entry starts, what COVERAGE tracks, and what counts as done.

FieldEntryCOVERAGE ledgerDone
Greenfielda registry jobthe forward ledger — covered / deferred / cutthe burndown reaches zero un-dispositioned rows
Brownfieldthe shipped surface, inventoried into preservation rows before the rework startsthe preservation ledger — covered / intentionally-removed / deferred, plus pending-decision rows that block the rework merge until dispositionedzero silent absences against what shipped
Bluefieldboth, at onceboth ledgers in one file; a modification splits into two rows sharing a mod: pair key — the preserved invariant and the decided change — so both ledgers see it and the pairing is machine-checkable; enforcement flips capture (warn) → enforce (fail) at the rework mergeboth ledgers clean in the same pull request
06

Adoption state

This page describes the target state. Part of the harness is live today; the rest is sequenced in Linear. Until each check's migration lands it runs as a warning, never a silent skip — the compatibility window is explicit, not assumed.

Landed
  • The coverage gate and the sources-drift tripwire, in capture mode — the three-layer guard (validator + CI + pre-commit).
  • The first shipped exemplar: the borrow / onboarding preservation spec, oracle-verified.
  • The artifact naming — SPEC.md / EVALS.md / COVERAGE.md, legacy names deprecation-warned.
Tracked in Linear · ASC-63 (with ASC-61)
  • Backbone migration into knowledge/backbone/, and the JTBD.md registry.
  • S<NNN> folder migration of the two dated specs (design-system → S001, borrow-onboarding → S002) + the SPECS.md ledger seed.
  • Required @S<NNN>-AC-<NN> scenario tags and jtbd: / id: frontmatter on the exemplar.
  • The remaining gates — G0, G1, G3, G4, G5 — and enforce-mode as an automatic required check on rework-merge PRs.
  • The enforce-mode hardening, and the duplicate-artifact + two-row (mod: pair) validations.
Manny E. Reimi
Manny E. Reimi
CPO, Ascend · [email protected]
SDD harness · canon v1.0.0 · 2026-07-16
Internal engineering practice. This page is the narrative twin of the harness canon (knowledge/conventions/sdd-harness.md in ascend-os); §06 gives the adoption snapshot, with live status tracked in Linear (ASC-63).