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>
88 lines
4.9 KiB
Markdown
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.
|