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 08 — Styles: A Plural Rhetoric

A single set of facts about a system can be written truthfully in more than one register. The facts do not change. The vocabulary, validators, templates, and reporters do.

The first seven chapters of this series treated the Requirement as if it had a single voice — the voice that ISO/IEC/IEEE 29148 trained us to hear. A Requirement has an id, a title, a priority, a statement, a rationale, a fit criterion, a source, a verification method, a risk. The pattern is EARS. The source kinds are stakeholder, regulation, standard, contract, incident. The status workflow is Draft → Approved → Implemented → Verified → Deprecated. That is one register, and it is a good one — but it is only one.

This chapter names the second move the DSL makes, after typing the Requirement as a first-class entity: it makes the register itself a parameter. A Requirement is not written in 29148-speak or in IEC 61511-speak or in A3-speak. A Requirement is written in the register its project pays the cost of. The register is a property of the project, not the domain. And because the register is a property — not a hard-coded assumption — it is something the type system can carry.

The machinery for this is called RequirementStyle. Five Styles ship in the box. The interface is small enough to quote in full. The registry that holds them is smaller still. And the cost of adding a sixth Style is exactly one register() call — no fork, no core edit, no merge pain. This chapter walks through the shape of a Style, surveys the five built-ins, shows the same Requirement rewritten in all five registers side-by-side, and closes with the pattern for adding your own.

Why plural rhetoric

A safety engineer at a refinery reads the sentence "every user input must be bounds-checked" and hears a Safety Instrumented Function. She wants to see a SIL rating, a demand mode, a proof-test interval, a hazard reference. Without those slots the sentence is not actionable; it is prose someone might have meant.

A lean practitioner at a manufacturing plant reads the same sentence and hears a waste-removal Kaizen. She wants to see a Gemba observation, a current-state metric, a target-state metric, a PDCA cycle, a standardisation plan. Without those slots the sentence is not actionable either; it is a slogan.

An agile PM at a startup reads the sentence and hears an INVEST-sized user story. He wants to see a role, a capability, a benefit, and a Given-When-Then acceptance scenario. Without those slots the sentence is not negotiable; it is a specification fragment someone will decompose badly, later, alone.

A Kanban flow lead at a consultancy reads the sentence and hears a Class of Service decision. He wants to know whether this is Standard flow, Expedite, FixedDate, or Intangible — because the board policy depends on it. Without that slot the sentence is something the queue will absorb silently.

A plain 29148 author reads the sentence and hears a Functional requirement with an EARS ubiquitous pattern. She wants to see a fit criterion, a source, a verification method. Without those slots the sentence is a paragraph in someone's prose document.

All four readers are right. The sentence "every user input must be bounds-checked" is true in all four registers, and each reader has a legitimate professional need to have it rewritten in her own. The failure mode of most requirements tooling is to force the four readers into the same template, which is to say: into a template that works for none of them. DOORS looks like Word. Jama looks like Word. ReqIF looks like XML. Jira looks like whatever its last administrator wanted. None of them carries the vocabulary the reader's profession is trained on.

The failure mode of the opposite tooling — one tool per register — is that nothing connects. The safety engineer's SIL register lives in exSILentia; the lean practitioner's A3 lives in PowerPoint; the agile PM's stories live in Jira; the Kanban board lives in Trello. Each tool has its own ontology, its own workflow, its own API, and they do not know about each other. A Requirement in one tool is not a Requirement in another; it is just a ticket-shaped object with a similar name.

@frenchexdev/requirements takes a third position. It encodes the pattern of writing a Requirement — the shape of the abstract class, the shape of the traceability graph, the shape of the compliance report — and makes the register a type parameter. Requirement<S> is generic over a RequirementStyle. The S narrows the statement patterns, the status workflow, the risk taxonomy, the source kinds, the rationale kinds, the verification methods, the validators, the templates, and the renderer — all at compile time. The same class hierarchy, the same decorator vocabulary, the same CLI, the same CI gate — five registers.

This is not cosmetic. It is SOLID at the meta-level. The Requirement<S> class is open for extension (a new Style) and closed for modification (the core stays the same). A custom Style is substitutable for DefaultStyle wherever Requirement<S> is used. The RequirementStyle interface is small enough that each sub-interface (vocabulary, validators, templates, reporter, adapters) owns exactly one concern. And the Requirement<S> class depends on the abstraction, not on DefaultStyle.

That is the claim. The rest of this chapter shows the machinery that makes it true.

The RequirementStyle shape

The whole interface fits on one screen. From packages/requirements/src/style.ts:

export interface RequirementStyle {
  readonly id: string;
  readonly version: string;

  readonly vocabulary: StyleVocabulary;
  readonly validators: StyleValidators;
  readonly templates: StyleTemplates;
  readonly reporter: RequirementReporter;

  readonly fitCriterionAdapters: readonly FitCriterionAdapter[];

  onWizardStart?(ctx: AdapterContext): Promise<void>;
}

Five slots, each a small interface, plus a metadata pair (id, version) and an optional lifecycle hook. The top-of-file comment is worth reading with care, because it names the SOLID contract the rest of the chapter leans on:

SOLID:
  S — each sub-interface owns one concern
      (vocabulary vs validators vs templates vs reporter)
  O — add a new Style without modifying the Requirement base class
  L — a custom Style is substitutable for DefaultStyle
  I — small interfaces; projects implement only what they override
  D — Requirement<S> depends on the Style abstraction, not DefaultStyle

Walk each slot.

StyleVocabulary — the data a Style supplies

The vocabulary is pure data — typed arrays and typed schemas that describe what kinds of things exist in this register. No behaviour, no functions, no side effects. A project reading a Style's vocabulary can tell you:

  • Which requirementKinds the project recognises (Functional, Safety, UserStory, Kaizen-Experiment, Expedite, …).
  • Which statusWorkflow states and transitions the project's Requirements move through.
  • Which riskTaxonomy levels exist (five-level severity, SIL 1–4, cost-of-delay curve shapes, impact-effort quadrants).
  • Which verificationMethods count as legitimate evidence (Test, SILProofTest, PDCA, BDD, ServiceLevelExpectation).
  • Which rationaleKinds a project recognises (evidence-based, hazard-analysis, kata-learning, cost-of-delay).
  • Which sourceKinds exist, each with its own typed slot schema (stakeholder role+date, regulation jurisdiction+act, HAZOP study+node+deviation, gemba walk date+observer).
  • Which statementPatterns exist, each with its own slot schema and its own human template.

Everything is readonly and const-typed. The KindsOf<S>, StatusesOf<S>, RiskLevelsOf<S>, VerificationMethodsOf<S>, RationaleKindsOf<S>, SourceKindsOf<S>, StatementPatternsOf<S> helper types at the bottom of style.ts extract the narrowed string unions from a const-typed Style, so Requirement<DefaultStyleType>['status'] is exactly 'Draft' | 'Approved' | 'Implemented' | 'Verified' | 'Deprecated' — not string. A typo is a compile error.

StyleValidators — the rules that catch wrong

The validators are the rules layer. Two functions, two scopes:

export interface StyleValidators {
  validateStatement(statement: unknown): ValidationResult;
  validateSpec(spec: unknown): ValidationResult;
}

validateStatement is the narrow check — it takes a statement value (an EARS event-driven structure, or a safety-function structure, or a user-story structure, or a kaizen-hypothesis structure) and confirms that every required slot for that pattern is present and well-formed. It runs per-prompt in the wizard, so the author gets feedback as soon as she hits Enter on a field.

validateSpec is the wide check — it takes the whole Requirement spec and confirms that every field (requirementKind, status, risk.level, verificationMethod, source.type, rationale.kind) is in-vocabulary, and recursively invokes validateStatement on the statement sub-object. It runs at wizard submit, at requirement sync, and at CI — three moments where a different failure mode matters. At submit, the author wants to know now. At sync, a hand-edit might have introduced drift. At CI, the gate must refuse to merge a spec authored against an incompatible schema version.

This is where a Style earns its keep. A Style without load-bearing validators is a naming convention with delusions of grandeur. A Style with real validators is a compiler front-end — it refuses malformed input. Chapter 09 goes deep on the Industrial validators (SIL required on Safety, SIL verification method on safety-function, standards-only source on Regulatory); Chapter 10 covers the Lean validators (Gemba source required on Waste-Elimination, PDCA verification required on Kaizen-Experiment); Chapter 11 covers the Kanban validators (COD required on Expedite, deadline required on FixedDate, threshold required on Intangible). Each Style's validators encode its professional discipline.

StyleTemplates — pre-filled skeletons

Templates are the scaffolds that requirement new --template <id> fills in. The DefaultStyle ships templates for the five EARS patterns. IndustrialStyle ships nine — sif-iec61511, process-interlock, alarm-isa182, cyber-zone-62443, batch-recipe-isa88, availability-sla, regulatory-ce-atex, gxp-21cfr11, interoperability-opcua. LeanStyle ships templates per Kaizen / A3 / standard-work pattern. AgileStyle ships templates per user-story / epic / enabler / spike / bug / tech-debt. KanbanStyle ships six templates — one per Class of Service, plus the LifeCycleReset and Defect variants.

A template is not a mandatory entry point; it is an ergonomic accelerator. A user who knows the Style cold can skip templates and fill every field herself. A user new to the Style uses a template to learn which slots matter for which kinds of work. Templates are pedagogy — the Style's vocabulary, applied.

RequirementReporter — how the Style renders

The reporter is the emit layer. Three functions:

export interface RequirementReporter {
  renderStatement(statement: unknown): string;
  renderRequirement(spec: unknown): string;
  renderMarkdown(spec: unknown): string;
}

renderStatement produces a single-line human sentence — "When the payment provider returns HTTP 502, 503 or 504, the system shall retry up to 3 times with exponential backoff." It's what requirements show prints at the top of its output, and what compliance cites in its failure messages. renderRequirement produces a multi-line console block for interactive inspection. renderMarkdown produces a publishable fragment — a section header, a metadata table, the rendered statement, the rationale, the fit criteria — suitable for a FAT binder, a PR description, or a chapter in a design document.

The reporter is Style-scoped because the rendering idioms are Style-scoped. IndustrialStyle's renderMarkdown emits a metadata table with SIL, proof interval, HAZOP source — because that is what an FSM auditor opens first. AgileStyle's renderMarkdown emits a user-story header, a Given-When-Then block, an INVEST checklist — because that is what a sprint review wants. LeanStyle's renderMarkdown emits the current-state / target-state / gap / countermeasure A3 layout. KanbanStyle's renderMarkdown emits a ticket card with Class of Service, COD, SLE, and promotion threshold. Same interface. Five renderers. Five registers on the page.

FitCriterionAdapter — external systems plug in

Fit criteria reference executable evidence — a unit test, a quality gate rule, a metric, a demonstration, a SIL calculation. Some fit criteria can be evaluated against live systems: a Datadog SLO query, a Grafana dashboard panel, a SonarQube rule, an exSILentia calculation, a Lighthouse score. Each Style declares which adapters it accepts through the fitCriterionAdapters array.

The adapter is a port. The built-in array is empty for the five shipping Styles — the ecosystem grows with the community, and projects drop in adapters via a config file without forking. This keeps the core package lean while leaving the extension surface open. A safety-critical project's FitCriterionAdapter registry is not the same as an agile startup's, and there is no reason the core should know about either.

onWizardStart — the optional hook

The last slot is optional. A Style that needs to warm something up before the wizard prompts — fetch a list of OKRs from an API, load a project's HAZOP study into memory, check that a standards database is reachable — declares onWizardStart. The default does nothing. The hook exists so Styles that integrate with external systems have a predictable moment to do it.

The five registered styles

Five Styles ship in the box. Each one is a coherent bundle — vocabulary, validators, templates, reporter, adapter array — assembled from a named intellectual lineage. Each one is the subject of a separate chapter later in this series. This section introduces them at a glance.

DefaultStyle — ISO/IEC/IEEE 29148 + Volere + EARS

The baseline. Five requirement kinds (Functional, NonFunctional, Constraint, Compliance, UserStory). A five-state workflow (Draft → Approved → Implemented → Verified → Deprecated). A four-level risk taxonomy (Critical, High, Medium, Low). Four verification methods (Test, Inspection, Analysis, Demonstration). Six rationale kinds. Eight source kinds with typed slots. Six statement patterns — the five EARS patterns plus a natural fallback.

