<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.2">Jekyll</generator><link href="https://www.germanvelasco.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.germanvelasco.com/" rel="alternate" type="text/html" /><updated>2026-09-03T09:38:25+00:00</updated><id>https://www.germanvelasco.com/feed.xml</id><title type="html">German Velasco</title><subtitle>Software developer and consultant. Let&apos;s build great software together.</subtitle><author><name>German Velasco</name></author><entry><title type="html">Don’t niche down. Position yourself.</title><link href="https://www.germanvelasco.com/blog/dont-niche-down-position-yourself" rel="alternate" type="text/html" title="Don’t niche down. Position yourself." /><published>2026-09-03T00:00:00+00:00</published><updated>2026-09-03T00:00:00+00:00</updated><id>https://www.germanvelasco.com/blog/dont-niche-down-position-yourself</id><content type="html" xml:base="https://www.germanvelasco.com/blog/dont-niche-down-position-yourself">&lt;figure&gt;
    &lt;img class=&quot;&quot; src=&quot;/images/position-yourself/chess.jpg&quot; alt=&quot;Moving chess piece&quot; /&gt;
    &lt;figcaption class=&quot;text-center&quot;&gt;Image by &lt;a href=&quot;https://unsplash.com/@wanderfleur&quot;&gt;Wander Fleur&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I’ve always heard it said, “Niche down!”&lt;/p&gt;

&lt;p&gt;But that never felt right to me.&lt;/p&gt;

&lt;p&gt;I mean, sure, niching down means you can be a bigger fish in a smaller pond, and
&lt;em&gt;maybe&lt;/em&gt; that leads to a better customer fit and more sales. But what you
actually want &lt;em&gt;is&lt;/em&gt; the better fit with customers, regardless of whether or not
that’s in a smaller pond.&lt;/p&gt;

&lt;p&gt;So, instead of niching down, &lt;em&gt;position&lt;/em&gt; yourself.&lt;/p&gt;

&lt;p&gt;What you are trying to capture is a place in the mind of the customer
(positioning), not a small pond to swim in (niching down).&lt;/p&gt;

&lt;p&gt;(Of course, in the process of positioning yourself, you might niche down. But if
that happens, that’s a side effect! It’s not the aim.)&lt;/p&gt;

&lt;h2 id=&quot;what-is-positioning&quot;&gt;What is positioning?&lt;/h2&gt;

&lt;p&gt;The first thing to understand is that your position isn’t yours. It exists in
the mind of the customer. The second thing to understand is that in your
customer’s mind, there is only ever one &lt;em&gt;number one&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;When I say “Kleenex”, you think tissue. It’s become almost synonymous because
they own the position.&lt;/p&gt;

&lt;p&gt;I imagine it’s the same in whatever space your product or service is trying to
fit in: there are already players there, and unless it’s a new market, there’s
already a leader.&lt;/p&gt;

&lt;p&gt;So, if you want prospective customers to even think of you, you have to ask
yourself: &lt;strong&gt;what makes you unique and better than what’s already out there?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Is your product faster than the competition’s?&lt;/li&gt;
  &lt;li&gt;Is it easier to use?&lt;/li&gt;
  &lt;li&gt;Is it higher quality?&lt;/li&gt;
  &lt;li&gt;Do you have a different way of solving a problem?&lt;/li&gt;
  &lt;li&gt;Something else?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&quot;https://www.goodreads.com/en/book/show/760025.Positioning&quot;&gt;&lt;em&gt;Positioning&lt;/em&gt;&lt;/a&gt; is about finding &lt;em&gt;how&lt;/em&gt; you are the best at something in a given
market, and telling prospective customers that.&lt;/p&gt;

&lt;p&gt;Then, your customers will understand at a glance what your product or service
does, and how it can help them.&lt;/p&gt;

&lt;p&gt;The beautiful thing is that once you find your position, you can also
re-position the competition! If your product is fast, theirs becomes the slow
one. If yours is easy to use, theirs is the complex one.&lt;/p&gt;</content><author><name>German Velasco</name></author><category term="blog" /><category term="positioning" /><summary type="html">I&apos;ve always heard it said, &quot;Niche down!&quot; But that never felt right to me. Instead of niching down, position yourself.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.germanvelasco.com/images/position-yourself/chess.jpg" /><media:content medium="image" url="https://www.germanvelasco.com/images/position-yourself/chess.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Diverge then converge with LLMs</title><link href="https://www.germanvelasco.com/blog/diverge-then-converge-with-llms" rel="alternate" type="text/html" title="Diverge then converge with LLMs" /><published>2026-08-03T00:00:00+00:00</published><updated>2026-08-03T00:00:00+00:00</updated><id>https://www.germanvelasco.com/blog/diverge-then-converge-with-llms</id><content type="html" xml:base="https://www.germanvelasco.com/blog/diverge-then-converge-with-llms">&lt;p&gt;One of the patterns I’ve found particularly helpful with LLMs is the
diverge-then-converge pattern.&lt;/p&gt;

&lt;h2 id=&quot;diverge--converge&quot;&gt;Diverge &amp;amp; Converge&lt;/h2&gt;

&lt;p&gt;Since LLMs are neural networks, when we give agents slightly different prompts,
we make their “thinking” travel through different neural pathways (at least,
that’s how I think of it).&lt;/p&gt;

&lt;p&gt;That makes LLMs really helpful at suggesting different solutions to a problem.&lt;/p&gt;

