docs(spec): EARS-lite behavioural criteria and a context snapshot

Both skills gain EARS-lite as an option alongside prose criteria: WHEN/WHILE/IF
plus the unconditional form, for behaviour that a measurable threshold cannot
express. feature-spec also gains a Context snapshot section — the executor of a
spec routinely cannot open the architecture document it points at, so the facts
it depends on get inlined with provenance.

Pressure-tested per #19, and the RED phase argued against half the issue. A
subagent with no skill wrote a strong spec and then explained that it had
deliberately NOT inlined architecture context, "because both are owned by the
existing architecture document and duplicating them here would create two
drifting sources of truth". That objection is correct, so the section is built
around answering it: snapshot what the implementer cannot look up, always cite
source and version, and state that the source wins on disagreement. Prose
criteria came out fine unaided; EARS phrasing did not appear at all, which is
where the real gap was.

The GREEN run then found seven defects in the first draft, all fixed here:

- The example was in the same domain as the test scenario, so it got copied
  rather than read — the spec inherited an extraction-confidence requirement
  its feature request never mentioned. The example is now deliberately
  off-domain, with that failure written up as the reason.
- EARS-lite had no unconditional form, so a genuine invariant ("no instruction
  above threshold ever reaches a file") had to be bent into a WHILE that a
  system trivially satisfies by never entering the state. Added.
- "Maps 1:1 onto a test" was false for the WHILE form, which is universally
  quantified. Softened to say which forms map to one test and which do not.
- The provenance rule had no answer for the case it was written for: an author
  who also cannot read the source. Now: mark the block UNVERIFIED in those
  words and make confirmation the implementer's first task.
- "Snapshot only what this spec's decisions rest on" was narrower than the
  section's own purpose; a table name underpins no decision and still blocks
  the migration. Widened, with the omissions stated deliberately.
- "Say so in the section itself" did not say what, at what granularity. Now one
  opening line, not a per-fact annotation.
- The template had no slot for EARS lines, so their placement was a guess.
  Added as a subsection with a note that they are properties, not checkboxes.

Closes #12
Bump-Type: minor
This commit is contained in:
2026-08-29 11:40:42 +02:00
parent bb912e5594
commit c7d6e149f2
2 changed files with 143 additions and 0 deletions
+93
View File
@@ -32,6 +32,90 @@ A feature spec is the implementation-level contract for one feature or component
2. **An "Out of scope" section is mandatory.** If you do not draw the boundary, the implementer (you, future-you, or another agent) will guess wrong.
3. **Every technical decision in "Technical approach" must have a rationale.** A decision is any choice where a reasonable alternative existed and was not taken. "Use Postgres" is not a decision; "use Postgres because the rest of the stack uses it and we need transactions across user + invoice" is.
## EARS-lite acceptance criteria
Measurable criteria answer "is it done?". They do not always answer "what should it do
when X happens?" — and behavioural requirements written as prose are where ambiguity
survives review. For any criterion that is conditional or event-driven, use EARS-lite
instead of prose:
```
WHEN <trigger> THE SYSTEM SHALL <observable response>
WHILE <state> THE SYSTEM SHALL <observable response>
IF <unwanted condition> THEN THE SYSTEM SHALL <observable response>
```
There is a fourth, unconditional form. Use it for invariants — the properties that must
hold with no trigger at all, which are usually the ones that matter most:
```
THE SYSTEM SHALL <observable response>
```
Worked, deliberately from a domain unlike whatever you are specifying:
```
THE SYSTEM SHALL reject a firmware image whose signature does not verify
WHEN a sensor reports a temperature above the shutdown limit
THE SYSTEM SHALL open the contactor within 200ms
WHILE the contactor is open
THE SYSTEM SHALL refuse every start command
IF a sensor reading is missing or stale by more than one interval
THEN THE SYSTEM SHALL treat it as above the limit
```
The example is off-domain on purpose. An on-domain example gets copied: a spec written
against an invoice-flavoured example inherited a requirement about extraction confidence
that its feature request never mentioned, and the author only noticed on review. Read
the shapes, not the content.
Each conditional line names a trigger and one observable response, which usually maps
onto a single test. The `WHILE` and unconditional forms are universally quantified —
"every start command", "a firmware image" — so they map onto a property or a set of
cases, not one assertion. That is still worth writing: it closes the requirements half
of the audit trail, which is otherwise issue-body prose.
**This is an option, not a replacement.** Threshold criteria ("p99 < 200ms under 100
RPS") stay as they are — EARS phrasing adds nothing to a number. Use it for conditional
behaviour, error handling, and state-dependent rules.
## Context snapshot
The executor of this spec will not see the conversation that produced it, and often
cannot see the architecture document either — a different agent, a fresh session, a
different machine. A spec that says "see the architecture doc" hands them a lookup they
may not be able to perform.
So inline the specific facts this spec depends on, not the whole document:
```markdown
## Context snapshot
Taken from <source> @ <commit-or-date>. Re-check before implementing if stale.
- The mailbox poller writes rows to `invoices` with `state = 'received'`.
- The payment-file writer reads `invoices` and emits one file per batch.
- Money is int64 minor units plus an ISO-4217 code; float is banned repo-wide.
```
Three rules keep this from becoming a second source of truth, which is the real
objection to inlining and a good one:
1. **Snapshot what the implementer cannot look up and cannot proceed without.** That is
wider than "facts this spec's decisions rest on" — a table name may underpin no
decision here and still be the thing that blocks writing the migration. It is still
narrower than the whole document: if a fact changes nothing and blocks nothing, leave
it out, and say what you left out so the omission reads as deliberate.
2. **Always cite source and version.** A snapshot without provenance cannot be
revalidated, and an unrevalidatable copy is exactly the drift everyone fears.
3. **If you could not read the source either, say so in those words.** Writing a spec
against a document you cannot open is common and is not a reason to skip the section
— it is a reason to mark the whole block `UNVERIFIED — reconstructed, not read`, and
to make confirming it the implementer's first task, before the first failing test.
An unmarked guess is the only genuinely dangerous version.
4. **The snapshot is evidence, not authority.** Open the section with one line to that
effect — "where this disagrees with the source, the source wins and this spec is
stale" — rather than annotating each fact.
## Spec Template
```markdown
@@ -42,12 +126,21 @@ A feature spec is the implementation-level contract for one feature or component
What problem does this feature solve? For whom? Why now?
(2-4 sentences. If you cannot answer "why now," ask before writing the rest.)
## Context snapshot
[Facts the implementer cannot look up, with source and version. See "Context snapshot" above.]
## Success Criteria
- [ ] Criterion 1 — measurable and verifiable
- [ ] Criterion 2 — measurable and verifiable
- [ ] Criterion 3 — measurable and verifiable
### Behavioural criteria (EARS-lite)
[Conditional and event-driven behaviour, one trigger and one observable response per
line. Not checkboxes — these are properties, and several need more than one test each.
Omit the subsection entirely if the feature has no conditional behaviour.]
## Constraints
Non-negotiable requirements the solution must satisfy.
+50
View File
@@ -200,6 +200,56 @@ Dev: [full command]
[Anything unresolved that needs input before implementation begins]
```
## EARS-lite acceptance criteria
Measurable criteria answer "is it done?". They do not always answer "what should it do
when X happens?" — and behavioural requirements written as prose are where ambiguity
survives review. For any criterion that is conditional or event-driven, use EARS-lite
instead of prose:
```
WHEN <trigger> THE SYSTEM SHALL <observable response>
WHILE <state> THE SYSTEM SHALL <observable response>
IF <unwanted condition> THEN THE SYSTEM SHALL <observable response>
```
There is a fourth, unconditional form. Use it for invariants — the properties that must
hold with no trigger at all, which are usually the ones that matter most:
```
THE SYSTEM SHALL <observable response>
```
Worked, deliberately from a domain unlike whatever you are specifying:
```
THE SYSTEM SHALL reject a firmware image whose signature does not verify
WHEN a sensor reports a temperature above the shutdown limit
THE SYSTEM SHALL open the contactor within 200ms
WHILE the contactor is open
THE SYSTEM SHALL refuse every start command
IF a sensor reading is missing or stale by more than one interval
THEN THE SYSTEM SHALL treat it as above the limit
```
The example is off-domain on purpose. An on-domain example gets copied: a spec written
against an invoice-flavoured example inherited a requirement about extraction confidence
that its feature request never mentioned, and the author only noticed on review. Read
the shapes, not the content.
Each conditional line names a trigger and one observable response, which usually maps
onto a single test. The `WHILE` and unconditional forms are universally quantified —
"every start command", "a firmware image" — so they map onto a property or a set of
cases, not one assertion. That is still worth writing: it closes the requirements half
of the audit trail, which is otherwise issue-body prose.
**This is an option, not a replacement.** Threshold criteria ("p99 < 200ms under 100
RPS") stay as they are — EARS phrasing adds nothing to a number. Use it for conditional
behaviour, error handling, and state-dependent rules.
For the executor-facing handoff, `feature-spec` also carries a **Context snapshot**
section — inline the architecture facts the implementer cannot look up.
## Reframing Vague Requirements
When you receive a vague requirement, translate it into specific success criteria before writing any spec content: