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 17 — Cross-Package Adoption — The Monorepo Sweep

Dog-fooding inside one package is credibility at the root. Dog-fooding across fourteen is credibility at scale.

The preceding sixteen chapters have stood on a single plot of land: packages/requirements/. Every decorator demonstrated, every test class, every Feature and Requirement quoted, came from that directory. That was deliberate. The claim of this series is that a Requirements DSL must validate itself with itself first — that any other adopter would be derivative until the mother tongue was spoken.

The mother tongue is now spoken. The question becomes: does anyone else in the neighbourhood answer?

This chapter walks the other thirteen packages of the @frenchexdev/* monorepo and answers that question in the affirmative. Every one of them declares Requirements. Every one of them declares Features. Every one of them has a test suite written entirely in @FeatureTest / @Verifies — zero describe, zero it. Every one of them passes npx requirements compliance --strict from its own root. The discipline is not an artefact of the authoring package; it travels.

The claim is not that the DSL is the only way to write TypeScript libraries. The claim is narrower and more testable: within this monorepo, it is how every library is written, without exception, and the absence of exception is itself a result.

The package list

A tree walk of packages/ at the commit that opens this chapter returns exactly fourteen directories. In alphabetical order:

  1. packages/fsm-compliance/ — the bridge that audits FSM → Feature links.
  2. packages/fsm-rendering/ — the SVG renderer for StateMachineGraph.
  3. packages/markdown-frontmatter-tools/ — idempotent frontmatter edits + codeblock detection.
  4. packages/npm-publish-validator/ — the preflight gate before pnpm publish.
  5. packages/requirements/ — the root package, subject of every previous chapter.
  6. packages/skills-build/ — helper toolkit for the site's build discipline.
  7. packages/skills-content/ — content-authoring helpers (tooltip lints, blog-series checkers).
  8. packages/skills-cv-build/ — md-to-pdf batch driver, originally the CV builder.
  9. packages/skills-fsm/ — FSM-specific authoring helpers (decorator templates, conventions).
  10. packages/skills-repo-structure/ — repo-layout invariants (paths, folder conventions).
  11. packages/skills-testing/ — testing-discipline helpers (coverage gates, harness wiring).
  12. packages/ssg-site/ — the generic SSG framework the site itself consumes.
  13. packages/ts-lints/ — AST-based compiler-API scanners (currently scan-sync).
  14. packages/typed-fsm/ — the decorator-first FSM framework on top of requirements.

Fourteen packages. One root (requirements). One framework layer (typed-fsm) on top of it. Two leaf packages (fsm-compliance, fsm-rendering) on top of that. One large monolithic adopter (ssg-site) that consumes the whole stack. Two library adopters (markdown-frontmatter-tools, ts-lints) that only consume requirements. One validator adopter (npm-publish-validator) that validates the whole workspace and also uses requirements on itself. And the six skills-* packages that constitute a family of opinionated helpers for the CV site's authoring discipline.

Every one of these has a requirements/ directory at its root. Every one of these directory layouts mirrors src/ one-for-one. Every one of these produces a passing npx requirements compliance run when invoked from its own workspace. Those three facts, uniformly true across fourteen different packages, are the single load-bearing observation of this chapter.

Package-by-package tour

The tour below spends the most prose on the six packages whose REQ inventories I can walk in detail: ssg-site, typed-fsm, fsm-compliance, fsm-rendering, npm-publish-validator, markdown-frontmatter-tools. The remaining eight packages are sketched shorter — enough to show the pattern, not a transcription of every Feature they hold.

ssg-site — the largest adopter

@frenchexdev/ssg-site is the generic SSG framework extracted from this very website. It is by a wide margin the largest package in the monorepo. The package.json declares nine distinct sub-path exports: ./build, ./mermaid, ./content-qa, ./dev, ./qa, ./toc, ./render, ./ports, and the root .. Heavy dependencies (puppeteer, jsdom, mermaid, esbuild) are optional peers, so consumers pay only for the sub-paths they instantiate.

Its requirements/features/ directory mirrors src/ one-for-one. A partial walk:

  • requirements/features/build/cache.ts, deps.ts, io-adapter.ts, js-bundler.ts, worker-pool.ts, publish-core.ts, publish-pipeline.ts, render-cache.ts, pages.ts, orphans.ts, copy-content-images.ts, copy-fonts.ts, assets-css.ts, assets-js.ts, version.ts, site-meta.ts, git-dates.ts, static.ts.
  • requirements/features/mermaid/validator.ts, manifest.ts, renderer.ts, validate-core.ts, parse-mermaid-directives.ts, captions-core.ts, find-missing-core.ts, resolve.ts, orchestrate.ts, placeholders.ts.
  • requirements/features/content-qa/link-validator.ts, validate-md-links.ts, find-unlabeled-codeblocks.ts.
  • requirements/features/dev/workflow.ts, hot-reload-wiring.ts, tui-rendering.ts, cli-ext-handlers.ts.
  • requirements/features/qa/a11y-assertions.ts, perf-budgets.ts, visual-tolerances.ts, capture-orchestrator.ts.
  • requirements/features/render/template.ts, page-renderer.ts, build-page.ts, hot-reload-strip.ts.
  • requirements/features/toc/build-toc.ts.
  • requirements/features/ports/icon-resolver.ts.

Take one of these — requirements/features/mermaid/validator.ts — and read it:

import { Feature, Priority, type ACResult } from '@frenchexdev/requirements';

/** Self-Feature for src/mermaid/validator.ts */
export abstract class ValidatorFeature extends Feature {
  readonly id = 'SSG-MERMAID-VALIDATOR';
  readonly title = 'Mermaid validator — block extraction, syntax sniff, caption check, drift detection';
  readonly priority = Priority.High;

