.nornir/assets/server-workspace-flow.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1320 720" font-family="'DejaVu Sans', 'Segoe UI', sans-serif">
<defs>
<linearGradient id="board" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0b1c2e"/><stop offset="1" stop-color="#06111d"/>
</linearGradient>
<linearGradient id="chip" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#14384f"/><stop offset="1" stop-color="#0e2a3e"/>
</linearGradient>
<linearGradient id="rpcchip" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#3a2a14"/><stop offset="1" stop-color="#2a1e0c"/>
</linearGradient>
<linearGradient id="king" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2d2410"/><stop offset="1" stop-color="#1c1607"/>
</linearGradient>
<linearGradient id="store" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#17402c"/><stop offset="1" stop-color="#0c2418"/>
</linearGradient>
<marker id="arrow" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
<path d="M0,0 L7,3 L0,6 Z" fill="#5fb6d6"/>
</marker>
</defs>
<!-- PCB substrate -->
<rect x="0" y="0" width="1320" height="720" fill="url(#board)"/>
<g stroke="#143a52" stroke-width="1"><line x1="0" y1="64" x2="1320" y2="64"/><line x1="0" y1="690" x2="1320" y2="690"/></g>
<g fill="#16415c"><circle cx="40" cy="40" r="2"/><circle cx="1280" cy="40" r="2"/><circle cx="40" cy="700" r="2"/><circle cx="1280" cy="700" r="2"/></g>
<text x="40" y="40" fill="#cfe6f2" font-size="22" font-weight="bold">nornir-server — how the server learns a workspace</text>
<text x="40" y="56" fill="#5f8aa3" font-size="12">redb registry is the source of truth · descriptor = git ssh URL or local path · empty registry is a valid resting state</text>
<!-- ===== TOP ROW: register → server → resolve descriptor ===== -->
<!-- Operator -->
<rect x="40" y="110" width="220" height="96" rx="8" fill="url(#chip)" stroke="#2c6e8f" stroke-width="1.5"/>
<text x="150" y="138" fill="#dff0f8" font-size="14" font-weight="bold" text-anchor="middle">Operator (CLI / viz)</text>
<text x="150" y="160" fill="#9fc4d6" font-size="11.5" text-anchor="middle">nornir workspace register</text>
<text x="150" y="178" fill="#9fc4d6" font-size="11.5" text-anchor="middle"><name> --descriptor X</text>
<text x="150" y="196" fill="#e0a85a" font-size="11" text-anchor="middle">X = git ssh URL | local path</text>
<!-- gRPC: Workspaces.Register -->
<rect x="320" y="110" width="210" height="96" rx="8" fill="url(#rpcchip)" stroke="#9a7330" stroke-width="1.5"/>
<text x="425" y="138" fill="#f3dca6" font-size="14" font-weight="bold" text-anchor="middle">nornir-server</text>
<text x="425" y="160" fill="#e6c88a" font-size="11.5" text-anchor="middle">Workspaces.Register (gRPC)</text>
<text x="425" y="180" fill="#bfa874" font-size="11" text-anchor="middle">authenticated port</text>
<text x="425" y="196" fill="#bfa874" font-size="11" text-anchor="middle">--addr + --token only</text>
<!-- Resolve descriptor -->
<rect x="590" y="96" width="240" height="124" rx="8" fill="url(#chip)" stroke="#2c6e8f" stroke-width="1.5"/>
<text x="710" y="122" fill="#dff0f8" font-size="14" font-weight="bold" text-anchor="middle">resolve descriptor</text>
<text x="710" y="146" fill="#9fc4d6" font-size="11.5" text-anchor="middle">git URL → clone the workspace</text>
<text x="710" y="162" fill="#9fc4d6" font-size="11.5" text-anchor="middle">repo (gix + russh)</text>
<text x="710" y="184" fill="#9fc4d6" font-size="11.5" text-anchor="middle">local path → read its dir /</text>
<text x="710" y="200" fill="#9fc4d6" font-size="11.5" text-anchor="middle">nornir-workspace.toml</text>
<!-- ===== CENTER: registry.redb (the KING) ===== -->
<rect x="430" y="290" width="460" height="130" rx="10" fill="url(#king)" stroke="#d9b25a" stroke-width="2.5"/>
<text x="660" y="320" fill="#f6e3b0" font-size="17" font-weight="bold" text-anchor="middle">👑 registry.redb — the source of truth</text>
<text x="660" y="344" fill="#e8d6a6" font-size="12" text-anchor="middle"><home>/.nornir/workspaces/registry.redb · single-writer (the server owns the lock)</text>
<text x="660" y="368" fill="#cdbf95" font-size="12" text-anchor="middle">row: name · mode · poll · members (git URLs) · descriptor_content (the raw TOML)</text>
<text x="660" y="390" fill="#cdbf95" font-size="12" text-anchor="middle">no descriptors/*.toml sibling file · idempotent upsert (re-register = update)</text>
<text x="660" y="410" fill="#9bd6a0" font-size="11.5" text-anchor="middle">wipe it → 0 workspaces, a valid resting state · re-add re-populates from the row's git URLs</text>
<!-- ===== BOTTOM ROW: poll/build → republish → warehouse → viz ===== -->
<!-- poll + clone members -->
<rect x="40" y="500" width="250" height="110" rx="8" fill="url(#chip)" stroke="#2c6e8f" stroke-width="1.5"/>
<text x="165" y="528" fill="#dff0f8" font-size="14" font-weight="bold" text-anchor="middle">eager build + poll loop (60s)</text>
<text x="165" y="552" fill="#9fc4d6" font-size="11.5" text-anchor="middle">ls-remote → on HEAD change,</text>
<text x="165" y="568" fill="#9fc4d6" font-size="11.5" text-anchor="middle">clone/fetch each member</text>
<text x="165" y="586" fill="#9fc4d6" font-size="11.5" text-anchor="middle">(gix + russh) → <root>/<ws>/git</text>
<!-- republish -->
<rect x="350" y="500" width="240" height="110" rx="8" fill="url(#chip)" stroke="#2c6e8f" stroke-width="1.5"/>
<text x="470" y="528" fill="#dff0f8" font-size="14" font-weight="bold" text-anchor="middle">republish hook</text>
<text x="470" y="552" fill="#9fc4d6" font-size="11.5" text-anchor="middle">knowledge scan (syn) +</text>
<text x="470" y="568" fill="#9fc4d6" font-size="11.5" text-anchor="middle">code index + snapshot</text>
<text x="470" y="586" fill="#9fc4d6" font-size="11.5" text-anchor="middle">(deep_scan from the row)</text>
<!-- warehouse -->
<rect x="650" y="500" width="240" height="110" rx="8" fill="url(#store)" stroke="#3a8a5c" stroke-width="1.5"/>
<text x="770" y="528" fill="#d8f0e0" font-size="14" font-weight="bold" text-anchor="middle">per-workspace warehouse</text>
<text x="770" y="552" fill="#a6d4b6" font-size="11.5" text-anchor="middle">Apache Iceberg over skade</text>
<text x="770" y="568" fill="#a6d4b6" font-size="11.5" text-anchor="middle">(redb catalog) — $HOME-derived</text>
<text x="770" y="586" fill="#a6d4b6" font-size="11.5" text-anchor="middle"><root>/<ws>/builds/warehouse</text>
<!-- viz / mcp -->
<rect x="950" y="500" width="240" height="110" rx="8" fill="url(#rpcchip)" stroke="#9a7330" stroke-width="1.5"/>
<text x="1070" y="528" fill="#f3dca6" font-size="14" font-weight="bold" text-anchor="middle">viz / MCP (thin)</text>
<text x="1070" y="552" fill="#e6c88a" font-size="11.5" text-anchor="middle">read it back over gRPC</text>
<text x="1070" y="568" fill="#bfa874" font-size="11.5" text-anchor="middle">Viz.* / Knowledge.* / Mimir.*</text>
<text x="1070" y="586" fill="#bfa874" font-size="11.5" text-anchor="middle">(read-only, lock-tolerant)</text>
<!-- ===== TRACES ===== -->
<g stroke="#3f7e9c" stroke-width="2" fill="none" marker-end="url(#arrow)">
<path d="M260,158 L316,158"/> <!-- operator → server -->
<path d="M530,158 L586,158"/> <!-- server → resolve -->
<path d="M710,220 L710,260 L660,260 L660,286"/> <!-- resolve → registry -->
<path d="M460,206 L460,250 L640,250 L640,286"/> <!-- server upsert → registry -->
<path d="M540,420 L165,420 L165,496"/> <!-- registry → poll/build (members) -->
<path d="M290,555 L346,555"/> <!-- poll → republish -->
<path d="M590,555 L646,555"/> <!-- republish → warehouse -->
<path d="M890,555 L946,555"/> <!-- warehouse → viz -->
</g>
<!-- poll loop self-arrow -->
<g stroke="#5f8aa3" stroke-width="1.5" fill="none" stroke-dasharray="4 3" marker-end="url(#arrow)">
<path d="M780,420 L780,470 L470,470 L470,496"/> <!-- registry → republish (descriptor/deep_scan) -->
</g>
<text x="980" y="445" fill="#6f9ab2" font-size="11">poll loop re-runs on each HEAD change → live updates over gRPC</text>
</svg>