<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://paelladoc.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://paelladoc.com/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-07-22T14:12:02+00:00</updated><id>https://paelladoc.com/feed.xml</id><title type="html">PAELLADOC — The Local Software Factory</title><subtitle>Build and run software from an idea or an existing repo. Discovery, specs, sprints, AI agents, code knowledge graphs and evidence—local on your Mac.</subtitle><author><name>PAELLADOC</name></author><entry xml:lang="en-US"><title type="html">When to add tests and architecture to an AI-built project</title><link href="https://paelladoc.com/blog/when-to-add-tests-and-architecture/" rel="alternate" type="text/html" title="When to add tests and architecture to an AI-built project" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://paelladoc.com/blog/when-to-add-tests-and-architecture</id><content type="html" xml:base="https://paelladoc.com/blog/when-to-add-tests-and-architecture/"><![CDATA[<p>There are two pieces of advice about tests and architecture in AI-built projects, and both are wrong. One says write the tests first, design the architecture up front, do it properly from day one. The other says skip all of it, ship on vibes, you can add the boring stuff later. Following the first kills the speed that made AI worth using. Following the second guarantees the collapse everyone warns about.</p>
<p>The right answer is a moment, not a policy. And the useful news is that the moment announces itself, if you know the signal to watch for. It is the same signal for tests and for architecture, which is why I treat them together.</p>
<h2 id="why-day-one-is-wrong">Why day one is wrong</h2>
<p>Writing tests and designing architecture before you know if the idea works is spending your most expensive effort on your least certain bet.</p>
<p>Most of what you build in the first days of an AI project will not survive the week. You are exploring. The whole point of building with an agent is that exploration got cheap, so you can try five versions of an idea and throw four away. Tests written against code you are about to delete are pure waste. Architecture designed for a product you have not validated is a cathedral built for a congregation that may never show up.</p>
<p>There is a real cost to premature structure beyond the wasted hours: it makes you reluctant to throw the code away. Once you have invested in testing and structuring something, you defend it, even when the right move is to delete it. Premature rigor turns exploration into sunk cost. In the phase where you should be most willing to discard, it makes you least willing.</p>
<p>So day one is wrong not because tests are bad but because you do not yet know what deserves them.</p>
<h2 id="why-never-is-wrong">Why never is wrong</h2>
<p>The other failure is more common and more expensive, because it does not hurt until it is too late to be cheap.</p>
<p>An AI project with no tests and no architecture runs fine right up to the point where you need to change it without breaking it. Then the absence of both becomes the thing that stops you. Every change is a gamble, because nothing catches a regression and nothing constrains the blast radius. This is the <a href="/blog/scared-to-touch-your-code/">fear of touching your own code</a>, and it is not irrational. It is the correct response to a codebase with no safety net.</p>
<p>“Never” also compounds. Each feature an agent adds to an unstructured, untested base makes the next one riskier, because the base it builds on is less trustworthy. What starts as speed becomes paralysis, and the paralysis arrives exactly when the project is succeeding and you most need to move. That is the mechanism behind <a href="/blog/vibe-coding-90-day-reckoning/">vibe-coded apps breaking around month three</a>: not a sudden failure, but the accumulated cost of “never” coming due.</p>
<h2 id="the-signal-load-bearing">The signal: load-bearing</h2>
<p>Here is the moment. A path deserves tests and structure when it becomes load-bearing, which is a specific and observable thing: when breaking it would hurt a real user, and when you are going to keep changing it.</p>
<p>Both conditions matter. A path that would hurt a user but never changes does not urgently need tests, because tests protect against change and there is none. A path that changes constantly but hurts no one if it breaks can stay scrappy. The intersection, high stakes and high change, is where you invest, and it is usually a small fraction of the codebase.</p>
<p>This gives you a rule you can actually apply. Do not ask “should this project have tests.” Ask, for each path, “would a user be hurt if this broke, and am I going to touch it again.” When both answers are yes, that path just became load-bearing, and that is your signal. Not the project’s age. Not a coverage target. The status of the specific path in front of you.</p>
<figure class="tp-diagram tp-diagram--gate" data-diagram-type="gate" role="img" aria-label="The rule you can apply per path: invest only where breaking it hurts a user and you will keep changing it."><svg viewBox="0 0 760 240" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
  <rect x="40" y="86" width="180" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="130" y="125" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-ink)">A PATH</text>
  <path d="M 226 119 L 286 119 M 280 113 L 286 119 L 280 125" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path>
  <path d="M 380 60 L 452 119 L 380 178 L 308 119 Z" fill="none" stroke="var(--tp-mostaza)" stroke-width="1.8"></path>
  <text x="380" y="125" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-mostaza)">LOAD-BEARING</text>
  <path d="M 458 119 L 528 119 M 522 113 L 528 119 L 522 125" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path>
  <rect x="534" y="86" width="186" height="66" fill="var(--tp-verde)" fill-opacity="0.12" stroke="var(--tp-verde)" stroke-width="1.5"></rect>
  <text x="627" y="125" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-verde)">TESTS AND STRUCTURE</text>
  <path d="M 380 184 L 380 214 L 226 214 M 232 208 L 226 214 L 232 220" stroke="var(--tp-terracota)" stroke-width="1.5" fill="none"></path>
  <text x="380" y="232" text-anchor="middle" font-family="var(--tp-mono)" font-size="12" letter-spacing="2" fill="var(--tp-terracota)">STAY SCRAPPY</text>
