ADR 0004 — substrate-release coordination: a publisher-emitted release BOM + verified per-repo adoption
Atlas does not define substrate versions; it verifies that publisher-emitted release facts and consumer-resolved dependency graphs agree on one immutable substrate release ID.
This ADR is the design-of-record after research consultation 0001 (an expert
review of the original proposal). The first draft of 0004 proposed a central
hand-authored substrate.json + an optional substrate: field + an
atlas substrate check. The consultation endorsed the paradigm — central
descriptor, per-repo opt-in, CI check — but corrected what the descriptor is: not
a hand-typed table of version strings, but a committed release bill of materials,
a generated/verified snapshot of publisher-emitted release facts plus enough
resolved-graph evidence to state exactly what atlas is and is not proving. That keeps
the constraints intact (atlas stays zero-dependency, member CI stays offline, each
repo still owns its pins) while ensuring the descriptor does not become the next
unowned convention — the very failure 0001/0002 exist to kill.
Context
0002 gave the constellation a typed edge graph: each
repo declares dependsOn: [{ to, kind }] in its atlas.json, and
atlas manifest validate --reconcile confirms every declared build/contract
edge against the repo's real manifest of record (go.mod / Cargo.toml /
package.json). That answers "is the edge real?" — existence.
It deliberately says nothing about versions. An edge stele → logos (build) is
reconciled the moment stele's go.mod names logos at all; which rev, and whether
the same logical version is used across stele's three language lanes, is unchecked.
A second cross-cutting concern now has no owner — exactly the shape 0001/0002 set out
to fix, one level down (versions instead of edges):
- Per-language pins legitimately differ in coordinate system and so cannot be one
number: Go pseudo-versions, Rust git revs, npm semver. Today stele pins
logos-contractat07cf2ae(Go) andcdd57b46(Rust) — two revs, with nothing asserting they resolve to the same logical substrate. - The anchor is informal. stele's manifests instruct that pins must "match the EXACT versions thesmos resolves." That convention lives in prose comments, owned by no tool — copies rot silently, the failure atlas exists to kill.
- The cost is concrete. stele#6
(the derived-query search / read-gate) is not blocked upstream — logos shipped the
searcher (
logos-kernel). It is blocked because adopting it is a co-resolution problem: every core must link exactly onelogos-contractand one ontos, the TS single-ontos-instance hazard applies, and the kernel arrives by three different delivery mechanisms (wazero/wasm in Go, native in Rust, bundled.wasmin TS). With no checked release, that bump is high-risk by hand; with one, it is "move to the next substrate release."
This concern passes the atlas membership test: it spans ≥2 systems and has no single-repo owner (ontos, logos, thesmos, stele each pin independently; "they must agree" lives in none of them).
A related but distinct axis is already in flight: #41 pins the infrastructure artifacts a leaf consumes from atlas/design (generated diagrams, the docs-toolchain, the render ABI) via a per-repo lockfile + digests + fan-out PRs. That is freshness of atlas's own outputs, not co-resolution of the substance stack. The two share machinery; they are not the same lockfile.
Why the original "hand-authored descriptor" framing was wrong
If atlas maintainers type 2026.06's coordinates into substrate.json, atlas
becomes "the person who typed the versions" — a new authority with no ground truth
behind it, the same drift-prone hand-copy 0002 abolished for edges. The fix is an
ownership inversion: publishers emit facts, atlas assembles and enforces them,
consumers adopt them. The descriptor is then a coordination artifact (a verified
snapshot), not the source of truth.
The three truths
Co-resolution is not one claim; it is the agreement of three distinct facts, each owned by a different party. Naming them keeps atlas from overclaiming:
- Publisher truth — "these artifacts/coordinates were built and published from
this source revision, with these substrate dependencies." Owned by each component
publisher (
ontos,logos,thesmos), emitted from its release workflow. - Consumer truth — "this repo's direct manifests, and its resolved-graph
evidence, use only the coordinates of release
2026.06." Owned by each consumer repo, asserted by its committed manifests/locks. - Atlas truth — "atlas did not choose versions; it verified that publisher truth and consumer truth match a named, immutable release." Owned by atlas — the gate, not the vendor.
Decision
Add, to the same manifest/reconcile spine, a committed substrate release BOM (assembled by atlas from publisher-emitted facts) and a per-repo adoption check that every member runs in its own CI — reporting honestly by proof level.
The descriptor — topology/substrate.json (a committed release BOM)
The descriptor splits into a stable catalog (logical-name → ecosystem identity)
and immutable releases (per-release coordinate facts). It is assembled and
audited from publisher manifests, not hand-typed.
{
"schemaVersion": 1,
"catalog": {
"ontos": {
"typeIdentityCritical": true,
"source": { "repo": "github.com/Bitspark/ontos" },
"go": { "module": "github.com/bitspark/ontos" },
"rs": { "crate": "ontos", "git": "https://github.com/Bitspark/ontos" },
"ts": { "package": "@bitspark/ontos-core", "registry": "https://npm.pkg.github.com" }
}
},
"releases": {
"2026.06": {
"status": "active",
"immutable": true,
"components": {
"ontos": {
"source": { "tag": "v0.1.4", "commit": "0123456789abcdef0123456789abcdef01234567" },
"go": { "version": "v0.1.4", "sum": "h1:..." },
"rs": { "ref": { "kind": "tag", "value": "v0.1.4" }, "resolvedCommit": "0123…4567" },
"ts": { "version": "0.1.4", "integrity": "sha512-…", "gitHead": "0123…4567" },
"substrateDeps": {}
},
"logos-contract": {
"source": { "tag": "logos-contract/v0.1.0", "commit": "cdd57b46513e2758a78e59c1c5517bd69414099a" },
"go": { "version": "v0.0.0-20260601061411-cdd57b46513e", "commit": "cdd57b46513e…" },
"rs": { "ref": { "kind": "rev", "value": "cdd57b46513e…" }, "resolvedCommit": "cdd57b46513e…" },
"ts": { "version": "0.1.0", "integrity": "sha512-…", "gitHead": "cdd57b46513e…" },
"substrateDeps": {
"ontos": { "go": "v0.1.4", "rs": { "kind": "tag", "value": "v0.1.4" }, "ts": "0.1.4" }
}
}
}
}
}
}
The descriptor's shape encodes five hardening decisions:
catalogowns the logical-name → ecosystem-name mapping (Go module path, Rust crate + git source, npm scoped package + registry), so "ontos means@bitspark/ontos-core" lives as data, not as hardcoded parser knowledge — andtypeIdentityCriticalflags the packages where a duplicate instance breaksinstanceof/ SPI type identity.- Source identity is canonical; ecosystem coordinates are bindings. Each release row anchors on a full upstream commit plus the human-recognizable tag, then the per-language coordinate. A Go pseudo-version is a locator, not the identity.
- All coordinates are structured — no bare strings. Rust especially is
{ ref: { kind: "tag" | "rev", value }, resolvedCommit }: a bare"cdd57b46"or"v0.1.2"loses whether the manifest declared a tag or a rev, which the honest comparison needs. npm carries the scoped package name + registry + integrity; Go carries module + version (+sum). - Full source commits are canonical; short SHAs appear only in diagnostics.
Signed tags and/or artifact-attestation references are optional publisher evidence.
For artifact-delivered components (
logos-kernel's.wasm), record artifactsha256/ SRI integrity, since source commit alone does not pin the built blob. - Each component stores its own publisher substrate-dependency closure
(
substrateDeps). This is the bridge from a surface check to the type-identity hazard: iflogos-contractwas published againstontos@v0.1.2, a release row that also tells consumers to pinontos@v0.1.4is internally inconsistent and must fail before any consumer sees it.
The descriptor governs only the substrate (the Greek-named substance stack + its
published contracts/kernels). It does not pin a repo's ordinary third-party
dependencies; those are each repo's own concern. A package a repo does not yet
consume (e.g. logos-kernel before stele#6) is simply present in the release and
unreferenced by that repo — declared intent, the same way an edge with no backing
dependency is "intent-only" under 0002, not an error.
Publisher manifests are the source of truth
Each component publisher emits a machine-readable release manifest (a file or release asset) recording its source commit, per-language coordinates, and its own substrate deps for a named release. Atlas imports those into the BOM and audits that the committed BOM is exactly what the publisher manifests imply. That ownership inversion — publisher repos emit facts; atlas assembles and enforces them; consumer repos adopt them — is the most important hardening move in this revision.
The manifest gains one optional field
A member's atlas.json may declare which release it targets:
{ "name": "stele", "...": "...", "substrate": "2026.06" }
Absent ⇒ the repo is not (yet) substrate-pinned; the check is a no-op for it (so
adoption is incremental, exactly like the docs block under 0003).
The resolved-graph snapshot — .atlas/substrate.lock.json (optional → required)
A surface check on direct manifests cannot see transitive divergence (Go MVS selecting a higher version, an npm nested duplicate, a Cargo duplicate package id). To close that gap without making atlas a resolver, a generated, committed substrate-only resolved snapshot records the inputs (hashed) and the selected substrate packages per lane:
{
"schemaVersion": 1,
"release": "2026.06",
"inputs": { "go.mod": "sha256:…", "Cargo.lock": "sha256:…", "package-lock.json": "sha256:…" },
"lanes": {
"go": { "selected": { "ontos": { "module": "github.com/bitspark/ontos", "version": "v0.1.4" } } },
"rs": { "selected": { "ontos": [ { "crate": "ontos", "source": "git+…?tag=v0.1.4#0123…" } ] } },
"ts": { "selected": { "@bitspark/ontos-core": [ { "version": "0.1.4", "integrity": "sha512-…" } ] } }
}
}
The snapshot is generated in a non-offline workflow (atlas substrate resolve,
which may run go list -m -json all, parse Cargo.lock / cargo metadata --locked,
parse package-lock.json) but verified offline by recomputing input hashes and
comparing the recorded substrate closure to the BOM. It starts optional (a
substrate-pinned repo may have only direct pins) and becomes required once a release
is generally adopted. Atlas may record resolver output; it must never compute a
"best" version itself.
The check — atlas substrate check (per-repo, local; in each member's CI)
The check is one command with explicit sub-rules, and reports by proof level so it never overclaims:
substrate/release-known
substrate/direct-pin-match
substrate/no-direct-double-pin
substrate/publisher-closure-match
substrate/resolved-lock-present
substrate/resolved-lock-inputs-current
substrate/resolved-graph-match
{
"check": "substrate",
"proof": { "surface": "pass", "publisherClosure": "pass", "resolvedGraph": "missing" },
"findings": [
{ "rule": "substrate/resolved-lock-missing", "severity": "warning",
"message": "No .atlas/substrate.lock.json; direct pins match 2026.06, but resolved-graph co-resolution was not verified." }
]
}
The three proof levels — and what each can and cannot prove:
| Proof level | Offline in member CI? | Proves | Does not prove |
|---|---|---|---|
| Surface (direct manifests) | Yes | Direct substrate pins match the release; no direct double-pins; declared lanes point at one release | Hidden transitive divergence (Go MVS, npm nested dup, Cargo duplicate id) |
Publisher-closure (substrateDeps) |
Yes | The release is internally coherent: each component's declared deps agree with the release's other rows | Whether the consumer's actual resolver selected that closure |
Resolved-graph (substrate.lock.json) |
Yes, once the snapshot exists | The committed resolved substrate graph matches the named release | Only as good as the snapshot's inputs and generation policy |
A divergence (e.g. stele's current Go 07cf2ae vs Rust cdd57b46 for the same
logical logos-contract) is a reported failure at the surface level, not silent.
The publisher-closure level additionally catches the original blind spot — a repo
pinning logos-contract correctly while it transitively pulls a different ontos —
without running any language toolchain. The check reads only committed files; like
every other atlas command it stays zero-dependency.
Command surface
atlas substrate import --release 2026.06 # assemble/refresh the BOM from publisher manifests
atlas substrate audit --release 2026.06 # verify the committed BOM == publisher manifests imply
atlas substrate plan --to 2026.07 # print intended edits + hazards (dry-run)
atlas substrate apply --to 2026.07 --write # rewrite direct pins + atlas.json (conservative codemod)
atlas substrate resolve --write # refresh .atlas/substrate.lock.json via language toolchains
atlas substrate check --strict # per-repo adoption check, reported by proof level
import/audit/resolve run where release truth is minted (atlas CI, publisher CI),
where network/toolchains are allowed; check runs offline in every member's CI
against the committed snapshot. apply is a conservative codemod: it preserves
formatting, replaces only exact recognized entries, fails closed on ambiguous syntax,
requires --force to rewrite unknown coordinates, and emits a unified diff under
--check/dry-run — so it never silently stomps a deliberate fork or hotfix.
Releases are append-only and immutable
A published, active release's coordinates are never silently edited. A bad release is deprecated (a status change steering new adopters away) or yanked (a status change marking it unsafe); new facts are appended as a new release row. This is the registry-immutability discipline applied to the substance axis.
Roll-forward, ownership, delivery
- Low-risk roll-forward is
plan → apply --write → resolve --write → check, with a canary sequence: publisher release manifests → atlas import PR → stele canary PR (the tri-core stress case) → other consumer fan-out PRs → mark the releaseadoptable/active. A release that cannot update stele is not generally adoptable. - Reuse #41's rails. The BOM is a sibling lockfile on the substance axis; rolling
a release out reuses #41's fan-out PRs (
repository_dispatch/ SHA-pinned reusable workflows) rather than reinventing propagation. - New gate alongside #47.
atlas substrate checkjoins the dormant cross-repo gates (--reconcile,workflow check,constellation-sync) activated under #47 — wired token-guarded, dormant until adopted, then required.
Ownership — split by fact type, not "atlas" generically
| Fact | Owner | Enforcement |
|---|---|---|
| Component source revision, artifacts, language coordinates, its substrate deps | Publishers (ontos, logos, thesmos maintainers) |
Publisher release workflow emits a signed/attested manifest |
| Release-train assembly ("2026.06 exists") | Substrate release captain / release group | CODEOWNERS + CI on topology/substrate.json |
| BOM schema, the checks, proof-level honesty | atlas maintainers | Schema/check tests in atlas |
| Adoption in a consumer repo | Consumer repo maintainers | Fan-out PR + consumer CI |
| Emergency yanks/deprecations | Release captain + affected component owner | Append-only status change |
The governance principle: publishers are accountable for truth; atlas is
accountable for refusing unverifiable truth; consumers are accountable for adoption.
A scheduled atlas-only atlas substrate audit --all-active re-verifies active
releases against publisher manifests/tags/registry metadata over time.
The work that realizes this ADR
This ADR is the design-of-record; the implementation lands as sub-issues under #65:
- #80 — the BOM descriptor schema
(
catalog+ structured coordinates +substrateDeps). - #81 —
substrate import/audit(assemble + verify the BOM from publisher manifests). - #82 —
substrate check(the per-repo, proof-level adoption check). - #83 —
.atlas/substrate.lock.json+substrate resolve(the resolved-graph snapshot). - #85 — governance (CODEOWNERS, owner split, append-only/yank discipline, scheduled audit).
Consequences
- The descriptor is a verified coordination artifact, not a hand-typed authority. Atlas assembles the BOM from publisher facts and refuses unverifiable ones; it pins by checking, never by vending. That keeps atlas from becoming the next unowned convention.
- The de-facto "match thesmos" convention becomes machine-checked, and the
existing Go/Rust
logos-contractdivergence in stele becomes visible rather than latent — at the surface level, with the transitive blind spot closed at the publisher-closure and resolved-graph levels. - The check is honest about its reach. It reports surface / publisher-closure / resolved-graph separately and never claims full co-resolution from a direct-manifest pass alone.
- stele#6 is de-risked: adopting
logos-kernelbecomes a substrate-release move with a guarantee all three cores advance together and none double-pins ontos — the hazard that makes the bumphighrisk today. - atlas stays what it is — maintainer/CI tooling, zero-dependency, never a runtime dependency; member CI stays offline (it consumes the committed snapshot).
- 0002 is refined, not superseded — edges (existence) and substrate (version
co-resolution) are two checks on the same
atlas.json+ manifest-of-record spine. - Adoption is incremental — repos without a
substratefield are unaffected; the resolved-graph snapshot starts optional and becomes required as a release is adopted; members opt in one PR at a time, as with thedocsblock.
Alternatives considered
- A central hand-authored version table (the original 0004 framing). Endorsed in paradigm, rejected in mechanism: hand-typed coordinates make atlas an unowned authority with no ground truth — the drift 0002 abolished for edges, re-introduced for versions. The BOM is assembled and audited from publisher facts instead.
- One version number for the whole substrate. Impossible to honor: Go pseudo-versions, Rust revs, and npm semver share no version space. The per-language structured-coordinate map is the irreducible shape.
- Let thesmos remain the implicit anchor ("pin whatever thesmos resolves"). The status quo: unowned, un-checked prose, already drifting across stele's lanes. (A machine-readable thesmos BOM that atlas imports is the explicit version of this and is compatible — but it is not sufficient alone, since consumers also depend on lower substrate layers directly.)
- Atlas as a full cross-repo resolver / monorepo, or Nix/Bazel-style pinning. Far heavier than the problem; abandons the per-repo-ownership model 0002 is built on. Atlas verifies co-resolution and records resolver output; it does not compute it.
- Dependabot / Renovate as the authority. Useful as update transport (grouped PRs) layered on top, but the unit they update must be the substrate release ID / the generated BOM — not independent per-ecosystem version bumps.
- Fold this into #41's lockfile. Tempting (shared machinery) but conflates two axes: #41 pins atlas/design infra artifacts; this pins the substance stack. A member must be able to bump one without the other. Same rails, separate descriptors.