  abstract extractsMultipleBlocksSequentially(): ACResult;
  abstract extractsEmptyBlock(): ACResult;
  abstract returnsEmptyWhenNoBlocks(): ACResult;
  abstract extractsAltLineFigureFormat(): ACResult;
  abstract extractsAltLineAltFormat(): ACResult;
  abstract leavesAltUndefinedWhenNoMatch(): ACResult;
  abstract leavesAltUndefinedWhenBlankBefore(): ACResult;
  abstract extractsCaptionLineItalic(): ACResult;
  abstract leavesCaptionUndefinedWhenNoMatch(): ACResult;

  abstract validSyntaxForKnownDiagramKind(): ACResult;
  abstract invalidSyntaxForBlankBlock(): ACResult;
  abstract invalidSyntaxForUnknownKind(): ACResult;

  abstract completeCaptionRequiresAltAndCaption(): ACResult;
  abstract incompleteWhenAltMissing(): ACResult;
  abstract incompleteWhenCaptionMissing(): ACResult;
  abstract incompleteWhenAltBlank(): ACResult;

  abstract detectsOrphanedSvgs(): ACResult;
  abstract detectsMissingSvgs(): ACResult;
  abstract detectsHashMismatches(): ACResult;
  abstract noDriftWhenHashesMatch(): ACResult;

  abstract listSvgsReturnsEmptyWhenDirMissing(): ACResult;
  abstract listSvgsFiltersToSvgOnly(): ACResult;
}

Twenty-two ACs on one Feature, grouped by the five exported functions of the validator module — extractMermaidBlocks, mermaidSyntaxIsValid, hasCompleteCaption, detectManifestDrift, listSvgsInDir. The AC method names describe the exact observable the test will check. The Feature class has no side effects: it is the typed spec of what the validator promises.

The ssg-site package does not, at the time of this writing, declare explicit Requirement classes for every Feature. Its Features sit directly on the shared root "Requirements" concept of the SSG (implicitly, "the site builds and validates correctly"). This is a deliberate choice: a 26-Feature monolith with no refinement graph would be cognitively expensive for little gain, and the one-to-one Feature-to-module mapping already gives most of the navigation benefit. What ssg-site does do, and does uniformly, is:

  • Every test in test/ is @FeatureTest(SomeFeature) at the class level, @Verifies('acName') at the method level.
  • Every describe/it in the entire package tree is zero — the REQ-DOG-FOOD discipline holds.
  • npx requirements compliance --strict from packages/ssg-site/ returns PASS: every non-excluded AC has a test, no critical-priority AC is uncovered, no orphan Feature, and (trivially, since ssg-site does not author Requirements) no orphan Requirement.

The quality-gate line of ssg-site/CLAUDE.md: "100% coverage; 26 Features, 476 ACs, 483 tests; PASS." The three extra tests over ACs are integration-level binders for the whole-pipeline end-to-end pass — ACs whose method names end in endToEnd* and that are counted separately in the compliance report.

The notable shape of ssg-site: its requirements/ tree is the biggest instance in the monorepo of the principle a Feature per source module. The 1:1 mapping means that when a reviewer reads a PR that touches src/mermaid/validator.ts, they can pull up requirements/features/mermaid/validator.ts beside it and see, in one read, exactly which observable behaviours the module owes the world. That is the benefit of the DSL made most visible at scale.

typed-fsm — the stack's ground truth

@frenchexdev/typed-fsm sits one layer above requirements and is the foundation of every FSM-related package in the monorepo. It contributes three decorators to the DSL — @FiniteStateMachine, @State, @Transition — and re-exports the requirements primitives (Feature, Priority, TestLevel, @FeatureTest, @Verifies) so consumers import once.

Its root Requirement:

export abstract class ReqTfsmDecoratorFirstFsmRequirement extends Requirement<DefaultStyleType> {
  readonly id = 'REQ-TFSM-DECORATOR-FIRST-FSM';
  readonly title = 'The FSM declaration vocabulary shall be TypeScript decorators + type parameters — no strings, no runtime reflection, no parallel YAML/XML format';
  readonly priority = Priority.High;
  readonly status = 'Approved' as const;
  readonly kind = 'Constraint' as const;

  readonly statement = {
    pattern: 'ubiquitous' as const,
    response: 'every state machine in the workspace shall be declared by annotating a TypeScript class with `@FiniteStateMachine({ features: [...] })`, typed against state/event type parameters, and no alternative serialisation format (YAML, XML, JSON, string enums) shall be accepted as the source of truth.',
  };

  readonly rationale = {
    claim: 'A decorator-first FSM keeps the declaration co-located with its domain types, preserves refactoring safety, and lets AST extractors produce a canonical graph deterministically — properties that parallel formats systematically break.',
    kind: 'evidence-based' as const,
    evidence: [
      { kind: 'expert-opinion' as const, expert: 'Stéphane Erard — feedback_dsl_open_closed_via_generators', recordedAt: '2026-04-14' },
    ],
  };

  readonly fitCriteria = [
    { kind: 'quality-gate' as const, tool: 'typed-fsm', rule: 'every `@FiniteStateMachine`-decorated class is extractable via `extractStateMachines()` into a canonical `StateMachineGraph` without ambiguity' },
    { kind: 'quality-gate' as const, tool: 'repo-scan', rule: 'no YAML/XML/JSON FSM definition files are committed to the repo outside of generated artefacts' },
  ];

  readonly verificationMethod = 'Test' as const;
}

Three things to notice.

First, the prefix discipline. Every Requirement in typed-fsm is prefixed REQ-TFSM-*. The prefix is mechanical — not an ID namespace enforced by the framework but a convention the package follows — and it makes cross-package traceability readable at a glance: a violation reported as REQ-TFSM-DECORATOR-FIRST-FSM announces both its home and its subject matter before the reader has read a single word of the title.

Second, the hierarchical pattern. REQ-TFSM-DECORATOR-FIRST-FSM is the root. Every other REQ-TFSM-* in the package carries @Refines(ReqTfsmDecoratorFirstFsmRequirement) at the class decorator position. The refinement graph is a fan: one root, four children (REQ-TFSM-AST-EXTRACTION-DETERMINISTIC, REQ-TFSM-CROSS-FSM-CONSISTENCY, REQ-TFSM-REVERSE-TRACEABILITY, and one shared with fsm-compliance via the satisfiedBy Feature register). The topology is readable by npx requirements trace gaps at any time.

Third, the count. Eight Features, 317 ACs, 351 tests — CLAUDE.md quality gate. That is a substantial package by any measure, but the REQ-count is deliberately small (four refining Requirements + the root). typed-fsm has learned that Requirements are expensive to enumerate faithfully, and pays the cost only where the WHY is genuinely contested. Elsewhere, it lets the Features carry the specification load directly — the same pattern ssg-site uses at scale.

The @Satisfies edges from typed-fsm Features to typed-fsm Requirements are internal to the package. But fsm-compliance and fsm-rendering, described below, hold @Satisfies edges that reach across into typed-fsm Requirements — a case of genuine cross-package traceability that the single-package case could not exhibit.

fsm-compliance — the bridge package

@frenchexdev/fsm-compliance is the single-Feature bridge package that joins typed-fsm (which extracts FSM graphs) with requirements (which loads Feature classes) and answers one question: which FSMs in the workspace are not linked to a Feature? That question is the entire mission. A single function, runAudit(deps), takes a pair of ports (readFeatures, readMachines) and returns a categorised report. Non-zero exit on orphans.

Its root Requirement:

export abstract class ReqFsmcNoOrphanFsmRequirement extends Requirement<DefaultStyleType> {
  readonly id = 'REQ-FSMC-NO-ORPHAN-FSM';
  readonly title = 'No @FiniteStateMachine class may exist without a verified link to a business Feature';
  readonly priority = Priority.High;
  readonly status = 'Approved' as const;
  readonly kind = 'Constraint' as const;

  readonly statement = {
    pattern: 'ubiquitous' as const,
    response: 'every class decorated with `@FiniteStateMachine` must declare a non-empty `features: [...]` array, and each referenced Feature class must exist in the workspace.',
  };

  readonly rationale = {
    claim: 'A state machine without a Feature link is engineering for its own sake — it has no traceable business AC and cannot be defended at review.',
    kind: 'evidence-based' as const,
    evidence: [
      { kind: 'expert-opinion' as const, expert: 'Stéphane Erard — feedback_code_has_business_purpose', recordedAt: '2026-04-14' },
    ],
  };
}

Notice the kind: 'Constraint'. This is not a deliverable. It is a prohibition — a negative statement about what shall not exist in the workspace. The DSL accommodates it by offering kind: 'Constraint' | 'Functional' | 'Performance' | 'Interface' | 'Security' | ... on every Requirement. REQ-FSMC-NO-ORPHAN-FSM being a Constraint is load-bearing information for the compliance report's summary section: it tells a reader that no Feature is meant to "implement" this — the Requirement is satisfied by the absence of orphan FSMs, not by the presence of implementing code.

One Feature (AuditFeature). Thirty-two ACs. Thirty-three tests. 100% coverage. npx requirements compliance from packages/fsm-compliance/ returns PASS. The package's quality gate: "Self-dogfooded." — every AC of its own auditing logic is covered by a test it wrote in its own DSL. It audits the audit.

fsm-compliance also demonstrates the minimum-viable cross-package adoption: one Feature class, two Requirement classes (the root plus REQ-FSMC-AUDIT-DETERMINISTIC), a test/ tree, and a bin entry. The whole package is readable in a single sitting and the DSL overhead is a handful of imports at the top of three files. It is a counter-example to the claim that "Requirements frameworks are only worth it on big projects."