&lt;figure&gt;
    &lt;style&gt;
    .neural-graphic {
      --bg: #0b1117;
      --network: rgba(185, 197, 210, 0.18);
      --network-node: rgba(196, 208, 220, 0.24);
      --signal: #6ee7ff;
      --signal-soft: rgba(110, 231, 255, 0.32);

      box-sizing: border-box;
      width: 100%;
      max-width: 900px;
      margin: 0 auto;
      overflow: hidden;
      border-radius: 18px;
      background:
        radial-gradient(circle at 18% 50%, rgba(110, 231, 255, 0.055), transparent 24%),
        radial-gradient(circle at 72% 44%, rgba(110, 231, 255, 0.025), transparent 30%),
        var(--bg);
      box-shadow: inset 0 0 0 1px rgba(255,255,255,0.035);
    }

    .neural-graphic,
    .neural-graphic * {
      box-sizing: border-box;
    }

    .neural-graphic svg {
      display: block;
      width: 100%;
      height: auto;
      aspect-ratio: 16 / 7;
    }

    .neural-graphic .network-path {
      fill: none;
      stroke: var(--network);
      stroke-width: 1.35;
      stroke-linecap: round;
      stroke-linejoin: round;
    }

    .neural-graphic .node {
      fill: var(--network-node);
    }

    .neural-graphic .active-path {
      fill: none;
      stroke: var(--signal);
      stroke-width: 3.1;
      stroke-linecap: round;
      stroke-linejoin: round;
      filter: url(#glow);
      opacity: 0;
    }

    .neural-graphic .wake {
      fill: none;
      stroke: var(--signal-soft);
      stroke-width: 7;
      stroke-linecap: round;
      filter: url(#blurGlow);
      opacity: 0;
    }

    @media (prefers-reduced-motion: reduce) {
      .neural-graphic .active-path,
      .neural-graphic .wake {
        display: none;
      }
    }
  &lt;/style&gt;
  &lt;div class=&quot;neural-graphic&quot; role=&quot;img&quot; aria-label=&quot;Animated neural network showing nearly identical starting points producing different trajectories&quot;&gt;
    &lt;svg viewBox=&quot;0 0 960 420&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
      &lt;defs&gt;
        &lt;filter id=&quot;glow&quot; x=&quot;-50%&quot; y=&quot;-50%&quot; width=&quot;200%&quot; height=&quot;200%&quot;&gt;
          &lt;feGaussianBlur stdDeviation=&quot;4&quot; result=&quot;blur&quot; /&gt;
          &lt;feMerge&gt;
            &lt;feMergeNode in=&quot;blur&quot; /&gt;
            &lt;feMergeNode in=&quot;SourceGraphic&quot; /&gt;
          &lt;/feMerge&gt;
        &lt;/filter&gt;
&lt;filter id=&quot;blurGlow&quot; x=&quot;-50%&quot; y=&quot;-50%&quot; width=&quot;200%&quot; height=&quot;200%&quot;&gt;
          &lt;feGaussianBlur stdDeviation=&quot;8&quot; /&gt;
        &lt;/filter&gt;

        &lt;marker id=&quot;tinyArrow&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; refX=&quot;5.5&quot; refY=&quot;3.5&quot; orient=&quot;auto&quot;&gt;
          &lt;path d=&quot;M0,0 L7,3.5 L0,7 Z&quot; fill=&quot;rgba(185,197,210,0.22)&quot; /&gt;
        &lt;/marker&gt;
      &lt;/defs&gt;

      &lt;!-- faint neural scaffold --&gt;
      &lt;g id=&quot;network&quot;&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M70 206 C150 195, 205 170, 270 180 S400 245, 470 210 S600 145, 700 170 S820 238, 930 208&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M70 208 C160 210, 205 235, 280 230 S390 170, 470 188 S610 280, 700 258 S820 185, 930 192&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M70 210 C155 230, 220 270, 300 260 S410 205, 500 245 S635 330, 730 300 S835 245, 930 272&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M70 204 C150 176, 210 125, 290 140 S405 210, 485 155 S625 75, 710 108 S820 160, 930 128&quot; /&gt;

        &lt;path class=&quot;network-path&quot; d=&quot;M255 180 C305 155, 330 110, 365 82&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M285 230 C330 260, 365 310, 415 335&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M470 210 C500 165, 535 135, 570 110&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M480 190 C525 220, 560 255, 605 286&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M500 245 C550 210, 595 190, 650 198&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M700 170 C745 145, 770 110, 805 82&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M700 258 C750 280, 785 320, 830 342&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M710 108 C770 102, 815 118, 855 142&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M730 300 C775 280, 825 275, 875 292&quot; /&gt;

        &lt;path class=&quot;network-path&quot; d=&quot;M150 198 C180 158, 215 135, 250 120&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M165 220 C195 245, 225 285, 260 310&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M370 196 C405 150, 430 120, 455 90&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M390 225 C420 270, 450 295, 480 322&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M585 160 C620 130, 650 115, 680 92&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M600 270 C635 305, 660 328, 690 350&quot; /&gt;

        &lt;!-- connector mesh --&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M250 120 C320 135, 355 175, 390 225&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M365 82 C410 110, 450 140, 480 190&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M415 335 C455 310, 470 280, 500 245&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M570 110 C610 135, 625 170, 650 198&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M605 286 C650 275, 690 260, 700 258&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M805 82 C835 100, 855 120, 875 145&quot; /&gt;
        &lt;path class=&quot;network-path&quot; d=&quot;M830 342 C855 322, 875 305, 895 282&quot; /&gt;

        &lt;!-- subtle nodes --&gt;
        &lt;g&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;70&quot; cy=&quot;206&quot; r=&quot;3.5&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;70&quot; cy=&quot;208&quot; r=&quot;3.5&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;70&quot; cy=&quot;210&quot; r=&quot;3.5&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;70&quot; cy=&quot;204&quot; r=&quot;3.5&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;270&quot; cy=&quot;180&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;280&quot; cy=&quot;230&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;300&quot; cy=&quot;260&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;290&quot; cy=&quot;140&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;470&quot; cy=&quot;210&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;470&quot; cy=&quot;188&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;500&quot; cy=&quot;245&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;485&quot; cy=&quot;155&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;700&quot; cy=&quot;170&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;700&quot; cy=&quot;258&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;730&quot; cy=&quot;300&quot; r=&quot;3&quot; /&gt;
          &lt;circle class=&quot;node&quot; cx=&quot;710&quot; cy=&quot;108&quot; r=&quot;3&quot; /&gt;
        &lt;/g&gt;
      &lt;/g&gt;

      &lt;!-- active trajectories --&gt;
      &lt;g id=&quot;trajectories&quot;&gt;
        &lt;path id=&quot;route1&quot; class=&quot;active-path&quot; pathLength=&quot;1&quot; d=&quot;M70 206 C150 195, 205 170, 270 180 S400 245, 470 210 S600 145, 700 170 S820 238, 930 208&quot; /&gt;
        &lt;path id=&quot;route2&quot; class=&quot;active-path&quot; pathLength=&quot;1&quot; d=&quot;M70 208 C160 210, 205 235, 280 230 S390 170, 470 188 S610 280, 700 258 S820 185, 930 192&quot; /&gt;
        &lt;path id=&quot;route3&quot; class=&quot;active-path&quot; pathLength=&quot;1&quot; d=&quot;M70 210 C155 230, 220 270, 300 260 S410 205, 500 245 S635 330, 730 300 S835 245, 930 272&quot; /&gt;
        &lt;path id=&quot;route4&quot; class=&quot;active-path&quot; pathLength=&quot;1&quot; d=&quot;M70 204 C150 176, 210 125, 290 140 S405 210, 485 155 S625 75, 710 108 S820 160, 930 128&quot; /&gt;

        &lt;path id=&quot;wake1&quot; class=&quot;wake&quot; pathLength=&quot;1&quot; d=&quot;M70 206 C150 195, 205 170, 270 180 S400 245, 470 210 S600 145, 700 170 S820 238, 930 208&quot; /&gt;
        &lt;path id=&quot;wake2&quot; class=&quot;wake&quot; pathLength=&quot;1&quot; d=&quot;M70 208 C160 210, 205 235, 280 230 S390 170, 470 188 S610 280, 700 258 S820 185, 930 192&quot; /&gt;
        &lt;path id=&quot;wake3&quot; class=&quot;wake&quot; pathLength=&quot;1&quot; d=&quot;M70 210 C155 230, 220 270, 300 260 S410 205, 500 245 S635 330, 730 300 S835 245, 930 272&quot; /&gt;
        &lt;path id=&quot;wake4&quot; class=&quot;wake&quot; pathLength=&quot;1&quot; d=&quot;M70 204 C150 176, 210 125, 290 140 S405 210, 485 155 S625 75, 710 108 S820 160, 930 128&quot; /&gt;
&lt;/g&gt;
    &lt;/svg&gt;
  &lt;/div&gt;

  &lt;script&gt;
    (() =&gt; {
      const routeOrder = [1, 3, 2, 4];
      const routes = routeOrder.map(n =&gt; ({
        path: document.getElementById(`route${n}`),
        wake: document.getElementById(`wake${n}`)
      }));

      const settings = {
        travelDuration: 2200,
        revealFraction: 0.20,
        fadeFraction: 0.18,
        pauseBetween: 300
      };

      function preparePath(path, wake) {
        path.style.strokeDasharray = &apos;1&apos;;
        path.style.strokeDashoffset = &apos;1&apos;;
        path.style.opacity = &apos;0&apos;;

        wake.style.strokeDasharray = &apos;0.14 1&apos;;
        wake.style.strokeDashoffset = &apos;0.14&apos;;
        wake.style.opacity = &apos;0&apos;;
      }

      routes.forEach(({ path, wake }) =&gt; preparePath(path, wake));

      async function animateRoute({ path, wake }) {
        path.style.opacity = &apos;1&apos;;
        wake.style.opacity = &apos;1&apos;;

        const start = performance.now();

        await new Promise(resolve =&gt; {
          function frame(now) {
            const elapsed = now - start;
            const t = Math.min(1, elapsed / settings.travelDuration);

            const reveal = Math.min(1, t / settings.revealFraction);
            path.style.strokeDashoffset = String(1 - reveal);

            wake.style.strokeDashoffset = String(0.14 - t);

            const fadeStart = 1 - settings.fadeFraction;
            let opacity = 1;
            if (t &gt; fadeStart) {
              opacity = 1 - ((t - fadeStart) / settings.fadeFraction);
            }

            path.style.opacity = String(opacity);
            wake.style.opacity = String(opacity * 0.75);

            if (t &lt; 1) {
              requestAnimationFrame(frame);
            } else {
              resolve();
            }
          }

          requestAnimationFrame(frame);
        });

        path.style.opacity = &apos;0&apos;;
        wake.style.opacity = &apos;0&apos;;
        path.style.strokeDashoffset = &apos;1&apos;;
        wake.style.strokeDashoffset = &apos;0.14&apos;;
      }

      async function loop() {
        let index = 0;
        while (true) {
          await animateRoute(routes[index]);
          await new Promise(r =&gt; setTimeout(r, settings.pauseBetween));
          index = (index + 1) % routes.length;
        }
      }

      if (!window.matchMedia(&apos;(prefers-reduced-motion: reduce)&apos;).matches) {
        loop();
      }
    })();
  &lt;/script&gt;


  &lt;figcaption class=&quot;text-center&quot;&gt;Slightly different starting points lead to different outcomes&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;But LLMs are also really good at summarizing, organizing, and synthesizing vast
amounts of information.&lt;/p&gt;

&lt;p&gt;When we put those two abilities together, we get an amazing tool to help us
solve problems.&lt;/p&gt;

&lt;p&gt;Here are three examples where I use the diverge-then-converge pattern.&lt;/p&gt;

&lt;h3 id=&quot;code-review&quot;&gt;Code Review&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/images/diverge-converge/code-review-diverge-converge.svg&quot; alt=&quot;Request code review. Three revies: correctness, security, and simplicity. LLM converges analysis.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I have a code review skill that first diverges by having an orchestrator agent
spin up three sub-agents to review code. Each sub-agent has a specific focus:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Correctness&lt;/li&gt;
  &lt;li&gt;Security&lt;/li&gt;
  &lt;li&gt;Simplicity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there are UI changes, I add a fourth sub-agent that compares the resulting
design with what was requested.&lt;/p&gt;

&lt;p&gt;But rather than read those “raw” results immediately, I have the orchestrator
agent categorize the results into important, nice-to-haves, and nits in an HTML
file that I can review.&lt;/p&gt;

&lt;h3 id=&quot;bug-fixes&quot;&gt;Bug Fixes&lt;/h3&gt;

&lt;p&gt;I know some people have created loops for fixing bugs: issue comes into error
tracker -&amp;gt; robot picks it up -&amp;gt; fixes it -&amp;gt; opens PR -&amp;gt; merges PR.&lt;/p&gt;

&lt;p&gt;In my experience, that works for simple errors.&lt;/p&gt;

&lt;p&gt;But when I try that for more complex errors, that loop leads to shallow fixes:
the agent will solve the obvious, terminal problem (“let me add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;not
is_nil(arg)&lt;/code&gt; clause”) rather than fixing the root cause (“we shouldn’t allow
these values to be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt;”).&lt;/p&gt;

&lt;p&gt;And yet, the majority of the time, I want the root-cause fix. That’s why I like
to have a diverge-then-converge pattern with the agents.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/diverge-converge/bug-report-diverge-converge.svg&quot; alt=&quot;Bug report. Three analysis: simple, thorough, and creative. LLM converges analysis.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Just as with code review, I have an orchestrator agent spin up sub-agents to
look for a fix with different aims.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Simple fix (sometimes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;not is_nil(arg)&lt;/code&gt; is all that’s needed)&lt;/li&gt;
  &lt;li&gt;Thorough fix (often suggests multi-step changes, closer to root cause fix)&lt;/li&gt;
  &lt;li&gt;Creative fix (helpful to catch system design flaws. Sometimes this is the
root issue)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, I have the orchestrator agent summarize and synthesize the suggested
fixes, so I can decide which course of action we should take. Often, it ends up
being a combination of two of the three approaches.&lt;/p&gt;

&lt;h3 id=&quot;design-variants&quot;&gt;Design Variants&lt;/h3&gt;

&lt;p&gt;I stole this idea from Tomasz Tomczyk when we were &lt;a href=&quot;https://www.elixirstreams.com/ai_files/pairing-with-tomasz-on-ai-workflow&quot;&gt;pairing on his
workflow&lt;/a&gt;. He had a design skill that would create three
alternative designs for a given page based on three different companies’ styles
(e.g. GitHub, Stripe, Figma).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/diverge-converge/design-diverge-converge.svg&quot; alt=&quot;Design prompt. Three LLM designs: minimal, bold, unexpected. LLM convergent design.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In my case, I have the orchestrator agent spin up three sub-agents to create the
same design in three different ways:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Minimal (trying to capture only the essentials)&lt;/li&gt;
  &lt;li&gt;Bold (a more opinionated design)&lt;/li&gt;
  &lt;li&gt;Unexpected (rethinks the approach)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the agents are done, they output the designs in HTML so I can review them.
Sometimes, there’s a clear winner, and we move on. But the benefit of having
three different approaches is that I can choose parts of each that might work
together.&lt;/p&gt;

&lt;p&gt;And that’s when I tell the agent to combine the pieces that I like into a final
design. The orchestrator agent then goes and creates a new design with all of my
preferences.&lt;/p&gt;</content><author><name>German Velasco</name></author><category term="blog" /><category term="ai" /><category term="llm" /><category term="software-development" /><summary type="html">One of the patterns I&apos;ve found particularly helpful with LLMs is the diverge-then-converge pattern. LLMs excel at both of those things separately, and when we put them together, they are an excellent tool to help us solve problems.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.germanvelasco.com/images/diverge-converge/converge-path.png" /><media:content medium="image" url="https://www.germanvelasco.com/images/diverge-converge/converge-path.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Are plan files the new 10k-line PRs?</title><link href="https://www.germanvelasco.com/blog/are-plan-files-the-new-10k-line-prs" rel="alternate" type="text/html" title="Are plan files the new 10k-line PRs?" /><published>2026-07-07T00:00:00+00:00</published><updated>2026-07-07T00:00:00+00:00</updated><id>https://www.germanvelasco.com/blog/are-plan-files-the-new-10k-line-prs</id><content type="html" xml:base="https://www.germanvelasco.com/blog/are-plan-files-the-new-10k-line-prs">&lt;p&gt;&lt;img src=&quot;https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHI2MzJubXRham9hdzF3dXM5eHNsYmRoaWlhaDJ0b3BkOXYzbGx3eSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/bEs40jYsdQjmM/giphy.gif&quot; alt=&quot;Tom (from Tom and Jerry) taping his eyes open&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Many people’s new way of working with LLMs is to start with an idea, have the
LLM create a spec (review and approve that), then have the LLM generate a &lt;em&gt;very&lt;/em&gt;
detailed plan.&lt;/p&gt;

&lt;p&gt;At that point, people have two options:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;They either spend a &lt;em&gt;lot&lt;/em&gt; of time reviewing the plan, ask for changes, and
finally approve it. Warning: this may take hours.&lt;/li&gt;
  &lt;li&gt;Or they skim the plan, but it’s too long, so they YOLO it and say “LGTM 👍”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that made me realize that plans are the new 10k-line PRs.&lt;/p&gt;

&lt;p&gt;Back before the days of LLMs, we used to say that the number of comments a PR
received was inversely related to the number of lines it had.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If the PR had 10 lines, it’d get 11 comments.&lt;/li&gt;
  &lt;li&gt;If the PR had 10k lines, it’d get one comment: “LGTM 👍”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/plans-the-new-10k-line-prs/tired-dev.png&quot; alt=&quot;A very tired person sitting in front of a computer in the dark&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The problem wasn’t that we were lazy.&lt;/p&gt;

&lt;p&gt;The problem was that to understand 10k lines of code requires many, many hours
of review. We’d have to transform that 2D text into a 3D mental model in our
heads. We’d have to carefully consider how the new vast amounts of code fit into
the existing system. We’d have to consider high-level architecture and low-level
details.&lt;/p&gt;

&lt;p&gt;Because of that, even if we spent 3 hours reviewing the PR, submitting comments,
reviewing changes, and doing that 2 to 3 times until everything looked good (a
process that I always found demoralizing for the reviewer and author alike), the
results were still worse than if the author had split the work at the beginning
into smaller slices.&lt;/p&gt;

&lt;p&gt;So, the joke went that we would just give the 10k-line PR a thumbs up. 👍&lt;/p&gt;

&lt;p&gt;Now, as an industry, we’re trying to figure out if we can stop reviewing code.
To do that, I see some people turning to the big-plan-upfront strategy to try
and specify the desired behavior ahead of time. After all, if we can do that, we
can let the latest models run independently for hours!&lt;/p&gt;

&lt;p&gt;But that suffers from the exact same problems we had when we tried to review
10k-line PRs: we have to transform the plan’s text into a 3D mental model in our
heads. We have to carefully consider how the new vast amounts of code the model
might generate will fit into the existing system. We have to consider high-level
architecture and low-level details. And because of that, the implementation will
be worse than if we split the work into smaller slices and work iteratively.&lt;/p&gt;

&lt;h2 id=&quot;thinner-slices-more-iteration&quot;&gt;Thinner slices. More iteration.&lt;/h2&gt;

&lt;p&gt;For years, software development best practices encouraged a more iterative
approach.&lt;/p&gt;

&lt;p&gt;We were always trying to capture a small slice – a user story or maybe even
just a use case – not a full-fledged specification. We would then build that
and reevaluate. In the process, we’d discover some pitfalls or improvements we
hadn’t foreseen at first.&lt;/p&gt;

&lt;p&gt;Then we’d iterate.&lt;/p&gt;

&lt;p&gt;And so, solutions would, to some degree, evolve from the kernel of the initial
idea, the realities of what was already present in the codebase, and the new
insights we gained with each iteration.&lt;/p&gt;

&lt;p&gt;And I think that’s still how we can work.&lt;/p&gt;

&lt;p&gt;My preferred flow (at least currently) is to plan the work in &lt;a href=&quot;https://www.germanvelasco.com/blog/break-apart-your-features-into-full-stack-slices&quot;&gt;full-stack
slices&lt;/a&gt;. Once I’ve split up my work into a slice, I then work on one or two use
cases (sometimes in Gherkin) with the LLM. We then talk about architecture and
agree on what the call stack should look like. Perhaps we do some ideation on UI
or UX as well.&lt;/p&gt;

&lt;p&gt;It’s only once we’ve agreed on all of that, that I let the LLM go. We have a
plan of action, but it’s not &lt;em&gt;the&lt;/em&gt; plan. It’s not a huge document sitting in my
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/plans&lt;/code&gt; directory. It’s encompassed by the use cases we agreed upon.&lt;/p&gt;

&lt;p&gt;And once that’s done, we have a shippable unit of work. We can stop and evaluate
what has been done and whether anything needs correction. That helps evolve the
design. Then, I repeat the same cycle with the next chunk of work.&lt;/p&gt;

&lt;h3 id=&quot;but-isnt-that-slow&quot;&gt;But isn’t that slow?&lt;/h3&gt;

&lt;p&gt;Not any slower than the big-plan-upfront approach. Instead of spending five hours
with my eyes bleeding while I read and change the 10k-line plan, I spend that
time iterating and evolving the design with the LLM.&lt;/p&gt;

&lt;p&gt;It’s a far more engaging and enjoyable experience, and I think it creates
better, more maintainable software.&lt;/p&gt;</content><author><name>German Velasco</name></author><category term="blog" /><category term="software-development" /><category term="ai" /><category term="llms" /><summary type="html">Many people&apos;s new way of working with LLMs is to start with an idea, have the LLM create a spec (review and approve that), then have the LLM generate a very detailed plan. At that point, people have two options: a) They either spend a lot of time reviewing the plan, ask for changes, and finally approve it. Warning: this may take hours. Or b) They skim the plan, but it&apos;s too long, so they YOLO it and say &quot;LGTM 👍&quot;.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.germanvelasco.com/images/plans-the-new-10k-line-prs/tired-dev.png" /><media:content medium="image" url="https://www.germanvelasco.com/images/plans-the-new-10k-line-prs/tired-dev.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Reflections on creating ExCrap without looking at the code</title><link href="https://www.germanvelasco.com/blog/i-wrote-excrap-without-looking-at-the-code" rel="alternate" type="text/html" title="Reflections on creating ExCrap without looking at the code" /><published>2026-06-25T00:00:00+00:00</published><updated>2026-06-25T00:00:00+00:00</updated><id>https://www.germanvelasco.com/blog/i-wrote-excrap-without-looking-at-the-code</id><content type="html" xml:base="https://www.germanvelasco.com/blog/i-wrote-excrap-without-looking-at-the-code">&lt;p&gt;I recently published &lt;a href=&quot;https://ex-crap.hexdocs.pm/readme.html&quot;&gt;ExCrap&lt;/a&gt;, an Elixir library for calculating Change Risk
Anti-Patterns scores – or as they’re commonly called CRAP scores.&lt;/p&gt;

&lt;p&gt;From the README:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;CRAP combines function-level cyclomatic complexity with function-level
coverage to highlight code that is risky to change because it is both complex
and under-tested.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because the library is small (it basically exposes a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt; task),
and the idea concrete enough, it made for a perfect experiment: creating the
library &lt;em&gt;without&lt;/em&gt; looking at the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the experiment?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There’s so much talk about &lt;a href=&quot;https://www.germanvelasco.com/blog/draw-the-rest-of-the-owl-software-factory-edition&quot;&gt;software factories&lt;/a&gt;, but I keep
asking myself: can we really verify the code the agents write without looking at
it? So, I wanted to treat the library’s code as a black box to force my brain to
think of new verification strategies.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/reflections-on-ex-crap/code-as-black-box.png&quot; alt=&quot;inputs -&amp;gt; black box -&amp;gt; outputs&quot; /&gt;&lt;/p&gt;

&lt;p&gt;That’s what this post is about.&lt;/p&gt;

&lt;p&gt;In it, I share a few things I tried while creating the library (some good, some
bad), and I end with some reflections.&lt;/p&gt;

&lt;p&gt;Here’s an outline if you want to skip around:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#initial-research-with-the-three-big-brains&quot;&gt;Initial research with the three big brains&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#the-first-few-phases-with-superpowers&quot;&gt;The first few phases with superpowers&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#how-do-i-verify-this-is-working&quot;&gt;How do I verify this is working?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#-external-verification&quot;&gt;External verification&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#-internal-code-health&quot;&gt;Internal code health&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#final-reflections&quot;&gt;Final reflections&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;initial-research-with-the-three-big-brains&quot;&gt;Initial research with the three big brains&lt;/h2&gt;

&lt;p&gt;The initial research itself was interesting. I could have done it myself and
given the agents a good prompt or context. But that’s not what I did.&lt;/p&gt;

&lt;p&gt;A little while ago, I attended a &lt;a href=&quot;https://maven.com/lean-software-production/hands-off-agentic-coding&quot;&gt;workshop&lt;/a&gt; on building software factories that
two old colleagues of mine were starting. In the first session, we talked about
bringing an idea and using the “three big brains” for research: GPT, Gemini, and
Claude.&lt;/p&gt;

&lt;p&gt;So, I used all three models to do deep research on a &lt;a href=&quot;https://github.com/germsvel/ex_crap/blob/main/docs/IDEA.md&quot;&gt;core idea&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I want to create an Elixir library that calculates C.R.A.P. scores (combining
test coverage and cyclomatic complexity). The library should be able to be
executed with mix to be run locally and in CI environments. And it should have
the ability to give me a score. If the score is beyond a certain level, we
should be able to fail CI (same as is possible with tools like Credo).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That resulted in &lt;a href=&quot;https://github.com/germsvel/ex_crap/tree/main/docs/research&quot;&gt;three research files&lt;/a&gt; that I committed to the repo, which
other agents then used as an index of information.&lt;/p&gt;

&lt;p&gt;Those files served as the foundation of the library. For example, that’s where
the agents who did the initial implementation got the knowledge for what CRAP
metrics were and how to calculate them.&lt;/p&gt;

&lt;p&gt;After that, the rest of the application was built using GPT 5.5 and OpenCode (in
case that matters to you).&lt;/p&gt;

&lt;h2 id=&quot;the-first-few-phases-with-superpowers&quot;&gt;The first few phases with superpowers&lt;/h2&gt;

&lt;p&gt;Once I had the research in place, I started implementing the application in
&lt;a href=&quot;https://www.germanvelasco.com/blog/break-apart-your-features-into-full-stack-slices&quot;&gt;full-stack slices&lt;/a&gt;. At the time, I was using &lt;a href=&quot;https://github.com/obra/superpowers&quot;&gt;superpowers&lt;/a&gt;, so some of my
prompts automatically triggered its brainstorming, writing-plans, and other
skills.&lt;/p&gt;

&lt;p&gt;I originally committed some of the artifacts, though I have since removed them
from the repo. But if you’d like a sample, you can still find a few of them in
the commit history like &lt;a href=&quot;https://github.com/germsvel/ex_crap/commit/7443ef2141a559cd1df1ec60716cc08a2af2b2b2&quot;&gt;this plan&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To be honest, planning is the part I liked the least.&lt;/p&gt;

&lt;p&gt;LLMs have made me hate markdown. I still &lt;em&gt;love&lt;/em&gt; markdown for writing, but
reading markdown created by an LLM just makes my eyes bleed. (I now prefer HTML
files as output.)&lt;/p&gt;

&lt;p&gt;I also prefer a more interactive flow than a giant upfront spec/plan. So, my
feature work today is more interactive, where the agent and I define some
Gherkin, talk about module/function architecture, and then I let the agent do a
slice of work.&lt;/p&gt;

&lt;p&gt;But I digress. For the initial implementation, I created plans, and if I’m
honest, I probably only skimmed them. So, the agents were guiding the roadmap, and
I was mostly flying blind.&lt;/p&gt;

&lt;h2 id=&quot;how-do-i-verify-this-is-working&quot;&gt;How do I verify this is working?&lt;/h2&gt;

&lt;p&gt;With all software, I think we need two types of verification:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The first is about whether or not the product we’re creating is what we
desired. This one is external to the black box, and therefore, visible to us. In
web apps, you could think of this as the UI you can click around. With ExCrap,
it’s mostly about running the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt; task.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The second is whether or not the codebase is healthy and evolving well. That
one is internal to the black box, and therefore, invisible to us.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, for us to fully verify our software product, we need to inspect the output
(visible) &lt;em&gt;and&lt;/em&gt; we need &lt;a href=&quot;https://www.germanvelasco.com/blog/draw-the-rest-of-the-owl-software-factory-edition#how-do-we-draw-the-rest-of-the-owl&quot;&gt;sensors and signals&lt;/a&gt; to give us information about
what’s happening inside the black box.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/reflections-on-ex-crap/black-box-sensors-signals.svg&quot; alt=&quot;inputs -&amp;gt; gray box -&amp;gt; outputs. The gray box has sensors that shoot out signals.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;-external-verification&quot;&gt;🧭 External verification&lt;/h2&gt;

&lt;p&gt;Since the library boils down to that single command, I could easily verify its
output manually. I ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt; over and over to make sure the library could
calculate CRAP metrics for itself.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/reflections-on-ex-crap/mix-crap-output.png&quot; alt=&quot;Output of `mix crap`. A series of green check marks.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;That also allowed me to improve the format of the output.&lt;/p&gt;

&lt;p&gt;For example, I wanted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt;’s output to be terse by default and verbose
only if we passed a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--verbose&lt;/code&gt; flag (the LLM’s original output was really
verbose). I wanted it to have colors: red on failure, green on success (the
LLM’s original output had no colors).&lt;/p&gt;

&lt;p&gt;But dog-fooding only gave me one data point. The “users” of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt; would be
other Elixir projects. Would it work in those?&lt;/p&gt;

&lt;h3 id=&quot;manually-testing-external-libraries&quot;&gt;Manually testing external libraries&lt;/h3&gt;

&lt;p&gt;As a first pass, I cloned some open source repos (like Phoenix and Ecto), added
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ex_crap&lt;/code&gt; as a dependency, and ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt; on those libraries.&lt;/p&gt;

&lt;p&gt;That immediately flagged issues with ExCrap – it couldn’t even read the Elixir
files in those projects correctly. All I got was an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:error, :invalid_source}&lt;/code&gt;
error.&lt;/p&gt;

&lt;p&gt;Unfortunately, the error didn’t even have a stacktrace (it just failed to read
the file), so there was no way of knowing what construct in those codebases
ExCrap was treating as invalid.&lt;/p&gt;

&lt;p&gt;So, I did what anyone leaning on agents will do: I grabbed the error and fed it
back to the agent.&lt;/p&gt;

&lt;p&gt;The agent had guesses at what might be causing the issue. But after a couple of
rounds of me running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt; on libraries and feeding the errors back to the
agent, it was clear we wouldn’t really solve the problem that way.&lt;/p&gt;

&lt;p&gt;But I had learned one thing: the errors were coming from ExCrap’s cyclomatic
complexity calculation. It seems that ExCrap only accounted for some Elixir
constructs, and when it found a file that had constructs it hadn’t accounted
for, it just returned &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:error, :invalid_source}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, I needed a way to test more combinations of Elixir constructs to make sure
the library could parse them. That’s when I reached for property-based tests.&lt;/p&gt;

&lt;h3 id=&quot;property-based-testing&quot;&gt;Property-based testing&lt;/h3&gt;

&lt;p&gt;I thought this would be a great opportunity to use property-based tests. I
needed many, varied combinations of inputs that should always satisfy a simple
property: the files should parse without error.&lt;/p&gt;

&lt;p&gt;But once again, not being able to look at the code meant it was hard to know how
to best write those properties. The best I could do was to have the agent
generate the tests.&lt;/p&gt;

&lt;p&gt;That helped, but there were two problems:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;I didn’t really know what we were testing with those property-based tests. I
could read the test descriptions with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--trace&lt;/code&gt;, but even then, I wasn’t really
sure what those tests were &lt;em&gt;actually&lt;/em&gt; testing. I only knew what the test
description claimed to be testing.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The second, bigger problem, was that even after creating the property-based
tests, and fixing the issues those caught, running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt; on many
open-source libraries would still fail.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s when I finally got the idea on how to close the loop.&lt;/p&gt;

