v0.1 is tagged. The skeleton stands. And the original plan said v0.2 would be graphs — dependency graphs, project graphs, the whole resolution layer.
But after researching it, I realized graphs are useless at this stage. There is no real workspace to resolve yet. No integrations to connect. The graph would just be a piece of code that does nothing because it has nothing to operate on. It would exist purely to satisfy the roadmap, not to solve an actual problem.
So I skipped it. Graphs will come later, in a future release, when there are enough moving parts to make resolution meaningful. For now, the real next step is integrations — the things that actually fill the workspace. That became the new v0.2 target.
And the first question was: how do integrations even enter the system?
The answer started with a problem I kept hitting in earlier rewrites — three separate registration paths. Integrations had one way in, adapters had another, and runtimes were their own thing. Every time I added something, I had to remember which path it belonged to. It was manageable at the scale of v0.1, but I could already feel it becoming a mess.
So I asked: what if everything entered the same way?
That is the core idea behind what I am calling the Unit System. One array in the config. One resolver in the Kernel. Three typed helpers — defineIntegration(), defineAdapter(), defineRuntime() — for type safety. But underneath, they all produce the same thing: a VelnoraUnit.
The Kernel does not care what kind of unit you are. It collects them all, resolves dependencies, and initializes in the correct order. Kind only matters for when you init — runtimes first, then adapters, then integrations — and for the specific hooks you expose.
This felt right immediately. The config went from three separate concepts to one flat array. And the Kernel went from three separate loaders to one resolver.
The part that took the most thinking was inter-unit communication. Units should never import each other directly — that would create hard coupling and break the whole plugin model. But they need to talk to each other. A Kubernetes integration needs Docker. A Spring Boot integration needs a JVM runtime.
The solution is a capability-based system. Each unit declares what it provides (capabilities) and what it requires. The resolver matches them by string. If you require 'docker', any unit that has 'docker' in its capabilities array satisfies it.
But here is where it gets interesting — the type safety.
I built a global type registry using TypeScript declaration merging. When you install @velnora/integrations-docker, its type declarations automatically register DockerApi into Velnora.UnitRegistry. Then when you call ctx.query('docker') inside another unit, TypeScript knows the return type is DockerApi. No imports. No type casting. Just install the package and the types flow.
Hard dependencies (requires) return the API directly. Soft dependencies (optionalRequires) return T | undefined. The generics on the define helpers infer the literal strings from the arrays, so TypeScript knows exactly which queries are safe and which might be undefined.
This is the kind of thing that makes me love TypeScript. The type system is doing real architectural work here — it is enforcing the dependency contract at compile time.
One design decision I am particularly happy with: composite units. A single unit can bundle child units inside itself using a units[] array. The user adds one thing to the config, and the Kernel unpacks everything.
Docker is the perfect example. As a user, you call docker() and you get both the integration (build images, push, create networks) and the runtime (Docker Engine). One function call. The Kernel flattens the tree, resolves dependencies across all of them, and inits in order.