.nornir/assets/release-flow.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1180 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="rail" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#173d57"/>
<stop offset="1" stop-color="#0c2438"/>
</linearGradient>
<linearGradient id="slot" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#123448"/>
<stop offset="1" stop-color="#0a2032"/>
</linearGradient>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#6fd3f5"/>
</marker>
</defs>
<rect x="0" y="0" width="1180" height="720" fill="url(#board)"/>
<g stroke="#143a52" stroke-width="1">
<line x1="0" y1="78" x2="1180" y2="78"/>
<line x1="0" y1="664" x2="1180" y2="664"/>
</g>
<g fill="#16415c">
<circle cx="40" cy="40" r="2"/><circle cx="1140" cy="40" r="2"/>
<circle cx="40" cy="684" r="2"/><circle cx="1140" cy="684" r="2"/>
</g>
<!-- title -->
<text x="40" y="40" fill="#eaf6ff" font-size="22" font-weight="700">nornir release flow</text>
<text x="40" y="62" fill="#9fc9e0" font-size="15">advise <tspan fill="#bfe6ff" font-weight="700">doctor</tspan> → fix <tspan fill="#bfe6ff" font-weight="700">bump</tspan> → rehearse in embedded <tspan fill="#7CFFB2" font-weight="700">/sparring</tspan> → promote to <tspan fill="#ff7a7a" font-weight="700">real crates.io</tspan> (typed confirm)</text>
<!-- legend -->
<g font-size="12.5">
<line x1="800" y1="58" x2="840" y2="58" stroke="#6fd3f5" stroke-width="3" marker-end="url(#arr)"/>
<text x="848" y="62" fill="#cfe6f5">flow</text>
<line x1="900" y1="58" x2="940" y2="58" stroke="#ff7a7a" stroke-width="2.5" stroke-dasharray="5 4"/>
<text x="948" y="62" fill="#cfe6f5">blocking / cut</text>
<rect x="1060" y="51" width="12" height="12" rx="3" fill="#7CFFB2"/><text x="1078" y="62" fill="#cfe6f5">go</text>
</g>
<!-- ===================== 1 · DOCTOR (advise) ===================== -->
<rect x="36" y="110" width="222" height="380" rx="12" fill="url(#chip)" stroke="#5aa6cf" stroke-width="2"/>
<text x="147" y="138" fill="#eaf6ff" font-size="15" font-weight="700" text-anchor="middle">1 · release doctor</text>
<text x="147" y="157" fill="#9fc9e0" font-size="10.5" text-anchor="middle">advisory · read-only · no mutation</text>
<rect x="56" y="174" width="182" height="34" rx="5" fill="url(#slot)" stroke="#ffcf6f" stroke-width="1.4"/>
<text x="147" y="189" fill="#ffcf6f" font-size="10.5" text-anchor="middle">🟡 dirty trees</text>
<text x="147" y="202" fill="#9fc9e0" font-size="9" text-anchor="middle">git status per repo</text>
<rect x="56" y="216" width="182" height="42" rx="5" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.4"/>
<text x="147" y="231" fill="#bfe6ff" font-size="10.5" text-anchor="middle">external dep skew</text>
<text x="147" y="244" fill="#7CFFB2" font-size="9" text-anchor="middle">💡 bump · <tspan fill="#9fc9e0">target = highest declared</tspan></text>
<text x="147" y="254" fill="#9fc9e0" font-size="9" text-anchor="middle">--forbid arrow=56 bans a major</text>
<rect x="56" y="266" width="182" height="42" rx="5" fill="url(#slot)" stroke="#ff7a7a" stroke-width="1.4"/>
<text x="147" y="281" fill="#ff7a7a" font-size="10.5" text-anchor="middle">⛔ held by transitive pin</text>
<text x="147" y="294" fill="#9fc9e0" font-size="9" text-anchor="middle">Cargo.lock dual-major tree:</text>
<text x="147" y="304" fill="#9fc9e0" font-size="9" text-anchor="middle">manifest bump won't take</text>
<rect x="56" y="316" width="182" height="42" rx="5" fill="url(#slot)" stroke="#ff7a7a" stroke-width="1.4"/>
<text x="147" y="331" fill="#ff7a7a" font-size="10.5" text-anchor="middle">✂ cycle-break advisor</text>
<text x="147" y="344" fill="#9fc9e0" font-size="9" text-anchor="middle">Tarjan SCC → cheapest</text>
<text x="147" y="354" fill="#9fc9e0" font-size="9" text-anchor="middle">edge to cut</text>
<rect x="56" y="366" width="182" height="34" rx="5" fill="url(#slot)" stroke="#ff7a7a" stroke-width="1.4"/>
<text x="147" y="381" fill="#ff7a7a" font-size="10.5" text-anchor="middle">✗ edition-2024 gate</text>
<text x="147" y="394" fill="#9fc9e0" font-size="9" text-anchor="middle">static + cargo 2024-lint pass</text>
<text x="147" y="424" fill="#86b3cc" font-size="10" text-anchor="middle">topo publish order +</text>
<text x="147" y="438" fill="#86b3cc" font-size="10" text-anchor="middle">blast radius (who to re-validate)</text>
<text x="147" y="464" fill="#a9d8ee" font-size="9.5" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">src/release/doctor.rs</text>
<text x="147" y="479" fill="#7fa8c4" font-size="9.5" text-anchor="middle">--rehearse bridges straight to 3</text>
<!-- doctor -> fix -->
<line x1="258" y1="300" x2="306" y2="300" stroke="#6fd3f5" stroke-width="3" marker-end="url(#arr)"/>
<!-- ===================== 2 · FIX (bump) ===================== -->
<rect x="306" y="150" width="210" height="220" rx="12" fill="url(#chip)" stroke="#5aa6cf" stroke-width="2"/>
<text x="411" y="178" fill="#eaf6ff" font-size="15" font-weight="700" text-anchor="middle">2 · Fix (applies)</text>
<text x="411" y="197" fill="#9fc9e0" font-size="10.5" text-anchor="middle">🔧 apply fixes → re-Advise</text>
<rect x="326" y="214" width="170" height="40" rx="5" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.4"/>
<text x="411" y="231" fill="#bfe6ff" font-size="10" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">apply_skew_bumps</text>
<text x="411" y="245" fill="#9fc9e0" font-size="9" text-anchor="middle">each CrateSkew → bump</text>
<rect x="326" y="262" width="170" height="40" rx="5" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.4"/>
<text x="411" y="279" fill="#bfe6ff" font-size="9.5" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">plan_version_bump</text>
<text x="411" y="293" fill="#bfe6ff" font-size="9.5" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">apply_bump_plan</text>
<text x="411" y="322" fill="#86b3cc" font-size="9.5" text-anchor="middle">cascade dependents' deps</text>
<text x="411" y="338" fill="#7CFFB2" font-size="9.5" text-anchor="middle">re-runs Advise so gates re-evaluate</text>
<text x="411" y="356" fill="#a9d8ee" font-size="9" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">fix_applied in state_json</text>
<!-- fix -> rehearse -->
<line x1="516" y1="260" x2="564" y2="260" stroke="#6fd3f5" stroke-width="3" marker-end="url(#arr)"/>
<!-- ===================== 3 · REHEARSE (/sparring) ===================== -->
<rect x="564" y="110" width="262" height="330" rx="12" fill="url(#rail)" stroke="#7CFFB2" stroke-width="2.5"/>
<text x="695" y="138" fill="#eaf6ff" font-size="15" font-weight="700" text-anchor="middle">3 · Rehearse</text>
<text x="695" y="157" fill="#7CFFB2" font-size="10.5" text-anchor="middle">embedded holger /sparring · ephemeral</text>
<rect x="586" y="176" width="218" height="44" rx="6" fill="url(#slot)" stroke="#7CFFB2" stroke-width="1.6"/>
<text x="695" y="193" fill="#bfe6ff" font-size="10.5" text-anchor="middle">bring up /sparring (in-process)</text>
<text x="695" y="207" fill="#9fc9e0" font-size="9" text-anchor="middle">EmbeddedHolger::start · owned runtime</text>
<rect x="586" y="228" width="218" height="48" rx="6" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.6"/>
<text x="695" y="245" fill="#bfe6ff" font-size="10.5" text-anchor="middle">publish each repo → /sparring</text>
<text x="695" y="258" fill="#9fc9e0" font-size="9" text-anchor="middle">topo order, dependencies first</text>
<text x="695" y="269" fill="#9fc9e0" font-size="9" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">cargo publish --registry sparring</text>
<rect x="586" y="284" width="218" height="48" rx="6" fill="url(#slot)" stroke="#6fd3f5" stroke-width="1.6"/>
<text x="695" y="301" fill="#bfe6ff" font-size="10.5" text-anchor="middle">build each dependent FROM /sparring</text>
<text x="695" y="314" fill="#9fc9e0" font-size="9" text-anchor="middle">proves the cascade resolves from a</text>
<text x="695" y="324" fill="#9fc9e0" font-size="9" text-anchor="middle">registry, not from path deps</text>
<text x="695" y="356" fill="#86b3cc" font-size="9.5" text-anchor="middle">source-replaces crates.io with /sparring;</text>
<text x="695" y="370" fill="#86b3cc" font-size="9.5" text-anchor="middle">everything else falls through upstream</text>
<text x="695" y="392" fill="#7CFFB2" font-size="10" text-anchor="middle" font-weight="700">GREEN → cleared to promote</text>
<text x="695" y="410" fill="#a9d8ee" font-size="9" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">src/release/stage.rs · holger torn down on return</text>
<text x="695" y="426" fill="#7fa8c4" font-size="9" text-anchor="middle">red rehearsal ⛔ blocks promote</text>
<!-- rehearse -> promote (gated) -->
<line x1="826" y1="250" x2="874" y2="250" stroke="#6fd3f5" stroke-width="3" marker-end="url(#arr)"/>
<!-- ===================== 4 · PROMOTE ===================== -->
<rect x="874" y="150" width="268" height="250" rx="12" fill="url(#chip)" stroke="#ff7a7a" stroke-width="2.5"/>
<text x="1008" y="178" fill="#eaf6ff" font-size="15" font-weight="700" text-anchor="middle">4 · Promote</text>
<text x="1008" y="197" fill="#ff7a7a" font-size="10.5" text-anchor="middle">IRREVERSIBLE · real crates.io</text>
<rect x="894" y="214" width="228" height="48" rx="6" fill="url(#slot)" stroke="#ff7a7a" stroke-width="1.6"/>
<text x="1008" y="231" fill="#ff7a7a" font-size="10.5" text-anchor="middle">strip [patch.crates-io]</text>
<text x="1008" y="244" fill="#9fc9e0" font-size="9" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">ensure_no_patch_crates_io</text>
<text x="1008" y="255" fill="#9fc9e0" font-size="9" text-anchor="middle">arrow-58 fork can't leak; asserts none remain</text>
<rect x="894" y="270" width="228" height="44" rx="6" fill="url(#slot)" stroke="#ff7a7a" stroke-width="1.6"/>
<text x="1008" y="287" fill="#ff7a7a" font-size="10.5" text-anchor="middle">typed confirm: PROMOTE</text>
<text x="1008" y="300" fill="#9fc9e0" font-size="9" text-anchor="middle">double-gated: rehearsal GREEN + phrase</text>
<text x="1008" y="332" fill="#86b3cc" font-size="9.5" text-anchor="middle">replays the SAME topo order against</text>
<text x="1008" y="346" fill="#86b3cc" font-size="9.5" text-anchor="middle">real crates.io</text>
<text x="1008" y="368" fill="#a9d8ee" font-size="9" text-anchor="middle" font-family="'DejaVu Sans Mono', monospace">release publish (non-dry) · viz spawn_promote</text>
<text x="1008" y="384" fill="#7fa8c4" font-size="9" text-anchor="middle">both paths call ensure_no_patch first</text>
<!-- ground rail: release run self-registers as a job -->
<rect x="36" y="540" width="1106" height="104" rx="10" fill="url(#rail)" stroke="#7fc8ea" stroke-width="2"/>
<text x="60" y="568" fill="#dff1ff" font-size="14" font-weight="700">the whole run is a ledger job</text>
<text x="60" y="590" fill="#bcd9ec" font-size="11">fat <tspan font-family="'DejaVu Sans Mono', monospace" fill="#bfe6ff">release run</tspan> opens a <tspan font-family="'DejaVu Sans Mono', monospace" fill="#7CFFB2">RELEASE_RUN</tspan> row (running → done|failed via the JobHandle Drop guard) in <tspan font-family="'DejaVu Sans Mono', monospace" fill="#bfe6ff">jobs.redb</tspan> —</text>
<text x="60" y="608" fill="#bcd9ec" font-size="11">parity with the server's <tspan font-family="'DejaVu Sans Mono', monospace" fill="#bfe6ff">Ops.RunRelease</tspan>. Single-writer: the fat process OR the server owns the ledger; the 🧬 viz observes it live by reading the same redb.</text>
<text x="60" y="630" fill="#7fa8c4" font-size="10.5" font-family="'DejaVu Sans Mono', monospace">advise = read-only · --rehearse = doctor + stage (one call path) · tests/release_bump_rehearse_promote.rs proves bump → rehearse-green → promote closes</text>
</svg>