&lt;h3 id=&quot;leaning-even-more-into-the-search--gather-methodology&quot;&gt;Leaning even more into the search &amp;amp; gather methodology&lt;/h3&gt;

&lt;p&gt;What I really wanted to do was to somehow automate what I was already doing
manually (running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt; on a bunch of open source libraries), and to do it
directly in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ex_crap&lt;/code&gt; library.&lt;/p&gt;

&lt;p&gt;Bringing all the libraries into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ex_crap&lt;/code&gt; project seemed like a bad idea,
but perhaps I could do something simpler and achieve the same result. Maybe I
could grab pieces of open source libraries and create sample modules that could
serve as fixtures for tests.&lt;/p&gt;

&lt;p&gt;So, just as with the research at the start of the project, I sent some agents to
search and gather.&lt;/p&gt;

&lt;p&gt;In this case, I tasked an agent to send subagents to find sample Elixir open
source repositories (I didn’t specify which). I told it to get a broad set of
concepts to cover all Elixir constructs. I then asked it to consolidate those
findings and generate sample Elixir modules that would compile and stand alone
(i.e. have no dependencies). Those would be the sample modules that I could use
as fixtures.&lt;/p&gt;

&lt;p&gt;The agent did a pretty good job of that.&lt;/p&gt;

&lt;p&gt;It created a series of sample modules, organized them into separate directories
based on what they’re meant to capture, and created tests that run those
fixtures.&lt;/p&gt;

&lt;p&gt;Here’s the directory structure from the &lt;a href=&quot;https://github.com/germsvel/ex_crap/blob/main/test/fixtures/elixir_samples/README.md&quot;&gt;samples README&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;elixir_samples/
├── 01_basic_modules/
├── 02_typespecs/
├── 03_structs_protocols/
├── 04_behaviours/
├── 05_macros/
├── 06_pattern_matching/
├── 07_genserver/
├── 08_supervisors_otp/
├── 09_protocols/
├── 10_advanced/
└── _raw_originals/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(A huge thank you to the &lt;a href=&quot;https://github.com/germsvel/ex_crap/blob/main/test/fixtures/elixir_samples/README.md#raw-originals-_raw_originals&quot;&gt;original libraries&lt;/a&gt; the agent used as good examples.)&lt;/p&gt;

&lt;p&gt;With those files as our fixtures, the agent wrote some tests that surfaced the
same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:error, :invalid_source}&lt;/code&gt; error, and it fixed the issues. After that, I
could get CRAP scores for all the libraries I was testing!&lt;/p&gt;

&lt;h2 id=&quot;-internal-code-health&quot;&gt;🏥 Internal code health&lt;/h2&gt;

&lt;p&gt;Throughout the process, I also kept trying to improve the internal verification
mechanisms of the library.&lt;/p&gt;

&lt;p&gt;First, with all the code I created, I used the &lt;a href=&quot;https://github.com/obra/superpowers/blob/main/skills/test-driven-development/SKILL.md&quot;&gt;superpowers TDD skill&lt;/a&gt;. That
meant that most of the code that was created had tests. But &lt;em&gt;I&lt;/em&gt; couldn’t read or
write those tests myself.&lt;/p&gt;

&lt;p&gt;So, I needed to figure out what would make for good sensors that would tell me
about the code, including the tests themselves!&lt;/p&gt;

&lt;p&gt;The first thing I did was to take a quick glance at what the tests &lt;em&gt;claim&lt;/em&gt; to be
testing by running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix test --trace&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a small sample:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ExCrap.ScoreTest &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;/ex_crap/score_test.exs]
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;score/2 returns complexity squared plus complexity &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;0 percent coverage &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1.1ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;L#9]
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;score/2 returns complexity unchanged &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;100 percent coverage &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.00ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;L#5]
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;score/2 preserves fractional scores &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;intermediate coverage &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.00ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;L#13]
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;score/2 rejects invalid complexity &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.00ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;L#17]
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;score/2 rejects invalid coverage &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.00ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;L#22]
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;score/2 rejects fractional coverage outside the valid range &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.00ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;L#28]

ExCrap.ElixirSamplesTest &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;/ex_crap/elixir_samples_test.exs]
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;canonical sample fixtures discovers only canonical samples &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;2.4ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;L#39]
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;canonical sample fixtures canonical samples live with &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;fixtures &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.01ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;L#35]
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;canonical sample fixtures all canonical samples analyze successfully &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;14.7ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;L#45]
  &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;canonical sample fixtures canonical sample aggregate complexity summaries remain stable &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;7.8ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;L#57]

&lt;span class=&quot;c&quot;&gt;# ... more tests ...&lt;/span&gt;

Finished &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;1.1 seconds &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.4s async, 0.6s &lt;span class=&quot;nb&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
22 properties, 197 tests, 0 failures
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That was okay, but it immediately raised the question: are those tests actually
testing anything?&lt;/p&gt;

&lt;p&gt;I remember a while ago, I had an agent write a test like this (not in this library,
but in a different project):&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;pretend to test something&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# in the config&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;bar:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In other words, the test would always run, do nothing, but show as passing in
the output. Just by reading the test description, it &lt;em&gt;seemed&lt;/em&gt; like the test was
testing something useful. But the code in question was never exercised.&lt;/p&gt;

&lt;p&gt;To try to counter that type of scenario, I used two other tools: code coverage
and mutation testing.&lt;/p&gt;

&lt;h3 id=&quot;code-coverage&quot;&gt;Code coverage&lt;/h3&gt;

&lt;p&gt;In Elixir, we get test coverage out of the box with ExUnit with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix test
--cover&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here are the current results for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ex_crap&lt;/code&gt; library:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Generating&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cover&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;results&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Percentage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Module&lt;/span&gt;                         &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|------------|--------------------------------|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;     &lt;span class=&quot;mf&quot;&gt;83.78&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ExCrap&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Coverage&lt;/span&gt;                &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;     &lt;span class=&quot;mf&quot;&gt;88.46&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Mix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Tasks&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Crap&lt;/span&gt;                 &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;     &lt;span class=&quot;mf&quot;&gt;90.00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ExCrap&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Mix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;BoundarySpec&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;     &lt;span class=&quot;mf&quot;&gt;90.74&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ExCrap&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Complexity&lt;/span&gt;              &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;     &lt;span class=&quot;mf&quot;&gt;96.30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ExCrap&lt;/span&gt;                         &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;     &lt;span class=&quot;mf&quot;&gt;97.18&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ExCrap&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Report&lt;/span&gt;                  &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;mf&quot;&gt;100.00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ExCrap&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Mix&lt;/span&gt;                     &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;mf&quot;&gt;100.00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ExCrap&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Scanner&lt;/span&gt;                 &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;mf&quot;&gt;100.00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ExCrap&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Score&lt;/span&gt;                   &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;mf&quot;&gt;100.00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Mix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Tasks&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Boundary&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Spec&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Accept&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;    &lt;span class=&quot;mf&quot;&gt;100.00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Mix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Tasks&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Boundary&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Spec&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Check&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|------------|--------------------------------|&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;     &lt;span class=&quot;mf&quot;&gt;92.09&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Total&lt;/span&gt;                          &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With that, I can at least see that the 197 tests and 22 property-based tests are
covering ~92% of the code. So at least as a whole, the tests are exercising a
large part of the codebase.&lt;/p&gt;

&lt;p&gt;Are they good tests? Do they only test behavior and not implementation? That, I
don’t know. For now, it’s the black box’s domain.&lt;/p&gt;

&lt;h3 id=&quot;mutation-testing&quot;&gt;Mutation testing&lt;/h3&gt;

&lt;p&gt;The next, more exciting tool I wanted to try in this project was mutation
testing.&lt;/p&gt;

&lt;p&gt;The idea behind mutation testing is that we change some of the code (e.g. swap
an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;and&lt;/code&gt; for an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;or&lt;/code&gt;), and run the tests. If they still pass, it means our tests
are not actually testing that code path.&lt;/p&gt;

&lt;p&gt;After some brief research, I decided to try &lt;a href=&quot;https://muex.hexdocs.pm/readme.html&quot;&gt;muex&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Muex gave me what I was looking for. Over the course of building the library, I
ran my trusty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix mutate&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix mutate.fast&lt;/code&gt; aliases frequently and found
mutants that survived. So, I fed those back into the agent to improve the tests.&lt;/p&gt;

&lt;p&gt;I did run into a lot of false positives, but that might have been a somewhat
unique scenario related to &lt;a href=&quot;https://boundary.hexdocs.pm/Boundary.html&quot;&gt;Boundary&lt;/a&gt;. Muex would remove a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use Boundary&lt;/code&gt;
declaration and say the mutation had survived. But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use Boundary&lt;/code&gt; is probably
one of a few &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use&lt;/code&gt; macros that don’t break your code if you remove them.
Nevertheless, it created a lot of noise. So, in the end, I had to trim what
types of mutators the library was looking for.&lt;/p&gt;

&lt;p&gt;If you’re curious, these are the two mutation aliases I currently have:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;ss&quot;&gt;mutate:&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;muex --mutators arithmetic,boolean,comparison,conditional,function_call,literal,return_value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s2&quot;&gt;&quot;mutate.fast&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;muex --mutators arithmetic,boolean,comparison,conditional,function_call,literal,return_value --optimize-level aggressive --max-per-function 5&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;using-boundary-for-module-architecture&quot;&gt;Using Boundary for module architecture&lt;/h3&gt;

&lt;p&gt;It’s still unclear if, in a future where the code is a black box, we’ll care
about the architecture of the code. Maybe we’ll just let the agents design it as
they see fit. But for now, I wanted to have some control over how modules depend
on one another.&lt;/p&gt;

&lt;p&gt;And for that, I used &lt;a href=&quot;https://boundary.hexdocs.pm/Boundary.html&quot;&gt;Boundary&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Boundary lets us specify some modules as boundaries and declare their
dependencies and exports. When those boundaries are violated, it throws a
warning. It’s the perfect sensor for module interdependencies.&lt;/p&gt;

&lt;p&gt;And it comes with a great &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix boundary.spec&lt;/code&gt; task. This is the current output
for ExCrap:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ExCrap
  exports:
  deps:

ExCrap.Complexity
  exports:
  deps:

ExCrap.Coverage
  exports:
  deps:

ExCrap.Mix
  exports:
  deps: ExCrap

ExCrap.Report
  exports:
  deps: ExCrap.Score

ExCrap.Scanner
  exports:
  deps: ExCrap.Complexity

ExCrap.Score
  exports:
  deps:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When I first introduced Boundary into the project, I had already built most of
the functionality. The spec it produced showed that we had some undesired
coupling. So, I had the agents modify the code to reduce the number of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exports&lt;/code&gt;
and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deps&lt;/code&gt; to what you see above.&lt;/p&gt;

&lt;h4 id=&quot;enforcing-boundaries&quot;&gt;Enforcing boundaries&lt;/h4&gt;

&lt;p&gt;But here was another problem. In the course of adding a new feature, an agent
could easily violate a boundary, get a warning, and then “fix” it by changing
the boundary definition — warning gone! But that obviously removes the
usefulness of the signal.&lt;/p&gt;

&lt;p&gt;To prevent that, I created two additional mix tasks to enforce that the boundary
definitions aren’t changed without human approval:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;boundary.spec.check&lt;/code&gt; task compares the output of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix boundary.spec&lt;/code&gt;
against output that was previously approved and saved. If the spec differs, then
it means the agent has changed the boundaries without approval.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;To approve a new boundary spec, a human needs to run an interactive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix
boundary.spec.accept&lt;/code&gt; task.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, an agent can just go ahead and edit the text file that contains the
spec’s source of truth. And no amount of &lt;del&gt;COMMANDS&lt;/del&gt; suggestions in the
AGENTS.md file will stop a determined agent. So, the workflow is far from
perfect, but for now, I’m comfortable with it since module architecture is not
mission-critical.&lt;/p&gt;

&lt;h3 id=&quot;crap-scores&quot;&gt;CRAP scores&lt;/h3&gt;

&lt;p&gt;Another signal I used to try to keep code healthy was CRAP scores (that’s why I
created the library).&lt;/p&gt;

&lt;p&gt;I’ve always found &lt;a href=&quot;https://credo.hexdocs.pm/Credo.Check.Refactor.CyclomaticComplexity.html&quot;&gt;Credo’s cyclomatic complexity&lt;/a&gt; check a little annoying
because it’ll flag any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;case&lt;/code&gt; statement that has many branches, even if each
branch is simple. And &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;case&lt;/code&gt; statements are very common in Elixir.&lt;/p&gt;