fsm-rendering — the heavy-dep isolator

@frenchexdev/fsm-rendering takes a StateMachineGraph (produced by typed-fsm) and emits SVG. It exists as a separate package for one reason: elkjs, the layout engine, is heavy, and not every consumer of typed-fsm wants to pay its cost. So fsm-rendering sits behind a LayoutEngine port — fakes in tests, real elkjs in the CLI shell — and is the place where the elkjs dependency lives exclusively.

Three Features: RendererFeature, RenderCoreFeature, RenderCacheFeature. Three Requirements, each a root-level statement about a property of the output:

  • REQ-FSMR-DETERMINISTIC-SVG — the same graph input must produce byte-identical SVG output across runs.
  • REQ-FSMR-RENDER-CACHE-CORRECT — the cache decision function must never return stale SVG for a graph whose canonical content has changed.
  • REQ-FSMR-LAYOUT-STABLE — small local perturbations of the graph must not produce globally reshuffled layouts (stability of positions across edits).

Three Requirements, three Features, seventy-six ACs, seventy-six tests. Exactly one test per AC. 100% coverage.

This is the shape I called narrow package in the cross-cutting section below: the REQ inventory is under ten entries, each Requirement names a genuine property worth stating, and the Feature count matches the module count. The port (LayoutEngine) lets the whole thing be tested in-memory without invoking elkjs, so the test suite runs in under two seconds on any machine. The DSL scales down to this size without feeling ceremonial.

npm-publish-validator — the gate before npm

@frenchexdev/npm-publish-validator is the gate every package in the monorepo passes through before pnpm publish. It walks the workspace, loads each package's package.json + LICENSE + README.md + dist/, runs a registry of rules, and returns a verdict. A --strict flag turns WARN into FAIL for use in prepublishOnly hooks.

Its Requirement inventory is the richest in the monorepo after requirements itself, precisely because the package's subject matter is rules, and each rule is a distinct statement about what shall be true:

  • REQ-NPV-ZERO-NPM-OOPS — the root: no package reaches npm with a gap a machine could have caught pre-publish.
  • REQ-NPV-BUILD-ARTIFACT-INTEGRITY — the dist/ directory must exist, contain .js + .d.ts for every public export, and match the main/types fields.
  • REQ-NPV-LEGAL-ATTRIBUTION — a LICENSE file at package root, content present, non-empty.
  • REQ-NPV-CONSUMER-DISCOVERABILITY — a README.md, a repository field, and a description — WARN-level by default, FAIL under --strict.
  • REQ-NPV-MONOREPO-RELEASE-SAFETYworkspace:* dependencies must be replaced by real version ranges before publish; changeset publish is the authorised way to do so.
  • REQ-NPV-TARBALL-NO-LEAKfiles field must be present and exclusive (no stray source files or .env in the tarball).
  • REQ-NPV-LICENSE-LEGAL-COMPAT — no devDependency with a GPL-family license can end up in a published runtime dep.
  • REQ-NPV-HUMAN-IN-THE-LOOP — the validator must run as a local prepublishOnly hook, not as a silent CI step; humans read the output.
  • REQ-NPV-DEVELOPER-ERGONOMICS — error messages must be actionable (file path + exact fix), not generic.

Nine Requirements. One Feature (NpmPublishValidatorCoreFeature) that holds one AC per rule × case (fail case, warn case, pass case, strict-upgrade case, allow-list case). The root statement, reproduced:

export abstract class ReqNpvZeroNpmOopsRequirement extends Requirement<DefaultStyleType> {
  readonly id = 'REQ-NPV-ZERO-NPM-OOPS';
  readonly title = 'No package reaches the npm registry with a gap a machine could have caught pre-publish';
  readonly priority = Priority.Critical;
  readonly status = 'Approved' as const;
  readonly kind = 'Constraint' as const;

  readonly statement = {
    pattern: 'event-driven' as const,
    trigger: 'before any invocation of `pnpm publish` or `changeset publish`',
    response: '`npm-publish-validate --strict` must have returned exit 0 for the target package(s)',
  };

  readonly rationale = {
    claim: 'Unpublishing a broken release is irreversible after 72 hours and costs avoidable human time; a static gate catches deterministic gaps at zero runtime cost.',
    kind: 'principle' as const,
    evidence: [
      { kind: 'metric' as const, name: 'packages-without-license', value: '8/8', date: '2026-04-14' },
      { kind: 'metric' as const, name: 'packages-without-readme',  value: '6/8', date: '2026-04-14' },
      { kind: 'metric' as const, name: 'packages-without-dist',    value: '2/8', date: '2026-04-14' },
    ],
  };
}

The evidence array is the item worth reading twice. Three metrics captured on the day the Requirement was written: 8/8 packages without a LICENSE file at that date, 6/8 without a README.md, 2/8 without a dist/. Those metrics are not cosmetic — they are the grounding evidence for the rationale.claim. A reviewer who disputes the Requirement's priority is disputing recorded numbers, not vibes.

