Some checks failed
Build and publish policy-nexus image / build-and-push (push) Failing after 0s
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
248 lines
31 KiB
HTML
248 lines
31 KiB
HTML
<!doctype html>
|
|
<html lang="en"><meta charset="utf-8">
|
|
<meta name="policy-source-revision" content="4b951be3947f9457cc091a7d359d9232646b4c93">
|
|
<meta name="policy-source-digest" content="b5e8582459f546ae789ad5fd62f458454aa19997b520e32b6b9f792d6af55987">
|
|
<title>What the Hub Projects</title>
|
|
<style>
|
|
:root{
|
|
--paper:#EDEEF0; --surface:#F6F7F8; --surface-2:#E4E6E9;
|
|
--ink:#171D24; --ink-2:#4A5561; --ink-3:#737E8A;
|
|
--rule:#D3D7DC; --rule-strong:#B6BCC3;
|
|
--brass:#8A6A2E; --brass-soft:#EFE5CD; --brass-line:#C9AE74;
|
|
--clay:#8A3A2C; --clay-soft:#F2DFDA;
|
|
--l0:#DCE0E2; --l1:#B9C4C7; --l2:#8CA1A6; --l3:#567D84; --l4:#23555E;
|
|
--chip-fg:#F6F7F8;
|
|
--font-display:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
|
|
--font-body:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
|
--font-mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
|
--measure:66ch;
|
|
}
|
|
@media (prefers-color-scheme:dark){
|
|
:root:not([data-theme="light"]){
|
|
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
|
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
|
--rule:#2A3138; --rule-strong:#3B444D;
|
|
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
|
--clay:#D08A76; --clay-soft:#3A211B;
|
|
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
|
--chip-fg:#12161A;
|
|
}
|
|
}
|
|
:root[data-theme="dark"]{
|
|
--paper:#12161A; --surface:#191E24; --surface-2:#222831;
|
|
--ink:#E6E9EC; --ink-2:#A3ADB7; --ink-3:#78838E;
|
|
--rule:#2A3138; --rule-strong:#3B444D;
|
|
--brass:#C9A45C; --brass-soft:#33290F; --brass-line:#6B5426;
|
|
--clay:#D08A76; --clay-soft:#3A211B;
|
|
--l0:#262C32; --l1:#35424A; --l2:#4A626B; --l3:#6A939D; --l4:#97C4CD;
|
|
--chip-fg:#12161A;
|
|
}
|
|
|
|
*{box-sizing:border-box}
|
|
body{
|
|
margin:0; background:var(--paper); color:var(--ink);
|
|
font-family:var(--font-body); font-size:17px; line-height:1.62;
|
|
-webkit-font-smoothing:antialiased;
|
|
}
|
|
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 96px}
|
|
.layout{display:grid;grid-template-columns:180px minmax(0,1fr);gap:56px;align-items:start}
|
|
@media (max-width:960px){.layout{grid-template-columns:1fr;gap:0}.rail{display:none}}
|
|
|
|
/* ---------- rail ---------- */
|
|
.rail{position:sticky;top:28px;padding-top:8px;font-family:var(--font-display);font-size:12px;line-height:1.5}
|
|
.rail ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
|
|
.rail a{color:var(--ink-3);text-decoration:none;display:flex;gap:9px}
|
|
.rail a:hover,.rail a:focus-visible{color:var(--brass)}
|
|
.rail .n{font-family:var(--font-mono);font-size:10px;color:var(--rule-strong);min-width:16px;padding-top:1px}
|
|
.rail .grp{margin-top:14px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
|
|
|
|
/* ---------- header ---------- */
|
|
header{padding:64px 0 40px;border-bottom:2px solid var(--ink);margin-bottom:44px}
|
|
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:14px;margin-bottom:22px}
|
|
.eyebrow .stat{color:var(--clay)}
|
|
h1{font-family:var(--font-display);font-weight:800;letter-spacing:-.035em;line-height:.94;font-size:clamp(46px,9vw,92px);margin:0 0 6px;text-wrap:balance}
|
|
.sub{font-family:var(--font-display);font-weight:500;font-size:clamp(16px,2.4vw,21px);letter-spacing:-.01em;color:var(--ink-2);margin:0 0 30px;max-width:34ch;line-height:1.3}
|
|
.metagrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px 28px;border-top:1px solid var(--rule);padding-top:20px}
|
|
.metagrid dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
|
|
.metagrid dd{margin:0;font-family:var(--font-display);font-size:13.5px;line-height:1.45;color:var(--ink)}
|
|
|
|
/* ---------- typography ---------- */
|
|
section{margin-bottom:60px;scroll-margin-top:24px}
|
|
h2{font-family:var(--font-display);font-weight:750;letter-spacing:-.022em;font-size:clamp(24px,3.4vw,31px);line-height:1.12;margin:0 0 18px;text-wrap:balance;display:flex;gap:14px;align-items:baseline}
|
|
h2 .sn{font-family:var(--font-mono);font-size:12px;font-weight:400;color:var(--brass);letter-spacing:.06em;flex:none;padding-top:2px}
|
|
h3{font-family:var(--font-display);font-weight:700;font-size:16px;letter-spacing:-.008em;margin:34px 0 10px;color:var(--ink)}
|
|
p{margin:0 0 15px;max-width:var(--measure)}
|
|
ul,ol{max-width:var(--measure);margin:0 0 15px;padding-left:20px}
|
|
li{margin-bottom:7px}
|
|
strong{font-weight:600}
|
|
em{font-style:italic}
|
|
code{font-family:var(--font-mono);font-size:.855em;background:var(--surface-2);padding:1px 5px;border-radius:2px}
|
|
a{color:var(--brass)}
|
|
.lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:60ch}
|
|
|
|
/* ---------- devices ---------- */
|
|
.callout{border-left:3px solid var(--brass);background:var(--brass-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
|
.callout p:last-child{margin-bottom:0}
|
|
.callout .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
|
.rule-quote{border-top:2px solid var(--ink);border-bottom:2px solid var(--ink);padding:26px 0;margin:28px 0;max-width:var(--measure)}
|
|
.rule-quote p{font-family:var(--font-display);font-weight:600;font-size:19px;line-height:1.38;letter-spacing:-.014em;margin:0;text-wrap:balance}
|
|
.hard{border-left:3px solid var(--clay);background:var(--clay-soft);padding:18px 22px;margin:0 0 24px;max-width:var(--measure)}
|
|
.hard .lbl{font-family:var(--font-mono);font-size:10px;letter-spacing:.13em;text-transform:uppercase;color:var(--clay);display:block;margin-bottom:8px}
|
|
.hard p:last-child{margin-bottom:0}
|
|
.dec{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.08em;color:var(--brass);text-transform:uppercase}
|
|
.vec{font-family:var(--font-mono);font-size:.9em;font-weight:600;background:var(--surface-2);padding:2px 7px;border-radius:2px;white-space:nowrap;letter-spacing:.04em}
|
|
|
|
/* ---------- tables ---------- */
|
|
.scroll{overflow-x:auto;margin:0 0 24px;-webkit-overflow-scrolling:touch}
|
|
table{border-collapse:collapse;width:100%;min-width:520px;font-family:var(--font-display);font-size:13.5px;line-height:1.45}
|
|
th{text-align:left;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);font-weight:400;padding:0 16px 8px 0;border-bottom:1px solid var(--rule-strong);vertical-align:bottom}
|
|
td{padding:11px 16px 11px 0;border-bottom:1px solid var(--rule);vertical-align:top;color:var(--ink-2)}
|
|
td:first-child{color:var(--ink);font-weight:600}
|
|
tbody tr:last-child td{border-bottom:none}
|
|
.lvl{font-family:var(--font-mono);font-weight:600;font-size:12px;letter-spacing:.04em;color:var(--ink)}
|
|
|
|
/* ---------- ladders ---------- */
|
|
.breakout{margin:34px 0 40px}
|
|
.bhead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;border-bottom:1px solid var(--rule-strong);padding-bottom:9px;margin-bottom:22px;flex-wrap:wrap}
|
|
.bhead h3{margin:0;font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--font-mono);font-weight:400;color:var(--ink-3)}
|
|
.bhead .note{font-family:var(--font-display);font-size:12.5px;color:var(--ink-3)}
|
|
.ladders{display:grid;gap:26px}
|
|
.ladder{display:grid;grid-template-columns:126px minmax(0,1fr);gap:18px;align-items:start}
|
|
@media (max-width:700px){.ladder{grid-template-columns:1fr;gap:10px}}
|
|
.ladder .pname{font-family:var(--font-display);font-weight:700;font-size:14px;letter-spacing:-.01em;padding-top:2px}
|
|
.ladder .pname span{display:block;font-family:var(--font-mono);font-size:10px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-top:3px}
|
|
.rungs{display:grid;gap:3px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
|
@media (max-width:700px){.rungs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
.rung{padding:9px 10px 11px;background:var(--surface);border-top:4px solid var(--l0);min-width:0}
|
|
.rung.r1{border-top-color:var(--l1)} .rung.r2{border-top-color:var(--l2)}
|
|
.rung.r3{border-top-color:var(--l3)} .rung.r4{border-top-color:var(--l4)}
|
|
.rung .code{font-family:var(--font-mono);font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--ink);display:block;margin-bottom:4px}
|
|
.rung .txt{font-family:var(--font-display);font-size:11.5px;line-height:1.34;color:var(--ink-2);display:block}
|
|
.rung.na{opacity:.42}
|
|
|
|
/* ---------- matrix ---------- */
|
|
.matrix-shell{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:stretch;margin-bottom:14px}
|
|
.ylab{writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);text-align:center;padding-bottom:22px}
|
|
.mgrid{display:grid;grid-template-columns:34px repeat(5,minmax(0,1fr));gap:3px}
|
|
.mcell{background:var(--surface);min-height:60px;padding:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}
|
|
.mcell.tint1{background:color-mix(in srgb,var(--l1) 26%,var(--surface))}
|
|
.mcell.tint2{background:color-mix(in srgb,var(--l2) 26%,var(--surface))}
|
|
.mcell.tint3{background:color-mix(in srgb,var(--l3) 24%,var(--surface))}
|
|
.mcell.tint4{background:color-mix(in srgb,var(--l4) 22%,var(--surface))}
|
|
.mcell.void{background:repeating-linear-gradient(135deg,transparent,transparent 5px,var(--rule) 5px,var(--rule) 6px);opacity:.55}
|
|
.rlab,.clab{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--ink-3);display:flex;align-items:center;justify-content:center}
|
|
.rlab{min-height:60px}
|
|
.clab{padding-top:7px;min-height:22px}
|
|
.pin{font-family:var(--font-mono);font-size:9.5px;font-weight:600;letter-spacing:.02em;background:var(--ink);color:var(--paper);padding:2px 5px;border-radius:2px;line-height:1.3;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.pin.ghost{background:transparent;color:var(--ink-2);border:1px dashed var(--rule-strong)}
|
|
.mnote{display:flex;gap:22px;flex-wrap:wrap;font-family:var(--font-display);font-size:12px;color:var(--ink-3);padding-top:6px}
|
|
.mnote .k{display:flex;align-items:center;gap:7px}
|
|
.sw{width:13px;height:13px;flex:none;background:var(--ink)}
|
|
.sw.g{background:transparent;border:1px dashed var(--rule-strong)}
|
|
.sw.v{background:repeating-linear-gradient(135deg,transparent,transparent 4px,var(--rule) 4px,var(--rule) 5px);border:1px solid var(--rule)}
|
|
@media (max-width:640px){
|
|
.mgrid{grid-template-columns:28px repeat(5,minmax(0,1fr))}
|
|
.mcell{min-height:52px;padding:4px}
|
|
.pin{font-size:8px;padding:1px 3px}
|
|
.rlab{min-height:52px}
|
|
}
|
|
|
|
/* ---------- methodology ---------- */
|
|
.verbs{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule)}
|
|
.verb{background:var(--surface);padding:18px 18px 20px}
|
|
.verb h4{font-family:var(--font-display);font-weight:750;font-size:15px;margin:0 0 7px;letter-spacing:-.01em}
|
|
.verb p{font-family:var(--font-display);font-size:12.5px;line-height:1.46;color:var(--ink-2);margin:0;max-width:none}
|
|
.verb .step{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.13em;color:var(--brass);display:block;margin-bottom:9px}
|
|
|
|
/* ---------- questions ---------- */
|
|
.qs{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--rule-strong)}
|
|
.q{display:grid;grid-template-columns:34px minmax(0,1fr) 170px;gap:18px;padding:16px 0;border-bottom:1px solid var(--rule);align-items:start}
|
|
@media (max-width:760px){.q{grid-template-columns:28px minmax(0,1fr);gap:12px}.q .owner{grid-column:2}}
|
|
.q .qn{font-family:var(--font-mono);font-size:11px;color:var(--brass);padding-top:3px}
|
|
.q .qt{font-family:var(--font-display);font-size:14px;line-height:1.48;color:var(--ink-2)}
|
|
.q .qt b{color:var(--ink);font-weight:700;display:block;margin-bottom:2px;font-size:14.5px}
|
|
.owner{font-family:var(--font-mono);font-size:10px;letter-spacing:.05em;color:var(--ink-3);padding-top:4px}
|
|
.owner .tag{display:inline-block;border:1px solid var(--rule-strong);padding:2px 7px;border-radius:2px}
|
|
.owner .tag.need{border-color:var(--clay);color:var(--clay)}
|
|
|
|
/* ---------- misc ---------- */
|
|
.numbers{font-family:var(--font-mono);font-size:12.5px;line-height:1.85;background:var(--surface);border-left:3px solid var(--l3);padding:16px 20px;margin:0 0 22px;overflow-x:auto;max-width:var(--measure)}
|
|
.numbers .v{color:var(--ink);font-weight:600}
|
|
.numbers .k{color:var(--ink-3)}
|
|
pre{font-family:var(--font-mono);font-size:12.5px;line-height:1.68;background:var(--surface);border-left:3px solid var(--rule-strong);padding:16px 20px;overflow-x:auto;margin:0 0 22px;max-width:var(--measure);color:var(--ink-2)}
|
|
.alt{border-bottom:1px solid var(--rule);padding:14px 0;max-width:var(--measure)}
|
|
.alt:last-of-type{border-bottom:none}
|
|
.alt b{font-family:var(--font-display);font-size:14px;display:block;margin-bottom:3px}
|
|
.alt p{font-size:14.5px;margin:0;color:var(--ink-2)}
|
|
.alt .verdict{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--clay)}
|
|
footer{border-top:2px solid var(--ink);margin-top:20px;padding-top:22px;font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--ink-3);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
|
.tm td,.tm th{text-align:center}
|
|
.tm td:first-child,.tm th:first-child{text-align:left}
|
|
.yes{color:var(--l4);font-weight:700}
|
|
.no{color:var(--clay);font-weight:700}
|
|
.kind{font-family:var(--font-mono);font-size:9px;letter-spacing:.09em;text-transform:uppercase;padding:2px 6px;border-radius:2px;white-space:nowrap;border:1px solid var(--rule-strong);color:var(--ink-3)}
|
|
.kind.adv{border-color:var(--clay);color:var(--clay)}
|
|
.routes{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:2px;background:var(--rule);border:1px solid var(--rule);margin:0 0 22px}
|
|
.route{background:var(--surface);padding:16px 18px}
|
|
.route h4{font-family:var(--font-display);font-weight:750;font-size:14px;margin:0 0 6px}
|
|
.route p{font-family:var(--font-display);font-size:12.5px;line-height:1.45;color:var(--ink-2);margin:0;max-width:none}
|
|
.route .tag{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--brass);display:block;margin-bottom:8px}
|
|
a:focus-visible,.rail a:focus-visible{outline:2px solid var(--brass);outline-offset:3px}
|
|
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
|
|
|
</style>
|
|
<div class="wrap"><header><div class="eyebrow"><span>CUST-ADR-012</span> <span class="stat">accepted · 1.0</span> <span>the-custodian</span> <span>reviewed 2026-08-25</span><span>generated from canonical source — do not edit</span></div><h1>What the Hub Projects</h1><p class="sub">Source: <code>the-custodian · canon/architecture/adr-012-projection-source-and-preliminary-overlay.md · 4b951be3947f9457cc091a7d359d9232646b4c93</code></p><p class="sub">Review due: 2027-02-25</p></header><div class="layout"><nav class="rail" aria-label="Sections"><ol><li><a href="#status"><span class="n">·</span>Status</a></li><li><a href="#context"><span class="n">·</span>Context</a></li><li><a href="#decision"><span class="n">·</span>Decision</a></li><li><a href="#consequences"><span class="n">·</span>Consequences</a></li><li><a href="#relationship-to-prior-decisions"><span class="n">·</span>Relationship to prior decisions</a></li><li><a href="#references"><span class="n">·</span>References</a></li></ol></nav><main><section id="status"><h2>Status</h2>
|
|
<p><strong>Accepted 2026-08-25</strong> by Bernd Worsch. Supersedes <code>ADR-010</code> 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".</p>
|
|
</section>
|
|
<section id="context"><h2>Context</h2>
|
|
<p><code>ADR-001</code> says work originates as repository files and the hub is a read model. <code>ADR-010</code> says central is authoritative <em>as a reading of the repositories</em>. Both leave one question unanswered, and it turns out to be the load-bearing one:</p>
|
|
<div class="rule-quote"><p><strong>Which</strong> repository files? The forge, or a working copy?</p></div>
|
|
<p>Verified on 2026-08-25, the answer today is <strong>neither</strong>.</p>
|
|
<p>Central runs one FastAPI pod on railiance01 with a CNPG database. Inspected from inside, it holds <code>/app</code> — its own source — and nothing else. No clone tree, no repository mount; the <code>sweep</code> hostPath that would give it one is disabled. <strong>The hub never reads a repository.</strong> It cannot see the forge, and it cannot see a workstation.</p>
|
|
<p>What actually happens is the inverse of the ADR language: <code>fix-consistency</code> and <code>registrar-reconcile</code> run <em>on a workstation</em>, read files in <code>/home/<user>/<repo></code>, and POST the result over a tunnel. The <code>host_paths</code> field records this plainly — 117 repositories carry a path on <code>bnt-lap001</code>, a laptop.</p>
|
|
<p>So the projection derives from <strong>whichever checkout most recently ran the sync</strong>. That is a third thing, distinct from both the forge and any particular working copy, and it is written down nowhere.</p>
|
|
<h3>Why this is not academic</h3>
|
|
<p>Every failure investigated under <code>CUST-WP-0067</code> and <code>CUST-WP-0068</code> is downstream of it:</p>
|
|
<ul><li>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.</li><li>50 repository records pointed at a retired forge, so the sync could not match the checkout and silently registered nothing.</li><li>Duplicate registrations accumulated: the same file registered twice under two identifiers, because two environments each believed they were projecting it.</li><li><code>git_fingerprint</code> — the field that should say which commit a projection reflects — still holds the <em>initial commit</em> for <code>the-custodian</code>, while <code>last_state_synced_at</code> is minutes old. There is no reliable way to ask central what state it is a projection <em>of</em>.</li></ul>
|
|
<h3>The forcing function</h3>
|
|
<p>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 <code>ADR-010</code> diagnosed, relocated one layer up and made harder to see.</p>
|
|
<p>The window to fix this cheaply is before the second contributor, not after.</p>
|
|
</section>
|
|
<section id="decision"><h2>Decision</h2>
|
|
<p><strong>1. The forge is the projection source.</strong> 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.</p>
|
|
<p>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 <code>git</code> binary.</p>
|
|
<p><strong>2. Every projected record carries the commit it derived from.</strong> A projection without provenance cannot be audited, and the existing <code>git_fingerprint</code> 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.</p>
|
|
<p><strong>3. Unpushed local work is a preliminary overlay, never the baseline.</strong> 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.</p>
|
|
<p>This implements <code>ADR-010</code> decision 6, which named "preliminary" but left it unbuilt.</p>
|
|
<p><strong>4. The overlay is a label, not a second store.</strong> 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.</p>
|
|
<p><strong>5. A working copy is a workspace, not a source of authority.</strong> 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.</p>
|
|
<p><strong>6. Push-based sync is retired as the primary path.</strong> Workstation tooling stops being how records reach central. It may continue to <em>propose</em> 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.</p>
|
|
<p><strong>7. The projection must be resettable from the forge, as a routine operation.</strong> There must be a supported way to reconcile the projection — for one repository or for the whole fleet — against what Forgejo holds: create what is missing, update what differs, and <strong>retire what no longer derives</strong>.</p>
|
|
<div class="rule-quote"><p><strong>Amended 2026-08-25, before implementation.</strong> This decision originally said <em>discard and rebuild</em>. That operation cannot exist, and the reason is a good one. <code>progress_events</code>, <code>tasks</code>, <code>decisions</code> and <code>review_contracts</code> all reference workplans with <code>ON DELETE RESTRICT</code>, and 1067 of 1075 workplans carry at least one such reference. Deleting a workplan would mean deleting the hub-native records attached to it — which <code>ADR-010</code> decision 4 forbids, and which the schema refuses to allow. The database is enforcing this ADR's own boundary one layer down. A progress event recording work on a workplan is a fact that happened; destroying it to tidy a derived projection would be losing hub-native truth to fix a derived-state problem, which is exactly backwards. Retirement achieves what reset is for — the projection converges on the forge, and records that no longer derive stop appearing as live work — without erasing anything that only the hub holds. Reset reconciles; it does not destroy.</p></div>
|
|
<p>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. <code>ADR-010</code> 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.</p>
|
|
<p>Three properties make it real rather than ceremonial:</p>
|
|
<ul><li><strong>Routine, not emergency.</strong> 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.</li><li><strong>Per repository is the unit.</strong> Not a convenience over a fleet-wide operation — the repository <em>is</em> the unit of reconstruction, and the fleet-wide form is defined as iteration over it.</li></ul>
|
|
<p>This follows from the source. Each repository is a separate forge repository with its own history and its own head commit, so its projection is derivable in isolation and provable in isolation: fetch that repository, rebuild its records, compare against that repository's head. Nothing about that requires knowing the state of any other repository, and a design that made it require so would be inventing a dependency the source does not have.</p>
|
|
<p>It also bounds the blast radius, which is what makes decision 7's "routine" achievable. A rebuild that can only run fleet-wide is an operation nobody runs casually, and therefore an operation nobody runs — which is precisely how <code>ADR-010</code>'s never-executed reconstruction claim came to be believed. Scoped to one repository, a rebuild is small enough to be ordinary, and ordinary is the only state in which it stays known-working.</p>
|
|
<p>It is also the only form that composes with the refusals below. A repository holding projection-only records can be held back and dispositioned while every healthy repository around it is rebuilt; an all-or-nothing reset is blocked entirely by a single bad repository, which in practice means it is blocked permanently.</p>
|
|
<p>With contributors, per-repository scope is what keeps one person's rebuild from touching another person's records.</p>
|
|
<p>The fleet-wide form must therefore be a loop over the per-repository form, sharing one implementation. The rarely-used dangerous path and the frequently used safe path must be the same code, so the rare one is exercised by the common one rather than trusted on the strength of never having been run.</p>
|
|
<ul><li><strong>Idempotent and verifiable.</strong> 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 (<code>ADR-007</code>) are what make this possible: the same commit yields the same record identities every time.</li></ul>
|
|
<p><strong>Retirement must be visible, not silent.</strong> A retired record states that the forge no longer derives it, and remains inspectable — including from whatever hub-native history is still attached. A record that merely disappears from a listing is indistinguishable from one that was never there.</p>
|
|
<p><strong>Reset does not restore the preliminary overlay.</strong> 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.</p>
|
|
<p><strong>A reset must refuse when records exist only in the projection.</strong> 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.</p>
|
|
<p>The refusal is evaluated per repository, in keeping with the scope above. A repository whose records all exist in the forge is rebuildable regardless of what any other repository holds, and a fleet-wide run must skip and report the repositories it refuses rather than abort the whole pass. Otherwise one unresolved repository blocks reconstruction everywhere, and the capability decays back into the untested assumption this decision exists to prevent.</p>
|
|
<p><strong>8. Formal git review stays optional.</strong> 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.</p>
|
|
</section>
|
|
<section id="consequences"><h2>Consequences</h2>
|
|
<p><strong>Positive.</strong> The hub becomes provably a projection: reconcilable on demand against its source, 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.</p>
|
|
<p><strong>Negative.</strong> 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.</p>
|
|
<p><strong>Migration.</strong> The <code>sweep</code> 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.</p>
|
|
<p><strong>Unresolved.</strong> 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 <code>ADR-010</code> 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.</p>
|
|
</section>
|
|
<section id="relationship-to-prior-decisions"><h2>Relationship to prior decisions</h2>
|
|
<ul><li><strong><code>ADR-001</code></strong> — unchanged. Work still originates as repository files; this ADR says which copy of them the hub reads.</li><li><strong><code>ADR-010</code></strong> — 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.</li><li><strong><code>ADR-003</code></strong> — partially superseded. Decision 2 composes fingerprints from filesystem <code>mtime</code>, which is a property of one workstation rather than of the source; under decision 1 here the input is the commit. Decision 5 already stated the rebuild principle correctly but had never been exercised; decision 7 here makes it an operation with a source, a scope and a verification.</li><li><strong><code>ADR-007</code></strong> — 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.</li></ul>
|
|
</section>
|
|
<section id="references"><h2>References</h2>
|
|
<ul><li><code>ADR-001</code> — workplans originate as repo files; hub is a read model</li><li><code>ADR-010</code> — hub authority, local cache, and the two kinds of hub data</li><li><code>ADR-007</code> — identifier uniqueness and derived identifiers</li><li><code>CUST-WP-0067</code> — hub target resolution; retired the impersonating local instance</li><li><code>CUST-WP-0068</code> — cache-only work-record recovery; surfaced the stale <code>git_fingerprint</code> and the duplicate registrations</li><li>Verification, 2026-08-25: central pod holds no repository files; <code>sweep</code> disabled; 117 repositories record a laptop path; <code>the-custodian</code> <code>git_fingerprint</code> is the initial commit while <code>last_state_synced_at</code> is current</li></ul>
|
|
</section><footer><span>CUST-ADR-012 · 1.0 · accepted</span><span>the-custodian · canon/architecture/adr-012-projection-source-and-preliminary-overlay.md · 4b951be3947f9457cc091a7d359d9232646b4c93</span></footer></main></div></div></html>
|