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

Part 05 — The kernel runtime: PatchBus, AST, Banner, EditLog

Article 04 specified the metamodel as data: what Concepts exist, what their properties and links are, how they relate. This article specifies the runtime: how the live state evolves at edit time, how mutations are coordinated across micro-DSLs, how identity survives reloads and refactors, how emitted artefacts stay idempotent, and how the local telemetry stream feeds usage analysis without leaking workspace content. Together with article 04, this article completes the kernel.

Four primitives carry the runtime: AST + NodeId (the live tree of Concept instances and their stable identifiers), PatchBus (the only API that mutates the tree), Banner (the idempotence marker every emitter writes), EditLog (the local-only append-only stream of mutations). Each is small. Each has been justified in article 02 against the three inclusion criteria. Each is described in this article in enough detail for any micro-DSL author to consume it without further questions.

AST and NodeId

The AST is the in-memory tree of Concept instances built at file-load time. Each instance is a typed object whose class corresponds to a @Concept declaration from the Structure Model — Feature, AcceptanceCriterion, Epic, Note. Properties are populated from the source file; @ChildLink collections are populated by recursive parsing; @ReferenceLink fields hold NodeId references that resolve lazily through the kernel's reference index.

The kernel exposes the AST through one read API and one write API. The read API is per-document: getRoot(uri): Node | undefined, getNode(uri, nodeId): Node | undefined, plus a tree walker walk(node, visitor). The write API is the PatchBus, described below — there is no other way to mutate the AST. Direct field assignment on a Node instance is forbidden by the typings: every mutating field is readonly from the consumer's perspective; PatchBus operations mutate the underlying state through a privileged path that no micro-DSL can take.

NodeId is the identifier that survives the moves the AST goes through. Two contracts:

  • Identity stability across reloads. A Node loaded today, saved, and reopened tomorrow has the same NodeId. The id is computed from the source position and structural fingerprint at parse time, with a deterministic hashing convention; reopening the same file produces the same ids modulo content change.
  • Identity stability across structural transformations. When the Refactoring micro-DSL (article 17) renames a Feature, the Feature's id property changes but its NodeId does not. When a Feature is moved between files, its NodeId stays. When a Feature is split into two (a Refactoring extract operation), the original keeps its NodeId and the new Feature gets a fresh NodeId; the kernel records the split so cross-references can be re-pointed.

What NodeId does not guarantee:

  • It does not survive deletes. A deleted Node's NodeId is retired and not reused.
  • It does not survive file deletes. If the file containing a Node is removed from the workspace, every Node in that file is gone, ids included.
  • It does not survive manual external edits that re-parse to a different shape. If a user edits a .req.ts outside VSCode in a way that changes the structural fingerprint of a Node, the kernel may issue a fresh NodeId to the re-parsed Node. The Refactoring micro-DSL handles this gracefully by maintaining a similarity index for cross-reference repair, but the strict guarantee is structural identity, not semantic identity.

The contract is documented in KERNEL-NODEID.md (TODO when the package exists) and tested with golden snapshots: a fixture spec.ts file is parsed, a NodeId is captured, the file is touched in various ways, the NodeId is re-checked. The test suite is one of the half-dozen things the kernel ships at v1.0.0 with no plan to ever change.

PatchBus

The PatchBus is the only mutation API. Four operations:

interface PatchBus {
  setProperty(nodeId: NodeId, propertyName: string, value: unknown): Promise<void>;
  insertChild(parentId: NodeId, linkName: string, child: NewNode, atIndex?: number): Promise<NodeId>;
  removeChild(parentId: NodeId, linkName: string, childId: NodeId): Promise<void>;
  setReference(sourceId: NodeId, linkName: string, targetId: NodeId | null): Promise<void>;
}

Each operation:

  1. Validates the operation against the Structure Model — setProperty checks the property exists on the Concept and the value matches the declared type and constraint; insertChild checks the link exists, the child Concept is assignable to the link's target, the cardinality is not exceeded; removeChild checks cardinality is not violated below the minimum; setReference checks the target Concept is assignable.
  2. Applies the mutation to the in-memory AST atomically.
  3. Updates the kernel's reference index — insertChild registers the child for resolution; removeChild invalidates references; setReference updates the index.
  4. Appends one entry to the EditLog.
  5. Emits a typed event so subscribers (Views, Symbols, the Custom Editor host's projections) refresh.

That is the entire mutation lifecycle. It is the same lifecycle for the Refactoring micro-DSL invoking a rename, for the Diagnostics micro-DSL applying a code action, for the Projection micro-DSL responding to a user dragging a child between siblings in the table view. There is no second path. Article 03 named the architectural justification (no horizontal coupling, single point of observability, atomicity); this article names the consequence: every micro-DSL that mutates state has the same shape — compute the patch, submit it, observe the event.

The async return type matters. PatchBus operations are asynchronous because validation may need to consult the workspace index (cross-file uniqueness checks, for example) and because the EditLog write is asynchronous. Consumers that need a synchronous mutation (rare, mostly inside the Custom Editor host's React render path) buffer changes locally and submit them in micro-task batches.

The PatchBus also exposes an inverse for undo/redo: every committed operation produces an inverse operation that, applied, reverts it. The Refactoring micro-DSL composes inverses to build "undo this rename" support; the Custom Editor host's WebView wires the inverses to cmd+z. Undo is therefore not a separate concern — it is a property derived from the PatchBus contract.

Banner

The Banner is the idempotence marker every kernel-aware emitter writes. The pattern is lifted directly from packages/ts-codegen-pipeline/src/emit/banner.ts, where it has been production-stable for the lifetime of the source-generator package. The shape:

// sourcegen-banner: <hash> generator=<id> generated-at=<iso8601>
// DO NOT EDIT — this file is regenerated by `npx sourcegen run`.
//
// ... emitted content ...

The <hash> is a SHA-256 of the emitted content (excluding the banner itself), computed at emit time. The <id> names the emitter that produced the file; <iso8601> records the timestamp. On regeneration, the emitter computes the would-be content, computes its hash, and compares against the existing banner's hash: if they match, the file is not rewritten (no diff, no change in modification time, no spurious VCS entry). If they differ, the file is rewritten with a new banner.

The pattern produces three load-bearing properties:

  • Idempotent regeneration. Running the Generator micro-DSL twice on an unchanged AST produces no file changes. Test fixtures rely on this: a CI step that runs regenerate and then git diff --exit-code confirms the AST is consistent with its emitted artefacts.
  • Drift detection. A file with a banner whose hash does not match the current content has been hand-edited. The kernel surfaces this through the Diagnostics micro-DSL with a "this file is generated; your edits will be overwritten" warning.
  • Cross-format support. The kernel-shipped Banner module recognises three comment styles — // for TypeScript, /* */ for JSON-with-comments, <!-- --> for HTML — and produces banners in the appropriate style per emit target. This is the one extension to the upstream ts-codegen-pipeline Banner that the kernel ships, and it is backward-compatible: TypeScript banners continue to work unchanged.

The MPS counterpart of Banner is the generator output identification comment MPS adds to every generated file. The mechanism is similar; the TypeScript adaptation just uses a hash where MPS uses a UUID-plus-timestamp. The hash makes idempotence comparable byte-for-byte without parsing the emitted file.

EditLog

The EditLog is the local-only, append-only stream of PatchBus operations. The contract:

  • Append-only. Once an entry is written, it is never modified or removed. The log can be truncated at file boundaries (a configurable rotation policy keeps the on-disk size bounded), but individual entries are immutable.
  • JSONL on disk. One operation per line, in a typed JSON format mirroring the PatchBus operation signatures, plus a millisecond timestamp and the originating micro-DSL id.
  • Per-workspace. The log lives at .ide-dsl/edit-log.jsonl relative to the workspace root, gitignored by default. It is not shared across workspaces; it is not uploaded anywhere unless the user has explicitly opted in to an analytics package (which is not part of the kernel; see article 02's god-object discussion).
  • Subscribable. The kernel exposes an onAppend event so subscribers (Views, Symbols, the Custom Editor host) can react to mutations without polling.

The motivations are three:

  1. Refresh primitives. Views (article 14) refreshes a tree on every relevant insertChild/removeChild. Symbols (article 16) invalidates index entries on every setProperty that touches an indexed property. The Custom Editor host (article 21) re-renders projections affected by every operation. Without the EditLog, each subscriber would need its own observer pattern; with it, the kernel publishes one event stream and everyone subscribes.
  2. Local analytics. The Custom Editor host's projection-usage tracking (article 21) walks the EditLog to learn which projections users actually open and edit. The walk is local; the telemetry is local; the user can inspect the log to see what is being measured.
  3. Recovery. A workspace whose AST is corrupted (a kernel bug, an extension crash, a bad refactor) can be rebuilt from the EditLog by re-applying every operation against an empty initial AST. The recovery is rarely needed but is the kind of property that proves itself the one time it is.

The EditLog is not a synchronisation mechanism for collaborative editing. CRDT-style cross-user merging is out of scope; the suite is single-user, single-workspace, single-process. A future collaboration story is conceivable — the EditLog format is JSONL with stable schema — but the kernel makes no commitment.

The kernel's complete public API

The five-concept kernel from article 02, made concrete:

// Metamodel (article 04)
export { Concept, Property, ChildLink, ReferenceLink };
export type { ConceptId, PropertyType, Cardinality };
export type { StructureModel, ConceptDeclaration };
export type { PropertyDeclaration, ChildLinkDeclaration, ReferenceLinkDeclaration };

// AST + NodeId (this article)
export type { NodeId, Node, NewNode };
export type { Document };
export { getStructureModel, getDocument };

// PatchBus (this article)
export type { PatchBus };
export { getPatchBus };

// Banner (this article, lifted from ts-codegen-pipeline)
export { withBanner, verifyBanner };
export type { BannerStyle };

// EditLog (this article)
export type { EditLogEntry };
export { getEditLog, onEditLogAppend };

// Reflection (decorator scanning, transitive from ts-morph)
export { findDecorated };

That is the entire kernel's public exports. The list lives in KERNEL-EXPORTS.md per the discipline article 02 names; it is reviewed at every minor release. New kernel exports require a one-line justification and a confirmation that ≥3 micro-DSLs consume them.

What the kernel runtime does not do

Three deliberate non-features, each named so the boundary is unambiguous:

  • It does not parse spec.ts files. Parsing is delegated to ts-morph (already a dependency of ts-codegen-pipeline); the kernel sits on top of ts-morph's AST and walks it with findDecorated. A future migration to a different TypeScript parser is a kernel internal detail; consumers do not see ts-morph in the kernel's public API.
  • It does not implement undo/redo UI. The PatchBus exposes inverses; the Custom Editor host wires them to cmd+z. The kernel does not own the user interaction.
  • It does not coordinate with VSCode. The kernel runs in plain Node — vitest, CLI, server-side processes — without ever importing vscode or LSP types. The hosts (article 20, 21) do that work. This is the article 02 criterion 3 made concrete: the kernel's tests run in 200ms, the kernel's API does not depend on whatever editor is in fashion.

A worked example: a refactoring rename, end to end

Concrete trace of how the kernel runtime carries one refactoring:

  1. The user invokes "Rename Feature" in VSCode at the cursor position of a Feature declaration.
  2. The LSP host (article 20) receives textDocument/rename, looks up the Refactoring micro-DSL contribution, calls its rename adapter.
  3. The Refactoring micro-DSL queries the kernel's reference index for every node that holds a @ReferenceLink to the target Feature's NodeId. The index returns [ref1, ref2, ref3].
  4. The Refactoring micro-DSL composes a list of PatchBus operations: one setProperty on the source Feature.id plus three setProperty operations on the source-text holding the references in their host files.
  5. The Refactoring micro-DSL submits the operations as a single transaction — the kernel exposes patchBus.transaction(ops) — which validates all operations, applies them atomically, appends them to the EditLog as one batch, and emits one onPatchBatch event.
  6. The Symbols micro-DSL's onEditLogAppend subscriber sees the batch, invalidates the affected index entries, schedules a re-index.
  7. The Views micro-DSL's tree subscriber sees the batch, refreshes the Feature tree node and its parent.
  8. The Custom Editor host (if open) re-renders the projections affected by the changes — text projection re-pretty-prints the affected files, table projection refreshes the Feature row.
  9. The Generator micro-DSL is not invoked automatically — regeneration is on-demand; the user triggers ide-dsl.regenerate when they are ready. When triggered, the generator walks the AST, computes the would-be output for FeatureValidator.ts, computes its Banner hash, sees the hash differs (because the Feature.id changed), rewrites the file with the new Banner.

The trace exhibits every kernel primitive in use. PatchBus carries the mutation; NodeId stability ensures the references stay valid (their target NodeId did not change, only the target's id property); EditLog publishes the event stream; Banner makes the regeneration idempotent. Four primitives, one trace, one consistent shape.

What this article verifies

This article verifies the four acceptance criteria of FEAT-MICRODSL-05 declared in assets/features.ts:

  • patchBusSingleMutationPathDeclared — the PatchBus section enumerates the four operations and the lifecycle, and the worked example shows the exclusivity (no other path).
  • nodeIdentityStabilityContractStated — the AST and NodeId section names the two contracts and the explicit non-guarantees, references the KERNEL-NODEID.md document and the golden snapshot tests.
  • bannerIdempotencePatternRecalledFromTsCodegenPipeline — the Banner section quotes the existing format, names the lift from ts-codegen-pipeline, names the three load-bearing properties and the cross-format extension.
  • editLogTelemetryScopedToLocalOnly — the EditLog section names the four contract properties, the .ide-dsl/edit-log.jsonl location, the gitignore default, the explicit non-upload, the contrast with telemetry packages.

What article 06 picks up

Articles 04 and 05 finish the kernel. Part III now spends fourteen articles on the micro-DSLs that consume it. The order is roughly outside-in: identity-shaped concerns first (Language, Syntax), then text-time concerns (Completion, Snippets, Hover, Diagnostics), then structural concerns (CodeLens, Commands, Views, Formatter, Symbols, Refactoring), then the projectional and generative concerns that pull everything together (Projection, Generator). Each article follows the eight-section anatomy fixed in article 06's introductory paragraph. Read 06 next.

⬇ Download