Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Chapter 02 — Why Dog-Food a Requirements DSL

A DSL whose authors do not use it has nothing to recommend it.

This chapter sits at position 02 in a thirty-three-chapter series. Position 02 matters: it is the first chapter that takes the DSL as given and asks what it is for. The previous chapters framed history and motivation; the next chapters will tour the decorator surface, the twenty-two Requirements, the five Styles, the four verification methods, and the test-surface-tour that closes the practical half of the series. Here, between retrospection and tour, is the single argument the whole series hangs on: a requirements DSL worth using is one whose authors use it on itself. The word for this is dog-fooding, and the Requirement that encodes it — REQ-DOG-FOOD — is what this chapter takes apart field by field.

Three sentences will frame what follows. First: a DSL whose authors do not use it has nothing to recommend it — it is a pamphlet, not an artefact. Second: the @frenchexdev/requirements package ships a Requirement called REQ-DOG-FOOD whose statement, when read literally, mandates that the package's own tests be written with its own decorators — @FeatureTest, @Verifies, @Satisfies — with zero describe and zero it. Third: this is not a posture. It is a hard gate inside the scanner the package itself ships. Run npx requirements compliance --strict against the package and a single stray describe( in the test tree fails the build.

The rest of this chapter does three things. It quotes the Requirement in full and unpacks every field, because the shape of this one class is the shape of the argument. It states the meta-circular paradox plainly — a Requirement inside a package mandating how that same package's tests are written — and shows why the paradox is actually a fixed point, not an infinite regress. And it carries the running example from chapter 00 — FEATURE-TRACE-EXPLORER-TUI — through the loop, so the abstraction lands on a concrete class.

A brief orientation for readers arriving here without having read the preceding chapters. Chapter 00 argued that the earlier typed-specs series used the word Requirement without modelling a Requirement type — the vocabulary was rhetorical, the types were Feature-only. Chapter 01 walked the forty-one-commit path by which the Requirement type was added. Chapter 01b reflected on why that order — Feature first, Requirement later — is not an accident but a pattern. This chapter takes the finished DSL as given and asks the first honest question about it: if the DSL is now a real thing, do its authors use it? The answer is the shape of REQ-DOG-FOOD.

Quoting the REQ in full

Here is the Requirement verbatim, copied from packages/requirements/requirements/requirements/req-dog-food.ts:

import { Requirement, Priority } from '../../src/base';
import type { DefaultStyleType } from '../../src/styles/default';

export abstract class ReqDogFoodRequirement extends Requirement<DefaultStyleType> {
  readonly id = 'REQ-DOG-FOOD';
  readonly title = 'The DSL must be tested with itself — @FeatureTest/@Verifies only, never describe/it';
  readonly priority = Priority.Critical;
  readonly status = 'Approved' as const;
  readonly kind = 'Constraint' as const;

  readonly statement = {
    pattern: 'ubiquitous' as const,
    response: 'use @FeatureTest and @Verifies for every test of every new command, against a Feature class under requirements/features/, and every Feature class must declare @Satisfies listing the Requirements it helps meet.',
  };

  readonly rationale = {
    claim: 'A DSL whose authors do not dog-food it has no credibility; this is already a non-negotiable project rule recorded in user memory.',
    kind: 'principle' as const,
    evidence: [
      { kind: 'precedent' as const, requirement: 'feedback_no_describe_it',
        rationale: 'User memory rule, absolute across all packages.' },
    ],
  };

  readonly fitCriteria = [
    { kind: 'quality-gate' as const, tool: 'rg', rule: '`rg "\\b(describe|it)\\(" packages/requirements/test` must return zero matches' },
    { kind: 'unit-test' as const, describes: 'every Feature class must be linked to ≥ 1 Requirement via @Satisfies; compliance --strict fails on any orphan',
      binds: ['satisfiesDecoratorRegistersBidirectionalLink', 'complianceStrictFailsOnFeatureWithoutSatisfies'] },
    { kind: 'coverage-threshold' as const, metric: 'line' as const, min: 98, scope: 'src/cli/**' },
  ];

  readonly verificationMethod = 'Test' as const;
  readonly source = { type: 'stakeholder' as const, role: 'project-rule', date: '2026-04-14' };
  readonly risk = { level: 'Critical' as const, ifNotMet: 'Package preaches a DSL it does not use; credibility collapses; users reject.' };
}

Thirty-odd lines of TypeScript. Every field is load-bearing. Take them one at a time.

id, title, priority, status, kind

The identifier REQ-DOG-FOOD is what the scanner will emit on reports, what other artefacts cross-reference, what the @Satisfies decorator on downstream Features will resolve to. IDs in this DSL are human-readable but not free-form — they follow a REQ-<SCREAMING-KEBAB> convention enforced by a style validator (chapter 08 covers style-driven ID validation). The scanner uses the ID as the primary key in its internal registry; every cross-reference in a compliance report resolves to this eight-character string.

The title is the one-sentence human summary — the thing a reviewer reads in the compliance report when the gate fails. There is deliberate tension in it: "The DSL must be tested with itself" is the high concept, "@FeatureTest/@Verifies only, never describe/it" is the mechanical cash-out. Both halves belong, and the em-dash is load-bearing — it is the glue between the credibility claim and the verifiable rule.

Priority.Critical binds to a cross-cutting project convention: a critical Requirement blocks compliance --strict, a non-critical Requirement only surfaces as a warning. The four-valued priority enum (Critical, High, Medium, Low) is the same one the typed-specs series introduced on Features; reusing it on Requirements is a deliberate vocabulary choice. The same word means the same thing across the two strata.

status: 'Approved' means the Requirement is live, enforced, not in draft — an approved Requirement with zero satisfiers is itself a compliance failure (see chapter 06 for the approval-satisfier interaction). The four-valued status lifecycle is Draft → Proposed → Approved → Deprecated; a Requirement in Draft status is readable by the scanner but not enforced, and a Deprecated Requirement is not expected to have satisfiers any more (but it retains its history).

kind: 'Constraint' distinguishes this from a functional, quality, or interface Requirement. A Constraint is a rule that governs how other work is done, not a deliverable in its own right. Dog-fooding is a rule about testing, so it is a Constraint. The other three kinds correspond to different prose registers: a Functional Requirement says what the system must do (scan the source, produce a report), a Quality Requirement says how well it must do it (within 10 seconds, with 98% coverage), an Interface Requirement says what shape the boundary must have (CLI takes a --strict flag). Constraints cut across the other three.

statement.pattern: 'ubiquitous'

The statement is typed as an EarsStatement — one of five EARS patterns plus a natural-language fallback. EARS is the Easy Approach to Requirements Syntax, a small grammar developed at Rolls-Royce in the 2000s that classifies every Requirement into one of a handful of conditional shapes: ubiquitous, event-driven, state-driven, unwanted-behaviour, optional-feature, complex. Each pattern has a prose skeleton the Requirement text must fit. Ubiquitous is the unconditional pattern — the rule applies in all conditions, at all times, in every release.

REQ-DOG-FOOD is ubiquitous because there is no condition under which it would be legitimate to write a new @frenchexdev/requirements test using describe/it. There is no escape hatch, no "unless", no "when". The rule is absolute. A full tour of EARS patterns and the other four registers lives in chapter 11; for now the relevant fact is that the type system distinguishes "unconditional rule" from "event-driven rule" at compile time, and the scanner validates the prose fits the pattern.

The five EARS patterns, briefly, for the reader wanting context:

  • Ubiquitous — "The shall ." Applies always. REQ-DOG-FOOD.
  • Event-driven — "When , the shall ." Applies when a specific event happens.
  • State-driven — "While , the shall ." Applies while the system is in a given state.
  • Unwanted-behaviour — "If , then the shall ." Applies when something goes wrong.
  • Optional-feature — "Where , the shall ." Applies when a configurable feature is active.
  • Complex — Any combination of the above.

The first five are the structural patterns; the sixth is the fallback for cases that combine them. EARS is prescriptive — a Requirement must fit one of the patterns or be tagged as Complex, there is no free-form prose allowed at the type level. This prescriptivism is often criticised (it can feel restrictive) but it pays off in exactly the situation REQ-DOG-FOOD exemplifies: a reader can tell at a glance that the rule is unconditional because the pattern field is ubiquitous. No conditional escape clauses to hunt for in the prose. The pattern label does the load that a long English sentence would otherwise have to carry.

statement.response — the rule itself

"use @FeatureTest and @Verifies for every test of every new command, against a Feature class under requirements/features/, and every Feature class must declare @Satisfies listing the Requirements it helps meet."

Parse the clauses. First: every test of every new command must use @FeatureTest and @Verifies. Quantifier on the test side — the domain is "all tests of all commands". Second: tests must target a Feature class under requirements/features/ — the domain on the Feature side is "everything under that folder". Third: every Feature class must declare @Satisfies listing the Requirements it helps meet — the closure relation. No Feature is a satisfaction orphan.

Three clauses, three axes of coverage. Test → Feature (via @FeatureTest). Test method → AC (via @Verifies). Feature → Requirement (via @Satisfies). The response string is not a single rule; it is a closure condition on the four-tier chain, collapsed into one sentence because EARS wants it in one sentence. Each clause binds to a distinct fit criterion below.

One feature of the prose deserves note: the rule mentions new commands rather than "all commands". This is deliberate. Legacy tests that predate the Requirement can — in principle — be grandfathered. In practice none were; the Requirement was added while the test tree was still small enough to convert all at once. But the prose leaves room for a common real-world situation (retrofitting a rule onto an existing codebase) that the fit criteria then tighten into a hard zero-match assertion. Prose and criteria are not literally identical; the prose is the general principle, the fit criteria are the current enforcement level. This is how EARS statements work in general — the response is the rule, the fit criteria are the measurement. Change the measurement, the rule still stands.

A second note: the rule names a specific directory, requirements/features/. This is the package-specific convention; the Feature classes live there, next to a sibling requirements/requirements/ directory holding Requirement classes. Other projects adopting the DSL can put their Features anywhere — the loadFeatures scanner takes a configurable root via its port. The Requirement is written against the package's own layout, but the decorator semantics are location-independent. A downstream adopter would write their own version of REQ-DOG-FOOD pointing at their own directory structure, or not have one at all if they chose a different testing style. The Requirement is not trying to legislate for other projects. It is legislating for this one.

rationale.claim — the credibility argument

"A DSL whose authors do not dog-food it has no credibility; this is already a non-negotiable project rule recorded in user memory."

Two moves in one sentence. The first is a general claim about all DSLs — the credibility thesis. The second is a specific claim about this project — the rule was already non-negotiable before the Requirement was written. The Requirement does not create the rule; it makes an existing rule formal. That distinction matters for the meta-circularity argument below: the Requirement is not legislating into a void, it is typing up a practice that pre-existed it.

rationale.kind: 'principle' and rationale.evidence

The rationale's kind discriminator is one of principle | metric | incident | study | expert-opinion | regulation | precedent. A principle rationale does not cite a number or an incident — it cites a stance. The evidence is therefore not a measurement; it is a precedent:

{ kind: 'precedent' as const, requirement: 'feedback_no_describe_it',
  rationale: 'User memory rule, absolute across all packages.' },

The requirement: slot on an evidence record is free-form (a string ID of the precedent), and here it names feedback_no_describe_it — one of the entries in the user's auto-memory index. Concretely: the Requirement's stated evidence is a rule in the author's memory file. The rule is external to this package (it applies to every package the author writes), but the artefact that records it — a markdown note — is part of the same closed, versioned, author-owned system that the Requirement inhabits.

This is the first place the meta-circularity peeks through. The rationale for the Requirement that makes the package self-verifying is itself an artefact the system treats as a source. The evidence is inside the loop.

There is a more careful version of this observation. The seven kinds of rationale (principle, metric, incident, study, expert-opinion, regulation, precedent) span the range from hard to soft justification. A regulation rationale cites an external legal or standards body and carries the weight of external authority. A metric rationale cites an empirical measurement — typically a defect rate or a user-impact number — and carries the weight of evidence. An incident rationale cites a specific past failure — "we shipped this bug on 2024-03-15, this Requirement exists to prevent recurrence". A study cites published research. An expert-opinion cites a named practitioner. A precedent cites a prior internal rule. A principle — the weakest kind, in the sense of least externally anchored — cites a stance.

REQ-DOG-FOOD's rationale is principle. The stance is "a DSL whose authors do not dog-food it has no credibility". The precedent evidence ties it back to a prior rule in user memory — so the stance is not invented here, it is recorded here. This is honest taxonomy: the author does not pretend the rule comes from ISO or a post-mortem. The rule comes from the author's own judgment, previously recorded, now made binding at the package level. A reader evaluating the Requirement can weigh its kind appropriately. Principle-backed rules have lower external weight than incident-backed rules, and the kind field lets the reader apply that weighting.

The design choice worth noting is that the DSL forces a kind selection. Every Rationale field must declare a kind — there is no default, no "unspecified", no way to elide the question. The author either claims the rule is principle-backed or incident-backed or something else, and the claim is visible to every reader. A Requirement whose rationale kind is suspiciously weak ("principle" for a rule that really should cite an incident) is caught by review.

fitCriteria — three criteria, three kinds

Fit criteria are the concrete, executable checks that say whether a Requirement is met. They are typed as a discriminated union with seven kinds; this Requirement uses three of them.

{ kind: 'quality-gate' as const, tool: 'rg',
  rule: '`rg "\\b(describe|it)\\(" packages/requirements/test` must return zero matches' },

