Bitspark constellation
accepted source ↗

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:

  1. Edges have no type. logos dependsOn ontos does 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."

  2. The truth lives far from the thing it describes. A central file means the arche team edits a file in atlas to record that arche depends on logos, and nothing ever checks that declaration against arche's real go.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 — with kind ∈ { 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": true and omit greek (design, atlas).
  • draft is 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). The atlas CLI validates against it (atlas manifest validate) using a zero-dependency checker, and a repo's atlas.json points at it via $schema so 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 declared build/runtime edge 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 invariantsdependsOn respects layer rank (substance never depends on downstream; orthogonal infra takes no inbound substance edge); every to resolves to a registered member; every non-draft member has a reachable manifest; a contract edge targets a system that publishes one.

Delivery (staged, per the agreed plan)

  • PR1 — the JSON Schema + atlas manifest validate / init, dogfooded by atlas's own atlas.json. (The schema + CLI is the spine everything else builds on.)
  • PR2doctor's semantic invariants (layer rank, orthogonality) over the graph.
  • PR3 — the registry + topology sync; flip constellation.json to the generated aggregate once the sibling repos carry manifests.
  • Seedingatlas.json into each sibling repo (one PR per repo, in that repo).
  • PR4atlas manifest validate --reconcile (declared ⇄ real package.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 with atlas manifest init, seeded from today's constellation.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/--reconcile reach other repos, via a local workspace or fetch — no new dependencies, npx github:Bitspark/atlas still 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}] in constellation.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.

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

GitHub