Chapter 22b — A Day in the Life of a New Requirement
A Requirement that takes more than a morning to author has already failed one of its own design goals.
Previous chapters looked at the DSL from the sides: the surface (chapter 03), the registry (chapter 14), the gates (chapter 13), the recovery paths (chapter 13c). This chapter looks at it from the front — from the seat where a human sits down, decides something must be true about the system, and types the sentence.
The scene is a single morning. Seven commits. One new Requirement, one Feature to satisfy it, one test to cover the ACs, four red compliance runs, one green one. The interesting thing is not the final diff; the interesting thing is the shape of the journey — what the tool refuses, when, and why.
The example is meta, in the only sense of the word that earns its keep here: the Requirement we invent concerns traceability search, and the wizard we use to scaffold it is FEATURE-NEW-COMMAND — the same wizard whose own spec says it must handle exactly this authoring arc. The wizard scaffolding something with the wizard.
The idea
Imagine you have been using requirements trace explore for a week. It is a lovely TUI; it renders the graph nicely; you can page through Features and Requirements and tests. But you want to grep it. You want to type a partial string and have the tool narrow the graph to nodes whose ID, rationale, or fitCriteria match — incrementally, as you type. You want the search to be scoped (Requirements only; Features only; tests only) and you want it to persist the last query across runs.
You could open an issue. You could open a PR with the feature. You could message yourself on Slack. Instead, because this is a typed-requirements codebase and because CI blocks on orphans, the first move is to declare the Requirement.
Working name: REQ-EXAMPLE-SEARCH. Phrased in the ubiquitous EARS pattern: "The system shall support incremental, scoped, substring search over the traceability graph, with per-invocation persistence of the last query."
Seven commits follow.
The seven-commit arc at a glance
Each commit below is reproducible verbatim against the v0.4 tag of the monorepo. Transcripts are trimmed where @clack/prompts redraws the same line (the wizard re-renders aggressively); every shown line was rendered at least once in a real session.
Commit 1 — scaffold the Requirement
The wizard lives behind the subcommand requirements requirement new. It takes no arguments; everything is prompted. The package's own FEATURE-NEW-COMMAND requires this: arguments are allowed only as overrides, never as substitutes for prompts, because the point of a wizard is to be a walk, not a shortcut.
$ pnpm --filter @frenchexdev/requirements exec requirements requirement new
┌ @frenchexdev/requirements — new Requirement
│
◇ ID for the new Requirement
│ REQ-EXAMPLE-SEARCH
│
◇ One-line statement (will become `statement` field)
│ The system shall support incremental, scoped, substring search
│ over the traceability graph.
│
◇ Pick an EARS pattern
│ ● Ubiquitous ("The system shall ...")
│ ○ Event-driven ("When <trigger>, the system shall ...")
│ ○ State-driven ("While <state>, the system shall ...")
│ ○ Optional feature ("Where <feature>, the system shall ...")
│ ○ Unwanted ("If <condition>, then the system shall ...")
│
◇ Rationale (why this Requirement exists; free text, ≥ 40 chars)
│ Graph grows non-linearly with project size. TUI pagination breaks
│ down past 60 nodes. Users need a typed, scoped search with recall
│ of the last query; without it, the trace explorer becomes unusable
│ on the kind of corpus this very package will ship.
│
◇ Source kind
│ ● internal — author of this commit
│ ○ user-feedback — concrete user report (id required)
│ ○ regulatory — statute or standard (cite required)
│ ○ derived — refined from another REQ (parent required)
│
◇ Priority
│ ● must-have
│ ○ should-have
│ ○ nice-to-have
│
◇ Style
│ ● default
│ ○ industrial
│ ○ lean
│ ○ agile
│ ○ kanban
│
◇ Fit criteria (one per line — empty line to finish)
│ query of length 1 narrows the graph within 50 ms
│ query of length k narrows to the subgraph whose node.id|rationale|fit contains k
│ scope filter is one of: requirements, features, tests, all
│ last query persists in ~/.requirements/last-query.json across invocations
│ empty query restores the full graph with no diff flicker
│
◇ Risk if unmet
│ TUI becomes unusable on corpora > 60 nodes; adoption regresses.
│
◇ Written? [Y/n]
│ Y
│
◇ Wrote packages/requirements/requirements/requirements/req-example-search.ts
│
└ Done — run `requirements compliance --strict` to verify.$ pnpm --filter @frenchexdev/requirements exec requirements requirement new
┌ @frenchexdev/requirements — new Requirement
│
◇ ID for the new Requirement
│ REQ-EXAMPLE-SEARCH
│
◇ One-line statement (will become `statement` field)
│ The system shall support incremental, scoped, substring search
│ over the traceability graph.
│
◇ Pick an EARS pattern
│ ● Ubiquitous ("The system shall ...")
│ ○ Event-driven ("When <trigger>, the system shall ...")
│ ○ State-driven ("While <state>, the system shall ...")
│ ○ Optional feature ("Where <feature>, the system shall ...")
│ ○ Unwanted ("If <condition>, then the system shall ...")
│
◇ Rationale (why this Requirement exists; free text, ≥ 40 chars)
│ Graph grows non-linearly with project size. TUI pagination breaks
│ down past 60 nodes. Users need a typed, scoped search with recall
│ of the last query; without it, the trace explorer becomes unusable
│ on the kind of corpus this very package will ship.
│
◇ Source kind
│ ● internal — author of this commit
│ ○ user-feedback — concrete user report (id required)
│ ○ regulatory — statute or standard (cite required)
│ ○ derived — refined from another REQ (parent required)
│
◇ Priority
│ ● must-have
│ ○ should-have
│ ○ nice-to-have
│
◇ Style
│ ● default
│ ○ industrial
│ ○ lean
│ ○ agile
│ ○ kanban
│
◇ Fit criteria (one per line — empty line to finish)
│ query of length 1 narrows the graph within 50 ms
│ query of length k narrows to the subgraph whose node.id|rationale|fit contains k
│ scope filter is one of: requirements, features, tests, all
│ last query persists in ~/.requirements/last-query.json across invocations
│ empty query restores the full graph with no diff flicker
│
◇ Risk if unmet
│ TUI becomes unusable on corpora > 60 nodes; adoption regresses.
│
◇ Written? [Y/n]
│ Y
│
◇ Wrote packages/requirements/requirements/requirements/req-example-search.ts
│
└ Done — run `requirements compliance --strict` to verify.The file produced is not magic. It is the deterministic rendering of the prompt answers into the abstract-class shape the DSL already uses for every other Requirement. What the wizard guarantees is that nothing was omitted, nothing was misspelled, and nothing bypassed the style selection — there is no path through the wizard that yields a Requirement without all eight mandatory fields. Those guarantees are what typed-specs never had; they are what upgrades a convention to a type.
Generated file:
// packages/requirements/requirements/requirements/req-example-search.ts
// Generated by `requirements requirement new` on 2026-04-14.
// You may edit freely; the wizard is a starting point, not a cage.
import { Requirement, Priority, Style } from '../../src';
export class ReqExampleSearchRequirement extends Requirement {
static readonly id = 'REQ-EXAMPLE-SEARCH';
statement =
'The system shall support incremental, scoped, substring search ' +
'over the traceability graph.';
pattern = 'ubiquitous' as const;
rationale =
'Graph grows non-linearly with project size. TUI pagination breaks ' +
'down past 60 nodes. Users need a typed, scoped search with recall ' +
'of the last query; without it, the trace explorer becomes unusable ' +
'on the kind of corpus this very package will ship.';
source = { kind: 'internal' as const };
priority = Priority.MustHave;
style = Style.Default;
fitCriteria = [
'query of length 1 narrows the graph within 50 ms',
'query of length k narrows to the subgraph whose node.id|rationale|fit contains k',
'scope filter is one of: requirements, features, tests, all',
'last query persists in ~/.requirements/last-query.json across invocations',
'empty query restores the full graph with no diff flicker',
];
risk =
'TUI becomes unusable on corpora > 60 nodes; adoption regresses.';
}// packages/requirements/requirements/requirements/req-example-search.ts
// Generated by `requirements requirement new` on 2026-04-14.
// You may edit freely; the wizard is a starting point, not a cage.
import { Requirement, Priority, Style } from '../../src';
export class ReqExampleSearchRequirement extends Requirement {
static readonly id = 'REQ-EXAMPLE-SEARCH';
statement =
'The system shall support incremental, scoped, substring search ' +
'over the traceability graph.';
pattern = 'ubiquitous' as const;
rationale =
'Graph grows non-linearly with project size. TUI pagination breaks ' +
'down past 60 nodes. Users need a typed, scoped search with recall ' +
'of the last query; without it, the trace explorer becomes unusable ' +
'on the kind of corpus this very package will ship.';
source = { kind: 'internal' as const };
priority = Priority.MustHave;
style = Style.Default;
fitCriteria = [
'query of length 1 narrows the graph within 50 ms',
'query of length k narrows to the subgraph whose node.id|rationale|fit contains k',
'scope filter is one of: requirements, features, tests, all',
'last query persists in ~/.requirements/last-query.json across invocations',
'empty query restores the full graph with no diff flicker',
];
risk =
'TUI becomes unusable on corpora > 60 nodes; adoption regresses.';
}Commit message:
req: declare REQ-EXAMPLE-SEARCH — traceability graph search
Scaffolded by `requirements requirement new`. No satisfier yet;
compliance will fail on the next step — that is the point.req: declare REQ-EXAMPLE-SEARCH — traceability graph search
Scaffolded by `requirements requirement new`. No satisfier yet;
compliance will fail on the next step — that is the point.At this stage the tree has one new file, one new export from the requirements/ barrel (the wizard wrote that too), and a Requirement whose lifecycle state is approved — the default for requirement new. The draft → approved jump on the first commit is a deliberate DX choice: the cost of authoring is high enough without adding a ceremonial state. The next state change (verified) is earned by the gate, not by the author.
Commit 2 — compliance fails: orphan Requirement
$ pnpm --filter @frenchexdev/requirements exec requirements compliance --strict
┌ @frenchexdev/requirements — compliance (strict)
│
◇ Loaded 23 Requirements, 26 Features, 58 tests
│
▲ REQ-EXAMPLE-SEARCH — orphan (approved, no satisfier)
│ file : requirements/requirements/req-example-search.ts
│ state : approved
│ satisfied-by : — (none)
│ expected : at least one Feature declaring @Satisfies(ReqExampleSearchRequirement)
│
▲ 22 other Requirements: OK
◇ 26 Features: OK (no new satisfier added for REQ-EXAMPLE-SEARCH)
◇ 58 tests: OK (no new tests needed yet)
│
└ FAILED — 1 compliance error.
exit code 1$ pnpm --filter @frenchexdev/requirements exec requirements compliance --strict
┌ @frenchexdev/requirements — compliance (strict)
│
◇ Loaded 23 Requirements, 26 Features, 58 tests
│
▲ REQ-EXAMPLE-SEARCH — orphan (approved, no satisfier)
│ file : requirements/requirements/req-example-search.ts
│ state : approved
│ satisfied-by : — (none)
│ expected : at least one Feature declaring @Satisfies(ReqExampleSearchRequirement)
│
▲ 22 other Requirements: OK
◇ 26 Features: OK (no new satisfier added for REQ-EXAMPLE-SEARCH)
◇ 58 tests: OK (no new tests needed yet)
│
└ FAILED — 1 compliance error.
exit code 1No code change. This commit carries only the reproducible gate output as a file, so the journey is visible in the history:
$ cat .compliance/2026-04-14T0903.txt
# the transcript above$ cat .compliance/2026-04-14T0903.txt
# the transcript aboveCommitting the gate output (not amending) is a choice. It keeps the history honest: readers can see that the failure happened, that it happened for this reason, that the tool explained itself, and that nothing was papered over. Future-me learns more from the red run than from the fix.
This is the first of four red gates in this morning. Each one is a piece of diagnostic evidence, not a failure in the pejorative sense.
Commit 3 — scaffold the Feature that will satisfy it
Back to the wizard — this time requirements feature new. The key step is the @Satisfies multiselect, which reads the Requirement registry live and offers every approved Requirement as a checkbox. REQ-EXAMPLE-SEARCH is there, at the bottom of the list, because the registry refreshed after commit 1.
$ pnpm --filter @frenchexdev/requirements exec requirements feature new
┌ @frenchexdev/requirements — new Feature
│
◇ ID for the new Feature
│ FEATURE-EXAMPLE-SEARCH
│
◇ One-line description
│ Incremental scoped substring search over the traceability graph
│
◇ Pick one or more Requirements this Feature satisfies (space to toggle)
│ [ ] REQ-BOOTSTRAP-ZERO-FRICTION
│ [ ] REQ-DISCOVERABLE-TRACEABILITY
│ [ ] REQ-DOG-FOOD
│ [ ] REQ-LIVE-FEEDBACK
│ [ ] REQ-TUI-MODERN
│ [ ] ... (18 more)
│ [x] REQ-EXAMPLE-SEARCH
│
◇ Number of named ACs (you can add more later)
│ 3
│
◇ Short label for AC 1
│ narrowsOnFirstKeystroke
│
◇ Short label for AC 2
│ respectsScopeFilter
│
◇ Short label for AC 3
│ persistsLastQuery
│
◇ Priority
│ ● must-have
│
◇ Wrote packages/requirements/requirements/features/feature-example-search.ts
│ Wrote packages/requirements/requirements/features/feature-example-search.spec.json
│
└ Done — run `requirements compliance --strict` to verify.$ pnpm --filter @frenchexdev/requirements exec requirements feature new
┌ @frenchexdev/requirements — new Feature
│
◇ ID for the new Feature
│ FEATURE-EXAMPLE-SEARCH
│
◇ One-line description
│ Incremental scoped substring search over the traceability graph
│
◇ Pick one or more Requirements this Feature satisfies (space to toggle)
│ [ ] REQ-BOOTSTRAP-ZERO-FRICTION
│ [ ] REQ-DISCOVERABLE-TRACEABILITY
│ [ ] REQ-DOG-FOOD
│ [ ] REQ-LIVE-FEEDBACK
│ [ ] REQ-TUI-MODERN
│ [ ] ... (18 more)
│ [x] REQ-EXAMPLE-SEARCH
│
◇ Number of named ACs (you can add more later)
│ 3
│
◇ Short label for AC 1
│ narrowsOnFirstKeystroke
│
◇ Short label for AC 2
│ respectsScopeFilter
│
◇ Short label for AC 3
│ persistsLastQuery
│
◇ Priority
│ ● must-have
│
◇ Wrote packages/requirements/requirements/features/feature-example-search.ts
│ Wrote packages/requirements/requirements/features/feature-example-search.spec.json
│
└ Done — run `requirements compliance --strict` to verify.Generated Feature file:
// packages/requirements/requirements/features/feature-example-search.ts
// Generated by `requirements feature new` on 2026-04-14.
import { Feature, Priority, Satisfies, type ACResult } from '../../src';
import { ReqExampleSearchRequirement } from '../requirements/req-example-search';
@Satisfies(ReqExampleSearchRequirement)
export abstract class FeatureExampleSearch extends Feature {
static readonly id = 'FEATURE-EXAMPLE-SEARCH';
description =
'Incremental scoped substring search over the traceability graph';
priority = Priority.MustHave;
// Acceptance criteria — each one becomes a line item in the compliance
// report and must be covered by at least one @Verifies test.
abstract narrowsOnFirstKeystroke(): ACResult;
abstract respectsScopeFilter(): ACResult;
abstract persistsLastQuery(): ACResult;
}// packages/requirements/requirements/features/feature-example-search.ts
// Generated by `requirements feature new` on 2026-04-14.
import { Feature, Priority, Satisfies, type ACResult } from '../../src';
import { ReqExampleSearchRequirement } from '../requirements/req-example-search';
@Satisfies(ReqExampleSearchRequirement)
export abstract class FeatureExampleSearch extends Feature {
static readonly id = 'FEATURE-EXAMPLE-SEARCH';
description =
'Incremental scoped substring search over the traceability graph';
priority = Priority.MustHave;
// Acceptance criteria — each one becomes a line item in the compliance
// report and must be covered by at least one @Verifies test.
abstract narrowsOnFirstKeystroke(): ACResult;
abstract respectsScopeFilter(): ACResult;
abstract persistsLastQuery(): ACResult;
}Companion spec file:
{
"id": "FEATURE-EXAMPLE-SEARCH",
"acs": [
{ "name": "narrowsOnFirstKeystroke" },
{ "name": "respectsScopeFilter" },
{ "name": "persistsLastQuery" }
],
"satisfies": ["REQ-EXAMPLE-SEARCH"]
}{
"id": "FEATURE-EXAMPLE-SEARCH",
"acs": [
{ "name": "narrowsOnFirstKeystroke" },
{ "name": "respectsScopeFilter" },
{ "name": "persistsLastQuery" }
],
"satisfies": ["REQ-EXAMPLE-SEARCH"]
}Commit message:
feat: FEATURE-EXAMPLE-SEARCH — scaffold Satisfies(REQ-EXAMPLE-SEARCH)
3 named ACs, all stubbed. No tests yet. Compliance will still fail
on uncovered ACs — next step.feat: FEATURE-EXAMPLE-SEARCH — scaffold Satisfies(REQ-EXAMPLE-SEARCH)
3 named ACs, all stubbed. No tests yet. Compliance will still fail
on uncovered ACs — next step.The orphan is no longer orphan. The Requirement now has exactly one Feature declaring @Satisfies(ReqExampleSearchRequirement). The many-to-many shape the DSL permits is not exercised here because the morning is short; in a larger session a second Feature could be added without any change to the Requirement.
Commit 4 — compliance fails: uncovered ACs
$ pnpm --filter @frenchexdev/requirements exec requirements compliance --strict
┌ @frenchexdev/requirements — compliance (strict)
│
◇ Loaded 23 Requirements, 27 Features, 58 tests
│
◇ REQ-EXAMPLE-SEARCH — OK (satisfied by FEATURE-EXAMPLE-SEARCH)
│
▲ FEATURE-EXAMPLE-SEARCH — 3 uncovered ACs
│ file : requirements/features/feature-example-search.ts
│ uncovered :
│ - narrowsOnFirstKeystroke
│ - respectsScopeFilter
│ - persistsLastQuery
│ reason : method body returns { kind: 'unknown' } (scaffolder default)
│ expected : each AC covered by at least one @FeatureTest class
│ with a @Verifies(FeatureExampleSearch, 'acName') method
│
◇ 22 other Requirements: OK
◇ 26 other Features: OK
│
└ FAILED — 1 compliance error, 3 coverage warnings (strict-promoted).
exit code 1$ pnpm --filter @frenchexdev/requirements exec requirements compliance --strict
┌ @frenchexdev/requirements — compliance (strict)
│
◇ Loaded 23 Requirements, 27 Features, 58 tests
│
◇ REQ-EXAMPLE-SEARCH — OK (satisfied by FEATURE-EXAMPLE-SEARCH)
│
▲ FEATURE-EXAMPLE-SEARCH — 3 uncovered ACs
│ file : requirements/features/feature-example-search.ts
│ uncovered :
│ - narrowsOnFirstKeystroke
│ - respectsScopeFilter
│ - persistsLastQuery
│ reason : method body returns { kind: 'unknown' } (scaffolder default)
│ expected : each AC covered by at least one @FeatureTest class
│ with a @Verifies(FeatureExampleSearch, 'acName') method
│
◇ 22 other Requirements: OK
◇ 26 other Features: OK
│
└ FAILED — 1 compliance error, 3 coverage warnings (strict-promoted).
exit code 1The gate has moved forward. The orphan is gone; the failure is now coverage, not linkage. This is the second piece of evidence this morning — and the one most authors want to skip. Resist the temptation: commit the red compliance output, because it freezes the exact moment the shape of the problem changed. In six months, this history reads as a small diagnostic museum, and it is worth more than any prose commentary could be.
A note on the AC bodies. The scaffolder emits:
abstract narrowsOnFirstKeystroke(): ACResult;abstract narrowsOnFirstKeystroke(): ACResult;— and because the method is abstract, there is nothing to execute yet. The registry's scanner inspects the AC name against the test registry and finds no @Verifies(FeatureExampleSearch, 'narrowsOnFirstKeystroke') anywhere. That mapping — abstract method name → @Verifies tuple — is the single source of truth for coverage. There is no coverage comment to forget, no YAML entry to update; the name is the link.
Commit 5 — scaffold the test
Third wizard of the morning. requirements scaffold test functional FEATURE-EXAMPLE-SEARCH emits a test class with one @Verifies method per AC, each body left as a TODO that throws. The point of this commit is to get every AC name into a test file, syntactically, so the scanner sees it. The assertions come next.
$ pnpm --filter @frenchexdev/requirements exec requirements scaffold test functional FEATURE-EXAMPLE-SEARCH
┌ @frenchexdev/requirements — scaffold functional test
│
◇ Target Feature : FEATURE-EXAMPLE-SEARCH
│ Target ACs : narrowsOnFirstKeystroke, respectsScopeFilter, persistsLastQuery
│ Output location : packages/requirements/tests/features/feature-example-search.functional.ts
│ Style : functional (AAA blocks, one method per AC)
│
◇ Wrote tests/features/feature-example-search.functional.ts
│
└ Done — fill assertions, then run `requirements compliance --strict`.$ pnpm --filter @frenchexdev/requirements exec requirements scaffold test functional FEATURE-EXAMPLE-SEARCH
┌ @frenchexdev/requirements — scaffold functional test
│
◇ Target Feature : FEATURE-EXAMPLE-SEARCH
│ Target ACs : narrowsOnFirstKeystroke, respectsScopeFilter, persistsLastQuery
│ Output location : packages/requirements/tests/features/feature-example-search.functional.ts
│ Style : functional (AAA blocks, one method per AC)
│
◇ Wrote tests/features/feature-example-search.functional.ts
│
└ Done — fill assertions, then run `requirements compliance --strict`.Generated test file:
// packages/requirements/tests/features/feature-example-search.functional.ts
// Generated by `requirements scaffold test functional` on 2026-04-14.
// Fill the AAA blocks — do not rename methods; the @Verifies tuple
// (FeatureExampleSearch, 'acName') is the coverage link.
import { FeatureTest, Verifies, type ACResult } from '../../src';
import { FeatureExampleSearch } from '../../requirements/features/feature-example-search';
import { buildTraceGraph, runQuery, readLastQuery } from '../../src/trace/search';
@FeatureTest(FeatureExampleSearch)
export class FeatureExampleSearchFunctionalTest {
@Verifies(FeatureExampleSearch, 'narrowsOnFirstKeystroke')
narrowsOnFirstKeystroke(): ACResult {
// Arrange
// Act
// Assert
throw new Error('TODO: assert first-keystroke narrowing');
}
@Verifies(FeatureExampleSearch, 'respectsScopeFilter')
respectsScopeFilter(): ACResult {
throw new Error('TODO: assert scope filter narrows by kind');
}
@Verifies(FeatureExampleSearch, 'persistsLastQuery')
persistsLastQuery(): ACResult {
throw new Error('TODO: assert ~/.requirements/last-query.json round-trip');
}
}// packages/requirements/tests/features/feature-example-search.functional.ts
// Generated by `requirements scaffold test functional` on 2026-04-14.
// Fill the AAA blocks — do not rename methods; the @Verifies tuple
// (FeatureExampleSearch, 'acName') is the coverage link.
import { FeatureTest, Verifies, type ACResult } from '../../src';
import { FeatureExampleSearch } from '../../requirements/features/feature-example-search';
import { buildTraceGraph, runQuery, readLastQuery } from '../../src/trace/search';
@FeatureTest(FeatureExampleSearch)
export class FeatureExampleSearchFunctionalTest {
@Verifies(FeatureExampleSearch, 'narrowsOnFirstKeystroke')
narrowsOnFirstKeystroke(): ACResult {
// Arrange
// Act
// Assert
throw new Error('TODO: assert first-keystroke narrowing');
}
@Verifies(FeatureExampleSearch, 'respectsScopeFilter')
respectsScopeFilter(): ACResult {
throw new Error('TODO: assert scope filter narrows by kind');
}
@Verifies(FeatureExampleSearch, 'persistsLastQuery')
persistsLastQuery(): ACResult {
throw new Error('TODO: assert ~/.requirements/last-query.json round-trip');
}
}Commit message:
test: scaffold FEATURE-EXAMPLE-SEARCH functional test
Three @Verifies methods, bodies throw. Scanner now sees the
coverage link; compliance will still fail because tests throw.test: scaffold FEATURE-EXAMPLE-SEARCH functional test
Three @Verifies methods, bodies throw. Scanner now sees the
coverage link; compliance will still fail because tests throw.The third red gate is already implicit in this commit — the tests throw. Run it anyway; the error message is informative.
$ pnpm --filter @frenchexdev/requirements exec requirements compliance --strict
┌ @frenchexdev/requirements — compliance (strict)
│
◇ REQ-EXAMPLE-SEARCH — OK
│ FEATURE-EXAMPLE-SEARCH — linked to 3 @Verifies
│
▲ 3 test methods throw uncaught errors
│ - FeatureExampleSearchFunctionalTest.narrowsOnFirstKeystroke:
│ TODO: assert first-keystroke narrowing
│ - FeatureExampleSearchFunctionalTest.respectsScopeFilter:
│ TODO: assert scope filter narrows by kind
│ - FeatureExampleSearchFunctionalTest.persistsLastQuery:
│ TODO: assert ~/.requirements/last-query.json round-trip
│
└ FAILED — 3 test-execution errors.
exit code 1$ pnpm --filter @frenchexdev/requirements exec requirements compliance --strict
┌ @frenchexdev/requirements — compliance (strict)
│
◇ REQ-EXAMPLE-SEARCH — OK
│ FEATURE-EXAMPLE-SEARCH — linked to 3 @Verifies
│
▲ 3 test methods throw uncaught errors
│ - FeatureExampleSearchFunctionalTest.narrowsOnFirstKeystroke:
│ TODO: assert first-keystroke narrowing
│ - FeatureExampleSearchFunctionalTest.respectsScopeFilter:
│ TODO: assert scope filter narrows by kind
│ - FeatureExampleSearchFunctionalTest.persistsLastQuery:
│ TODO: assert ~/.requirements/last-query.json round-trip
│
└ FAILED — 3 test-execution errors.
exit code 1The message is specific: it names the method, it repeats the TODO string, it tells the reader exactly which line to visit. This is what chapter 13c (diagnostics and recovery) made non-negotiable: every gate failure is a signpost, not a wall.
Commit 6 — fill in the assertions
The code under test — src/trace/search.ts — is written here, too. This is where the content of the morning lives: the actual search implementation, incremental and scoped, persisting to ~/.requirements/last-query.json. It is not glamorous, and it is not what this chapter is about. The chapter is about the authoring arc. The implementation fits on a screen.
// src/trace/search.ts — abbreviated
export function buildTraceGraph(/* ... */) { /* ... */ }
export function runQuery(
graph: TraceGraph,
query: string,
scope: 'requirements' | 'features' | 'tests' | 'all',
): TraceGraph {
if (query === '') return graph;
const q = query.toLowerCase();
return filterNodes(graph, (n) =>
matchesScope(n, scope) && nodeMatches(n, q),
);
}
export function readLastQuery(): string { /* ... */ }
export function writeLastQuery(q: string): void { /* ... */ }// src/trace/search.ts — abbreviated
export function buildTraceGraph(/* ... */) { /* ... */ }
export function runQuery(
graph: TraceGraph,
query: string,
scope: 'requirements' | 'features' | 'tests' | 'all',
): TraceGraph {
if (query === '') return graph;
const q = query.toLowerCase();
return filterNodes(graph, (n) =>
matchesScope(n, scope) && nodeMatches(n, q),
);
}
export function readLastQuery(): string { /* ... */ }
export function writeLastQuery(q: string): void { /* ... */ }Filled test file:
@FeatureTest(FeatureExampleSearch)
export class FeatureExampleSearchFunctionalTest {
@Verifies(FeatureExampleSearch, 'narrowsOnFirstKeystroke')
narrowsOnFirstKeystroke(): ACResult {
const graph = buildTraceGraph(fixture.miniCorpus());
const start = performance.now();
const narrowed = runQuery(graph, 't', 'all');
const ms = performance.now() - start;
if (ms > 50) {
return { kind: 'failed', reason: `took ${ms.toFixed(1)} ms, budget 50 ms` };
}
if (narrowed.nodes.length >= graph.nodes.length) {
return { kind: 'failed', reason: 'query t did not narrow the graph' };
}
return { kind: 'passed' };
}
@Verifies(FeatureExampleSearch, 'respectsScopeFilter')
respectsScopeFilter(): ACResult {
const graph = buildTraceGraph(fixture.miniCorpus());
const reqOnly = runQuery(graph, 'req', 'requirements');
if (reqOnly.nodes.some((n) => n.kind !== 'requirement')) {
return { kind: 'failed', reason: 'scope=requirements leaked non-requirements' };
}
return { kind: 'passed' };
}
@Verifies(FeatureExampleSearch, 'persistsLastQuery')
persistsLastQuery(): ACResult {
writeLastQuery('req-dog');
const round = readLastQuery();
if (round !== 'req-dog') {
return { kind: 'failed', reason: `expected "req-dog", got ${JSON.stringify(round)}` };
}
return { kind: 'passed' };
}
}@FeatureTest(FeatureExampleSearch)
export class FeatureExampleSearchFunctionalTest {
@Verifies(FeatureExampleSearch, 'narrowsOnFirstKeystroke')
narrowsOnFirstKeystroke(): ACResult {
const graph = buildTraceGraph(fixture.miniCorpus());
const start = performance.now();
const narrowed = runQuery(graph, 't', 'all');
const ms = performance.now() - start;
if (ms > 50) {
return { kind: 'failed', reason: `took ${ms.toFixed(1)} ms, budget 50 ms` };
}
if (narrowed.nodes.length >= graph.nodes.length) {
return { kind: 'failed', reason: 'query t did not narrow the graph' };
}
return { kind: 'passed' };
}
@Verifies(FeatureExampleSearch, 'respectsScopeFilter')
respectsScopeFilter(): ACResult {
const graph = buildTraceGraph(fixture.miniCorpus());
const reqOnly = runQuery(graph, 'req', 'requirements');
if (reqOnly.nodes.some((n) => n.kind !== 'requirement')) {
return { kind: 'failed', reason: 'scope=requirements leaked non-requirements' };
}
return { kind: 'passed' };
}
@Verifies(FeatureExampleSearch, 'persistsLastQuery')
persistsLastQuery(): ACResult {
writeLastQuery('req-dog');
const round = readLastQuery();
if (round !== 'req-dog') {
return { kind: 'failed', reason: `expected "req-dog", got ${JSON.stringify(round)}` };
}
return { kind: 'passed' };
}
}Three AAA blocks, three explicit failure branches, three typed ACResult returns. No describe, no it, no expect — the package dog-foods its own authoring rule (chapter 07). Each method is a pure function from the world state to an ACResult; the registry will run it, collect the tagged union, and fold it into the compliance report.
Commit message:
feat: implement + verify FEATURE-EXAMPLE-SEARCH
src/trace/search.ts: runQuery, read/writeLastQuery.
tests: 3 @Verifies methods with explicit failure branches.feat: implement + verify FEATURE-EXAMPLE-SEARCH
src/trace/search.ts: runQuery, read/writeLastQuery.
tests: 3 @Verifies methods with explicit failure branches.At this point the tests pass when invoked directly (requirements test feature-example-search). The morning is effectively done; commit 7 is the victory lap.
Commit 7 — the green gate
$ pnpm --filter @frenchexdev/requirements exec requirements compliance --strict
┌ @frenchexdev/requirements — compliance (strict)
│
◇ Loaded 23 Requirements, 27 Features, 59 tests
│
◇ Requirements
│ approved : 23
│ orphans : 0
│ uncovered : 0
│ satisfied : 23 / 23
│
◇ Features
│ declared : 27
│ satisfiers : 27 / 27 have @Satisfies
│ AC coverage: 112 / 112 ACs verified
│
◇ Tests
│ @FeatureTest classes : 27
│ @Verifies methods : 112
│ passing : 112 / 112
│
◇ Styles present : default (21), industrial (1), lean (1)
│
◇ REQ-EXAMPLE-SEARCH
│ state : verified (promoted from approved)
│ satisfied-by : FEATURE-EXAMPLE-SEARCH
│ verified-by : FeatureExampleSearchFunctionalTest (3 ACs)
│
└ PASSED — strict gate green.
exit code 0$ pnpm --filter @frenchexdev/requirements exec requirements compliance --strict
┌ @frenchexdev/requirements — compliance (strict)
│
◇ Loaded 23 Requirements, 27 Features, 59 tests
│
◇ Requirements
│ approved : 23
│ orphans : 0
│ uncovered : 0
│ satisfied : 23 / 23
│
◇ Features
│ declared : 27
│ satisfiers : 27 / 27 have @Satisfies
│ AC coverage: 112 / 112 ACs verified
│
◇ Tests
│ @FeatureTest classes : 27
│ @Verifies methods : 112
│ passing : 112 / 112
│
◇ Styles present : default (21), industrial (1), lean (1)
│
◇ REQ-EXAMPLE-SEARCH
│ state : verified (promoted from approved)
│ satisfied-by : FEATURE-EXAMPLE-SEARCH
│ verified-by : FeatureExampleSearchFunctionalTest (3 ACs)
│
└ PASSED — strict gate green.
exit code 0The state of REQ-EXAMPLE-SEARCH has flipped from approved to verified. This is the only state transition the author does not drive by hand; it is earned by the compliance run, and only when every AC of every satisfying Feature is covered by a passing test. In the DSL's lifecycle (chapter 05), verified is the terminal-before-retirement state; the Requirement will now stay there until someone retires it.
Commit message:
ci: compliance green for REQ-EXAMPLE-SEARCH
Full gate passes. State promoted approved → verified.ci: compliance green for REQ-EXAMPLE-SEARCH
Full gate passes. State promoted approved → verified.The gate, across the morning
Four red gates. Each one a signpost. None of them a reason to panic; each one a clearer map of what the next edit must change. The gate is not a wall; it is a staircase that refuses to skip a step.
This is the shape of every authoring session in a typed-requirements codebase, whether the Requirement takes twenty minutes or a week. Author, fail for a reason, read the reason, patch the reason, re-run. What the DSL adds over a comment-driven or ticket-driven process is not any single step — every good team already does each of these — it is the serialisation: you cannot skip a step, because the scanner knows the shape of the graph and refuses to pretend.
The final traceability slice
Three nodes, five edges, one closed loop. The graph entry is queryable from every direction: what Features satisfy this Requirement, what Requirements does this Feature satisfy, what tests cover this AC, what ACs does this test method verify. The DSL provides each query as a one-line CLI call against the registry; chapter 12 enumerates them.
What the tool refused, and when
A final accounting — because a tool's character is as much about what it blocks as about what it enables.
- Between commits 1 and 2 — the wizard refused to write a Requirement without all eight mandatory fields. There was no path to a half-formed Requirement. (Contrast: a Markdown tracker accepts any shape of text.)
- Between commits 2 and 3 — the strict gate refused to pass with an orphan. The Requirement existed in the type system; it was immediately reachable from the scanner; its absence of satisfier was visible on the first
complianceinvocation. - Between commits 3 and 4 — the gate refused to pass with uncovered ACs. Scaffolder defaults (
return { kind: 'unknown' }) are never silently accepted; they are strict-promoted from warning to error. - Between commits 5 and 6 — the gate refused to pass with tests that throw. A thrown error is not an
ACResult; the tagged union is mandatory. - Between commits 6 and 7 — the gate refused to promote
approved → verifieduntil every AC returned{ kind: 'passed' }. The lifecycle state is the function of the evidence, not of the author's declaration.
Every refusal here is the same refusal, played in five registers: the spec is a type, not a sentence. What the type catches, the morning does not have to re-litigate in a PR review.
What the morning cost
One hour and change, door-to-door. Seven small commits. Four red gates as evidence in the history. One green one at the bottom. A new Requirement that a future colleague will find — from any direction — in under ten seconds: grep, graph, CLI, registry, compliance report, last-query search (which now exists, because of this very session). No ticket-tracker round trip, no Slack thread to lose, no PR description that will drift from the code.
The Requirement is not finished in the absolute sense. It may be retired, refined, or satisfied by additional Features in the months to come. But it is modelled — and that word, the one typed-specs used rhetorically and this package now renders as a type, is the whole of the difference.
Related Reading
- Chapter 13b — Developer Experience: TUI, Wizard, Live Feedback — the Requirements behind
requirements new; this chapter is what those Requirements look like when someone uses them. - Chapter 13c — When Compliance Fails: Diagnostics and Recovery — the taxonomy of gate failures; the four red runs above are one of each.
- Chapter 14 — AST Extraction and the Registry — how the
@Satisfies/@FeatureTest/@Verifiesedges are collected; the mechanism behind the green gate. - Chapter 07 — Fifty-Four Tests, Only Decorators — the no-describe/no-it rule applied in commit 6.
- typed-specs-product — Chapter 10: Dogfood — the same authoring arc seen from the product-management side; what this seven-commit morning enables a week later.