This is what Requirement-as-first-class-type buys over Requirement-as-prose. The evidence is a typed field, not a paragraph; the date is a typed brand, not a free-text "last year or so"; the rationale is a {claim, kind, evidence[]} record, not a footnote. The Requirement becomes reviewable with the same tooling that reviews code.

markdown-frontmatter-tools — the micro-adopter

@frenchexdev/markdown-frontmatter-tools is the smallest adopter in the monorepo after the skills-* family. Two exported functions:

  • backfillFrontmatterField(source, key, value) — idempotent YAML frontmatter edit, returns {content, changed}.
  • findUnlabeledCodeblocks(source) — detector that returns {line, column}[] for code fences missing a language tag.

Two Features. Three Requirements. Sixteen ACs. Sixteen tests. 100% coverage. The whole requirements/ tree fits on one screen.

And yet the pattern holds. REQ-MFT-CONTENT-SAFETY is the root. Two child Requirements refine it: REQ-MFT-FRONTMATTER-VALID (idempotency + structure preservation) and REQ-MFT-CODEBLOCKS-LABELED (codeblock detection is complete + precise). The latter two are decorated with @Refines(ReqMftContentSafetyRequirement) — the same refinement syntax requirements demonstrated on itself, used here on a package whose entire public surface is two functions.

The example is worth dwelling on because it answers the recurring objection: "This DSL is fine for a big project, but surely it is overhead for a two-function library." The counter-evidence is markdown-frontmatter-tools. Three files in requirements/ (root Requirement, two refining Requirements, plus the two Feature files in requirements/features/). Twelve hours to author originally, once the pattern was established elsewhere. Nothing in the authoring felt heavier than writing the CHANGELOG paragraph that was in any case going to be written. And what was gained: the test file becomes machine-readable, the @Refines edges become traversable by requirements trace, and the package's contract becomes self-documenting in the same format as every other package in the workspace.

Read the REQ-MFT-FRONTMATTER-VALID statement for the exact voice:

@Refines(ReqMftContentSafetyRequirement)
export abstract class ReqMftFrontmatterValidRequirement extends Requirement<DefaultStyleType> {
  readonly id = 'REQ-MFT-FRONTMATTER-VALID';
  readonly title = 'backfillFrontmatterField must be idempotent and preserve existing structure';
  readonly priority = Priority.High;
  readonly status = 'Approved' as const;
  readonly kind = 'Constraint' as const;

  readonly statement = {
    pattern: 'event-driven' as const,
    trigger: 'when `backfillFrontmatterField(source, key, value)` is invoked',
    response: 'the function must add the key/value if absent, leave the markdown untouched if the key is already present, refuse to write if the frontmatter block is unclosed, and never alter the body.',
  };

  readonly rationale = {
    claim: 'Backfill scripts run repeatedly across the content corpus. A non-idempotent backfill silently corrupts frontmatter or duplicates keys.',
    kind: 'evidence-based' as const,
    evidence: [
      { kind: 'expert-opinion' as const, expert: 'Stéphane Erard — content-authoring pipeline', recordedAt: '2026-04-14' },
    ],
  };
}

An EARS event-driven statement — when X happens, Y must hold — expressed as typed fields, with a fitCriteria array, a verificationMethod, a risk object with an ifNotMet narrative and a list of mitigations. This is the whole API contract of backfillFrontmatterField in one place, in a form a compliance tool can read. The function's TSDoc would not contain half as much.

ts-lints — the scanner host

@frenchexdev/ts-lints is a tiny, growing family of AST-based scanners that use the TypeScript compiler API directly rather than going through ESLint. The current release ships one scanner — scan-sync, which detects synchronous fs / child_process usage in modules that should stay async — and the package is structured to absorb additional scanners one at a time without touching the existing ones.

Two Requirements:

  • REQ-TSL-IMPORT-HYGIENE — the umbrella root. Import discipline across the workspace: no synchronous I/O in hot paths, no deep relative imports, no circular imports (planned scanners).
  • REQ-TSL-SYNC-USAGE-TRACKED — the first refinement. Every synchronous fs/child_process import must be detected with precise file/line/column reporting and an allow-list mechanism for legitimate entry-script usage.

The second is worth quoting:

@Refines(ReqTslImportHygieneRequirement)
export abstract class ReqTslSyncUsageTrackedRequirement extends Requirement<DefaultStyleType> {
  readonly id = 'REQ-TSL-SYNC-USAGE-TRACKED';
  readonly title = 'Every synchronous fs/child_process import must be detected, with file/line/column reporting';
  readonly priority = Priority.High;

  readonly statement = {
    pattern: 'event-driven' as const,
    trigger: 'when `scanSyncUsage({ files, allow?, readFile })` walks a TypeScript codebase',
    response: 'the scanner must report every `*Sync` import or `require()` call in `fs`/`fs/promises`/`child_process` (including `node:` prefix) with precise `{file, line, column, module, symbol}`, and skip type-only imports + allow-listed files.',
  };

  readonly rationale = {
    claim: 'A coarse "you have sync somewhere" report is unactionable. Engineers need the exact file/line/column to fix or to add a justified allow-list entry.',
    kind: 'evidence-based' as const,
  };
}