DefaultStyle is the 90% case. It was assembled from ISO/IEC/IEEE 29148:2018, the Robertsons' Volere shell, Mavin et al.'s EARS, Gojko Adzic's Specification by Example, and SysML's satisfy/verify/refine verbs. It is deliberately opinionated so that projects starting from zero do not have to be. When a project's register is more specialised — safety-critical, lean, agile, flow-driven — it picks a more specialised Style.

The file is packages/requirements/src/styles/default.ts. The full pitch lives at packages/requirements/docs/pitches/default.md. Chapter 09 is the deep dive.

IndustrialStyle — IEC 61508/61511/62443 + ISA-88/95 + GAMP 5 + 21 CFR Part 11

The heavy-gate Style. Safety, Security, Regulatory, Interoperability, Traceability requirement kinds. A thirteen-state workflow with explicit SafetyApproval, SecurityApproval, CustomerApproval, FactoryTested, SiteTested, Commissioned, InProduction, UnderChange gates. SIL 1–4 risk taxonomy. SILProofTest, SILValidation, Certification, Audit verification methods. Ten source kinds — iec-standard, iso-standard, isa-standard, regulation, customer-urs, hazop, lopa, fmea, cybersecurity-threat, vendor-recommendation. Four industrial statement patterns in addition to EARS — safety-function, interlock, alarm, security-zone.

IndustrialStyle is the Style for systems integrators shipping SIS, DCS, SCADA, and OT cybersecurity projects against IEC 61508, IEC 61511, IEC 62443, ISA-88, ISA-95, ISA-18.2, 21 CFR Part 11, and GAMP 5. Its validators enforce the three load-bearing invariants that make the difference between a desk-check pass and a TÜV assessment pass: Safety requires SIL 1–4, safety-function requires a SIL verification method, Regulatory requires a standard-kind source.

The file is packages/requirements/src/styles/industrial.ts. The pitch lives at packages/requirements/docs/pitches/industrial.md. Chapter 09 is the deep dive.

LeanStyle — Toyota + A3 + PDCA + Gemba + VSM + Kaizen

The hypothesis-first Style. Nine requirement kinds — ValueAdding, NonValueAddingButNecessary, Waste-Elimination, Flow-Improvement, Quality-BuiltIn, Customer-Pull, Standardization, Kaizen-Experiment, Respect-for-People. An eight-state PDCA workflow — ProblemStated → CurrentStateMapped → TargetStateDefined → CountermeasureProposed → Experimenting → Validated → Standardized → Deprecated — with rework edges for failed experiments, wrong targets, and superseded standards. An impact-effort risk taxonomy (no likelihood axis — we do not play probabilities, we go see and act). Verification methods centred on PDCA, Gemba observation, metric measurement.

Source kinds are where LeanStyle earns its keep: gemba-walk, vsm, value-stream-metric, kata-coaching, customer-interview, poka-yoke-incident, takt-observation, standard-work-deviation. The validator refuses a Waste-Elimination requirement without a gemba-walk or vsm source. Armchair kaizen is rejected at the schema boundary. The statement patterns — problem-statement, kaizen-hypothesis, standard-work, value-proposition — force authors to write hypotheses with validation windows and metrics, not slogans.

The file is packages/requirements/src/styles/lean.ts. The pitch lives at packages/requirements/docs/pitches/lean.md. Chapter 10 is the deep dive.

AgileStyle — Scrum + XP + SAFe + BDD

The backlog-as-source-code Style. Eight requirement kinds — UserStory, Epic, Enabler, Spike, TechnicalDebt, Bug, NonFunctional, Research. An eight-state Scrum/Kanban FSM — Backlog → Refined → Ready → InProgress → InReview → Done → Released → Archived — with the two rework edges every real team lives in: InReview → InProgress (review bounce-back) and Ready → Refined (refinement blocker). A five-level severity taxonomy with a likelihood × impact matrix an English-speaking PO can parse without an actuarial table. Eight verification methods — AcceptanceTest, DefinitionOfDone, DemoToPO, UserValidation, PairReview, TDD, BDD, ExploratoryTesting.

Statement patterns encode the Connextra user-story (role / capability / benefit), the Gherkin given-when-then (precondition / trigger / outcome), the SAFe epic with mandatory kpi slot, the SAFe enabler, the XP spike with timebox and deliverable, and the bug with typed repro steps. The validator enforces INVEST's Valuable — a user story with an empty benefit slot is rejected at the schema boundary.

The file is packages/requirements/src/styles/agile.ts. The pitch lives at packages/requirements/docs/pitches/agile.md. Chapter 11 is the deep dive.

KanbanStyle — Anderson's Kanban Method + Classes of Service

The flow-first Style. Six requirement kinds — the six Classes of Service: Standard, Expedite, FixedDate, Intangible, LifeCycleReset, Defect. A nine-state workflow — Backlog → Selected → Analysis → Development → Testing → Ready → Deployed → Observed → Archived — with rework edges for escaped defects and pull-backs for honest "we shouldn't have pulled that yet" reversals. A five-level cost-of-delay risk taxonomy — DelayHarmUrgent, DelayHarmFixedDate, DelayHarmLinear, DelayHarmIntangible, DelayHarmMinimal — because in Kanban, risk is the shape of the COD curve, not an independent severity.

Source kinds are flow-driven: cfd-observation, lead-time-distribution, ops-incident, customer-sla, flow-metric, blocked-ticket-report. Validators enforce Anderson's four disciplines: Expedite carries a quantified COD; FixedDate carries a deadline and penalty; Intangible carries a promotion threshold; Standard cites flow evidence. Class of Service stops being a Jira label juniors forget to set and becomes a typed artefact the compiler checks.

The file is packages/requirements/src/styles/kanban.ts. The pitch lives at packages/requirements/docs/pitches/kanban.md. Chapter 11 is the deep dive.

The registry — open/closed via a port

Five Styles are a good start. The claim of this chapter is that the sixth Style — the one your project needs and the community has not written yet — is as easy to add as the five that ship. The machinery for that claim is the StyleRegistry.

From packages/requirements/src/styles/registry.ts:

export interface StyleRegistry {
  get(id: string): RequirementStyle | undefined;
  list(): readonly RequirementStyle[];
  register(style: RequirementStyle): void;
}

export const BUILT_IN_STYLES: readonly RequirementStyle[] = [
  DefaultStyle,
  IndustrialStyle,
  LeanStyle,
  AgileStyle,
  KanbanStyle,
];

export function createStyleRegistry(
  styles: readonly RequirementStyle[] = BUILT_IN_STYLES,
): StyleRegistry {
  return new MapStyleRegistry(styles);
}

export const DEFAULT_STYLE_REGISTRY: StyleRegistry = createStyleRegistry();

Three moving parts. BUILT_IN_STYLES is the canonical array — the five Styles the package ships. createStyleRegistry() is a factory that takes a readonly array and returns a mutable registry. DEFAULT_STYLE_REGISTRY is the singleton pre-populated with the five built-ins — the one most code paths will use.

The interface is three methods. get(id) looks up a Style by its unique id. list() returns every registered Style in registration order. register(style) adds or replaces a Style in the registry.

This is the registry pattern — the same pattern the Scaffolder port uses (see Chapter 12), the same pattern ScaffolderRegistry exposes. The SOLID intent is explicit in the file's header comment:

SOLID:
 - OCP: `register()` adds a project-supplied Style without modifying built-ins
 - DIP: tests / tools depend only on the registry, never on individual Style files
 - ISP: tiny surface — get(byId) / list() / register()

Concretely, a project that needs a sixth Style does this:

import { createStyleRegistry } from '@frenchexdev/requirements';
import { MyLegalStyle } from './styles/legal';

const registry = createStyleRegistry();
registry.register(MyLegalStyle);

// The core never had to know about MyLegalStyle.
// Every iterating tool picks it up for free.

MyLegalStyle is a RequirementStyle value — a const-typed object implementing the interface. The core package does not import it. The core package's tests do not import it. No file in @frenchexdev/requirements knows MyLegalStyle exists. And yet every tool that iterates registry.list() — the CLI requirements new command that asks the user which Style to use, the compliance report that prints per-Style statistics, the schema generator that emits one JSON schema per Style — picks it up automatically. That is what open-closed via a port means in practice: the core is closed to modification (no edit to the five built-in files, no edit to the Requirement<S> class, no edit to the CLI) and open to extension (a new adapter the registry accepts at runtime).

The registry is a port. The five built-ins are the included adapters. MyLegalStyle is the project-supplied adapter. The pattern is the same pattern used for FitCriterionAdapters, for TestScaffolders, for the FileSystem port in the analysis core — a single architectural idiom, applied at every extension point the package exposes.

The registry, drawn

Diagram 1: the StyleRegistry as a port, the five built-ins as adapters, and a dotted slot for a user-added sixth style.

Diagram 8.1 — The StyleRegistry as a port; the five built-in Styles as adapters; a dotted slot shows the open extension point for a project-supplied sixth Style. No core file knows the sixth Style exists — every iterating tool picks it up via registry.list().

Diagram
The StyleRegistry is a port and the five built-in Styles are adapters. A user-added sixth Style plugs in without modifying any core file — every tool that iterates registry list picks it up automatically.

The five solid arrows are the built-ins — registered at factory time by createStyleRegistry(BUILT_IN_STYLES). The dotted arrow is the project-supplied adapter — registered at application startup by registry.register(MyLegalStyle). The core package (left box) is untouched. The five built-in files (middle box) are untouched. The only file that knows all six exist is the one file that called register() — the project's own bootstrap.

This is the minimum structural commitment that makes open-closed real. Most libraries claim OCP and then leak assumptions — a hard-coded switch over built-in ids, a private constructor, a sealed union, a reflection-based plugin scanner that only works for packages in node_modules. The three-method registry interface has none of those. It is the smallest surface that lets a new adapter participate as a peer to the five shipping ones.

Chapter 12 — The Scaffolder Registry — shows the same pattern applied to test scaffolders. Chapter 16 — Cross-Package Adoption — walks through the project-side adoption pattern for a real sixth Style, building against the published package.

Same REQ, five registers

The shape claim is now concrete. The registry is small, the interface is small, the five built-ins are coherent. What remains to be shown is what the same facts look like when a project picks a different Style.

The Requirement this chapter uses is REQ-REFACTOR-SAFE — a real Requirement in the package's own requirements/ directory. It is well-suited for this exercise because refactor safety is a concept every register has a way of talking about. A safety engineer hears "broken references after a rename" as a configuration-management hazard. A lean practitioner hears it as waste (relearning, defects). An agile PM hears it as a developer-facing user story. A Kanban lead hears it as a flow-preserving improvement. And a 29148 author hears it as a NonFunctional Quality requirement.

All five rewrites below describe the same fact: running requirements feature rename <old> <new> must atomically update every reference — class name, file name, spec.json, every @Verifies<OldFeature>() call-site, every @Satisfies(Old) reference — or refuse. The facts do not change. The vocabulary, validators, templates, and reporter output do.

Register 1 — DefaultStyle (29148 + Volere + EARS)

This is the actual shape the Requirement takes in the package today. An EARS event-driven pattern with a 29148-shaped spec.

export abstract class ReqRefactorSafeRequirement
  extends Requirement<DefaultStyleType>
{
  readonly id = 'REQ-REFACTOR-SAFE';
  readonly title = 'Identifier renames must propagate atomically across all linked artefacts';
  readonly priority = Priority.Medium;
  readonly status = 'Draft' as const;
  readonly kind = 'NonFunctional' as const;

  readonly statement = {
    pattern: 'event-driven' as const,
    trigger: 'a user runs `requirements feature rename <old> <new>`',
    response: 'rename the class, the file, the spec.json, every `@Verifies<OldFeature>()` call-site, and every `@Satisfies(Old)` reference atomically in a single commit-ready diff, or refuse if any site would be ambiguous.',
  };

  readonly rationale = {
    kind: 'evidence-based' as const,
    claim: 'Renaming a Feature or AC today is scary because references live in N test files and M Requirement classes; a codemod removes the fear.',
    evidence: [
      { kind: 'study' as const, title: 'A Study of Rename Refactorings',
        authors: 'Liu, Feldthaus, Møller', year: 2012 },
    ],
  };

  readonly source = {
    type: 'stakeholder' as const,
    role: 'principle/refactor-safety',
    date: '2026-04-14',
  };

  readonly verificationMethod = 'Test' as const;

  readonly risk = {
    level: 'Medium' as const,
    ifNotMet: 'Users avoid renames; names rot; vocabulary drifts.',
  };
}