&lt;p&gt;So, before learning about CRAP scores, I was reluctant to treat cyclomatic
complexity as a signal that the agent could use to correct itself.&lt;/p&gt;

&lt;p&gt;But since CRAP scores combine cyclomatic complexity with test coverage, I
actually like the metric a lot more. If a function has good test coverage for
all the potential branches in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;case&lt;/code&gt; statement, the score goes down a lot
(technically, a fully covered function’s score is equal to the original
cyclomatic complexity score, but CRAP’s default threshold score of 30 is a lot
more forgiving than Credo’s 9).&lt;/p&gt;

&lt;p&gt;So throughout the creation of the library, I ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt; (or my other trusty
alias &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix test.crap&lt;/code&gt;) to see that everything was green.&lt;/p&gt;

&lt;p&gt;When I wanted more details, I ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap --verbose&lt;/code&gt;. And if I wanted to see
what modules were getting too close to the threshold, I ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap&lt;/code&gt; with a
lower &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--max-score&lt;/code&gt; flag (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix crap --max-score 20&lt;/code&gt;).&lt;/p&gt;

&lt;h3 id=&quot;adding-more-static-checks&quot;&gt;Adding more static checks&lt;/h3&gt;

&lt;p&gt;Last but not least, I also reached for static analysis tools in my quest to keep
the black box healthy. For ExCrap, I added &lt;a href=&quot;https://credo.hexdocs.pm/overview.html&quot;&gt;Credo&lt;/a&gt;, &lt;a href=&quot;https://quokka.hexdocs.pm/readme.html&quot;&gt;Quokka&lt;/a&gt;, and &lt;a href=&quot;https://sobelow.hexdocs.pm/readme.html&quot;&gt;Sobelow&lt;/a&gt;,
though I added them pretty late in the process.&lt;/p&gt;

&lt;p&gt;Since it was late in the game, Credo and Quokka (which automatically runs on
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix format&lt;/code&gt;) didn’t find any glaring issues (that I could tell). Sobelow, on
the other hand, caught a few things. The overall majority were minor (which I
ignored), but it caught a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;String.to_atom&lt;/code&gt; call that I was very glad to fix.&lt;/p&gt;

&lt;p&gt;After all was said and done, I added all three to my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;precommit&lt;/code&gt; alias that is
the core of my fast checks:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;s2&quot;&gt;&quot;test.crap&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;test --cover --export-coverage default&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;test.coverage&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;crap&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;precommit:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;format&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;test.crap&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;boundary.spec.check&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;credo --strict&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;sobelow --skip&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;final-reflections&quot;&gt;Final reflections&lt;/h2&gt;

&lt;p&gt;So, having now published the library, how do I &lt;em&gt;feel&lt;/em&gt; about the project?&lt;/p&gt;

&lt;p&gt;I have to say, I still feel &lt;em&gt;unsettled&lt;/em&gt;. It is very strange to have a project
under my name that I’m not entirely sure I understand.&lt;/p&gt;

&lt;p&gt;Before I published the library, I opened the black box and looked inside. I
wanted to make sure I wasn’t shipping malware to people in the Elixir community.
I didn’t change anything because nothing stood out as malicious or bad, and I
wanted to keep the experiment somewhat “pure” until I wrote about it.&lt;/p&gt;

&lt;p&gt;And that’s one part of the experiment I’m taking with me: I just couldn’t verify
(or shake the feeling) that I wasn’t shipping something bad to other Elixir
folks without peeking inside the box. So, for me, at least for now, I want to be
able to see inside the box.&lt;/p&gt;

&lt;p&gt;So, what was &lt;a href=&quot;https://github.com/germsvel/ex_crap/tree/main/lib&quot;&gt;inside the box&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;The code isn’t at all how I would’ve written it. That’s not to say it’s
necessarily bad. I actually think a lot of it looks interesting. But I didn’t
spend hours and hours understanding it and internalizing it, so it looks foreign
to me.&lt;/p&gt;

&lt;p&gt;Aside from that, the library is small and simple enough that I’m happy to
publish it, not in the least because I want to use it in my own projects.&lt;/p&gt;

&lt;h3 id=&quot;meta-lessons-about-working-with-llms&quot;&gt;Meta lessons about working with LLMs&lt;/h3&gt;

&lt;p&gt;I want to leave you with two (meta) lessons learned.&lt;/p&gt;

&lt;p&gt;The first is something I’ve come to realize about working with LLMs (which,
honestly, should’ve already been apparent as a software developer). It is best
to do things manually first. Discover the rough edges. Fine-tune the flow. Once
you’ve done that, and the action starts feeling repetitive, then automate it (or
hand it to an agent).&lt;/p&gt;

&lt;p&gt;The second lesson is that you sometimes have to lean &lt;em&gt;more&lt;/em&gt; into how LLMs work.
It was surprising to me at first (though in hindsight it shouldn’t have been)
that the best solution to my failing-to-parse-files issue was to go get more
data, refine it, and use it for my tests. That’s something that might’ve been
too expensive (or annoying) to do without agents. But with agents, it was a nice
way to provide the data we needed.&lt;/p&gt;

&lt;p&gt;Thank you for reading and going on this journey with me. In this wild west of
AI, I encourage you to experiment more, and then share what you learn!&lt;/p&gt;</content><author><name>German Velasco</name></author><category term="blog" /><category term="elixir" /><category term="software-development" /><category term="ai" /><category term="llms" /><summary type="html">I recently published ExCrap, an Elixir library for calculating Change Risk Anti-Patterns scores -- or as they&apos;re commonly called CRAP scores. It made for a perfect experiment: creating the library without looking at the code.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.germanvelasco.com/images/reflections-on-ex-crap/code-as-black-box.png" /><media:content medium="image" url="https://www.germanvelasco.com/images/reflections-on-ex-crap/code-as-black-box.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Why dead code feels hard to delete</title><link href="https://www.germanvelasco.com/blog/why-dead-code-feels-hard-to-delete" rel="alternate" type="text/html" title="Why dead code feels hard to delete" /><published>2026-06-19T00:00:00+00:00</published><updated>2026-06-19T00:00:00+00:00</updated><id>https://www.germanvelasco.com/blog/why-dead-code-feels-hard-to-delete</id><content type="html" xml:base="https://www.germanvelasco.com/blog/why-dead-code-feels-hard-to-delete">&lt;p&gt;&lt;img src=&quot;/images/delete-dead-code/delete-dead-code-comic-strip.png&quot; alt=&quot;Two scenes. Scene 1: dev 1 says, should we delete this unused code? Scene 2: dev 2 says, but I spent so much time writing this!&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It’s always surprising when people don’t want to delete dead code because they
think they &lt;em&gt;might&lt;/em&gt; need it in the future or because they worked so hard on it.&lt;/p&gt;

&lt;p&gt;Recently, I had one of those conversations with a colleague. I was trying to
convince him that we should delete unused code, and that made me think: why is
it that we sometimes feel reluctant to delete dead code?&lt;/p&gt;

&lt;p&gt;I think there are a couple of mental models that can help us understand why: the
&lt;a href=&quot;https://en.wikipedia.org/wiki/Sunk_cost&quot;&gt;sunk cost fallacy&lt;/a&gt; and the &lt;a href=&quot;https://en.wikipedia.org/wiki/Loss_aversion&quot;&gt;loss aversion principle&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;sunk-cost-fallacy&quot;&gt;Sunk cost fallacy&lt;/h2&gt;

&lt;p&gt;Code isn’t free. Yes, today LLMs make it cheaper to write code (or at least some
&lt;em&gt;type&lt;/em&gt; of code). But shipping good features requires planning, coordination,
research, and even the coding itself requires us to make many decisions about
how to fit it into the existing system. That’s expensive.&lt;/p&gt;

&lt;p&gt;But if it’s so expensive, how could we just throw it away?&lt;/p&gt;

&lt;p&gt;Because the cost is what we call a &lt;a href=&quot;https://en.wikipedia.org/wiki/Sunk_cost&quot;&gt;sunk cost&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;a sunk cost is a sum paid in the past that should no longer be relevant to
decisions about the future&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As humans, we tend to think that a thing (in this case, code) is valuable
&lt;em&gt;because&lt;/em&gt; it was costly to create. Especially if &lt;em&gt;we&lt;/em&gt; incurred the cost. So, we
end up keeping the thing, even if it makes it more costly going forward (that’s
why it’s a fallacy).&lt;/p&gt;

&lt;p&gt;In reality, we should want to avoid &lt;em&gt;future&lt;/em&gt; costs, regardless of what happened
in the past. That’s what we should optimize for. Dead code just makes it
harder for us to reason about the codebase, and it makes it more costly to
introduce new code. That’s why we should remove it.&lt;/p&gt;

&lt;p&gt;So rationally, we should remove it. But it still &lt;em&gt;feels&lt;/em&gt; bad sometimes. Why
is that?&lt;/p&gt;

&lt;h2 id=&quot;loss-aversion-principle&quot;&gt;Loss aversion principle&lt;/h2&gt;

&lt;p&gt;I think the &lt;a href=&quot;https://en.wikipedia.org/wiki/Loss_aversion&quot;&gt;loss aversion principle&lt;/a&gt; can help us understand the feeling:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;loss aversion is a cognitive bias in which the same situation is perceived as
worse if it is framed as a loss, rather than a gain&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, if something seems like a loss, it feels worse. For example, we &lt;em&gt;feel&lt;/em&gt; worse
if someone gives us $100 and later takes it away than if we had never received
the $100 in the first place. In both cases, we end up in the same place, but
getting it taken away feels worse. (If you don’t believe me, promise your kid
ice cream and then renege on the promise.)&lt;/p&gt;

&lt;p&gt;I think the same is true of code.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/delete-dead-code/adding-dead-code-comic-strip.png&quot; alt=&quot;Two scenes. Scene 1: dev 1 says, can I add a bunch of dead code for free? Scene 2: dev 2 says, are you nuts?&quot; /&gt;&lt;/p&gt;

&lt;p&gt;If I were to tell you that I could add 10k lines of unused code in your
codebase instantaneously and completely free, would you accept that?&lt;/p&gt;

&lt;p&gt;Of course not!&lt;/p&gt;

&lt;p&gt;Why would you want dead code that will make things more difficult for your team
in the future? Who cares that it’s free to add today?&lt;/p&gt;

&lt;p&gt;But if you think about it, that’s &lt;em&gt;exactly&lt;/em&gt; the same thing we’re doing when we
keep dead code. We just &lt;em&gt;inverted&lt;/em&gt; the scenario.&lt;/p&gt;

&lt;p&gt;We feel bad &lt;em&gt;losing&lt;/em&gt; the code. But when we’re in our right mind, we wouldn’t
accept useless code just because it was free.&lt;/p&gt;

&lt;h2 id=&quot;a-maxim-to-remember&quot;&gt;A maxim to remember&lt;/h2&gt;

&lt;p&gt;That’s why I have a maxim:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Code is a liability&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Remember that. &lt;strong&gt;Code is a liability.&lt;/strong&gt; It’s not free to create or maintain. The
less of it you have, the better. We only need as much of it as is required to
serve our customers. Not a tab or space more.&lt;/p&gt;</content><author><name>German Velasco</name></author><category term="blog" /><category term="software-development" /><category term="mental-models" /><summary type="html">It&apos;s always surprising when people don&apos;t want to delete dead code because they think they might need it in the future or because they worked so hard on it. Recently, I had one of those conversations with a colleague. I was trying to convince him that we should delete unused code, and that made me think: why is it that we sometimes feel reluctant to delete dead code?</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.germanvelasco.com/images/delete-dead-code/delete-dead-code-comic-strip.png" /><media:content medium="image" url="https://www.germanvelasco.com/images/delete-dead-code/delete-dead-code-comic-strip.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Draw the rest of the ___ owl (software factory edition)</title><link href="https://www.germanvelasco.com/blog/draw-the-rest-of-the-owl-software-factory-edition" rel="alternate" type="text/html" title="Draw the rest of the ___ owl (software factory edition)" /><published>2026-06-12T00:00:00+00:00</published><updated>2026-06-12T00:00:00+00:00</updated><id>https://www.germanvelasco.com/blog/draw-the-rest-of-the-owl-software-factory-edition</id><content type="html" xml:base="https://www.germanvelasco.com/blog/draw-the-rest-of-the-owl-software-factory-edition">&lt;p&gt;&lt;img src=&quot;/images/draw-rest-of-owl/build-a-fing-software-factory.svg&quot; alt=&quot;Two pictures side by side. On the left: two circles. On the right: a fully drawn owl. On the left: &apos;Use LLMs&apos;. On the right: &apos;Build a software factory&apos;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;My guess is that most software development teams in the world are at a point
where we are drawing the two circles (Don’t quote me on that. I don’t have data
to back that up). Those are all the teams where developers use LLMs daily.&lt;/p&gt;

