@@ -0,0 +1,87 @@
|
||||
# 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 (`docs/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.
|
||||
Reference in New Issue
Block a user