One Feature (SyncUsageFeature), 28 ACs, 30 tests, 100% coverage. The package's open/closed discipline is visible at the Requirement level: REQ-TSL-IMPORT-HYGIENE is deliberately generic so that future scanners (import-depth, cycles, barrel-file detection) can @Refines it without the umbrella needing rewording. Adding a second scanner is a matter of dropping src/<new-scanner>.ts + requirements/features/<new-scanner>.ts + requirements/requirements/req-tsl-<new-property>.ts + test/<new-scanner>.test.ts and nothing else. Zero edits to existing files.

skills-cv-build — the batch builder

@frenchexdev/skills-cv-build is the md-to-pdf batch driver originally extracted from Stéphane Erard's CV build pipeline. It takes a config listing a set of markdown source files and their target PDF outputs, calls md-to-pdf for each, and guarantees atomic "build this whole set" semantics. A --watch mode rebuilds on change via chokidar.

Two Features: BuilderFeature (single-job build) and BatchFeature (the batch orchestrator). Two Requirements:

  • REQ-SCB-CV-PDF-PARITY — the PDF output must preserve visual parity with the site's rendered markdown (fonts, heading scale, code-block framing, image placement).
  • REQ-SCB-BATCH-IDEMPOTENT — re-running runBatch with unchanged inputs must be a no-op at the file-system level (mtimes preserved, no spurious rewrites).

Twenty ACs, 21 tests, 100% coverage. The Feature tree is smaller than npm-publish-validator's but the port discipline is stricter: PdfBuilder, FileOps, Clock, Logger are all ports, and the tests never touch a real md-to-pdf or a real clock. The package is as much a demonstration of port-first design as it is of Requirements-first authoring.

The five skills-* siblings

The remaining five skills-* packages — skills-build, skills-content, skills-fsm, skills-repo-structure, skills-testing — each hold a small toolkit of opinionated helpers for one authoring discipline. Each one has:

  • A CLAUDE.md describing its mission in 20-30 lines.
  • A requirements/features/ tree with between one and four Features.
  • A requirements/requirements/ tree with a single root Requirement.
  • A test suite in test/ using @FeatureTest / @Verifies exclusively.
  • A quality-gate line: coverage percentage, Feature count, AC count, test count, PASS.

skills-build — helpers around the npm run build:static pipeline (worker pool wiring, mermaid manifest reconciliation). Rooted on REQ-SKB-PIPELINE-DISCIPLINE.

skills-content — content-authoring lints (tooltip presence, blog-series ordering, frontmatter completeness). Rooted on REQ-SKC-AUTHORING-DISCIPLINE.

skills-fsm — FSM-authoring conventions (decorator templates, state/event naming, features: [...] non-empty). Rooted on REQ-SKF-FSM-CONVENTIONS.

skills-repo-structure — repo-layout invariants (package paths, folder conventions, file presence). Rooted on REQ-SKR-LAYOUT-INVARIANT.

skills-testing — testing-discipline helpers (coverage-gate enforcement, harness wiring, no-describe-no-it sniff). Rooted on REQ-SKT-TEST-DISCIPLINE.

Five packages. Five roots. One pattern. Each of these would, in a pre-DSL world, have been a README.md paragraph in the monorepo root with a bullet list of conventions — unverifiable, unstated as types, liable to rot. Under the DSL they are each a small adopter with its own compliance run and its own PASS line.

Patterns observed across packages

Step back from the tour. Walk the fourteen adopters again and three shapes repeat.

Narrow packages — under ten REQs

markdown-frontmatter-tools (3 REQs), fsm-compliance (2 REQs), fsm-rendering (3 REQs), ts-lints (2 REQs), skills-cv-build (2 REQs), the five skills-* siblings (1 REQ each), and typed-fsm (4 REQs) all sit in this bracket. Their REQ inventories are focused on what shape of output the library must produce — API shape, correctness invariants, deterministic output, idempotency, precise error reporting. The rationale fields are short. The risk.ifNotMet narratives are one sentence.

The narrow-package signature: one root Requirement that captures the whole mission, one-to-three refining Requirements that pin down the non-obvious properties, and everything else in the Feature classes. @Refines is the only decomposition decorator used. Cross-package @Satisfies edges are rare (only fsm-compliance and fsm-rendering import Requirement classes from typed-fsm, and even then it is the @Satisfies on their internal Features that reaches across, not a @Refines between Requirements).

Medium packages — ten to twenty REQs

Only npm-publish-validator currently sits in this bracket, with its 9 Requirements — on the low end of the range, and likely to grow as additional publish rules are added. The medium-package signature adds a second concern beyond API shape: cross-cutting concerns that the library cares about on behalf of its users.

For npm-publish-validator these include legal attribution (licensing), tarball safety (no accidental .env leak), monorepo release safety (workspace-to-registry conversion), developer ergonomics (actionable error messages), and human-in-the-loop policy (the validator is run locally, not silenced in CI). Each of those is a genuine property worth stating separately, and the rationale of each carries non-trivial evidence. The medium-package REQ inventory begins to need navigation: npx requirements trace matrix NPV-CORE already returns a table of 9 rows × ~40 AC columns, and that is with a single Feature. Two Features and the matrix needs hierarchical browsing.