Renderer output (requirements show REQ-REFACTOR-SAFE):

REQ-REFACTOR-SAFE  Identifier renames must propagate atomically across all linked artefacts
  priority: medium  status: Draft  kind: NonFunctional
  statement: When a user runs `requirements feature rename <old> <new>`, the system shall
             rename the class, the file, the spec.json, every @Verifies<OldFeature>() call-site,
             and every @Satisfies(Old) reference atomically in a single commit-ready diff,
             or refuse if any site would be ambiguous.

Validators active on this spec:

  • validateStatement checks the event-driven pattern has non-empty trigger and response.
  • validateSpec.requirementKind checks NonFunctional is in Functional | NonFunctional | Constraint | Compliance | UserStory.
  • validateSpec.status checks Draft is in the five-state workflow.
  • validateSpec.risk.level checks Medium is in Critical | High | Medium | Low.
  • validateSpec.source.type checks stakeholder has the required slots (role, date).

This is the 29148 + Volere + EARS register. A 29148 auditor reading this spec sees a well-formed NonFunctional requirement with a stated source, a rationale kind, a verification method, and a risk assessment. Everything a 29148 quality-characteristics checklist looks for is present.

Register 2 — IndustrialStyle (IEC 61511 + 21 CFR Part 11)

The same fact, rewritten as if the project were a pharmaceutical manufacturing integrator subject to FDA audit and IEC 61511 functional safety. The register recasts refactor-safety as a Traceability requirement — the ability of the computerised system to maintain audit-trail integrity through rename operations — with 21 CFR Part 11 implications.

export abstract class SifRefactorSafeRequirement
  extends Requirement<IndustrialStyleType>
{
  readonly id = 'SIF-REFACTOR-SAFE-001';
  readonly title = 'CSV identifier renames must preserve audit-trail integrity per 21 CFR Part 11 §11.10(e)';
  readonly priority = Priority.High;
  readonly status = 'SafetyApproval' as const;
  readonly kind = 'Traceability' as const;

  readonly statement = {
    pattern: 'event-driven' as const,
    trigger: 'a validation engineer executes rename-refactor command on a SIF identifier',
    response: 'the system shall propagate the rename atomically to every linked FRS / FDS / FAT / HAZOP reference and emit a tamper-evident HistoryEntry signed per 21 CFR Part 11 §11.70, or refuse the operation and log the refusal with a non-rename HistoryEntry.',
  };

  readonly rationale = {
    kind: 'regulatory-compliance' as const,
    summary: 'FDA CSV §4.6 audit-trail integrity. Rename without propagation is a data-integrity deviation triggering 483 observations.',
  };

  readonly source = {
    type: 'regulation' as const,
    jurisdiction: 'US',
    act: '21 CFR Part 11',
    article: '§11.10(e)',
  };

  readonly verificationMethod = 'Audit' as const;

  readonly risk = {
    level: 'SIL2' as const,
    ifNotMet: 'Audit-trail gap during FDA inspection; 483 observation; potential warning letter.',
  };
}

Renderer output — the FAT-binder Markdown:

# SIF-REFACTOR-SAFE-001 — CSV identifier renames must preserve audit-trail integrity

| Field | Value |
|---|---|
| Kind | Traceability |
| SIL / Risk | SIL2 |
| Priority | High |
| Status | SafetyApproval |
| Verification method | Audit |
| Source | regulation (jurisdiction=US, act=21 CFR Part 11, article=§11.10(e)) |

## Statement

> When a validation engineer executes rename-refactor command on a SIF identifier, the system
> shall propagate the rename atomically to every linked FRS / FDS / FAT / HAZOP reference and
> emit a tamper-evident HistoryEntry signed per 21 CFR Part 11 §11.70, or refuse and log.

**If not met**: Audit-trail gap during FDA inspection; 483 observation; warning letter.

Validators active on this spec:

  • validateStatement checks event-driven slots.
  • validateSpec.risk.levelwould fail if set to Medium (a non-SIL level) on a Safety kind. Here the kind is Traceability, so SIL2 is an allowed signalling level.
  • validateSpec.source.type — because kind = Regulatory implies a standard-kind source, and the spec cites 21 CFR Part 11 as a regulation, the validator passes.
  • validateSpec.verificationMethodAudit is an IndustrialStyle verification method appropriate for a Traceability requirement citing Part 11.
  • validateSpec.statusSafetyApproval is one of the thirteen states in the IndustrialStyle heavy-gate lifecycle.

The difference from the default register is not cosmetic. It is structural. The thirteen-state lifecycle means this Requirement cannot reach Implemented without an explicit SafetyApproval step recorded in the HistoryEntry log; the SIL risk level means the audit surface is pre-tagged for TÜV review; the regulation source with jurisdiction/act/article slots means the Requirement is indexable by the regulation it derives from. An FDA inspector asking "show me every requirement that derives from 21 CFR Part 11 §11.10(e)" can get a machine answer in a grep.

Register 3 — LeanStyle (A3 / PDCA / Gemba)

The same fact, rewritten as an A3 problem statement inside a PDCA cycle. The register recasts refactor-safety as an observed waste — engineers avoiding renames, names rotting, the downstream cost of naming drift — and the rename codemod as the countermeasure being experimented with.

