Last post, the Unit System had its first real types — RuntimeUnit, PackageManager, and defineRuntime(). The runtime layer was done. But a runtime by itself does nothing. It knows how to run things, but nothing is asking it to run anything yet. The system needed the other two unit kinds — integrations and adapters — before any of it would actually connect.

IntegrationUnit

IntegrationUnit is the interface for framework-specific plugins — React, NestJS, Angular, Docker as an orchestrator, and anything else that wires into the Velnora lifecycle. If runtimes are the foundation and adapters are the build tools, integrations are the things that actually give a workspace its identity.

The key design decision here was making all lifecycle hooks optional. An integration only implements what it needs. If it just needs to expose an API during init, it implements configure(). If it also needs to do something at build time, it adds build(). If it needs neither, it can still exist as a unit that just declares capabilities for others to depend on.

This is Interface Segregation in practice. The Kernel duck-types before calling — 'configure' in unit — so there is no dead code, no empty method stubs, no forced implementations.

Two Phases, Two Contexts

Every unit kind now gets phased contexts. The configure() hook receives an IntegrationConfigureContext — full access to ctx.expose() and ctx.query(). The build() hook receives an IntegrationBuildContext — query-only, because by the time you are building, registration is closed. No new APIs can be exposed during a production build.

Right now all three unit kinds share the same idea, but the context types are not fully separated yet. Runtimes, adapters, and integrations each have different phase-specific needs, and the concrete context interfaces still need to be split out per kind. That is a task for the next round.

This separation is not just for safety. It makes the intent clear at the type level. If you are inside build(), TypeScript will not even let you call expose(). You know exactly what phase you are in by looking at the context type.

defineIntegration

Just like defineRuntime(), the factory is thin. You pass your integration definition, it injects kind: 'integration' and returns a VelnoraUnit. Same pattern, same type inference, same generics capturing literal strings from requires and capabilities.

All three define helpers — defineRuntime(), defineAdapter(), and defineIntegration() — support a second calling convention: a factory function. Instead of passing a plain object, you can pass a function that receives ConfigEnv (command, mode, etc.) and returns the config. This is useful when you need to change behavior based on whether you are running dev or build. The factory wraps your function and injects kind at call-time.

Both conventions produce the same thing. Static for simple cases, factory for environment-aware ones. The pattern is identical across all three unit kinds.

Init Order

In theory, integrations should initialize last — after runtimes, after adapters. The idea is that by the time an integration runs its configure() hook, every runtime is booted and every adapter is ready, so the integration can safely query anything it depends on.

Within the integration tier, a topological sort by requires and optionalRequires would determine exact ordering. So if your integration depends on 'docker', the Docker integration would configure first. This is the design — the actual resolver is not wired up yet.

One open question: what happens when A requires B and B requires A? A circular dependency. The honest answer is I have not decided yet. The resolver could reject it at startup, or it could try to break the cycle with a warning. For now the design assumes no cycles — if you create one, you get what you deserve. This will need a real answer before the resolver ships.

AdapterUnit — The Middle Layer

With integrations done, the adapter was next. AdapterUnit represents build-tool orchestrators — Vite, Webpack, Turbopack, esbuild, and similar. Unlike integrations, adapters have required hooks: dev(project, ctx) and build(project, ctx). Both take a Project — the specific project directory the adapter is operating on — plus a typed context. An adapter must handle both modes. There is no optional here — if you are an adapter, you know how to start a dev server and you know how to produce a production build. And dev() returns a DevServerResult — connection info that the Host uses to wire up the dev server.

But the biggest design shift from earlier iterations of the spec was removing the mode field. I originally had mode: 'thread' | 'process' on the adapter itself. That was wrong. Whether something runs in-thread or as a child process is not the adapter's decision — it is the runtime's decision. A Vite adapter should not care if it runs inside Node's process or gets spawned separately. It just says what to run. The runtime says how. Velnora executes the plan.