Large packages — twenty-plus REQs

Only requirements itself sits in this bracket, with its 22 Requirements covered across chapters 04-05 of this series. The large-package signature adds a third concern: REQs cluster by subpath, and the refinement graph becomes a hierarchy. requirements does this implicitly via its root-and-refining-children pattern; ssg-site, at its current scale, would be another candidate but has chosen to keep its Requirement set small (one implicit root) and let its 26 Features carry the specification load directly.

The large-package threshold to watch for is whether the @Refines tree stays a shallow fan (one root, N children, depth 2) or grows into a deeper hierarchy (root, mid-tier Requirements, leaf Requirements, depth 3+). The DSL supports both; the requirements package is currently at depth 2, and my current working hypothesis is that depth 3 is a smell — a depth-3 tree usually means a mid-tier Requirement should be a Feature in disguise.

The invariant across all three shapes

Across all fourteen packages, one discipline holds without exception: zero describe, zero it, anywhere in the test trees. Every test file opens with:

import { FeatureTest, Verifies, type ACResult } from '@frenchexdev/requirements';
import { SomeFeature } from '../requirements/features/some-feature.js';

@FeatureTest(SomeFeature)
export class SomeFeatureTest {
  @Verifies('someAcName')
  someAcName(): ACResult {
    // ...
    return { satisfied: true };
  }
}

Not one of the fourteen packages has a single Vitest-native describe/it block. The REQ-DOG-FOOD discipline that originated in the requirements package — "the DSL must be tested with itself" — has propagated to every other package in the monorepo without exception. The monorepo-wide compliance run is the proof: npx requirements compliance --strict invoked at the workspace root visits every package, scans every test file, and confirms the rule. If a stray it(...) appeared anywhere, the scan would flag it and the strict flag would exit non-zero. It does not. The discipline is not an authoring convention — it is an enforced invariant.

The failure that this prevents

Imagine a failure the DSL has been designed to catch, and picture its shape without the DSL.

ssg-site's requirements/features/mermaid/validator.ts declares, among its twenty-two ACs, the four detectManifestDrift checks: detectsOrphanedSvgs, detectsMissingSvgs, detectsHashMismatches, noDriftWhenHashesMatch. Each of those ACs is satisfied by a test method in test/mermaid/validator.test.ts, class ValidatorFeatureTest, each @Verifies('detectsOrphanedSvgs'), and so on.

Now imagine a refactor. A contributor changes the caption-validation logic in src/mermaid/validator.tshasCompleteCaption is rewritten to be stricter (it now rejects captions that are only whitespace, where previously it accepted them). The refactor does not touch the manifest-drift logic at all. But in passing, it renames detectManifestDrift to detectDrift, because the contributor thinks it reads better. All four detectsOrphanedSvgs et al. tests still point at the renamed function. The tests still pass.

Now imagine the downstream consequence. The @Verifies('detectsOrphanedSvgs') decorator still references the AC name detectsOrphanedSvgs. The ValidatorFeature class still declares the abstract method detectsOrphanedSvgs(): ACResult. The test class still implements it. The compliance run still reports a match. But the function name the test exercises — that is a runtime detail outside the DSL's type system.

This is the crucial point. The DSL does not catch every bug. It catches exactly the class of bugs it was designed for: missing coverage, orphan Features, orphan Requirements, broken cross-chain links. A silent rename of a private helper is not in that class.

What is in that class? Consider a different refactor. The contributor is tidying src/mermaid/validator.ts and, reading the file, decides that listSvgsInDir is not really part of the validator's public surface — it belongs in src/mermaid/manifest.ts. They move it. In doing so, they delete the corresponding ACs listSvgsReturnsEmptyWhenDirMissing and listSvgsFiltersToSvgOnly from ValidatorFeature. But the test file ValidatorFeatureTest still has two methods decorated with @Verifies('listSvgsReturnsEmptyWhenDirMissing') and @Verifies('listSvgsFiltersToSvgOnly').

The keyof T & string constraint on @Verifies<T> now fails to compile. ValidatorFeature no longer has those AC names in its keyof. The test file does not typecheck. tsc --noEmit fails. The PR is red before Vitest is even invoked.

Or the converse: the contributor adds listSvgsInDir to manifest.ts, introduces the corresponding Feature class ManifestFeature, adds the two ACs to its abstract method surface, but forgets to write tests for them. npx requirements compliance --strict from packages/ssg-site/ scans the test tree, finds no @Verifies('listSvgsReturnsEmptyWhenDirMissing') on any @FeatureTest(ManifestFeature) class, and reports the gap. Non-zero exit. The PR is red, a different red this time, before merge.

The cross-package variant makes this more pointed. Suppose ssg-site's mermaid renderer declared @Satisfies(ReqFsmrDeterministicSvgRequirement) — a cross-package satisfaction edge reaching into fsm-rendering's Requirement namespace. (It does not today; I am imagining.) Now a contributor changes fsm-rendering to remove REQ-FSMR-DETERMINISTIC-SVG entirely, because the Requirement is being split into two. The import in ssg-site's mermaid/renderer.ts (the hypothetical @Satisfies edge) breaks at TypeScript level. The build fails in ssg-site before a single test runs. The coupling is made visible by the type system, not by a grep or a Slack message.

The DSL's promise is narrow. It does not say the tests are correct; it says the tests name the ACs they claim to verify, by identifier the compiler checks, and every AC has a test. Those two invariants, under Vitest's ordinary execution, constitute a net that catches a specific kind of drift — the kind where the words in the spec and the assertions in the tests drift out of alignment silently. Monorepo-wide, that kind of drift is the most common failure mode in a growing library. The DSL closes it.

The package graph

A single mermaid flowchart captures the fourteen-package topology — the build-dep edges and the dev-dep edges among them. requirements is the root. typed-fsm extends it. fsm-compliance and fsm-rendering sit above typed-fsm. ssg-site consumes the deepest stack. The skills-* siblings, ts-lints, markdown-frontmatter-tools, and npm-publish-validator consume requirements directly for dog-food purposes but export their own library surface.

Diagram
The monorepo package graph. Solid arrows are build dependencies, dashed arrows are dev or peer dependencies, and the audit edges show where npm-publish-validator reads each core package before release.

Figure 17.1 — The fourteen-package graph of the @frenchexdev monorepo. Solid arrows are build-time dependencies (the consumer cannot compile without the producer). Dashed dev arrows mark dog-food relationships: every package develops against @frenchexdev/requirements in its devDependencies for self-audit, even when the published tarball carries no runtime reference. Dashed peer arrows mark peer relationships; ssg-site takes typed-fsm and fsm-rendering as peers so the consumer of ssg-site chooses whether to pay for FSM rendering. Dashed audits arrows mark the control direction of npm-publish-validator: it runs against every other package as its workspace before publish, including itself. The colour graduation (root indigo → framework indigo → bridge blue-indigo → consumer lavender → leaf pale-indigo → skills near-white) tracks position in the stack rather than importance. Read the figure as: one root, one framework layer, two FSM-specific bridges, one large consumer, and nine leaf packages that each adopt the DSL for their own self-audit without entering into the runtime dependency graph.

Alt text: Indigo-themed flowchart with the @frenchexdev/requirements package at the top, a typed-fsm framework node below it, fsm-compliance and fsm-rendering branching off typed-fsm, ssg-site consuming the deepest stack, and nine leaf packages (npm-publish-validator, markdown-frontmatter-tools, ts-lints, skills-cv-build, skills-build, skills-content, skills-fsm, skills-repo-structure, skills-testing) each connected back to requirements via dashed dev-dependency arrows. Dashed 'audits' arrows fan out from npm-publish-validator to every other node.

The three things to notice in the figure:

First, requirements is the only node with no incoming solid arrows from inside the monorepo. It is the root of the DAG by construction. Every other package's type-level vocabulary reaches back to it, directly or transitively.

Second, the dev edges are the dog-food relationships. Even packages that do not re-export anything from requirements (markdown-frontmatter-tools, ts-lints, npm-publish-validator, the skills-* family) still declare it as a devDependency — because they use it to describe their own tests. When those packages are published to npm, the requirements import is not in the runtime dependency manifest. The discipline is an authoring discipline, not a consumer burden.

Third, npm-publish-validator has fan-out audit edges to every other package. The validator's job at publish time is to walk the whole workspace and apply its rules. The graph makes this legibly one-way: npm-publish-validator is downstream of requirements (it dog-foods for its own tests) but is independently upstream of every other package in its enforcement role. This is a rare case where a leaf-in-the-dep-graph package has an orthogonal role across the workspace.

Running-example recap

The series' running example — FEATURE-TRACE-EXPLORER-TUI — lives in packages/requirements/requirements/features/feature-trace-explorer-tui.ts. It satisfies three Requirements, all REQ-* prefixed for the requirements package itself. It does not cross any package boundary in its @Satisfies list; it is a thoroughly internal example of the pattern.

But every single one of the other thirteen packages carries the same shape. ReqMftFrontmatterValidRequirement is satisfied by BackfillFrontmatterFieldFeature. ReqFsmcNoOrphanFsmRequirement is satisfied by AuditFeature. ReqNpvZeroNpmOopsRequirement is satisfied by NpmPublishValidatorCoreFeature. ReqTfsmDecoratorFirstFsmRequirement is satisfied by FsmDecoratorFeature, ExtractStateMachinesFeature, ScanBindingsFeature, and more.

Every one of those satisfactions is a @Satisfies(...) class decorator at the Feature level. Every one of those Features has abstract AC methods. Every one of those ACs has a @Verifies(...) test. The pattern does not change across packages; only the domain vocabulary changes.

This is, in the end, the substance of "dog-food the DSL." Write the DSL. Test the DSL with the DSL. Use the DSL to describe every package that extends the DSL. Use the DSL to describe every package that merely consumes the DSL. At no point along that chain does an escape hatch get opened. The rule held in one package. The rule holds in fourteen. If a fifteenth is added next month, the rule will hold in fifteen. That is what an ecosystem looks like from inside.

⬇ Download