provisioning/adrs/adr-040-lian-build-lift-out.ncl

84 lines
6.2 KiB
Text
Raw Permalink Normal View History

let d = import "adr-defaults.ncl" in
d.make_adr {
id = "adr-040",
title = "buildkit-launcher lifted out as lian-build: standalone build substrate peer project",
status = 'Accepted,
date = "2026-05-01",
context = "provisioning/platform/crates/buildkit-launcher and provisioning/extensions/components/buildkit_runner implemented the build substrate — ephemeral buildkit compute provisioning, OCI cache management, golden image lifecycle — inside the provisioning workspace. ADR-039 defined the architecture; buildkit-launcher was the implementation. Two structural problems emerged: (1) the build substrate domain (ephemeral compute, OCI cache, multi-actor sessions, provider abstraction) is orthogonal to provisioning's core domain (workspace lifecycle management); evolution of cache namespacing, provider traits, or session models required provisioning releases; (2) vapora (multi-agent orchestration) and workspace CI pipelines (Woodpecker) needed build substrate access without depending on the full provisioning binary and config system. The component passed all four criteria of the ontoref lift-out pattern (ADR-016 in ontoref): orthogonal concern, consumer plurality, release cadence divergence, config path-agnostic.",
decision = "buildkit-launcher and buildkit_runner are extracted from provisioning as lian-build (炼), a standalone build substrate project at /Users/Akasha/Development/lian-build. provisioning retains catalog/lian-build/ — the NCL schemas, defaults, and component declarations that allow workspace infras to supply BuildDirectives to lian-build. Workspace infras declare their build intent using lian-build's NCL vocabulary; provisioning's runtime calls the lian-build binary with the generated directive config. No provisioning crate is imported by lian-build as a library dependency. buildkit-launcher and buildkit_runner workspace member entries are removed from provisioning/platform/Cargo.toml.",
rationale = [
{
claim = "Orthogonal domain justifies independent release lifecycle",
detail = "provisioning's core loop is: read NCL workspace definition → reconcile component state → apply changes. lian-build's core loop is: receive BuildDirectives → provision ephemeral compute → run buildkitd → collect artifacts → tear down. These loops share no state and evolve on different schedules. Adding a second registry provider to lian-build should not require a provisioning release, and vice versa.",
},
{
claim = "Consumer plurality is proven",
detail = "At extraction time: provisioning (workspace component builds), vapora (multi-agent build sessions), workspace CI pipelines (Woodpecker steps). Three distinct callers, each with different invocation patterns and config sources, confirm lian-build's value is not provisioning-specific.",
},
{
claim = "catalog/lian-build/ is the correct integration surface",
detail = "provisioning loads lian-build's BuildDirectives schema as an extension, making it available to workspace infra NCL. The workspace declares build intent; provisioning validates it against lian-build's schema and passes it to the binary. This is the correct dependency direction: provisioning knows about lian-build's vocabulary, but lian-build does not know about provisioning's internal structures.",
},
],
consequences = {
positive = [
"lian-build releases independently: provider additions, cache policy changes, session model improvements do not block provisioning",
"vapora and workspace CI pipelines consume lian-build directly without routing through provisioning",
"provisioning/platform/Cargo.toml shrinks: two workspace members removed",
"ComputeProvider and RegistryProvider trait boundaries are declared at project inception, not retrofitted",
],
negative = [
"Two schema maintenance surfaces: lian-build/schemas/ (source of truth) and provisioning/catalog/lian-build/ (consumer-side reference)",
"Workspace infras that previously used buildkit_runner component definitions must migrate to lian-build's BuildDirectives schema",
],
},
alternatives_considered = [
{
option = "Keep buildkit-launcher as a provisioning crate, expose via provisioning subcommand",
why_rejected = "Prevents vapora and Woodpecker from using the build substrate without depending on provisioning. Release coupling blocks provider evolution. Four-criterion test (ontoref ADR-016) makes the extraction unambiguously correct.",
},
],
constraints = [
{
id = "extensions-not-binary-dep",
claim = "provisioning must not import lian-build as a Rust library dependency; interaction is via CLI invocation with NCL-generated config",
scope = "provisioning/platform/Cargo.toml, provisioning/catalog/",
severity = 'Hard,
check = {
tag = 'Grep,
pattern = "lian-build",
paths = ["provisioning/platform/Cargo.toml"],
must_be_empty = true,
},
rationale = "Library dependency would re-couple provisioning's build cycle to lian-build's. The integration surface is the CLI binary + NCL schema, not the Rust crate graph.",
},
{
id = "extensions-lian-build-present",
claim = "provisioning/catalog/lian-build/ must be present and contain the BuildDirectives schema before the workspace member entries are removed",
scope = "provisioning/catalog/lian-build/",
severity = 'Hard,
check = {
tag = 'FileExists,
path = "provisioning/catalog/lian-build/build_directives.ncl",
present = true,
},
rationale = "Removing the workspace member without the extension schema would break workspace infras that declare build components. Schema first, then removal.",
},
],
related_adrs = ["adr-039-build-infrastructure-ephemeral", "ontoref:adr-016-component-lift-out-pattern"],
ontology_check = {
decision_string = "buildkit-launcher and buildkit_runner extracted as lian-build standalone project; provisioning retains catalog/lian-build/ as integration surface; no Rust library dependency from provisioning to lian-build",
invariants_at_risk = [],
verdict = 'Safe,
},
}