Filling the Unit System: IntegrationUnit, AdapterUnit, and the Full Boot Sequence

Published: (February 25, 2026 at 01:52 PM EST)
7 min read
Source: Dev.to

Source: Dev.to

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.

Design highlights

  • All lifecycle hooks are 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 merely declares capabilities for others to depend on.
  • This is Interface Segregation in practice. The kernel duck‑types before calling — e.g. 'configure' in unit — so there is no dead code, no empty method stubs, and no forced implementations.


Two Phases, Two Contexts

Every unit kind now gets phased contexts.

HookContext typeCapabilities
configure()IntegrationConfigureContextFull access to ctx.expose() and ctx.query()
build()IntegrationBuildContextQuery‑only (registration is closed, no new APIs can be exposed)

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.

The 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:

defineIntegration(integrationDefinition)
  • It injects kind: 'integration' and returns a VelnoraUnit.
  • The same pattern, same type inference, same generics capturing literal strings from requires and capabilities.

Calling conventions

All three define helpers — defineRuntime(), defineAdapter(), and defineIntegration() — support a second calling convention: a factory function.

defineIntegration((env: ConfigEnv) => ({
  // …config that can depend on env.command, env.mode, etc.
}))
  • Use this 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: a static definition for simple cases, a 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. 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 determines the exact ordering.
    • Example: if your integration depends on 'docker', the Docker integration would configure first.

The resolver that performs this sorting is not wired up yet.

Open question: circular dependencies

What happens when A requires B and B requires A?

  • I haven’t decided yet.
  • Possible strategies: reject the cycle at startup, or break it with a warning.
  • For now the design assumes no cycles — if you create one, you get what you deserve. This will need a concrete 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.

Required hooks

dev(project: Project, ctx: AdapterDevContext): DevServerResult
build(project: Project, ctx: AdapterBuildContext): BuildResult
  • Both hooks are required.
  • dev() returns a DevServerResult — connection info that the Host uses to wire up the dev server.

Design shift: removing mode

Earlier iterations had mode: 'thread' | 'process' on the adapter itself. That was wrong. Whether something runs in‑thread or as a child process is the runtime’s decision, not the adapter’s.

  • A Vite adapter should not care if it runs inside Node’s process or gets spawned separately.
  • It only says what to run.
  • The runtime decides how to run it.

Execution flow

  1. The adapter queries the runtime via ctx.query().
  2. It calls something like execute().
  3. The runtime returns a declarative ExecutionPlan (a discriminated union).
    • Thread mode – carries a run function that Velnora calls in‑process.
    • Process mode – carries the binary, args, and cwd for Velnora to spawn as a child process.

Either way, the adapter code stays the same. The same adapter works with Node, Bun, or any other runtime.

Phased contexts

  • AdapterDevContext for dev()
  • AdapterBuildContext for build()

defineAdapter() follows the same pattern as the other helpers: static object or factory function, kind injected automatically, full type inference on dependencies.


The Full Boot Sequence (In Theory)

The init order described above is not implemented yet, but the intended flow is:

  1. Runtimes boot first.
  2. Adapters initialize next (dev/build hooks become available).
  3. Integrations configure last, after all runtimes and adapters are ready.
  4. Within each tier, a topological sort based on requires/optionalRequires resolves the exact ordering.

Once the resolver is wired up, the system will enforce this sequence, detect cycles, and provide clear error messages.

How It Should Work

Runtimes – language runtimes boot first

Adapters – build tools next

Integrations – frameworks last

Within each tier, a topological sort by requires and optionalRequires determines the exact ordering. You drop units into one array and the Kernel figures out the rest—that is the design. What actually happens in practice is a different story, but this is the contract the system is built around.

All three unit kinds are now implemented:

LayerUnits / APIs
RuntimeRuntimeUnit, PackageManager, Toolchain, defineRuntime()
IntegrationIntegrationUnit, phased contexts, defineIntegration()
AdapterAdapterUnit, the execution‑model split, defineAdapter()

The Unit System is complete—every unit kind has its interface, its factory, and its place in the boot sequence.


A Note on AI and How I Work

I use AI for generating tests and JSDoc, and in rare cases for code. Even when it generates code, I review it multiple times before applying it to the project and committing. The code architecture is mine.

The way the work gets seen—which packages should exist, how the spec should read, how the design should be documented—is where I use Notion AI. Each time I work on a feature, the process takes almost a couple of hours:

  1. Understand the design.
  2. Consult with AI.
  3. Analyze the AI output again.
  4. Document it properly.

The thinking is mine; the documentation process is a collaboration.

Current limitation:
Almost every code assistant has usage caps—except Notion AI, which I have already exhausted. Consequently, tests and JSDoc generation are temporarily unavailable until I get a local assistant running. I’m setting one up, but it isn’t fully ready yet. Right now I’m creating a connection between my PC and my Mac so I can run it remotely, independent of distance.


What Is Next

The Unit System is complete—every unit kind has its interface, its factory, and its place in the boot sequence. The next step is wiring them into the Kernel’s actual resolver and creating the first real unit packages. This means:

  • Real defineRuntime() calls for Node and Bun.
  • A real defineAdapter() for Vite.
  • A real defineIntegration() for React.

Interfaces are done; now it’s time to fill them.

0 views
Back to Blog

Related posts

Read more »

[Boost]

Profile !Vincent A. Cicirellohttps://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaw...