docs(canon): draft ADR-012 on what the hub actually projects
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 <noreply@anthropic.com>
This commit is contained in:
parent
0dfdb9b63f
commit
b01f75d793
1 changed files with 166 additions and 0 deletions
|
|
@ -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/<user>/<repo>`,
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue