the-custodian/canon/architecture/adr-012-projection-source-and-preliminary-overlay.md
codex 9162f7e530
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
docs(canon): ADR-012 — require a routine reset of the projection from the forge
Adds decision 7: the projection must be discardable and rebuildable from what
Forgejo holds, per repository and fleet-wide.

This is what makes the other decisions checkable. A read model that cannot be
rebuilt from its source is a database with a projection's reputation, and the
difference only surfaces when someone needs to rebuild it. ADR-010 already
asserts a cache may be reconstructed at any time; that claim has never been
executed.

Three properties keep it real: routine rather than emergency, scoped per
repository, and idempotent enough to verify against the forge — which derived
identifiers make possible.

Two refusals are explicit. Reset cannot restore the preliminary overlay, since
the forge does not hold it. And reset must refuse when records exist only in
the projection: on 2026-08-25 that was 111 work records, which a rebuild would
have erased while reporting success.

Also flags an unresolved boundary: hub-native records are not forge-derived and
must survive a rebuild of forge-derived state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 21:36:21 +02:00

208 lines
11 KiB
Markdown

---
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. The projection must be resettable from the forge, as a routine operation.**
There must be a supported way to discard the projection — for one repository or
for the whole fleet — and rebuild it from what Forgejo holds.
This is the decision that makes the others checkable rather than merely stated.
A read model that cannot be rebuilt from its source is not a projection; it is a
database with a projection's reputation, and the difference only becomes visible
on the day someone needs to rebuild it. `ADR-010` decision 2 already asserts that
a cache "may be discarded and reconstructed from the repositories at any time" —
that claim has never been executed, and an untested rebuild path is an assumption,
not a capability.
Three properties make it real rather than ceremonial:
- **Routine, not emergency.** It should be run deliberately and often enough that
it is known to work, not discovered under pressure. A reset that has never been
performed is indistinguishable from one that does not work.
- **Scoped.** Per repository as well as fleet-wide. Rebuilding one repository's
records must not require discarding everything, or it will never be used.
- **Idempotent and verifiable.** A reset followed by a reset produces the same
projection, and the result can be compared against the forge to show it matches.
Derived identifiers (`ADR-007`) are what make this possible: the same commit
yields the same record identities every time.
**Reset does not restore the preliminary overlay.** Overlay records exist
precisely because the forge does not hold them, so a rebuild from the forge
cannot reproduce them and must not pretend to. Reset therefore discards
preliminary state, and must say so plainly before it runs.
**A reset must refuse when records exist only in the projection.** If the hub
holds records with no counterpart in the forge, rebuilding destroys them. That is
not hypothetical: as of 2026-08-25, 111 work records existed only in a retired
local database, and a rebuild at that moment would have erased them. The reset
path must detect that condition and stop, naming what would be lost, rather than
proceed and report success.
**8. 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.** The hub becomes provably a projection: rebuildable on demand, and
therefore knowable to be one. 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. Nor does it settle how hub-native records — progress events, decisions,
inbox messages, which `ADR-010` decision 4 classes as originating in the hub —
survive a reset. They are not forge-derived and must not be destroyed by a
rebuild of forge-derived state; the boundary needs drawing before reset is
built. 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