From b01f75d79380f463bc7228a80aa43940fa7ee28d Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 25 Aug 2026 21:28:27 +0200 Subject: [PATCH] docs(canon): draft ADR-012 on what the hub actually projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-001 and ADR-010 both leave unanswered which repository files the hub reads. Verified 2026-08-25: neither the forge nor a working copy — the projection derives from whichever checkout most recently ran the sync, which is written down nowhere. Proposes the forge as projection source, unpushed local work as a labelled preliminary overlay rather than a second store, and commit provenance on every record. Implements ADR-010 decision 6's unbuilt notion of "preliminary" and sharpens decision 1 rather than reversing it. Status: proposed — needs review before anything is built on it. Co-Authored-By: Claude Opus 5 --- ...ojection-source-and-preliminary-overlay.md | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 canon/architecture/adr-012-projection-source-and-preliminary-overlay.md diff --git a/canon/architecture/adr-012-projection-source-and-preliminary-overlay.md b/canon/architecture/adr-012-projection-source-and-preliminary-overlay.md new file mode 100644 index 0000000..fadc74f --- /dev/null +++ b/canon/architecture/adr-012-projection-source-and-preliminary-overlay.md @@ -0,0 +1,166 @@ +--- +id: CUST-ADR-012 +type: architecture-decision-record +title: "What the Hub Projects: Forge as Projection Source, Working Copies as Preliminary Overlay" +status: proposed +owner: the-custodian +revision: "draft-1" +last_reviewed: "2026-08-25" +review_interval: 6m +decided_by: Bernd Worsch +date: "2026-08-25" +tags: ["architecture", "state-hub", "forgejo", "projection", "provenance", "authority", "contributors", "adr-001", "adr-010"] +--- + +# ADR-012: What the Hub Projects — Forge as Projection Source, Working Copies as Preliminary Overlay + +## Status + +Proposed. Supersedes `ADR-010` decision 1's phrase "authoritative as a reading of +the repositories" by making the reading concrete, and implements decision 6's +unbuilt notion of "preliminary". + +## Context + +`ADR-001` says work originates as repository files and the hub is a read model. +`ADR-010` says central is authoritative *as a reading of the repositories*. Both +leave one question unanswered, and it turns out to be the load-bearing one: + +> **Which** repository files? The forge, or a working copy? + +Verified on 2026-08-25, the answer today is **neither**. + +Central runs one FastAPI pod on railiance01 with a CNPG database. Inspected from +inside, it holds `/app` — its own source — and nothing else. No clone tree, no +repository mount; the `sweep` hostPath that would give it one is disabled. **The +hub never reads a repository.** It cannot see the forge, and it cannot see a +workstation. + +What actually happens is the inverse of the ADR language: `fix-consistency` and +`registrar-reconcile` run *on a workstation*, read files in `/home//`, +and POST the result over a tunnel. The `host_paths` field records this plainly — +117 repositories carry a path on `bnt-lap001`, a laptop. + +So the projection derives from **whichever checkout most recently ran the sync**. +That is a third thing, distinct from both the forge and any particular working +copy, and it is written down nowhere. + +### Why this is not academic + +Every failure investigated under `CUST-WP-0067` and `CUST-WP-0068` is downstream +of it: + +- A local hub and central both served port 8000, and every default reached the + local one for seven weeks. Neither could be distinguished by what it projected. +- 50 repository records pointed at a retired forge, so the sync could not match + the checkout and silently registered nothing. +- Duplicate registrations accumulated: the same file registered twice under two + identifiers, because two environments each believed they were projecting it. +- `git_fingerprint` — the field that should say which commit a projection + reflects — still holds the *initial commit* for `the-custodian`, while + `last_state_synced_at` is minutes old. There is no reliable way to ask central + what state it is a projection *of*. + +### The forcing function + +With one contributor, "last writer wins" is invisible, because there is only one +writer. With several, each pushes a projection of a different repository state +into one shared read model, and each will look locally consistent while +disagreeing with the others. That is the peer-database problem `ADR-010` +diagnosed, relocated one layer up and made harder to see. + +The window to fix this cheaply is before the second contributor, not after. + +## Decision + +**1. The forge is the projection source.** Central derives its projection from +Forgejo — the pushed state of the default branch — not from any working copy. +"Authoritative as a reading of the repositories" now names a specific reading: +what the forge holds. + +Forgejo runs in the same cluster as central, so this is an in-cluster fetch. No +tunnel, no cross-network credentials, and central already carries a `git` binary. + +**2. Every projected record carries the commit it derived from.** A projection +without provenance cannot be audited, and the existing `git_fingerprint` proves +the point by being wrong and silent about it. Records must reference the commit +that produced them, and a projection whose commit is older than the forge's head +must be visibly stale rather than quietly current. + +**3. Unpushed local work is a preliminary overlay, never the baseline.** A +working copy may contribute records the forge does not yet hold. They are marked +preliminary, attributed to their environment, and never silently merged into the +derived baseline. When the commit that carries them reaches the forge, the +baseline absorbs them and the overlay entry retires. + +This implements `ADR-010` decision 6, which named "preliminary" but left it +unbuilt. + +**4. The overlay is a label, not a second store.** Preliminary records live in +the same projection, flagged. They are not a parallel database and not a local +hub. Every local projection this ecosystem has had was eventually mistaken for +authority; the defence is that there is nothing separate to mistake. + +**5. A working copy is a workspace, not a source of authority.** Deleting a +checkout after pushing must lose nothing and invalidate nothing. Validation of a +projection must not require a local clone of the repository being validated. + +**6. Push-based sync is retired as the primary path.** Workstation tooling stops +being how records reach central. It may continue to *propose* preliminary +records under decision 3, but the baseline is derived, not received. This closes +the gap where "central derives" was policy while "the laptop pushes" was +practice. + +**7. Formal git review stays optional.** Deriving from the default branch gives a +shared baseline without requiring pull requests. Review can be adopted per +repository where it earns its keep; this ADR neither mandates nor forbids it. + +## Consequences + +**Positive.** Truth becomes checkable by anyone, from anywhere, without a clone. +Multiple contributors share one baseline instead of overwriting each other's +views. Provenance becomes auditable — every record can name its commit. The +"push then delete the working copy" case simply works. The distinction between +committed and uncommitted work becomes visible in the model rather than a matter +of who ran which command last. + +**Negative.** Git becomes load-bearing for the hub: Forgejo availability now +affects projection freshness. A derive loop needs a cadence, and freshness +becomes a property to monitor rather than assume. Unpushed work becomes +explicitly second-class — which is its honest status, but it will feel like a +restriction to a solo developer used to local-first behaviour. + +**Migration.** The `sweep` hostPath — central reading a node-local clone tree — +is a half-measure toward this decision and was disabled pending "governed remote +reconciliation" while railiance01 checkouts still targeted Gitea. As of +2026-08-25 all 79 node checkouts track Forgejo, so that stated blocker has +cleared. Sweep should be evaluated as a stepping stone or retired in favour of a +direct forge fetch, not left dormant with an obsolete justification. + +**Unresolved.** This ADR does not settle the derive cadence, whether central +clones or uses the Forgejo API, how preliminary records are surfaced in the +dashboard and MCP, or what happens to a preliminary record whose commit never +arrives. Those belong to implementation. + +## Relationship to prior decisions + +- **`ADR-001`** — unchanged. Work still originates as repository files; this ADR + says which copy of them the hub reads. +- **`ADR-010`** — decisions 1 and 5 are sharpened, not reversed: central still + derives and still does not accept pushes of derived state. Decision 6's + "preliminary" gains a mechanism. The local-cache-versus-database framing stands. +- **`ADR-007`** — derived identifiers become more valuable here: a forge-derived + projection and a preliminary overlay compute the same identifier for the same + record, so absorbing an overlay entry needs no reconciliation. + +## References + +- `ADR-001` — workplans originate as repo files; hub is a read model +- `ADR-010` — hub authority, local cache, and the two kinds of hub data +- `ADR-007` — identifier uniqueness and derived identifiers +- `CUST-WP-0067` — hub target resolution; retired the impersonating local instance +- `CUST-WP-0068` — cache-only work-record recovery; surfaced the stale + `git_fingerprint` and the duplicate registrations +- Verification, 2026-08-25: central pod holds no repository files; `sweep` + disabled; 117 repositories record a laptop path; `the-custodian` + `git_fingerprint` is the initial commit while `last_state_synced_at` is current