ADR 0002 — repo manifests: each system declares its own place and edges
- Status: accepted
- Date: 2026-06-02
- Refines: 0001 (the source-of-truth clause)
Context
0001 made topology/constellation.json the single,
machine-readable source of truth: one central file in atlas declares every system
(gloss, is, layer, repo, draft, orthogonal) and every edge, inline,
as untyped dependsOn name lists. That was the right v0. Two needs now strain it:
Edges have no type.
logos dependsOn ontosdoes not say how — a build/link dependency, a runtime call, or a dependence on a published contract/SPI. Without that, impact analysis is coarse (every edge looks equally load-bearing) and the graph can't distinguish "recompile" from "re-check the contract."The truth lives far from the thing it describes. A central file means the
archeteam edits a file in atlas to record that arche depends on logos, and nothing ever checks that declaration against arche's realgo.mod/package.json. The declaration is unowned by the depending repo and un-reconciled with its code — exactly the "copies rot silently" failure 0001 set out to avoid, one level up.
A central registry that points at each repo, with each repo declaring its own identity and edges next to its code, fixes both: edges gain a type, ownership moves to the depending repo, and reconciliation ("declared edge ⇄ real dependency") becomes a local check that runs in that repo's own CI.
Decision
Each member repo carries an atlas.json manifest describing its own node and its
typed outbound edges. atlas keeps a thin registry of members; the existing
constellation.json becomes a generated aggregate of the manifests.
The manifest — atlas.json at each repo root
{
"name": "logos",
"greek": "λόγος",
"gloss": "the account of what is",
"is": "reasoning — works out what follows, as accounts you can re-check",
"layer": "substance",
"dependsOn": [
{ "to": "ontos", "kind": "build" },
{ "to": "arche", "kind": "contract" }
]
}
- Edges are full objects
{ "to", "kind" }— no string shorthand — withkind ∈ { build, runtime, contract }:build— compiles/links against it (verifiable from the manifest of record:package.json,go.mod,Cargo.toml, …).runtime— calls or needs it while running (a service/process dependency).contract— depends on its published interface/SPI, not its internals.
- Infrastructure repos set
"orthogonal": trueand omitgreek(design, atlas). draftis not in the manifest — a draft member may not have a repo yet — it lives in the registry.- The manifest is governed by a JSON Schema shipped in atlas
(
schema/atlas-manifest.schema.json, draft 2020-12). TheatlasCLI validates against it (atlas manifest validate) using a zero-dependency checker, and a repo'satlas.jsonpoints at it via$schemaso editors give autocomplete and inline errors. The schema is the one definition of the manifest's shape — CLI and editors read the same file, so they cannot drift.
The registry — topology/registry.json in atlas (hand-authored)
The one thing that genuinely has no per-repo home: who is in the family, and where.
{
"members": {
"ontos": { "repo": "https://github.com/Bitspark/ontos" },
"logos": { "repo": "https://github.com/Bitspark/logos" },
"arche": { "repo": "https://github.com/Bitspark/arche" },
"stele": { "repo": "https://github.com/Bitspark/stele", "draft": true },
"thesmos": { "repo": "https://github.com/Bitspark/thesmos", "draft": true },
"design": { "repo": "https://github.com/Bitspark/design" },
"atlas": { "repo": "https://github.com/Bitspark/atlas" }
}
}
The aggregate — topology/constellation.json (now generated)
atlas topology sync reads the registry, then each member's atlas.json (from a
local workspace of checkouts if present, else fetched via raw.githubusercontent.com
with zero-dep fetch), and writes the assembled graph to constellation.json with a
generated-header. family.md and the constellation diagram are generated from that,
unchanged. --check fails CI on drift, exactly as topology gen --check does today.
Validation splits in two
- Per-repo, local (
atlas manifest lint [--reconcile], run in each repo's CI): manifest is schema-valid; with--reconcile, every declaredbuild/runtimeedge matches a real dependency in that repo's manifest of record, and real sibling dependencies aren't left undeclared. - Global, over the aggregate (
atlas doctor, run in atlas CI): acyclic (today); plus new semantic invariants —dependsOnrespects layer rank (substance never depends on downstream; orthogonal infra takes no inbound substance edge); everytoresolves to a registered member; every non-draft member has a reachable manifest; acontractedge targets a system that publishes one.
Delivery (staged, per the agreed plan)
- PR1 — the JSON Schema +
atlas manifest validate/init, dogfooded by atlas's ownatlas.json. (The schema + CLI is the spine everything else builds on.) - PR2 —
doctor's semantic invariants (layer rank, orthogonality) over the graph. - PR3 — the registry +
topology sync; flipconstellation.jsonto the generated aggregate once the sibling repos carry manifests. - Seeding —
atlas.jsoninto each sibling repo (one PR per repo, in that repo). - PR4 —
atlas manifest validate --reconcile(declared ⇄ realpackage.json/go.mod), the part that reads each repo's manifest of record.
Consequences
- Migration touches every repo. Each of the eight members gains an
atlas.json(bootstrappable withatlas manifest init, seeded from today'sconstellation.json) — one PR per repo, in that repo, on its own workflow. atlas goes first; the others follow and are not changed unilaterally from here. - Two generated layers, both
--check-guarded: registry + manifests (hand) →constellation.json(generated) →family.md/ diagram (generated). More moving parts, but each link is checked, and the hand-authored truth shrinks and moves to where it's owned. - 0001 is refined, not superseded: the charter, boundary, and membership test stand; only "constellation.json is the hand-authored source of truth" changes — it becomes the generated aggregate of per-repo manifests + the registry.
- Access stays cheap and zero-dep: day-to-day commands read the committed
aggregate; only
sync/--reconcilereach other repos, via a local workspace orfetch— no new dependencies,npx github:Bitspark/atlasstill runs anywhere. - A repo with no manifest is a visible error, not silent absence — the registry
names every member, so a missing/unreachable manifest fails
doctor.
Alternatives considered
- Keep it central, just type the edges inline (
dependsOn: [{to, kind}]inconstellation.json). Simplest, no migration — but edges stay unowned by the depending repo and un-reconciled with its code; the rot risk that motivated this remains. Typed edges without relocating ownership solve only half the problem. - Fully distributed, no registry (discover members by scanning the GitHub org). The org holds many non-member repos; discovery would be fragile and implicit. An explicit registry is the one irreducibly-central fact.
- Top-level
edges: [{from, to, kind}]array in atlas. Normalizes the graph but keeps it central and unowned — same ownership/reconciliation gap as the first.