The first is a quality-gate criterion bound to the rg (ripgrep) tool. The rule is a raw ripgrep invocation. The assertion is zero matches — no file in packages/requirements/test contains a describe( or it( token. This is not a lint in the style sense; it is a grep. Grep is simple enough to be auditable by anyone who reads the Requirement. You do not need to understand the scanner to verify this fit criterion; you need to run a shell command.

There is a design decision worth unpacking here. The DSL could have required a richer integration — a bespoke TypeScript lint rule, a parser plugin, a specialised AST walker. It chose ripgrep. The choice costs precision (a describe in a comment is, as noted above, ignored by the word-boundary anchor, but a very cleverly crafted false positive could slip through) in exchange for auditability. A user who reads the Requirement and wonders "is the gate really that strict?" can run the ripgrep themselves in one second. No build setup, no plugin install, no version compatibility. The check is transparent by being dumb. The dumbness is a feature.

Compare with a hypothetical alternative where the fit criterion said something like "an internal DescribeDetectingLinter runs with the strict profile on every test file". That phrasing is more precise but less auditable. A user wanting to verify the rule would have to inspect the linter's implementation, understand its options, reproduce its environment. Ripgrep is universal. The cost of universal tooling is that the tool is simple; the benefit is that anyone can run it. REQ-DOG-FOOD paid the cost for the benefit.

A side effect: when the package grows and the dog-food check starts to need more sophistication, the fit criterion will gain a second quality-gate entry. The existing ripgrep stays as a belt-and-braces check, and the new (more sophisticated) check is added alongside. The taxonomy is additive. A Requirement can carry any number of fit criteria of any kind.

{ kind: 'unit-test' as const,
  describes: 'every Feature class must be linked to ≥ 1 Requirement via @Satisfies; ...',
  binds: ['satisfiesDecoratorRegistersBidirectionalLink',
          'complianceStrictFailsOnFeatureWithoutSatisfies'] },

The second is a unit-test criterion. Its describes field states the rule in prose; its binds array names the AC methods that prove it. Two ACs, both inside the package — one on the decorator (the @Satisfies decorator must register a bidirectional link in the registry), one on the scanner (compliance --strict must fail on any Feature without @Satisfies). Note the pattern: a fit criterion that talks about Feature closure is verified by two unit tests on the mechanics that enforce that closure. The fit criterion names the ACs by their literal method names; those method names are checked by the compiler against the real Feature classes via keyof T & string on @Verifies. Misname an AC and the binding fails to type-check.

This is the first direct meta-circular binding in the Requirement. The Requirement names test methods whose existence proves the Requirement is enforced. Those test methods are inside the package. Those test methods use @Verifies (per REQ-DOG-FOOD itself). So the Requirement cites, as its own evidence, tests that comply with the Requirement. The self-reference is local and explicit: not a general claim ("all tests comply") but a pair of named ACs.

What happens if one of those named ACs is removed? The scanner fails. The binds array expects to find those specific method names in the test tree, bound to appropriate Feature classes. If they do not exist, the fit criterion cannot be checked, and the Requirement reports as unverifiable. Unverifiable is a distinct state from failed — a Requirement that cannot be checked is not green and not red, it is incomplete. The compliance report distinguishes the three states so the reader knows whether a red result is due to a genuine violation or to a broken fit criterion.

The binds mechanism is how fit criteria achieve stability under refactoring. As long as the two named ACs exist somewhere in the package, the Requirement can be verified, regardless of which Feature they live on or which test file contains them. A refactor that moves the AC from one Feature to another does not break the Requirement's check. What would break it is renaming the AC. That fragility is accepted; the method name is the contract between the Requirement and the tests, and a rename is equivalent to a contract change. If the contract changes, the Requirement must be updated alongside — this is exactly the kind of cross-artefact change the DSL makes visible.

{ kind: 'coverage-threshold' as const, metric: 'line' as const,
  min: 98, scope: 'src/cli/**' },

The third is a coverage-threshold criterion — 98% line coverage on src/cli/**. The CLI is the user-facing surface of the package; if the CLI is under-tested, the dog-food claim is performative. The scope glob is explicit; the metric is line (not branch or function, though both are separately tracked per the package's own vitest config). 98% is lower than the 100% the package currently runs at — the Requirement leaves a two-point slack so a brand-new command file can land before its coverage settles.

The choice of scope is worth noting. The glob src/cli/** covers only the command-line surface, not the entire source tree. Why? Because the CLI is where users meet the package. If a user types npx requirements compliance --strict and the command throws on a common input, the dog-food claim dies — the user never sees the well-tested internals, they see the badly-tested edge. The threshold guards the user-facing edge specifically. The internals (scanner core, analysis utilities, type helpers) are covered by other Requirements in the registry with their own thresholds. Layered coverage gates are one way the twenty-two Requirements divide the quality territory without any one Requirement having to carry all of it.

The choice of line over branch or function is another trade-off. Line coverage is easier to achieve and easier to interpret; branch coverage is stricter but can be hard to move past 95% without contrived tests. REQ-DOG-FOOD uses line because the goal is a baseline reach (every line is exercised at least once) rather than a combinatorial exhaustion (every branch is taken). Other Requirements in the registry — REQ-COVERAGE-MINIMA, specifically — set branch thresholds where they matter. Dividing the gates this way keeps each one readable and each threshold defensible on its own terms.

Why 98% and not 100%? The two-point slack is the practical answer: it lets a new command file land in one commit and receive its tests in a follow-up without blocking the build for half a day. The principle is that a gate should be tight enough to catch regressions but loose enough to allow incremental work. 100% gates create a cliff — a single new line is either covered or the build fails — and that cliff pushes authors toward over-long commits or skipped tests. 98% gates create a slope: add a new file, coverage drops to 98.something, add the tests, coverage returns to 100. The slope is more liveable than the cliff without sacrificing the gate.

Three kinds, one Requirement. One of them is a literal shell command; one is a pair of TypeScript AC methods; one is a number from the coverage tooling. The fit criteria taxonomy makes the check runnable without making the Requirement text unreadable — the prose stays in describes, the mechanism stays in the kind-and-tool fields.

The seven fit-criterion kinds the DSL ships — quality-gate, unit-test, coverage-threshold, latency, throughput, availability, security-control — are not arbitrary. They correspond to the seven most common measurement modes practitioners actually use when deciding whether a rule is met. A rule about test structure uses the grep-like quality-gate. A rule about behaviour uses a unit-test binding. A rule about code reach uses a coverage-threshold. A rule about response time uses latency (with p95 / p99 percentile selectors). A rule about security uses security-control (with CWE/CVE references). The kinds keep the fit-criteria taxonomy close to the measurement vocabulary real teams use, without forcing every rule into the same shape.

This is also the Style plug-in point. A custom Style can add new fit-criterion kinds — an industrial-safety Style might add SIL-level as a kind — but the core three used here (quality-gate, unit-test, coverage-threshold) are present in every Style because they are the three any software Requirement can have. The taxonomy is extensible without being unbounded.

verificationMethod: 'Test'

The four allowed verification methods are Test | Inspection | Analysis | Demonstration. The Requirement is verified by Test — which, given the content of the rule ("tests must use these decorators"), is the first self-referential move at the type level. The Requirement that governs tests is itself verified by tests. Bootstrapping concern? Not yet. See section 4.

The four-valued taxonomy follows the ISO/IEC/IEEE 29148 vocabulary and has earned its place in most formal requirements engineering traditions. Inspection means a human reads an artefact and checks a property — appropriate for documentation completeness, for instance. Analysis means a static or model-based check — appropriate for properties that cannot be tested dynamically but can be reasoned about from the source (dependency cycles, theorem-prover discharges, control-flow invariants). Demonstration means observing the system in operation without measuring specific assertions — appropriate for end-to-end scenarios where behaviour is judged subjectively. Test means running an executable check that returns pass/fail. REQ-DOG-FOOD is Test because its fit criteria are executable (the ripgrep runs, the unit tests run, the coverage measurement runs), and each returns a binary result.

A Requirement could, in principle, declare multiple verification methods — the data model allows it, though REQ-DOG-FOOD uses just one. A Requirement verified by both Test and Inspection would be one whose automated checks are supplemented by human review. The choice of a single Test method here is a statement that the fit criteria are complete — nothing is left for human eyeballs, the automation suffices. That in turn is a claim the author is willing to be held to; if a real dog-food violation slips past all three fit criteria, the fault is in the fit criteria, not in human vigilance.

source

readonly source = { type: 'stakeholder' as const, role: 'project-rule', date: '2026-04-14' };

The Requirement did not come from ISO 29148. It did not come from a safety standard. It came from the stakeholder — specifically, from the author-as-stakeholder acting in the role of project-rule-setter. This is honest. A requirements system that pretends every rule came from an external authority is lying about half its rules. Most rules in most projects come from project conventions, not from external standards, and the source taxonomy should reflect that. Eight kinds of source live in the RequirementSource union — stakeholder, regulation, standard, incident, empirical, analogy, heuristic, historical — and this one is of the first kind.

The role sub-field disambiguates the kind of stakeholder. "project-rule" is distinct from "product-owner" or "end-user" or "security-officer" — each role has different weight, different expertise, different scope of authority. The DSL does not enforce a closed list of roles (they are free strings) but it does require the author to write one. The act of naming the role is itself a discipline: the author must decide what hat they are wearing when imposing the rule. Here the hat is "project-rule", the author acting as the project's rule-setter. The date — 2026-04-14 — is the day the Requirement became binding, the cut-off date discussed earlier as the bootstrap point.

For comparison, a Requirement sourced from a regulation would look like:

readonly source = {
  type: 'regulation' as const,
  document: 'GDPR Article 32',
  jurisdiction: 'EU',
  effectiveDate: '2018-05-25',
};

Different shape, different weight. A regulation-sourced Requirement answers to the regulation's authority; a stakeholder-sourced Requirement answers to the named stakeholder. The compliance report can group Requirements by source kind, which is how a project audits "which of our rules have external backing and which are internal?". REQ-DOG-FOOD is in the internal bucket; that honesty is itself part of its credibility.

risk

readonly risk = { level: 'Critical' as const,
  ifNotMet: 'Package preaches a DSL it does not use; credibility collapses; users reject.' };

The risk text is the stake. Not a technical stake — a trust stake. If REQ-DOG-FOOD is violated, the package loses the property that makes it credible in the first place. A requirements DSL that does not eat its own food is a pamphlet. The risk level is Critical because credibility, once lost, is expensive to recover.

The risk field pairs a categorical level (Low | Medium | High | Critical) with a prose description of the failure mode. The level determines how the gate behaves: Critical risk Requirements block compliance --strict, High risk Requirements cause a warning exit code, Medium and Low are informational. The prose ifNotMet is what the reader reads when the gate fires. A useful prose is specific about consequence — not "bad things happen" but "users reject". The short sharp sentences in this field are chosen for the failure mode they describe; they should still make sense to a reader six months from now who has forgotten the original context.

There is a small discipline worth naming around risk prose. Writing "users reject" rather than "product fails" concentrates the loss on a specific audience and a specific outcome. The word "credibility" is load-bearing: the failure is not technical, it is social. A DSL that does not dog-food itself still compiles, still runs, still has passing tests. What it loses is the reader's trust. The risk prose names the concrete thing lost, not an abstraction.

Eleven fields, one class. The DSL's own axiom, written in the DSL. Reading the class top to bottom one more time now: identifier, title, priority, status, kind, statement, rationale, fit criteria, verification method, source, risk. Every field is part of the argument. None is ornamental. The decision to make Requirements rich — to make every Requirement carry eleven fields of metadata, not just a name and a rule — is in service of exactly this reading. A Requirement that can be fully understood from its own source text does not need a separate policy document. It is the policy document, and it happens to be typed.

The meta-circular paradox, stated plainly

Now the sharpened version of the question this chapter exists to answer.

REQ-DOG-FOOD lives inside packages/requirements/requirements/requirements/. The scanner it activates — the one that enforces its fit criteria — lives inside packages/requirements/src/cli/. The tests it governs live inside packages/requirements/test/. The decorators whose use it mandates live inside packages/requirements/src/decorators.ts. Every artefact the Requirement mentions, enforces, or depends on is in the same package as the Requirement itself.

Lay out the geography on a single line, so the co-location is visible:

packages/requirements/
├── src/
│   ├── decorators.ts              ← the decorators the Requirement mandates
│   ├── cli/                        ← the scanner that enforces the Requirement
│   └── base.ts                     ← Requirement/Feature base classes
├── requirements/
│   ├── requirements/
│   │   └── req-dog-food.ts         ← the Requirement itself
│   └── features/
│       └── feature-*.ts            ← Features that @Satisfies the Requirement
└── test/                           ← the tests the Requirement governs

Five directories, one package, one package.json. No sibling package provides a testing framework. No external tool provides a scanner. No parent repository provides a verification authority. When npx requirements compliance --strict runs, the binary is installed from this package; when the scanner walks the tree, the tree is this package; when the tree references decorators, the decorators are in this package. The artefact is internally complete.

One qualifier worth naming: the package does depend on a few external tools. ts-morph parses the source. vitest runs the tests and produces the coverage report. fast-check drives property-based invariants. ripgrep handles the grep fit criteria. These are dependencies, not verifiers. They provide infrastructure the scanner orchestrates; they do not replace the scanner's own work. A stricter reading of "self-contained" might object to the dependencies; a reasonable reading accepts them because they are public, stable, widely-adopted tools that the author did not write. The dog-food claim is not "this package has no dependencies". It is "this package does not use a sibling requirements DSL to verify itself". The dependencies it does have are of a different category.

For the package to comply with its own Requirement:

  1. The Requirement class (ReqDogFoodRequirement) must be picked up by a scanner.
  2. The scanner is shipped by the same package as the Requirement.
  3. The scanner must read the Requirement's fit criteria.
  4. The scanner must then check that the tests (which are themselves in the same package) use the decorators (which are defined in the same package) correctly.
  5. The scanner's own tests must themselves comply with the Requirement it is scanning.

Five steps, three uses of "in the same package". The loop is closed at every level. There is no external arbiter. There is no sibling framework checking the framework. There is no independent test harness. The package is its own verifier.

Is this circular? Yes. If by circular we mean "the thing being checked is the same as the thing doing the checking", then yes, it is circular three times over: the rule is in the package, the checker is in the package, the tests proving the checker are in the package.

Is this a paradox? No. That is the point of the next section.

Framing the worry precisely

The worry a careful reader might have at this point is not vague. It is a specific concern that shows up any time a system claims to verify itself. Roughly: if the verifier is part of the system being verified, then any bug in the verifier can hide itself. The verifier could — in principle — fail to notice its own bug. A self-certifying system risks becoming a tautology: the system is correct because the system says so.

This is a real concern, and it deserves a direct answer rather than a hand-wave. The reply, previewed here and expanded in the next section, is that the self-reference in REQ-DOG-FOOD is of a specific narrow kind: the Requirement constrains the form of tests (decorator-based, not describe-based) but not the content (what the tests actually assert). The Requirement does not say "all tests must pass". The Requirement says "all tests must be written in a particular way". The form is cheap to verify externally — a ripgrep catches it. The content is verified by ordinary test execution, which is subject to independent verification (the tests either throw or they do not). The two checks are coupled but distinguishable.

This is why the fit criteria are written as they are. The grep criterion is public and cross-checkable. The unit-test criterion names specific AC methods that a reader can locate and inspect. The coverage criterion delegates to vitest. None of the three requires trusting the scanner to police itself without external witness.

Why it's a fixed point, not an infinite regress

The word "paradox" carries the suggestion of ill-definedness — a statement that cannot be assigned a truth value, a Russell-set that shreds the foundations of the system it appears in. The meta-circle here is not of that kind. It is a fixed point in the well-defined sense: a configuration where the application of a rule returns the configuration unchanged.

The Y-combinator intuition, in one paragraph

In untyped lambda calculus the Y-combinator defines a recursive function without naming it: Y f = f (Y f). Read naively, the right-hand side expands forever — f (Y f) = f (f (Y f)) = f (f (f (Y f))) and so on. But if f is well-chosen — if its evaluation terminates on concrete arguments — the "expansion" never actually happens. The Y-combinator is a shape, not a computation. When f is applied to a base case, the recursion bottoms out. The apparent self-reference is harmless because the evaluation stratifies.

The package works on the same principle. The "apparent self-reference" of REQ-DOG-FOOD governing the tests that verify REQ-DOG-FOOD looks like infinite regress. It is not, because the evaluation stratifies: the scanner operates one level below the Requirement. The Requirement is data-at-rest when the scanner reads it. The scanner is code-in-motion when it runs. Data and code are different modes of being for the same source file, and the meta-circle threads between the two modes without collapsing them.

This is the same architectural move Hofstadter calls strange loops in Gödel, Escher, Bach and I Am a Strange Loop: the appearance of self-reference is real, but the recursion is well-founded because it passes through a level-shift (execution → text → execution) that cuts what would otherwise be an infinite loop into a finite process. A strange loop with a ground is a fixed point. A strange loop without a ground is a paradox. The difference is one inspection layer.

Source-as-data vs compiled scanner

Three distinct layers need to be kept apart to see why.

Layer 1 — execution. When npx requirements compliance --strict runs, the thing that executes is the compiled scanner. The scanner's TypeScript has been transpiled to JavaScript (via tsx or the package's build pipeline), loaded into the Node.js runtime, and is running as ordinary program code. At this layer, the scanner does not know it is "the scanner of its own package". It is just code.

Layer 2 — source-as-data. The scanner then reads source files. It reads req-dog-food.ts, feature-trace-explorer-tui.ts, every file in test/. It reads them as text. It parses them into ASTs via ts-morph. The content of those files is data to the scanner, not program. The scanner does not evaluate the Requirement; it inspects the Requirement's class shape — its decorators, its literal property values, its class name. The Requirement is treated as if it were a JSON blob with slightly richer types.

Layer 3 — compiled Requirement. The Requirement class itself is also compiled TypeScript — it is loaded into Node.js when any downstream Feature imports it via @Satisfies(ReqDogFoodRequirement, …). At this third layer the Requirement is code. But the scanner does not interact with the Requirement at this layer. The scanner's view is strictly layer 2.

The recursion terminates at layer 2 because source text is finite. The req-dog-food.ts file has thirty-odd lines. The scanner reads those thirty lines, extracts the fit criteria, and moves on. There is no re-entry. The scanner does not invoke the Requirement; it inspects it and leaves.

The bootstrap analogy

A compiler written in its own language is a standard example of well-founded self-reference. The TypeScript compiler is written in TypeScript. How does it compile itself?

The answer is the bootstrap: at some point, an ancestor version of the compiler was compiled by something else — an earlier JavaScript implementation, a hand-written parser, a bootstrap tool. That ancestor then compiled the next version. The next version compiled the one after. At each step, the compiler in hand (already compiled, already running as data-in, code-out) processes the source of its successor. No step is self-invocation. Every step is one compiled program reading one source file.

The technique has a name — bootstrapping, sometimes self-hosting. It is the standard way mature language implementations achieve independence from their parent language. GCC compiles itself. Rust compiles itself. The TypeScript compiler compiles itself. The first generation was written in another language; every subsequent generation is written in the language the compiler implements. There is no infinite regress because the chain has a terminating first link — a compiler written in assembly, or C, or whatever predecessor tool actually existed. After that first link, every step is well-founded.

@frenchexdev/requirements works the same way. At some point the package was built the first time. The build was not itself gated by compliance --strict, because compliance --strict did not yet exist. Then compliance --strict was added and run. It passed. From that moment onward, every subsequent change to the package is gated: before a PR lands, the current compiled scanner runs against the new source, and the gate either passes or fails.

The first run is a boot. Every subsequent run is a verification. There is no moment where the scanner must verify itself before existing. The existence comes from the bootstrap; the verification is what the bootstrap then licenses.

A minor philosophical point that the bootstrap analogy clarifies: the question "who verifies the first version of the verifier?" has an answer, and the answer is "nobody — it is accepted by convention, then every subsequent version is verified by its predecessor". This is not a flaw. This is how all self-hosting systems work. Asking for more is asking for something no actual system provides. The honest posture is to name the bootstrap and move on. REQ-DOG-FOOD names it, in the sense that the Requirement's creation date (2026-04-14, in the source field) records the moment the rule began to bind. Nothing earlier than that date was gated by the rule. Everything later than that date is. A cut-off date is the bootstrap made explicit.

Why the termination matters

If the recursion did not terminate — if, to verify the scanner, you had to first verify the verifier of the scanner, and so on — the package would be vacuously self-approving. An infinite regress collapses to "anything goes, there is no ground". What we have instead is a ground: the scanner operates on source text, source text is finite, the scan runs in bounded time, and the result is either green or red. The ground is not inside the loop; it is the layer-2 inspection that cuts the loop after one pass.

This is what distinguishes a credible self-referential system from a vacuous one. Self-reference with a ground is a fixed point. Self-reference without a ground is a paradox. REQ-DOG-FOOD has a ground: the finite source text of the package, readable by the scanner without executing any of its contents.

What the fixed point proves, what it does not

A fixed point is not a soundness proof. The scanner passing on its own source does not prove the scanner is correct — it proves the scanner is internally consistent with the rules it enforces. There is a gap between "the scanner enforces the rules correctly" and "the rules are the right rules". The first is a mechanical property, verifiable by running the scanner against the package. The second is a design claim, verifiable only by argument and experience.

REQ-DOG-FOOD's fit criteria are modest on purpose. "Zero describe/it tokens in the test tree" is a weak claim — it says nothing about whether the tests are good, only about whether they are written in the package's own DSL. A test file that uses @FeatureTest and @Verifies but asserts nothing would still pass the ripgrep check. The criterion is a necessary condition for dog-fooding, not a sufficient one. The other Requirements in the package — REQ-COVERAGE-MINIMA, REQ-PROPERTY-BASED-COVERAGE, REQ-RUN-COMPLIANCE-ON-SELF — fill in the sufficiency. No single Requirement carries the full argument; the closure is a mesh, not a chain.

This point matters for honest presentation. A fixed point is powerful because it closes a loop. It is limited because a closed loop is only as trustworthy as the rules the loop circulates. The value of REQ-DOG-FOOD is not that it certifies the package correct. Its value is that it prevents a specific failure mode — the failure where the authors preach one testing discipline and practise another. Preventing that failure mode raises the floor. It does not raise the ceiling.

Why the paradox matters for credibility

A DSL whose self-test is trivial passes nothing. A DSL whose self-test is the exact same rigour its users are expected to apply earns trust.

Every time a PR touches packages/requirements/src/**, the same compliance --strict the package tells its users to run blocks the PR if REQ-DOG-FOOD's fit criteria are violated. The author cannot quietly skip the rule. The gate is not an optional CI step on a cloud runner — per the project's operational posture (feedback_no_cloud_cicd) it is a pre-push check on the author's own machine, and a failure stops the workflow before anything reaches main.

Users of the package can, with one ls, verify the claim. The test directory contains about seventy test files across the four-tier chain. Every one of them exports a class decorated with @FeatureTest(SomeFeature). Every method on every class is decorated with @Verifies<SomeFeature>('someAc'). There is no describe. There is no it. A single rg '\b(describe|it)\(' packages/requirements/test/ — the exact invocation the fit criterion specifies — returns zero lines.

Compare two worlds.

In the first, a testing framework uses a second, independent testing framework to verify itself. This is the common case. Jest's test suite uses Jest for its unit tests but relies on substantial infrastructure outside Jest for its meta-tests. Mocha's test suite is similar. The arrangement is practical — it lets the authors move forward — but it introduces a trust gap: the second framework has not passed the bar the first framework is selling.

In the second, the framework uses itself to verify itself. This is rarer. It is harder, because the framework must be featureful enough on day one to carry its own test load. It is more credible, because every claim the framework makes about test quality is a claim its own tests meet. The @frenchexdev/requirements package is in the second camp.

This is not rhetoric. It is a structural property enforced by a grep.

A concrete example of what non-dog-food looks like

To sharpen the contrast, consider a hypothetical requirements DSL that has not adopted REQ-DOG-FOOD. Its test tree looks like this:

// test/scanner.test.ts
describe('RequirementScanner', () => {
  it('discovers requirements in the source tree', () => {
    const scanner = new RequirementScanner(...);
    expect(scanner.scan('./fixtures')).toHaveLength(3);
  });

  it('fails on orphan features', () => {
    const scanner = new RequirementScanner(...);
    expect(() => scanner.scanStrict('./fixtures/with-orphan'))
      .toThrow(/orphan feature/);
  });
});

The tests work. They run. The scanner is tested. But notice what the tests are not doing. They are not linking themselves to a Feature class. They are not declaring which AC each test satisfies. They are not participating in the four-tier chain the DSL provides. They are in a parallel world: the tests use Jest's describe/it, the DSL talks about Feature classes and ACs, and the two worlds never touch. A reader cannot ask "which AC does the test discovers requirements in the source tree verify?" — because the test does not declare one. The mapping exists only in the author's head.

This is the world REQ-DOG-FOOD prevents. In the dog-fooded world, the same tests look like this:

// test/feature-scanner.test.ts
@FeatureTest(FeatureScannerFeature)
export class FeatureScannerFeatureTest {

  @Verifies<FeatureScannerFeature>('scannerDiscoversRequirementsInSourceTree')
  async scannerDiscoversRequirementsInSourceTree() {
    const scanner = new RequirementScanner(...);
    expect(await scanner.scan('./fixtures')).toHaveLength(3);
  }

  @Verifies<FeatureScannerFeature>('scannerFailsOnOrphanFeatures')
  async scannerFailsOnOrphanFeatures() {
    const scanner = new RequirementScanner(...);
    await expect(scanner.scanStrict('./fixtures/with-orphan'))
      .rejects.toThrow(/orphan feature/);
  }
}

Different top-level shape, same body. The bodies of the test methods are ordinary expect-style assertions — no DSL-specific runtime. What the DSL adds is the decorator surface: @FeatureTest pins the whole class to a Feature, @Verifies pins each method to a named AC on that Feature. Those names are checked at compile time against the Feature's abstract methods. A reader looking at any line can answer "what AC does this test prove?" — the answer is in the decorator. A reader looking at the Feature class can answer "which tests verify this AC?" — the scanner walks the registry.

The switch from describe/it to @FeatureTest/@Verifies is structural. It looks cosmetic but it is not. It is the difference between tests that live in a parallel universe from the spec and tests that live in the spec's own graph.

Credibility is the payload, not the rhetoric

A reader might suspect that "credibility" is a soft word hiding a marketing impulse. It is not, in this context, and the Requirement's risk.ifNotMet is specific about why: "users reject". The consequence is not abstract reputational damage. It is the concrete user action of walking away from the library.

The reasoning is this. A requirements DSL is a tool users adopt for long-term value. Adoption requires trust, because the switching cost is non-trivial — once a team has wired the DSL into their source tree, their tests, their CI, the cost of switching to another DSL is the cost of unwinding all that wiring. Adoption therefore requires evidence of reliability at a stronger standard than a library with low switching cost. A library where you can try it today and rip it out tomorrow with one npm uninstall demands little trust up front. A library you build your whole spec discipline around demands a lot.

One way to generate that trust is a track record: "we have been in production for five years at companies you have heard of". @frenchexdev/requirements does not have that track record — it is an unreleased package at version 0.x. The substitute it can provide, credibly, is self-use. The claim "we use it" is verifiable in one git clone + rg + compliance --strict. Three commands, three minutes, and the prospective user has evidence stronger than a testimonial because they ran the check themselves.

This is not a hypothetical audience. OSS adopters do run this kind of check. They clone, they look at the test tree, they grep for the framework's own patterns. A test tree that uses sibling frameworks to test the main framework raises eyebrows. A test tree that uses the main framework to test itself raises none. The eyebrows are the measurement; REQ-DOG-FOOD is what keeps them flat.

The economics of the rule

There is a cost to dog-fooding. It is not zero. The cost is that the DSL's authors cannot write tests with the off-the-shelf ergonomics of describe/it that every JavaScript developer has internalised. They have to write them in the DSL's own shape. If the DSL's shape is awkward, every test in the package feels it, every day.

This cost is paid in exchange for two things. First, the feedback signal: if the shape hurts the authors, the authors notice immediately and fix it, and downstream users never feel the hurt. Second, the credibility payload: every test in the package is evidence that the shape works. The two are complementary. A DSL that is painful to use on itself will either be fixed (because the pain hurts the authors) or abandoned (because the authors give up and use describe/it for their own tests, at which point REQ-DOG-FOOD starts to fail and the credibility evaporates).

The rule is therefore self-sustaining only if the DSL is good enough to be usable on itself. An un-usable DSL will fall into one of two end-states: it will be refactored until it is usable, or the project will be honest and drop the Requirement. Either way, REQ-DOG-FOOD does its job — by refusing the third (and unfortunately common) end-state: an un-usable DSL whose authors quietly use another framework while selling theirs.

The social cost of non-dog-fooding

There is a second, less-often-stated cost to a framework that does not eat its own food, beyond the credibility cost. The authors never feel the friction their users feel. Every awkwardness in the API, every confusing error message, every ceremony around boilerplate — all of it lands on the users but not on the authors, because the authors have a private sibling API they use in preference. The feedback loop between user pain and author priorities is broken at the source.

Dog-fooding closes that loop. When the authors of @frenchexdev/requirements add a new decorator, they write its first use site in the package's own test tree. When they change a fit criterion type, they propagate the change to the existing Requirement corpus — twenty-two Requirements' worth of fit criteria — before anything ships. When they make an error message harder to read, they see it in their own next test run. The feedback is immediate, local, and unavoidable.

This is not a hypothesis. It is a matter of record in the package's git log: several rounds of API refinement happened after the author noticed a shape that was uncomfortable to declare repeatedly in their own test tree. The discomfort was the signal. The refactor was the response. The users will never see the original shape — only the refactored one.

One specific example for texture. The @Verifies decorator was originally designed to take a method reference at the value level: @Verifies(FeatureFoo.prototype.someAc). This is the more JavaScript-idiomatic form. The author then wrote fifty tests using it and realised the reference syntax was clumsy in the source text — it read like line noise and added no type-safety beyond the string form. The decorator was refactored to take a string typed against keyof T: @Verifies<FeatureFoo>('someAc'). Fifty tests were rewritten. The users of the DSL never saw the original awkward form; they see only the clean final shape. The rewrite was painful, but the pain was the author's alone, and the payoff was a better API for every subsequent user. That is the dog-food feedback loop working in the small, day by day.

A test for credibility: can the users audit the claim?

One test of a dog-food claim is whether it is auditable by an outsider. Some claims are closed — you have to take the author's word. "We use it internally" is closed: the internal use happens behind a VPN. "We run the same gate in CI" is closed if the CI is not public.

REQ-DOG-FOOD's fit criteria are open. The ripgrep invocation is public — anyone can clone the repository and run it. The AC bindings are public — anyone can grep for @Verifies('satisfiesDecoratorRegistersBidirectionalLink') and find the test. The coverage gate is public — the project's vitest config and the latest coverage report are in the repo. Every check a reader might want to run themselves is runnable without access to the author's machine or the author's cloud.

This is a small thing that matters. An open claim can be falsified. A closed claim can only be believed or not. REQ-DOG-FOOD is open, and that is itself a choice encoded in the fit criteria: the ripgrep tool is a public tool, the test method names are public strings, the coverage scope is a public glob. The Requirement could have been written with closed criteria (a custom internal tool, a private coverage backend) and it would have carried less credibility. The choice of public tools is part of the credibility payload.

Running example — where REQ-DOG-FOOD lands on the explorer TUI

Return to the running example. The file feature-trace-explorer-tui.ts begins:

import { Feature, Priority, Satisfies, type ACResult } from '../../src';
import { ReqDiscoverableTraceabilityRequirement } from '../requirements/req-discoverable-traceability';
import { ReqDogFoodRequirement } from '../requirements/req-dog-food';
import { ReqParallelDeliverableRequirement } from '../requirements/req-parallel-deliverable';

@Satisfies(
  ReqDiscoverableTraceabilityRequirement,
  ReqDogFoodRequirement,
  ReqParallelDeliverableRequirement,
)
export abstract class FeatureTraceExplorerTuiFeature extends Feature {
  readonly id = 'FEATURE-TRACE-EXPLORER-TUI';
  // ... ten abstract ACs
}

Three classes imported as Requirement references. One @Satisfies call with three class arguments. Ten abstract AC methods on the Feature. The middle Requirement in the list is the one this chapter is about.

Trace what happens when compliance --strict runs against this file.

The scanner discovers FeatureTraceExplorerTuiFeature by walking requirements/features/. It reads the class's decorators via ts-morph AST inspection, finds @Satisfies(...), resolves each argument to a Requirement class by name, and registers three bidirectional links in its in-memory registry — one per argument. The registry now knows, from this one file, that REQ-DOG-FOOD has (at least) FEATURE-TRACE-EXPLORER-TUI among its satisfiers, that REQ-DISCOVERABLE-TRACEABILITY has it among its satisfiers, that REQ-PARALLEL-DELIVERABLE has it among its satisfiers — three rows in the trace matrix.

Bidirectional is load-bearing. The registry does not just record "this Feature satisfies this Requirement". It also records the inverse: "this Requirement is satisfied by this Feature". When a reader runs trace matrix REQ-DOG-FOOD, the report walks the registry from the Requirement side and lists every Feature that declared satisfaction. When a reader runs trace chain FEATURE-TRACE-EXPLORER-TUI someAc, the report walks from the Feature side and lists the Requirements upstream plus the test method downstream. Both walks use the same registry; both directions are first-class. This is the many-to-many property in miniature: one declaration, two query paths, both fast.

The scanner then walks test/ looking for a test class bound to this Feature. It finds test/feature-trace-explorer-tui.test.ts — a file whose default-exported class is decorated @FeatureTest(FeatureTraceExplorerTuiFeature). The scanner reads each method on that class and checks each one is decorated with @Verifies<FeatureTraceExplorerTuiFeature>('someExistingAc'). If a method has no @Verifies, or if the AC name does not match one of the ten on the Feature, the scanner fails with a diagnostic that names the file, the method, and the expected AC name from the Feature class's abstract methods.

Note the typing move. @Verifies<FeatureTraceExplorerTuiFeature>('ac') is a generic decorator. The keyof T & string constraint on its argument means the TypeScript compiler — not the scanner — already rejects AC names that do not exist on the Feature. The scanner's check is defence in depth: it catches the case where someone disabled the TypeScript type-check (via // @ts-ignore or a config bypass) and slipped through a malformed decorator. In well-formed code the compiler has already done the work; the scanner re-verifies at runtime to be sure.

Next the scanner applies REQ-DOG-FOOD's fit criteria. It runs the literal ripgrep from the first fit criterion against the package's test/ directory. Zero matches is green. It checks the two binding ACs from the second fit criterion are themselves present and tested — satisfiesDecoratorRegistersBidirectionalLink and complianceStrictFailsOnFeatureWithoutSatisfies both live on feature classes inside the package. It reads the coverage report produced by the package's own vitest run and checks that src/cli/** is at or above 98% line coverage.

Three fit criteria, three checks. If any fails, compliance --strict exits non-zero. Green requires all three.

Worth pausing on the second check — the AC-binding check — because it has a subtlety. The fit criterion does not say "there must be a test method called satisfiesDecoratorRegistersBidirectionalLink". It says "there must be an AC, on some Feature, with that method name, and some test method must be decorated @Verifies(...) with that name". The scanner has to resolve the binding two-deep: find a Feature with that abstract method, then find a test for that Feature that verifies that specific AC. If either end is missing, the binding is unresolved and the gate fails. This is how fit criteria stay decoupled from implementation file layout: the criterion names a concept (an AC by name), not a path.

Now the loop closes on itself. The scanner has its own Feature — something like FEATURE-COMPLIANCE-REPORT (covered in full in chapter 06). That Feature also declares @Satisfies(..., ReqDogFoodRequirement, ...). It has its own test class under test/. Those tests also use only @FeatureTest and @Verifies. When the scanner walks the tree, it finds its own test class, checks its own @Verifies methods, applies its own ripgrep rule to its own test/ contents, and validates its own coverage.

Five passes later, the scanner has scanned the Feature that scans everything — including itself. The loop is closed. The output is one green line per Requirement, one green line per Feature, a summary count of ACs covered, and a zero exit code. Or a red line, an explanation, and a non-zero exit.

A concrete example of the output, for orientation. A successful run on the current tree produces something like:

requirements compliance --strict
--------------------------------------
REQ-DOG-FOOD                    3 satisfiers   3/3 fit criteria PASS
REQ-DISCOVERABLE-TRACEABILITY   7 satisfiers   2/2 fit criteria PASS
REQ-PARALLEL-DELIVERABLE        12 satisfiers  1/1 fit criteria PASS
... (19 more)

22 Requirements, 22 PASS
78 Features, 78 covered by tests
778 tests, all via @FeatureTest/@Verifies

rg "\b(describe|it)\(" packages/requirements/test  => 0 matches [OK]
line coverage src/cli/**                           => 100.00% [OK] (gate: 98%)

compliance --strict: PASS

The scanner does not hide its work. Each Requirement is printed with its satisfier count and fit-criterion results. Each fit criterion shows the tool it ran and the outcome. A red run prints the same shape with failing criteria marked and the offending file/line pinpointed. This transparency is a side-effect of the fit criteria being declarative: because every check is a typed object in the Requirement class, the scanner can print its work without guessing what to show.

The explorer TUI is one ripple. The whole package is a net of them.

Walking the net from a single starting point

To feel the connectedness of the net, pick any single file in the package and walk outward. Start with test/feature-trace-explorer-tui.test.ts (the test file for the running example). Follow each link:

  • The test class's @FeatureTest(FeatureTraceExplorerTuiFeature) decorator points to the Feature class in requirements/features/feature-trace-explorer-tui.ts.
  • The Feature class's @Satisfies(..., ReqDogFoodRequirement, ...) decorator points to three Requirement classes in requirements/requirements/.
  • Each Requirement's fit criteria may reference bound AC names — those resolve back to abstract methods on some Feature, which resolve to test files in test/.
  • Each test method's @Verifies<Feature>('acName') annotation pins the method to a specific AC on the Feature from step 2.

Four link-types, each typed, each traversable in either direction. A reader with the trace command can walk the graph interactively: trace chain FEATURE-TRACE-EXPLORER-TUI someAc prints the upstream (which Requirements this AC contributes to) and downstream (which test method proves it) in one pass. trace matrix REQ-DOG-FOOD prints all Features that declared satisfaction and their test coverage. trace gaps prints every AC without a test method, every Feature without a test class, every Approved Requirement without a satisfier.

Four commands, one graph. The graph is what the scanner builds from the decorator inspection; the commands are the reports. Both are in the same package. REQ-DOG-FOOD's job is to ensure the graph is complete — no islands, no orphans, no parallel tracks.

Why FEATURE-TRACE-EXPLORER-TUI carries three Requirements

A final observation on the running example. The Feature satisfies three Requirements: REQ-DISCOVERABLE-TRACEABILITY (because the TUI is the user-discoverability angle on the graph), REQ-DOG-FOOD (because its test file is an instance of the pattern), REQ-PARALLEL-DELIVERABLE (because it is shipped independently of the core scanner). Three unrelated loyalties, one Feature.

This is the many-to-many satisfaction the earlier chapter argued was the heart of the upgrade from typed-specs. A Feature in the real world usually participates in multiple concerns: a UX concern, a testing concern, a release-logistics concern. A flat Feature-only taxonomy collapses these into one line of text. The typed @Satisfies list keeps them distinct and queryable. When someone asks "which Features are discoverability concerns?", the trace matrix for REQ-DISCOVERABLE-TRACEABILITY answers. When someone asks "which Features exemplify dog-fooding?", the trace matrix for REQ-DOG-FOOD answers. Same Feature, three answers, no duplication.

The three loyalties are not equal in weight. REQ-DOG-FOOD is a constraint (a rule about form); REQ-DISCOVERABLE-TRACEABILITY is functional (a rule about capability); REQ-PARALLEL-DELIVERABLE is a quality (a rule about shipping cadence). Three kinds, three weights, one Feature. The DSL makes this visible; a flat taxonomy would hide it.

What the explorer TUI would have looked like without REQ-DOG-FOOD

Strip the second class out of the @Satisfies list:

@Satisfies(
  ReqDiscoverableTraceabilityRequirement,
  ReqParallelDeliverableRequirement,
)
export abstract class FeatureTraceExplorerTuiFeature extends Feature { ... }

Nothing else changes. The Feature still declares the same ten ACs. The scanner still discovers it, still checks it has a test class, still runs the test class's @Verifies methods against its AC list, still builds a coverage report. The gate still passes or fails on the Feature's own merits.

What changes is the meaning of the test file. Without the Requirement in the list, the test file is a test of one Feature. With the Requirement in the list, the test file is one unit of evidence for a cross-cutting policy — a policy that says every test in the package must be written in the package's own DSL. The test file's content does not change. Its role changes.

This is the many-to-many property in miniature. A single Feature's tests can simultaneously serve as evidence for a specific Feature claim and for a general package-wide policy. The same text carries both loads because the DSL has two strata to hang loads on. In a Feature-only DSL like typed-specs, the second load had nowhere to attach.

Why the second Requirement is in the list at all

A reader might reasonably ask: if every test in the package satisfies REQ-DOG-FOOD by construction, why does this specific Feature need to declare it? Is the declaration redundant?

It is not. The declaration is the Feature opting in to be counted as a satisfier of the Requirement. The compliance scanner uses the declared @Satisfies list to answer the question "which Features satisfy REQ-DOG-FOOD?" — a question that appears in the trace matrix REQ-DOG-FOOD report. Without the declaration, the Feature would still transitively contribute (its tests would still avoid describe/it), but it would not appear as a named satisfier in the report. The user asking "what Features does this Requirement cover?" would not see it.

The @Satisfies list is, in other words, both an enforcement mechanism (via the compliance gate) and a documentation mechanism (via the trace matrix). Declaring REQ-DOG-FOOD on this Feature says "this Feature is an intentional instance of the dog-food pattern, not an accidental one". That intentionality is what the trace matrix records.

There is a deeper point here about the difference between enforcement and evidence. A Requirement can be enforced structurally — the ripgrep check runs against every test file, whether it intends to or not. A Requirement can also be evidenced narratively — a Feature declares @Satisfies(ReqDogFoodRequirement) to say "and here is one concrete Feature where this Requirement applies, by design". Structural enforcement is cheap but anonymous. Narrative evidence is slightly more expensive (one line of decorator) but named. The trace matrix wants names, not just counts. So the declaration is redundant for enforcement purposes and irreducible for documentation purposes.

This is the same move a tagged union makes over a single-kind datatype. You could represent a payment with just an amount field. You get cleaner reports and fewer bugs by also tagging it with kind: 'refund' | 'purchase'. The tag is redundant for the arithmetic — a negative amount already distinguishes refund from purchase. The tag is essential for the presentation — a report can group by kind without re-deriving the distinction. @Satisfies is the tag on a Feature: redundant for raw enforcement, essential for the trace narrative.

Complement, not duplicate — how this differs from typed-specs-product/10-dogfood

An earlier article on this site — typed-specs-product/10-dogfood.md — drew a dog-fooding argument at a different stratum. The reader who has already absorbed that argument may reasonably wonder whether this chapter repeats it. It does not, and the difference is worth making explicit.

That product article imagined tspec-the-SaaS: a hosted backlog, a public compliance dashboard, a web UI where tspec's own features are tracked in tspec's own system. The loop it drew closed at the product surface. Every tspec feature appears in the tspec backlog. The tspec scanner scans the tspec source tree. The tspec quality gate blocks the tspec release pipeline. The argument is about a product's relationship to its own artefact store: backlog, dashboard, reports, release gate. The dog-fooding is user-facing.

This chapter draws the loop at a different stratum — the decorator-source stratum, one level below the product. There is no dashboard in sight. There is no backlog. There is a test file in a test directory, a decorator on a test class, a fit criterion in a Requirement class, and a scanner that reads all three. The dog-fooding is at the source-code surface.

The two strata are independent. A product could close the product-surface loop (tracking its own features in its own backlog) without closing the source-surface loop (writing its tests in its own DSL). Equally, a package could close the source-surface loop (this chapter's topic) without closing the product-surface loop (there is no hosted backlog for @frenchexdev/requirements yet — the Requirements live in the filesystem, next to the code). Both closures are possible; the package in this chapter does the second, the article in the other series imagined the first.

Two strata, same word, different closures. The product article is the outer ring of the loop. This chapter is the inner ring. A reader interested in the full shape reads both; the series this chapter opens will cover the outer ring in chapter 14 once the inner ring is fully laid out.

One note worth making about the relation. The product-surface dog-food is a strong argument for SaaS buyers — it is what you point at in a pitch. The source-surface dog-food is a strong argument for OSS users and contributors — it is what you point at when someone asks "can I trust the library?". Different audiences, different loops, different pitches. The package that does both wins on both fronts. The package that does neither is a pamphlet.

The daily consequences of the meta-circle

Abstract arguments about meta-circularity can feel remote. The consequences in practice are concrete and small, and worth naming.

When the author adds a new CLI command. The command needs a Feature class. The Feature class needs @Satisfies pointing at one or more Requirements (including REQ-DOG-FOOD, if the command's tests are going to count as dog-food evidence). The command needs a test file. The test file must use @FeatureTest and @Verifies. The test file must not contain describe( or it(. All of this is enforced by the gate before the commit can land. The author therefore writes the command and its test in one session, uses the scaffolder to generate the boilerplate, and runs compliance --strict before pushing. The feedback loop is tight — minutes, not days.

When the author refactors the scanner. The scanner's own Feature class must continue to have valid @Satisfies declarations. Its test file must continue to use decorators. If the refactor inadvertently introduces a describe( (maybe copy-pasted from another project), the gate fails locally before push. The refactor is tightened before anyone else sees it. The meta-circle here acts as a tripwire: it catches the category of mistake where the author forgets the rules their own tool enforces.

When a new contributor joins. The contributor reads the test directory. They see zero describe( blocks. They see every test class decorated. They see the shape. They write new tests in the same shape, because the old shape is the only shape present. There is no README to consult, no convention document to memorise — the codebase teaches by example, and the example is uniform because REQ-DOG-FOOD has kept it uniform. The onboarding is shorter because the pattern is consistent.

When the author considers a refactor that would violate the rule. Suppose the author thinks: wouldn't it be easier to just use vitest's describe/it for some internal helper tests? The gate stops them. The author either abandons the idea, or (if they judge it sufficiently important) goes through the formal act of deprecating REQ-DOG-FOOD. The formal act is expensive enough that trivial rule-breaking does not happen. The rule is firm against casual erosion.

When a user reads the package for the first time. The user clones the repo, skims test/, and notices the pattern. They form a quick judgment: "this project takes its own DSL seriously". The judgment shapes their confidence in adopting the DSL themselves. The meta-circle is doing its credibility work here without anyone explicitly pointing at it. The artefact speaks.

These are the mundane consequences of a well-designed fixed point. Nothing dramatic happens on any given day. Small disciplines accumulate into a project that looks different from the baseline. The baseline, over many projects, is a requirements system that talks about discipline and a codebase that does not practise it. The dog-fooded package inverts that ratio. The talk is minimal; the practice is the whole codebase.

Diagram 1 — the circular validation loop

Diagram
Figure 02.1 — The circular validation loop. Six nodes, six arrows. The package's own Requirement sits at the top, flows through Features and Tests into the scanner, and the scanner reads the Requirement back to check closure. One full pass of the loop is one run of compliance --strict.

Six nodes, six edges. Five solid edges represent the artefact chain — Requirement declared, Feature declaring satisfaction, Test declaring which Feature, method declaring which AC, scanner reading all of it, report produced. The one dotted edge is the meta-circular closure: the scanner reads the Requirement to know what to check. The loop is closed at the source level, not at the execution level.

Diagram 2 — the paradox-vs-fixed-point view

Diagram
Figure 02.2 — Why the circle terminates. The scanner reads source-as-data (layer 2), which is finite. The source compiles into live code (layer 1), but the scanner never invokes that live code; it only inspects the text. The recursion has a floor.

The vertical arrangement tells the story. The scanner at the bottom inspects source at the middle. Source compiles into live code at the top. The dotted non-edge from scanner to live Requirement marks the crucial fact: the scanner never calls the Requirement class, never instantiates it, never triggers any of its side effects. The Requirement is data, not function, as far as the scanner is concerned. That is what makes the recursion well-founded.

How the non-invocation works in the implementation

A reader who wants the mechanical detail — how does the scanner extract data from a class without invoking it? — can open src/analysis/loadFeatures.ts. The scanner uses ts-morph to parse the TypeScript source into an AST, then walks the AST to find class declarations with the right decorator and structure. It reads the readonly id = initializer as a literal string node, reads the @Satisfies(...) decorator as a call-expression node, and resolves each argument to a class reference via the AST's symbol table. None of this involves running TypeScript code. It is all string-and-node manipulation on a parse tree.

This is why the scanner can be re-run against arbitrary package trees without running their code — no side effects, no environment setup, no transitive dependencies. The scanner is pure analysis over text. In functional programming terms, it is a read-only function from "source tree" to "report". Pure in the strongest sense: same input, same output, no side channel.

The choice of ts-morph over the alternative (TypeScript compiler API directly) is incidental to the meta-circle argument but worth noting for implementation literacy. ts-morph provides a thin ergonomic wrapper over the same underlying compiler, with convenience methods for common AST manipulations. Either choice would produce the same mechanical behaviour: read source, don't run it. The Requirement's verifiability property does not depend on the specific AST library; it depends on the category of analysis being textual rather than executional.

Objections and replies

Three predictable objections deserve short, sharp replies before this chapter closes.

"Isn't dog-fooding just marketing?"

No, because the gate is binding, not performative. Per feedback_no_cloud_cicd, this project has no cloud CI/CD — the build pipeline runs locally, the author pushes to main, Vercel serves static files. That operational choice would, in another setting, be a license to cheat: without a cloud runner enforcing the gate, the author could skip it. The response is that compliance --strict is wired into the pre-push workflow on the author's own machine. The gate fails locally before the push happens. A marketing dog-food is one where the authors talk about it and do not enforce it. A binding dog-food is one where the enforcement is on the critical path of every push.

The test for whether dog-fooding is real is binary: does a stray describe( fail the build? Here the answer is yes. The specific shell incantation that fails lives in the Requirement's fit criteria, quoted verbatim above. A reader can run it themselves against the checked-out repository and verify.

"What if the DSL is too awkward to use on itself?"

Then the DSL has a real problem, and dog-fooding surfaces it fast. This is a feature of the posture, not a bug. A DSL that hides behind its brochure can ship awkward shapes forever. A DSL that ships its own test suite written in its own language cannot. The feedback loop is tight: every test the author writes in the package is a test written in the DSL, and every shape that fights back surfaces as a commit that takes longer than it should.

The countervailing Requirement that disciplines this is REQ-BOOTSTRAP-ZERO-FRICTION — the rule that says the DSL must be adoptable in one afternoon by a new user. REQ-DOG-FOOD pushes toward rigour; REQ-BOOTSTRAP-ZERO-FRICTION pushes back toward ergonomics. The two are in deliberate tension, and both appear in chapter 04 in the tour of all twenty-two Requirements the package ships. If REQ-DOG-FOOD fit criteria cannot be met without making the DSL unusable for newcomers, then REQ-BOOTSTRAP-ZERO-FRICTION fails and the shape of the DSL must change. The two Requirements keep each other honest.

"What about the bootstrap?"

Handled above in the fixed-point section, but to state the punchline: the first compliance run was of a package that contained all the pieces simultaneously — types, decorators, scanner, Requirements, Features, tests. No earlier version needed to exist for that first run to be well-defined, because the recursion terminates on layer-2 source inspection, not on execution. After the first run passed, every subsequent run is a verification of finite source by finite code. The loop was never ill-founded.

A sharper version of the bootstrap worry: how do we know the very first run was honest? If the scanner had a bug on day one that made it green when it should have been red, the whole trust chain rests on a bad foundation.

The reply has two parts. First, the package's 778 tests were written in the DSL the package ships. If the scanner were wrong enough to miss a describe( token, the failure would be visible in the test tree's contents — a reader can grep for describe( themselves and see what the scanner claims to see. The check is cross-verifiable by any external tool. Second, the package's own coverage report is produced by vitest, a mature independent tool — the 98% threshold is reported by vitest, not by the scanner, and the scanner only reads vitest's output. The dog-food claim does not depend on the scanner verifying itself across all axes; it depends on the scanner being correctly coupled to external tools that verify independent properties. Ripgrep checks the token claim. Vitest checks the coverage claim. The scanner orchestrates, it does not self-certify.

"What if a contributor does not know to use the decorators?"

Then the first commit that adds a describe( fails the gate and does not reach main. The scanner's error message points at the file and line, quotes the Requirement ID, and prints the fit criterion verbatim. A contributor who has not read the documentation discovers the rule the first time they try to add a test the old way. The rule teaches itself to newcomers by blocking them.

This is why the rule is encoded as a typed artefact rather than a project convention in a README. A README can be skipped. A failing compliance gate cannot. The teaching is structural.

"Isn't this over-engineering for a small package?"

The package has twenty-two Requirements and twenty-odd Features; it is not small. But even granting the premise — that a tighter regime might suit a tinier library — the choice here is deliberate: the package is a reference implementation of its own DSL. Its role is not just to be itself; its role is to demonstrate what a requirements-DSL-respecting codebase looks like. Under-engineering it would undermine its demonstration value. A reference is allowed to carry more ceremony than a typical instance, because the extra ceremony is the thing the reference exists to make visible.

A corollary worth stating. Downstream users adopting @frenchexdev/requirements in their own codebases are not expected to match the package's own rigour. They can adopt one Requirement (or none), they can declare fewer fit criteria, they can skip the dog-food closure entirely if their project is small enough not to need it. The DSL is a menu, not a manifesto. REQ-DOG-FOOD is a rule the package adopts for itself because its credibility depends on it. A user's credibility depends on different things — their own track record, their own project's trust requirements — and their configuration should reflect that. The package over-engineers itself so that downstream users can calibrate with evidence, not with an absent reference point.

"What if the rule blocks a legitimate refactor?"

A legitimate refactor that happens to introduce a describe( block fails the gate. The author has three choices: revert the describe, rewrite as @FeatureTest/@Verifies (usually small), or formally deprecate REQ-DOG-FOOD in the registry. The third is allowed by the DSL — a Requirement's status can transition from Approved to Deprecated — and if the authors decided dog-fooding was no longer serving the project, they could make that transition explicit. The cost of deprecation is visible: every subsequent compliance report would note REQ-DOG-FOOD as deprecated, every Feature that declared @Satisfies(ReqDogFoodRequirement) would register a dangling link, and the trace matrix would carry a column of deprecations.

The point is not that the rule is unshakeable. The point is that shaking it is visible. A convention in a README can be eroded quietly. A Requirement with Deprecated status in the registry is a public record of the erosion. Either way the rule stands or falls, but the DSL makes the fall legible.

"Does this not punish incremental development?"

A reader worried about iteration speed might object: if every new test must use the decorator surface, and the decorator surface requires a Feature class, and the Feature class requires a @Satisfies list, then writing a quick test for a bug fix takes four artefacts instead of one. Does this not slow the loop?

The short answer is that the scaffolder exists to handle the ceremony. npx requirements scaffold test <FEATURE-ID> generates the test class, the @FeatureTest decorator, and one @Verifies stub per AC on the Feature. The author fills in the test body; the structural boilerplate is produced. For a bug fix on an existing Feature, the flow is: add an AC to the Feature (one abstract method), run scaffold test, the scaffolder adds a @Verifies method for the new AC, the author writes the body. Four artefacts is the concept count; the keystroke count is not much higher than describe/it.

The longer answer is that the extra ceremony is the point. The reason @Satisfies is mandatory is that every new Feature is an opportunity to declare which Requirements it exists to serve. A Feature without that declaration drifts — it becomes a deliverable without a policy, which is exactly the kind of orphan the typed-specs series could not prevent. The ceremony is not friction in the pejorative sense; it is the small forcing function that keeps the graph complete. The cost is measured in seconds per new Feature. The payoff is measured in having a graph at all.

This is also where the tension with REQ-BOOTSTRAP-ZERO-FRICTION lives, and the tension is productive. REQ-BOOTSTRAP-ZERO-FRICTION pushes toward ergonomics: the scaffolder, the feature new wizard, the replay <spec> flag, the three-way diff review on sync. REQ-DOG-FOOD pushes toward rigour: the gate, the zero-match grep, the coverage threshold. The two tensions push the DSL toward a sweet spot where the ceremony is present but automated, so authors feel the discipline without feeling the drudgery. If the sweet spot is missed — too much drudgery without automation — the project is in trouble and REQ-DOG-FOOD starts to fail. If the sweet spot is hit — drudgery automated, discipline preserved — both Requirements pass and the package ships.

"What counts as a test in the grep check?"

The ripgrep invocation is rg "\b(describe|it)\(" packages/requirements/test. The word-boundary anchor (\b) prevents false positives on variable names containing describe or it as substrings. The parenthesis requires the call-site form — a function-call pattern — rather than any mention of the identifier. A comment // this describes the test does not match. A file with describe: as a JSDoc tag does not match. A test that imports describe from another module without calling it does not match. The check is narrow on purpose.

Worth noting what the check does not catch. It does not catch test(...), the Jest/Vitest alternative to it. It does not catch suite(...), the Mocha alternative to describe. It does not catch bench(...) from vitest benchmarks. These are all legitimate omissions — the Requirement's prose names describe/it specifically, and the fit criterion is faithful to the prose. If the package later decides to ban additional patterns, the fit criterion can be tightened. For now, the rule is scoped to the two most common offenders.

A tighter check might alternatively use a linter rule (eslint-plugin-requirements) to catch a wider pattern. The choice of ripgrep is a trade-off: grep is universally available, linter rules require toolchain integration. Grep loses some precision. The loss is accepted because the enforcement surface is tiny and the precision can be tightened later without breaking the rule's shape.

The wider context — why this matters for the whole series

Before closing with the running-example recap, it helps to say what this chapter's argument is preparing the reader for across the next thirty-odd chapters.

The series this chapter belongs to is structured around the twenty-two Requirements the @frenchexdev/requirements package ships. Each chapter takes one or a small cluster of Requirements and walks through the concrete engineering they drove. REQ-DOG-FOOD comes first in the sequence not because it is technically more important than the others, but because it is the axiom. It is the rule that licenses every other rule in the registry.

Consider the structure. REQ-COVERAGE-MINIMA says the package must hit 100% line coverage. But coverage is meaningful only if the tests themselves are structurally sound. REQ-PROPERTY-BASED-COVERAGE says certain invariants must be checked with property-based testing via fast-check. But property-based tests are meaningful only if they are wired into the same four-tier chain as unit tests. REQ-EXAMPLES-COMPILE says every TypeScript example in the documentation must actually type-check. But example code is meaningful only if it demonstrates the same shape the authors themselves use. Each of those Requirements depends on REQ-DOG-FOOD being true — depends on the authors actually writing tests in the DSL, using the DSL as documented, not silently using a parallel track.

Strip REQ-DOG-FOOD out of the registry, and every other Requirement weakens. Coverage becomes a number in isolation. Property-based checks become a box ticked on a dashboard. Examples become brochures. The shape of the whole package unravels into a set of performance metrics that could be achieved by any project, without any of them being evidence that the DSL is a working artefact.

This is the axiomatic role. Not the most technically complex Requirement in the registry. Not the most external-authority-backed. The axiom whose truth is the precondition for the meaningfulness of the others. A reader who grants this chapter grants the foundation the rest of the series builds on. A reader who does not grant it can read the rest of the series as a technical tour without a trust claim; useful, but not what the authors intend. This chapter is the trust claim.

A brief note on rhetoric

A note for the careful reader on rhetorical posture. The word "credibility" appears many times in this chapter. It is not a stylistic tic. Credibility is the specific technical property the Requirement delivers, and calling it anything else would underclaim. Reliability is a different property (achieved by tests passing). Correctness is a different property (achieved by matching a spec). Credibility is the property of being trust-worthy to an outsider who has not yet invested in using the library. It is downstream of reliability and correctness but not identical to them — a library can be reliable and correct and still not credible, if its evidence is closed. REQ-DOG-FOOD delivers the third property by making the evidence open.

Calling the property by its actual name matters in a field where words often slip. "Quality" means too many things. "Trust" is too broad. "Credibility" is tight: the property of having your claims believed by a reader who has not yet tried the library. That is what self-dog-fooding buys, and no shorter word names it exactly.

Running-example recap

The explorer TUI enters this chapter with one extra thing visible that chapter 00 noted but did not unpack: its @Satisfies list includes ReqDogFoodRequirement. That inclusion is what makes the file test/feature-trace-explorer-tui.test.ts a direct instance of dog-fooding. The Feature declares a typed loyalty to the Requirement; the test file under the same package is the thing that proves the loyalty is honoured.

Strip away REQ-DOG-FOOD from the @Satisfies list and the Feature is still valid, its tests still run, its AC coverage still counts. Keep it, and the test file becomes part of the evidence for the Requirement itself. One line of decorator argument turns one test file from "a test" into "a unit of dog-food". The running example will keep showing up across the series with this extra loyalty visible; the rest of the chapters unpack different facets of the same closed loop.

A compact table of the three loyalties on this one Feature, for reference as the reader continues:

Requirement Kind What it binds
REQ-DISCOVERABLE-TRACEABILITY Functional User discoverability of the traceability graph
REQ-DOG-FOOD Constraint Decorator-only testing, enforced by gate
REQ-PARALLEL-DELIVERABLE Quality Independent shipping cadence

Three Requirements, three kinds, one Feature. Every subsequent chapter will pick one column of this table and zoom in. Chapter 05 zooms on the functional column. Chapter 11 zooms on the style-register angle. Chapter 15 zooms on the quality column. This chapter zoomed on the constraint column — the middle row — and argued that its presence is what makes the whole table trustworthy rather than just technically correct.

One final reflection — the politics of self-verification

A thread worth pulling at the end, because it will return in later chapters. A DSL that verifies itself is not politically neutral. It takes a stance about who owns the verification — the authors, not an external body. This stance has trade-offs.

The benefit, covered at length above, is credibility. The author's claim about their own discipline is testable by any reader in three commands. No appeal to authority is needed.

The cost is that the authors are the authority. If they loosen a fit criterion, no external reviewer flags it. If they deprecate a Requirement, no external standards body objects. The package's verification system is as trustworthy as the authors' honesty about what the system does. This is not a unique situation — every open-source project has this property to some degree — but it becomes sharper when the verification is explicit and public. A project with no explicit verification can be evaluated on reputation alone. A project with explicit self-verification invites scrutiny of the verification itself.

The answer to the scrutiny is the git history. Every change to REQ-DOG-FOOD is a commit. Every fit criterion tightening or loosening is a diff. Every deprecation leaves a trail. A reader sceptical of the authors' self-imposed rigour can walk the history and see whether the rules were tightened over time (a positive sign) or quietly relaxed (a worrying sign). The authors cannot hide the trajectory; the Requirement class is versioned like every other source artefact.

This is what makes self-verification credible rather than self-serving. Self-verification with hidden history is self-serving. Self-verification with visible history is auditable. REQ-DOG-FOOD's history is public from day one, and the trajectory — so far — is toward tightening, not loosening. The 98% coverage threshold is the only point of slack, and even that was argued above as a design choice (slope over cliff) rather than a concession.

Later chapters — specifically chapter 22 — return to the governance angle in depth. For now the short version: self-verification is defensible when the self-verifier is transparent about what they are doing. REQ-DOG-FOOD is transparent. Its rules are in source. Its checks are open-tool invocations. Its history is in git. Its failure modes are named in the risk.ifNotMet field. The author is not asking for trust; the author is providing material for a sceptic to verify. That is the political content of the dog-food posture, and it is part of the credibility payload too.

Summary — the one-paragraph version

For a reader wanting the compressed form: REQ-DOG-FOOD is a Constraint Requirement in the @frenchexdev/requirements package, stating that every test in the package must use the package's own decorators (@FeatureTest, @Verifies) rather than generic describe/it blocks, that every Feature must declare @Satisfies linking to at least one Requirement, and that the CLI surface must hold 98% line coverage. Three fit criteria enforce the rule: a literal ripgrep, two AC bindings on the decorator registry, a vitest-produced coverage number. The Requirement is meta-circular — it lives in the same package whose tests it governs — but the circle is a fixed point rather than a paradox because the scanner reads source-as-data rather than executing it, giving the recursion a ground. The value of the rule is credibility: a requirements DSL is trusted when its authors use it on themselves, and the trust is auditable in one git clone. The running example FEATURE-TRACE-EXPLORER-TUI declares this Requirement in its @Satisfies list, making its own test file a direct instance of the dog-food pattern. The chapter argued for the fixed-point reading, covered the eleven fields of the Requirement class in detail, addressed six objections, and pointed ahead to the twenty-odd Requirements whose meaningfulness depends on REQ-DOG-FOOD being true.

  • typed-specs-product/10-dogfood.md — the product-surface complement. Dog-fooding of the tspec product (the imagined SaaS backed by this DSL) at the user-facing level. That chapter draws the loop closing at the backlog/dashboard; this chapter draws it closing at the decorator-source. Both true, different strata.
  • closing-the-loop/01-the-closed-loop.md — the broader feedback-loop philosophy the site has developed across several series. Self-referential enforcement is a special case of a general pattern: make the system produce artefacts that its own scanners can read back.
  • Chapter 00 — Named but Not Modelled — the frame for the whole series. Why the word Requirement appeared in typed-specs prose before the type existed, and what the new package adds.
  • Chapter 03 — The Decorator Surface — a full tour of the six decorators (@FeatureTest, @Verifies, @Satisfies, @Refines, @Expects, @Exclude). This chapter treats them as atomic; that one opens them up.
  • Chapter 18 — The Meta-Circle in Diagrams — the five-angle diagrammatic rendering of the self-referential loop. Two diagrams in this chapter scratch the surface; that chapter does the full illumination.

Previous: Chapter 01b — Historical Path: Feature First, Requirement Later | Next: Chapter 03 — The Decorator Surface

⬇ Download