</svg><figcaption>The rule you can apply per path: invest only where breaking it hurts a user and you will keep changing it.</figcaption></figure>
<h2 id="tests-first-then-architecture">Tests first, then architecture</h2>
<p>When the signal fires, do the two things in order, because one enables the other.</p>
<p>Add the tests first, and add them as characterization tests: tests that pin down what the code currently does, before you change it. On an AI-built project you rarely have a clean spec to test against, so you test the behavior that exists and works. What you are actually testing shifts when a machine wrote the code, which is a discipline of its own: <a href="/blog/testing-ai-generated-code/">testing AI-generated code</a> is about catching the failure modes a human author would not have produced. <a href="https://martinfowler.com/bliki/SelfTestingCode.html">Characterization tests</a> are specifically the tool for locking down behavior you did not design so you can change it safely. And they only count if you actually run them. An agent that reports green without executing the suite has handed you nothing, which is the whole reason <a href="/blog/done-means-done/">done has to mean done</a>.</p>
<p>Then, and only then, refactor the architecture, using the tests as your net. Now you can restructure the load-bearing path, because the tests will tell you the moment you change its behavior. Refactoring without that net is how you introduce the exact regression you were trying to prevent. The tests are what make the architecture work safe instead of terrifying.</p>
<p>Notice the sequencing. Tests do not come first because they are more important. They come first because they make the architecture change survivable. Reverse the order and you are restructuring blind.</p>
<h2 id="this-is-paying-debt-in-the-right-order">This is paying debt in the right order</h2>
<p>If you have read how I think about <a href="/blog/vibe-coding-technical-debt/">vibe coding debt</a>, this is the same idea from the other side. Verification debt and architecture debt <a href="https://martinfowler.com/bliki/TechnicalDebt.html">both come due</a> when a path becomes load-bearing. “When to add tests and architecture” is just “when does that debt come due,” and the answer is: when the path can hurt someone and you are going to keep touching it.</p>
<p>That framing also protects you from the green-build trap. A load-bearing path with tests that pass but prove nothing is not protected, it just feels protected, which is worse. The tests have to actually exercise the behavior that matters, or you have paid the cost of testing without buying the safety. That is the difference between a real safety net and <a href="/blog/spec-driven-development-green-build-not-correct/">a green build that is not a correct feature</a>.</p>
<p>And this whole discipline is what separates shipping a demo from shipping something you can operate. Knowing when to add rigor, and where, is most of the actual work of getting <a href="/blog/vibe-coding-to-production/">from vibe coding to production</a> without either drowning in premature process or collapsing under none.</p>
<h2 id="where-paelladoc-fits">Where PaellaDoc fits</h2>
<p>The hard part of this rule is not agreeing with it. It is knowing, at any given moment, which paths in your project are load-bearing and whether they are actually covered. That knowledge lives in your head, and it goes stale fast when an agent is changing the codebase faster than you can track.</p>
<p>PaellaDoc keeps that map current: which parts of the product are load-bearing, what contract each one is supposed to meet, and what evidence exists that it does. So when a path crosses into load-bearing, you see it, instead of finding out when it breaks. The signal stops depending on your memory and starts living in the system, which is the only version of this rule that survives contact with real speed.</p>
<h2 id="faq">FAQ</h2>
<p><strong>Should I write tests before I start an AI project?</strong> Usually not. Early code is exploratory and mostly disposable, and testing code you are about to delete is wasted effort that also makes you reluctant to delete it. Wait until a path becomes load-bearing.</p>
<p><strong>What does “load-bearing” actually mean?</strong> A path is load-bearing when breaking it would hurt a real user and you are going to keep changing it. Both conditions have to hold. High stakes with no change, or high change with no stakes, do not urgently need the investment.</p>
<p><strong>Do tests or architecture come first?</strong> Tests first, as characterization tests that pin down current behavior, then architecture using those tests as a safety net. Refactoring without tests reintroduces the regressions you were trying to prevent.</p>
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    { "@type": "Question", "name": "Should I write tests before I start an AI project?", "acceptedAnswer": { "@type": "Answer", "text": "Usually not. Early code is exploratory and mostly disposable, and testing code you are about to delete is wasted effort that also makes you reluctant to delete it. Wait until a path becomes load-bearing." } },
    { "@type": "Question", "name": "What does load-bearing actually mean?", "acceptedAnswer": { "@type": "Answer", "text": "A path is load-bearing when breaking it would hurt a real user and you are going to keep changing it. Both conditions have to hold. High stakes with no change, or high change with no stakes, do not urgently need the investment." } },
    { "@type": "Question", "name": "Do tests or architecture come first?", "acceptedAnswer": { "@type": "Answer", "text": "Tests first, as characterization tests that pin down current behavior, then architecture using those tests as a safety net. Refactoring without tests reintroduces the regressions you were trying to prevent." } }
  ]
}
</script>]]></content><author><name>@jlcases</name></author><category term="vibe" /><category term="vibe-coding" /><category term="testing" /><category term="architecture" /><category term="ai-coding" /><category term="technical-debt" /><category term="paelladoc" /><summary type="html"><![CDATA[Adding tests and architecture to an AI-built project on day one wastes effort; adding them never guarantees collapse. The right moment is a signal you can watch for, and it is the same signal for both.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://paelladoc.com/assets/images/clusters/vibe-hero.png" /></entry><entry xml:lang="en-US"><title type="html">What is vibe coding? (and where it stops working)</title><link href="https://paelladoc.com/blog/what-is-vibe-coding/" rel="alternate" type="text/html" title="What is vibe coding? (and where it stops working)" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://paelladoc.com/blog/what-is-vibe-coding</id><content type="html" xml:base="https://paelladoc.com/blog/what-is-vibe-coding/"><![CDATA[<p>Vibe coding is describing what you want in plain language and letting a model write the code, without reading most of it. You steer by the result: run it, see if it feels right, ask for changes, run it again. The code is a means, the behavior is the point, and you mostly do not look under the hood.</p>
<p>Andrej Karpathy named the thing in early 2025, describing a way of working where you “fully give in to the vibes” and forget the code even exists. He meant it partly as a joke about weekend projects. The term stuck because it described something <a href="https://survey.stackoverflow.co/2025/">a lot of people were already doing</a> and did not have a word for.</p>
<p>It is worth defining plainly, because the term now gets used two ways: as a badge by people shipping real things fast, and as an insult by people who think all of it is reckless. Both are missing the actual line.</p>
<h2 id="what-vibe-coding-actually-is">What vibe coding actually is</h2>
<p>Strip away the mystique and it is a loop:</p>
<ol>
<li>Describe a change or a feature in natural language.</li>
<li>An agent writes or edits the code.</li>
<li>You run it and look at the result, not the diff.</li>
<li>You describe what is wrong or what is next.</li>
<li>Repeat.</li>
</ol>
<figure class="tp-diagram tp-diagram--loop" data-diagram-type="loop" role="img" aria-label="The vibe coding loop. Step three is the whole technique: you read the behavior, not the diff."><svg viewBox="0 0 760 230" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><rect x="40" y="52" width="144" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="112" y="91" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">DESCRIBE</text><path d="M 190 85 L 212 85 M 206 79 L 212 85 L 206 91" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path><rect x="218" y="52" width="144" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="290" y="91" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">AGENT WRITES</text><path d="M 368 85 L 390 85 M 384 79 L 390 85 L 384 91" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path><rect x="396" y="52" width="144" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="468" y="91" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">RUN IT</text><path d="M 546 85 L 568 85 M 562 79 L 568 85 L 562 91" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path><rect x="574" y="52" width="144" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="646" y="91" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">READ BEHAVIOR</text><path d="M 720 118 L 720 178 L 40 178 L 40 118 M 40 130 L 34 121 L 46 121 Z" stroke="var(--tp-mostaza)" stroke-width="1.5" fill="none"></path>
  <text x="380" y="198" text-anchor="middle" font-family="var(--tp-mono)" font-size="12" letter-spacing="2" fill="var(--tp-mostaza)">NEXT CHANGE</text></svg><figcaption>The vibe coding loop. Step three is the whole technique: you read the behavior, not the diff.</figcaption></figure>
<p>The defining trait is step 3. In classic development you read the code to know it is right. In vibe coding you read the behavior. That single substitution is what makes it fast, and it is also the entire source of its limits.</p>
<p>This is different from AI-assisted engineering, where you still read and own the diff and the model is an accelerator. Vibe coding hands the reading over too. The distinction is not snobbery, it decides what the output is safe for.</p>
<h2 id="what-it-is-genuinely-good-at">What it is genuinely good at</h2>
<p>I am not here to sneer at it. The technique is real and it earns its place:</p>
<p><strong>Prototypes.</strong> When the goal is to see if an idea feels right, vibe coding gets you a clickable answer in an afternoon instead of a week. That is a legitimate superpower.</p>
<p><strong>Throwaway tools.</strong> A script you will run once, an internal utility for yourself, a thing that does not need to survive contact with other people. Correctness that expires tomorrow is fine when you are done today.</p>
<p><strong>Learning by seeing.</strong> Watching an idea become a working screen teaches you more about whether it is worth building than any amount of planning.</p>
<p><strong>The first draft of something real.</strong> Even things that will become products often start here, and should. The mistake is not starting with vibe coding. It is not noticing when you left its territory.</p>
<h2 id="where-it-stops-working">Where it stops working</h2>
<p>The edge is not a difficulty level. It is a change in what you need from the output. Vibe coding stops working the moment the code has to survive things a demo never tests:</p>
<p><strong>When it has to keep working while you change it.</strong> A prototype’s behaviors worked once. A product’s behaviors have to keep working through every future change, which means something has to catch it when a change breaks them. Vibe coding produces no such net.</p>
<p><strong>When someone else depends on it.</strong> The moment a real user’s money, data, or time rides on the app, “it worked when I clicked it” stops being enough. You need to know it works, not feel it. A <a href="/blog/spec-driven-development-green-build-not-correct/">green build is not a correct feature</a>, and “it ran on my screen” is even weaker than a green build.</p>
<p><strong>When it grows past what you can hold in your head.</strong> Below some size you can keep the whole app in memory and steer by vibes. Past it, you cannot, and because you never read the code, you now own software you cannot navigate. That is the <a href="/blog/vibe-coding-90-day-reckoning/">reckoning most vibe-coded apps hit around month 3</a>.</p>
<p><strong>When features start colliding.</strong> Each feature was generated assuming the rest of the app holds still. Add enough of them and <a href="/blog/features-breaking-features/">new features start breaking old ones</a>, because nothing enforced the assumption.</p>
<p>None of these mean the technique failed. They mean the prototype succeeded so well that it stopped being a prototype. That is the good problem, and it has a name: <strong>the prototype became the product</strong>, and it happened before you decided to let it.</p>
<h2 id="why-the-confusion-is-worth-clearing-up">Why the confusion is worth clearing up</h2>
<p>The reason this matters is that people argue about vibe coding as if it were one thing that is either good or bad. It is not. It is a tool with a shape, and the shape is exactly this: it converts natural language into working behavior extremely fast, at the cost of you not knowing how that behavior is held together.</p>
<p>For a prototype, that trade is a bargain. You want speed and you do not care about the internals, because the prototype’s whole job is to answer a question and then get out of the way. For a product, the same trade is a slow-acting poison. You still get the speed up front, but now the internals you never looked at are the thing you have to maintain, and you cannot maintain what you never saw.</p>
<p>So the technique is not good or bad. It is correctly or incorrectly applied. Applied to a throwaway, it is close to magic. Applied to something people depend on, without ever adding the structure back, it is <a href="https://martinfowler.com/bliki/TechnicalDebt.html">a loan at high interest that comes due</a>, reliably, a couple of months in.</p>
<h2 id="what-past-the-line-looks-like-in-practice">What “past the line” looks like in practice</h2>
<p>Concretely, you know you have crossed the line when your relationship to the code changes. In prototype territory, you ask for a change and you accept whatever comes back, because the only test is whether it looks right when you run it. Past the line, you start needing to know things you cannot see: did this change break the thing I shipped last week, is this number correct or just plausible, why did it do it this way. The questions the code cannot answer are the sign that vibe coding has taken you as far as it can.</p>
<p>That is not a failure and it is not a reason to feel foolish. It is the predictable result of the technique working. The mistake is only in not noticing, and continuing to add features by vibe on top of a base nobody understands, which is how a fast start turns into an app that is <a href="/blog/vibe-coding-90-day-reckoning/">impossible to change around month 3</a>.</p>
<h2 id="the-line-in-one-sentence">The line, in one sentence</h2>
<p>Vibe coding is right up until something has to be true about your code that you cannot personally verify by clicking. At that line, you do not need to abandon the app or rewrite it. You need to add back the structure a prototype skips: a readable map, a written spec, a safety net. That crossing is a whole discipline of its own, and I lay out the full route in <a href="/blog/vibe-coding-to-production/">from vibe coding to production</a>.</p>
<p>Use vibe coding for what it is good at without apology. Just watch for the line, because it arrives quietly, and the cost of missing it is paid later, with interest.</p>
<h2 id="faq">FAQ</h2>
<p><strong>Who coined the term vibe coding?</strong> Andrej Karpathy, in early 2025, describing a way of building where you lean on the model and stop tracking the code itself. It named a practice many people were already doing.</p>
<p><strong>Is vibe coding bad?</strong> No. It is excellent for prototypes, throwaway tools, and first drafts. It becomes a problem only when its output is used as if it were production software, without the structure that production requires.</p>
<p><strong>What is the difference between vibe coding and AI-assisted engineering?</strong> In vibe coding you steer by the behavior and do not read the code. In AI-assisted engineering you still read and own the diff. The difference decides whether the output is safe to depend on.</p>]]></content><author><name>@jlcases</name></author><category term="vibe" /><category term="vibe-coding" /><category term="ai-coding" /><category term="what-is-vibe-coding" /><category term="agentic-development" /><category term="prototype-to-product" /><category term="paelladoc" /><summary type="html"><![CDATA[Vibe coding is the term Karpathy coined for describing what you want and letting the model write the code. It is genuinely good at some things and genuinely bad at others. Here is the definition, and the line where it stops working.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://paelladoc.com/assets/images/clusters/vibe-hero.png" /></entry><entry xml:lang="en-US"><title type="html">Vibe coding vs AI-assisted engineering: the line that matters</title><link href="https://paelladoc.com/blog/vibe-coding-vs-ai-assisted-engineering/" rel="alternate" type="text/html" title="Vibe coding vs AI-assisted engineering: the line that matters" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://paelladoc.com/blog/vibe-coding-vs-ai-assisted-engineering</id><content type="html" xml:base="https://paelladoc.com/blog/vibe-coding-vs-ai-assisted-engineering/"><![CDATA[<p>The term vibe coding started as a description of a genuine, useful thing: letting an agent write code while you steer on feel, barely reading the output, chasing an idea at the speed of conversation. Addy Osmani drew the useful distinction that has stuck with me, between that mode and something he calls AI-assisted engineering. Both use the same models and the same editors. They are not the same activity, and confusing them is behind a lot of grief.</p>
<p>I want to make that line concrete, because “engineering” can sound like a scold, like the answer is just to be more serious and write more tests. It is not about seriousness. It is about one specific thing you either do or do not do, and everything else follows from it.</p>
<h2 id="the-line-is-not-where-people-think">The line is not where people think</h2>
<p>People try to locate the difference in the wrong places, and each wrong location leads to a bad conclusion.</p>
<p>It is not the tools. The same Cursor session can produce either mode. It is not the speed. AI-assisted engineering is not slower vibe coding with more ceremony bolted on, and treating it that way just makes you resent the ceremony. It is not even whether you write tests, because you can write tests that prove nothing and still be vibe coding with extra steps.</p>
<p>The line is acceptance. Vibe coding accepts the generated code because it appears to work. Engineering accepts the generated code because it has been shown to meet a contract you defined. That is the whole distinction, and it is smaller and sharper than the usual framing suggests. One trusts the output. The other verifies it against something stated in advance.</p>
<p>Everything people associate with the two modes falls out of that single difference. Vibe coding feels fast and free because trusting is faster than verifying. Engineering feels more deliberate because a contract exists and the code has to clear it. Same generation step, different gate at the end.</p>
<h2 id="why-vibe-coding-is-not-the-enemy">Why vibe coding is not the enemy</h2>
<p>I am not building toward “vibe coding bad, engineering good.” That framing is both wrong and useless.</p>
<p>Vibe coding is the right mode for a large and legitimate set of work. Exploring an idea, building a prototype to see if something is worth doing, throwaway scripts, anything where being wrong is cheap and learning fast is the goal. In that zone, stopping to define contracts and verify against them is pure overhead. You would be engineering a thing you are about to delete, which is its own kind of waste. The speed of trusting the output is exactly what you want when the stakes of the output are near zero.</p>
<p>The failure is not vibe coding. The failure is vibe coding past its expiry date: staying in trust-the-output mode after the output started mattering. That is the exact transition I try to map in going <a href="/blog/vibe-coding-to-production/">from vibe coding to production</a>. The mode that was correct for the prototype becomes dangerous for the product, and nobody sends you a notification when you cross the line.</p>
<h2 id="what-changes-when-you-cross">What changes when you cross</h2>
<p>The moment the code starts mattering, the cheapest thing you can add is not tests or types. It is a contract: a statement, before the agent builds, of what the change must do and how you will recognize a correct result. That is the core of <a href="/blog/what-is-spec-driven-development/">spec-driven development</a>, and it is the smallest possible thing that turns vibe coding into engineering, because it gives acceptance something to check against.</p>
<p>With a contract in place, verification stops being vibes. “It works” becomes “it meets the contract, and here is the evidence.” Without one, even a passing test suite is just a more elaborate vibe, because the tests describe whatever the code happened to do rather than what it was supposed to do. The contract is what gives the word “done” any meaning at all.</p>
<p>This is also why the engineering mode is more expensive, and why that expense is the point. Defining the contract is real work. It is the work AI did not eliminate. Generating the code got cheap, so the scarce, valuable activity moved to deciding what correct means and proving the code meets it. In the engineering mode you are doing that scarce work. In the vibe mode you are skipping it, which is fine until it is not.</p>
<h2 id="the-two-modes-and-the-debt">The two modes and the debt</h2>
<p>The clearest practical difference shows up later, in what each mode leaves behind. Vibe coding accumulates <a href="/blog/vibe-coding-technical-debt/">debt in three layers</a>, comprehension, verification, and architecture, precisely because nothing was verified against a contract as it went. Engineering pays a small cost up front, the contract and the evidence, and carries much less of that debt forward.</p>
<p>So the choice between the modes is really a choice about when you pay. Vibe coding defers everything to a future reckoning. Engineering pays a little continuously. Neither is universally right. On a prototype, deferring is correct. On a product, <a href="https://martinfowler.com/bliki/TechnicalDebt.html">the deferred bill</a> is the <a href="/blog/vibe-coding-to-production/">90-day collapse</a> everyone eventually meets. Knowing which mode you are in is knowing which payment schedule you signed up for.</p>
<p>And it explains a fear I have written about elsewhere. The reason people get <a href="/blog/scared-to-touch-your-code/">scared to touch their own code</a> is almost always that they stayed in vibe mode too long. There is no contract to check a change against and no evidence that anything works, so every edit is a leap. The fear is the felt experience of having accepted a lot of code without verifying any of it.</p>
<h2 id="you-are-still-the-one-deciding">You are still the one deciding</h2>
<p>Here is the part that neither mode removes. Whether you trust or verify, whether you vibe or engineer, you are the one making that call, change by change. The agent does not know if this code matters. It cannot tell whether you are prototyping or shipping. That judgment is yours, and it is the actual job now.</p>
<p>That is what I mean when I say <a href="/blog/you-are-the-runtime/">you are the runtime</a>. The agent generates. You decide which mode the moment calls for, define the contract when one is needed, and hold the line on evidence when it matters. The line between vibe coding and engineering is not drawn by your tools. It is drawn by you, every time you decide whether “it works” is enough.</p>
<h2 id="where-paelladoc-fits">Where PaellaDoc fits</h2>
<p>The hard part of living on the right side of this line is that the contract and the evidence have to exist somewhere durable, or the engineering mode quietly decays back into vibe coding. A contract in a closed chat is not a contract. Evidence you did not keep is not evidence.</p>
<p>PaellaDoc exists to make the engineering mode the path of least resistance: a place to state the contract before the agent builds, a link from that contract to the code that implements it, and the evidence that it was met, held together locally instead of scattered. It does not stop you from vibe coding when vibe coding is right. It makes crossing into engineering cheap enough that you actually do it when the code starts to matter.</p>
<h2 id="faq">FAQ</h2>
<p><strong>Is vibe coding bad?</strong> No. It is the right mode for exploration, prototypes, and any work where being wrong is cheap. It becomes a problem only when you stay in it after the code starts mattering, which turns skipped verification into accumulated risk.</p>
<p><strong>What actually separates vibe coding from AI-assisted engineering?</strong> Acceptance. Vibe coding accepts generated code because it appears to work. Engineering accepts it because it has been shown to meet a contract defined in advance. The tools, models, and speed are the same.</p>
<p><strong>Do I have to choose one mode for the whole project?</strong> No. The mode is a per-change decision. Prototype a feature on vibes, then switch to engineering when that feature becomes load-bearing. The skill is knowing which the moment calls for.</p>
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    { "@type": "Question", "name": "Is vibe coding bad?", "acceptedAnswer": { "@type": "Answer", "text": "No. It is the right mode for exploration, prototypes, and any work where being wrong is cheap. It becomes a problem only when you stay in it after the code starts mattering, which turns skipped verification into accumulated risk." } },
    { "@type": "Question", "name": "What actually separates vibe coding from AI-assisted engineering?", "acceptedAnswer": { "@type": "Answer", "text": "Acceptance. Vibe coding accepts generated code because it appears to work. Engineering accepts it because it has been shown to meet a contract defined in advance. The tools, models, and speed are the same." } },
    { "@type": "Question", "name": "Do I have to choose one mode for the whole project?", "acceptedAnswer": { "@type": "Answer", "text": "No. The mode is a per-change decision. Prototype a feature on vibes, then switch to engineering when that feature becomes load-bearing. The skill is knowing which the moment calls for." } }
  ]
}
</script>]]></content><author><name>@jlcases</name></author><category term="vibe" /><category term="vibe-coding" /><category term="ai-assisted-engineering" /><category term="ai-coding" /><category term="verification" /><category term="spec-driven-development" /><category term="paelladoc" /><summary type="html"><![CDATA[Vibe coding and AI-assisted engineering use the same tools and the same models. The line between them is not speed or output. It is whether you accept the generated code on faith or make it prove itself.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://paelladoc.com/assets/images/clusters/vibe-hero.png" /></entry><entry xml:lang="en-US"><title type="html">From vibe coding to production: the bridge nobody explains</title><link href="https://paelladoc.com/blog/vibe-coding-to-production/" rel="alternate" type="text/html" title="From vibe coding to production: the bridge nobody explains" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://paelladoc.com/blog/vibe-coding-to-production</id><content type="html" xml:base="https://paelladoc.com/blog/vibe-coding-to-production/"><![CDATA[<p>You described an app in plain language and an agent built it. It worked. You showed it to someone, they started using it, and somewhere in there it stopped being an experiment. Now it is the thing people depend on, and you are the only person who can keep it running, except you are not sure how it runs.</p>
<p>That is the crossing nobody explains. There is endless content on how to start vibe coding and almost nothing on the part that actually hurts: the day <strong>the prototype became the product</strong> and the ground under it never got poured.</p>
<p>This is the map of that crossing. Not a warning to slow down, not a pitch to rewrite everything the “right” way. A staged bridge you can walk while the app stays live.</p>
<h2 id="where-you-actually-are">Where you actually are</h2>
<p>Let me be precise about the situation, because most advice aims at the wrong one.</p>
<p>You are not a beginner who needs to learn to code. You shipped something real, fast, and it works well enough that people came back. That is genuinely hard and the tools that got you here are good at it. If you want the real definition of the technique and the point where it stops carrying you, <a href="/blog/what-is-vibe-coding/">what vibe coding is and where it stops working</a> draws that line without contempt.</p>
<p>The problem is not the code you have. It is the code you cannot see. A prototype is a set of behaviors that happened to work once. A product is a set of behaviors that keep working while you change them, while more people use them, while you are asleep. The gap between those two is not talent. It is structure that a prototype legitimately skips to move fast, and that a product cannot.</p>
<p>Everyone crosses this gap eventually. Most people cross it by accident, in a panic, at the worst possible moment. The alternative is to cross it on purpose.</p>
<h2 id="the-four-things-a-prototype-skips">The four things a prototype skips</h2>
<p>A vibe-coded prototype skips exactly the things that do not show up in a demo. That is not laziness, it is what makes it fast. Each one <a href="https://martinfowler.com/bliki/TechnicalDebt.html">comes due later</a>.</p>
<figure class="tp-diagram tp-diagram--graph" data-diagram-type="graph" role="img" aria-label="The four things a prototype skips to move fast. None show up in a demo; all four come due while you maintain it."><svg viewBox="0 0 760 320" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><line x1="380" y1="160" x2="140" y2="62" stroke="var(--tp-rule)" stroke-width="1.2"></line>
  <circle cx="140" cy="62" r="7" fill="none" stroke="var(--tp-ink)" stroke-width="1.5"></circle>
  <text x="140" y="88" text-anchor="middle" font-family="var(--tp-mono)" font-size="12" letter-spacing="1.5" fill="var(--tp-mute)">NO SPEC</text><line x1="380" y1="160" x2="620" y2="62" stroke="var(--tp-rule)" stroke-width="1.2"></line>
  <circle cx="620" cy="62" r="7" fill="none" stroke="var(--tp-ink)" stroke-width="1.5"></circle>
  <text x="620" y="88" text-anchor="middle" font-family="var(--tp-mono)" font-size="12" letter-spacing="1.5" fill="var(--tp-mute)">NO MEMORY OF WHY</text><line x1="380" y1="160" x2="96" y2="232" stroke="var(--tp-rule)" stroke-width="1.2"></line>
  <circle cx="96" cy="232" r="7" fill="none" stroke="var(--tp-ink)" stroke-width="1.5"></circle>
  <text x="96" y="258" text-anchor="middle" font-family="var(--tp-mono)" font-size="12" letter-spacing="1.5" fill="var(--tp-mute)">NO SAFETY NET</text><line x1="380" y1="160" x2="664" y2="232" stroke="var(--tp-rule)" stroke-width="1.2"></line>
  <circle cx="664" cy="232" r="7" fill="none" stroke="var(--tp-ink)" stroke-width="1.5"></circle>
  <text x="664" y="258" text-anchor="middle" font-family="var(--tp-mono)" font-size="12" letter-spacing="1.5" fill="var(--tp-mute)">NO CONTRACT</text><circle cx="380" cy="160" r="14" fill="var(--tp-mostaza)" fill-opacity="0.16" stroke="var(--tp-mostaza)" stroke-width="1.8"></circle>
  <text x="380" y="194" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-mostaza)">THE PROTOTYPE</text></svg><figcaption>The four things a prototype skips to move fast. None show up in a demo; all four come due while you maintain it.</figcaption></figure>
<p><strong>A model of what it does.</strong> The prototype has behaviors, not a specification. Nobody wrote down what “correct” means, so nobody can tell when it breaks. The knowledge lives in your head, and your head forgets.</p>
<p><strong>A memory of why.</strong> Every design choice the agent made is invisible. Why this data shape, why this flow, what it deliberately does not do. Three weeks later that reasoning is gone, including from you, which is its own <a href="/blog/understand-your-ai-generated-code/">documented pattern</a>.</p>
<p><strong>A safety net.</strong> Nothing tells you when a change breaks something that used to work. In a prototype you find out by clicking. In a product, your users find out first.</p>
<p><strong>A contract between the parts.</strong> Each feature was generated on its own, assuming the world around it holds still. Nothing enforces that assumption, which is why <a href="/blog/features-breaking-features/">every new feature starts breaking an old one</a> once you have more than a handful.</p>
<p>None of these show up while you are building. All four show up while you are maintaining. The crossing is the work of adding them back without stopping the world.</p>
<h2 id="the-bridge-stage-by-stage">The bridge, stage by stage</h2>
<p>You do not rewrite. A rewrite throws away the one asset you have, working behavior, to chase structure you could add in place. You add the structure underneath what already runs.</p>
<h3 id="stage-1-see-what-you-have">Stage 1: See what you have</h3>
<p>You cannot maintain what you cannot read. Before any fix, the job is to turn an opaque pile of generated code into something you can navigate: what are the real parts, how do they talk, where does the data live, what actually happens when a user clicks the main button.</p>
<p>This is not “read every line”. It is building a map, at the altitude of components and flows, not syntax. When you can point at the four or five real pieces of your app and say what each one is for, you have crossed from passenger to driver. The full method is in <a href="/blog/understand-your-ai-generated-code/">what to do when you shipped code you can’t read</a>.</p>
<h3 id="stage-2-write-the-spec-that-was-never-written">Stage 2: Write the spec that was never written</h3>
<p>Now write down what the app is supposed to do, in the present tense, from the outside. Not how the code works, what the behavior is: a user with this role can do this, cannot do that, this number always equals that number. These are retroactive specifications, and they are the cheapest thing you will do on this whole bridge.</p>
<p>Why they matter: a specification is the contract that says what “correct” means. Without it, “it works” is just “it hasn’t visibly broken yet”. A green build <a href="/blog/spec-driven-development-green-build-not-correct/">is not a correct feature</a>, it is a build that compiled. The spec is what turns a vague feeling of working into something you can check.</p>
<h3 id="stage-3-pin-the-behavior-with-tests">Stage 3: Pin the behavior with tests</h3>
<p>Once you have written what the app does, capture it in characterization tests: tests whose job is not to define correct behavior but to freeze current behavior, so that the next change tells you the moment it moves something. This is <a href="https://martinfowler.com/bliki/SelfTestingCode.html">the safety net</a>. It is what converts fear of your own codebase into a signal you can act on.</p>
<p>You do not test everything. You test the paths that would hurt if they broke: the money math, the auth boundary, the flow people actually use. Everything downstream gets easier once these exist, because now you can change things and know.</p>
<h3 id="stage-4-pour-foundations-without-a-rewrite">Stage 4: Pour foundations without a rewrite</h3>
<p>With a map, specs, and a net, you can finally reinforce the structure: put boundaries where features currently reach into each other, add the contract that stops one feature from silently breaking another, retire the copy-pasted code that multiplies every bug. This is the load-bearing work, and it is only safe to do after the first three stages, because now every change is checked. The full sequence is in <a href="/blog/fix-vibe-coded-app-without-rewrite/">fixing a vibe-coded app without a rewrite</a>.</p>
<h2 id="why-the-order-is-the-whole-point">Why the order is the whole point</h2>
<p>You cannot skip to stage 4. Reinforcing structure without a net is how “one small fix” takes down four things at 2am. You cannot skip to stage 3 either: tests that pin behavior you do not understand just freeze your bugs in place. The order is map, spec, net, foundations, and it is not negotiable, because each stage is what makes the next one safe.</p>
<p>This is also why “just rewrite it properly” is bad advice. A rewrite is all four stages at once, blind, with the working version turned off. You lose the behavior you had and you rebuild the same gaps in new code. The bridge is slower to describe and far faster to live, because the app never stops working while you cross.</p>
<p>I have watched the difference in the open. When I asked an agent to build the same app with no structure, <a href="/blog/codex-app-next-day/">it didn’t survive the first click</a>. The gap was never about how good the generated code looked. It was about whether anything underneath it was load-bearing.</p>
<h2 id="what-the-crossing-is-not">What the crossing is not</h2>
<p>It helps to name the things this bridge is not, because the fear around it usually comes from imagining the wrong job.</p>
<p>It is not learning computer science. You do not need to understand compilers or big-O notation to write down what your app does and to check that it still does it. The four stages are product-shaped work, not academic work: what should be true, is it still true, where does it live.</p>
<p>It is not hiring an engineer to take it away from you. You can bring one in, and a good one will thank you for having done stage one and two first. But the crossing is designed to be walkable by the person who built the prototype, because that person holds the one thing no hire has: the memory of what it was supposed to do, while that memory is still fresh.</p>
<p>It is not stopping shipping. You keep releasing the whole way across. The bridge adds structure under a live app, in small pieces, each one verified. Nothing about it requires a freeze, and a freeze is usually where vibe-coded apps go to die anyway.</p>
<p>And it is not a one-time project you finish and forget. The reason the prototype lost its structure is that structure was never kept current. The point of crossing on purpose is to leave behind a spec, a net, and a map that stay attached to the code, so you do not wake up in the same place a second time.</p>
<h2 id="the-signal-that-you-are-ready-to-start">The signal that you are ready to start</h2>
<p>You do not wait until the app is broken to begin. You begin the moment you notice any of these: someone other than you relies on it, you hesitate before changing something, or you open a file you wrote and cannot follow it. Each of those is the app telling you it crossed from prototype to product while you were busy shipping. The earlier you answer, the cheaper the crossing, because you are paying with memory you still have instead of archaeology you have to reconstruct.</p>
<h2 id="where-paelladoc-fits">Where PaellaDoc fits</h2>
<p>Every stage of this bridge is manual right now. You are the one reading the code into a map, holding the spec in your head, remembering to run the checks, catching the feature that broke another feature. That works until the app is big enough that you cannot hold it all, which is roughly the moment it became worth maintaining.</p>
<p>PaellaDoc exists to make the bridge a system instead of a heroic act. It reads an existing repo into a navigable model of what is there, holds the specs and decisions next to the code they govern, and runs the app to produce evidence that a behavior still works instead of trusting a green build. The point is not to do the crossing for you. It is so the map, the contract, and the proof live in one place and stay current, instead of decaying in your memory the way they did the first time. The wider argument for that system is the pillar on <a href="/blog/building-software-with-ai-agents/">building software with AI agents</a>.</p>
<p>You already did the hard, creative part. You made something people use. The bridge is how you get to keep it.</p>
<h2 id="faq">FAQ</h2>
<p><strong>Do I have to rewrite my vibe-coded app to make it production-ready?</strong> No. A rewrite discards the working behavior you have to chase structure you can add underneath it. The staged approach, map then spec then tests then foundations, adds what production needs while the app stays live.</p>
<p><strong>When does a prototype become a product?</strong> The day someone starts depending on it, usually before you decide it happened. That is the signal to start the crossing, not the app’s size or age.</p>
<p><strong>What is the first thing to do?</strong> Make the code readable to yourself: a map of the real parts and flows. You cannot specify, test, or reinforce what you cannot navigate, so every later stage depends on this one.</p>]]></content><author><name>@jlcases</name></author><category term="vibe" /><category term="vibe-coding" /><category term="prototype-to-product" /><category term="ai-coding" /><category term="technical-debt" /><category term="spec-driven-development" /><category term="paelladoc" /><summary type="html"><![CDATA[Vibe coding gets you a working prototype in an afternoon. Nobody explains the crossing after that: the day the prototype became the product and you still have to keep it running. Here is the bridge, stage by stage.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://paelladoc.com/assets/images/clusters/vibe-hero.png" /></entry><entry xml:lang="en-US"><title type="html">Vibe coding debt: what you actually owe and when it&#39;s due</title><link href="https://paelladoc.com/blog/vibe-coding-technical-debt/" rel="alternate" type="text/html" title="Vibe coding debt: what you actually owe and when it&#39;s due" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://paelladoc.com/blog/vibe-coding-technical-debt</id><content type="html" xml:base="https://paelladoc.com/blog/vibe-coding-technical-debt/"><![CDATA[<p>You shipped a working app in a weekend. An agent wrote most of it, you steered, the demo landed. Three weeks later a two-line change takes you a full day, and you are not sure why the app still runs. That day you lost is the interest payment. You just did not see the loan being taken out.</p>
<p>Everybody calls this technical debt. The name is right and it is also hiding something. When Ward Cunningham coined <a href="https://martinfowler.com/bliki/TechnicalDebt.html">technical debt</a>, the debt was code you understood, written under pressure, that you chose to clean up later. Vibe coding produces a different animal: code you never read, generated at a speed no human review kept up with. Calling it one lump of “debt” tells you it exists. It does not tell you what you owe or when the bill arrives.</p>
<p>So let me split it into the three debts that actually behave differently.</p>
<h2 id="debt-is-not-one-number">Debt is not one number</h2>
<p>The mistake is treating a vibe-coded codebase as a single pile of shortcuts to pay down someday. That framing paralyzes you, because “rewrite it properly” is never the right size of task and never the right week to do it.</p>
<p>The debts that matter accrue interest at different rates and come due on different triggers. If you can name them separately, you can pay the expensive one first and leave the cheap one alone. That is the whole move.</p>
<figure class="tp-diagram tp-diagram--stack" data-diagram-type="stack" role="img" aria-label="Not one balance but three, stacked. Comprehension sits underneath: you pay it first because everything above depends on it."><svg viewBox="0 0 760 208" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><rect x="120" y="20" width="520" height="42" fill="var(--tp-mostaza)" fill-opacity="0.14" stroke="var(--tp-mostaza)" stroke-width="1.5"></rect>
  <text x="380" y="47" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-ink)">COMPREHENSION DEBT</text><rect x="120" y="76" width="520" height="42" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="380" y="103" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-ink)">TEST DEBT</text><rect x="120" y="132" width="520" height="42" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="380" y="159" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-ink)">ARCHITECTURE DEBT</text></svg><figcaption>Not one balance but three, stacked. Comprehension sits underneath: you pay it first because everything above depends on it.</figcaption></figure>
