Add storyboard handout for split-loop-with-lambda mechanics

Step-by-step refactoring instructions as a single markdown handout with
one diagram per step (legend + steps 0-3, overload inset, pure test,
debrief slides for the dead call and the drift anti-pattern). Derived
from the pattern draft, the learning hour plan, and the rehearsal
findings; slide-ready for later.
This commit is contained in:
Your Name
2026-09-27 18:04:33 +00:00
parent b993f25c53
commit 231f6aa72f
10 changed files with 779 additions and 0 deletions
@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" 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="#dc2626"/>
</marker>
</defs>
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 0 — one method, three loops, one unwanted dependency</text>
<text x="24" y="56" font-size="13" fill="#475569">The weather loop reaches outside the method to Scorer (which uses Random) — that is why the test is red &amp; flaky.</text>
<!-- method -->
<rect x="60" y="74" width="340" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">longRangeForecast()</text>
<!-- reset loop -->
<rect x="80" y="112" width="300" height="56" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="134" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (IceCream flavour : IceCream.values())</text>
<text x="94" y="154" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"> result.put(flavour, 0);</text>
<!-- weather loop -->
<rect x="80" y="184" width="300" height="112" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="206" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (Instant interestingDate : DATES)</text>
<text x="94" y="228" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626">scorer.updateSelection();</text>
<text x="94" y="248" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">daysForward = DAYS.between(ANCHOR, date)</text>
<text x="94" y="268" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626">weather = scorer.lookupWeather(daysForward);</text>
<!-- scoring loop -->
<rect x="80" y="312" width="300" height="104" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="334" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (flavour : keySet) result += 10</text>
<text x="94" y="356" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">if (sunnyHolidays &gt; 2 &amp;&amp; Vanilla) += 5</text>
<text x="94" y="376" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">if (sunnyHolidays &gt; 1 &amp;&amp; weather[1]) += 2</text>
<text x="94" y="402" font-size="10" fill="#64748b">scoring loop</text>
<!-- Scorer -->
<rect x="580" y="200" width="150" height="80" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
<text x="655" y="230" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
<text x="655" y="250" font-size="11" fill="#7f1d1d" text-anchor="middle">stands for the weather</text>
<text x="655" y="266" font-size="11" fill="#7f1d1d" text-anchor="middle">service — uses Random</text>
<line x1="384" y1="228" x2="572" y2="222" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
<line x1="384" y1="268" x2="572" y2="254" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
<!-- test -->
<rect x="580" y="330" width="150" height="60" rx="8" fill="#ffffff" stroke="#94a3b8" stroke-dasharray="4 3"/>
<text x="655" y="354" font-size="12" fill="#334155" text-anchor="middle">approval test</text>
<text x="655" y="372" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">red &amp; flaky ✗</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" 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="#dc2626"/>
</marker>
</defs>
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 1 — extract the dependency-using code into a lambda</text>
<text x="24" y="56" font-size="13" fill="#dc2626" font-weight="bold">Both scorer calls go inside the lambda — the void one too. The invocation stays in the loop.</text>
<!-- method -->
<rect x="60" y="74" width="340" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">longRangeForecast()</text>
<!-- reset loop -->
<rect x="80" y="112" width="300" height="56" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="134" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (IceCream flavour : IceCream.values())</text>
<text x="94" y="154" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"> result.put(flavour, 0);</text>
<!-- weather loop -->
<rect x="80" y="184" width="300" height="144" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="204" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (Instant interestingDate : DATES)</text>
<!-- lambda (inside loop, where the code used to be) -->
<rect x="94" y="214" width="256" height="74" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
<text x="106" y="234" font-size="12" font-weight="bold" fill="#9333ea">λ dayOffsetToWeather</text>
<text x="106" y="254" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">scorer.updateSelection();</text>
<text x="106" y="272" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">return scorer.lookupWeather(days);</text>
<!-- invocation -->
<circle cx="90" cy="308" r="4" fill="#9333ea"/>
<text x="102" y="312" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#6b21a8">expectedWeather.add(λ.apply(daysForward));</text>
<!-- scoring loop -->
<rect x="80" y="344" width="300" height="80" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="368" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (flavour : keySet) result += 10</text>
<text x="94" y="388" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">+5 / +2 ifs, unchanged</text>
<text x="94" y="410" font-size="10" fill="#64748b">scoring loop</text>
<!-- Scorer -->
<rect x="580" y="200" width="150" height="80" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
<text x="655" y="230" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
<text x="655" y="250" font-size="11" fill="#7f1d1d" text-anchor="middle">still called —</text>
<text x="655" y="266" font-size="11" fill="#7f1d1d" text-anchor="middle">but only via the λ now</text>
<line x1="354" y1="244" x2="572" y2="228" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
<line x1="354" y1="272" x2="572" y2="256" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" 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="#dc2626"/>
</marker>
<marker id="arrP" 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="#9333ea"/>
</marker>
</defs>
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 2 — slide the lambda definition to the top of the method</text>
<text x="24" y="56" font-size="13" fill="#9333ea" font-weight="bold">Slide, don't move: the definition slides up, the invocation stays exactly where it was.</text>
<!-- method -->
<rect x="60" y="74" width="340" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">longRangeForecast()</text>
<!-- lambda at top -->
<rect x="94" y="104" width="256" height="74" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
<text x="106" y="124" font-size="12" font-weight="bold" fill="#9333ea">λ dayOffsetToWeather</text>
<text x="106" y="144" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">scorer.updateSelection();</text>
<text x="106" y="162" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">return scorer.lookupWeather(days);</text>
<!-- reset loop -->
<rect x="80" y="196" width="300" height="56" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="218" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (IceCream flavour : IceCream.values())</text>
<text x="94" y="238" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"> result.put(flavour, 0);</text>
<!-- weather loop -->
<rect x="80" y="268" width="300" height="100" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="290" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (Instant interestingDate : DATES)</text>
<!-- ghost of old position -->
<rect x="94" y="300" width="256" height="28" rx="6" fill="none" stroke="#9333ea" stroke-width="1" stroke-dasharray="4 3"/>
<text x="106" y="318" font-size="10" fill="#9333ea" font-style="italic">λ used to be defined here</text>
<circle cx="90" cy="352" r="4" fill="#9333ea"/>
<text x="102" y="356" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#6b21a8">expectedWeather.add(λ.apply(daysForward));</text>
<!-- scoring loop -->
<rect x="80" y="384" width="300" height="40" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="94" y="409" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">scoring loop: += 10 / +5 / +2</text>
<!-- slide arrow -->
<line x1="219" y1="298" x2="219" y2="184" stroke="#9333ea" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrP)"/>
<text x="232" y="192" font-size="12" font-weight="bold" fill="#9333ea">slide</text>
<!-- Scorer -->
<rect x="580" y="200" width="150" height="80" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
<text x="655" y="230" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
<text x="655" y="250" font-size="11" fill="#7f1d1d" text-anchor="middle">still called —</text>
<text x="655" y="266" font-size="11" fill="#7f1d1d" text-anchor="middle">but only via the λ now</text>
<line x1="354" y1="130" x2="572" y2="226" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
<line x1="354" y1="160" x2="572" y2="250" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,67 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" 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="#dc2626"/>
</marker>
<marker id="arrP" 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="#9333ea"/>
</marker>
</defs>
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 3 — extract the weather loop; the lambda becomes a parameter</text>
<text x="24" y="56" font-size="13" fill="#475569">Extract Method on the expectedWeather block. Checkpoint: grep scorer → no hits; try static → compiles.</text>
<!-- left: original method -->
<rect x="50" y="74" width="300" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="66" y="96" font-size="14" font-weight="bold" fill="#0f172a">longRangeForecast()</text>
<!-- lambda at top -->
<rect x="64" y="104" width="272" height="68" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
<text x="78" y="126" font-size="12" font-weight="bold" fill="#9333ea">λ dayOffsetToWeather</text>
<text x="78" y="146" font-size="10" font-family="Menlo, Consolas, monospace" fill="#dc2626">scorer.updateSelection();</text>
<text x="78" y="162" font-size="10" font-family="Menlo, Consolas, monospace" fill="#dc2626">return scorer.lookupWeather(daysForward);</text>
<!-- reset loop -->
<rect x="64" y="188" width="272" height="44" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="78" y="214" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">reset loop: result.put(flavour, 0)</text>
<!-- call site -->
<rect x="64" y="248" width="272" height="48" rx="8" fill="#ffffff" stroke="#475569" stroke-width="1.5"/>
<text x="78" y="268" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">expectedWeather =</text>
<text x="78" y="286" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">collectExpectedWeather(<tspan fill="#9333ea" font-weight="bold">λ</tspan>)</text>
<!-- scoring loop -->
<rect x="64" y="312" width="272" height="84" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="78" y="334" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (flavour : keySet) result += 10</text>
<text x="78" y="354" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">if (sunny &gt; 2 &amp;&amp; Vanilla) += 5</text>
<text x="78" y="374" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">if (sunny &gt; 1 &amp;&amp; weather[1]) += 2</text>
<!-- purple arrow: lambda becomes parameter -->
<line x1="340" y1="272" x2="452" y2="146" stroke="#9333ea" stroke-width="1.5" marker-end="url(#arrP)"/>
<text x="396" y="214" font-size="10" font-weight="bold" fill="#9333ea" text-anchor="middle" transform="rotate(-48 396 214)">λ becomes a parameter</text>
<!-- right: extracted method -->
<rect x="440" y="74" width="290" height="280" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="456" y="96" font-size="13" font-weight="bold" fill="#0f172a">collectExpectedWeather</text>
<text x="456" y="112" font-size="11" fill="#475569">static — no fields, all inputs are parameters</text>
<!-- parameter pill -->
<rect x="456" y="122" width="258" height="32" rx="16" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
<text x="585" y="142" font-size="11" font-weight="bold" fill="#9333ea" text-anchor="middle">λ LongFunction&lt;Boolean&gt; dayOffsetToWeather</text>
<!-- weather loop -->
<rect x="456" y="166" width="258" height="110" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="470" y="190" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (Instant d : DATES)</text>
<text x="470" y="212" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">daysForward = between(ANCHOR, d)</text>
<text x="470" y="234" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">expected.add(<tspan fill="#9333ea" font-weight="bold">λ</tspan>.apply(daysForward))</text>
<text x="470" y="260" font-size="10" fill="#64748b">no scorer reference in here</text>
<text x="456" y="308" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">return expectedWeather;</text>
<!-- scorer, out of reach -->
<rect x="560" y="386" width="160" height="50" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="640" y="408" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
<text x="640" y="426" font-size="10" fill="#7f1d1d" text-anchor="middle">out of reach from in here</text>
<line x1="640" y1="384" x2="640" y2="360" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
<text x="650" y="376" font-size="10" font-weight="bold" fill="#dc2626">✗ cannot reach in</text>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -0,0 +1,53 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" 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="#dc2626"/>
</marker>
<marker id="arrG" 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="#475569"/>
</marker>
</defs>
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Inset — the overload: let the λ come from outside</text>
<text x="24" y="56" font-size="13" fill="#475569">Add the λ as a parameter (Change Signature), drop the local. A no-arg overload bakes in the real λ — callers unchanged.</text>
<!-- left: no-arg overload -->
<rect x="50" y="90" width="320" height="250" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="66" y="114" font-size="13" font-weight="bold" fill="#0f172a">longRangeForecast()</text>
<text x="66" y="130" font-size="10" fill="#475569">no-arg overload — existing callers keep working</text>
<rect x="66" y="144" width="288" height="92" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
<text x="80" y="166" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#9333ea">daysForward -&gt; {</text>
<text x="80" y="186" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626"> scorer.updateSelection();</text>
<text x="80" y="206" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626"> return scorer.lookupWeather(days);</text>
<text x="80" y="226" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#9333ea">}</text>
<text x="66" y="278" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">return longRangeForecast(<tspan fill="#9333ea" font-weight="bold">λ</tspan>);</text>
<text x="66" y="316" font-size="10" fill="#64748b">the "real" weather lambda lives here</text>
<!-- delegate arrow -->
<line x1="374" y1="150" x2="424" y2="150" stroke="#475569" stroke-width="1.5" marker-end="url(#arrG)"/>
<text x="376" y="140" font-size="10" fill="#475569">delegates</text>
<!-- right: the seam -->
<rect x="430" y="90" width="300" height="250" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="446" y="114" font-size="12" font-weight="bold" fill="#0f172a">longRangeForecast(<tspan fill="#9333ea">λ</tspan>)</text>
<text x="446" y="132" font-size="10" fill="#475569">the seam: weather is a parameter now</text>
<rect x="446" y="150" width="268" height="40" rx="8" fill="#ffffff" stroke="#475569" stroke-width="1"/>
<text x="460" y="166" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">expectedWeather =</text>
<text x="460" y="182" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a"> collectExpectedWeather(<tspan fill="#9333ea" font-weight="bold">λ</tspan>)</text>
<rect x="446" y="206" width="268" height="40" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="460" y="230" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">reset loop: result.put(flavour, 0)</text>
<rect x="446" y="262" width="268" height="56" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="460" y="284" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">scoring loop: += 10 / +5 / +2</text>
<text x="460" y="304" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return result;</text>
<!-- scorer still reachable from the no-arg overload only -->
<rect x="80" y="380" width="180" height="56" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
<text x="170" y="402" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
<text x="170" y="422" font-size="10" fill="#7f1d1d" text-anchor="middle">only the no-arg overload knows it</text>
<line x1="170" y1="378" x2="170" y2="346" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" 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="#dc2626"/>
</marker>
<marker id="arrGr" 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="#16a34a"/>
</marker>
</defs>
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 5-lite — test through the seam with a pure lambda</text>
<text x="24" y="56" font-size="13" fill="#16a34a" font-weight="bold">The test passes alwaysSunny. Scorer is never touched: no Random, deterministic, glanceable.</text>
<!-- test -->
<rect x="50" y="160" width="230" height="130" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="66" y="186" font-size="13" font-weight="bold" fill="#14532d">LongRangeForecastsTest</text>
<text x="66" y="210" font-size="10" fill="#475569">LongFunction&lt;Boolean&gt;</text>
<text x="66" y="232" font-size="12" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#16a34a">alwaysSunny = days -&gt; true</text>
<text x="66" y="258" font-size="11" font-family="Menlo, Consolas, monospace" fill="#14532d">longRangeForecast(alwaysSunny)</text>
<!-- middle: overload -->
<rect x="340" y="150" width="180" height="170" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="354" y="176" font-size="11" font-weight="bold" fill="#0f172a">longRangeForecast(<tspan fill="#9333ea">λ</tspan>)</text>
<text x="354" y="204" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">collectExpected-</text>
<text x="354" y="220" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">Weather(<tspan fill="#9333ea" font-weight="bold">λ</tspan>)</text>
<text x="354" y="248" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">reset loop</text>
<text x="354" y="274" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">scoring loop</text>
<text x="354" y="296" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">+= 10 / +5 / +2</text>
<!-- right: extracted method -->
<rect x="560" y="150" width="170" height="170" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="574" y="176" font-size="10" font-weight="bold" fill="#0f172a">collectExpectedWeather(λ)</text>
<text x="574" y="204" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for d : DATES</text>
<text x="574" y="226" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">expected.add(<tspan fill="#9333ea" font-weight="bold">λ</tspan>.apply(d))</text>
<text x="574" y="252" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return expectedWeather</text>
<text x="574" y="290" font-size="10" fill="#16a34a" font-weight="bold">pure ✓</text>
<!-- green arrows -->
<line x1="284" y1="225" x2="334" y2="225" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrGr)"/>
<text x="286" y="214" font-size="10" fill="#16a34a">alwaysSunny</text>
<line x1="524" y1="225" x2="554" y2="225" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrGr)"/>
<!-- scorer, never called -->
<rect x="560" y="370" width="170" height="56" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.8"/>
<text x="645" y="392" font-size="12" font-weight="bold" fill="#94a3b8" text-anchor="middle">Scorer</text>
<text x="645" y="412" font-size="10" fill="#64748b" text-anchor="middle">never called by the test ✗</text>
<line x1="645" y1="368" x2="645" y2="326" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
<text x="655" y="350" font-size="11" font-weight="bold" fill="#dc2626">✗</text>
<text x="50" y="430" font-size="12" fill="#475569">Approved: {Strawberry:12, Chocolate:12, Vanilla:17, } — every run, not just a lucky roll.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Debrief — the lambda made dead code visible</text>
<text x="24" y="56" font-size="13" fill="#475569">updateSelection() re-rolls Scorer.flavour — state nothing in this method ever reads. Delete it; the approved file is unchanged.</text>
<!-- lambda box -->
<rect x="200" y="110" width="360" height="124" rx="10" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
<text x="216" y="136" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#9333ea">daysForward -&gt; {</text>
<text x="216" y="162" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">scorer.updateSelection();</text>
<line x1="216" y1="157" x2="408" y2="157" stroke="#dc2626" stroke-width="1.5"/>
<text x="420" y="162" font-size="10" font-weight="bold" fill="#dc2626">✗ delete me</text>
<text x="216" y="188" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return scorer.lookupWeather(daysForward);</text>
<text x="216" y="212" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#9333ea">}</text>
<!-- result -->
<text x="200" y="290" font-size="13" font-weight="bold" fill="#16a34a">✓ approved file unchanged:</text>
<text x="200" y="316" font-size="13" font-family="Menlo, Consolas, monospace" fill="#166534">{Strawberry:12, Chocolate:12, Vanilla:17, }</text>
<!-- explanation -->
<rect x="200" y="350" width="360" height="72" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-width="1"/>
<text x="216" y="374" font-size="11" fill="#334155">Why: Scorer.flavour's only real consumer is the unrelated</text>
<text x="216" y="392" font-size="11" fill="#334155">salesForecast(). In the long-range loop it was copy-paste</text>
<text x="216" y="410" font-size="11" fill="#334155">residue — visible only once the lambda made it explicit.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,40 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" 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="#dc2626"/>
</marker>
</defs>
<rect width="760" height="460" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Anti-pattern — the drift: scorer left behind in the extracted method</text>
<text x="24" y="56" font-size="13" fill="#dc2626" font-weight="bold">Lambda-ize only the value-producing call, and the dependency travels with the extraction.</text>
<text x="24" y="74" font-size="13" fill="#dc2626" font-weight="bold">Happened in 2 of 6 agent rehearsal runs — behaviour passed, design goal silently failed.</text>
<!-- drifted extracted method -->
<rect x="250" y="110" width="300" height="190" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="266" y="134" font-size="12" font-weight="bold" fill="#0f172a">collectExpectedWeather(<tspan fill="#9333ea">λ</tspan>)</text>
<text x="516" y="136" font-size="22" font-weight="bold" fill="#dc2626">✗</text>
<rect x="266" y="148" width="268" height="110" rx="8" fill="#eef2ff" stroke="#818cf8"/>
<text x="280" y="172" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (Instant d : DATES)</text>
<text x="280" y="196" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626">scorer.updateSelection(); ✗</text>
<text x="280" y="218" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">daysForward = between(ANCHOR, d)</text>
<text x="280" y="240" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">expected.add(<tspan fill="#9333ea" font-weight="bold">λ</tspan>.apply(daysForward))</text>
<text x="266" y="284" font-size="10" fill="#dc2626">not static anymore — it uses the field</text>
<!-- scorer, still wired in -->
<rect x="600" y="140" width="130" height="56" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
<text x="665" y="162" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
<text x="665" y="182" font-size="10" fill="#7f1d1d" text-anchor="middle">still wired in</text>
<line x1="552" y1="192" x2="594" y2="172" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrR)"/>
<!-- what it breaks -->
<text x="250" y="330" font-size="12" fill="#475569">Behaviour looks fine (the test can still pass) — but the split is cosmetic:</text>
<text x="250" y="350" font-size="12" fill="#475569">step 4, moving the method to a class without Scorer, is now blocked.</text>
<!-- catch it -->
<rect x="250" y="374" width="310" height="72" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1.5"/>
<text x="266" y="398" font-size="11" font-weight="bold" fill="#92400e">Catch it mechanically:</text>
<text x="266" y="418" font-size="11" fill="#92400e">• grep scorer on the extracted method → no hits</text>
<text x="266" y="436" font-size="11" fill="#92400e">• try static → won't compile with a field ref</text>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 470" font-family="Helvetica, Arial, sans-serif">
<defs>
<marker id="arrR" 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="#dc2626"/>
</marker>
<marker id="arrP" 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="#9333ea"/>
</marker>
<marker id="arrG" 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="#475569"/>
</marker>
</defs>
<rect width="760" height="470" fill="#ffffff"/>
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">How to read the diagrams</text>
<rect x="50" y="58" width="110" height="32" rx="8" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="180" y="79" font-size="13" fill="#334155">composed method / class under test</text>
<rect x="50" y="106" width="110" height="32" rx="8" fill="#eef2ff" stroke="#818cf8" stroke-width="1.5"/>
<text x="180" y="127" font-size="13" fill="#334155">loop — the thing we split / extract</text>
<rect x="50" y="154" width="110" height="32" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
<text x="180" y="175" font-size="13" fill="#334155">unwanted dependency — Scorer ≈ weather service</text>
<line x1="50" y1="216" x2="160" y2="216" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
<text x="180" y="221" font-size="13" fill="#334155">dependency call — the thing that wants to disappear</text>
<rect x="50" y="250" width="110" height="32" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
<text x="180" y="271" font-size="13" fill="#334155">λ — a function value</text>
<rect x="50" y="298" width="110" height="32" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="180" y="319" font-size="13" fill="#334155">pure / test</text>
<line x1="50" y1="360" x2="160" y2="360" stroke="#9333ea" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrP)"/>
<text x="180" y="365" font-size="13" fill="#334155">slide — definition moves, invocation stays</text>
<line x1="50" y1="408" x2="160" y2="408" stroke="#475569" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrG)"/>
<text x="180" y="413" font-size="13" fill="#334155">extraction / other motion</text>
<text x="24" y="452" font-size="12" fill="#64748b">Code lines inside boxes are monospace. Red = the unwanted dependency; purple = the lambda and everything it makes possible.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB