.nornir/assets/release-convergence-planes.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1180 620" 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="planeC" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0e2a3e"/>
<stop offset="1" stop-color="#0a2032"/>
</linearGradient>
<linearGradient id="planeS" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#123a42"/>
<stop offset="1" stop-color="#0c2a30"/>
</linearGradient>
<linearGradient id="planeL" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#14384f"/>
<stop offset="1" stop-color="#0e2a3e"/>
</linearGradient>
<marker id="arrow" markerWidth="9" markerHeight="9" refX="7" refY="4" orient="auto">
<path d="M0 0 L8 4 L0 8 Z" fill="#6fd3f5"/>
</marker>
<filter id="glowRed" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowGrn" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect x="0" y="0" width="1180" height="620" fill="url(#board)"/>
<g stroke="#143a52" stroke-width="1">
<line x1="0" y1="80" x2="1180" y2="80"/>
<line x1="0" y1="576" x2="1180" y2="576"/>
</g>
<g fill="#16415c">
<circle cx="40" cy="42" r="2"/><circle cx="1140" cy="42" r="2"/>
<circle cx="40" cy="596" r="2"/><circle cx="1140" cy="596" r="2"/>
</g>
<!-- title -->
<text x="40" y="48" fill="#eaf6ff" font-size="22" font-weight="700">release convergence — three graphs, one target</text>
<text x="40" y="70" fill="#9fc9e0" font-size="14">the same dependency DAG in <tspan fill="#78b4ff" font-weight="700">LOCAL</tspan> · <tspan fill="#f0be5a" font-weight="700">SPARRING</tspan> · <tspan fill="#78d296" font-weight="700">CRATES_IO</tspan> — snap together as crates converge</text>
<!-- legend -->
<g font-size="12.5">
<rect x="812" y="34" width="12" height="12" rx="2" fill="none" stroke="#e04848" stroke-width="2"/>
<text x="830" y="44" fill="#c9dbe8">needs-release / lock-desync (red)</text>
<rect x="812" y="52" width="12" height="12" rx="2" fill="none" stroke="#f0b446" stroke-width="2"/>
<text x="830" y="62" fill="#c9dbe8">bump-drift (amber)</text>
<rect x="1012" y="34" width="12" height="12" rx="2" fill="none" stroke="#5ac88c" stroke-width="2"/>
<text x="1030" y="44" fill="#c9dbe8">converged (green)</text>
<text x="1030" y="62" fill="#7fa8bf">✓ = planes coincide</text>
</g>
<!-- ============ LEFT: DIVERGED (planes split) ============ -->
<text x="70" y="118" fill="#bfe6ff" font-size="15" font-weight="700">DIVERGED — planes split, markers show where to fix</text>
<!-- three receding planes (back = cratesio, front = local) -->
<!-- CRATES_IO plane (back) -->
<polygon points="150,210 470,190 500,300 180,320" fill="url(#planeC)" stroke="#3a6f4f" stroke-width="1" opacity="0.85"/>
<text x="156" y="205" fill="#78d296" font-size="12" font-weight="700">CRATES_IO</text>
<!-- SPARRING plane (mid) -->
<polygon points="130,250 450,230 480,340 160,360" fill="url(#planeS)" stroke="#8a7a40" stroke-width="1" opacity="0.9"/>
<text x="136" y="245" fill="#f0be5a" font-size="12" font-weight="700">SPARRING</text>
<!-- LOCAL plane (front) -->
<polygon points="110,290 430,270 460,380 140,400" fill="url(#planeL)" stroke="#3f6f92" stroke-width="1"/>
<text x="116" y="285" fill="#78b4ff" font-size="12" font-weight="700">LOCAL</text>
<!-- a crate that NEEDS RELEASE: split across planes, red beam -->
<g filter="url(#glowRed)">
<line x1="300" y1="230" x2="270" y2="330" stroke="#e04848" stroke-width="2.5"/>
</g>
<rect x="286" y="222" width="30" height="18" rx="3" fill="#0e2a3e" stroke="#78d296" stroke-width="1.4" opacity="0.7"/>
<rect x="270" y="262" width="30" height="18" rx="3" fill="#0e2a3e" stroke="#f0be5a" stroke-width="1.4" opacity="0.8"/>
<rect x="252" y="315" width="40" height="26" rx="4" fill="#132f45" stroke="#e04848" stroke-width="2.2"/>
<text x="272" y="332" fill="#eaf6ff" font-size="10.5" text-anchor="middle">dwarves</text>
<text x="272" y="360" fill="#e04848" font-size="9.5" text-anchor="middle">needs-release</text>
<!-- a crate that is BUMP-DRIFT: split local vs sparring, amber -->
<line x1="390" y1="292" x2="405" y2="252" stroke="#f0b446" stroke-width="2.2"/>
<rect x="392" y="244" width="30" height="18" rx="3" fill="#0e2a3e" stroke="#f0be5a" stroke-width="1.6"/>
<rect x="374" y="300" width="40" height="26" rx="4" fill="#132f45" stroke="#f0b446" stroke-width="2.2"/>
<text x="394" y="317" fill="#eaf6ff" font-size="10.5" text-anchor="middle">skade</text>
<text x="394" y="344" fill="#f0b446" font-size="9.5" text-anchor="middle">bump-drift</text>
<!-- ============ RIGHT: CONVERGED (planes coincide) ============ -->
<text x="700" y="118" fill="#bfe6ff" font-size="15" font-weight="700">CONVERGED — the three planes coincide (green)</text>
<polygon points="720,300 1040,280 1070,390 750,410" fill="url(#planeL)" stroke="#3f6f92" stroke-width="1"/>
<polygon points="720,300 1040,280 1070,390 750,410" fill="none" stroke="#5ac88c" stroke-width="1" opacity="0.5"/>
<text x="726" y="295" fill="#8fd8b0" font-size="12" font-weight="700">LOCAL = SPARRING = CRATES_IO</text>
<!-- converged crate nodes on the single plane -->
<g filter="url(#glowGrn)">
<rect x="770" y="320" width="60" height="30" rx="5" fill="#12352a" stroke="#5ac88c" stroke-width="2.2"/>
</g>
<text x="800" y="339" fill="#eaf6ff" font-size="10.5" text-anchor="middle">edda</text>
<text x="822" y="326" fill="#5ac88c" font-size="12">✓</text>
<g filter="url(#glowGrn)">
<rect x="880" y="330" width="60" height="30" rx="5" fill="#12352a" stroke="#5ac88c" stroke-width="2.2"/>
</g>
<text x="910" y="349" fill="#eaf6ff" font-size="10.5" text-anchor="middle">dwarves</text>
<text x="932" y="336" fill="#5ac88c" font-size="12">✓</text>
<g filter="url(#glowGrn)">
<rect x="960" y="315" width="60" height="30" rx="5" fill="#12352a" stroke="#5ac88c" stroke-width="2.2"/>
</g>
<text x="990" y="334" fill="#eaf6ff" font-size="10.5" text-anchor="middle">holger</text>
<text x="1012" y="321" fill="#5ac88c" font-size="12">✓</text>
<line x1="880" y1="345" x2="830" y2="335" stroke="#3f6f92" stroke-width="1.4" marker-end="url(#arrow)"/>
<line x1="960" y1="330" x2="940" y2="345" stroke="#3f6f92" stroke-width="1.4" marker-end="url(#arrow)"/>
<!-- convergence arrow between the two scenes -->
<path d="M500 340 C 600 360, 640 350, 715 350" fill="none" stroke="#6fd3f5" stroke-width="2.5" marker-end="url(#arrow)" stroke-dasharray="7 5"/>
<text x="600" y="336" fill="#9fdcf5" font-size="12.5" text-anchor="middle" font-weight="700">release cascade →</text>
<text x="600" y="372" fill="#7fa8bf" font-size="11" text-anchor="middle">apply(PublishStart · Published · LocalBumped) · node-by-node</text>
<!-- footer: the model + derivation -->
<g font-size="12.5">
<rect x="70" y="470" width="1040" height="86" rx="8" fill="#0a2233" stroke="#173d57" stroke-width="1"/>
<text x="88" y="494" fill="#bfe6ff" font-weight="700">engine-agnostic model (one-way, like the runa adapter):</text>
<text x="88" y="516" fill="#9fc9e0" font-family="monospace" font-size="12">dwarves graph_diff + 3 version sources → ReleaseConvergence { nodes: CrateConvergence{L,S,C,status}, edges } → ReleaseView</text>
<text x="88" y="538" fill="#7fa8bf" font-family="monospace" font-size="11.5">derive_status: L==S==C ⇒ Converged · C<L ⇒ NeedsRelease · S≠C ⇒ LockDesync · S≠L ⇒ BumpDrift · (live) Publishing</text>
</g>
</svg>