export abstract class RenameCodemodKaizen
  extends Requirement<LeanStyleType>
{
  readonly id = 'KAIZEN-RENAME-SAFETY-001';
  readonly title = 'Eliminate the rename-avoidance waste pattern in the requirements DSL';
  readonly priority = Priority.Medium;
  readonly status = 'CountermeasureProposed' as const;
  readonly kind = 'Kaizen-Experiment' as const;

  readonly statement = {
    pattern: 'kaizen-hypothesis' as const,
    change: 'Ship a `requirements rename` codemod that atomically propagates identifier renames across all linked artefacts.',
    expectedOutcome: 'Rename-avoidance frequency drops from observed 100% (zero cross-artefact renames in Q4 2025) to <20% (at least one rename per contributor per month) by 2026-Q3.',
    reasoning: 'Gemba observation of the contributor workflow shows developers silently skip renames when references live in multiple files; removing the cognitive cost removes the avoidance.',
    validationWindow: 'two calendar quarters, 2026-Q2 through 2026-Q3',
    metric: 'rename_events_per_contributor_per_month >= 1',
  };

  readonly rationale = {
    kind: 'kata-learning' as const,
    summary: 'Coaching kata with five contributors revealed the same avoidance pattern in every case; countermeasure proposed per Toyota Kata target-condition step.',
  };

  readonly source = {
    type: 'gemba-walk' as const,
    date: '2026-04-08',
    observer: 'FrenchExDev',
    wasteCategory: 'Relearning',
    observation: 'Every contributor interviewed reported avoiding renames because of fear of broken references; three of five had an open PR with a known-bad name.',
  };

  readonly verificationMethod = 'PDCA' as const;

  readonly risk = {
    level: 'HighImpactLowEffort' as const,
    ifNotMet: 'Names rot; vocabulary drifts; new contributors inherit a frozen ontology the project can no longer correct.',
  };
}

Renderer output — the A3 Markdown:

# KAIZEN-RENAME-SAFETY-001 — Rename-avoidance waste

**Current state**: Zero cross-artefact renames in Q4 2025; five of five contributors
avoid renames because of fear of broken references.

**Target state**: Rename events per contributor per month ≥ 1 by 2026-Q3.

**Gap**: The cognitive cost of a rename (N test files + M Requirement classes) is higher
than the perceived value of a cleaner name.

**Countermeasure**: Ship `requirements rename` codemod — atomic cross-artefact propagation.

**Validation window**: Two quarters, 2026-Q2 through 2026-Q3.

**Metric**: rename_events_per_contributor_per_month ≥ 1.

**Source**: Gemba walk, 2026-04-08, waste category = Relearning.

Validators active on this spec:

  • validateStatement checks the kaizen-hypothesis pattern has non-empty change, expectedOutcome, reasoning, validationWindow, metric.
  • validateSpec.kindKaizen-Experiment requires verificationMethod = PDCA. Setting Test would fail here.
  • validateSpec.source.typegemba-walk is required for Waste-Elimination, allowed for Kaizen-Experiment, required on kinds tied to observed waste. Armchair kaizen (no gemba source) is rejected.
  • validateSpec.risk.level — the LeanStyle taxonomy is impact × effort. HighImpactLowEffort is the just-do-it quadrant.

This register is incommensurable with the previous two in vocabulary — Kaizen-Experiment, hypothesis, validation window, gemba-walk — but commensurable in facts. The same rename-safety problem is being described. What changes is the professional frame: a lean practitioner needs to know the current-state metric, the target-state metric, the gap, and the countermeasure. A 29148 author does not. An IEC 61511 engineer does not. Each is right for her own audit.

Register 4 — AgileStyle (Scrum + XP + SAFe + BDD)

The same fact, rewritten as an INVEST-sized user story with Given-When-Then acceptance scenarios.

export abstract class StoryRenameCodemod
  extends Requirement<AgileStyleType>
{
  readonly id = 'STORY-RENAME-CODEMOD-001';
  readonly title = 'Atomic cross-artefact rename for Features and Requirements';
  readonly priority = Priority.Medium;
  readonly status = 'Ready' as const;
  readonly kind = 'UserStory' as const;

  readonly statement = {
    pattern: 'user-story' as const,
    role: 'developer refactoring a Feature or Requirement name',
    capability: 'a single command that renames the class, the file, the spec.json, and every @Verifies / @Satisfies reference',
    benefit: 'I can fix naming mistakes without fear of shipping broken traceability',
  };

  readonly acceptanceScenarios = [
    {
      pattern: 'given-when-then' as const,
      precondition: 'a Feature FOO satisfied by REQ-A and verified by three tests in two files',
      trigger: 'I run `requirements feature rename FOO BAR`',
      outcome: 'the class, the file, the spec.json, every @Verifies<FooFeature>() call-site, and the @Satisfies(FOO) reference are rewritten to BAR in one commit-ready diff; tsc stays green',
    },
    {
      pattern: 'given-when-then' as const,
      precondition: 'a Feature FOO is referenced by a site where the rename would be ambiguous',
      trigger: 'I run `requirements feature rename FOO BAR`',
      outcome: 'the command refuses, prints the ambiguous site(s), and leaves the working tree untouched',
    },
  ];

  readonly rationale = {
    kind: 'team-retro' as const,
    summary: 'Sprint 38 retro: every dev on the team flagged rename-avoidance as a pain point.',
  };

  readonly source = {
    type: 'retro-finding' as const,
    retroDate: '2026-04-08',
    sprintNumber: 38,
  };

  readonly verificationMethod = 'BDD' as const;

  readonly risk = {
    level: 'Medium' as const,
    ifNotMet: 'Naming debt accumulates; developers avoid the refactor; the debt register grows by one story-point a sprint.',
  };

  readonly estimate = { unit: 'story-points' as const, value: 5 };
}

Renderer output — the sprint-review card:

# STORY-RENAME-CODEMOD-001 — Atomic cross-artefact rename

**As a** developer refactoring a Feature or Requirement name
**I want** a single command that renames the class, the file, the spec.json, and every
          @Verifies / @Satisfies reference
**So that** I can fix naming mistakes without fear of shipping broken traceability

## Acceptance scenarios

**Scenario 1**: Happy-path rename
- **Given** a Feature FOO satisfied by REQ-A and verified by three tests in two files
- **When** I run `requirements feature rename FOO BAR`
- **Then** every site is rewritten to BAR in one commit-ready diff; tsc stays green

**Scenario 2**: Refuses on ambiguity
- **Given** a Feature FOO is referenced by a site where the rename would be ambiguous
- **When** I run `requirements feature rename FOO BAR`
- **Then** the command refuses, prints the ambiguous site(s), and leaves the working tree untouched

**Estimate**: 5 story points
**Status**: Ready

