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

88 lines
4.9 KiB
Markdown

# 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.