Last post, the first runtime and adapter were alive. The type system had been fighting back — declaration merging across packages, build tool swaps — but honestly? I kind of forgot about that problem. It is still there, just not blocking anything right now. Sometimes you move forward and the old fight waits. 😄 The units existed. They could register. But the plumbing between "units declared in a config file" and "units actually running" did not exist yet.
That changed. This round of work is quieter than the last, but it touches everything: config resolution, a shared base context, a working expose-and-query system, and a few small utilities to keep the registry sane.
First Things First: Loading the Units
Before anything else can work, the Kernel needs to know what units it is dealing with. That is what config.resolve.units does — it takes the flat list of units from your config file and resolves them into something the Kernel can actually use. The units go in as references — strings, factory functions, imported objects — and they come out as loaded, validated, ready-to-boot definitions that the Kernel knows how to classify and initialize.
This is the bridge between "I declared units in my config" and "the Kernel knows what to do with them." Without it, the unit array is just data sitting in a file. With it, the Kernel can classify units by kind, topological-sort them by dependencies, and start calling lifecycle hooks in the right order.
It is not glamorous work. But it is the plumbing that makes everything else possible.
One Context to Share Them All
With units resolved, the next question is: what do they get to work with? Every unit kind in Velnora gets a context object — the thing passed into lifecycle hooks like configure() and build(). The context is how a unit talks to the rest of the system: exposing APIs, querying other units, reading config.
Until now, each context was defined independently. IntegrationConfigureContext had its own ctx.query(). IntegrationBuildContext had its own. AdapterDevContext — same thing, redeclared from scratch. That meant duplicated surface area everywhere, and any change to the shared behavior had to be mirrored across all of them. It was manageable with three unit kinds. It would not be manageable at ten.
The fix is straightforward — create a BaseContext that carries everything shared, and let the specific contexts extend from it. Integration contexts inherit the base and add integration-specific capabilities on top. Adapter contexts do the same. Runtime contexts do the same. The shared parts live in one place. The specialized parts stay where they belong.
This is not a clever idea. It is just the first time it is actually built.
Expose, Query, Done
The BaseContext now fully implements the exposing and querying mechanism — the two operations that every unit needs regardless of kind. This is ctx.expose() and ctx.query() actually working, not just typed, but wired up end-to-end. A unit can expose an API under a key during configure(), and any other unit that declared a dependency on it can query that key and get the real implementation back. No casting, no any, no manual lookups. The foundation that the entire Unit System was designed around is now real.
Backing the BaseContext is the GlobalRegistry — a central registry object that holds all exposed APIs at runtime. When a unit calls ctx.expose("docker", dockerApi), it writes to the GlobalRegistry. When another unit calls ctx.query("docker"), it reads from the same place. One source of truth for the entire unit graph.
Every registry entry is keyed, and the keys are properly typed and enforced. The same key that TypeScript checks at compile time is the one used to look things up at runtime. If the types say "docker" exists, the runtime registry has a slot for "docker". If they do not, the slot does not exist and the query fails loudly.
With these in place, the BaseContext is not just a shared interface anymore. It is the working layer that integrations, adapters, and runtimes all build on top of. The thing that was just types two posts ago is now running code.
A Helper for the Lazy
One small helper that came out of this work — makeRegistryObject. It is a convenience for lazy people. You give it a name and a shape, and it builds you a nested object where every level is also a usable string key. So instead of writing "kernel:config:units" by hand and hoping you did not typo it, you just do result.config.units and it gives you that exact string. You can use it as a lookup key and dot into it for child keys — both at the same time.
For example, makeRegistryObject("kernel", { config: { units: "units" } }) gives you a value where result is "kernel", result.config is "kernel:config", and result.config.units is "kernel:config:units". Each level works as a registry key. No manual string building, no typos, full autocomplete.
It is a tiny thing. But in a monorepo with a growing number of registry keys, it saves a lot of stupid mistakes.
The Pause
After wiring up the base context and the config resolution, I started pushing into the runtime implementation — the actual guts of how a runtime like Node or Bun boots its toolchain, resolves packages, and executes code. And then I stopped.
Not because something broke. Because I realized I was about to make decisions that would be very expensive to undo. The runtime layer touches everything: how units get their toolchains, how execution plans are built, how the package manager abstraction plugs in, how thread-mode and process-mode execution diverge. One wrong abstraction here and every adapter, every integration, every future runtime inherits the mistake.
So I went back to research. Reading through the earlier design notes, sketching alternatives, revisiting the Toolchain API and the Adapter Protocol to see if the boundaries still made sense after everything that changed in the last few rounds. Sometimes the most productive thing you can do is stop coding and think. This is one of those times.
What Is Next
Once the runtime design solidifies, the next step is implementing it: a real Node runtime that boots its toolchain through the BaseContext, resolves packages through the registry, and produces execution plans that adapters can consume without knowing which runtime is behind them. That is the test that proves the whole stack works end-to-end.
Top comments (0)