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

Part 03: Requirements as Code

The package that gives the mega-split its philosophical centre is @frenchexdev/requirements-requirements. Its src/ contains no logic. It is a directory of twenty-two req-*.ts files, each exporting a single class that extends Requirement<S> from the shared kernel. Those twenty-two files are the requirements specification of the @frenchexdev/requirements-* engine itself. The package preaches what it lives, and the preaching is type-checked by the same compiler that type-checks everything else.

What "requirements-as-code" means here

A traditional requirements specification lives in a Word document, a wiki, a Jira project, or — in the more disciplined organisations — a structured artefact written against a templating standard like ISO/IEC/IEEE 29148 or SysML. The spec is prose; the code is the implementation; a traceability matrix bridges the two by reference. The matrix is maintained by hand, drifts under load, and goes stale the first time a deadline pushes harder than the audit.

Requirements-as-code collapses that arrangement. The spec is no longer prose-with-IDs that points at code; the spec is itself code. Each Requirement is a TypeScript class — id, title, priority, status, kind, statement, rationale, fitCriteria, verificationMethod, source, risk, optionally tracedTo — typed against a RequirementStyle that constrains the vocabulary (e.g. "Functional" / "NonFunctional" / "Constraint" for kind, "Critical" / "High" / "Medium" / "Low" for priority.level). The class file imports Requirement and Priority from @frenchexdev/requirements-shared-kernel, and is then exported by the package's index.ts so any other package can import { ReqDogFoodRequirement } from '@frenchexdev/requirements-requirements' and link a Feature to it via @Satisfies(ReqDogFoodRequirement).

A Feature class — Feature subclass under another package's src/ — declares @Satisfies(ReqXyz) for every Requirement it helps meet. The compliance reporter walks both registries and produces, on demand, the bidirectional traceability matrix that the audit would otherwise require a human to maintain. The matrix is not generated from the spec; the matrix is the spec, read by the same scanner that reads everything else.

The canonical example: req-dog-food.ts

The simplest illustration is the requirement that mandates the practice itself. Here is the entire file, verbatim:

import { Requirement, Priority } from '@frenchexdev/requirements-shared-kernel';

export abstract class ReqDogFoodRequirement extends Requirement {
  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.' };
}

There is no prose comment explaining what the requirement is for; every field is structured. The statement declares an EARS pattern ("ubiquitous") and the response clause. The rationale carries a claim, a kind ("principle"), and evidence — itself a typed array of { kind, requirement, rationale } triples. The fitCriteria is a heterogeneous union: a quality-gate with a tool and a rule, a unit-test with a list of test-binding names the scanner will look for, and a coverage-threshold with a metric, a minimum, and a scope. The risk declares both the level and the consequence of failure. Nothing here is a magic string; everything is constrained by RequirementStyle and narrowed by as const.

Two more Requirements illustrate the same shape with different domains. req-dsl-complete.ts declares that the DSL must track Requirements, not just Features — the meta-requirement that justifies the existence of requirements-requirements itself; its rationale invokes ISO/IEC/IEEE 29148 and SysML as precedents. req-spec-is-source-of-truth.ts declares that spec.json is canonical and that generation must be idempotent and sync-able; its rationale invokes Terraform and Kubernetes as the industry-standard precedents for the plan → preview → apply pattern.

Why the package contains no logic

requirements-requirements/src/index.ts does two things and no third: it re-exports the twenty-two req-*.ts Requirements, and it re-exports the Feature classes that each implementation file across the ecosystem owns. Here is the relevant slice:

// @frenchexdev/requirements-requirements — Domain vocabulary.
// All Requirements + Features for the @frenchexdev/requirements-* ecosystem live here.
// Reqs and Features ARE source code; this package holds the lot.

// ─── Requirements ───
export * from './req-dog-food';
export * from './req-parallel-deliverable';
export * from './req-dsl-complete';
export * from './req-refactor-safe';
export * from './req-vocab-industry-aligned';
// … plus seventeen more

// ─── Features ───
export * from './base';
export * from './decorators';
export * from './compliance-core';
export * from './scaffold-core';
export * from './test-bindings-scanner';
// … plus ~25 more

There is no runtime code that does anything when imported. There is no factory, no registry, no side effect. Every file is a declaration. The package builds with tsc alone, no codegen, no pre-build step, no template expansion. That is the point: a specification that did anything at import time would not be a specification — it would be an implementation.

SOLID, DRY, and the elimination of duplicate truth

The pre-split state — covered in Part 01 — duplicated truth in three places. The requirements CLI shipped its own req-discoverable-traceability.ts. The requirements-lib barrel shipped a second copy. Both copies tracked the same Requirement, both copies were edited when the Requirement changed, and DRY's "every piece of knowledge has one representation" was violated by construction. The roadmap's Phase 4 deletes both duplicates and lets every consumer import the single canonical version from requirements-requirements.

SOLID enforces the same property under different names. SRP says the package that owns the Requirements should own them once; OCP says new Requirements should arrive as new files, not by modifying old ones (every req-*.ts is a standalone module — additive); LSP says every Requirement is substitutable for the abstract Requirement<S> (the scanner does not care which concrete class it is reading); DIP says the consumers — Features in other packages — depend on the abstract Requirement, not on a concrete implementation that ships its own logic.

The keystone effect is that the audit trail collapses to a git log packages/requirements-requirements/src/. Every requirement change is a commit. Every commit moves the spec. Every spec move is type-checked the same way any other code change is type-checked. The traceability matrix is generated on demand by npx requirements compliance — never written by hand, never out of sync, because the matrix is just a query over the registries that the decorators populate at module load time.

What dog-fooding earns

Dog-fooding has a reputation as a slogan that engineers cite when they want to claim virtue without doing the work. The version here is stricter: the package named "requirements" must track Requirements with the same DSL it asks every consumer to use; if it does not, the package is, in req-dog-food.ts's own words, "a DSL that preaches what it does not live". The risk field of REQ-DOG-FOOD is blunt: "Package preaches a DSL it does not use; credibility collapses; users reject."

The mega-split makes dog-fooding cheap. Every new Tier-1 or Tier-2 package adds its own Features under requirements-requirements/src/feature-*.ts and links them to Requirements with @Satisfies. A new Requirement — say, a Tier-2 use case that needs an audit-trail hook — becomes a new req-*.ts file, a new export, a new node in the trace graph. The compliance reporter, scoped per-package, fails strict mode the moment a Feature lacks a @Satisfies link or a Requirement lacks a Feature realising it. npx requirements compliance --strict is the gate; the gate is enforced for the same package that defines it. Self-hosting closes the loop.

The next two pages walk through the Tier-0 packages themselves: first the zero-dependency shared kernel that owns the base classes and decorators, then the vocabulary package that owns the spec.

⬇ Download