Part 02 — The shared kernel: DDD applied to an IDE suite
The thesis from article 01 named a kernel without saying what is in it. This article does the work. It recalls Eric Evans' shared kernel pattern from Domain-Driven Design (Addison-Wesley, 2003, chapter 14, Maintaining Model Integrity); it derives three inclusion criteria for what belongs in the kernel of an IDE suite; it reviews every plausible candidate against those criteria; it names the result. The result happens to be the same five concepts the thesis paragraph announced — M2 decorators, AST, PatchBus, Banner, EditLog — but the point of this article is not the list. The point is the discipline by which the list stays small.
The discipline matters because the kernel is the load-bearing decision of the whole architecture. Add too much to the kernel and every micro-DSL release becomes a kernel release: the god-object failure mode that ate the meta-ide-dsl prototype on a different axis. Add too little and the micro-DSLs duplicate concerns until they re-implement the kernel, badly, N times: the anaemic kernel failure mode that ate Drupal modules in the 2010s and Wagtail blocks in 2018. Both failure modes are familiar, both are documented, both are avoidable if the inclusion criteria are stated up front and held against every candidate.
Evans recalled
The relevant passage is short. Evans is describing strategies for letting two teams work on related-but-distinct domains without their models drifting apart:
Designate some subset of the domain model that the two teams agree to share. Of course this includes, along with this subset of the model, the subset of code or of the database design associated with that part of the model. This explicitly shared stuff has special status, and shouldn't be changed without consultation with the other team.
— Eric Evans, Domain-Driven Design (2003), p. 354
Two properties are doing the work. The shared subset is explicit — it has a name, a location, a contract; nobody is shared-by-accident. And changes to the shared subset are costly — they require consultation; nobody changes the kernel unilaterally. The pattern is not put common code in a util folder; it is commit, in writing, to a small set of co-owned concepts whose evolution is governed.
The contrast Evans draws elsewhere in the chapter is with two alternatives. Customer/Supplier teams describe the case where one team's model legitimately drives another's; the supplier serves the customer's needs but does not share code; the relationship is asymmetric. Anti-corruption layer describes the case where two existing models cannot be reconciled and need to be translated at the boundary; the relationship is one of insulation rather than sharing. Shared kernel is the middle position: shared, governed, small. It is the right pattern when several teams need a common vocabulary and are willing to pay the coordination cost on every kernel change.
For an IDE suite, all three patterns will ultimately apply — and the article 03 bounded contexts exposition relies on the anti-corruption layer for some boundaries — but the kernel question itself, what do all the micro-DSLs co-own?, is the shared-kernel question. The answer is small.
Three inclusion criteria
What earns a place in the shared kernel? Three criteria, derived from Evans' two properties (explicit + costly) plus one constraint specific to a TypeScript suite of VSCode extensions.
Criterion 1 — known and used by at least three micro-DSLs. A concept used by one micro-DSL belongs in that micro-DSL. A concept used by two might still belong in one of them (the other being a customer). A concept used by three or more is, by definition, the kind of common vocabulary the shared-kernel pattern is for. The "three" is not magical — Evans does not give a number — but two is too low a bar (it lets random pairwise commonalities promote themselves) and four is too high (it delays kernelisation past the point where the duplication has become entrenched). Three is the smallest number that forces shared ownership.
Criterion 2 — stable. The kernel is what every micro-DSL imports, every host composes, every consumer transitively depends on. A change to the kernel triggers a coordinated release of every micro-DSL that uses the changed concept, plus every host, plus every consumer. The cost per change is roughly N times the cost of a same-sized change to a single micro-DSL. So the kernel must contain only concepts whose semantics are expected to be stable: things you would not refactor without a major architectural reason. Concepts that are still "in motion" — being explored, redesigned, validated against new use cases — must stay in a micro-DSL until they have settled, even if criterion 1 is met.
Criterion 3 — no VSCode dependency. This criterion is specific to our context and is what keeps the kernel testable, replaceable, and portable. Every concept in the kernel must be expressible without importing vscode, vscode-languageserver, @vscode/..., or any other editor-specific library. The reason is twofold. Practically, kernel unit tests must run without a VSCode runtime — vitest, plain Node, sub-second execution. Strategically, a future move to a different editor (Zed, Neovim, JetBrains Fleet, a web IDE) must be a matter of swapping hosts, not rewriting the kernel. If the kernel imports vscode, every editor port is a fork. If the kernel does not, every editor port is just three new host packages.
The criteria compose strictly: a concept must satisfy all three to belong in the kernel. Concepts that satisfy the first two but fail the third belong in a host. Concepts that satisfy the first and third but fail the second belong in a micro-DSL. Concepts that satisfy only one criterion belong wherever they happen to be used.
The candidate list, reviewed
What concepts could plausibly live in the kernel? A long list is possible. The honest exercise is to list them all and rule them out one by one.
Concept, Property, ChildLink, ReferenceLink (the M2 decorators). Every micro-DSL describes its surface in terms of Concepts: Syntax binds tokens to Concept properties, Completion suggests Concept instances, Hover renders Concept content, Refactoring transforms Concept structure. Criterion 1: easily satisfied — fourteen micro-DSLs, all consume them. Criterion 2: the M2 vocabulary is, in any metamodel-driven system, the most stable concept; CMF design and meta-ide-dsl have used the same four shapes for a year. Criterion 3: pure metadata, zero runtime dependency, no VSCode reference. In the kernel.
AST and Node identity (NodeId). The AST is the in-memory tree of Concept instances; NodeId is the identifier that survives reloads, file moves, splits, merges. Every micro-DSL that mutates state (Refactoring, Diagnostics' code actions, every Projection co-edit) needs the AST. Every micro-DSL that resolves references (Hover cross-file, Symbols workspace index, CodeLens dynamic counts) needs NodeId. Criterion 1: unanimous. Criterion 2: identity contracts are the kind of thing you fix once and then commit to forever; changing what NodeId promises mid-flight is a project-ending refactor. Criterion 3: in-memory data structure, no editor dependency. In the kernel.
PatchBus. The single mutation API. Articles 17 (Refactoring) and 18 (Projection) and 21 (Custom Editor host) and 22 (Composition) all rely on it as the only path that mutates the AST. Three or more micro-DSLs co-own it by the strict reading of criterion 1. Criterion 2: a mutation API is the most contract-sensitive part of any system; once published, it cannot evolve except by careful versioning. Criterion 3: pure in-memory, zero VSCode. In the kernel.
Banner (idempotence marker). Lifted directly from packages/ts-codegen-pipeline/src/emit/banner.ts. The Generator micro-DSL emits files; the Refactoring micro-DSL re-emits files; the Projection micro-DSL re-emits the text projection on every save. All three need an idempotence marker so a regeneration that produces the same output does not show as a diff. Criterion 1: three consumers minimum. Criterion 2: the SHA256 + metadata convention is already production-stable in ts-codegen-pipeline; importing it into the kernel inherits its stability. Criterion 3: pure data, no editor dependency. In the kernel.
EditLog. Local, append-only stream of PatchBus operations. Used by Views (article 14) for tree refresh, by Symbols (article 16) for index invalidation, by the Custom Editor host (article 21) for projection synchronisation, plus optional analytics. Four consumers, criterion 1 satisfied. Criterion 2: the format — JSONL of typed PatchBus ops — is small, well-defined, and unlikely to change. Criterion 3: file-based, no editor dependency. In the kernel.
That is five concepts. The exercise continues with the candidates that fail the criteria, because the rejections are how the boundary stays sharp.
LSP types (Position, Range, TextDocument, Diagnostic, ...). Used by every LSP-flavoured micro-DSL — Completion, Hover, Diagnostics, CodeLens, Symbols, Formatter, Refactoring. Criterion 1: easily satisfied (seven consumers). Criterion 2: stable (the LSP spec evolves slowly). Criterion 3: fails — these come from vscode-languageserver-types or vscode-languageserver-protocol, both of which carry a VSCode-flavoured runtime. In the LSP host, not the kernel. Each micro-DSL exposes its LSP-flavoured outputs through an adaptation declared in the LSP host's contribution interface.
The Structure Model (the in-memory typed graph of all declared Concepts). Built by scanning @Concept declarations across the workspace. Used by every micro-DSL. Criterion 1: unanimous. Criterion 3: zero editor dependency. Criterion 2: fails the strict reading — the contents of the Structure Model change every time a Concept is added; what is stable is the shape of the Structure Model, not the model itself. Resolution: the type of the Structure Model belongs in the kernel; the live instance is built by the kernel runtime and exposed; the contents are per-workspace and not part of the kernel API. In the kernel as a type, not as data.
The Custom Editor WebView bootstrap (Vite + React + the projection registry). Used by the Custom Editor host (article 21) and indirectly by the Projection micro-DSL (article 18). Criterion 1: only two direct consumers — fails. Criterion 3: fails — Vite and React are heavyweight runtime dependencies, and the WebView API is editor-specific. In the Custom Editor host, not the kernel.
Decorator scanning (findDecorated and friends). Used by every micro-DSL that wants to walk a spec.ts file. Criterion 1: unanimous. Criterion 2: stable (the findDecorated API in ts-codegen-pipeline is already production). Criterion 3: depends on ts-morph. The judgment call: does ts-morph count as a VSCode-flavoured dependency? It does not — it runs in plain Node, has no editor coupling, and is already a peer dependency of ts-codegen-pipeline which itself is in the kernel's import graph. In the kernel, as a transitively-allowed dependency, with the explicit note that a future Node-less environment (browser-only) would need to substitute it.
Telemetry beyond EditLog. Anything that wants to upload edit logs, count micro-DSL invocations, measure projection latency. Used by, optimistically, two micro-DSLs and one host. Criterion 1: borderline. Criterion 2: telemetry contracts evolve constantly. Criterion 3: depends on the upload backend, which may or may not be VSCode-coupled. Out of the kernel. Telemetry beyond the local-only EditLog is a separate package, opt-in, optional, replaceable.
Performance instrumentation. Used by, in principle, every micro-DSL — but in practice the abstraction usually devolves into a per-handler timing wrapper that is better off being a thin utility per micro-DSL. Criterion 1: borderline. Criterion 2: low — performance instrumentation evolves with the observability stack. Out of the kernel.
The exercise stops here, with five concepts in and a long tail of plausible-but-rejected candidates. The list is closed in the sense that no further candidate that the suite obviously needs has been left out; the list is open in the sense that, two years from now, a sixth concept might earn its place — but the burden of proof will be the same three criteria.
The god-object failure mode
The opposite of the small kernel is the kernel that grows by accretion. A concept satisfies criterion 1 and is added; another concept satisfies criterion 1 and is added; the criteria are not re-checked when the kernel ships v1.5.0. By v2.0.0 the kernel has thirty-seven exports, half of which are used by exactly one micro-DSL each. Every micro-DSL pins the kernel version. Every release of the kernel triggers a coordinated release of fourteen micro-DSLs. The kernel is now the release, and the micro-DSLs are tags on it.
The failure mode has a name in DDD: it is the god-object kernel, and Evans warns against it explicitly:
The shared kernel cannot be changed as freely as other parts of the design. Decisions involve consultation with another team. Automated tests must be run with both teams' code. Usually, it is wise to have a notification system to alert one team to changes made by the other. ... The shared kernel will often be the CORE DOMAIN, some set of GENERIC SUBDOMAINS, or both, but it can be any part of the model that is required by both teams.
— Evans, p. 354
The keyword is required. A concept earns kernel status when it is required by multiple consumers, not when it is convenient. The detection mechanism is straightforward: maintain an explicit, public list of kernel exports, with a one-line per-export note saying which micro-DSLs consume it. Any export with fewer than three downstream consumers is a candidate for demotion. The list lives in the kernel package itself, in KERNEL-EXPORTS.md (TODO when the package exists), and is reviewed at every minor release.
The detection mechanism's value is the conversation it forces. Why is this in the kernel? should have a clear answer; if it does not, the export should move out. The conversation is uncomfortable in the same way removing dead code is uncomfortable — it requires saying out loud that something added in good faith is no longer pulling its weight — and is the only way to keep the kernel from growing.
The kernel stability contract
The other half of governance is what counts as a breaking kernel change.
The contract this series adopts is borrowed directly from production semver, applied with one tightening: any change to a kernel export's type signature — added required parameter, removed export, renamed type, narrowed return — is a major version bump. Any change to a kernel export's implementation that preserves the type signature is a minor version bump, with a release note enumerating which downstream behaviours might shift. Any change that touches no exported type and no exported behaviour is a patch.
In practice, kernel major bumps should be rare — once a year at most, and ideally less. Every major bump triggers a coordinated release of every micro-DSL that imports the changed export, every host, and every consumer. The cost is real and is the price the suite pays for the stability the kernel provides. If kernel major bumps happen more than once a year, criterion 2 (stability) was misjudged for some kernel concept and that concept should be demoted.
This is also why the kernel ships its own CHANGELOG.md separate from the suite's, and why the kernel's CI runs the full integration test suite of every micro-DSL on every PR. The cost of a kernel change is high, so the cost of misjudging a kernel change must be paid before merge, not after.
What this article verifies
This article verifies the four acceptance criteria of FEAT-MICRODSL-02 declared in assets/features.ts:
- dddSharedKernelDefinitionRecalled — the section Evans recalled quotes the relevant passage and names the two properties (explicit + costly) doing the work.
- inclusionCriteriaForKernelStated — the section Three inclusion criteria lists known-and-used by ≥3, stable, no VSCode dependency, with one paragraph defending each.
- godObjectAntiPatternAddressed — the section The god-object failure mode names the failure, quotes Evans on the cost, proposes the explicit
KERNEL-EXPORTS.mddetection mechanism. - kernelStabilityContractStated — the section The kernel stability contract states the semver-with-tightening rule, names the once-a-year-at-most cadence, requires the integration-test gate.
What article 03 picks up
This article gave the kernel a list and a discipline. Article 03 turns to the other side of the architecture — the bounded contexts. It applies Evans' bounded-context pattern to define what a micro-DSL is as a unit (one bounded context, one publishable npm package, one decorator family, one set of emitted artefacts), introduces the anti-corruption layer at the kernel boundary so micro-DSLs can have their own internal model without leaking it into the kernel, and states the dependency direction theorem — kernel ← micro-DSL ← host — that the rest of the series treats as load-bearing. Read 03 next.