Part 03 — Bounded contexts in VSCode: anti-corruption, ports, composition rules
Article 02 named what is in the kernel and how it stays small. This article does the symmetric work for the other side of the architecture: what a micro-DSL is, how it stays separate, and how the suite acquires its shape through hosts that compose multiple micro-DSLs without coupling them to each other. The vocabulary comes again from Evans, applied with one specifically-VSCode tightening.
The argument has a simple shape and three load-bearing claims. First: each micro-DSL is a DDD bounded context — internal model free, vocabulary local, communication with the kernel deliberately translated. Second: an anti-corruption layer at every kernel boundary keeps a micro-DSL's internal model from leaking into the kernel and from being coupled to changes in the kernel's representation. Third: the dependency direction is one-way and acyclic — kernel ← micro-DSL ← host — and the host is the only place where the suite acquires a multi-micro-DSL shape. The rest of the series, all nineteen articles, depends on these three claims being held to the letter.
Bounded contexts, recalled
The relevant Evans passage names the pattern and the boundary it draws:
Explicitly define the context within which a model applies. Explicitly set boundaries in terms of team organization, usage within specific parts of the application, and physical manifestations such as code bases and database schemas. Keep the model strictly consistent within these bounds, but don't be distracted or confused by issues outside.
— Eric Evans, Domain-Driven Design (2003), p. 336
The properties Evans names — team organisation, application surface, physical manifestation — translate directly to a TypeScript suite. Team organisation: each micro-DSL has a single owning team (a person, in many of our cases, but the discipline is the same). Application surface: each micro-DSL addresses one VSCode capability — Syntax addresses highlighting, Hover addresses informational mouse-over, Refactoring addresses semantically-safe transformations. Physical manifestation: each micro-DSL ships as one npm package, with its own package.json, its own index.ts, its own version, its own publish step.
Inside the context, vocabulary is consistent and local. The Syntax micro-DSL talks about tokens, scopes, grammars, injections; the Hover micro-DSL talks about patterns, content providers, resolution; the Refactoring micro-DSL talks about transforms, applicability, preview. None of those vocabularies need to agree with each other. The Hover micro-DSL does not need to know what a token means; the Syntax micro-DSL does not need to know what applicability means. The boundary is what makes them free to optimise each vocabulary for its own use case.
What every bounded context does need to agree on is the kernel: the M2 vocabulary (Concept, Property, ChildLink, ReferenceLink), the AST shape, the PatchBus contract, the NodeId identity, the Banner format, the EditLog stream. These are the shared language across boundaries. The kernel is small (article 02) precisely so that what every micro-DSL must agree on is bounded; the rest, every micro-DSL is free to invent.
What goes inside a micro-DSL
A bounded context is a free hand inside a strict boundary. Concretely, every micro-DSL package contains:
- A decorator family. Public API: the decorators users put on their
spec.ts. The Syntax micro-DSL exports@Tokenand@SemanticToken; the Snippets micro-DSL exports@Snippet; the Diagnostics micro-DSL exports@Diagnosticplus the auto-derivation hook. Decorators are pure metadata, calqued on@Generatefromts-codegen-pipeline. They are stable (in the kernel-stability sense — adding optional decorator fields is a minor bump; removing or renaming is major). - An extractor. Walks
spec.tsfiles viafindDecorated(from the kernel), collects the micro-DSL's decorators into typed in-memory representations. The internal representation is the micro-DSL's own model — the Hover micro-DSL builds aHoverProviderRegistry, the Snippets micro-DSL builds aSnippetCatalog, the Refactoring micro-DSL builds aRefactoringSet. None of these types appears in the kernel's exports; none of them needs to. - An emitter. Produces the artefacts the host (LSP, Custom Editor, Extension) consumes. For Syntax: a TextMate JSON file plus
contributes.grammars. For Snippets: a.code-snippets.jsonfile pluscontributes.snippets. For Hover: an LSP handler registration. The micro-DSL is fully responsible for the shape of what it emits; the host only reads what the micro-DSL declares it produces. - A contribution declaration. A typed
MicroDslContribution<T>value that the micro-DSL exports and the host imports. The contribution lists what the micro-DSL extracts, what it emits, where it plugs in. This is the only part of the micro-DSL the host depends on; everything else is private. - Tests, scoped per package. Vitest, in-memory, kernel-driven, no editor runtime. The kernel provides the AST and the Structure Model; the test sets up a fixture spec.ts, runs the micro-DSL's extractor and emitter, snapshots the output. Test layers are documented in
closing-the-loopand adopted directly here.
That is the full list. A micro-DSL package has no vscode import in its source. It has no dependency on any other micro-DSL package. It has one dependency on the kernel and zero downstream awareness of which hosts will consume its contribution.
The anti-corruption layer at the kernel boundary
Evans introduces the anti-corruption layer as the pattern for insulating one bounded context from another:
Create an isolating layer to provide clients with functionality in terms of their own domain model. The layer talks to the other system through its existing interface, requiring little or no modification to the other system. Internally, the layer translates in both directions as necessary between the two models.
— Evans, p. 365
In our suite, the anti-corruption layer is two-way translation between the kernel's vocabulary (Concept, Property, NodeId, AST) and the micro-DSL's internal vocabulary (whatever the micro-DSL invents — HoverPattern, SnippetTemplate, TokenScope). The kernel never mentions the micro-DSL's types; the micro-DSL never exposes the kernel's types in its public API beyond what is structurally required.
Concretely, the anti-corruption layer for the Hover micro-DSL takes the kernel-typed AST node at the cursor and produces a HoverPattern instance — the micro-DSL's own type — by inspection. The pattern matcher then runs in the micro-DSL's local vocabulary, untouched by kernel changes that do not affect node identity. When the kernel renames NodeId to NodeIdentity (a major-version event), the Hover micro-DSL changes one line in its anti-corruption layer; the rest of the micro-DSL's code is unaffected. Without the layer, every kernel rename propagates into every method of every micro-DSL; with the layer, the propagation stops at the boundary.
The cost of the layer is real. It is one extra translation per request, plus one extra type per micro-DSL, plus the discipline to not short-circuit the layer when a quick refactor would expose a kernel type directly. The benefit is that micro-DSLs can evolve internally — a complete rewrite of the Hover pattern matcher is a patch release of the Hover micro-DSL, not a kernel change — and the kernel can evolve its representation without breaking every consumer. The benefit dominates over a multi-year horizon; the cost dominates over the first month. The series adopts the discipline anyway, because the failure mode of not doing it (kernel renames triggering coordinated re-releases of fourteen micro-DSLs every quarter) is exactly what the architecture is built to avoid.
The dependency direction theorem
Three architectural levels, one direction of dependency:
kernel
^
| imports
|
+----------+----------+----------+----------+
| | | | |
micro-DSL micro-DSL micro-DSL micro-DSL micro-DSL
(Syntax) (Hover) (Snippets) (Diagnostics)(Generator)
^ ^ ^ ^ ^
| | | | |
+----------+----------+----------+----------+
|
| imports
|
host
(LSP / Custom Editor / Extension) kernel
^
| imports
|
+----------+----------+----------+----------+
| | | | |
micro-DSL micro-DSL micro-DSL micro-DSL micro-DSL
(Syntax) (Hover) (Snippets) (Diagnostics)(Generator)
^ ^ ^ ^ ^
| | | | |
+----------+----------+----------+----------+
|
| imports
|
host
(LSP / Custom Editor / Extension)The arrows point up the dependency graph: the kernel knows nothing about micro-DSLs; each micro-DSL knows about the kernel only; each host knows about the kernel and several micro-DSLs. No micro-DSL imports another micro-DSL. No host is imported by anyone in the suite (hosts are the consumption surface).
The theorem is a constraint, not an observation. It is held by package.json boundaries, by lint rules, by the integration test that fails if a kernel package introduces an import from a micro-DSL package. Holding it pays three returns:
- Acyclic by construction. The graph is a DAG with three layers. There is no cycle to break, no temporal coupling between micro-DSLs, no situation where adding a feature to micro-DSL A requires touching micro-DSL B.
- Independent reasoning. Reading the Syntax micro-DSL is reading the Syntax micro-DSL plus the kernel. The reader does not need to know how Hover works to understand Syntax. Onboarding cost stays linear in the number of consumed packages.
- Composition is a consumer concern. The user of the suite chooses which micro-DSLs to compose; the suite does not impose a fixed combination. The minimal Requirements IDE composes six micro-DSLs (article 22); a richer IDE composes more; a stripped IDE composes fewer.
The discipline also forecloses one tempting shortcut: cross-cutting concerns implemented as utilities shared between micro-DSLs. The temptation appears when, for example, two micro-DSLs both want to walk the workspace looking for files of a given language. The naive solution: a findLanguageFiles helper in a shared package. The disciplined solution: the kernel either exposes the API (if it meets the criteria from article 02) or each micro-DSL writes its own (if it does not). The naive solution leaks into a god-object kernel within months; the disciplined solution forces the question should the kernel own this? every time, which is the question we want to keep asking.
Why no micro-DSL imports another micro-DSL
The "no horizontal imports" rule is the most-questioned one. The natural objection: but the Refactoring micro-DSL needs to walk the symbol index that the Symbols micro-DSL builds; surely Refactoring should import Symbols?
The answer is that the symbol index is kernel-shaped, not Symbols-shaped. The Symbols micro-DSL builds the index, but the type of the index — what a SymbolEntry is, how to look one up by name, how to subscribe to updates — lives in the kernel, in the Structure Model. The Symbols micro-DSL's job is to populate the index from @ChildLink declarations. The Refactoring micro-DSL's job is to consume the index for impact analysis. Both micro-DSLs talk to the kernel; neither imports the other.
This split — who populates, who consumes, what they share is in the kernel — is the model for every cross-micro-DSL collaboration. Two micro-DSLs that need to share state share it through the kernel's typed state surface, not through each other's internals. If a piece of state is candidate for sharing but does not satisfy the article 02 inclusion criteria for the kernel, it stays in one micro-DSL and the other duplicates the work — duplication is preferable to coupling, as the closing-the-loop series argues at length for the test side and which applies just as well at the architecture side.
The cost: some redundancy. The Hover micro-DSL and the Symbols micro-DSL both walk @ChildLink declarations — they walk for different purposes (Hover for content, Symbols for the index) but the walk itself is similar. Either both walks live in their respective micro-DSL (duplication, no coupling), or the walk lives in the kernel (no duplication, kernel grows). Article 02's criteria decide: the walk is used by ≥3 micro-DSLs (Hover, Symbols, Refactoring at minimum), it is stable, it has no VSCode dependency — so it goes in the kernel as walkChildLinks. The decision was already made by the criteria; the rule "no horizontal imports" did not need to choose.
The host as the composition surface
Hosts are where the suite acquires a shape. There are three of them:
- The LSP host (article 20). Composes every micro-DSL whose contribution includes LSP handlers — Completion, Hover, Diagnostics, CodeLens, Symbols, Formatter, Refactoring (its rename action). At startup, the host walks the kernel's contribution registry, discovers which micro-DSLs have registered LSP handlers, and routes each LSP request to the relevant contributions in turn. The host is the router; it owns no behaviour.
- The Custom Editor host (article 21). Composes the Projection micro-DSL with the Language micro-DSL's identity to register itself as the editor for the relevant file extensions. Bootstraps a Vite-bundled React WebView, exposes the PatchBus across the WebView boundary, mounts whichever projections the user has registered. The host is the mounter; it owns the WebView lifecycle but not the projections.
- The Extension host (the package emitted to consumers). Composes Language, Syntax, Snippets, Commands, Views — every micro-DSL that contributes to the VSCode extension manifest's
contributes.*blocks. Generatespackage.json,extension.ts, the activation events, the dependency declarations. The host is the packager; it produces the.vsix-ready folder.
A host imports several micro-DSL packages but never depends on a specific micro-DSL in its public type signatures. It depends on the contribution interface declared in the kernel (MicroDslContribution<T> and friends) and discovers concrete contributions at startup. Adding a fourteenth micro-DSL to the suite is adding it to the consumer's package.json and re-running the discovery; no host code changes.
This is the Dependency Inversion Principle held strictly: the host depends on an abstraction (the contribution interface), not on a concrete micro-DSL. It is also why the host package itself is not a particularly interesting engineering artefact — most of the cleverness has been pushed into the kernel (which owns the contract) and the micro-DSLs (which implement against it). The host is small, mechanical, and easily replaced. Replacing the LSP host with one that targets a different LSP version is a host change; the micro-DSLs do not move.
A worked example: how Hover composes
Concrete example to make the architecture tangible. The user hovers over Requirements.FEATURE-156 in a feature-checkout.req.ts file:
- VSCode sends
textDocument/hoverto the LSP server emitted by the LSP host. - The LSP host looks up registered Hover providers in the kernel contribution registry. The Hover micro-DSL has registered one.
- The LSP host calls the Hover micro-DSL's adapter (the anti-corruption layer): "the cursor is at line 23, column 19; here is the kernel-typed AST node".
- The Hover micro-DSL's pattern matcher inspects the node, recognises
Requirements.FEATURE-156as aFeatureRefConcept, queries the kernel index for the canonical declaration ofFEATURE-156anywhere in the workspace. - The Hover micro-DSL produces markdown content combining the feature title, priority, and acceptance criteria. It returns this as a
HoverPattern.Result— the micro-DSL's internal type. - The anti-corruption layer translates the
HoverPattern.Resultto an LSPHovervalue. - The LSP host returns the
Hoverto VSCode. - VSCode renders.
The Symbols micro-DSL was not invoked. The Syntax micro-DSL was not invoked. The Refactoring micro-DSL was not invoked. The kernel index was consulted, populated by Symbols at indexing time but accessed through a kernel API, not a Symbols API. Each layer's responsibility is bounded; each boundary is enforced by the type system and the package boundary.
If a sibling DSL — the workflow DSL — wants the same hover pattern for workflow stage references, the workflow DSL ships a spec.ts declaring its own Concepts, registers its own Hover provider with the same Hover micro-DSL, and inherits the same routing for free. The Hover micro-DSL did not need to know the requirements DSL existed; it does not need to know the workflow DSL exists. It implements hover-over-typed-references; the Concepts come from each DSL's own spec.
This is the payoff. The same Hover micro-DSL serves any DSL that declares the right Concepts, by composition rather than inheritance.
What this article verifies
This article verifies the four acceptance criteria of FEAT-MICRODSL-03 declared in assets/features.ts:
- boundedContextDefinitionApplied — the section Bounded contexts, recalled quotes Evans and applies the three properties (team, surface, manifestation) to micro-DSLs concretely.
- antiCorruptionLayerExplained — the section The anti-corruption layer at the kernel boundary quotes Evans, names the two-way translation, costs and benefits.
- dependencyDirectionRulesStated — the section The dependency direction theorem draws the diagram, names the rule, names how it is enforced (
package.json, lint, integration test). - compositionViaHostsIntroduced — the section The host as the composition surface names the three hosts and explains their composition role; the section A worked example: how Hover composes runs one composition end to end.
What article 04 picks up
Articles 01–03 set the architecture: thesis, kernel, bounded contexts. Article 04 makes the kernel concrete by specifying the four M2 decorators — @Concept, @Property, @ChildLink, @ReferenceLink — that every micro-DSL consumes. The article walks each decorator's options, names what @Property({ constraint }) seeds in the Diagnostics micro-DSL (article 11), and contrasts @ChildLink with @ReferenceLink to show why the distinction is load-bearing for both Symbols (article 16) and Refactoring (article 17). Read 04 next.