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

What If Requirement Is Just One Extension Kind?

This is the dissolution test. The hypothesis says requirement is one extension kind among many — peer to type, class, method, module, application, type-system. If that is true, the existing @frenchexdev/requirements-shared-kernel should be re-readable as the implementation of a single extension functor, Ext_R, with no information lost and some information gained by the re-reading.

If the re-reading is information-neutral — if it produces the same kernel with different label paint — the hypothesis is decorative and I should stop.

I want to do the re-reading honestly, knowing in advance that there is a real chance it produces nothing.

The existing kernel in one paragraph

The current kernel exposes seven decorators and a small handful of brand types. @FeatureTest(F) marks a class as the test suite for a Feature F. @Verifies<F>('acName') marks a test method as verifying one acceptance criterion of F. @Expects(TestLevel.UNIT, TestLevel.E2E, ...) declares which test levels a verification expects. @Satisfies(R1, R2, ...) declares that a Feature class satisfies one or more Requirements. @Refines(Rparent) declares that a child Requirement refines a parent. @TypeCheckOnly() and @Exclude() are tagging decorators for the analyzer. The Feature and Requirement themselves are TypeScript classes with branded identity and an explicit Status workflow drawn from a pluggable RequirementStyle.

Read that paragraph again as a single functor and see what happens.

Re-reading as Ext_R

Under the hypothesis, every decorator in the kernel is the implementation of one operation: projecting an intention into the requirement category, $\text{Ext}_R : \mathcal{I} \to \mathcal{R}$. The projection produces:

  • A Feature class — the shape of the verifiable contract.
  • A set of acceptance criteria — the checkable claims about the contract.
  • A set of Requirement objects — the obligations the contract carries.
  • A set of @Verifies test methods — the verifications that turn the claims into pass/fail facts.
  • A Status workflow — the lifecycle the contract moves through.

Each of these is a piece of the projection. They are not independent objects; they are co-produced by the same projection step. The existing kernel treats them as independent (a @FeatureTest class is decorated separately from the @Satisfies class is decorated separately from the @Refines class), but under the hypothesis they are all one fact in five fragments.

What would change if they were treated as one fact? Three things at least.

First, the assertion of @Satisfies would degrade. As I noted in Part 1, @Satisfies(R1, R2) is a load-bearing claim: it asserts that a Feature stands in a particular relation to particular Requirements, and the kernel has no way to detect that the assertion is wrong. Under the hypothesis, the relation is a consequence of shared parentage — both Feature and Requirements come from the same Ext_R(Intention) projection — and the assertion becomes a derived attribute rather than a hand-written claim. The detection of "developer linked Feature to wrong Requirements" becomes "developer materialised the wrong projection" — a different (and arguably less common) error mode.

Second, @Refines would generalise. The existing kernel has @Refines for Requirement-to-Requirement refinement. Under the hypothesis, @Refines is the projection of a morphism inside $\mathcal{I}$ — if intention $I_2$ refines intention $I_1$, then $\text{Ext}_R(I_2)$ refines $\text{Ext}_R(I_1)$. This is the functoriality law. It implies that every extension kind, not just the requirement kind, should preserve refinement under projection. A @TypeExtension of a refining intention should refine the @TypeExtension of the parent intention. The existing kernel has no such mechanism for type extensions because it has no type extensions; the hypothesis says the mechanism should be uniform across all kinds, and the existing @Refines is just the manifestation of that uniformity in the requirement kind.

Third, the RequirementStyle pluggable would be revealed as a special case of something larger. The current kernel has @frenchexdev/requirements-styles — five pluggable workflow styles (Default, Industrial, Agile, Lean, Kanban) each defining a statusWorkflow with states and transitions. Under the hypothesis, this is not requirement-specific; it is an extension-style mechanism that any extension kind could use. A TypeExtension could have a style governing how types evolve (provisional → stable → deprecated). A ModuleExtension could have a style governing how module surfaces evolve (internal → public → frozen → deprecated). The "status workflow" idea is currently buried inside the requirement kind because the requirement kind is the only one named, but the idea is not requirement-specific.

These three changes, taken together, are the unification claim. The hypothesis says that the existing kernel is doing things that are not really requirement-specific, that look requirement-specific only because there is no other extension kind to compare them to, and that would generalise naturally if other extension kinds existed.

The dissolution test, taken honestly

The test I committed to in Part 1 is: does this reframing change anything we would write differently tomorrow? I want to run the test on the three changes above and see whether any of them produce concrete code differences.

On @Satisfies degrading to derived attribute. The change in code is real but small. The current @Satisfies(R1, R2) decoration would be replaced by a derivation step that looks at the shared Intention and emits the same set of Requirements. If the derivation is implemented as a source generator (which is the natural fit given the existing ts-codegen-pipeline infrastructure), the developer-facing API changes: they write @Feature(intentionOf: UserAuthentication) and the Satisfies linkage is generated. This is a real code difference. Whether it is a valuable code difference depends on whether the derived linkage catches errors the manual decoration would have missed.

I am cautiously optimistic but not certain. The cases I can imagine are: (1) a Feature gets @Satisfies(R_old) but the Requirement was renamed and the decoration still type-checks because the brand is structural — derivation would surface the rename; (2) a Feature @Satisfies four Requirements but the Intention only generates three, indicating either the Feature is over-scoped or the Intention is under-specified — derivation would surface the mismatch. Both are real failure modes I have seen. Neither is a frequent failure mode. The bet is that they are frequent enough to justify the derivation step, and I do not know yet if the bet pays.

On @Refines generalising to all extension kinds. This is the change that is hardest to evaluate because the other extension kinds do not exist yet. The claim is anticipatory: if I had TypeExtension and ModuleExtension, refinement would propagate, and that propagation would catch errors where a child intention's projection diverges from the parent intention's projection. The catch is interesting in principle. In practice, I cannot point to a place in the current codebase where this catch would have fired. The propagation is theoretically sound and empirically speculative.

On styles generalising beyond requirements. This one is the most interesting and the most uncertain. I genuinely do not know whether a TypeExtensionStyle (with states like provisional/stable/deprecated) would be useful or just bureaucratic. The current ad-hoc convention — JSDoc @deprecated, brand stability hints — works fine for most codebases. Routing through a pluggable style mechanism would be more visible but also heavier. The bet is that, for codebases beyond a certain size or with regulatory requirements (the IndustrialStyle is the existing precedent for that), the visibility is worth it. For ordinary application codebases, the visibility is probably overhead.

Two readings, both kept open

There are two coherent readings of the dissolution test, and I want to record both without choosing yet.

Reading A (the hypothesis lives). The re-reading produces three concrete changes — derived @Satisfies, uniform @Refines, generalisable styles — and at least two of those changes correspond to real failure modes in the current codebase. The unification is not free (it costs a kernel rewrite and a migration path) but it produces new structure that the existing kernel does not have access to. Under this reading, the hypothesis is alive and the series should continue.

Reading B (the hypothesis is decorative). The re-reading produces three changes, but each of them is a re-statement of mechanisms the kernel already has. @Satisfies is already derivable in most cases by static analysis; uniform @Refines is a generalisation the kernel does not need because no other extension kind has shown up to use it; the style mechanism already generalises to anything by being pluggable. Under this reading, the hypothesis is naming nothing new — it is providing a commentary track on the existing kernel that does not actually change the kernel. The series would still be worth writing as a clarification of the kernel's logic, but the proposed packages/intentions and packages/extensions/* packages should never ship.

I do not know which reading is correct. I lean toward Reading A because of the cross-cutting case (the Part 6 territory) where the existing kernel really does not have an answer and the hypothesis does. But I want to be honest that Reading B is live and may win.

The counter-argument: requirement IS the API

There is a specific objection I want to engage that goes deeper than dissolution. It says: requirement-as-code is already a stable, well-tested API. Repositioning it as one extension kind among many adds a layer of indirection that buys nothing for the cases the existing API handles well, and the cases it does not handle well are not worth a kernel redesign.

This is the practitioner's objection. It is the one I would have made myself five months ago, before I started writing the cross-cutting cases in Part 6.

The honest response has three parts.

First, the objection is largely correct for the cases the existing API handles well. The Feature → AC → Test pipeline is well-tested, dog-fooded, and not asking for a redesign. The repositioning does not break it; it would re-express it. Re-expression is not free, and if the re-expression produced no new capabilities, it would not be worth the migration cost. I concede this part.

Second, the objection misses the cross-cutting and the type-system cases. These are the cases the existing kernel cannot reach without contortions. The current workaround — model the cross-cutting promise as a Requirement, link it to multiple Features via repeated @Satisfies, hope the linkage stays in sync — works mechanically but does not capture the single-source nature of the cross-cutting promise. A change to the cross-cutting promise has to be propagated by hand to every Feature; the kernel does not know the propagation is required. The hypothesis says: model the promise as an Intention, project it into the relevant extension kinds, and the propagation becomes structural rather than manual.

Third, the objection assumes the migration cost is the dominant cost. It might be. But the alternative cost — continuing to handle the cross-cutting and type-system cases with ad-hoc patterns — is not zero either, and is harder to measure because it shows up as gradual drift rather than visible breakage.

I cannot resolve this objection without the diagnostic in Part 10. The diagnostic is designed to measure exactly this: does the re-projection of an existing Feature through an Intention surface anything the existing kernel does not? If it does, Reading A is right. If it does not, Reading B is right and the objection wins.

What stays open

I do not know whether the three changes I sketched are exhaustive — there may be more changes in the kernel under the re-reading that I have not surfaced. I do not know whether the failure modes the changes catch are frequent enough to justify the migration. I do not know whether the style generalisation is useful or bureaucratic. I do not know whether Requirement is an extension kind (peer with Type, Class) or an extension instance (one specific projection of one specific intention) — both readings work and they imply different package layouts.

Part 5 is where I start sketching the other extension kinds concretely, starting with the type case. The reason for starting there is that types are the case where the framing has the lowest friction and the cleanest API surface, so if the kernel has the right shape, the type extension API should fall out of it without effort. If it doesn't, I will know something is wrong with the kernel.

⬇ Download