ADR 0010: Two-stage recoverable self-hosting
Ownership update: ADR 0012 moved every implementation path below to the standalone
github.com/spice-framework/toolchainmodule. The architectural decision remains active for that repository and is no longer implemented in core.
Status: Accepted
Context
Spice should use its own application model for Spice-owned applications and libraries. Making the only compiler executable depend on generated output, however, would create a bootstrap cycle: a missing or damaged generated target could require that same target in order to regenerate it.
The production command also needs to prove real framework value rather than wrapping its existing dispatch behind a cosmetic annotation. It must exercise typed construction, imported library defaults, module validation, lifecycle, generated source mapping, and typed test replacement.
Decision
Spice uses two executable stages:
cmd/spice-bootstrapis an ordinary-Go recovery compiler. It importsinternal/cliand compiler packages but no generated application package.cmd/spiceis the production application. It imports the committedinternal/spicegen/spicetarget, constructs and starts it, invokes its typedCommandcomponent, and stops it with a fresh bounded context.
internal/spiceapp owns the application marker and module declaration.
internal/autoconfigure contributes the reviewed fallback runtime, 13
independent CLI handler factories, and the command factory through an explicit
blank import. The generated target constructs each handler as a distinct
interface bean, injects the ordered []cli.Handler collection into the
command, and exposes every node through typed Components and
BeanOverrides.
The compiler, CLI, development loop, guarded generator filesystem, LSP, and application marker declare an executable Modulith canvas. Compiler packages consumed across a module boundary are explicit named interfaces. Canonical auto-configuration descriptors stay auxiliary and cannot contribute module metadata.
Compiler, generator, CLI implementation, and guarded-filesystem packages may
not import the production generated target. The repository bootstrap gate
audits that stage zero has no generated dependency and stage one has exactly
the spice generated target tree. It verifies target freshness using both
executables and retains an isolated zero-output deterministic recovery proof.
Boundaries
- Stage zero is recovery infrastructure, not a second product implementation.
- Both commands delegate to the same
internal/clibehavior. - Stage zero assembles the same exported runtime and handler factories manually; stage one obtains them through generated dependency injection.
- Generated construction performs no reflection, runtime package scan, global registration, or string-based lookup.
- The production manifest and generated Go are committed and guarded by normal ownership checks.
- Missing production output must always be recoverable with stage zero and the repository vendor graph while network access is disabled.
Consequences
- Spice’s shipped command exercises the same generated application contract offered to users.
- Self-hosting cannot strand the compiler behind its own generated output.
- CLI application tests use exact typed overrides and normal lifecycle cleanup.
- A typed handler override must flow into the generated interface collection, and production LSP/configuration behavior must execute through that graph.
- Any production graph change must regenerate the
Spicetarget and pass both dependency audits. - Parser, type checker, and renderer packages remain ordinary Go rather than being forced into runtime bean semantics.