diff --git a/canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md b/canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md index 1aa2516..5da1407 100644 --- a/canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md +++ b/canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md @@ -109,9 +109,15 @@ Decision 1 must therefore be enforced before derivation ships. *Interim state (A).* Until derivation lands, exactly one instance writes hub identifiers into repository files. Other instances may read, project, and serve, -but must not mint workplan or task UUIDs into git-tracked files. The interim -registrar is the automated production instance; workstation hubs are development -read replicas. +but must not mint workplan or task UUIDs into git-tracked files. + +> **Corrected 2026-08-17, superseded by `ADR-008` decisions 1–3.** This decision +> originally described workstation hubs as "development read replicas". That was +> wrong on both counts: the workstation instance was not a replica, and it was +> the *larger* of the two, holding 306 more workplans than the primary. The two +> instances were peer databases. `ADR-008` establishes the central hub as +> authoritative and local instances as rebuildable caches, which is what makes +> this interim rule coherent. The interim is policy, enforced by discipline, and it has a real cost: registration requires connectivity to the registrar, so disconnected work cannot diff --git a/canon/architecture/adr-008-hub-authority-and-local-cache-model.md b/canon/architecture/adr-008-hub-authority-and-local-cache-model.md new file mode 100644 index 0000000..69801e0 --- /dev/null +++ b/canon/architecture/adr-008-hub-authority-and-local-cache-model.md @@ -0,0 +1,158 @@ +--- +id: ADR-008 +type: architecture-decision-record +title: "Hub Authority, Local Cache, and the Two Kinds of Hub Data" +status: proposed +decided_by: Bernd Worsch +date: "2026-08-17" +tags: ["architecture", "state-hub", "repo-manager", "hub-core", "authority", "cache", "offline", "adr-001"] +--- + +# ADR-008: Hub Authority, Local Cache, and the Two Kinds of Hub Data + +## Status + +Proposed. + +## Context + +Investigating a workplan-identifier flip-flop (`ADR-007`) revealed that two +State Hub instances were operating as **peer databases**, not as a primary and a +replica: + +| Address | Instance | Workplans | +|---|---|---| +| `127.0.0.1:8000` (IPv4) | local uvicorn on docker postgres | 955 | +| `[::1]:8000` (IPv6) | ssh tunnel `state-hub-primary` → railiance01 | 649 | + +Both listen on port 8000, separated only by IP family, so every tool defaulting +to `127.0.0.1` reached the local instance while believing it was the primary. + +Measured divergence — 320 records exist locally but not on the primary: + +```text +backed by a file that exists on disk 288 fully rebuildable +no backing file recorded 28 not rebuildable as-is +same filename, different UUID 4 duplicate registration +``` + +**No file was missing for any of the 288.** The divergence is therefore almost +entirely redundant: it can be discarded and rebuilt from repositories without +losing anything. + +`ADR-001` already says work originates as repository files and the hub is a read +model. The two-peer-database situation contradicts that in practice: a second +database accumulated 306 net records of its own. + +## Decision + +**1. One authoritative hub, deriving from repository files.** The central State +Hub — running on railiance — is authoritative. It is authoritative *as a reading +of the repositories*, not as a second place data lives. Repository files remain +the source of truth (`ADR-001`). + +**2. A local hub is a cache, never a database.** Local instances hold a +rebuildable projection. A cache may be discarded and reconstructed from the +repositories at any time, and losing it must never lose work. + +This replaces the peer-database arrangement. It is also why the divergence is +tractable: a divergent database is a merge problem, a stale cache is a refresh +problem. + +**3. Local work requires no hub at all.** Repository files are self-describing — +identifier, status, tasks, all in frontmatter. Working in a repository requires +reading files, not querying an index. A cache is an optimization for cross-repo +questions, never a prerequisite for doing work. + +**4. Hub data is classified by origin, and the two kinds have opposite rules.** + +| | File-derived | Hub-native | +|---|---|---| +| Examples | workplans, tasks, statuses, dependencies | progress events, decisions, inbox messages, token events | +| Source of truth | the repository file | the hub | +| Offline behaviour | write the file and commit — the commit *is* the write | buffer locally, replay when reachable | +| Conflict model | **none**; conflicts are git conflicts, resolved in git | **none**; append-only merges regardless of order | +| Central accepts pushes? | no — it derives | yes | + +Neither kind needs a hub-side conflict model. That is the point of the split: if +central derives file-backed state, it cannot hold a conflicting version of it — +it re-derives whatever git settles on. Two people editing one workplan is a git +conflict and belongs to git. + +**5. Central derives file-backed state; it does not accept pushes of it.** +"Authoritative" means authoritative *reading*, so nothing may inject derived +state directly. Hub-native records are the exception and keep a real write path. + +**6. Preliminary until confirmed.** Locally registered data and uncommitted +repository state are preliminary until the central service has seen them. +Mitigation is by changing the repository files and the local cache — never by +editing central to match a local view. + +Combined with `ADR-007` decision 2 (identifiers derived from `PREFIX-WP-NNNN`), +"preliminary" largely stops mattering: a cache computes the same identifier +central will, so offline-registered data is already correct on arrival and needs +confirmation rather than reconciliation. + +**7. Every record has exactly one authoritative hub.** The State Hub retirement +splits one hub into several. Multiple central hubs are permitted only under this +rule: the owning hub is determined by the record's repository and domain. Without +it, the same peer-database divergence recurs at larger scale. + +**8. Cache reads are advisory and must carry their age.** Cross-repo answers from +a cache are advisory and should be presented with staleness. For the repository +an agent is working in, the file is truth and the cache is never consulted for +correctness. + +## Orphan disposition + +The 28 records with no backing file are the only ones a cache rebuild would drop. +They fall into three classes, to be separated before any rebuild: + +1. **Broken links** — a file exists but `backing_filename` was never recorded. + `RMGR-WP-0004` is one: the workplan file exists and is committed. These are + metadata repairs, not data loss, and are likely the largest class. +2. **Live hub-first records** — `proposed`, `ready`, or `backlog` with no file, + in `activity-core`, `core-hub`, `hub-core`, `issue-core`, `ops-hub`, + `prj-forgejo-org-refactor`, `railiance-enablement`, `railiance-infra`, + `reef-railiance`. Each needs a repository file written or an explicit drop. + These are `ADR-001` violations and must not be preserved as hub-only records. +3. **Closed hub-first records** — `finished` or `archived` with no file. Retain + as historical provenance where cheap; do not reconstruct plans that are done. + +A cache rebuild enforces `ADR-001` retroactively: the only casualties are records +that broke it. + +## Consequences + +**Positive.** The divergence becomes discardable rather than mergeable. Offline +work is fully supported without a write buffer for file-backed state — the git +commit is the write. No hub-side conflict model is needed for either data kind. +Authority stops being a policy claim and becomes a structural property. + +**Negative.** The 28 orphans require case-by-case disposition before a rebuild. +Any consumer that treats a local hub as authoritative must be corrected. The +one-hub-per-record rule constrains the retirement's hub split. + +**Rescoping.** `STATE-WP-0068` (offline write buffer and edge relay) is scoped as +a single mechanism. Under decision 4, most of what it buffers does not need +buffering — only the append-only hub-native stream does. Its scope should be +re-examined before more is built on it; this may reduce work rather than add it. + +**Correction to `ADR-007`.** Decision 2 there calls the workstation instance a +"development read replica". It was neither a replica nor smaller — it held 306 +more workplans than the primary. Superseded by decisions 1–3 here. + +## Implementation + +Owned by `repo-manager` for file-derived state (decision `747011c6`; it already +owns repository representation, file-backed record indexing, and reconciliation) +and by `hub-core` for hub-native records. Tracked under `RMGR-WP-0005`. + +## References + +- ADR-001 — workplans originate as repo files; hub is a read model +- ADR-007 — identifier uniqueness, derived identifiers, worker topology +- Decision `747011c6` — repository standards belong to Repo Manager +- `RMGR-WP-0005` — registrar consolidation and deterministic identifiers +- `STATE-WP-0068` — offline write buffer and edge relay (rescope candidate) +- Divergence measurement, 2026-08-17: 955 local / 649 primary / 320 local-only