Skip to main content
Documentation menu

Capability roadmap: ledger

This is the working ledger for the capability roadmap: ten requested areas taken from design to practical completion through stages 01-14, against the shared contracts. It records, per area, the current source evidence, a workflow users cannot complete today, the necessary changes, dependencies, acceptance criteria, and status; then the documentation discrepancies found on the way and the progress record. Reconcile it against the repository and open pull requests whenever the work resumes, and update it in the same change as the work it describes.

Research baseline: main at 62f46d84523b04bc0e4d4e8d7506753659aed239 (2026-10-29). Evidence cites a file and the symbol in it, since line numbers drift. Evidence comes from reading the source; where nothing ran, the entry says so.

Status vocabulary

not started -> in progress -> implemented (merged with docs and tests) -> validated (the stage’s real-project demonstration ran on the platforms recorded in the entry). blocked names its blocker. A scope document changing is never implemented. adopted is the finished state of design-only work (stage 00): its documents are on main, and it implements nothing.

Stages

StageOutcomeAreasMain ownersNeedsStatus
00Shared contracts, this ledger, scope alignmentalldocs/-adopted
01Registry optional dependencies and transitive features in one active graph1cabin-feature, cabin-resolver, cabin-index, cabin-index-http, cabin-workspace, cabin00implemented
02Interface definitions, visibility-driven propagation, final-link closure, per-source options2cabin-core, cabin-manifest, cabin-build, cabin-driver, cabin-package00; integrates 01implemented
03Per-revision resolution metadata; old pins keep their meaning1 (metadata)cabin-index, cabin-index-http, cabin-resolver, cabin-lockfile, cabin-registry-file, cabin-publish, cabin-registry-verify, registry/00, 01; 02’s metadataimplemented
04Build and target contexts, tool dependencies, --target <triple>5, 4 (tool foundation), 9 (platform), 1 (contexts)cabin-core, cabin-manifest, cabin-workspace, cabin-feature, cabin-resolver, cabin-lockfile, cabin-build, cabin-toolchain, cabin00-03implemented
05Compile/link probes, sysroots and SDKs, MinGW on evidence9, 5cabin-toolchain, cabin-core::compiler, cabin-driver02, 04in progress
06Root-selected system providers3cabin-system-deps, cabin-workspace, cabin-manifest, cabin-config, cabin-build01-05not started
07Generated actions and build-machine tools4cabin-build, cabin-driver, cabin-ninja, cabin-manifest, cabin-core02, 04-06; 03’s metadatanot started
08Static, shared, and loadable artifacts; PIC, DLLs, runtime lookup6cabin-core, cabin-build, cabin-driver, cabin-ninja, cabin02, 04-07not started
09Install, CMake Config and pkg-config export, relocation7owner chosen in 09; cabin-build02, 06-08not started
10C++20 modules8cabin-core, cabin-build, cabin-driver, cabin-ninja, cabin-toolchain02, 04, 05, 07-09not started
11Everyday CLI: test argv, test concurrency, JUnit, examples, target selection10cabin, cabin-test00 for independent parts; 04, 08, 10in progress
12OpenSSL and OpenCL packages; Boost TLS and Computevalidation of 1-7, 9-10ports/, Boost ledger01-09; 11 where needednot started
13Python and MPI packages; remaining Boost gapsvalidation of 1-10ports/, Boost ledger01-12not started
14Integrated acceptance across the ten areas1-10all00-13not started

A stage is not one pull request: each stage picks cohesive PR boundaries, and dependent work starts only after what it needs is merged on main. Independent CLI work from stage 11 may land early.

Areas

1. Dependency resolution

Stages 01 and 03; contexts in 04; validated in 12-14.

Today.

  • PubGrub’s private packages are the root, each package name, and each (name, feature) pair (cabin-resolver/src/provider.rs, Node), so one version per package name and one feature set per package; lockfile modes are prefer-locked, locked, update-all, and update-package (cabin-resolver/src/input.rs).
  • Since stage 01, features resolve with versions: a (name, feature) pair’s candidates are the versions declaring the feature, an enabled feature adds its entries, and registry optional dependencies activate exactly when a feature enables them (provider.rs, constraints). cabin-feature resolves the local packages and hands the requests on registry edges to the resolver (FeatureResolution::versioned_requests); local packages, patch forks included, are single-version candidates (cabin/src/cli/seed.rs, local_packages). Index metadata loads lazily from the file and sparse HTTP indexes (ResolveError::MetadataNeeded); the eager prefetch still walks non-optional edges only (cabin_index::walk_dependencies).
  • Feature resolution keeps one feature set per package for the whole graph (cabin-feature, FeatureResolution) and propagates dependency-edge feature requests into the fetched graph’s compiles and target gates (cabin-ports/sqlite3’s single-threaded reaches compilation, tests/cli/registry_ports.rs); the building commands refuse a final feature pass that enables a dependency resolution did not select. The configuration report (cabin/src/cli/build_prep.rs, resolve_build_configurations) reads the local feature resolution.
  • cabin.lock stores name, version, checksum, and resolved direct dependencies per registry package (cabin-lockfile/src/model.rs, LockedPackage); the checksum pins the packaging revision; path packages are not locked.
  • Index revisions carry checksum, published-at, source, and, since stage 03, their own resolution when it differs from the version’s (cabin-index/src/model.rs, RevisionMetadata): the version-level dependencies, features, standards, and links are the current revision’s, and each pin a lockfile mode keeps resolves with its own revision’s (cabin-resolver/src/lib.rs, select_locked_revisions). The file and hosted registries write it (cabin-registry-file/src/index.rs, registry/src/documents.rs).

Blocked workflow. Before stage 01, a consumer enabling a published package’s feature that pulls in an optional registry dependency (feature = ["dep:cabin-ports/zlib"]) got nothing: resolution skipped the dependency. Stage 01 unblocked that, so Boost integrations can be offered as opt-in features; cabin-ports/boost-iostreams still leaves out its bzip2/lzma/zstd filters until those libraries have ports. Since stage 03, a port can gain a dependency at the same upstream version as a packaging revision.

The port publisher adds a constraint of its own: xtask-port-publish refuses cfg(...) dependency tables and orders publication by each port’s own default feature closure, not by feature requests on its dependency edges (xtask-port-publish/src/plan.rs; foundation ports). The first port that needs either lifts it, in stage 01 or in the port stage that needs it (12 or 13).

Necessary changes. Stage 01: feature closure that agrees with candidate selection and backtracking; metadata acquisition separated from archive fetching; one active graph for resolve, update, fetch, build, test, metadata, tree, explain, and vendor; patches and source replacement on the same path. Stage 03: resolution metadata per revision, revision selection for every lockfile mode, client/server schema negotiation. Stage 04: per-context closures.

Acceptance. Stage 01’s scenarios (registry A->B->C feature chain, default-features diamond, older candidate chosen for a feature, optional-edge backtracking, non-matching cfg edge, registry-reached path patch with an optional versioned dependency, unknown feature, locked replay, offline rebuild) and stage 03’s decisive regression (r1 without B, r2 with B: an r1 lock keeps r1’s graph and bytes through file, HTTP, and vendor paths; cabin update moves to r2’s).

Representative projects. Boost TLS features (stage 12); cabin-ports/sqlite3’s single-threaded feature as the regression guard for edge feature requests that already work; cabin-ports/boost-iostreams compression filters once their libraries are ported.

2. Usage requirements

Stage 02; consumed by 06, 08, 09, 10; validated in 12-14.

Today.

  • public per deps entry (cabin-core/src/model.rs, TargetDep) decides which compiles see a dependency’s consumer interface: its include-dirs and interface-defines reach the depending target’s compiles, and that target’s consumers only through public edges (cabin-build/src/planner/lowering.rs, collect_interface). The post-resolution standard check (cabin-workspace/src/standards.rs) and the build-time interface-standard check use the same visibility. Registry packages’ directories become system includes.
  • defines and package flag layers apply to the declaring package’s own compiles, and ldflags to its own link steps, only (cabin-build/src/planner/mod.rs); target interface-defines reach consumers only, and a target’s own defines override an incoming one. Two visible dependencies defining one macro differently fail the build with both paths, recorded per compile so cabin check prunes a dependency’s conflict. defines on a header-only target is a manifest error.
  • The final link takes the archives of the whole transitive closure, private edges included, in dependency-graph order (collect_link_libs), then package-level link-libs and Apple frameworks (Apple platforms only) in the same package order, a name two packages list repeated except on Apple platforms (collect_link_names).
  • Target instruction-sets reach one source’s compile on x86 only: -m<name> for GCC, Clang, and clang-cl, nothing for cl.exe (cabin-build/src/planner/mod.rs, PreparedSource).
  • Registry packages’ raw cflags / cxxflags / ldflags are dropped (cabin-core/src/build_flags.rs, apply_package_layer).
  • Implemented and not missing: C and C++ as separate languages with separate standards; target public edges; interface standard ranges; cfg-conditional sources and flag layers; compiler-conditioned flag layers (cc, cxx, cc_version, cxx_version); assembly sources.
  • Package flag layers’ include-dirs reach only the declaring package’s own compiles: the producer-only include form (contracts section 2); there is no per-target one.

Blocked workflow. None left from stage 02: Boost.Stacktrace and Boost.Contract consumers link the compiled archive without defining a configuration macro, and Boost.Atomic and Boost.Log build their SSE/AVX sources on x86 (ports/boost-*/1.92.0/cabin.toml).

Necessary changes. Interface definitions; a producer-only include form; propagation driven by edge visibility; the final-link closure kept separate from compile visibility with order preserved; per-source options; conflicting-definition diagnostics; the same interface carried through package metadata, planning, lowering, and compile_commands.json. Migration of the private-edge include leak (contracts section 2).

Acceptance. C and C++, header-only and static libraries, public/private diamonds, frameworks, static private link requirements, definition conflicts; a Stacktrace consumer that links without defining any configuration macro.

Representative projects. boost-stacktrace, boost-contract, boost-atomic, boost-log, libpng -> zlib, spdlog -> fmt.

3. System providers

Stage 06; validated in 12-14.

Today.

  • { version = "...", system = true } names a pkg-config module (cabin-manifest/src/parse/dependency.rs); pkg-config is the only probe (cabin-system-deps).
  • Only primary packages are probed (cabin/src/cli/system_deps.rs, collect_active_system_deps); results merge into the declaring package’s own flags, and --libs lands in that package’s ldflags, which reaches only its own executable links.
  • Probing is refused on MSVC (ensure_dialect_supports_system_deps).
  • [patch] accepts only a path source (cabin-core/src/patch.rs, PatchSource); nothing lets a consumer swap a Cabin package for an installed copy.
  • Ports may not declare system = true (xtask-port-publish, enforced and documented in foundation ports); no port has a system/vendored feature.

Blocked workflow. An application that depends on cabin-ports/zlib cannot build against the distribution’s zlib without editing every manifest on the path. Boost.MySQL, Boost.Redis, Boost.Compute, Boost.Python, and Boost.MPI are blocked because their SDKs can be neither ports nor declared system dependencies (Boost ledger).

Necessary changes. Root-owned provider policy and its precedence against [patch], source replacement, and config; per-domain provider consistency; interface modeling (version, targets, components, usage requirements); transitive application; static/shared pkg-config semantics; ABI-appropriate MSVC discovery; a decided migration for system = true.

Acceptance. One unchanged consumer switched between source and system providers by root policy alone; transitive diamond; absent and mismatched versions; multiple components; a cross sysroot; an MSVC-compatible Windows library; no silent fallback.

Representative projects. zlib, then OpenSSL (stage 12), Python and MPI (stage 13).

4. Code generation

Stage 07; build-tool foundation in 04; validated in 08, 10, 12-14.

Today.

  • The action set is closed: compile, syntax check, assemble, archive, link (cabin-driver/src/action.rs, BuildAction; cabin-driver/src/lower.rs). A compile edge’s only explicit input is its source, and every edge’s one implicit input is the toolchain stamp (cabin-ninja/src/writer.rs, TOOLCHAIN_STAMP).
  • build.ninja is rewritten on every build; incremental correctness rests on that, Ninja’s command-line tracking, compiler depfiles, and the stamp, which changes with the detected compilers’ and archiver’s versions. BuildConfiguration::fingerprint (cabin-core/src/config.rs) is computed but not used by the planner.
  • Dependency kinds are normal, dev, and build (cabin-core/src/model.rs, DependencyKind). A build dependency’s default targets build for the build machine, but no action runs them: a project runs its generator itself, between builds.
  • Ports avoid generation: libpng copies a prebuilt pnglibconf.h, catch2, sqlite3, and miniz use amalgamations, boost-log drops the Windows event log (needs mc.exe).

Blocked workflow. A project that uses Protocol Buffers cannot run protoc from a dependency and compile its output; no configure-style header can be generated; flex/bison, Qt moc, and mc.exe workflows are impossible.

Necessary changes. Tool dependencies and build-context units (stage 04); generated-file actions with declared inputs, outputs, environment, and ordering; configuration templates and compile checks (with stage 05 probes); root execution policy (contracts section 4); generated public headers in the usage interface.

Acceptance. A small C generator, then real protoc -> generated C++ -> runtime -> run, with tool/runtime version agreement; multiple outputs and discovered inputs; Windows mc.exe; clean, parallel, no-op, input-change, tool-change, and failure-recovery builds.

Representative projects. Protocol Buffers, a flex/bison fixture, boost-log event log, Qt moc (with stage 08).

5. Cross-compilation

Stages 04 and 05; validated in 06-10, 12-14.

Today.

  • cfg(...) predicates evaluate for the --target triple (cabin-core/src/condition.rs, TargetPlatform::from_triple), or without it for TargetPlatform::current(), built from std::env::consts: there env follows the OS, abi is always unknown, and target is <arch>-<family>-<os>, not a real triple.
  • --target <triple> builds under <build-dir>/<triple>/, passes Clang --target=, and checks that GCC, MSVC, or clang-cl already builds for the triple (cabin-toolchain/src/target.rs). For another machine, cabin run and cabin test run executables only through the [runner] configured for the triple (cabin-config), and the build machine’s plain pkg-config is refused.
  • [build-dependencies] build in a second context for the build machine, under <profile>/build-machine/, with their own features, flags, cfg(...) evaluation, and links check (cabin/src/cli/build_prep.rs, plan_build_context). Both contexts share the detected compilers, so when --target names another machine only Clang-family compilers build them; GCC, MSVC, and clang-cl are refused (check_build_context_compilers).
  • Clang’s Target: line and the MSVC banner’s architecture fill CompilerIdentity.target; a native build warns when it names another machine.
  • MSVC discovery uses Cabin’s own architecture (cabin-toolchain/src/msvc.rs); MASM is always ml64.exe, x64 only (cabin-driver/src/dialect.rs, Dialect::assembler).
  • --sysroot or the [sysroot] entry for --target reaches every GCC and Clang command of the target context, and Apple Clang builds name the platform’s SDK (cabin-toolchain/src/sysroot.rs); the build context keeps the compilers’ default sysroot.

Blocked workflow. Building an aarch64 Linux binary from an x86-64 machine, or a Windows ARM64 binary from x64, with the build tools running natively.

Necessary changes. --target <triple>; per-context platform and toolchain; diagnostics for a requested target that contradicts the compiler’s actual target or the sysroot; tool dependencies on the build machine; per-unit output separation; never running target binaries implicitly; host pkg-config never contaminating a target sysroot.

Acceptance. A build-machine C generator whose output compiles into a program for a different target; one source used in both contexts with different features; native behavior unchanged; verified cross combinations recorded.

Representative projects. A generator fixture, then Protocol Buffers (stage 07) and a cross Abseil/protobuf build (stage 14).

6. Library variants

Stage 08; validated in 09, 12-14.

Today.

  • TargetKind is library (static archive), header-only, executable, test, example (cabin-core/src/model.rs); static library names follow the dialect, lib<name>.a or <name>.lib (cabin-driver/src/dialect.rs).
  • Profiles select optimization, debug information, and assertions (BuiltinProfile::defaults, lowered by the driver) and separate output directories; nothing selects -fPIC, /MD or /MT, or a library variant, and there is no shared or module kind.
  • Features are additive and cannot change a target’s kind (features).

Blocked workflow. Producing a shared library or a Python extension module; linking a C++ shared library that embeds a private static dependency; consuming a DLL from a Windows executable.

Necessary changes. Static/shared selection with a compatible default for library; typed ELF, Mach-O, and Windows artifact behavior (SONAME, install names, rpaths, import libraries, exports); PIC; runtime and CRT settings; loadable-module naming; runtime discovery for cabin run and cabin test.

Acceptance. Stage 08’s demonstration: a C shared library used from C and C++, a C++ shared library with a private static dependency, static/shared switching, a Windows DLL consumer, ELF and Mach-O runtime lookup, a loaded module, a small QtCore program with moc.

Representative projects. zlib shared and static, QtCore, Boost.Python extension (stage 13).

7. Installation and interoperability

Stage 09; validated in 10 and 14.

Today. No install command (cabin/src/cli/mod.rs, Command); nothing writes .pc or CMake package files; compile_commands.json is written beside build.ninja; cabin metadata reports no artifact paths; the build-directory layout is described but not declared stable.

Blocked workflow. A CMake project cannot find_package a library built by Cabin, and a pkg-config consumer has no .pc file.

Necessary changes. Install selection, prefixes, and staging; CMake Config/Targets/version files with IMPORTED targets; pkg-config files with Requires.private / Libs.private; build-tree versus install-tree exports; relocation; safe handling of destination escapes and collisions; diagnostics for requirements an export cannot represent.

Acceptance. Install C and C++ libraries, then build and run a separate CMake consumer through find_package without hand-written flags; move the prefix and repeat with the source tree gone; pkg-config consumers for static and shared configurations.

Representative projects. zlib, fmt, a generated-header library from stage 07.

8. C++ modules

Stage 10; validated in 14.

Today. No module support: source classification accepts .c, .cc, .cpp, .cxx, .c++, .C, .S, and .asm (cabin-core/src/source_language.rs); no scanner, dyndep, or BMI handling; build.ninja already requires Ninja 1.10, which has dyndep. The fmt port leaves out its module unit. The standard-compatibility spec explicitly does not model BMI compatibility. Issue #1505 asks for module support.

Blocked workflow. A package exporting a named module that another package imports.

Necessary changes. Module source classification; scanning with the compilers’ own scanners (P1689 where supported) under the same configuration as compilation; dyndep ordering; BMIs as configuration-sensitive products; module visibility through the usage contract; import std and header units as separate capabilities; scaffolding and a support matrix.

Acceptance. Real modular projects with cross-package imports, partitions, generated interfaces, mixed translation units, incremental import changes, and installed consumption, on the compiler versions recorded as verified.

Representative projects. The fmt module unit, a scaffolded module package.

9. Toolchains

Stages 05 and 04; used by 06-14.

Today.

  • Implemented and not missing: GCC, Clang, Apple Clang, MSVC, and clang-cl detection (cabin-core/src/compiler/identity.rs); MSVC environment discovery (cabin-toolchain/src/msvc.rs); MASM and GNU assembly; per-slot precedence CLI > env > config > cfg table > manifest > default (cabin-toolchain/src/resolve.rs); compiler wrappers; compiler-conditioned flag layers; a C or C++ link driver chosen from the linked objects (link_driver_language).
  • Detection runs --version, and GCC’s -dumpmachine under --target. The fingerprint hashes tool specs and the --target triple, not resolved paths or versions; the toolchain stamp beside build.ninja records versions, so a compiler whose version changes rebuilds at the same path. MinGW is best-effort. A sysroot is explicit (--sysroot, [sysroot]) or, for Apple Clang only, the SDK SDKROOT or xcrun names; metadata reports which (toolchain.sysroot.source).

Blocked workflow. Generating a configuration header from a compile check; a supported MinGW toolchain.

Necessary changes. Typed compile and link probes built with the selected toolchain, target, sysroot, and flags, and reused, if at all, only under a key of those inputs; sysroot and SDK selection with explicit choices over discovery; configured choices distinguished from detected facts in diagnostics and metadata; MinGW promoted only with CI evidence.

Acceptance. C-only, C++, and mixed targets; compiler mismatch; a sysroot that compiles but cannot link; SDK change and probe invalidation; MinGW/MSVC ABI mismatch; paths with spaces.

Representative projects. The stage 04 cross fixture; OpenSSL’s configuration (stage 12).

10. Developer-facing CLI workflows

Stage 11; integrated with 04, 08, 10; validated in 14.

Today.

  • Visible commands: init, new, add, remove, build, check, clean, run, test, update, publish, fmt, tidy, version; hidden: metadata, resolve, fetch, vendor, tree, explain, package, login, logout, yank, compgen, mangen (cabin/src/cli/mod.rs).
  • cabin run --bin and cabin test --test select targets; nothing selects an example or a single build target; cabin test runs up to --test-jobs executables at once with an optional --test-timeout, its -j limits the build only (cabin-test, run_tests), it passes the arguments after -- to each executable, and --junit writes a report with one test case per executable (junit_report).

Blocked workflow. Running one example; building one target.

Necessary changes. Example and target selection kept distinct from --target; dev dependencies activated only for the selected workflow; shared-runtime and cross-runner integration; help, completions, man pages, and docs.

Acceptance. Stage 11’s validation list (argv edge cases, parallel success and failure, large and invalid-UTF-8 output, timeouts and cleanup, valid XML, unknown/gated/ambiguous targets, dev dependency selection, examples, cross runners).

Representative projects. examples/unit-test-gtest, examples/library-with-tests.

Scope documents aligned in stage 00

Stage 00 moved the requested work out of the deferred scope without marking anything implemented: docs/architecture.md (“Scope and limitations” and the invariants note), the root AGENTS.md deferred-feature rule, the deferred lists in toolchains and profiles, the port policy in foundation ports and ports/README.md, and the Boost ledger’s gap table.

Statements that stay true until a stage changes the behavior, and that stage must update:

Documentation discrepancies

Found while gathering evidence. Fixed means fixed in stage 00; the others name the stage or change expected to fix them.

WhereSaysCodeDisposition
Toolchains deferred listcompiler-conditional flags are deferredcc, cxx, cc_version, cxx_version flag-table keys existfixed
Toolchains deferred listSARIF/JSON capabilities “are detected”no such capability fieldfixed
docs/architecture.md C++ semantic invariantssystem-dependency flags “are wired into consumers that link the producing target”; a header-only link interface is “include-dir + system deps”primary packages only; --libs reaches the declaring package’s own links; link-libs is what propagatesfixed (wording); behavior is stage 06
Manifest, cabin-core TargetDep docpublic “does not change how anything builds”; consumed by nothingit drives the post-resolution standard checkfixed in stage 02
Standard-compatibility spec D5a private edge does not expose the dependency to the consumer’s own consumersinclude directories follow private edges to every transitive consumerfixed in stage 02 (the code now follows D5)
Profiles, build_flags.rslink-libs belong to a target’s objectscollected per packagefixed in stage 02 (docs say per package)
Targets, TargetKind doca library is a .a archive<name>.lib on MSVCstage 08
Target dependenciescfg(env = "musl") example; target documented as a triple in condition.rsenv always follows the OS; target is <arch>-<family>-<os>stage 04 under --target; the build machine’s own values are unchanged
Toolchains, compiler cache, architecturethe wrapper’s source is fingerprintedonly kind, spec, and version are hashedfixed in stage 04 (docs)
Toolchainsthe fingerprint reaches cabin run and cabin testthey discard itfixed in stage 04 (docs)
Profilesoutput layout packages/<package>/<target>/...artifacts in packages/<pkg>/, objects in packages/<pkg>/obj/<target>/stage 09 (layout contract)
Metadata, tree, explainmetadata is byte-identical across machinesit includes resolved tool paths and pkg-config outputstage 05 (configured versus detected)
System dependencies, docs/architecture.md (cabin-system-deps)only the selected members are primary; probes fire only for a selected primary packageevery primary package is probed; only the MSVC gate narrows to the selection (not run)stage 06
LockfileHTTP index, authentication, registry server, and publishing are unsupported; workspace lockfiles without a root package are unsupportedall existfixed in stage 01
Lockfile formatdependencies are transitive names; source includes local provenancedirect resolved index dependencies; only "index" parsesfixed in stage 01
Package indexresolution never reads revisionslocked validation reads revisionsfixed in stage 03
cabin resolve helpselection only narrows diagnostics; resolves “against a local JSON package index”selection scopes resolution (workspaces); --index-url worksstage 11
Cargo-inspired interfacesubcommand table; -j “identical”omits login, logout, yank, resolve, compgen, mangen; test has no -jstage 11 (test -j fixed in its second change)
docs/architecture.mdSystemDependencyProbeReportthe type is SystemDependencyResolutionstage 06

Port corrections waiting on stage 03

None: since stage 03 a respin may change resolution metadata, so no correction waits on it.

Progress record

Adopted contracts. Stage 00 adopted the shared contracts: package versus build-unit identity, usage-requirement kinds and edge rules, root-selected providers, generated actions with root execution policy, and per-revision metadata as stage 03’s target.

Stage 04 adopts one lockfile for every platform: resolution treats every cfg(...) declaration as active, the workspace loader loads every platform’s path dependencies, and builds evaluate conditions for the platform they build for. Its pull requests, in order: that lockfile; --target <triple> with per-target output directories, Clang’s --target=, and compiler-target diagnostics; a toolchain identity stamp every action depends on; [build-dependencies] with build-context units, per-context features, and per-context links; runners for cabin run and cabin test, after stage 11’s test-concurrency change.

Stage 05’s pull requests, in order: sysroots and the Apple SDK; typed compile, link, and run probes; a toolchain per context; MinGW promoted on build, link, run, and CI evidence.

Changes. Stage 00: this ledger, the contracts page, and the scope alignment listed above. Stage 00 counts as adopted once this ledger is on main.

Stage 01: feature-aware dependency resolution (area 1, “Today”). Every command that resolves - resolve, update, fetch, vendor, build, run, test - shares one selection over file and sparse-HTTP indexes; metadata and explain build-config report configurations from the local feature resolution, and tree and explain package leave out optional dependencies no enabled feature turns on. Patch forks reached through registry edges are ordinary single-version candidates, which replaced the activation loop, its residue packages, and the build-time-only collision check for fork optional dependencies; the transitive fork version check is now a resolution conflict. The lockfile format is unchanged; lockfile documents how features meet its contents. Contracts section 1’s seam for stage 04: the resolver’s feature packages (Node::Feature) and its single platform are what per-context resolution splits, while Node::Package stays shared, keeping one version per name across contexts.

Stage 02, first change: edge visibility governs compile interfaces (area 2, “Today”), with target interface-defines, header-only defines rejected, and definition conflicts diagnosed. The migration audit found one private port edge whose consumers relied on the old leak through their own sources, not through the producer’s headers: libpng -> zlib stays private (png.h does not include zlib.h), and the libpng-usage example now declares the zlib it includes; boost-iostreams -> zlib is the only other private port edge. No port changed.

Stage 02, second change: the final link orders link-libs by the dependency graph, as its archives already were, instead of a stack walk that put a sibling-reached dependency’s libraries first; first-occurrence deduplication, which could move a library ahead of one that needs it, now applies to Apple links only, whose linker resolves in any order (a reversed -l order links on macOS; a repeated -l warns). Typed frameworks join every flag layer, kept for registry packages like link-libs and ignored off Apple. No linker groups: the target graph is acyclic.

Stage 02, third change: target instruction-sets give one source typed x86 instruction sets, as upstream CMake’s per-source COMPILE_FLAGS do for Boost.Atomic and Boost.Log. cl.exe gets no flag, as upstream passes none on x64; clang-cl gets -m<name>, since it rejects such an intrinsic without one (clang-cl 23: “always_inline function ‘_mm256_add_epi32’ requires target feature ‘avx2’”). Off x86 the table is ignored, like frameworks off Apple.

Stage 02, fourth change: the four Boost ports build as upstream’s CMake does. boost-stacktrace and boost-contract give consumers upstream’s public *_STATIC_LINK through interface-defines; boost-contract becomes a compiled library, whose source compiles to nothing without that macro. boost-atomic and boost-log add their SSE2/SSE4.1 and SSSE3/AVX2 sources on x86 with their instruction-sets and upstream’s USE_* definitions; both libraries pick a variant by CPUID at run time unless the compiler’s baseline already has its instruction set. None of the four was published, so each changed in place.

Stage 03, first change: a packaging revision whose dependencies, features, standards, or links differ from the version’s carries its own in a resolution object, in a schema-2 index document that earlier clients refuse (they accept schema 1 only); a schema-1 file or a current revision carrying one is invalid. Every pin the lockfile mode keeps resolves with its revision’s metadata: the CLI applies it before each solve round, so the sparse-HTTP lazy loader, the lockfile writer, and the fetch plan read the same graph. Plain resolution never moves a pin to another revision of its version; cabin update --package does. The file registry records the earlier revisions’ resolution when a respin changes those fields, instead of refusing it, so a links-only respin no longer stamps its table onto earlier revisions; cabin vendor lifts the vendored revision’s into the version fields. The hosted registry is unchanged: it refuses such respins and serves no resolution until the second change.

Stage 03, second change: the hosted registry accepts a respin that changes resolution metadata - the transactional guards, their storage-accounting mirrors, and the preflight read that shaped their 409 are gone - and its package document gives each superseded verified revision whose stored dependencies, features, standards, or links differ from the current revision’s its own resolution, becoming schema 2. Legacy rows keep their meaning: every published version has one revision (production index and public stats read on 2026-10-30: 17 packages, 17 versions), so no served document changes. The links table order check stays, now because the read plane serves stored tables verbatim.

Stage 04, first change: the lockfile serves every platform. The resolver, the sparse-HTTP prefetch walk, the local versioned-dependency collectors, and dependency resolution’s feature pass (cabin_feature::resolve_features with no platform) include every cfg(...) declaration; the workspace loader loads a conditional path dependency everywhere and keeps its condition on the edge, and the build planner resolves target dependencies only through edges matching the platform it builds for. A lockfile written earlier lacks the dependencies gated to other platforms, and --locked reports it out of date until one unlocked resolution adds them; a build now fetches those packages too.

Stage 04, second change: --target <triple> on build, check, run, test, metadata, and explain selects the platform cfg(...) conditions evaluate for, classified by TargetPlatform::from_triple, and the build writes under <build-dir>/<triple>/. Clang-family compilers get --target=<triple> on every compile, assemble, and link; GCC (-dumpmachine), MSVC, and clang-cl (the banner, whose architecture now fills CompilerIdentity.target) must already build the triple’s machine, and C and C++ compilers from different families are refused. Without --target, a compiler for another machine draws a warning. cabin run and cabin test build for another machine, then fail naming what they built; such a build refuses the build machine’s pkg-config unless PKG_CONFIG_LIBDIR or CABIN_PKG_CONFIG is set; the configuration fingerprint folds the triple in; the link-failure hint evaluates edges for the build’s platform; and cabin clean --target cleans one triple’s outputs.

Stage 04, third change: cabin build, check, run, and test write each detected tool’s identity (family, version, --version line, target; not its path, which the commands already carry) beside build.ninja as cabin-toolchain.json, rewritten only when it changes, and every Ninja edge depends on it, so a compiler whose reported version changes at the same path rebuilds everything; before, only a changed command line did. The first build of an existing build directory therefore rebuilds once, and cabin package leaves the file out as it does build.ninja. The fingerprint documentation now matches the code: the wrapper’s source is not hashed, and cabin run and cabin test do not consume the fingerprint.

