Files
skills/skills/feature-spec/SKILL.md
T
mathias 1d368d4b5d
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
docs(skills): drop the Mode 2 routing notes, fix the contributing steps
Seven skills carried a `## Mode 2 Routing Note` citing a routing pod ("Plan 6")
that does not exist. Several admitted in their own text that nothing about the
skill changes between modes — so it was roughly ten lines of context on every
load for zero behavioural effect. The content is not worthless, just not
per-skill: it now lives as one table in README.md, with the caveat that nothing
depends on it.

The related line inside the session_log sections stays, because that one does
motivate behaviour — but it no longer cites a component that has never shipped.
It now says what is true: pass-rate is what a future routing decision would be
based on, and a skill that never logs contributes no evidence either way.

While in README: the contributing steps were stale enough to fail CI for anyone
who followed them. They said to add a row to SKILLS_INDEX.md by hand — the table
is generated now, and `task check` fails on a hand-edited diff — and listed none
of the four frontmatter keys the gates require. Step 4 now names the
pressure-test bar from #19 rather than leaving it to be discovered.

Closes #36
Bump-Type: patch
2026-08-29 14:50:14 +02:00

12 KiB

name, surfaces, summary, use_when, description
name surfaces summary use_when description
feature-spec
claude-code
library
Implementation-level feature spec (tighter than spec-driven-dev) Scoping one feature inside an already-specced project Write a tight implementation-level spec for a single feature or component before coding it. Use after a project-level spec exists (see spec-driven-dev) and you need to nail down one feature inside it.

Feature Spec

Overview

A feature spec is the implementation-level contract for one feature or component, written by an engineer before code. It is shorter and more focused than a project-level spec (spec-driven-dev) — it assumes the project context, the stack, and the user are already known. Its job is to force one decision: what does done look like, and what is explicitly not in this feature.

Core principle: If you cannot write three measurable success criteria, you do not understand the feature well enough to build it.

When to Use

  • About to start implementing a feature inside an existing, already-specced project
  • A change that touches more than one file or one package
  • A change that introduces a new public API, schema, or external contract
  • Any time the requirements feel "obvious" — that feeling usually means assumptions are stacking silently

When NOT to use:

  • Project kickoff or first-time architecture decisions — use spec-driven-dev instead, which covers the broader lifecycle (Objective, Tech Stack, Project Structure, Boundaries, etc.)
  • Single-line fixes, typos, dependency bumps
  • Spike or throwaway prototypes (note them as such; no spec needed)

Iron Laws

  1. Success criteria must be measurable. "The system is fast" is banned. "p99 < 200ms under 100 RPS" is valid. If you cannot state a measurement, the criterion is not a criterion.
  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:

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

# Spec: [Feature Name]

## Problem Statement

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.
Examples: "must not change the public API", "must complete in < 500ms",
"must not require a database migration", "must work offline".

## Out of Scope

What this feature explicitly does NOT do, even if related.
Anything plausibly in scope that you decided to defer goes here.

## Technical Approach

The architecture or design decisions, with rationale for each.
Each bullet should answer "why this choice over the obvious alternative."

## Risks

What could go wrong, and how it would be mitigated or detected.
At least one risk must be listed; "no risks" usually means insufficient analysis.

Worked Example

# Spec: IBAN extraction from Swedish PDF invoices

## Problem Statement

Invoice processors at small accounting firms manually copy IBANs from PDF
invoices into the banking system. The current parser handles ~60% of formats;
40% require manual fallback, defeating the automation premise.

Why now: the pilot customer is onboarding three new firms next month, all of
which use formats currently in the 40%.

## Success Criteria

- [ ] IBAN extracted correctly from ≥ 95% of a 200-invoice test corpus
- [ ] Zero false positives (extracting a non-IBAN string as an IBAN) on the corpus
- [ ] p99 extraction time < 500ms per invoice
- [ ] Returns a structured error (not a panic) for unrecognized formats

## Constraints

- Must not require a new dependency (PDF library is already chosen)
- Must work without network access (no external IBAN validation API)
- Must be callable from the existing `Parser.Extract(ctx, r)` signature

## Out of Scope

- Non-Swedish IBAN formats (separate spec when needed)
- BIC/SWIFT extraction (handled by a separate parser)
- OCR — input is assumed to be text-extractable PDF, not scanned image
- Validating that the IBAN refers to a real account

## Technical Approach

- Layered regex-then-checksum approach: regex narrows candidates, MOD-97
  checksum validates. Rationale: pure regex is too permissive; pure
  checksum requires too many candidates to test.
- Use the existing `pdfplumber` text layer rather than re-extracting.
  Rationale: extraction is already the slow path; reusing the layer
  avoids doubling extraction time.
- Return `(iban string, ok bool, err error)` not `(*IBAN, error)`.
  Rationale: the caller almost always wants to know "did we find one"
  separately from "did extraction itself fail," and a pointer return
  invites nil-deref bugs.

## Risks

- Regex tuned to current corpus may overfit; mitigation: corpus is held out
  per-firm, so we will see degradation at the next firm onboarding before
  it reaches production.
- MOD-97 implementation bugs are subtle; mitigation: cross-check against
  the Wikipedia reference implementation in tests.

Common Rationalizations

Rationalization Reality
"It's a small feature, I don't need a spec" Small features still need a measurable definition of done. The spec stays small too.
"I'll write the spec after, when I know more" Then it is documentation, not a spec. The point is forcing clarity before the code locks in your assumptions.
"The success criteria are obvious" If they are obvious, they are easy to write down. If they are hard to write down, they are not obvious.
"I don't have time to enumerate out-of-scope items" You will spend more time arguing over scope creep at review than writing this section now.

Brain MCP Integration

The brain holds prior feature specs across the project. Use it to keep constraints and out-of-scope decisions coherent across related features.

At spec start:

  • Run brain_query for prior specs in the same area or for the same problem domain. Reusing the same constraints and out-of-scope decisions across related features keeps the system coherent.

After spec is approved:

  • Run brain_write with the spec's success criteria and out-of-scope list. Future related specs can cross-reference rather than re-derive.

Logging

Call session_log once at the end of every phase to record the outcome. Pass-rate is computed downstream by the /pass-rate HTTP endpoint, which treats pass as success, fail as failure, skip as neither.

At end of each phase:

  • session_log with {session_id: "<this session's id>", skill: "feature-spec", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}

Phases for this skill: draft

Status semantics:

  • pass — the phase's intended outcome was reached (spec drafted and approved).
  • fail — the phase's intended outcome was NOT reached (spec rejected, blocked on unresolved questions).
  • skip — phase was skipped intentionally.

Why this matters: pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.

Cross-References

  • Load spec-driven-dev instead if this is the first spec for a project (broader template, includes tech stack, project structure, boundaries).
  • Load problem-analysis skill before this if the problem statement itself feels fuzzy.
  • Load tdd skill once the spec is approved — each success criterion becomes a test.
  • Load planning skill if the spec implies more than ~5 implementation tasks.