The Coarse-Grained Projections
The previous article handled the fine-grained cases (class, method, function) and conceded substantial overlap with AOP. This article handles the coarse-grained cases (module, application) where AOP is no longer in the picture but a different objection takes its place: at this granularity, "extension" stops carrying any new meaning. An entire module is its implementation. An entire application is its implementation. Saying that the module or application is an extension of an upstream intention sounds like a rationalisation of how the code came to exist, not an architectural distinction.
This article will defend the framing for the module case partially and for the application case mostly by appealing to a particular feature: multiple competing projections of the same intention. If that feature is real, the granularity argument fails. If it is not, the granularity argument wins and @ModuleExtension and @ApplicationExtension should not exist.
The module case
Consider a module's public surface:
// auth/index.ts
export { AuthSession } from './auth-session';
export { authenticate } from './authenticate';
export { revokeSession } from './revoke-session';
export type { UserCredentials, AuthError } from './types';// auth/index.ts
export { AuthSession } from './auth-session';
export { authenticate } from './authenticate';
export { revokeSession } from './revoke-session';
export type { UserCredentials, AuthError } from './types';What the module exports is what the rest of the system can see. What the module does not export is the rest of the module's body — internal types, internal helpers, the database adapter, the password hashing primitive, the rate limiter. The module's intention, in the framing of this series, is roughly: "the rest of the system shall perform authentication operations through opaque session handles, without ever touching credential structure or session internals."
The decoration would look like:
// auth/index.ts
/** @moduleExtension OpaqueAuthSurface */
export { AuthSession } from './auth-session';
export { authenticate } from './authenticate';
export { revokeSession } from './revoke-session';
export type { UserCredentials, AuthError } from './types';// auth/index.ts
/** @moduleExtension OpaqueAuthSurface */
export { AuthSession } from './auth-session';
export { authenticate } from './authenticate';
export { revokeSession } from './revoke-session';
export type { UserCredentials, AuthError } from './types';The analyzer would check that the module's exports conform to the intention. Specifically: that no internal type leaks (e.g., the password hashing primitive is not transitively exported), that the session type is opaque (no fields exposed beyond what the intention declares), that no debug-only or test-only helpers escape.
The capability sounds useful but it is essentially @internal JSDoc enforcement with a different decoration. ESLint already has rules for this. The TypeScript compiler has tsdoc-internal-tag proposals. The capability exists.
What does the intention framing add? Three things, and I want to be honest about how much each one is worth.
First, the named upstream object. With @internal, the reason a thing is internal lives in JSDoc comments scattered across the module. With @moduleExtension OpaqueAuthSurface, the reason has a name and a single point of definition. The named object can be queried, refined, refactored. The JSDoc approach gives the constraint without the name; the intention framing gives both.
Second, cross-module coherence. If two modules both project the OpaqueAuthSurface intention (because they are alternative implementations — say auth-jwt/ and auth-sessions/), they should expose congruent public surfaces. The intention framing makes this checkable; the JSDoc approach does not.
Third, lifecycle. As mentioned in Part 4, the existing RequirementStyle workflow mechanism could generalise. A module extension could carry a ModuleExtensionStyle with states like experimental → stable → deprecated, and the workflow could be enforced at the package level. JSDoc-internal does not do this; package-level deprecation today happens by convention.
The "cross-module coherence" capability is the one that I find most interesting and least over-claimed. It is genuinely something the existing tooling does not do, and it appears specifically when you have multiple modules implementing the same upstream concept. Which brings me to the application case.
The application case
The application case is the coarsest. An entire application — every module, every package, every type, every method — is the projection of one (or a few) high-level intentions:
// at the root of the application's package.json or equivalent
{
"name": "@frenchexdev/cv-site",
"applicationExtensions": [
"StaticSiteWithGitPushDeploy",
"AuthorOwnedContentPipeline",
"RequirementsDogfooded"
]
}// at the root of the application's package.json or equivalent
{
"name": "@frenchexdev/cv-site",
"applicationExtensions": [
"StaticSiteWithGitPushDeploy",
"AuthorOwnedContentPipeline",
"RequirementsDogfooded"
]
}The application declares which intentions it materialises. The analyzer's job at this granularity is necessarily looser — it cannot check the whole application body against the intentions in any rigorous sense. What it can do is check coherence between high-level architectural decisions and the declared intentions: that StaticSiteWithGitPushDeploy is consistent with the absence of a server runtime in the codebase, that AuthorOwnedContentPipeline is consistent with the presence of content under content/ and no shared editing system, that RequirementsDogfooded is consistent with the presence of @frenchexdev/requirements-* packages dog-fooded by the site's own build pipeline.
These checks are not deep static analyses; they are presence-and-absence sanity checks. Their value, if they have value, is in catching architectural drift over time — when the codebase starts to acquire shapes that contradict its stated application intentions (a server runtime appears, breaking StaticSiteWithGitPushDeploy; a CMS appears, breaking AuthorOwnedContentPipeline; the requirements dog-fooding gets disabled, breaking RequirementsDogfooded).
The objection: this is just implementation
The objection is sharp at this granularity. It runs: an application is its implementation. Saying that the application is an "extension" of an upstream intention is rationalising the decisions that produced the codebase. The intention is a post-hoc summary, not a separate object. Adding a registry of "application intentions" is paperwork, not architecture.
There is a lot of force to this. I want to engage it in three steps.
Step one: take the objection's strongest reading. The strongest reading is that at the application granularity, no projection-relation actually exists. The application was built by humans making decisions in sequence; the "intentions" written down today are reconstructions of those decisions. The reconstructions are stories the team tells itself about why the codebase looks the way it does. Stories can be useful for onboarding, for documentation, for retrospective coherence — but they are not architectural primitives.
I think the strongest reading is correct for many applications. Most applications were not designed top-down from named intentions; they were built feature by feature with intentions emerging informally. Calling those emerging intentions "ApplicationExtensions" and putting them in a registry is post-hoc labelling. It is not without value (post-hoc labelling can be a forcing function for future coherence), but it is not what the intention framing would have to deliver to justify itself.
Step two: identify when the objection fails. The objection fails when an application is consciously the projection of an explicit upstream object and the upstream object has multiple competing projections across different codebases or deployments. The example I keep coming back to is RequirementStyle. The existing @frenchexdev/requirements-styles package contains five competing projections of the same kernel: DefaultStyle, IndustrialStyle, AgileStyle, LeanStyle, KanbanStyle. Each is a complete application of the same requirements kernel, with a different workflow, different terminology, different constraints. They are not five implementations of five different things; they are five projections of one intention (roughly: "capture the discipline this organisation uses around requirement lifecycle") into five different application shapes.
This is the precedent for what the application-level intention framing would deliver. The intention RequirementLifecycleDiscipline has five extensions — DefaultStyle, IndustrialStyle, AgileStyle, LeanStyle, KanbanStyle — and they share enough structure that they can be made interchangeable at the consumer's option. The framing does not invent the relationship; the relationship already exists in requirements-styles. The framing names it and proposes that other parts of the codebase do the same thing.
The application case becomes interesting, in other words, when there are parameterised application families: cases where the same upstream intention legitimately projects into multiple competing concrete applications. Without that, the application case collapses into "label your app and store the label somewhere."
Step three: limit the claim. Given the partial concession in step one and the conditional defence in step two, the honest claim is: @ApplicationExtension is useful only for applications that participate in a family of parameterised applications projecting from a shared intention. For one-off applications, the framing produces post-hoc labels and the objection wins. The proposed packages/extensions/extensions-application package should therefore be a narrow utility, not a general-purpose tool.
This is a significant scoping concession. It means the application case is the smallest of the extension kinds in terms of audience, not the largest. The other kinds (type, class, method) are broadly applicable; the application kind is narrowly applicable to a specific architectural pattern. The framing should not pretend otherwise.
The cross-cutting between module and application
There is a small note I want to leave between these two cases. A module extension is usually intra-application; a single application has many modules. An application extension is usually inter-application; the same intention has multiple application projections.
The vocabulary is consistent at the surface level — both are Extension decorations — but the granularity of the projection-relation differs. Modules project from intentions that are local to the application; applications project from intentions that are shared across multiple applications. If the kernel does not distinguish between these two scopes, it will produce confusing behaviour where module-level intentions are queried at the inter-application level (yielding nothing) or application-level intentions are queried at the intra-application level (yielding the whole application, which is useless).
The kernel should have a scope attribute on the intention itself: Intention<T, Scope.Module>, Intention<T, Scope.Application>. This is one of the details I do not want to commit to before the diagnostic in Part 10 runs — the diagnostic might suggest the scope distinction is unnecessary or that more scopes are needed.
The requirements-styles precedent in detail
I want to spend a paragraph on the requirements-styles precedent because it is the strongest existing argument for the framing, and I should not bury it.
The package contains five workflow styles, each defining states, initial, transitions[], and terminal. The styles are interchangeable: a consumer of the requirements kernel can swap DefaultStyle for IndustrialStyle and the kernel still works. The swap is type-safe, runtime-safe, and behaviour-preserving in the sense that all the kernel's operations (status transitions, compliance checks, lifecycle queries) continue to function — they just operate on a different state space.
This is exactly the multiple competing projections case the application framing would generalise. The five styles are five ApplicationExtension(RequirementLifecycleDiscipline) instances. Each is a valid concrete application of the same intention; the consumer picks the one that matches their organisational discipline. The kernel is the shared upstream; the styles are the extensions.
If the application framing were already in place, the requirements-styles package would have been designed as a family of @ApplicationExtension registrations, with the kernel providing the registry and the swap-and-validate mechanism. As it stands, the package implements all of this in custom code (the RequirementStyle interface, the style-registration plumbing, the style-aware status transitions). The custom code works fine. The framing would have made it routine.
The retrospective recognition matters: the framing did not anticipate requirements-styles; requirements-styles anticipated the framing. If I were writing the kernel today knowing what requirements-styles revealed, I would build the application-extension mechanism first and the styles second. That is the kind of evidence that makes me suspect the framing is real.
What stays open
I do not know whether the scope distinction (module vs application) is one bit or several bits. I do not know whether the cross-module coherence check is asked for often enough to justify the analyzer work. I do not know whether the application case generalises beyond the requirements-styles precedent or whether that precedent is the only application case in the codebase that would benefit from the framing — and if it is the only one, the framing is over-engineered for a single use site.
Part 8 is the most abstract article and the one I am least confident in. It takes the framing into the type-system territory, where the intention projects not into individual types but into a discipline governing a family of types. The objection there is the Haskell-envy objection, which I cannot fully refute.