The link you would expect to be tightest
Look back at the legend. The arrow from a Requirement to its Acceptance Criteria feels like it should be the most rigid bond on the whole map — the why and its measure, surely welded together. Bake the ACs into the requirement, you might think, and be done.
That instinct is wrong, and the system is built on knowing why. A requirement is a promise — durable, slow to change, written in the language of stakeholders. An acceptance criterion is a measure — operational, revisable, written in the language of whoever has to prove it. Welding them together forces every refinement of how we check to look like a change to what we promised. The map seizes up. So the link is, on purpose, distended.
Where the ACs actually live
In the real types, acceptance criteria are not fields of Requirement at all. The chain runs through the Feature:
Requirement —satisfiedBy→ Feature —acs→ AC —@Verifies→ TestRequirement —satisfiedBy→ Feature —acs→ AC —@Verifies→ TestA Requirement knows which features satisfy it, and a Feature carries its acceptance criteria. The two ends of the promise-to-measure relationship are joined through the what, not nailed to each other. That one indirection is what lets a team rewrite how a requirement is verified without touching the requirement, and lets a requirement be answered by several features without duplicating itself.
And crucially, the Requirement is generic over the rules that govern it:
abstract class Requirement<S extends RequirementStyle = RequirementStyle> {
abstract readonly status: StatusesOf<S>; // narrowed to the project's workflow
abstract readonly kind: KindsOf<S>; // narrowed to the project's kinds
abstract readonly statement: RequirementStatement; // shaped by the style's patterns (EARS by default)
abstract readonly verificationMethod: VerificationMethodsOf<S>;
abstract readonly risk: RequirementRisk<RiskLevelsOf<S>>;
// …
}abstract class Requirement<S extends RequirementStyle = RequirementStyle> {
abstract readonly status: StatusesOf<S>; // narrowed to the project's workflow
abstract readonly kind: KindsOf<S>; // narrowed to the project's kinds
abstract readonly statement: RequirementStatement; // shaped by the style's patterns (EARS by default)
abstract readonly verificationMethod: VerificationMethodsOf<S>;
abstract readonly risk: RequirementRisk<RiskLevelsOf<S>>;
// …
}The type parameter S is not decoration. It is the seam where the looseness becomes typed. What a status is allowed to be, what a kind is allowed to be, how a statement is shaped — none of that is hard-coded into the requirement. It is read off of S. The link between a requirement and the rest of the map is governed by a thing you can swap: a Style.
What a Style is
A RequirementStyle is the ontology and the behaviours a project adopts for its requirements — a single coherent bundle:
interface RequirementStyle {
readonly id: string;
readonly version: string;
readonly vocabulary: StyleVocabulary; // kinds, status workflow, risk taxonomy, verification methods, statement patterns
readonly validators: StyleValidators; // statement-shape and spec-shape checks
readonly templates: StyleTemplates; // pre-filled skeletons for `requirement new`
readonly reporter: RequirementReporter; // how a requirement renders to Markdown / console
readonly fitCriterionAdapters: readonly FitCriterionAdapter[]; // pluggable evaluators (Datadog, Grafana, …)
}interface RequirementStyle {
readonly id: string;
readonly version: string;
readonly vocabulary: StyleVocabulary; // kinds, status workflow, risk taxonomy, verification methods, statement patterns
readonly validators: StyleValidators; // statement-shape and spec-shape checks
readonly templates: StyleTemplates; // pre-filled skeletons for `requirement new`
readonly reporter: RequirementReporter; // how a requirement renders to Markdown / console
readonly fitCriterionAdapters: readonly FitCriterionAdapter[]; // pluggable evaluators (Datadog, Grafana, …)
}The vocabulary is the part the type system reads. Declare a style as const, and TypeScript narrows the requirement's fields to exactly that project's terms — no string-typing, no runtime reflection, the invariant carried at compile time:
type KindsOf<S> = S['vocabulary']['requirementKinds'][number];
type StatusesOf<S> = S['vocabulary']['statusWorkflow']['states'][number];type KindsOf<S> = S['vocabulary']['requirementKinds'][number];
type StatusesOf<S> = S['vocabulary']['statusWorkflow']['states'][number];So a kind of "Obligation" in a legal project and a kind of "Safety" in an industrial one are both type-checked — against that project's vocabulary, not a universal enum the framework imposed. The looseness of the link is real, but it is not slack: it is a typed degree of freedom.
The default is the point
Here is the line that matters for adoption: you do not have to build any of this. A default style ships in the box — @frenchexdev/requirements/style, a preset built on ISO/IEC/IEEE 29148, Volere, and EARS. New project, no configuration: you get sensible requirement kinds, a Draft → Approved → Implemented → Verified → Deprecated workflow, a risk taxonomy, and EARS statement patterns, all typed, immediately.
When the default does not fit your domain, you override per concern rather than fork the framework. The family ships five registered styles out of the box — Default, Industrial (IEC 61508 / SIL levels), Lean (A3 / PDCA), Agile (Connextra / Gherkin), Kanban (flow / classes of service) — through a StyleRegistry, and you can register your own. The same facts, told in five registers; see Styles — A Plural Rhetoric for the same requirement rewritten in each.
This is the open/closed principle made physical, and it is the philosophical commitment from Part 02 cashed out in code. The system is open to your domain's vocabulary — you parameterise it with an ontology — and closed against drift: whatever vocabulary you choose, the compiler narrows to it and the invariant guard enforces it. A DSL parameterised by an ontology is never blocked on a pre-built library; you extend it by supplying a style, never by mutating the kernel. The framework refuses to pretend it knows what your promises are. It only insists they be typed.
That distended, default-provided link is exactly what makes the map both yours and durable — which is what the last part needs, because durability and machine-traversability are the two things that let an AI fleet work on the map without tearing it.