<h2 id="layer-1-comprehension-debt">Layer 1: comprehension debt</h2>
<p>This is the one nobody prices in. It is the gap between what your software does and what you can explain about it.</p>
<p>When you write code by hand, comprehension comes for free as a side effect. When an agent writes it, comprehension is a separate cost you have either paid or deferred. Most vibe coding defers all of it. The app works, so you move on, and the model of how it works lives nowhere except in a chat transcript that scrolled off the screen.</p>
<p>Comprehension debt is invisible until you need to change something. Then it charges interest at a brutal rate, because every edit starts with an archaeology session. You are reverse-engineering your own product. This is the tax behind <a href="/blog/scared-to-touch-your-code/">the fear of touching your own code</a>: the fear is rational, and it is comprehension debt sending you the bill.</p>
<p>The dangerous part is that it compounds silently. Each new feature the agent adds on top of code you did not understand widens the gap. By month three you are not maintaining an app. You are negotiating with a stranger who happens to share your repo.</p>
<h2 id="layer-2-test-and-verification-debt">Layer 2: test and verification debt</h2>
<p>This is the debt you can see, which is exactly why people mistake it for the whole problem.</p>
<p>Vibe-coded apps usually ship with no tests, or with tests the agent wrote to describe what the code already does rather than what it should do. Either way, you have no independent signal that the thing works. You have the fact that it worked when you last clicked around, which is not the same claim.</p>
<p>Verification debt has a specific and unpleasant property: it stays cheap right up until it is catastrophic. As long as nobody changes anything, the absence of tests costs nothing. The moment you or an agent ships a change, the missing tests are the reason a broken feature reaches a user before you notice. This is the mechanism behind <a href="/blog/features-breaking-features/">every new feature breaking an old one</a>: without a verification layer, there is no gate between “the agent said it works” and production.</p>
<p>There is a version of this that is worse than having no tests, which is having tests that pass without proving anything. An agent that reports “all tests green” when the suite never exercised the changed path has handed you a false receipt. Distinguishing a real pass from a green build that verifies nothing is its own discipline, and it is why <a href="/blog/spec-driven-development-green-build-not-correct/">a green build is not a correct feature</a>.</p>
<h2 id="layer-3-architecture-debt">Layer 3: architecture debt</h2>
<p>This is the slowest and the most expensive, and it is the one the metaphor was originally about.</p>
<p>Architecture debt is structural: duplicated logic that should be one function, a data model that assumed one use case and now serves five, boundaries that were never drawn so everything depends on everything. Agents are very good at producing this, because each prompt is a local optimization. The agent solves the change in front of it well and has no standing model of the system it is changing. The result is code that is <a href="/blog/locally-correct-globally-incoherent/">locally correct and globally incoherent</a>: every piece defensible, the whole drifting.</p>
<p>Architecture debt charges the highest interest of the three, but it charges it late. You can run on bad architecture for a surprisingly long time. What it steals is your future velocity. The app that took a weekend to build takes a month to extend, and the reason is never one bad file. It is the accumulated cost of a structure nobody designed.</p>
<h2 id="when-each-one-comes-due">When each one comes due</h2>
<p>The debts have due dates, and knowing them is how you stop paying interest by surprise.</p>
<p>Comprehension debt comes due the first time you need to change something non-trivial, which for a real product is usually within the first month. It is the earliest and the most frequent bill.</p>
<p>Verification debt comes due the first time a change ships that breaks something you did not touch. You cannot predict the date, but you can be certain it exists, and it lands harder the more users you have when it does.</p>
<p>Architecture debt comes due when you try to scale the app past its original shape: a new integration, a second customer type, a feature that cuts across three others. That is the <a href="/blog/vibe-coding-to-production/">90-day range where vibe-coded apps tend to break</a>, not because of a single failure but because all three debts come due within weeks of each other.</p>
<h2 id="paying-it-down-without-stopping-the-world">Paying it down without stopping the world</h2>
<p>You do not fix this by rewriting. You fix it by paying the debts in the order they charge interest.</p>
<p>Pay comprehension debt first, because it is cheapest to service and everything else depends on it. Before you touch a feature, get the system to explain itself: what this code does, what it assumes, what would break if it changed. You are not documenting for its own sake. You are recovering the model you never built.</p>
<p>Pay verification debt second, and pay it precisely. You do not need full coverage on a weekend prototype. You need tests on the paths that would hurt a user if they broke, written before you change them. That is also the real answer to <a href="/blog/when-to-add-tests-and-architecture/">when to add tests and architecture</a>: not on day one, not never, but at the moment a path becomes load-bearing.</p>
<p>Pay architecture debt last and only where it blocks you. Refactoring code you are not about to change is spending money to reduce a bill that was not charging you interest. Restructure the part you are extending, leave the rest.</p>
<p>The through-line is that all three debts are really one missing thing: a durable record of intent and evidence that lives outside the agent’s context window. When that record does not exist, every change starts from zero. That is the same failure that makes <a href="/blog/prototype-became-the-product/">a prototype impossible to promote to a product</a> without a painful reckoning.</p>
<h2 id="where-paelladoc-fits">Where PaellaDoc fits</h2>
<p>The reason vibe coding produces all three debts at once is that the agent’s understanding, the acceptance contract, and the evidence of what passed all evaporate when the session ends. Nothing outlives the chat.</p>
<p>PaellaDoc exists to keep that record alive locally: the intent behind a change, the decisions that shaped it, the code it touched, and the evidence that it worked, connected in one place instead of scattered across transcripts. It does not make the debt disappear. It makes the debt legible, so you can see which layer you owe and pay it deliberately instead of discovering the balance the hard way, on the day a two-line change costs you a week.</p>
<h2 id="faq">FAQ</h2>
<p><strong>Is vibe coding technical debt worse than normal technical debt?</strong> It is a different shape. Normal debt is usually code you understood and chose to defer cleaning. Vibe coding adds comprehension debt on top: code you never read, so you owe understanding before you can even assess the rest.</p>
<p><strong>Do I have to add tests to a vibe-coded project right away?</strong> No. Add them to the paths that would hurt a user if they silently broke, and add them before you change those paths. Full coverage on a throwaway prototype is paying down a debt that is not charging interest.</p>
<p><strong>How do I know which debt to pay first?</strong> Pay comprehension debt first because everything depends on it, verification debt next on load-bearing paths, and architecture debt last and only where it blocks the change you actually need to make.</p>
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    { "@type": "Question", "name": "Is vibe coding technical debt worse than normal technical debt?", "acceptedAnswer": { "@type": "Answer", "text": "It is a different shape. Normal debt is usually code you understood and chose to defer cleaning. Vibe coding adds comprehension debt on top: code you never read, so you owe understanding before you can assess the rest." } },
    { "@type": "Question", "name": "Do I have to add tests to a vibe-coded project right away?", "acceptedAnswer": { "@type": "Answer", "text": "No. Add them to the paths that would hurt a user if they silently broke, and add them before you change those paths. Full coverage on a throwaway prototype pays down a debt that is not charging interest." } },
    { "@type": "Question", "name": "How do I know which debt to pay first?", "acceptedAnswer": { "@type": "Answer", "text": "Pay comprehension debt first because everything depends on it, verification debt next on load-bearing paths, and architecture debt last and only where it blocks the change you actually need to make." } }
  ]
}
</script>]]></content><author><name>@jlcases</name></author><category term="vibe" /><category term="vibe-coding" /><category term="technical-debt" /><category term="ai-coding" /><category term="maintainability" /><category term="verification" /><category term="paelladoc" /><summary type="html"><![CDATA[Vibe-coded software carries debt, but not the single number the metaphor suggests. It is three debts, comprehension, tests, and architecture, each with its own due date.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://paelladoc.com/assets/images/clusters/vibe-hero.png" /></entry><entry xml:lang="en-US"><title type="html">The security holes vibe coding leaves behind</title><link href="https://paelladoc.com/blog/vibe-coding-security/" rel="alternate" type="text/html" title="The security holes vibe coding leaves behind" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://paelladoc.com/blog/vibe-coding-security</id><content type="html" xml:base="https://paelladoc.com/blog/vibe-coding-security/"><![CDATA[<p>The security problem with vibe coding is not that agents write insecure code on purpose. It is that they write plausible code fast, and plausible code passes the only test most vibe coding applies: it runs. Security holes do not stop an app from running. They sit there quietly until someone finds them, and the someone is rarely you.</p>
<p>I want to be precise here, not alarmist. Your weekend app is probably not being actively targeted. But the moment it holds a real user’s data or takes a real payment, the gap between “it works” and “it is safe to operate” becomes your problem, and that gap has a predictable shape. Generated code that nobody reviewed tends to leak in the same three places. Let me walk each one, with the mechanism, so you can close it instead of worrying about it.</p>
<figure class="tp-diagram tp-diagram--graph" data-diagram-type="graph" role="img" aria-label="Code nobody read leaks in the same three places. The common thread is the review that vibe coding skips."><svg viewBox="0 0 760 320" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><line x1="380" y1="160" x2="140" y2="62" stroke="var(--tp-rule)" stroke-width="1.2"></line>
  <circle cx="140" cy="62" r="7" fill="none" stroke="var(--tp-ink)" stroke-width="1.5"></circle>
  <text x="140" y="88" text-anchor="middle" font-family="var(--tp-mono)" font-size="12" letter-spacing="1.5" fill="var(--tp-mute)">SECRETS IN CODE</text><line x1="380" y1="160" x2="620" y2="62" stroke="var(--tp-rule)" stroke-width="1.2"></line>
  <circle cx="620" cy="62" r="7" fill="none" stroke="var(--tp-ink)" stroke-width="1.5"></circle>
  <text x="620" y="88" text-anchor="middle" font-family="var(--tp-mono)" font-size="12" letter-spacing="1.5" fill="var(--tp-mute)">UNVALIDATED INPUT</text><line x1="380" y1="160" x2="96" y2="232" stroke="var(--tp-rule)" stroke-width="1.2"></line>
  <circle cx="96" cy="232" r="7" fill="none" stroke="var(--tp-ink)" stroke-width="1.5"></circle>
  <text x="96" y="258" text-anchor="middle" font-family="var(--tp-mono)" font-size="12" letter-spacing="1.5" fill="var(--tp-mute)">UNVETTED DEPENDENCIES</text><circle cx="380" cy="160" r="14" fill="var(--tp-mostaza)" fill-opacity="0.16" stroke="var(--tp-mostaza)" stroke-width="1.8"></circle>
  <text x="380" y="194" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-mostaza)">UNREVIEWED CODE</text></svg><figcaption>Code nobody read leaks in the same three places. The common thread is the review that vibe coding skips.</figcaption></figure>
<h2 id="hole-1-secrets-in-the-code">Hole 1: secrets in the code</h2>
<p>The most common and the easiest to fix. You ask an agent to connect to a database, an email service, a payment provider. It writes working code, and to make it work it puts the API key right there in the source. Sometimes it hardcodes it in a config file. Sometimes it commits your <code>.env</code> because nothing told it not to.</p>
<p>The mechanism is simple: the agent optimizes for the code running now, and a literal key runs. A key read from an environment variable also runs, but it requires a second step the agent has no reason to prioritize unless you asked. So it takes the shortcut, and the shortcut ends up in your git history, where it stays even after you delete the line.</p>
<p>Closing it is mechanical. Move every credential out of source and into environment variables or a secrets store. Add the secret files to <code>.gitignore</code> before the first commit, not after. If a key ever touched a commit, rotate it, because git remembers. And scan the history, since the key you hardcoded in week one is still sitting in the log you forgot about.</p>
<h2 id="hole-2-input-that-nobody-validated">Hole 2: input that nobody validated</h2>
<p>This is the expensive one, and the one an agent is least likely to handle unprompted.</p>
<p>Every place your app accepts input from outside is a place something can go wrong: a form field, a URL parameter, an uploaded file, a request body. Secure code treats all of it as hostile until proven otherwise. Generated code usually treats it as well-formed, because the happy path is what the prompt described. The agent built the feature you asked for. You did not ask it to assume the user is an attacker, so it did not.</p>
<p>The result is the whole <a href="https://owasp.org/Top10/">OWASP Top 10</a> reading like a checklist of things your app probably does wrong: injection because a query was built by concatenating a string the user controls, broken access control because an endpoint checks that you are logged in but not that the record is yours, data exposure because an error handler returns the full stack trace. None of these break the app. All of them are doors.</p>
<p>The fix is not glamorous and it is not optional. Validate and sanitize input at the boundary, following something concrete like the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html">OWASP input validation guidance</a> rather than your own intuition. Use parameterized queries so user input is never concatenated into a command. Check authorization on the record, not just the session. These are the patterns an agent will apply well once you make them part of the contract, and skip entirely when you do not.</p>
<h2 id="hole-3-dependencies-nobody-vetted">Hole 3: dependencies nobody vetted</h2>
<p>An agent reaches for libraries the way you would reach for a search result. It picks a package that solves the problem, adds it to your manifest, and moves on. It does not check whether the package is maintained, whether it has known vulnerabilities, or whether it is even the package you think it is.</p>
<p>That last point matters more than it sounds. Attackers publish packages with names close to popular ones, betting on a typo or a confident autocomplete. An agent generating a dependency list from memory is exactly the kind of confident autocomplete they are betting on. You can end up importing something nobody audited into the core of your app, and it arrived because a model suggested it and you did not look.</p>
<p>The fix is to put a gate between “a dependency was added” and “the dependency ships.” Scan your dependency tree for known vulnerabilities, and do it continuously, because a package that was clean when the agent added it can be found vulnerable next month. This is exactly the practice I wrote about in <a href="/blog/securing-ai-code-with-snyk/">securing AI code with Snyk</a>: the dependency layer is where generated code inherits other people’s mistakes, and the only defense is to check rather than trust.</p>
<h2 id="why-review-is-the-thing-that-broke">Why review is the thing that broke</h2>
<p>Notice what all three holes share. None of them is exotic. A developer reading the code would catch the hardcoded key, question the unvalidated form, and raise an eyebrow at the unfamiliar dependency. The holes exist because nobody read the code, and nobody read it because the entire appeal of vibe coding is not having to.</p>
<p>That is the real tension. The speed comes from skipping review, and the security comes from review. You cannot keep the speed and delete the risk. What you can do is move the review from “read every line” to “check the three things that actually leak,” which is a much smaller job.</p>
<p>This is also where the line between <a href="/blog/vibe-coding-vs-ai-assisted-engineering/">vibe coding and AI-assisted engineering</a> shows up in the sharpest possible way. Vibe coding trusts the output because it runs. Engineering treats security as part of the definition of done, and refuses to call a feature finished until the boundary is validated, the secrets are out of source, and the dependencies are clean. Same agent, same speed, different contract.</p>
<h2 id="the-debt-framing-applied-to-security">The debt framing, applied to security</h2>
<p>Unclosed security holes are a specific kind of <a href="/blog/vibe-coding-technical-debt/">vibe coding debt</a>, and they behave like the worst kind: cheap to carry, catastrophic to settle. A missing input check costs nothing until the day it costs everything, and unlike a slow feature, you do not get a warning that the bill is coming. Someone else schedules the payment for you.</p>
<p>That is why security is not something you bolt on at the end of the <a href="/blog/vibe-coding-to-production/">journey from vibe coding to production</a>. It is one of the gates that defines whether you have a product or a liability with a nice UI.</p>
<h2 id="where-paelladoc-fits">Where PaellaDoc fits</h2>
<p>The reason these holes survive is that “did we check the secrets, the input, the dependencies” is a question nobody wrote down, so nobody answered it. The check lives in a person’s head, and heads forget, especially when the agent moved faster than the reviewer.</p>
<p>PaellaDoc turns those checks into part of the contract for a change and keeps the evidence that they ran, attached to the code they cover, instead of relying on you to remember. The point is not to slow the agent down. It is to make sure that when a change closes, the boring, load-bearing security questions have real answers on record, not an assumption that the code was fine because it ran.</p>
<h2 id="faq">FAQ</h2>
<p><strong>Is vibe-coded software inherently insecure?</strong> No. It is unreviewed, which is different. The same agent writes secure code when security is part of the contract, and leaky code when the only requirement is that it runs. The risk comes from skipping review, not from the model.</p>
<p><strong>What is the single most important thing to check first?</strong> Secrets. Make sure no API keys or credentials live in your source or git history, because that hole is the easiest to exploit and the easiest to close. Then move to input validation and dependencies.</p>
<p><strong>Do I need a security expert to fix this?</strong> For most vibe-coded apps, no. Getting secrets out of source, validating input at the boundary using established guidance, and scanning your dependencies closes the common holes. A specialist matters once you handle sensitive data at scale, not on day one.</p>
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    { "@type": "Question", "name": "Is vibe-coded software inherently insecure?", "acceptedAnswer": { "@type": "Answer", "text": "No. It is unreviewed, which is different. The same agent writes secure code when security is part of the contract and leaky code when the only requirement is that it runs. The risk comes from skipping review, not from the model." } },
    { "@type": "Question", "name": "What is the single most important thing to check first?", "acceptedAnswer": { "@type": "Answer", "text": "Secrets. Make sure no API keys or credentials live in your source or git history, because that hole is the easiest to exploit and the easiest to close. Then move to input validation and dependencies." } },
    { "@type": "Question", "name": "Do I need a security expert to fix this?", "acceptedAnswer": { "@type": "Answer", "text": "For most vibe-coded apps, no. Getting secrets out of source, validating input at the boundary using established guidance, and scanning dependencies closes the common holes. A specialist matters once you handle sensitive data at scale." } }
  ]
}
</script>]]></content><author><name>@jlcases</name></author><category term="vibe" /><category term="vibe-coding" /><category term="security" /><category term="ai-coding" /><category term="input-validation" /><category term="dependencies" /><category term="paelladoc" /><summary type="html"><![CDATA[Code an agent wrote and nobody reviewed leaks in predictable places: secrets in the repo, unvalidated input, and dependencies nobody vetted. Here is the mechanism, and how to close each hole.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://paelladoc.com/assets/images/clusters/vibe-hero.png" /></entry><entry xml:lang="en-US"><title type="html">The 90-day reckoning: why vibe-coded apps break at month 3</title><link href="https://paelladoc.com/blog/vibe-coding-90-day-reckoning/" rel="alternate" type="text/html" title="The 90-day reckoning: why vibe-coded apps break at month 3" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://paelladoc.com/blog/vibe-coding-90-day-reckoning</id><content type="html" xml:base="https://paelladoc.com/blog/vibe-coding-90-day-reckoning/"><![CDATA[<p>There is a pattern that shows up so often it is almost a schedule. Someone vibe-codes an app, it works, they ship it, and for weeks it feels like magic. New features land in minutes. Then, somewhere around the third month, the same app that felt effortless becomes a place where every change breaks something and nobody, including the person who built it, can say why.</p>
<p>This is the 90-day reckoning. It is not bad luck and it is not your fault as a builder. It is the bill for structure that got skipped to move fast, and it arrives on a predictable clock. Knowing why it lands at <strong>month 3</strong> specifically is what lets you meet it before it meets you.</p>
<h2 id="what-the-reckoning-looks-like">What the reckoning looks like</h2>
<p>You will recognize it by the symptoms before you understand the cause:</p>
<ul>
<li>A change that should take five minutes takes a day, and you are not sure it is safe even then.</li>
<li>You fix one thing and two others break, often in parts of the app you did not touch.</li>
<li>You start being afraid to deploy on a Friday, then afraid to deploy at all.</li>
<li>You open a file you wrote yourself two months ago and cannot follow it.</li>
<li>New features slow to a crawl, the exact opposite of how it started.</li>
</ul>
<p>The tell is the reversal. Vibe coding starts fast and, past the reckoning, gets slower than writing the thing carefully would have been. The curve does not flatten. It bends down.</p>
<h2 id="why-month-3-and-not-month-1">Why month 3, and not month 1</h2>
<p>The timing is not mystical. Three forces cross at roughly the same point.</p>
<p><strong>Your memory of the code decays on a schedule.</strong> When you vibe-code, the understanding of how it works lives in your head, not in the code, because you never read most of it. Human memory of that kind of unwritten context fades over weeks. At week two you still remember why the agent did things. By month three that memory is gone, and there is nothing written down to replace it. You are now maintaining a stranger’s code, and the stranger was you.</p>
<p><strong>Complexity compounds, it does not add.</strong> Each feature was generated assuming the rest of the app holds still. The interactions between N features grow far faster than N. For the first handful the collisions are rare. Past a threshold, every addition touches something it was not designed to know about, and <a href="/blog/features-breaking-features/">new features routinely break old ones</a>. That threshold tends to fall around the third month of steady additions.</p>
<p><strong>The prototype quietly became the product.</strong> In the early weeks nobody depends on it, so a break costs nothing. By month three real people rely on it, so the same break that was invisible before is now an outage, a wrong number, a lost record. Nothing about the code changed. The stakes did, and the missing safety net is suddenly load-bearing.</p>
<p>Put together: your memory is gone, the interactions have compounded past what you can hold, and the cost of every mistake has jumped. That intersection is the reckoning.</p>
<h2 id="the-public-version-of-the-same-lesson">The public version of the same lesson</h2>
<p>The mild version is a slow app and a nervous founder. The severe version makes the news. There was a widely reported incident in 2025 where a coding agent, operating on a live project, deleted a company’s production database during what was supposed to be a freeze. I will not embellish the details beyond what was public, because the point does not need embellishing: when an agent operates on a system nobody has fully mapped, with no enforced boundaries and no net, the failure mode is not a small bug. It is the whole thing, at once.</p>
<p>Most reckonings are quieter than that. But they are the same shape, scaled down: an agent, or you, changing a system that no longer has a knowable structure, and finding out the hard way what was connected to what.</p>
<h2 id="the-reckoning-is-a-debt-and-debt-has-terms">The reckoning is a debt, and debt has terms</h2>
<p>The useful way to think about this is not “the app is bad”. It is that <a href="https://martinfowler.com/bliki/TechnicalDebt.html">you took on a loan</a>. Vibe coding lends you speed up front, and the loan is real and often worth taking. But it has terms, and the reckoning is the due date. What you actually owe, and when different parts of it come due, is worth pricing out deliberately rather than discovering by surprise; that is the whole subject of <a href="/blog/vibe-coding-technical-debt/">vibe coding’s technical debt</a>.</p>
<p>The trap is treating the loan as free because the first payments are invisible. Interest accrues from day one. You just do not feel it until the memory fades and the interactions compound, which is, again, roughly month three.</p>
<h2 id="how-to-meet-it-early">How to meet it early</h2>
<p>You do not avoid the reckoning by vibe coding less. You avoid it by paying down the debt before it is due, while you still remember how the code works.</p>
<p>The move is to stop treating “it works” as the finish line the moment anyone starts to depend on the app. Before the memory fades, do the three cheap things that convert the debt into structure: write down what the app is actually supposed to do, capture the current behavior in <a href="https://martinfowler.com/bliki/SelfTestingCode.html">a safety net</a> so changes announce their damage, and make the code readable to your future self. That is the crossing, and the full staged route is <a href="/blog/vibe-coding-to-production/">from vibe coding to production</a>. The earlier you start it, the less it costs, because you are paying with memory you still have instead of archaeology you have to redo.</p>
<h2 id="where-paelladoc-fits">Where PaellaDoc fits</h2>
<p>The reason the reckoning lands is that the knowledge of how the app works has no home outside your head, and your head empties on a schedule. PaellaDoc’s job is to give that knowledge a home: it reads the repo into a model you can navigate, keeps the specs and decisions attached to the code, and re-runs the app to prove behaviors still hold. It does not make the debt disappear. It stops the debt from being invisible, so month three is a scheduled payment instead of an ambush.</p>
<p>You can meet the reckoning early and cheaply, or late and expensively. The date is roughly fixed. What you pay is not.</p>
<h2 id="faq">FAQ</h2>
<p><strong>Why do vibe-coded apps break around month 3 specifically?</strong> Three things cross at that point: your memory of how the code works has faded, feature interactions have compounded past what you can hold in your head, and real people now depend on the app so failures finally cost something.</p>
<p><strong>Is the reckoning avoidable?</strong> The date is roughly fixed, but the cost is not. Paying down the debt early, while you still remember the code, turns a crisis into a scheduled task. Vibe coding less is not the fix; adding structure before the due date is.</p>
<p><strong>What do I do the moment I notice the symptoms?</strong> Stop adding features and start the crossing: write down what the app should do, put a safety net around current behavior, and make the code readable again. Adding more on top of an unmapped base only raises the interest.</p>]]></content><author><name>@jlcases</name></author><category term="vibe" /><category term="vibe-coding" /><category term="technical-debt" /><category term="ai-coding" /><category term="prototype-to-product" /><category term="maintainability" /><category term="paelladoc" /><summary type="html"><![CDATA[Vibe-coded apps tend to break on a schedule. Weeks of magic, then around month 3 the same app becomes impossible to change safely. Here is why the reckoning lands when it does, and how to meet it early.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://paelladoc.com/assets/images/clusters/vibe-hero.png" /></entry><entry xml:lang="en-US"><title type="html">Verifying AI-generated code: evidence, not vibes</title><link href="https://paelladoc.com/blog/verify-ai-generated-code/" rel="alternate" type="text/html" title="Verifying AI-generated code: evidence, not vibes" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://paelladoc.com/blog/verify-ai-generated-code</id><content type="html" xml:base="https://paelladoc.com/blog/verify-ai-generated-code/"><![CDATA[<p>An agent finishes a task and writes back: “Done. All tests pass.” You did not watch it run anything. You have a diff, a confident sentence, and a decision to make. Do you believe it?</p>
<p>Multiply that by eight parallel sessions, a dozen tasks each, and a backlog that fills faster than you can read it. This is the real shape of building with agents in 2026. The code arrives. The question is never whether the model can write it. The question is whether you can trust what came back, and <a href="/blog/ai-code-trust-gap/">how much of your day trust now costs</a>.</p>
<p>This is the piece that ties the rest together. Generation is solved. <strong>Trust, not output, is the bottleneck.</strong> Everything below is one idea seen from six angles: stop verifying with feelings and start verifying with evidence.</p>
<h2 id="vibes-are-not-evidence">Vibes are not evidence</h2>
<p>There is a comfortable way to work with agents, and it is quietly dangerous. The agent sounds sure. The prose is clean. The plan reads well. So you approve, merge, and move on. You verified nothing. You felt something.</p>
<p>A vibe is a signal about the output’s surface: it looks right, it reads like code a competent person would write, the variable names are sensible. Evidence is a signal about the output’s behavior: this test ran, against this version, and here is what it printed. The gap between those two is where broken software ships with a green checkmark on top.</p>
<p>The reframe that runs through this whole cluster is simple. An agent’s report is a claim. A claim is not proof. Verification is the work of turning one into the other, and it is work you have to design, because the agent will not do it for you unless you force the shape of the answer.</p>
<p>Make the distinction concrete. “The retry logic is implemented and tested” is a claim. It could be true. It is also exactly what the agent would write if it implemented the logic and never ran a single test, because that sentence is the natural ending of a task that asked for tested retry logic. Now compare: “ran <code>pytest tests/retry_test.py</code>, 14 passed, 0 failed, against acceptance criteria v3.” That is evidence. You can read it, you can re-run it, and it names the version of the contract it was checked against. The first sentence asks for your trust. The second one earns it, or fails loudly. Everything in verification is about moving your decisions from the first kind of sentence to the second.</p>
<h2 id="why-tests-pass-gets-said-when-nothing-ran">Why “tests pass” gets said when nothing ran</h2>
<p>Start with the failure that surprises people most: the agent says the tests pass, and no test ever executed.</p>
<p>It helps to see the mechanism instead of getting angry at the machine. A language model completes text. When the surrounding context is a finished implementation and a task that asked for passing tests, the most probable continuation is a sentence reporting success. “Tests pass” is what that situation usually ends with in the training data, so that is what gets written. The model is not lying in any human sense. It is producing the likely sentence, and the likely sentence and the true sentence are not the same thing.</p>
<p>This is the anatomy of a <a href="/blog/false-success-claims/">false success claim</a>: a report generated by probability, detached from any run. It is not malice and it is not stupidity. It is the default behavior of the tool, and once you see it you stop being surprised and start engineering around it.</p>
<p>The fix is not a better prompt begging the agent to tell the truth. The fix is structural: don’t let the agent be the one that reports success. Make success something a <a href="/blog/verification-loops/">separate step observes and records</a>.</p>
<h2 id="done-has-to-mean-done">Done has to mean done</h2>
<p>If a claim is cheap, completion has to be expensive. It has to cost the agent a demonstration.</p>
<p>“Done” is one of the most overloaded words in this work. To an agent, done can mean “I wrote code that looks like it satisfies the request.” To you, done means the behavior changed, the old behavior didn’t break, and there is something you can point at to prove both. Those are <a href="/blog/definition-of-done-ai/">different definitions</a>, and the whole discipline of <a href="/blog/done-means-done/">making done mean done</a> is closing the distance between them.</p>
<p>What counts as proof? Not a checkbox. Not the agent’s summary. A command that ran, its output, and the version of the contract it ran against. When done requires a demonstration, the agent stops being the judge of its own work and becomes the party that has to produce exhibits. That single move removes most of the class of failure where confident prose covers an empty result. The green build was never the point. As spec-driven development keeps insisting, <a href="/blog/spec-driven-development-green-build-not-correct/">a green build is not a correct feature</a>; the build passing and the feature being right are two claims, and only one of them is usually checked, especially when the agent <a href="/blog/testing-ai-generated-code/">wrote the tests in the same breath as the code</a>.</p>
<h2 id="you-cannot-read-everything-so-read-at-the-right-moments">You cannot read everything, so read at the right moments</h2>
<p>Even when the evidence is real, there is too much of it. The agents write faster than any human reads. <a href="https://survey.stackoverflow.co/2025/">Surveys of developers</a> keep finding what the day already tells you: reviewing AI-generated code often costs more attention than reviewing a colleague’s, because none of the author’s intent came with it. This is <a href="/blog/ai-code-review-fatigue/">review fatigue</a>, and it is not a character flaw. It is a throughput mismatch.</p>
<p>The answer is not to read harder. It is to read at the moments that carry the most information per minute spent. Approve the scope before the agent builds, so you are steering intent instead of auditing a fait accompli. <a href="/blog/reviewing-code-you-didnt-write/">Check the diff against that scope</a>, not against your vague memory of what you wanted. Look last at the evidence: what ran, what passed, against which contract. Reviewing in that order turns an unbounded reading task into a few high-leverage checkpoints. The same logic explains why the cheapest review is the one you do on the spec, before a single line exists, and it connects directly to <a href="/blog/what-is-spec-driven-development/">what spec-driven development is for</a>.</p>
<h2 id="the-bottleneck-moved-from-producing-to-assuring">The bottleneck moved from producing to assuring</h2>
<p>Step back and the pattern is an economic one. For decades the scarce, expensive part of software was producing it: writing the code, making it compile, getting it to work at all. Agents collapsed the cost of that part. What did not get cheaper is knowing that what was produced is correct, safe, and coherent with everything around it.</p>
<p>So the bottleneck moved. It went <a href="/blog/software-assurance/">from production to assurance</a>. The expensive station on the line is no longer the one that writes code, it is the one that decides whether to trust it. If your process still treats production as the hard part and assurance as a rubber stamp at the end, you have optimized the wrong station, and it shows up as a codebase that grows fast and rots faster. This is the same shift that turns each isolated, plausible change into a system that is <a href="/blog/locally-correct-globally-incoherent/">locally correct and globally incoherent</a> when nobody is assuring the whole.</p>
<h2 id="close-work-with-the-proof-attached">Close work with the proof attached</h2>
<p>The last angle is where all of this lands: what you keep when a task is done. Most workflows keep the diff and throw away the evidence. The test ran, printed green, and the output vanished into a terminal scrollback nobody will find again. Three weeks later you cannot tell whether that green ever happened or against which version.</p>
<p><a href="/blog/evidence-based-development/">Evidence-based development</a> is the practice of closing work with its proof attached: the change, and next to it what was run, what passed, and against which contract, stored where the next person and the next agent can find it. A completed checkbox is status. It is not evidence. When implementation is abundant and the author is often a model, durable proof stops being paperwork and becomes the only thing that lets you trust the state of the system without re-verifying it by hand. This is why a specification is only half the story unless the proof travels with it, the argument behind <a href="/blog/software-specifications-should-be-portable/">portable software specifications</a>.</p>
<h2 id="the-chain-from-claim-to-proof">The chain, from claim to proof</h2>
<p>Read as one flow, the six angles are a single pipeline that a claim travels down until it comes out the other side as something you can trust. The agent makes a claim. Completion refuses to accept the claim on its own word and demands a demonstration. The demonstration has to be a real run, not a generated sentence, which is why understanding the false success matters: it tells you the claim and the run were never connected in the first place. The run produces evidence, and that evidence is captured and kept next to the change instead of evaporating. Review then happens at the moments that carry the most information, reading the captured evidence rather than reconstructing it. And the whole line is sized by its slowest station, assurance, because that is now the constraint.</p>
<figure class="tp-diagram tp-diagram--flow" data-diagram-type="flow" role="img" aria-label="The six angles as one pipeline: a claim only becomes trust by surviving every station in order."><svg viewBox="0 0 760 190" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><rect x="40" y="62" width="108" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="94" y="101" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">CLAIM</text><path d="M 154 95 L 176 95 M 170 89 L 176 95 L 170 101" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path><rect x="182" y="62" width="108" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="236" y="101" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">DEMONSTRATE</text><path d="M 296 95 L 318 95 M 312 89 L 318 95 L 312 101" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path><rect x="324" y="62" width="108" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="378" y="101" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">RUN</text><path d="M 438 95 L 460 95 M 454 89 L 460 95 L 454 101" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path><rect x="466" y="62" width="108" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="520" y="101" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">EVIDENCE</text><path d="M 580 95 L 602 95 M 596 89 L 602 95 L 596 101" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path><rect x="608" y="62" width="108" height="66" fill="none" stroke="var(--tp-mostaza)" stroke-width="1.5"></rect>
  <text x="662" y="101" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-mostaza)">ASSURE</text></svg><figcaption>The six angles as one pipeline: a claim only becomes trust by surviving every station in order.</figcaption></figure>
<p>Miss any link and the chain leaks. Demand a demonstration but let the agent narrate it, and the false success walks straight through. Capture evidence but throw it away at merge, and review has nothing to read. Assure diligently but only at the end, and you have optimized the wrong station while unverified work piled up in front of it. The point of a hub is to make the links visible, because in practice teams fix one and leave the others open, then wonder why the trust never arrives.</p>
<h2 id="trust-not-output-is-the-bottleneck">Trust, not output, is the bottleneck</h2>
<p>Put the six angles together and you get a single claim. <a href="/blog/building-software-with-ai-agents/">Building software with agents</a> is not held back by how much code you can generate. It is held back by how much of that code you can trust without stopping to check it by hand. Every hour spent re-verifying work an agent already claimed to finish is the tax on missing evidence.</p>
<p>Verification with evidence is how you pay that tax down. Not by trusting more, and not by reading everything, but by making the system produce proof as a byproduct of doing the work, so trust becomes something you can inspect instead of something you have to feel.</p>
<h2 id="where-paelladoc-fits">Where PaellaDoc fits</h2>
<p>I build <a href="/">PaellaDoc</a> because this problem is the one that costs me the most every day. The move it makes is to refuse self-reported success. Nothing reaches “done” on a sentence. It reaches done when the acceptance contract set before the work began has been executed against the real result and the evidence has been captured next to the change. The agent stops being the judge and becomes the party that produces exhibits. Trust stops being a feeling and becomes a folder you can open.</p>
<p>The agents will keep getting better at writing code. That was never the hard part. The hard part is knowing they were right, and knowing it fast enough to keep moving.</p>
<h2 id="faq">FAQ</h2>
<p><strong>What does it mean to verify AI-generated code?</strong> It means replacing the agent’s claim of success with checkable proof: a command that ran, its output, and the version of the acceptance contract it ran against, stored next to the change. The agent reports; a separate step verifies and records.</p>
<p><strong>Why do agents say tests pass when they didn’t run?</strong> Because a language model completes the most probable text for the situation, and after a finished implementation the probable continuation is a success sentence. It is generating the likely report, not observing a real run. The fix is structural: don’t let the agent be the one that declares success.</p>
<p><strong>Isn’t reviewing everything the safe answer?</strong> No, it does not scale. Agents produce more than anyone can read closely. The durable answer is to review at high-leverage moments (scope, then diff, then evidence) and to make the system capture proof automatically, so trust rests on evidence rather than on exhaustive reading.</p>]]></content><author><name>@jlcases</name></author><category term="verify" /><category term="verify-ai-code" /><category term="ai-coding" /><category term="verification" /><category term="software-assurance" /><category term="evidence-based-development" /><category term="paelladoc" /><summary type="html"><![CDATA[The generation of code is solved. What isn't solved is trust. An agent that reports 'done, tests pass' has produced the most probable sentence, not a verified result. Verifying AI-generated code means replacing the feeling of completion with evidence you can inspect: the claim, the proof, and the gate between them.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://paelladoc.com/assets/images/clusters/verify-hero.png" /></entry><entry xml:lang="en-US"><title type="html">Verification loops: hooks, gates and the end of self-reported success</title><link href="https://paelladoc.com/blog/verification-loops/" rel="alternate" type="text/html" title="Verification loops: hooks, gates and the end of self-reported success" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://paelladoc.com/blog/verification-loops</id><content type="html" xml:base="https://paelladoc.com/blog/verification-loops/"><![CDATA[<p>An agent finishes a task and writes: “Done. All tests pass.” Maybe it ran them. Maybe it looked at the code, decided the tests would pass, and reported the conclusion it expected. The message reads identically either way. That is the whole problem in one sentence. <a href="/blog/false-success-claims/">Self-reported success</a> is a claim shaped exactly like proof, with none of the substance, and by default you accept it because checking every time is exhausting.</p>
<p>A verification loop removes the choice. The agent stops being the thing that decides whether it succeeded. A gate does, and the gate runs on evidence.</p>
<h2 id="why-self-reported-success-is-worthless">Why self-reported success is worthless</h2>
<p>The failure is structural, not a matter of a lying model. An agent optimizes for producing the output that looks like task completion. A confident “tests pass” is high-probability text after a task that resembles finishing. Whether a test process actually exited zero is a fact about the world; the sentence is a fact about the model’s expectations. Most of the time they line up. The times they don’t are exactly the bugs you needed to catch, and they arrive wearing the same green message as the successes.</p>
<p>You cannot close this gap by asking the agent to be more careful, or by adding “and make sure the tests really pass” to the prompt. That produces more confident language, not more verification. The gap closes only when the claim and the proof are produced by different processes, and the one you trust is not the one being graded.</p>
<h2 id="the-loop-concretely">The loop, concretely</h2>
<p>A verification loop has four parts, and each one has to be mechanical.</p>
<p>A trigger. Something fires the check automatically at a fixed moment, not <a href="/blog/done-means-done/">when the agent feels done</a>. The moment is usually the end of a turn, a file write, or a commit attempt.</p>
<p>A check that produces a verdict. A command the agent did not write and cannot edit runs and exits with a status. Tests, a type check, a linter, a build, a script that greps the diff for a forbidden pattern. The verdict is the exit code, not the agent’s summary of it.</p>
<p>A gate that acts on the verdict. On pass, the work proceeds or merges. On fail, the work is blocked and the failure is handed back. The gate is not advisory. A gate the agent can talk its way past is a suggestion.</p>
<p>A return path. On failure, the actual error, the real stderr, goes back into the agent’s context with an instruction to fix and re-run. Then the loop repeats. The agent fixes, the check fires again, the gate re-evaluates. It closes when the check passes or when a limit is hit and a human is called.</p>
<p>That last part is what makes it a loop and not just a wall. The agent gets to keep trying, but every attempt has to survive the same gate, and the gate reads reality. “Tests pass” stops being something the agent says and becomes something the agent has to make true, over and over, until the machine agrees.</p>
<h2 id="hooks-where-the-loop-attaches-to-the-world">Hooks: where the loop attaches to the world</h2>
<p>A loop is only as good as its trigger, and the trigger has to be something the agent cannot skip. This is what hooks are for. A hook is a point where your tooling runs your command automatically on an event, with no cooperation required from the model.</p>
<p>There are layers, and you want several.</p>
<p>At the agent-harness layer, modern coding tools let you run a command on lifecycle events, when the agent finishes responding, before or after a tool call, before a file is written. This is the tightest loop, because the failure gets back into context immediately and the agent fixes it in the same session while it still remembers what it was doing.</p>
<p>At the version-control layer, a pre-commit or pre-push hook runs checks before the change is allowed into history. Slower to react than a harness hook, but it catches anything that slipped past, and it doesn’t care which agent or human produced the change.</p>
<p>At the integration layer, the checks run again on every push, independent of anyone’s local setup. This is old ground for human teams. <a href="https://martinfowler.com/articles/continuousIntegration.html">Continuous integration</a> already means every change is verified automatically against the whole system, and the DORA research has spent years showing that <a href="https://dora.dev/capabilities/continuous-integration/">continuous integration</a> and <a href="https://dora.dev/capabilities/test-automation/">test automation</a> are what let teams move fast without breaking things. Agents did not invent the need for this. They made it non-negotiable, because the volume of unreviewed change went up by an order of magnitude.</p>
<figure class="tp-diagram tp-diagram--stack" data-diagram-type="stack" role="img" aria-label="Same checks at three distances from the keyboard. The harness hook is tightest, so the agent self-corrects in the loop."><svg viewBox="0 0 760 208" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><rect x="120" y="20" width="520" height="42" fill="var(--tp-mostaza)" fill-opacity="0.14" stroke="var(--tp-mostaza)" stroke-width="1.5"></rect>
  <text x="380" y="47" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-ink)">HARNESS HOOK</text><rect x="120" y="76" width="520" height="42" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="380" y="103" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-ink)">PRE-COMMIT HOOK</text><rect x="120" y="132" width="520" height="42" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="380" y="159" text-anchor="middle" font-family="var(--tp-mono)" font-size="13" letter-spacing="1.5" fill="var(--tp-ink)">INTEGRATION CI</text></svg><figcaption>Same checks at three distances from the keyboard. The harness hook is tightest, so the agent self-corrects in the loop.</figcaption></figure>
<p>You want the fast harness hook so the agent self-corrects in the loop, and the slower version-control and integration hooks so nothing reaches the shared branch on the strength of a sentence. Same checks, different distances from the keyboard. The need multiplies when you <a href="/blog/multi-agent-orchestration/">run several agents in parallel</a>: no human is watching any single turn, so the gate is the only thing holding the line.</p>
<h2 id="what-the-gate-should-actually-check">What the gate should actually check</h2>
<p>A gate is worthless if it checks the wrong thing, and there is a strong pull toward gates that are easy to pass and prove nothing. “It compiles” is a gate an agent clears while shipping the wrong behavior. “The tests pass” is only as strong as the tests, and if the agent wrote the tests in the same breath as the code, the gate is checking whether the code agrees with itself.</p>
<p>The gate should check against something the agent did not author. Acceptance criteria written before generation. A <a href="/blog/testing-ai-generated-code/">test suite the implementation never saw</a>. A security scan with rules you set. A build that has to produce a running artifact, not just a clean type check. The general principle, the one that separates a real gate from theatre: the check has to be able to fail. If you cannot describe the input that turns it red, it is not verifying anything.</p>
<h2 id="the-failure-everyone-builds-first">The failure everyone builds first</h2>
<p>The common first version of a verification loop is a hook that runs the tests and prints the result, and then the agent reads the result and reports it to you. This is not a loop. It is the same self-reported success with an extra step, because the agent is still the last thing between the check and your decision, and the agent can still summarize a failure as “mostly passing, one minor issue, done.”</p>
<p>The loop only works when the gate acts on the raw verdict directly. The exit code blocks the merge. The failing test output, unedited, is what re-enters the context. The moment a human or an agent gets to paraphrase the verdict before it takes effect, you have reopened the exact gap the loop was built to close.</p>
<p>This is the mechanism underneath the whole idea of <a href="/blog/verify-ai-generated-code/">verifying AI-generated code with evidence instead of vibes</a>. Not more careful prompts. Not a better model. A loop where success is defined by a check the agent cannot edit, triggered by a hook it cannot skip, enforced by a gate it cannot talk past, repeated until reality agrees.</p>
<h2 id="where-paelladoc-fits">Where PaellaDoc fits</h2>
<p>Wiring this by hand is the part nobody budgets for. You end up being the trigger, running the tests after each session; the gate, deciding whether the failures matter; and the return path, pasting the errors back and asking for a fix. That is you doing the loop’s job manually, one session at a time. PaellaDoc runs the loop for you: checks fire on the events, the gate reads exit codes and not summaries, failures go back to the agent with the real output, and the run does not close as done until the evidence exists. You set what “verified” means once, and stop being the thing standing between a confident sentence and the merge button.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="what-is-a-verification-loop-for-coding-agents">What is a verification loop for coding agents?</h3>
<p>It is a four-part mechanism that takes the “did it succeed” decision away from the agent: a trigger fires a check automatically, a check the agent cannot edit produces a verdict from its exit code, a gate acts on that verdict by blocking or passing the work, and a return path feeds real failures back for a fix. It repeats until the check passes or a limit calls in a human. “Tests pass” becomes something the agent has to make true, not something it says.</p>
<h3 id="why-isnt-a-hook-that-just-runs-the-tests-and-reports-the-result-enough">Why isn’t a hook that just runs the tests and reports the result enough?</h3>
<p>Because that leaves the agent as the last thing between the check and your decision, so it can still summarize a failure as “mostly passing, one minor issue, done.” That is self-reported success with an extra step, not a loop. The loop only works when the gate acts on the raw verdict directly: the exit code blocks the merge and the unedited failure output re-enters the context. The moment anyone paraphrases the verdict before it takes effect, the gap reopens.</p>
<h3 id="how-do-hooks-stop-an-agent-from-skipping-verification">How do hooks stop an agent from skipping verification?</h3>
<p>A hook runs your command automatically on an event, with no cooperation required from the model. Use several layers: a harness hook that fires when the agent finishes a turn or writes a file (tightest, so it self-corrects in-session), a pre-commit or pre-push hook before the change enters history, and integration CI that re-runs on every push independent of anyone’s local setup. Same checks at three distances from the keyboard.</p>
<h3 id="what-should-a-verification-gate-actually-check">What should a verification gate actually check?</h3>
<p>Something the agent did not author. “It compiles” clears while shipping the wrong behavior, and “tests pass” is only as strong as the tests, which are worthless if the agent wrote them alongside the code. Check against acceptance criteria written before generation, a test suite the implementation never saw, a security scan with your rules, or a build that must produce a running artifact. The rule that separates a real gate from theatre: it has to be able to fail. If you cannot describe the input that turns it red, it verifies nothing.</p>]]></content><author><name>@jlcases</name></author><category term="verify" /><category term="verification-loops" /><category term="ai-coding" /><category term="verification" /><category term="continuous-integration" /><category term="paelladoc" /><summary type="html"><![CDATA[Every coding agent ends its turn the same way: it declares success. Sometimes it ran the tests. Sometimes it decided, from the shape of the code, that they would probably pass. You can't tell which from the message. A verification loop makes the distinction structural instead of trusting: the agent doesn't get to end its own turn, a gate does, and the gate only opens on evidence the agent can't manufacture. This is the mechanism, the hooks that fire it, and where the loop closes.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://paelladoc.com/assets/images/clusters/verify-hero.png" /></entry><entry xml:lang="en-US"><title type="html">You shipped code you can&#39;t read. Now what?</title><link href="https://paelladoc.com/blog/understand-your-ai-generated-code/" rel="alternate" type="text/html" title="You shipped code you can&#39;t read. Now what?" /><published>2026-07-20T00:00:00+02:00</published><updated>2026-07-20T00:00:00+02:00</updated><id>https://paelladoc.com/blog/understand-your-ai-generated-code</id><content type="html" xml:base="https://paelladoc.com/blog/understand-your-ai-generated-code/"><![CDATA[<p>Here is a sentence a lot of people are thinking and very few are saying out loud: <strong>I can’t read my own code.</strong> The app works, real people use it, you built it, and if you open any file you cannot really say what it does or why. Every change is a bet. Every bug is an excavation. You are running software you do not understand, and you are the whole support team.</p>
<p>This is one of the most common and least discussed states in modern software. It is not a personal failing and it is not permanent. But the way out is not the obvious one, and the obvious one will drown you.</p>
<h2 id="the-obvious-plan-that-fails">The obvious plan that fails</h2>
<p>The instinct is: sit down and read the whole thing until you understand it. Line by line, file by file, until it clicks.</p>
<p>This fails for a specific reason. AI-generated code is often locally reasonable and globally sprawling. There is a lot of it, it is spread thin, and reading it top to bottom gives you thousands of details and no structure. You finish more tired and no clearer, because understanding software is not about having read the lines. It is about knowing the shape: what the real parts are, how they connect, where the important behavior lives. That is a different thing from the text, and reading the text does not hand it to you.</p>
<p>The second failing instinct is to rewrite it, so at least you will understand the new version. That trades a working app you do not understand for a broken app you do, and rebuilds the same gaps in the process. Almost never the answer.</p>
<h2 id="what-understanding-actually-means">What understanding actually means</h2>
<p>You do not need to know every line. No working engineer knows every line of a system they own. What they have is a map at the right altitude, and the ability to zoom in on demand.</p>
<p>Concretely, being in control of your app means you can answer, without reading code:</p>
<ul>
<li>What are the four or five real pieces of this app, and what is each one for?</li>
<li>When a user does the main thing, what happens, in order, across those pieces?</li>
<li>Where does the data live, and what shape is it?</li>
<li>If I change this, what else could feel it?</li>
</ul>
<p>That is the target. Not fluency in every file. A navigable model of the whole, with the detail available when you need it and out of your way when you do not.</p>
<h2 id="how-to-build-the-map">How to build the map</h2>
<p>You can do a version of this by hand today. It is slow, but it works, and it is worth doing at least once so you know what the map should contain.</p>
<p><strong>Start from the outside, not the code.</strong> List what the app does from a user’s point of view. The features, the flows, the roles. This is the spine you will hang everything else on.</p>
<p><strong>Trace one flow all the way through.</strong> Pick the single most important thing a user does. Follow it from the screen to wherever the data ends up, naming each part it passes through. Do not read anything you do not have to. You are drawing the route, not surveying the terrain.</p>
<figure class="tp-diagram tp-diagram--flow" data-diagram-type="flow" role="img" aria-label="Tracing one flow end to end: the fastest way to turn opaque code into a map you can navigate."><svg viewBox="0 0 760 190" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><rect x="40" y="62" width="144" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="112" y="101" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">SCREEN</text><path d="M 190 95 L 212 95 M 206 89 L 212 95 L 206 101" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path><rect x="218" y="62" width="144" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="290" y="101" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">COMPONENT</text><path d="M 368 95 L 390 95 M 384 89 L 390 95 L 384 101" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path><rect x="396" y="62" width="144" height="66" fill="none" stroke="var(--tp-rule)" stroke-width="1.5"></rect>
  <text x="468" y="101" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-ink)">LOGIC</text><path d="M 546 95 L 568 95 M 562 89 L 568 95 L 562 101" stroke="var(--tp-mute)" stroke-width="1.5" fill="none"></path><rect x="574" y="62" width="144" height="66" fill="none" stroke="var(--tp-mostaza)" stroke-width="1.5"></rect>
  <text x="646" y="101" text-anchor="middle" font-family="var(--tp-mono)" font-size="14" letter-spacing="1.5" fill="var(--tp-mostaza)">DATA</text></svg><figcaption>Tracing one flow end to end: the fastest way to turn opaque code into a map you can navigate.</figcaption></figure>
<p><strong>Name the real parts.</strong> By the end of a couple of traces, the same few components keep showing up. Those are your real parts. Write them down with one sentence each. Most apps have surprisingly few.</p>
<p><strong>Ask the agent to explain, then verify.</strong> You can have a model summarize a file or a flow. This is genuinely useful and genuinely dangerous, because a plausible summary of code that does something else is worse than no summary. Treat every explanation as a claim to check against behavior, not a fact. The same discipline that a <a href="/blog/spec-driven-development-green-build-not-correct/">green build is not proof</a> applies here: an explanation that sounds right is not the same as one that is right.</p>
<p>When you can sketch the parts and trace the main flow from memory, you have crossed from passenger to driver. You still cannot read every line. You no longer need to.</p>
<h2 id="write-it-down-before-you-forget-it-again">Write it down before you forget it again</h2>
<p>The cruel part of this state is that you were in it once already, at the start, and it faded. The understanding you build now will fade too, unless it lives somewhere outside your head. So as you build the map, write it down: the parts, the flows, what each is for. This is the beginning of the documentation the prototype never had, and it is what stops you landing right back here in two months. A map that only lives in memory is a map you will lose, which is why it eventually wants to become <a href="/blog/codebase-maps-for-agents/">a living map of the code, not bigger prompts</a> you retype every session.</p>
<h2 id="this-is-stage-one-of-a-longer-bridge">This is stage one of a longer bridge</h2>
<p>Getting readable is not the whole job, it is the first stage of it. Once you can navigate the app, the next moves are to write down what it is supposed to do, put a safety net under the current behavior, and reinforce the structure, in that order. Understanding is what makes all three possible: you cannot specify, test, or fix what you cannot find. The full route from here is <a href="/blog/vibe-coding-to-production/">from vibe coding to production</a>.</p>
<h2 id="where-paelladoc-fits">Where PaellaDoc fits</h2>
<p>The manual map works once. The problem is that the code keeps changing and the map in your head goes stale immediately, which is how you got here. PaellaDoc reads an existing repo into a navigable model of its real parts and flows, and keeps that model attached to the code as it changes, so the map stays true instead of rotting the day after you draw it. The goal is not to read the code for you. It is so “what does this app actually do” has an answer that survives longer than your memory of building it.</p>
<p>You are not stuck. You are one map away from being in control of your own app again.</p>
<h2 id="faq">FAQ</h2>
<p><strong>How do I understand AI-generated code I didn’t really read?</strong> Not by reading every line. Build a map at the level of parts and flows: what the real pieces are, how the main user action moves through them, and where the data lives. Understanding is knowing the shape, not having read the text.</p>
<p><strong>Should I rewrite code I don’t understand?</strong> Almost never. A rewrite trades a working app you don’t understand for a broken one you do, and recreates the same gaps. Map it and reinforce it in place instead.</p>
<p><strong>Can I just ask the AI to explain my code?</strong> As a starting point, yes, but treat every explanation as a claim to verify against actual behavior. A confident summary of code that does something else is worse than no summary.</p>]]></content><author><name>@jlcases</name></author><category term="vibe" /><category term="vibe-coding" /><category term="ai-coding" /><category term="codebase-understanding" /><category term="maintainability" /><category term="prototype-to-product" /><category term="paelladoc" /><summary type="html"><![CDATA[The app is live, real people use it, and you cannot follow a single file of the code that runs it. Here is the way back to control, and it is not reading every line or rewriting the whole thing.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://paelladoc.com/assets/images/clusters/vibe-hero.png" /></entry></feed>