Stage 04, fourth change: [build-dependencies], with their cfg(...) and workspace forms, declare packages built for the build machine (contracts section 1). They resolve with the normal ones into the same lockfile, so fetch, vendor, --locked, and --offline cover them, and a local package declaring one name in both kinds offers the resolver both requirements. cabin_feature::resolve_build_context_features resolves the build context’s features from the build edges of the target context’s packages, apart from the target context’s; the resolver checks links per context (ResolveInput::link_domains); and build, run, and test append a second plan for the build machine under <profile>/build-machine/, with the build machine’s per-package flags and cfg(...) evaluation. cabin check checks only the selected packages’ own sources, so it plans no build context. Under --target, the build context passes no --target= and receives no environment flags, and a toolchain that builds only its target is refused when that target is another machine. cabin package refuses build dependencies until index metadata carries them, and system = true is refused in the build context.

Stage 04, fifth change: the [runner] config table maps a target triple to a program and its leading arguments, and cabin run --target and cabin test --target run each executable through the triple’s entry, without a shell, before falling back to running it directly on the build machine or refusing; the refusal now names the table. The executable’s path follows the runner’s arguments, then the executable’s own. A relative program path resolves against the current directory, as a [toolchain] path does, and a runner that cannot start is named in the test result.

Stage 05, first change: --sysroot <dir>, or the top-level [sysroot] config entry for the --target triple, passes GCC and Clang --sysroot=<dir>, or -isysroot <dir> for an Apple triple, on every compile, assemble, and link command of the target context (cabin-driver/src/lower.rs); --sysroot wins, and a sysroot with MSVC or clang-cl is an error (cabin_toolchain::explicit_sysroot). Without a choice, an Apple Clang build for macOS or iOS names the platform’s SDK, an absolute SDKROOT or what xcrun --show-sdk-path names (cabin_toolchain::discover_apple_sdk), so an SDK switch reaches the command lines and rebuilds; cabin tidy’s compile database names it too. cabin metadata reports toolchain.sysroot with a cli, config-file, or detected source, and the entries under config.sysroots; the configuration fingerprint hashes an explicit sysroot as written and leaves a detected SDK out, as it leaves out resolved tool paths. The build context keeps the compilers’ default.

Stage 05, second change: cabin_toolchain::probe builds a probe program through cabin-driver’s lowering, with the compiler, --target=, sysroot, GNU mode, and flags the caller passes, from the directory the build runs commands in, and answers a header, type, symbol, compile, link, or run check with Answer::Yes, Answer::No naming the failing step and its output, or Answer::Unknown when the program built but nothing can run it (no runner for a target the build machine cannot run); a compiler or linker that cannot start, crashes, is killed, or times out is a ProbeError. When cabin build, run, or test fails with a configured sysroot, the CLI probes a minimal program per link driver with the plan’s flags and names the sysroot in a help: when it compiles but does not link, or does not compile. Probe results are not reused: the only consumer runs after a failure, where a reused answer could outlive a sysroot fixed in place, so there is nothing to invalidate yet. Stage 07’s configuration checks call probe with a target’s effective standard, GNU mode, CompileArguments, and link flags, and Execution from [runner] and TargetPlatform::runs_on_build_machine; an Unknown must not become a defined macro. Reusing answers across builds needs a key over the compiler path and version banner, dialect, target, sysroot, standard, flags, source, and execution; a sysroot’s contents are not in it.

Stage 11, first change (independent CLI work, ahead of stages 04-10): cabin test -- <args> passes the arguments after -- to every selected test executable as an OsString list, without a shell; a bare argument without -- is rejected rather than read as a filter.

Stage 11, second change: cabin test --test-jobs <N> (default 1) runs that many test executables at once, and -j limits the build only, through the same precedence as cabin build. One coordinator thread owns every child and the sink: with one job, output streams live as before; with more, each executable’s output is handed over whole when it finishes. --test-timeout <SECONDS> kills the test process (not processes it started); a test that cannot start is a failed result rather than an aborted run; once a test exits, output a leftover process holds open is read for one more second and the result is marked cut off. Every test runs; the runner kills the tests still running when it fails itself (a sink or read error), while Ctrl-C keeps the terminal’s default, reaching the tests too. Tests get an empty stdin.

Stage 11, third change: cabin test --junit <PATH> writes a JUnit report through quick-junit once the tests have run: one testsuite per package and one testcase per executable, since Cabin observes no test cases inside one, each with its run time and lossily decoded output; an exit code, a signal, or a timeout is a failure, a test that cannot start an error, and cut-off output an output-cut-off property.

