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

Epilogue — typed-specs was a ladder; this is the roof

A ladder is judged by whether you can stand on its top rung. A roof is judged by whether something can live under it.

Three sentences for a long series

The series argued, across thirty-three files, a single claim in three parts. First, that the word Requirement had been carried by the typed-specs work without ever being modelled by it — that the vocabulary was ahead of the types, and the gap was load-bearing. Second, that closing the gap required not just adding a Requirement base class but building a refinement graph, a many-to-many satisfaction relation, a plural-style dimension, and a compile-time @Verifies binding — each of which changes what the decorator layer can say about the program beneath it. Third, that the only credible test of such a DSL is to point it at itself: twenty-two Requirements, twenty-five Features, fifty-four test classes, all declared in the package's own grammar, with the package's own gate running over them.

That is the whole argument. Everything else in the series was the work of making it visible.

What changed at the decorator layer that could not change before

Re-read the decorator surface of typed-specs — @Implements, @Covers, @Exclude — and notice what it could not say. It could not name a Requirement, because no Requirement type existed to be named. It could not express that one Feature satisfied several Requirements, because satisfaction was not a relation, it was an identity. It could not express that one Requirement was refined by others, because refinement was not a relation either; the word requirement collapsed into the word feature at the type level, and a collapsed relation cannot compose.

The @frenchexdev/requirements decorator surface is different in exactly the places those sentences locate.

  • @Requirement declares a concrete Requirement subclass. A type. A first-class M2 citizen.
  • @Refines(Parent) carries a class reference, not a string. The refinement graph is walkable with instanceof, not with startsWith.
  • @Satisfies(ReqA, ReqB, ReqC) takes a variadic of class references, not a single string. Many-to-many is the default, not a workaround.
  • @Verifies(FeatureClass, 'acName') binds a test method to a named AC of a specific Feature, with keyof T checking at compile time — the same type-level trick typed-specs pioneered for @Implements, now extended to test classes with no describe/it involved.
  • @FeatureTest(FeatureClass) is the decorator that names the test class as a test class — not a Vitest helper, an identity in the DSL's own ontology.

None of this is exotic. It is what you get when Requirement stops being a word in frontmatter and starts being a class in a registry. The decorator layer carries the difference the way a keystone carries an arch: the blocks beneath were waiting for it.

The running thread, one last time

The primary pedagogical example of the series was FEATURE-TRACE-EXPLORER-TUI — the interactive TTY browser over the traceability graph that npx requirements trace explore will eventually ship. It was chosen because it is small enough to quote in full and because it satisfies more than one Requirement, so the many-to-many shape is visible in every chapter that cites it.

In typed-specs, this feature could have been declared. It would have looked like this:

export abstract class FeatureTraceExplorerTuiFeature extends Feature {
  readonly id = 'FEATURE-TRACE-EXPLORER-TUI';
  readonly title = 'Interactive TTY browser over the traceability graph';
  readonly priority = Priority.Low;

  abstract traceExplorerBuildsGraph(): ACResult;
  abstract traceExplorerHandlesArrowKeyNavigation(): ACResult;
  // ...
}

And it would have been correct as far as it went. The abstract methods would have compiled, the @Implements decorator on a test class would have bound a Vitest test to each AC, and the compliance scanner would have reported coverage. That work was real. The ladder held.

What it could not have said, because the types did not exist to say it with, was why this feature exists. In the new package, the same class wears its @Satisfies list openly:

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

Three class references, three first-class Requirements, each with its own refinement parent, its own style, its own acceptance story. A reader who opens the file now can walk upward — from the Feature to the Requirements it satisfies, to the parent Requirements those refine, to the style those Requirements carry — without leaving the type system. A reader who opens the file in typed-specs would have found an orphan. The word "navigation" in the title would have been the only pointer to intent; the intent itself would have lived in prose somewhere else.

That is the roof. The ladder could carry you to a Feature. The roof lets the Feature have a ceiling over it.

Pointers back to typed-specs/07-roadmap.md

The roadmap at typed-specs/07-roadmap.md named seven gaps in the V1 system. It is worth being explicit about which of those the @frenchexdev/requirements work closes and which it does not.

Closed by this work.

  • The gap admitted in the roadmap itself — the tracedTo field pointing at a higher-level requirement. That field is now @Satisfies / @Refines with class-valued arguments rather than strings, and both relations are walkable at compile time.
  • Gap 4: Reverse Mapping. Given a test class, the @FeatureTest(FeatureClass) decorator declares which Feature it exercises; given a Feature, @Satisfies declares which Requirements it meets; given a Requirement, the registry answers which Features cover it. Reverse mapping is now structural, not a post-hoc scanner invention.
  • Gap 7: Compliance as a Feature. Infrastructure concerns — the dog-food mandate, the parallel-deliverable constraint, the discoverable-traceability requirement — are now themselves Requirement subclasses, not orphan scripts. The DSL treats its own operational concerns in its own grammar.

Not closed by this work. Listing these honestly matters more than listing the wins.

  • Gap 1: Workflow Integration. The compliance scanner still runs as a discrete step. It has better diagnostics and a better gate, but it is not yet a step in every project's workflow menu by default.
  • Gap 2: Pre-Commit Hooks. The package ships a CLI gate but no opinionated husky scaffolding. A team adopting it still writes their own pre-push entry.
  • Gap 3: Historical Trend Tracking. Coverage is computed fresh each run. No persisted time series, no monotonic-non-decreasing promise, no dashboard.
  • Gap 5: ESLint Enforcement. Shift-left rules in the IDE remain unwritten. A missing @Verifies surfaces at build time, not at edit time.
  • Gap 6: Dynamic Test Granularity. The @Verifies decorator binds one test method to one AC, which is a clear step up from coversACs batches — but there is still no generator that produces a per-AC test file skeleton.

The typed-specs roadmap was honest about what V1 did not do. This series is honest about what the package does not yet do either. A roof can still leak. The shape of what remains matters because it tells the next series where to start.

Pointer forward — the three appendices

The series closes with three reference documents rather than a dramatic chapter, because the material earned it.

  • Appendix A — Glossary. Every Requirement ID, every Feature ID, every Style, every decorator, every vocabulary term, in one sortable document. Read this when a chapter cites a name you have forgotten.
  • Appendix B — Frequently Asked Questions. The questions that came up repeatedly while writing — why not Jira, why not OSLC, why not Catala, why not Fitnesse, why no describe/it — answered without defensiveness.
  • Appendix C — Bibliography and Prior Art. ISO 29148, SysML v2, EARS, IEC 61508, Catala, the Diem CMF lineage, Bertrand Meyer's Design by Contract, and the typed-specs series itself. What the DSL borrowed, what it rejected, what it owes.

The appendices are not decoration. They are where the next reader — the one who adopts the package — will look first.

What remains

A final honesty.

The package is small. Twenty-two Requirements, twenty-five Features, fifty-four test classes, one running monorepo. It is not a product. It is a proof that the grammar is liveable at the scale of a single author's own work. The gap between liveable by one and liveable by a team is the gap the next year will have to close, and that gap contains the unclosed roadmap items above, plus the ones no one has named yet. The package's own REQ-PARALLEL-DELIVERABLE exists precisely because we do not yet know whether a team can adopt this DSL without serialising through a single author.

There is also a meta-honesty to record. The series is thirty-two thousand lines long. That is a lot of prose for a small package. The length was a deliberate choice — to write it once and thoroughly, rather than repeatedly half-finish it — and I stand by the choice. But a DSL whose story needs thirty-three files to tell is a DSL whose story is still finding its shortest form. Future writing about @frenchexdev/requirements should be shorter, because the reader's primer is now this series and the primer need not be repeated.

And the roof, like every roof, is provisional. The typed-specs work was correct within its ladder. The work described here is correct within its roof. Something will come along — a meta-metamodel, a Catala-style legal jonction, a refinement calculus over the @Refines graph, a formal Isabelle/HOL backend for the five styles — that makes this roof look like a ladder in turn. That is how this is supposed to go. The honest claim is not that @frenchexdev/requirements is finished. The honest claim is that its shape is defensible and its gaps are named.

Thanks

If you have read the series end to end — or even read in one of the four reader paths the hub lays out — you have given this work more attention than it can expect. Thank you. The package exists because the gap it closes bothered me enough to write thirty-three files about it; the series exists because you were willing to read them. Those two facts are the full economy of what this work is.

Now close the tab, open the package, scaffold a Requirement, and watch the gate flip green. That is the last instruction the series will give.

⬇ Download