Files
khosra/ideas/specs-as-secondary-artifacts.md
T
bdeshiandClaude Opus 5 ec6e9838f0 rename docs/ to harness/, and reserve docs/ for the reader
docs/content-model.md opens with "Engine specification". It is also where the
rule lives that a leading underscore makes a file unaddressable — and the human
who owns this site did not know that rule, because nothing in this repository is
addressed to an author. Twelve documents named docs/ while being exclusively
about building the parser is a signpost pointing at the wrong room.

Naming the directory for its audience makes the gap visible instead of hiding it.
docs/ is now reserved and deliberately absent: an empty docs/ is an honest
statement that end-user documentation does not exist, where docs/ full of parser
specs was a claim that it did.

HARNESS.md stays at the root. Root holds the three entry points — README.md for a
human, CLAUDE.md for an agent, HARNESS.md for whoever maintains the machine — and
harness/README.md is the map of the directory, so moving the guide inside would
have collided with it for nothing.

Mechanical and wide: 100 path references across 24 files. Every verify.sh gate
that names a doc by path, the directory lists the dangling-path and ADR-number
gates scan, surface.sh's output target, the Makefile, CLAUDE.md's read order, the
skill, four commands, and two Go package comments. A first pass with a shell loop
silently edited only four files and the rest still said docs/; the fix was to
write the file list out and check the remaining count was zero rather than trust
the loop's exit status.

No rule, threshold, gate or obligation moved — this is a rename, and the gates
demonstrated it twice: they stayed green on the new paths, and the ADR-number gate
caught ADR-0082 before the entry existed.

Deferred, both on the human's call: the end-user documentation site itself, which
wants its own decision about where it lives and whether its claims are gated; and
moving examples/ under docs/, since demo-site is a live site root that verify.sh,
the coverage test and make demo all point at, and moving it would couple a rename
to a design nobody has made.

31 files, +146/-106.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:11:06 +06:00

4.9 KiB

Specs as secondary artifacts, and named tests

Status: parked Raised: 2026-07-28

Why it came up

The harness is deliberately not an SDD framework: the goal is to maintain content and trust the harness to keep the code minimal and maintainable, rather than to author requirements for every change. But two things are missing that SDD would have given for free — a place to put a well-defined spec when one already exists, and any trace from a code change back to the behaviour it was supposed to produce.

Proposal: per-feature specs as secondary artifacts. ADRs, state.md and the engine-wide contracts stay primary. A spec may be written first (true SDD for a well-defined feature) or generated after the fact from what was requested and what was built. Either way it can then be edited, and the edit is implemented — flowing upward into the harness where it touches anything the harness owns.

What is already established

  • The harness has no per-feature artifact. A plan lives in the conversation; what survives is an ADR plus a state.md delta. There is no way to ask later "which behaviour was this code meant to have".
  • Tests are mandatory for cmd/ and internal/ changes but nothing ties a test to the rule it verifies. content-model.md says a bundle key excludes language, and that a bundle supplying both about.md and about.en.md is rejected — nothing checks a test exists for either.
  • One fact, one place is enforced throughout (harness/README.md authority table). Specs must not become a fourth copy of decisions.

The non-overlapping slot

The only content a spec should hold is what nothing else does:

Artifact Holds
ADR why a load-bearing choice was made, permanently
state.md what exists right now
architecture.md, content-model.md engine-wide shape and contract
spec per-feature observable behaviour: given X, the engine does Y

Acceptance criteria, in other words — which is also exactly the traceability gap. So the cheapest useful spec is a list of criteria whose names are the test names, and the named-test convention below is the subset of this proposal that can ship on its own.

The rule that stops it rotting: two states, one owner each

A generated spec describes what exists; an edited spec describes what should exist. Flipping between those per edit, with nothing marking which, is how spec systems rot — after three round trips nobody knows whether a clause is a record or a requirement. So each spec carries a status and it cycles:

  • derived — describes what exists. The code wins; a mismatch is a spec bug, fixed silently.
  • authored — a human edited it, or wrote it first. The spec wins; a mismatch is unimplemented work. The next implement pass brings the code into line and flips it back to derived.

verify.sh can surface authored specs as a warning — pending requirements become visible state rather than a silent backlog. This mirrors the existing "if state.md disagrees with the code, the code wins" rule, which works precisely because the direction is never ambiguous.

Two things to get right or it is worse than nothing

A derived spec must not paraphrase the code. If it restates the implementation it carries no information, costs tokens every feature, and goes stale instantly. Its value is the half the code cannot hold: the request as made, the behaviour at the edges, what was deliberately not done. Capture the request, not the diff.

Not every change earns a spec. A spec per one-line tweak is overhead. The natural threshold is the one already enforced: a change that must ship a test is a change with observable behaviour worth pinning, and the spec's criteria are that test's names. Changes below that line get an ADR or nothing.

Upward flow needs no new machinery

An edited spec is just another request, so the existing conflict check (CLAUDE.md rule 9) applies unchanged: contradicts an ADR or an engine contract → hard conflict, stop and surface; is a load-bearing choice → it produces an ADR; is plain new behaviour → code plus spec, nothing else. "Flows upward" means the spec is an entry point to the loop, not a parallel process.

Cheapest next step

Two independent pieces, in order:

  1. Named tests, alone. A line in conventions.md: a rule stated in an engine contract earns a test named after it — TestBundleKeyExcludesLanguage, TestDuplicateVariantRejected. Zero infrastructure, most of the traceability, and it makes the spec step later almost free because the criteria already exist as test names.
  2. specs/<feature>.md, one file per feature with a Status: line, an index like ideas/, excluded from the Go gates the way ideas/ and reference/ are, read on demand and never swept.

Open question if this is adopted: whether a spec is ever deleted. Suggest not — a derived spec for a shipped feature is the closest thing to living documentation of behaviour, and it is cheap to keep.