From 51a2d396fd8a36895c32708dc67770b9caa9cb40 Mon Sep 17 00:00:00 2001 From: Mathias Date: Sat, 29 Aug 2026 11:40:42 +0200 Subject: [PATCH] docs(spec): EARS-lite behavioural criteria and a context snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- skills/feature-spec/SKILL.md | 93 +++++++++++++++++++++++++++++++++ skills/spec-driven-dev/SKILL.md | 50 ++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/skills/feature-spec/SKILL.md b/skills/feature-spec/SKILL.md index 09d9fda..29c5628 100644 --- a/skills/feature-spec/SKILL.md +++ b/skills/feature-spec/SKILL.md @@ -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 THE SYSTEM SHALL +WHILE THE SYSTEM SHALL +IF THEN THE SYSTEM SHALL +``` + +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 +``` + +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 @ . 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. diff --git a/skills/spec-driven-dev/SKILL.md b/skills/spec-driven-dev/SKILL.md index 0d8ce57..39e9dba 100644 --- a/skills/spec-driven-dev/SKILL.md +++ b/skills/spec-driven-dev/SKILL.md @@ -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 THE SYSTEM SHALL +WHILE THE SYSTEM SHALL +IF THEN THE SYSTEM SHALL +``` + +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 +``` + +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: