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

Why Requirement is Not the Root

This is the article where I admit I am not sure I built the right kernel.

I have shipped @frenchexdev/requirements-* — seventeen packages, hexagonal architecture, pluggable workflow styles, AST scanner, mutation-scoped behavioral checks, strict-mode CLI gate. It is dog-fooded by this site. It works. The compliance report on the build I am writing this article inside passes green. The architecture is not under attack.

What is under attack, in this series, is the naming of the root.

The arrows all point somewhere

If you open @frenchexdev/requirements-shared-kernel and read the decorators in order, you notice something:

@FeatureTest(UserAuthentication)
class UserAuthenticationTests {
  @Verifies<UserAuthentication>('rejectsInvalidCredentials')
  rejects_invalid_credentials() { /* ... */ }

  @Verifies<UserAuthentication>('expiresIdleSessions')
  expires_idle_sessions() { /* ... */ }
}

@Satisfies(RequirementSecurity_001, RequirementSecurity_002)
class UserAuthentication implements Feature {
  rejectsInvalidCredentials!: AC;
  expiresIdleSessions!: AC;
}

@Refines(RequirementSecurity_PARENT)
class RequirementSecurity_001 implements Requirement { /* ... */ }

Every single decorator in that snippet is a pointer. @FeatureTest points from a test class to a Feature class. @Verifies points from a test method to an acceptance criterion on that Feature. @Satisfies points from a Feature to one or more Requirements. @Refines points from a child Requirement to a parent Requirement.

The graph that emerges has a strict directionality: tests point to features, features point to requirements, requirements point to other requirements. You can walk the graph upward — from test to feature to requirement to parent requirement — and at every step the arrow lands on a named, decorated, scannable target.

Until you reach the top.

When you reach the top — the Requirement with no parent, the one with no @Refines decoration — the arrow you were following stops on a dangling node. The requirement does not point further up because, in the current kernel, there is no further up. The chain terminates.

Except — and this is the nag — the chain feels like it terminates a hop too early.

A top-level Requirement still came from somewhere. A stakeholder wrote it. A developer extracted it from a conversation. A regulator imposed it. A domain reading produced it. The Requirement is the artifact of an act of intending, and the act of intending is not the same thing as the artifact it produced. The kernel models the artifact and elides the act.

The pressentiment is that the act of intending has a shape, and that shape — once named — is the actual root.

What the decorators are not saying

Look at @Satisfies(RequirementSecurity_001, RequirementSecurity_002) again. It is a class decorator on a Feature, naming the Requirements that Feature satisfies. The semantic load of Satisfies is heavy: it claims that if the Feature's ACs all pass, then the Requirements are met.

But what does it claim about why those particular Requirements are linked to that particular Feature? Nothing. The link is asserted, not derived. A different developer could have linked the same Feature to a different set of Requirements; the kernel has no way to detect that the link is wrong, because the kernel does not know what generated either side of the link.

If both the Feature and the Requirement were derived from the same upstream object — an Intention<UserAuthentication> that generated both the Feature shape and the Requirement set — then the link between them would not need to be asserted. It would be a fact of their shared parentage. @Satisfies would degrade from a load-bearing decorator to a derived attribute, and the failure mode "developer linked Feature to wrong Requirements" would become "developer projected the same Intention twice and got two coherent projections" — which is a different and much smaller failure mode.

That is the kind of change in code I would write tomorrow that would justify the reframing. If the reframing does not produce that kind of change anywhere in the kernel, I will know it was decorative.

The first counter-argument: isn't this just DDD aggregate roots?

The honest objection — the one I want to engage at length, because it is the one that almost stopped this series from happening — is that the "intention" I am gesturing at is just a DDD aggregate root under a fancier Latin name.

There is real force to this. A DDD aggregate root is also an upstream source from which downstream artifacts (entity boundaries, invariants, repository contracts) are derived. A DDD aggregate root also generates its children rather than being linked to them. A DDD aggregate root is also the answer to the question "where does this entity come from" when the entity has no parent. If Intention<UserAuthentication> is just AggregateRoot<UserAuthentication> in different clothes, then I have re-invented DDD and the whole series should be deleted.

I do not think the objection is fully decisive, but I want to record carefully what I think it gets right and what I think it misses.

What it gets right: the function of the upstream source — being the generator from which downstream artifacts are derived — is essentially the same in both framings. A DDD aggregate root generates entities; an intention generates extensions. If you read those two sentences and substitute entity ↔ extension and aggregate root ↔ intention, they are isomorphic. The naming is the only difference. Naming is not nothing, but it is not architecture.

What it misses: DDD aggregate roots are a modelling construct for the domain. They are about how the business carves up the world into transactional consistency boundaries. They are bounded to the runtime types and the persistence story. An intention, in the sense I want to try out, is broader and shallower. It is not necessarily a domain object. It can be — but it can also be something like Intention<NoUnauthorizedAccess> which is not a domain concept but a cross-cutting promise about how all domains in this codebase behave. DDD does not have a home for that kind of object. AOP has a home for the projection of that object onto methods, but no home for the object itself.

So the partial answer to "isn't this just DDD" is: the function overlaps, but the scope differs. DDD covers a subset of what intention would have to cover. If the series only ends up covering the DDD subset, the objection wins and I will say so in Part 9 and reduce the proposal to "rename requirements-* and stop".

The fuller answer requires Parts 6, 7, and 8, where the cross-cutting cases and the type-system cases sit. I will return to this objection there.

What I notice when I try to draw the picture

When I try to draw the graph that the existing kernel produces, I get something like:

Test ──@Verifies──> AC ──(member of)──> Feature ──@Satisfies──> Requirement ──@Refines──> Requirement

The arrows all flow leftward (the test verifies an AC of a feature that satisfies a requirement that refines a parent requirement). Every node in that chain is a code artifact. Every arrow in that chain is a decorator-stamped fact.

When I try to draw the graph that the hypothesised kernel would produce, I get something like:

Intention ──projects──┬──> TypeExtension      (the types that carry the intention)
                      ├──> ClassExtension     (the classes that carry the intention)
                      ├──> MethodExtension    (the methods that carry the intention)
                      ├──> ModuleExtension    (the modules that carry the intention)
                      ├──> ApplicationExt.    (the apps that carry the intention)
                      ├──> TypeSystemExt.     (the type discipline that carries it)
                      └──> RequirementExt.    (the verifiable contract for it)

The arrow direction has reversed. Instead of artifacts pointing upward at requirements, the upstream intention points outward at its projections. This is not just a graphical flourish; it changes who is responsible for asserting what. In the existing kernel, the artifact asserts its link to a requirement (@Satisfies(...)). In the hypothesised kernel, the intention enumerates its projections, and the artifacts may not need to assert anything at all about why they exist — their existence under a particular intention is what gives them their reason to be.

This reversal is the part that makes me want to keep writing. It is the part that, if it holds, would actually change code I write tomorrow. It is also the part that, if it does not hold, would dissolve into nothing.

What stays open

I do not know, at the end of this article:

  • Whether Intention<T> is a class, a value, a brand, a registry entry, or all four.
  • Whether the projection from intention to extension is one-to-many (tree-shaped) or many-to-many (graph-shaped).
  • Whether the requirement extension kind has any special status (it was the first one I built and it ships compliance gates that no other extension kind would ship) or whether it is fully a peer.
  • Whether reversing the arrow direction is a real architectural shift or just a re-drawing of the same relations.

Part 02 is the etymology and the categorical move. It is also where I commit to "generative metaphor" rather than "truth claim" — a commitment that is itself contestable and which I want to defend explicitly before moving on.

⬇ Download