Bitspark constellation
accepted source ↗

ADR 0009 — the pin trinity: one per-repo lock + one pin explain for the design / atlas / substrate refs a repo runs against

  • Status: accepted
  • Date: 2026-06-04
  • Refines: 0005 §4 (the toolchain-pin + repository_dispatch propagation rails)
  • Relates: 0006 (substrate channels), 0008 (the release model)
  • Tracking: #140 · depends on #41/#92 (the per-repo lockfile + docs-toolchain bundle) and #47 (gate activation)

Three pins are accreting with no shared declaration and no shared question. A repo already pins which design it draws against, and is about to pin which atlas/docs toolchain it runs and which substrate closure it resolves to — three mechanisms, three places, three cadences. Nobody can answer "given repo X, which design + atlas + substrate should it run, and is it actually on them?" That unowned, un-queryable convention is the rotting-convention failure 0001/0004 fight, displaced one level up — into the toolchain pins instead of the versions.

Context

Three independent pinning mechanisms are emerging, each added for its own axis:

  1. design — the DESIGN_REF SHA pinned in atlas's ci.yml (#125, live): the exact design commit the constellation figure is generated and figure-linted against. atlas adopts a new design deliberately, so a moving design main can't red an unrelated PR.
  2. atlas / docs toolchain — the per-repo lockfile + pinned docs-toolchain bundle (.atlas/lock.json, #41/#92, deferred): which atlas CLI + docs toolchain a repo regenerates its cross-repo artifacts with, so figure/doc generation is reproducible and not "whatever npx fetched today."
  3. substrate — the substrate subscription in a repo's atlas.json (a release id under 0004; a channel name under 0006, partly live): which version closure the repo's lanes co-resolve to.

Each was justified on its own — figure freshness, toolchain reproducibility, version co-resolution — and none is wrong. But there is no unified declaration (the three pins live in a CI env var, a not-yet-built lockfile, and a manifest field) and no unified query: nothing prints, for one repo, the trinity it should run and whether it is on it. That is two distinct gaps — a declaration gap and a diagnostic gap — and left unaddressed they reproduce the failure these ADRs exist to kill: a cross-repo invariant ("a repo runs a coherent (design, atlas, substrate) triple") that no file owns and no check enforces, so it drifts silently. The membership test holds (this spans ≥2 systems and has no single-repo owner), and #140 sanctions a dedicated pinning ADR; this is it.

Decision

Treat the three pins as one trinity, declared in one generated lock and answered by one diagnostic, kept coherent by the same fan-out that already moves substrate.

1. Name the trinity

A repo pins along three axes, and only three: design (the look & read toolchain it draws against), atlas (the build & relate toolchain it regenerates with), and substrate (the version closure it resolves to). "Which (design, atlas, substrate) does repo X run?" is the one question; the three pins are its three coordinates.

2. One generated lock is the single source of truth

The per-repo .atlas/lock.json (the lockfile #41/#92 introduce) carries all three resolved pins, not just the docs toolchain: the resolved design ref + its input digest, the atlas/ docs-toolchain digest, and the substrate channel → resolved release id + closure digest. The lock is a generated fact (the artifact-policy class): atlas lock write resolves the repo's declared subscriptions into it, --check drift-gates it, and it is never hand-edited — it sits beside constellation.json and .docs-manifest.json as generated, not authored, state.

The split is deliberate: the subscription stays declared and human (the substrate channel name in atlas.json; a repo's opt-in to design/atlas toolchain tracking); the resolution of those subscriptions into concrete digests is generated into the lock. A human names a channel; only resolution names a SHA.

3. One diagnostic answers the question

atlas pin explain --repo <name> prints the trinity for one repo: each axis's declared subscription, its resolved pin in the lock, and the actual state (on/off-channel, fresh/stale, lock-vs-live drift). With no --repo it is the constellation dashboard — every member's triple, the way substrate conformance already reports wiring. atlas pin check is the gate form (nonzero on drift), wired token-guarded into member CI alongside the dormant gates (#47). The diagnostic gap closes with a command, not a doc.

4. One fan-out keeps the three coherent

The pins do not update on three independent cadences. The same repository_dispatch + SHA-pinned reusable-workflow rails that advance a substrate channel (0005 §4 / 0006 §5) advance the design and atlas/toolchain pins too: whichever axis moved opens one auto-bump PR that rewrites the lock, gated on the consumer's own pin check. "Which design/atlas/substrate runs everywhere" becomes a coordinated, auditable move, never three hand-bumps that can disagree.

5. The lock is schema'd — no new unowned surface

.atlas/lock.json is a declaration file like the rest, so it gets a schema and joins the declaration taxonomy; fold its schema into the schema-gap work (#176) rather than shipping a fourth code-only-validated file.

Consequences

  • One place to read, one command to ask. "What does this repo run?" is the lock; "is it current?" is pin check/pin explain. The trinity stops being tribal knowledge spread across a CI env var, a manifest field, and a toolchain bundle.
  • It depends on the unbuilt pieces, by design. The lockfile + docs-toolchain bundle (#41/#92) and the gate activation (#47) are deferred; this ADR's value is that they get built unified — one lock carrying three axes, one pin surface, one fan-out — instead of a third silo bolted beside the first two. It sequences them, it does not pre-empt them.
  • Shared bot/trust surface with 0006. Auto-bumping the toolchain pins rides the same scoped fan-out identity that advances substrate channels; named here so #41/#47 build that trust surface once, for all three pins, gated on the consumer's own check (never a self-attestation).
  • atlas dogfoods first. Today's atlas-only DESIGN_REF env var (#125) is pin #1's bootstrap; this generalizes it into atlas's own lock entry before any rollout, so atlas runs the model it ships — the same discipline 0006 applies to the member wiring.
  • A real, bounded ongoing cost. A per-repo lock file and a pin command surface are new surface; but single-sourced (one generated lock, one command, one fan-out), the cost is bounded and is the price of the convention being owned instead of evangelized.

Alternatives considered

  • Document the three pins, don't unify them. A docs/ page explaining the trinity. Rejected as the whole answer: prose can neither resolve the query nor gate drift — the convention still rots between reads. (The model is now documented in docs/substrate.md and docs/model.md, but documentation is the map, not the mechanism.)
  • pin explain as a pure query over the three existing locations, no unified lock. Half the value: a query is worth shipping, but without one resolved lock the three pins still advance on three cadences and can disagree between query runs. The lock is what makes "current" a well-defined, gateable fact rather than a momentary snapshot.
  • Fold all three pins into atlas.json. Rejected: resolved digests are generated facts, not editorial judgment, so they belong in a generated lock beside constellation.json — not in the hand-authored manifest. atlas.json keeps the human subscription (the channel name); the lock holds the resolution.
  • Per-axis lockfiles (.atlas/design.lock, .atlas/substrate.lock, …). Rejected: that is the three-silo problem with more files. One lock + one fan-out is the point.

The work that realizes this ADR

Deferred to the #41/#47 convergence cycle, tracked under #140, built unified from the start:

  • .atlas/lock.json carrying all three axes + its schema (with #176), and atlas lock write|check (#41/#92) extended past the docs toolchain to the design ref and substrate channel→release.
  • atlas pin explain --repo <name> (the per-repo + dashboard diagnostic) and atlas pin check (the drift gate, activated with #47).
  • The design/atlas toolchain pins join the substrate fan-out — one auto-bump PR for whichever axis moved, gated on the consumer's own check (#41/#47, the 0006 §5 rails). The atlas SENDER half is live (#182): atlas dispatch pin-plan computes the design + atlas bump from atlas's own committed .atlas/lock.json (the trinity authority, #255), and the pin-dispatch.yml caller hands the bitspark.pin.bumped payload (a new kind:"pin" branch of the shared dispatch-payload schema) to the ONE reusable _dispatch.yml — reusing, not duplicating, the substrate fan-out family. The member-side RECEIVER that applies the bump (rewrites its lock + auto-PRs, gated on pin check) is the #239-style member adoption, tracked as the #182 follow-up.
  • Generalize atlas's own DESIGN_REF (#125) into its lock entry — atlas runs the trinity model before it asks any member to.

The Bitspark constellation — how the systems are built and relate.

GitHub