Validators active on this spec:

  • validateStatement checks user-story slots — role, capability, benefit. The INVEST-Valuable validator refuses an empty benefit; a user story without a so-that clause fails at the schema boundary.
  • validateSpec checks that every acceptanceScenarios[].pattern is given-when-then or natural; given-when-then has required slots precondition, trigger, outcome.
  • validateSpec.verificationMethodBDD implies the scenarios are executable as .feature files; AcceptanceTest or TDD would also be valid.
  • validateSpec.source.typeretro-finding requires retroDate and sprintNumber slots.

This is the register a PO reads without translating. The Connextra template is the vocabulary they learned. The Gherkin scenarios are executable against a BDD runner (Cucumber, SpecFlow) without reformatting. The INVEST validator enforces the professional discipline ("V for Valuable") mechanically — a story without a benefit slot is not a story, regardless of how many points the team sized it.

Register 5 — KanbanStyle (Anderson's Classes of Service)

The same fact, rewritten as a Kanban ticket with an explicit Class of Service. Because the rename codemod is blocking productive flow — engineers avoiding renames, names rotting, new contributors inheriting a frozen vocabulary — it qualifies as Expedite under Anderson's rules, with a quantifiable cost of delay.

export abstract class KanbanRenameCodemodTicket
  extends Requirement<KanbanStyleType>
{
  readonly id = 'KB-RENAME-CODEMOD-001';
  readonly title = 'Rename codemod — flow-preserving improvement';
  readonly priority = Priority.High;
  readonly status = 'Selected' as const;
  readonly kind = 'Expedite' as const;

  readonly statement = {
    pattern: 'class-of-service' as const,
    cod: {
      shape: 'DelayHarmUrgent',
      unit: 'engineer-hours-wasted-per-sprint',
      value: 8,
      rationale: 'Six contributors × 1.5 hours/sprint avoiding renames and reasoning about stale references; observed via blocked-ticket-report 2026-Q1.',
    },
    action: 'Ship `requirements rename` codemod — atomic cross-artefact propagation or refusal.',
    wipPolicy: 'Expedite — one in flight max, preempts Standard.',
  };

  readonly rationale = {
    kind: 'cost-of-delay' as const,
    summary: 'Anderson §5: Expedite is justified when the quantified COD exceeds the opportunity cost of preempting one Standard item. 8 engineer-hours/sprint exceeds the sprint cost of any Standard item in the current backlog.',
  };

  readonly source = {
    type: 'blocked-ticket-report' as const,
    reportId: 'BTR-2026-Q1',
    date: '2026-04-08',
    affectedTickets: 6,
  };

  readonly verificationMethod = 'ServiceLevelExpectation' as const;

  readonly sle = {
    leadTimePercentile: 'P85',
    target: '3 calendar days from Selected to Deployed',
  };

  readonly risk = {
    level: 'DelayHarmUrgent' as const,
    ifNotMet: 'COD compounds; rename-avoidance becomes a normalised practice; the backlog accumulates naming-debt tickets quarterly.',
  };
}

Renderer output — the Kanban ticket card:

# KB-RENAME-CODEMOD-001 — Rename codemod

| Field | Value |
|---|---|
| Class of Service | Expedite |
| Cost of Delay | 8 engineer-hours/sprint (shape: DelayHarmUrgent) |
| WIP policy | one in flight max, preempts Standard |
| SLE | P85 lead time ≤ 3 calendar days |
| Status | Selected |
| Source | blocked-ticket-report BTR-2026-Q1 (6 affected tickets) |

## Action

Ship `requirements rename` codemod — atomic cross-artefact propagation or refusal.

## COD rationale

Six contributors × 1.5 hours/sprint avoiding renames and reasoning about stale references
= 8 engineer-hours/sprint. Observed via blocked-ticket-report 2026-Q1.

Validators active on this spec:

  • validateStatement checks the class-of-service pattern has a non-empty action and wipPolicy.
  • Expedite requires COD. The validator refuses an Expedite kind without a quantified cod object — this is Anderson's rule encoded. An Expedite without a COD is drama, not discipline.
  • validateSpec.source.typeblocked-ticket-report requires reportId, date, affectedTickets — flow evidence, not stakeholder opinion.
  • validateSpec.verificationMethodServiceLevelExpectation is the flow-based verification method: a prediction about lead time at a specified percentile.

This is the register a Kanban-mature team reads without translating. The Class of Service is typed, not a label. The COD is quantified, not implied. The SLE is a typed prediction, not an aspiration. The WIP policy is stated alongside the work, not buried in a Confluence page last edited in 2023.

The diagram — same REQ, five registers

Diagram 2: one REQ at the centre; five register arrows radiating out; each annotated with its register's vocabulary plus its most load-bearing validator.

Diagram 8.2 — The same underlying fact (atomic cross-artefact rename) rewritten in five registers. Each register activates different validators and produces different reporter output. The facts don't change; the vocabulary, the rules, and the rendering do.

Diagram
One underlying fact rewritten in five registers. Each register activates different validators and produces a different reporter output; the facts do not change but the vocabulary, the rules, and the rendering do.

Five registers. Five validators. Five renderers. One fact.

How styles compose with the rest of the DSL

A point worth making explicit before the chapter closes: the Style is a property of the Requirement, not of the Feature. Features are Style-neutral.

Concretely, this is what the two class signatures look like:

export abstract class Feature {
  abstract readonly id: string;
  abstract readonly title: string;
  abstract readonly priority: Priority;
  // ... no Style parameter.
}

export abstract class Requirement<S extends RequirementStyle> {
  abstract readonly id: string;
  abstract readonly title: string;
  // ... every other field narrowed by S.
}

A Feature declares which Requirements it satisfies via @Satisfies(Req1, Req2, …). The Requirements it references can each carry a different Style. A single Feature can satisfy one DefaultStyle Requirement, one IndustrialStyle Requirement, one LeanStyle Requirement, all at once — and the Feature class does not change.

This is a deliberate decoupling. The Feature is the deliverable — the thing your team ships, runs, tests, and maintains. The Requirement is the rule — the thing your stakeholders, auditors, regulators, or flow metrics hold you to. A single deliverable can simultaneously satisfy a SIL-2 safety rule, a GDPR regulatory rule, a team-internal INVEST story, and an Anderson-quantified Expedite ticket. The four rules are written in four registers by four audiences. The one deliverable is the intersection.

The practical consequence: a monorepo can have requirements/ directories with Styles mixed at the spec-file level. req-dog-food.ts can extend Requirement<DefaultStyleType>; sif-heater-trip.ts can extend Requirement<IndustrialStyleType>; kaizen-lead-time.ts can extend Requirement<LeanStyleType>. The CLI picks up each Requirement's Style from its type argument, runs the appropriate validators, and renders with the appropriate reporter. The Features that satisfy these Requirements — all of them — declare @Satisfies(...) pointing at the Requirement classes, regardless of Style.

This is what makes the Style system practical. A big-bang migration — "we are switching from DefaultStyle to IndustrialStyle, all Requirements get rewritten this sprint" — is not required. A project can adopt IndustrialStyle for its safety-critical sub-module, LeanStyle for its improvement backlog, AgileStyle for its product backlog, all in the same repo, under the same requirements compliance gate. Each register coexists with the others. The Feature layer stays neutral.

Adding a sixth style

The pattern is documented in packages/requirements/docs/HOW-TO-CREATE-YOUR-STYLE.md and tested in HOW-TO-TEST-YOUR-STYLE.md. The three-step shape:

Step 1 — implement the RequirementStyle interface. A Style is a const-typed object. Declare its vocabulary (arrays, FSMs, schemas), its validators (two functions), its templates (skeletons), its reporter (three renderers), and its fitCriterionAdapters (an array, often empty at first). The file shape mirrors default.ts closely — copy it, rename, adjust.

Step 2 — register it. In the project's bootstrap:

import { createStyleRegistry } from '@frenchexdev/requirements';
import { MyLegalStyle } from './styles/legal';
import { MyAcademicStyle } from './styles/academic';

export const projectRegistry = createStyleRegistry();
projectRegistry.register(MyLegalStyle);
projectRegistry.register(MyAcademicStyle);

Step 3 — wire the CLI to use the project registry instead of the default one. The CLI accepts a --registry <module> flag (or reads it from requirements.config.json), imports the named module, and iterates over projectRegistry.list() for every operation that needs the list of Styles.

No edit to @frenchexdev/requirements. No fork. No patch PR. The sixth Style is a peer to the five shipping Styles in every respect — its templates show up in requirement new, its validators gate its own Requirements, its reporter renders its own Markdown. The core package stays untouched and upgradeable; the project's Style ships in the project's repo, next to the code it governs.

This is the acid test for the SOLID claims earlier in the chapter. Open-closed is not a slogan. You can run register() today, write a Requirement with your new Style, commit the whole thing, and npx requirements compliance --strict will iterate your Style alongside the five built-ins without knowing which of the six is which. That is the contract the registry port delivers.

Forward-link to Chapter 16 — Cross-Package Adoption for the full end-to-end pattern of building a sixth Style in a sibling package and consuming it from a downstream project.

Running-example recap

The running example of this series is FEATURE-TRACE-EXPLORER-TUI — the interactive TTY browser over the traceability graph. Its @Satisfies(...) list points at three Requirements: ReqDiscoverableTraceabilityRequirement, ReqDogFoodRequirement, ReqParallelDeliverableRequirement. All three happen to extend Requirement<DefaultStyleType> today, because the package's own register for its internal rules is the 29148 + Volere + EARS baseline.

The Feature class does not declare a Style. It does not know about Styles. If the three Requirements it satisfies were rewritten in IndustrialStyle — for example, if this package were being adopted inside a safety-critical FSM shop that held its internal tooling to IEC 61508 — FeatureTraceExplorerTuiFeature would not change. The class would keep its id, its title, its priority, its ten abstract AC methods, and its three-element @Satisfies(...) list. What would change is the Requirements it points at: they would extend Requirement<IndustrialStyleType>, carry SIL levels, cite iec-standard sources, and render with the IndustrialStyle reporter.

That is the Style system's practical promise. The Feature layer is the structural invariant. The Requirement layer is where the register lives. The same deliverable satisfies different rules in different registers, and the traceability graph that connects deliverables to rules survives the change of rule-register.

Summary — what this chapter committed to

  • A Requirement is a typed artefact; the register it is written in is a type parameter.
  • The RequirementStyle interface has five slots — vocabulary, validators, templates, reporter, adapter array — each owning one concern.
  • Five Styles ship in the box — DefaultStyle, IndustrialStyle, LeanStyle, AgileStyle, KanbanStyle — one per professional paradigm.
  • The StyleRegistry is a three-method port — get, list, register — that lets a project add a sixth Style without touching the core package.
  • The same fact (atomic cross-artefact rename) can be written in all five registers. The facts do not change; the vocabulary, validators, and rendering do.
  • Features are Style-neutral. Requirements carry the Style. A single Feature can satisfy Requirements in different Styles simultaneously.
  • The registry pattern — port + built-in adapters + project adapters — is the same pattern used for scaffolders, fit-criterion adapters, and the file-system port. One architectural idiom, applied at every extension point.

Chapters 09, 10 and 11 go deep on the individual Styles. Chapter 09 is IndustrialStyle — the thirteen-state heavy-gate lifecycle, the SIL validators, the HAZOP / LOPA / 62443 source kinds. Chapter 10 is LeanStyle — the PDCA FSM, the Gemba source enforcement, the A3 renderer. Chapter 11 is AgileStyle and KanbanStyle together — INVEST enforcement, Gherkin scenarios, Classes of Service, COD quantification. Chapter 12 shows the same registry pattern applied to test scaffolders; Chapter 16 walks through cross-package adoption.

  • A Style is a tiny compiler. Test it like one. — the philosophical anchor for this chapter. If a Style's vocabulary is a keyword table, its validators are a type-checker, and its reporter is a code generator, then a Style is a compiler front-end — and it needs the same discipline a compiler front-end needs.
  • Contention over Convention — the broader meta-argument about why typed, checkable artefacts beat unchecked conventions. Styles are the concrete instance of the meta-point.
  • Chapter 09 — IndustrialStyle deep dive (forward link).
  • Chapter 10 — LeanStyle deep dive (forward link).
  • Chapter 11 — AgileStyle and KanbanStyle deep dive (forward link).
  • Chapter 16 — Cross-package adoption: building a sixth Style in a sibling package and consuming it from a downstream project (forward link).
⬇ Download