&lt;p&gt;Some may only use them as assistants (e.g. asking them questions). But others no
longer open their editors much, &lt;a href=&quot;https://www.germanvelasco.com/blog/stop-writing-code-by-hand&quot;&gt;if at all&lt;/a&gt;. They
mostly interact with the LLMs through a CLI tool like OpenCode, Claude Code, or
Codex, and have the LLM write all the code, even if that means just fixing a
typo.&lt;/p&gt;

&lt;p&gt;Still, I consider all of that to be on the “draw some circles” side.&lt;/p&gt;

&lt;p&gt;But then you hear about teams that might be getting closer to buiding software
factories, like the team described in Simon Willison’s &lt;a href=&quot;https://simonwillison.net/2026/Feb/7/software-factory/&quot;&gt;blog post&lt;/a&gt; titled “How
StrongDM’s AI team build serious software without even looking at the code”.&lt;/p&gt;

&lt;p&gt;And that’s the “just draw the rest of the &lt;strong&gt;___&lt;/strong&gt; owl”.&lt;/p&gt;

&lt;p&gt;There’s a &lt;strong&gt;monumental gap&lt;/strong&gt; there. And that’s the gap I’m trying to understand.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;How can we trust the code without looking at it? (And does that change as
models get better?)&lt;/li&gt;
  &lt;li&gt;How can we verify that the code is doing what we want it to do?&lt;/li&gt;
  &lt;li&gt;How can we verify that the code is &lt;em&gt;not&lt;/em&gt; doing what we &lt;em&gt;don’t&lt;/em&gt; want it to do?
(e.g. secretly phoning home)&lt;/li&gt;
  &lt;li&gt;How can we make sure we’re building software that will be able to change over
time? (We don’t want software that calcifies past the first few versions, where
a change breaks a seemingly unrelated piece of code elsewhere)&lt;/li&gt;
  &lt;li&gt;How can we make sure the LLMs aren’t writing code that is slowly and silently
decaying until a final day of reckoning? (We don’t want to be like &lt;a href=&quot;https://en.wikipedia.org/wiki/The_Black_Swan:_The_Impact_of_the_Highly_Improbable&quot;&gt;the
turkeys&lt;/a&gt; that are happy and well fed for 364 days of the year, only
to be butchered on Thanksgiving Day)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;how-do-we-draw-the-rest-of-the-owl&quot;&gt;How do we draw the rest of the owl?&lt;/h2&gt;

&lt;p&gt;If we’re going to treat the code the LLMs write as a black box that we never
read, then all we can know about it is the inputs we provide, the outputs it
creates, and the signals we get from what Birgitta Böckeler’s &lt;a href=&quot;https://martinfowler.com/articles/harness-engineering.html&quot;&gt;blog
post&lt;/a&gt; calls “sensors”.&lt;/p&gt;

&lt;p&gt;This is how I envision one factory line (in our larger software factory):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/draw-rest-of-owl/black-box-factory-line.svg&quot; alt=&quot;A box with inputs going into it and outputs coming out. There are sensors inside the box that send out signals of what is working and what isn&apos;t&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;context-is-not-king&quot;&gt;Context is not king&lt;/h3&gt;

&lt;p&gt;The inputs are the prompts and any context we provide the LLMs. Those matter.
But I’m not convinced &lt;em&gt;more&lt;/em&gt; context is what gets us to software factories.&lt;/p&gt;

&lt;p&gt;Some people argue that if we can just provide a full specification, the LLMs
will build it right. I disagree. I’m not convinced a 30-page tome that we create
in “plan” mode is really how we get software factories. A better plan doesn’t
help us know if the LLM has built the right thing, especially as more features
are added over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No, if we’re going to build software factories, I think it all comes down to
&lt;em&gt;verification&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We need to make sure that the software we’re building behaves like we want it
to, &lt;em&gt;while also&lt;/em&gt; making sure the codebase can change over time without decaying.&lt;/p&gt;

&lt;p&gt;Sadly, I see some people reverting to doing a lot of manual testing (e.g. they
test their changes via some UI and say, “looks good”). Manual testing is fine,
but it suffers from the problems it’s always suffered: it’s slow and expensive
(and therefore cannot cover all possible scenarios), and it’s not easily
repeatable (so it’s easy to miss regressions). That’s why automated testing is
so much better — it’s fast, cheap to run, deterministic, and repeatable.&lt;/p&gt;

&lt;p&gt;So, if we cannot verify the behavior by just clicking around the app, what do we
do?&lt;/p&gt;

&lt;p&gt;As someone who’s been practicing TDD/BDD for over a decade, my first inclination
was to treat tests as a verification tool. After all, automated tests should
verify the behavior of our application.&lt;/p&gt;

&lt;p&gt;But if the LLMs are the ones writing the tests &lt;em&gt;inside&lt;/em&gt; the black box, how can
we verify the verifying mechanism?&lt;/p&gt;

&lt;h3 id=&quot;we-need-more-sensors&quot;&gt;We need more sensors&lt;/h3&gt;

&lt;p&gt;This is where I think we need to make the most progress. If it is indeed
possible to create &lt;em&gt;good&lt;/em&gt; software factories, we need more information about
what’s happening inside the black box.&lt;/p&gt;

&lt;p&gt;Remember, we want signals that help us answer questions like these:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Is the application behavior correct?&lt;/li&gt;
  &lt;li&gt;Are the automated tests the LLMs write actually covering the code they created?&lt;/li&gt;
  &lt;li&gt;Is the code architecture evolving well?&lt;/li&gt;
  &lt;li&gt;What’s the health of the code? Is it decaying?&lt;/li&gt;
  &lt;li&gt;Is the code performing unwanted side effects?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far, my preferred flow includes writing Gherkin scenarios that act as an
invariant the LLMs cannot break. I couple that with code coverage to make sure
the tests are exercising new and existing code paths. And I’ve started using
mutation testing to ensure those LLM-generated tests aren’t fake.&lt;/p&gt;

&lt;p&gt;For high-level architecture, I’ve been relying on DDD-style bounded contexts by
checking module boundaries with libraries like &lt;a href=&quot;https://boundary.hexdocs.pm/Boundary.html&quot;&gt;Boundary&lt;/a&gt; for Elixir. That
architecture should not change without human approval.&lt;/p&gt;

&lt;p&gt;I’m also interested in exploring things like &lt;a href=&quot;https://en.wikipedia.org/wiki/Temporal_logic_of_actions&quot;&gt;TLA+&lt;/a&gt; to see if we can have a more
formal specification of the runtime temporal logic. But I haven’t gone far in
that direction yet.&lt;/p&gt;

&lt;p&gt;For code decay, I’m using static analysis tools (like &lt;a href=&quot;https://credo.hexdocs.pm/overview.html&quot;&gt;Credo&lt;/a&gt; for Elixir) to
help measure things like cyclomatic complexity, and I recently built &lt;a href=&quot;https://ex-crap.hexdocs.pm/readme.html&quot;&gt;ExCrap&lt;/a&gt; to
to perform &lt;a href=&quot;https://blog.ndepend.com/crap-metric-thing-tells-risk-code/&quot;&gt;CRAP analysis&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For security, I’m using libraries like &lt;a href=&quot;https://sobelow.hexdocs.pm/readme.html&quot;&gt;Sobelow&lt;/a&gt; and other LLMs to perform
adversarial reviews. (Actually, I use adversarial LLMs for all of the above.)&lt;/p&gt;

&lt;p&gt;But I’m still working on drawing that owl (it still looks like two circles to
me). So, I think we need more (and better) sensors and signals. And then,
ideally, we can feed the signals back to the LLM so the signals also serve as
self-corrective mechanisms.&lt;/p&gt;

&lt;p&gt;Oh, and hey, if we end up &lt;em&gt;not&lt;/em&gt; wanting a software factory after all, all these
sensors can help us humans build better software anyway!&lt;/p&gt;</content><author><name>German Velasco</name></author><category term="blog" /><category term="software-development" /><category term="software-factories" /><category term="ai" /><category term="llms" /><summary type="html">My guess is that most software development teams in the world are at a point where we are drawing the two circles. Those are all the teams where developers use LLMs daily. But then you hear about teams that might be getting closer to building software factories. And that&apos;s the &quot;just draw the rest of the ___ owl&quot;. How do we draw the rest of the owl?</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.germanvelasco.com/images/draw-rest-of-owl/build-a-fing-software-factory.png" /><media:content medium="image" url="https://www.germanvelasco.com/images/draw-rest-of-owl/build-a-fing-software-factory.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Stop writing code by hand (and feel the friction)</title><link href="https://www.germanvelasco.com/blog/stop-writing-code-by-hand" rel="alternate" type="text/html" title="Stop writing code by hand (and feel the friction)" /><published>2026-06-09T00:00:00+00:00</published><updated>2026-06-09T00:00:00+00:00</updated><id>https://www.germanvelasco.com/blog/stop-writing-code-by-hand</id><content type="html" xml:base="https://www.germanvelasco.com/blog/stop-writing-code-by-hand">&lt;p&gt;I know LLMs are a contentious topic. I’m not here to hype them or try to make
you use one if you’re morally opposed to them. That’s not what this post is
about.&lt;/p&gt;

&lt;p&gt;But if you’re using LLMs in your day-to-day workflow, and you’re only using them
as assistants that you ask questions of (like a glorified Google in your
terminal or IDE), or if you call on them like a genie, only when &lt;em&gt;you&lt;/em&gt; think
they’re fit for the task (e.g. “change variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x&lt;/code&gt; to be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;y&lt;/code&gt;”), then this post
is for you.&lt;/p&gt;

&lt;p&gt;I think the &lt;strong&gt;best&lt;/strong&gt; next step you can take is to close your editor or IDE and
&lt;em&gt;stop writing code by hand&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not because writing code by hand is bad or slow. Or because we need to burn more
tokens to make some weird flex. Not even because the CEO of your company
mandates it.&lt;/p&gt;

&lt;p&gt;I think you should stop writing code by hand because working with LLMs opens up
a &lt;em&gt;new&lt;/em&gt; way of working. And the best way to learn how to work in that new way is
to stop writing code by hand. It’s only then that you’ll discover all the
&lt;em&gt;friction&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Having the LLM write the code doesn’t mean that you can’t look at what it
creates (that’s another topic for another day). And very importantly, it doesn’t
mean you have to accept what it writes in the way it writes it.&lt;/p&gt;

&lt;p&gt;That discrepancy between what you want and what it does – that’s the friction I
want you to feel.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/dont-write-code-by-hand/llm-frustration.png&quot; alt=&quot;software developer frustrated at LLMs not doing the right thing&quot; /&gt;&lt;/p&gt;

&lt;p&gt;But don’t revert to writing code by hand! If you dislike what the LLM writes,
stop, take a minute, and try to think, “how could I have gotten it to do what I
wanted in the first place?” Then, try again.&lt;/p&gt;

&lt;p&gt;Maybe the LLM is producing too much output (find a way to work in thinner
slices), or maybe it’s deviating from a standard you have in your codebase (find
a way to automatically enforce that standard), or maybe it’s writing tests in a
particular flavor you don’t like (then you realize the LLM is just copying the
style from another file, and your codebase is inconsistent. Make it consistent).&lt;/p&gt;

&lt;p&gt;Whatever the case, that feeling of &lt;strong&gt;frustration&lt;/strong&gt; that the LLM isn’t doing what
you want is &lt;em&gt;good&lt;/em&gt; (in this case). It’s your spider-sense telling you that using
the LLM in its &lt;em&gt;raw&lt;/em&gt; form is not good enough for what you want. You have to
learn how to use the new tool &lt;em&gt;and&lt;/em&gt; its ecosystem.&lt;/p&gt;

&lt;p&gt;Maybe you need some new “skills” or planning tools or something else. Whatever
the case, the simple action of &lt;em&gt;not writing code by hand&lt;/em&gt; and &lt;em&gt;feeling the
friction&lt;/em&gt; is opening an entirely new way of working.&lt;/p&gt;

&lt;p&gt;But remember (and this is important too) that whenever you’re learning something
new, you might go a bit slower at first. Remember that (especially if you’re a
very senior engineer), because it might have been a long time since you last
struggled so much with a new tool.&lt;/p&gt;

&lt;p&gt;You probably already have your setup tuned perfectly to behave just the way you
want it. And now, well, now you have a machine that everyone says will speed you
up, but it’s simply slowing you down. Give it time. Work on removing that
friction.&lt;/p&gt;</content><author><name>German Velasco</name></author><category term="blog" /><category term="ai" /><category term="llms" /><category term="software-development" /><summary type="html">If you&apos;re using LLMs in your day-to-day workflow, and you&apos;re only using them as assistants that you ask questions of (like a glorified Google in your terminal or IDE), or if you call on them like a genie, only when you think they&apos;re fit for the task (e.g. let&apos;s remove that duplication), then this is for you.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.germanvelasco.com/images/dont-write-code-by-hand/llm-frustration.png" /><media:content medium="image" url="https://www.germanvelasco.com/images/dont-write-code-by-hand/llm-frustration.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Daily writing retrospective</title><link href="https://www.germanvelasco.com/blog/daily-writing-retrospective" rel="alternate" type="text/html" title="Daily writing retrospective" /><published>2026-06-02T00:00:00+00:00</published><updated>2026-06-02T00:00:00+00:00</updated><id>https://www.germanvelasco.com/blog/daily-writing-retrospective</id><content type="html" xml:base="https://www.germanvelasco.com/blog/daily-writing-retrospective">&lt;p&gt;Back in April, I decided to write daily for my newsletter. Up to that point, my
newsletter was mostly a delivery mechanism for my blog. But I wanted to change
that. I wanted to write more.&lt;/p&gt;

&lt;p&gt;This is what I said in my first post:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;It’s been a while since I’ve written in my newsletter (hope you’re still here!)&lt;/p&gt;

  &lt;p&gt;In this age of AI, I find that I want to write more (not less) in my &lt;em&gt;own&lt;/em&gt;
voice. To me, writing is thinking, and I have a lot of thoughts rolling in my
brain that I want to clarify and solidify – so what better way to do that
than to write?&lt;/p&gt;

  &lt;p&gt;But I have to dust some cobwebs.&lt;/p&gt;

  &lt;p&gt;Well, I love Martin Fowler’s &lt;a href=&quot;https://martinfowler.com/bliki/FrequencyReducesDifficulty.html&quot;&gt;“if it hurts, do it more often”&lt;/a&gt;
aphorism. I frequently say it in programming or business circles. Why shouldn’t
it apply to writing as well?&lt;/p&gt;

  &lt;p&gt;So, I’m going to try to write daily (for 30 days) and see if it sticks. At
least for week days (weekends might be tough). If that’s not interesting to
you, you can always unsubscribe. But before you do so, give me a chance!&lt;/p&gt;

  &lt;p&gt;I hope that what I write is useful to you.&lt;/p&gt;

  &lt;p&gt;More than anything, I’d love to also hear from you.&lt;/p&gt;

  &lt;p&gt;If I write something that interests you or sparks something in your mind, I’d
love to hear it. Maybe this can become more of a conversation than me screaming
into the void.&lt;/p&gt;

  &lt;p&gt;Well… that’s it. Hope you enjoy the ride with me.&lt;/p&gt;

  &lt;p&gt;Best,&lt;/p&gt;

  &lt;p&gt;German&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my typical retrospective format, I’ll ask:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;What went well?&lt;/li&gt;
  &lt;li&gt;What went wrong?&lt;/li&gt;
  &lt;li&gt;What could we improve?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-went-well&quot;&gt;What went well?&lt;/h2&gt;

&lt;p&gt;First, the wins.&lt;/p&gt;

&lt;h3 id=&quot;i-wrote-a-lot-more&quot;&gt;I wrote a lot more!&lt;/h3&gt;

&lt;p&gt;I wrote 29 posts in a span of ~40 days. I missed some days. But for the most
part, writing daily – and publishing daily – was a tremendous forcing
function.&lt;/p&gt;

&lt;p&gt;Nothing like a good deadline to force yourself to &lt;em&gt;just ship&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It wasn’t easy. I had to make time for writing (typically at 5am), since I
didn’t have time otherwise. And it took about an hour to write each short post.
But I loved being able to ship daily, even when I wasn’t fully satisfied with
what I wrote. (Don’t let perfect be the enemy of good, right?)&lt;/p&gt;

&lt;h3 id=&quot;i-published-blog-posts&quot;&gt;I published blog posts&lt;/h3&gt;

&lt;p&gt;Out of that daily writing, I also created three blog posts:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.germanvelasco.com/blog/managerial-economics-ai-and-software-development&quot;&gt;What Managerial Economics can tell us about AI and Software Development&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.germanvelasco.com/blog/pair-programming-is-a-cheat-code&quot;&gt;Pair-programming is a cheat code&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.germanvelasco.com/blog/thinking-about-the-extremes-to-break-out-of-local-maxima&quot;&gt;Thinking about the extremes to break out of local maxima&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;it-helped-me-think&quot;&gt;It helped me think&lt;/h3&gt;

&lt;p&gt;In my first post I said “to me writing is thinking”. And boy did it get me
thinking.&lt;/p&gt;

&lt;p&gt;Because I had to write daily, I was constantly thinking of what I would write
the next day, jotting down some ideas and notes, and it did help me work through
some of the “thoughts” I had in my head (the ones I mentioned in my first post).&lt;/p&gt;

&lt;h3 id=&quot;it-helped-me-connect-with-readers&quot;&gt;It helped me connect with readers&lt;/h3&gt;

&lt;p&gt;One of the coolest things was when a reader responded to a post. I love
connecting with others over ideas. And a few people responded to some of my
posts excitedly agreeing, asking questions, or offering their perspective.&lt;/p&gt;

&lt;p&gt;It was honestly one of the best surprises about writing daily. Before I started,
I didn’t know what to expect: either utter silence or perhaps outrage (those are
the fears in our heads). Instead, people were interested and excited.&lt;/p&gt;

&lt;p&gt;To those of you who wrote back, thank you!&lt;/p&gt;

&lt;h3 id=&quot;my-newsletter-doesnt-just-have-to-be-a-delivery-mechanism&quot;&gt;My newsletter doesn’t just have to be a delivery mechanism&lt;/h3&gt;

&lt;p&gt;Because I connected with readers, and because I have now made my newsletter into
a vehicle that is more than just delivering blog posts, I feel comfortable
writing there.&lt;/p&gt;

&lt;p&gt;That may not seem big, but it’s a huge win for me.&lt;/p&gt;

&lt;p&gt;Before this exercise, I felt like my newsletter had an implicit agreement that
it was purely a delivery mechanism for blog posts. After 29 posts, I think the
people who only wanted it for that reason have unsubscribed. :)&lt;/p&gt;

&lt;p&gt;So, overall, I’m very glad I did it. But I want to make some changes. So, on to
the what went wrong section.&lt;/p&gt;

&lt;h2 id=&quot;what-went-wrong&quot;&gt;What went wrong?&lt;/h2&gt;

&lt;h3 id=&quot;its-costly&quot;&gt;It’s costly&lt;/h3&gt;

&lt;p&gt;I &lt;em&gt;loved&lt;/em&gt; the forcing function of writing daily. But the reality is that it is
hard to always wake up at 5am and write.&lt;/p&gt;

&lt;p&gt;Sleep is very important to me. Due to previous health issues, I try to
prioritize sleep as much as possible, and my kids are also grateful when I’m not
grumpy and tired. And though I wish I didn’t need as much sleep as I do, I think
making sure I sleep well is the right trade-off.&lt;/p&gt;

&lt;p&gt;I also have a bad habit of wanting to do too many things. So, as I was writing
daily, I was also exploring new AI workflows, recording a couple of remote
pairing sessions (which I’m still editing), and trying to get more information
on how other teams are using AI. Oh, and I also had to do my regular consulting
job. That’s a lot to fit into a single week.&lt;/p&gt;

&lt;p&gt;So, even though I love writing daily, there are days when it is more important
that I get an extra hour of sleep or that I edit a video.&lt;/p&gt;

&lt;h3 id=&quot;i-forgot-to-decouple-deployments-from-releasing&quot;&gt;I forgot to decouple deployments from releasing&lt;/h3&gt;

&lt;p&gt;Another huge problem was that by writing daily, I meant &lt;em&gt;publishing&lt;/em&gt; daily.&lt;/p&gt;

&lt;p&gt;In software, I always encourage teams to decouple deployments from releasing
software to customers. We tend to use feature flags for that. That way, you can
deploy whenever you want – ideally continuously. But you only release a feature
when it’s ready.&lt;/p&gt;

&lt;p&gt;Well, if I’d done that with writing, I could have written daily but only
published when I felt ready. In that case, I might have felt differently about
some of my posts.&lt;/p&gt;

&lt;p&gt;Which brings me to the next point.&lt;/p&gt;

&lt;h3 id=&quot;some-ideas-need-more-space-and-time&quot;&gt;Some ideas need more space and time&lt;/h3&gt;

&lt;p&gt;Publishing daily meant I couldn’t fully work out some of the thornier ideas in
my head.&lt;/p&gt;

&lt;p&gt;Some writing just doesn’t fit into a single day – or into a single email, for
that matter. Many thoughts are just complicated, and I feel like I have to
untangle them before I can write about them.&lt;/p&gt;

&lt;p&gt;I mean, that’s part of the beauty of writing: it forces you to work out those
complicated topics until you can explain them simply. But because I had to
publish something &lt;em&gt;every&lt;/em&gt; day, I just couldn’t spend the time I needed on the
thornier ideas.&lt;/p&gt;

&lt;p&gt;So, publishing daily sometimes forced me to write about a simpler idea at the
cost of refining a thornier one.&lt;/p&gt;

&lt;h3 id=&quot;posts-are-siloed&quot;&gt;Posts are siloed&lt;/h3&gt;

&lt;p&gt;This is a good and a bad thing.&lt;/p&gt;

&lt;p&gt;Because posts are not available in the general internet (unlike a blog post is),
I felt more okay with publishing things that I wouldn’t have otherwise. I don’t
mean that the posts were bad, but there’s an internal pressure when I publish a
blog post that I have to make sure it’s &lt;em&gt;really&lt;/em&gt; good because some unknown
masses will read it.&lt;/p&gt;

&lt;p&gt;In reality, for all I know, more people actually read the newsletter posts than
my blog posts. But because the blog posts are “out there”, I feel that pressure.
And that pressure can lead to &lt;em&gt;not&lt;/em&gt; writing. So, the siloing can be a good
thing.&lt;/p&gt;

&lt;p&gt;But it’s definitely &lt;em&gt;sad&lt;/em&gt; that they’re siloed.&lt;/p&gt;

&lt;p&gt;I want to write to communicate with others. And I &lt;em&gt;love&lt;/em&gt; blogs because to me
they are part of the beautiful fabric of the internet. People sharing learnings,
lessons, and ideas without gatekeeping. So, I’m a little sad that I have things
written that live in emails and cannot be easily shared.&lt;/p&gt;

&lt;h2 id=&quot;what-could-we-improve&quot;&gt;What could we improve?&lt;/h2&gt;

&lt;p&gt;Having said all that, I hope to make some tweaks.&lt;/p&gt;

&lt;p&gt;I won’t continue to put the must-publish-daily pressure on myself. I wish I
didn’t have to remove that (I like the forcing function), but I have to
prioritize other things right now.&lt;/p&gt;

&lt;p&gt;But that doesn’t mean I’ll stop writing!&lt;/p&gt;

&lt;p&gt;On the contrary, I hope to continue to write more. I’ll still make 5am my
writing time. It works most days. I just might not be able to hit it daily.&lt;/p&gt;

&lt;p&gt;And I will continue writing in my newsletter. But I hope to publish a lot more
to my blog as well. In a way, I would love it if I could write daily on my blog.
That’s a bit wild, but I think it’s the ideal.&lt;/p&gt;

&lt;p&gt;And I also hope to make my newsletter more of a 2-way channel.&lt;/p&gt;

&lt;p&gt;I want to continue connecting directly with readers of the newsletter. That was
one of my favorite parts of writing daily, so I’m really hoping I continue doing
that.&lt;/p&gt;

&lt;h2 id=&quot;write-to-me-or-send-me-your-blog&quot;&gt;Write to me or send me your blog!&lt;/h2&gt;

&lt;p&gt;Finally, some parting thoughts.&lt;/p&gt;

&lt;p&gt;First, if you’re reading this, thanks for reading! Thank you for giving me some
of your precious time.&lt;/p&gt;

&lt;p&gt;Second, I encourage you to write more! (Make sure &lt;em&gt;you&lt;/em&gt; write, not the robots!)&lt;/p&gt;