Validation. Stage 00 changes documentation only; the website gate covers it. Stage 01: resolver unit tests for each acceptance scenario (feature chain, defaults diamond, older candidate for a feature, optional-edge backtracking, non-matching cfg edge, unknown feature, locked replay, lazy metadata), CLI tests for the registry chain built and replayed --frozen, an inactive optional dependency absent from file and HTTP registries, a registry-reached path patch enabling its optional versioned dependency, and dependency configurations in metadata; cargo ci on macOS. Not run: Linux and Windows locally (CI covers them), and no real-project demonstration - stage 12 validates. Stage 02, first change: planner tests for private and public edges, public chains and diamonds, definition conflicts (both paths, escaping, check pruning), own-define override, and the visible set feeding the build-time standard check; manifest tests for the interface-defines grammar and kinds; the libpng-usage and png-info examples built and run; cargo ci on macOS. Stage 02, second change: planner tests for link order through a private diamond edge and a multi-target package, repetition, Apple deduplication, and frameworks on Apple and elsewhere, each shown to fail when the rule it covers is broken; lowering, manifest, flag-layer, and fingerprint tests; a CoreFoundation consumer built and run on macOS (arm64) through a path dependency’s frameworks, which fails to link without it. Not run: iOS. Stage 02, third change: manifest, Package validation, planner (C and C++, sibling and consumer compiles untouched, non-x86, cl.exe and clang-cl mixed either way), and lowering tests; clang-cl 23 and Clang cross-compiling SSE4.1 and AVX2 intrinsics for x86_64 Windows and Linux, which fail without -m<name>; the simd-dispatch example built and run on macOS (arm64, generic path). Its CLI test runs the SSE4.1 variant on CI’s x86_64 Linux (whose compiler rejects the source without the flag) and Windows x64 (cl.exe, no flag). Not run: an x86 build here (no Rosetta), and clang-cl end to end, which no CI job uses. Stage 02, fourth change: the Stacktrace and Contract consumer tests define nothing and fail to compile when the interface definition stops arriving; the Log test checks a 2100-byte dump, long enough for either SIMD routine, against the generic format. Below an SSE4.1 baseline, Atomic’s lock_pool.cpp dispatch references the search variants, so a missing one fails the test’s link; running one takes a dozen addresses waited on in one lock-pool bucket, which no test arranges. The publisher’s dry run of each port’s closure on macOS (arm64, baseline sources); Clang compiling the four ISA sources for x86_64 at the x86-64 baseline: the SSE4.1, SSSE3, and AVX2 sources are rejected without their -m<name>, and the SSE2 one, inside that baseline, compiles either way; CI’s port dry run builds and runs the tests on x86_64 Linux (GCC) and Windows x64 (cl.exe). Not run: an x86 run here. Stage 03, first change: the decisive regression as a CLI test over a file registry, with acme/fmt r1 lacking acme/b and r2 adding it: the r1 lockfile stays byte-identical through plain resolution, fetch --locked over the file and HTTP paths fetches r1’s archive with no acme/b to lock, a vendored copy carries r1’s dependencies and fetches offline under --locked, and cabin update locks acme/b with r2’s checksum; removing the per-pin metadata selection fails it at the unchanged-lockfile check. Resolver unit tests for the four modes; loader tests for parsing, selection, and both rejections; file-registry and vendor unit tests for recording and lifting. Stage 03, second change: a registry unit test composes a document whose superseded revision differs (schema 2, its resolution, none on the current or an agreeing revision) and one whose revisions agree (schema 1); a SQL test inserts and counts a dependency-changing respin under the opt-in, which the removed guard refused, and reads both stored documents back through the listing the read plane composes from. Stage 04, first change: a CLI test locks a registry dependency gated to another OS and one reached through a path dependency gated the same way, and fails with the loader’s platform filter restored; resolver, feature-resolver, patch-validation, planner, and tree/explain unit tests for the every-platform resolution and the per-platform edge filter; cabin tidy and cabin build CLI tests, each shown to fail without its fix, for a path dependency gated to another OS and a disabled optional one whose registry dependencies resolution skips. Stage 11, first change: clap tests for the separator (empty and, on Unix, non-UTF-8 values, a second --, flag-shaped values, a bare argument rejected), and two C test targets printing their argv (empty, spaced, quoted, and Unicode arguments) end to end on macOS, with CI’s Linux and Windows legs (a narrow main on Windows reads the ANSI code page, so Unicode is checked off Windows only). Stage 11, second change: runner tests on Unix for two tests that pass only when run together beside a failing one, a timeout kill, a leftover process holding the pipes, a mebibyte per stream plus a non-UTF-8 byte handed over whole from two concurrent tests, output a slow sink still holds after the test exits, and a sink failure killing a running test, the last three guards shown to fail when removed; spawn failures on every platform; a CLI test where a C test passes only beside a hung C++ test that times out under --test-jobs 2 --test-timeout 5, shown to fail with one job (macOS here, CI’s Linux and Windows legs), and that -j reaches Ninja while the same flag after -- does not.

Stage 04, second change: Apple Clang 21 on macOS (arm64) cross-builds a C and a C++ executable for x86_64-apple-macos13, each an x86_64 Mach-O carrying its cfg(arch = "x86_64") source, beside an unchanged native build that runs; the test fails with --target= removed. CLI tests: cabin run --target and cabin test --target build and run nothing, while a triple naming the build machine runs; a fake GCC whose -dumpmachine names another machine fails the build; metadata --target evaluates conditions for a riscv64 triple; the pkg-config refusal and both ways past it; cabin clean --target. Unit tests for triple classification (Arm’s none vendor, distro triples without an env), MSVC banners, compiler-target checks, the planner and lowering of --target=, and the fingerprint; cargo ci on macOS. Verified cross combination: Apple Clang, macOS arm64 to macOS x86_64, C and C++. Not run: a Linux cross toolchain (none here; CI’s Linux and Windows legs run the portable tests), MSVC or clang-cl cross, and running the x86_64 output (no Rosetta).

Stage 04, third change: a CLI test replaces a C and a C++ compiler at the same paths with ones reporting another version, and both executables relink while unchanged compilers leave them untouched and a C compiler moved elsewhere leaves the C++ program untouched; each part fails with its guard removed (the stamp input on the edges, paths left out of the identity). A unit test ignores the name GCC was invoked as (cc or gcc). Writer tests for the stamp on C and C++ compile, check, assemble, archive, and link edges and its rewrite only on a changed identity; cargo ci on macOS.

Stage 04, fourth change: a CLI test builds a C generator for the build machine whose dependency util has feature loud only in the build context, runs it by hand, and compiles its output into a C++ program whose own util lacks the feature; with Apple Clang, the same project cross-builds for x86_64-apple-macos13 with an arm64 generator and an x86_64 program carrying the generated text. CLI tests: per-context links for path and registry packages, a registry build dependency locked, vendored, and built offline, the packaging refusal, and a cross GCC refused by build but not by check. Unit tests: the manifest kind, build-context features (the declaring condition for the target platform, the build context’s own for the build machine), per-domain links in the resolver and the seed, the build-context compiler check, both kinds’ requirements for one name, and scoped clean. Each guard - the feature context, both cfg(...) platforms, the resolver’s domains, the registry-only domain, the fork and dev-dependency domain roots, the merged requirements, the output directory, and check skipping the build context - was shown to fail its test when removed; cargo ci on macOS. Verified cross combination: Apple Clang, a macOS arm64 generator feeding a macOS x86_64 C and C++ program. Not run: a build context on Linux or Windows outside CI’s portable tests.

Stage 04, fifth change: with Apple Clang, a CLI test cross-builds for x86_64-apple-macos13 and hands the C program and the C test to a runner, named by a path relative to the current directory, that records its arguments, through cabin run and cabin test; a Unix unit test hands a test file that is not executable to a runner that prints its arguments, and another names a runner that cannot start. Removing the runner from the test runner’s spawn, either command’s runner check before its refusal, the relative path’s resolution, or the runner’s name in the spawn error fails these tests; config unit tests for the table and its rejections; cargo ci on macOS. Verified cross combination: Apple Clang, macOS arm64 to macOS x86_64 through a runner. Not run: an emulator such as qemu-user, or Rosetta (not installed here).

Stage 05, first change: on macOS, a CLI test builds a C and a C++ executable with the SDK xcrun names, then through --sysroot under a path with spaces, then with SDKROOT naming another of Xcode’s SDK paths; each switch relinks both, both run, an unchanged rebuild relinks nothing, and cabin tidy keeps the SDK in the compile database. Another reads toolchain.sysroot for a detected SDK, a package-config entry under --target, --sysroot over that entry, and an entry from a relative CABIN_CONFIG, which must come out absolute; explain build-config records a choice as written and no detected SDK. A Linux CLI test builds both through --sysroot naming / under a path with spaces. Unit tests: the table’s rejections and relative paths, both lowered spellings, the MSVC-dialect refusal, the SDK xcrun names per platform (Intel iOS triples included), SDKROOT precedence, and the fingerprint following the written choice. Removing the Apple lowering, the SDKROOT read, tidy’s SDK, or the absolute path fails the macOS CLI tests; cargo ci on macOS. Not run: a sysroot for another machine (none installed here), the Linux test outside CI, and iOS SDKs.

Stage 05, second change: unit tests drive probe with a scripted runner: each check stops after its last step, a failing step is the answer and names the step, a crashing probe program is a no, a compiler that cannot start or is killed is an error, a run with nothing to run it is Unknown once the program builds, and the commands carry the target, sysroot, flags, and runner. Integration tests ask the build machine’s default toolchain C and C++ checks with each answer and step, and name a compiler that does not exist. A macOS CLI test builds against a sysroot holding only the SDK’s headers and reads the C and C++ help: lines. Run on macOS (Apple Clang); Linux and Windows (MSVC) only in CI. No Linux fixture for the hint: GCC keeps library paths relative to its own installation under a sysroot, so a headers-only sysroot can still link there.

Stage 11, third change: a unit test reads back a serialized report of two packages covering every outcome, with markup, a non-UTF-8 byte, and U+FFFE and U+FFFF in the output (the last two shown to reach the report unreplaced without the fix); the CLI concurrency test checks the report it writes despite its timed-out test.

Unfinished. Stage 05’s remaining changes and stages 06-14. Follow-ups found in stage 01, outside its scope: xtask-port-publish still orders publication by each port’s own default feature closure (area 1, “Blocked workflow”); a transitively requested unknown feature is reported as a resolution conflict naming <package>[<feature>] rather than with the direct-request diagnostic; metadata and explain build-config see local edges only, so a registry edge’s request on a member or fork is missing from their configuration report; tree and explain package filter by included package, not by edge, so a disabled optional edge onto a package another edge includes still shows; over sparse HTTP, each optional dependency a feature enables costs one fetch round and re-solve.

Follow-ups found in stage 02: a missing-header link diagnostic does not suggest a public edge or a direct dependency; the libpng port’s comment claims png.h includes zlib.h, a comment-only packaging-revision correction for a later respin; BuildConfiguration::fingerprint covers no target-level field other than instruction-sets (sources, defines, interface-defines, include-dirs, deps), short of crates/AGENTS.md, though nothing keys a build on it yet; no CI job uses clang-cl.

Follow-ups found in stage 04: cabin tree and cabin tidy take no --target and follow the build machine’s edges; the build machine’s own env, abi, and target values are still derived from the OS rather than detected; GNU-dialect executables keep a bare name for a Windows target, though MinGW GCC and Clang write <name>.exe (stage 05’s MinGW work). From the fourth change: both contexts share one toolchain, so GCC, MSVC, and clang-cl cannot build with build dependencies for another machine, and environment flags do not reach the build context there (stage 05 gives each context its toolchain; render_build_ninja then needs rules per dialect, since it emits one rule set, archive and depfile handling included, from the graph’s dialect); the build context gets no system dependencies (stage 06); index metadata does not carry build dependencies, so a package declaring them cannot be published (with stage 07’s metadata); [features] cannot name a build dependency, and cabin add has no --build; resolution’s links domains over-approximate a context by following every resolved edge, including optional edges only the other context enables, a fork reached through a registry edge claims in both contexts and offers its build dependencies to the resolver as normal ones (so build dependencies only such a fork declares get no domain of their own), and the resolver’s held-back pruning stays graph-wide, while the build’s per-context check is exact; compile_commands.json lists a source built in both contexts twice; cabin tree and cabin explain walk build edges with one platform. Found alongside, in both contexts: a target’s deps entry naming a disabled optional dependency still builds and links it (cabin-build/src/planner/lowering.rs, resolve_target_dep), though features says a disabled optional dependency never links.

Handoff to stages 05 and 07

Stage 04 leaves these seams:

  • Platforms. --target is classified by TargetPlatform::from_triple; PreparedWorkspace carries the target platform, BuildContextPrep the build machine’s (cabin/src/cli/build_prep.rs). Outputs live under <build-dir>/<triple>/<profile>/, the build context’s under build-machine/ beside packages/ (cabin_build::planner::BUILD_CONTEXT_DIR).
  • Toolchains (stage 05). Both contexts share one detected toolchain, and cabin_toolchain::check_build_context_compilers refuses GCC, MSVC, and clang-cl when --target names another machine. A toolchain per context replaces that check; render_build_ninja then needs rules per dialect, since BuildGraph::append assumes one. Clang-family compilers get PlanRequest::clang_target, which the build context leaves unset. A probe result that changes commands must reach the command lines or the toolchain stamp (cabin_ninja::writer::TOOLCHAIN_STAMP), the two inputs every edge rebuilds on.
  • Running (stage 05 probes). A target executable runs only natively or through PreparedWorkspace::runner, the [runner] entry for --target; a probe that must run a program built for another machine uses the same entry or runs nothing.
  • Build-machine tools (stage 07). PlanRequest::build_context plans a context’s units under BUILD_CONTEXT_DIR, and plan_build_context appends that plan to the target’s, building the default targets of the packages build edges reach (BuildContextPrep::roots). Their features come from cabin_feature::resolve_build_context_features, and the resolver checks links per context (ResolveInput::link_domains). A build-context output runs on the build machine directly, never through a runner.

Stage 03 makes a revision’s resolution metadata dependencies, features, standards, and links. A declaration a later stage adds to index metadata that resolution reads - build or tool dependencies, providers, actions - joins that set in cabin-index (ResolutionMetadata), the file registry (resolution_fields), the hosted registry (RESOLUTION_FIELDS), and cabin vendor (rewrite_vendored_revision); otherwise an old pin resolves it with the current revision’s value.

Production rollout: the hosted change deploys from main with the registry workflow, and no stored row changes. A respin that changes resolution metadata makes its package’s document schema 2, which clients built before stage 03 refuse; no released client is affected, since 0.17.0 predates the revisions model and refuses every hosted document already.