&lt;p&gt;If you’d like a reader, send me an email and send me your blog. I love
discovering new blogs to read.&lt;/p&gt;</content><author><name>German Velasco</name></author><category term="blog" /><category term="software-development" /><category term="writing" /><summary type="html">Back in April, I decided to write daily for my newsletter. Up to that point, my newsletter was mostly a delivery mechanism for my blog. But I wanted to change that. I wanted to write more.</summary></entry><entry><title type="html">Thinking about the extremes to break out of local maxima</title><link href="https://www.germanvelasco.com/blog/thinking-about-the-extremes-to-break-out-of-local-maxima" rel="alternate" type="text/html" title="Thinking about the extremes to break out of local maxima" /><published>2026-05-22T00:00:00+00:00</published><updated>2026-05-22T00:00:00+00:00</updated><id>https://www.germanvelasco.com/blog/thinking-about-the-extremes-to-break-out-of-local-maxima</id><content type="html" xml:base="https://www.germanvelasco.com/blog/thinking-about-the-extremes-to-break-out-of-local-maxima">&lt;p&gt;Thinking about the extremes is a powerful tool to help refine our intuition,
unearth second-order effects, discover novel ways of doing things, and generally
find alternate solutions to problems when we’re stuck.&lt;/p&gt;

&lt;p&gt;Think of it like this: you’re a person trying to climb a hill.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/local-maxima/marginal-step-takes-you-up.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;One way to do that is to take a step in any direction. If you discover that the
next step is higher than your previous one, great! You’re making progress.&lt;/p&gt;

&lt;p&gt;You repeat that process over and over again, taking one step at a time. Each
time, you check if you’re higher than before. If you are, you’re going uphill!&lt;/p&gt;

&lt;p&gt;That’s marginal improvement, and it’s typically good. After only a finite number
of steps, you’ll reach the top of the hill.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/local-maxima/top-of-the-hill.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;But once you’re at the top, any step you take will be a step downhill. You’ve
reached &lt;em&gt;a&lt;/em&gt; maximum.&lt;/p&gt;

&lt;p&gt;How can you reach higher places? You can’t use the same “one small step”
mentality. You actually need to change your approach.&lt;/p&gt;

&lt;p&gt;And that’s where thinking about extremes can be helpful.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/local-maxima/global-maxima.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It’s only by thinking, “how can I climb Mount Everest?” that you realize you
might need to get on a plane and fly to Nepal.&lt;/p&gt;

&lt;p&gt;I think the same is true in business and software development. You can often get
very far by making the next marginal improvement. One step at a time is good!
You might even reach some heights. But at some point, you’ll likely get stuck in
a local maximum. And it’s at that point that thinking about the extremes can
help.&lt;/p&gt;

&lt;p&gt;Consider this example. If you’re only trying to grow your revenue by 3% this
year, you’ll probably focus on marginal improvements. But if you start thinking
about how you can double or triple your revenue this year, you’ll come up with
some wild, new ideas because you &lt;em&gt;have&lt;/em&gt; to. There’s no other way to reach that
goal.&lt;/p&gt;

&lt;p&gt;Or think about query optimization in software development. You might try to
optimize the query as much as possible. But at some point, you can’t make it any
faster (you’ve hit a local minimum). If you think about the extreme you might
ask, what if we could avoid making the database call altogether? You might
discover a way to support the feature without that query or a different way to
get the same information.&lt;/p&gt;

&lt;p&gt;Steady progress is good and important. But every once in a while, you get stuck.
That’s a good time to start thinking about the extremes.&lt;/p&gt;</content><author><name>German Velasco</name></author><category term="blog" /><category term="mental-models" /><category term="business" /><category term="software-development" /><summary type="html">Thinking about the extremes is a powerful tool to help refine our intuition, unearth second-order effects, discover novel ways of doing things, and generally find alternate solutions to problems when we&apos;re stuck.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.germanvelasco.com/images/local-maxima/top-of-the-hill.png" /><media:content medium="image" url="https://www.germanvelasco.com/images/local-maxima/top-of-the-hill.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Pair-programming is a cheat code</title><link href="https://www.germanvelasco.com/blog/pair-programming-is-a-cheat-code" rel="alternate" type="text/html" title="Pair-programming is a cheat code" /><published>2026-05-01T00:00:00+00:00</published><updated>2026-05-01T00:00:00+00:00</updated><id>https://www.germanvelasco.com/blog/pair-programming-is-a-cheat-code</id><content type="html" xml:base="https://www.germanvelasco.com/blog/pair-programming-is-a-cheat-code">&lt;p&gt;&lt;img src=&quot;/images/pair-programming-is-a-cheat-code.png&quot; alt=&quot;pair-programming is a cheat code written on a whiteboard&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I had that written on my office whiteboard for a while last year.&lt;/p&gt;

&lt;p&gt;I had just finished working with a client for over a year where the whole team
pair-programmed full-time (we also did a lot of mobbing).&lt;/p&gt;

&lt;p&gt;I was then starting work with a new client, and I wanted to remind myself to
push for pair-programming as much as possible because it is a cheat code for
developers &lt;em&gt;AND&lt;/em&gt; for businesses.&lt;/p&gt;

&lt;p&gt;Here are some of “cheats” pair-programming gives you.&lt;/p&gt;

&lt;h2 id=&quot;pair-programming-increases-decision-speed&quot;&gt;Pair-programming increases decision speed&lt;/h2&gt;

&lt;p&gt;I think this is one of the most underrated benefits of pair-programming.&lt;/p&gt;

&lt;p&gt;When we’re building software, there are countless decisions we have to make.
Those decisions take time. And frequently, they take coordination across teams.
That’s really costly.&lt;/p&gt;

&lt;p&gt;And those decisions tend to have ramifications that compound.&lt;/p&gt;

&lt;p&gt;It’s typically cheaper for a business to change course early in the
feature-development lifecycle. So, you want to arrive at the right decision as
early as possible. With pair-programming, you have two people with full context
able to bounce ideas off each other, challenge assumptions, and refine solutions
quickly.&lt;/p&gt;

&lt;p&gt;Compare that to finishing your feature, opening a pull-request, and waiting a
few hours (or days) for someone to &lt;em&gt;then&lt;/em&gt; question a piece of work. You go back
and forth on alternatives (what you would’ve done with a pair at the moment you
were coding that thing), and finally, you have to rewrite large pieces of the
code because the decision made early informed the design of the whole feature.&lt;/p&gt;

&lt;h2 id=&quot;code-review-is-done-in-real-time-with-full-context&quot;&gt;Code-review is done in real-time with full context&lt;/h2&gt;

&lt;p&gt;It’s sad that most companies have a culture of doing code-review through
pull-requests. GitHub has been a huge influence in that. And it’s a great model
for low-trust environments (where PR author and reviewer don’t know each other).
There’s high friction, and that’s a good thing.&lt;/p&gt;

&lt;p&gt;But your company should be a high-trust environment. You know your team. You
sync with them daily! So, to do code-review in the same slow, async way we do
with open-source code seems suboptimal.&lt;/p&gt;

&lt;p&gt;And when you’re reviewing a pull-request, you’re often missing context for the
work that’s being done, decisions made along the way, and sometimes why the team
is even building a given feature. So, as the reviewer, you have to either gain a
lot of context to review the code well, or your review is worse than it would be
if you had all that context.&lt;/p&gt;

&lt;p&gt;Well, do you know who has all that context? The pair. And the pair can review
the code &lt;em&gt;as they write it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That doesn’t mean others can’t look at the code. If you’re working on something
gnarly that you want more eyes on, you can always tap another pair and ask them
to do a review on a call. All of a sudden you have 4 people (2 pairs) reviewing
the code while it’s being written. Very effective and efficient.&lt;/p&gt;

&lt;h2 id=&quot;knowledge-transfer-happens-naturally&quot;&gt;Knowledge transfer happens naturally&lt;/h2&gt;

&lt;p&gt;In programming, we’re always talking about the &lt;a href=&quot;https://en.wikipedia.org/wiki/Bus_factor&quot;&gt;bus factor&lt;/a&gt; or domain knowledge
silos. We don’t want a single developer to “own” a piece of code, and if they
leave the company, get sick, or get hit by a bus, nobody else knows how to touch
it.&lt;/p&gt;

&lt;p&gt;(Also, note that they’re likely the &lt;em&gt;only&lt;/em&gt; person who can review PRs related to
that area of the codebase. Makes that PR problem even worse!)&lt;/p&gt;

&lt;p&gt;So, teams tend to need to put a lot of effort into artifacts (documentation,
videos, etc.) that can share that knowledge with other developers in case that
bus really comes.&lt;/p&gt;

&lt;p&gt;But documentation is costly and at best a proxy. Usually, it’s out of date and
incomplete.&lt;/p&gt;

&lt;p&gt;The code is the source of truth and the full specification of what our
application currently does. So, it would be nice if we could rely on that. Of
course, it takes time for people to gain knowledge of a particular domain in the
codebase.&lt;/p&gt;

&lt;p&gt;That’s where switching pairs frequently helps tremendously.&lt;/p&gt;

&lt;p&gt;If you frequently switch who you pair with, that siloed context is immediately
spread out across the team. The more you switch pairs, the more the context
spreads. You don’t have to do anything special (other than rotate pairs).&lt;/p&gt;

&lt;h2 id=&quot;pair-programming-is-the-best-way-to-teach-junior-developers&quot;&gt;Pair-programming is the best way to teach junior developers&lt;/h2&gt;

&lt;p&gt;And since sharing context is so easy with pair-programming, it’s also one of the
best ways to teach junior developers. I’ve seen junior developers get good at an
alarmingly fast pace. And they’ll ask the best questions, questions that frankly
make you (the senior developer) better at your craft.&lt;/p&gt;

&lt;h2 id=&quot;pair-programming-helps-developers-learn-better-ways-to-work-and-solve-problems&quot;&gt;Pair-programming helps developers learn better ways to work and solve problems&lt;/h2&gt;

&lt;p&gt;The previous point leads to this one.&lt;/p&gt;

&lt;p&gt;When you’re pair-programming, people have insight into &lt;em&gt;how&lt;/em&gt; you work, not just
the final code in a pull-request. That is phenomenal for two reasons:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;a) Others learn from things you do. Things they didn’t even know were
possible! It opens minds to whole new categories.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;b) You learn from questions your pair asks! It’s amazing how a simple question
can lead you to realize you haven’t re-evaluated the best way to solve a problem
in many years.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(And note that we’re all switching between (a) and (b) all the time. I learn
from you and you learn from me. And we both ask questions of each other.)&lt;/p&gt;

&lt;h2 id=&quot;is-it-still-useful-in-an-ai-world&quot;&gt;Is it still useful in an AI world?&lt;/h2&gt;

&lt;p&gt;Yes, yes it is!&lt;/p&gt;

&lt;p&gt;Programming with AI is changing rapidly. How can your team learn to use these
new tools as fast as possible?&lt;/p&gt;

&lt;p&gt;I think there’s no better way to do that (and to develop best practices) than to
pair a lot and rotate pairs frequently.&lt;/p&gt;

&lt;p&gt;Think about it the other way. The more you silo information by &lt;em&gt;not&lt;/em&gt; pairing,
the slower the spread of knowledge and good practices. So, pair-programming is
even &lt;em&gt;more&lt;/em&gt; (not less) useful in this world full of LLMs.&lt;/p&gt;

&lt;h2 id=&quot;give-it-a-try&quot;&gt;Give it a try&lt;/h2&gt;

&lt;p&gt;There are more reasons why pair-programming is a cheat code, but hopefully those
are good enough to give you a taste of the value it can bring to your team.&lt;/p&gt;

&lt;p&gt;If your team doesn’t do pair-programming, I encourage you to give it a &lt;em&gt;real&lt;/em&gt;
try. Read a little about how to do it well (and there are ways to do it well and
ways to do it poorly). And stick with it. It’s a skill that needs to be learned,
so don’t expect to be immediately proficient at it.&lt;/p&gt;</content><author><name>German Velasco</name></author><category term="blog" /><category term="pair-programming" /><summary type="html">I had that written on my office whiteboard for a while last year. I had just finished working with a client for over a year where the whole team pair-programmed full-time (we also did a lot of mobbing). I was then starting work with a new client, and I wanted to remind myself to push for pair-programming as much as possible because it is a cheat code for developers AND for businesses. Here are some of &quot;cheats&quot; pair-programming gives you.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.germanvelasco.com/images/pair-programming-is-a-cheat-code.png" /><media:content medium="image" url="https://www.germanvelasco.com/images/pair-programming-is-a-cheat-code.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>