<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[The Verification Gap]]></title><description><![CDATA[AI writes your code. Who verifies the intent?]]></description><link>https://blog.reqproof.com</link><image><url>https://substackcdn.com/image/fetch/$s_!S0PM!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05b39701-c5c1-4bb8-a129-9088c32fe3b6_1024x1024.png</url><title>The Verification Gap</title><link>https://blog.reqproof.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 28 May 2026 19:51:21 GMT</lastBuildDate><atom:link href="https://blog.reqproof.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Leonid Bugaev]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[verificationgap@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[verificationgap@substack.com]]></itunes:email><itunes:name><![CDATA[Leonid Bugaev]]></itunes:name></itunes:owner><itunes:author><![CDATA[Leonid Bugaev]]></itunes:author><googleplay:owner><![CDATA[verificationgap@substack.com]]></googleplay:owner><googleplay:email><![CDATA[verificationgap@substack.com]]></googleplay:email><googleplay:author><![CDATA[Leonid Bugaev]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Death of Security by Obscurity]]></title><description><![CDATA[I should feel very scared right now.]]></description><link>https://blog.reqproof.com/p/death-of-security-by-obscurity</link><guid isPermaLink="false">https://blog.reqproof.com/p/death-of-security-by-obscurity</guid><dc:creator><![CDATA[Leonid Bugaev]]></dc:creator><pubDate>Thu, 28 May 2026 18:21:35 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/16ad71ee-35f2-47fb-bf83-0c69a01c7cfa_1584x672.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I should feel very scared right now. Everyone should be freaking scared. But I think we have had so many emotional events happening in the world recently that people have stopped feeling much of anything, and that is the most dangerous part of where we are. The line we used to tell ourselves &#8212; &#8220;we are not a bank, we are not NASA, we don&#8217;t need that level of security&#8221; &#8212; is no longer an option. We just haven&#8217;t felt it yet.</p><p>Try this thought experiment. Imagine your company&#8217;s source code is made public tomorrow. All of it. How would you feel? I bet most of you would be freaking scared. Not because of IP. Because of the quality. Because of the spaghetti conditions in some files. Because of the strange customer-specific branch nobody touched in three years. Because of the comment that says &#8220;// TODO: fix this before prod&#8221; still sitting in prod. Because of the auth path that &#8220;almost&#8221; works. Because of the secret that probably should have been rotated. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.reqproof.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Verification Gap! Subscribe to read my journey on re-discovering software engineering craft</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>For years, a lot of teams treated security as something between a badge, a process, and a hope. Of course everyone said the right thing. &#8220;We treat security as a first-class citizen,&#8221; and so on. But in practice, unless you were in a regulated industry, security was often optional in the only sense that matters &#8212; optional in priority. You followed best practices. You ran dependency scanners. Maybe you had a penetration test every six months because customers asked for it. Maybe you had a badge in your sales deck. But you weren&#8217;t really thinking about security as part of how the product works.</p><p>Banks, Automotive, Aerospace was different. In those industries, security is existential &#8212; if a bank loses trust, the bank is dead, and if an automotive system fails the wrong way, people die. So they built heavy processes around it: requirements, reviews, evidence, traceability, release gates. All the painful stuff. For a long time it was easy for the rest of us to look at that and say: yes, but we are not a bank.</p><p>I used to think this way too. At Tyk, I work with banks, governments, and large enterprises, and I was often annoyed by how slow some of their security processes were. Every release needed another check. Every patch had to go through another team. Every dependency update could become a discussion. Sometimes it took weeks or months. From the outside it looked like bureaucracy, and a lot of it was bureaucracy. But I changed my mind on the core idea. Those industries understood something the rest of us could safely ignore for a while: security is not something you add at the end. It is part of what the system is.</p><h2>Security is a market</h2><p>This is the part most engineers do not internalise. There is a real economy of people who make money by finding vulnerabilities in software. Some sell to bug bounty programmes. Some sell to brokers. Some sell to whoever is buying. And some just use what they find directly &#8212; exfiltrate data, sell the data, blackmail companies, take systems hostage.</p><p>That market used to be expensive to enter. Finding bugs took time. Understanding a custom system took time. Building an exploit took time. So attackers focused where the return was high &#8212; WordPress, Drupal, popular CMS plugins, well-known SaaS &#8212; anything they could exploit a million times after building it once. If you were niche, you were maybe scanned, but rarely understood. That asymmetry was your moat. Nobody admitted it out loud, but it was the moat.</p><p>A few years ago at Tyk we had a slightly crazy idea: let&#8217;s find open-source Tyk users across the world and see if some of them could become paid users. The idea wasn&#8217;t crazy. The crazy part was how easy the technical side was. I wrote a scanner that could scan the public IPv4 internet in a matter of hours. <strong>The whole internet</strong>. Once you do something like that yourself, &#8220;nobody will find us&#8221; stops sounding like a serious argument.</p><p>You can reproduce a small version of this at home. Run a basic HTTP application on a fresh public IP, expose a port, and watch the logs. Within minutes you start seeing requests: WordPress paths, admin URLs, old plugin routes, random probes, exploit attempts for software you are not even running. Most of it is dumb traffic. That is exactly the point. The internet does not need to know who you are before it starts touching your system.</p><p>So scanning was already cheap. The thing that just changed is understanding.</p><h2>What AI actually changed</h2><p>AI did not invent insecure software. We were already very good at writing insecure software. What AI changed is the cost of finding the insecurity, and the cost of understanding an unfamiliar system. A model can read a codebase and ask the questions a tired team will never ask, because everyone is busy shipping the next thing. It can build a personalised exploit for an unfamiliar service in hours, sometimes minutes. It can chain three small bugs that nobody would have chained manually because the manual cost was too high.</p><p>The reason this is genuinely scary is the floor, not the ceiling. The economics have flipped to the point where a kid in a basement with a decent model can build a personalised exploit for your specific codebase, scan the whole public internet in an afternoon, find every instance of your software, and run that exploit against all of them. Nothing about that sentence requires a state actor.</p><p>And this is not theoretical. Anthropic&#8217;s Project Glasswing reports that Anthropic and around 50 partners used Claude Mythos Preview to find more than ten thousand high- or critical-severity vulnerabilities across important software in the first month, with the bottleneck shifting from finding vulnerabilities to verifying and patching them. &#65532; Cloudflare pointed the same model at more than fifty of their own repositories and found that real vulnerability research needs a harness &#8212; architecture context, narrow tasks, validation &#8212; but with that harness, it works. &#65532; Anthropic also says Mythos-class capabilities will soon exist in many AI labs. &#65532; Open source historically catches up in around six months. The gap closes; it does not stay open.</p><p>So the relevant question is not whether this exact model is public today. The question is whether your security model assumes this capability stays rare. I would not bet a company on that.</p><h2>Assume your source code is already out</h2><p>Here is the uncomfortable truth: you should stop wondering whether your source code is going to leak. You should assume it already has.</p><p>I know how that sounds. But look at what just happened to GitHub. On May 20, 2026, GitHub said it had detected and contained a compromise of an employee device involving a poisoned third-party VS Code extension. GitHub&#8217;s assessment was that GitHub-internal repositories were exfiltrated, with the attacker&#8217;s claim of around 3,800 repositories being directionally consistent with the investigation. GitHub said it had no evidence of impact to customer repositories outside its own internal ones, but it still had to rotate critical secrets and continue analysing logs. &#65532;</p><p>Think about who this happened to. This is GitHub. Owned by Microsoft. With MDM, EDR, hardened endpoints, mature processes, more security engineering than almost any company on earth. One developer. One poisoned VS Code extension. Source code gone.</p><p>And the exposure window was tiny. The Nx Console advisory says the malicious version was live in the Visual Studio Marketplace for about 18 minutes and in OpenVSX for about 36 minutes. Eighteen minutes was enough.</p><p>Now compare that to your company. If this happened to GitHub, with everything GitHub has, do you really believe nobody has done the equivalent to your team? Be honest. How many extensions did your team install last week? Do you know what any of them do? When did each one last update? Who reviewed it?</p><p>So assume it. Assume some snapshot of your source code is already out there. It does not even need to be the latest one &#8212; an older snapshot is enough to know where to look. And once an attacker has that, they can use AI to do exactly what defenders are starting to do: read everything, model the system, and build the exploit shaped specifically for you.</p><p>That changes the threat model fundamentally. Black-box testing &#8212; sending requests, observing responses, fuzzing endpoints, inferring behaviour &#8212; is already dangerous. White-box is a different category. With your code in hand, the attacker does not need to guess. They can follow your authentication logic. They can inspect your authorisation paths. They can see your tenant isolation, find the one resolver that does not validate ownership, see the timeout path nobody tested, find the internal endpoint that was &#8220;safe&#8221; because nobody knew it existed, find the retry that is not idempotent, read the comment that says &#8220;this should never happen,&#8221; and find the strange customer-specific branch that everyone forgot about.</p><p>If your answer to that scenario is &#8220;well, they probably do not know how the system works,&#8221; then the source code itself was part of your security boundary. And that boundary is gone.</p><h2>CI/CD is not plumbing anymore</h2><p>The other place this hits hard is the build system. We used to think of CI/CD as plumbing. From an attacker&#8217;s point of view, CI/CD is one of the most interesting machines in the company &#8212; it usually has source code, deployment credentials, package publishing tokens, cloud access, GitHub tokens, and secrets for half of the internal systems.</p><p>The Trivy incident in March 2026 is the most uncomfortable example because Trivy is a security tool. A trusted security scanner became the attack vector &#8212; version tags in aquasecurity/trivy-action were force-pushed to credential-stealing malware, and the action stole everything CI runners had access to. &#65532; CanisterWorm followed a similar pattern: attackers stole npm tokens from compromised pipelines and used them to publish backdoored versions across every namespace they could reach. The malicious packages ran on postinstall &#8212; install was enough. &#65532;</p><p>So zero trust now has to include code. But notice the ladder. First, don&#8217;t trust your dependencies &#8212; pin versions, quarantine updates. Then, don&#8217;t trust the actions running in your pipelines &#8212; pin those to commit SHAs, not tags. Then ask the harder question: what if the platform itself is compromised, the way GitHub just was? At that point pinning helps but is no longer a complete answer. Each step up the ladder gives the attacker less leverage, but no step makes the problem zero.</p><p>This sounds exhausting. It is. But the alternative is worse.</p><h2>The CVE model is dead</h2><p>The CVE model still matters, but it cannot be the centre of your security process anymore. For a lot of teams the hidden workflow is still: wait for the CVE, check the severity, patch by priority, hope customers update before something bad happens. That workflow was already fragile. The new world breaks it.</p><p>VulnCheck found that in the first half of 2025, 32.1% of known exploited vulnerabilities had exploitation evidence on or before the day the CVE was issued. For a large share of exploited vulnerabilities, the CVE was not the start of danger. It was already late. &#65532;</p><p>The system producing CVEs is also overloaded. NIST said CVE submissions increased 263% between 2020 and 2025. NIST enriched nearly 42,000 CVEs in 2025, more than any prior year, and still said it was not enough to keep up. In April 2026, NIST moved to a risk-based enrichment model where some CVEs are listed but not immediately enriched. &#65532;</p><p>And none of that machinery will know the weird things inside your own system. A CVE will not know that your GraphQL resolver crashes a customer&#8217;s system on one malformed input. It will not know that your retry path is unsafe when the downstream service writes but your load balancer times out first. It will not know that your PII is in the same database as everything else, and one forgotten SQL injection path could expose more than anyone expected. You need a model of your own system, not just a feed of public bugs.</p><p>The intuitive response is &#8220;patch faster.&#8221; That is not enough either. No matter how fast you patch, if your architecture and development flow do not enforce security boundaries by default &#8212; if they do not force you to think about security as you write the code &#8212; patching will not save you. Every new release becomes a new attack surface, and a personalised exploit can be ready in five minutes. I am not exaggerating.</p><p>Cloudflare made the more important point. They described teams talking about a two-hour SLA from CVE release to patch in production, but if regression testing takes a day, getting to two hours means skipping something. Their conclusion is architectural: make exploitation harder even when a bug exists, put defences in front of the application, design the system so one flaw does not give access to everything else, and make fixes deployable everywhere at once. &#65532;</p><p>That is the difference between reactive security and designed security. Reactive security tries to outrun the attacker. Designed security assumes bugs exist and limits what one bug can do.</p><h2>So how do you live in this world?</h2><p>The first thing is to actually accept where you are. You have to go through the stages &#8212; angry, scared, in denial, eventually acceptance. Most teams stop at denial. They tell themselves the old story: we are not big enough, we are not interesting enough, who would target us. That story is over.</p><p>Once you accept it, the next move is not &#8220;make everything secure.&#8221; That is too vague and usually becomes theatre. Start with the worst case. What can kill your company?  How would I leak all customer data? How would I bypass authentication? How would I bypass tenant isolation? How would I poison a release? How would I get production credentials out of CI? How would I make one customer&#8217;s system go down with a single malformed request? How would I turn a slow downstream service into a cascading outage?</p><p>This is uncomfortable, but it gives you priorities. The worst case is not the same for every company. For a bank, anything touching the money is existential. If a bank loses money, the bank is dead. For a SaaS company automating LinkedIn outreach, leaking a list of customer emails is bad but survivable. The same company being used to impersonate its users and send messages on their behalf is not survivable. That is the death of the company.</p><p>If everything is critical, nothing is critical. But some things really are: authentication, authorisation, tenant isolation, secrets, CI/CD, package publishing, admin APIs, customer data, billing data, and anything that turns one bug into many customers affected. Be honest about which of these are existential for you, and put real process around those.</p><p>By real process I do not mean bureaucracy for its own sake. I mean what banks and government suppliers actually do, and I am saying this from experience &#8212; I spent years building software that went through that kind of pen-testing, the long kind, with people who do this for a living. Pin everything that runs in your build. Make secrets short-lived. Quarantine dependency updates before they reach your main branch. Treat anything that executes code in your dev or build environment as part of the product, because it is.</p><p>And then there is the boring part, the part that works much better than people want to admit: <strong>checklists</strong>.</p><p>Checklists are not exciting. They are one of the main reasons software engineering has ever shipped anything reliable. They are why planes fly. They are why a CT scanner does not lie to a radiologist. The reason they work is not because they are clever. They work because they force you to think about things you would otherwise skip.</p><p>If you have a login system, you should be forced to think about password reset, previous password reuse, account enumeration, timing attacks, brute force, lockout, and what happens when the email provider is down. If you make an outbound HTTP call, you should be forced to think about timeouts, DNS hangs, retries, idempotency, downstream slowness, partial success, and what happens if the service receives your request but your load balancer times out before you get the response. If your Go code starts a goroutine, you should be forced to think about cancellation, ownership, leaks, blocked channels, and behaviour under load.</p><p>None of this is advanced security research. It is basic engineering. But it only happens when the process forces it to happen. The hardest bugs to find are not the ones you wrote wrong; they are the ones you never wrote at all, because you never thought about that case.</p><p>And if you decide not to handle something, fine &#8212; say it. Log it as a known issue. Put an expiration on it. Hidden gaps are the dangerous ones.</p><h2>Putting security on autopilot</h2><p>After years of pen-testing work with banks and governments, I noticed the same kinds of gaps kept coming back. Login systems that didn&#8217;t think about timing attacks. HTTP calls that didn&#8217;t handle partial success. Goroutines that leaked under load. Not exotic bugs &#8212; basic ones, in different codebases, over and over.</p><p>So I started writing them down. Open-source projects I had investigated, real incidents I had seen up close, every checklist I had built across years of audits &#8212; all of it into a catalogue. That catalogue is what Proof runs on.</p><p><a href="https://reqproof.com">Then I built automation around it</a>, because checklists in someone&#8217;s head do not scale. This is also why I care about MC/DC. Line coverage tells you a line ran. Modified Condition/Decision Coverage &#8212; required by the FAA for Level A software, where failure could be catastrophic &#8212; asks whether every logical part of a decision independently affected the outcome. &#65532; The bug is rarely &#8220;this function was never tested.&#8221; It is &#8220;this function was tested, but not when auth is false, tenant is different, feature flag is enabled, downstream state is stale, and the retry path is active.&#8221; Not the happy path. The combination nobody specified.</p><p><a href="https://reqproof.com">Proof</a> works in both directions. From spec to code: if your requirement says &#8220;user can log in,&#8221; Proof attaches sub-requirements for password reset, timing attacks, enumeration, lockout, dependency failures &#8212; and the CI check literally fails if a required item has no test attached. If you decide not to handle one of them, fine &#8212; mark it as a known issue, attach an expiry, and it stays visible instead of disappearing. From code to spec: static patterns scan for signals (HTTP client, goroutine, database call, queue), and when one is found, Proof asks the spec whether you have described what happens when the service is slow, down, or partially successful. If the spec is missing, the code is shouting at the spec.</p><p>Over time the spec stops being a static document and becomes a living source of truth &#8212; in my case, a graph of small interconnected requirements that I treat as more authoritative than the code itself, because the code can drift and the spec is the contract. Spec links to code, code to tests, tests back to spec. If one of them changes, the link becomes suspect, and you have to look again.</p><p>This does not replace human security work. It just makes the questions impossible to skip.</p><h2>Security is everyone&#8217;s problem now</h2><p>Not every company needs to copy bank-level process. That would kill a lot of teams. But every software company needs to copy the posture. Security is not something you bolt on at the end. Your moat is no longer the software you build, or the market expertise, or being first. Your moat is being something people can trust to depend on for a long time.</p><p>So assume the internet will find you. Assume your dependencies are not safe by default. Assume one developer tool can become the entry point. Assume your source code has already leaked. Assume attackers can use AI to understand your system faster than you can.</p><p>Then ask what still protects you. Do you know which parts of the system can kill the company? Can you rotate secrets quickly? Do you have tests for the behaviours that matter, not just the lines that execute? Do you know when code, tests, and specs drift apart?</p><p>Security by obscurity is what dies when attention gets cheap. That world is going away. The practical question now is simple. When someone looks closely at your system &#8212; with automation, with your source code in front of them, with AI, and with patience &#8212; what will they find?</p><p>And what will be your answer?</p><div><hr></div><p>If any of this is hitting close to home and you want to see what putting security on autopilot looks like in practice, get in touch. I'd love to hear what your team is dealing with &#8212; and show you how Proof works on real code. <a href="https://reqproof.com">reqproof.com</a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.reqproof.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Verification Gap! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Source of truth: Code, Spec, or Requirement?]]></title><description><![CDATA[When code becomes easy to produce, the hard part is remembering what we meant.]]></description><link>https://blog.reqproof.com/p/code-spec-or-requirement</link><guid isPermaLink="false">https://blog.reqproof.com/p/code-spec-or-requirement</guid><dc:creator><![CDATA[Leonid Bugaev]]></dc:creator><pubDate>Thu, 14 May 2026 14:50:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nDMH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nDMH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nDMH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!nDMH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!nDMH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!nDMH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nDMH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png" width="1376" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1376,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1473621,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.reqproof.com/i/197232948?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nDMH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!nDMH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!nDMH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!nDMH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0a4b03-1908-4c6b-b09b-66bd6f1809b9_1376x768.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>The code runs. The code breaks. The code is what production uses.<br><br>Specs and docs can help, but they often become stale. So we learned not to trust them too much.<br><br>Code is honest in a way documents are not. It may be wrong, but it does exactly what it does.<br><br>But I think there was another reason we trusted code so much.<br><br>For a long time, code was manual work. We wrote it ourselves. We spent time with it. We were not only typing syntax; we were thinking through the system while writing it. The thinking and the implementation were almost the same activity.<br><br>That is why code deserved this level of trust. Not because code was perfect. It was not. But because the code carried a lot of the human judgement that produced it.</p><p>With agentic coding, this becomes more complicated.</p><p>As an individual contributor, I can move incredibly fast now. I can open Claude Code or a similar tool, give it direction, and shape the project while it is being built. I may start with a rough spec or just an idea, but the real decisions often happen during implementation. I try something. I see the code. I realise the original idea was not quite right. I adjust. The agent tries another version.</p><p>This is not bad. Actually, it is one of the best parts of working this way. Implementation gives feedback. Sometimes the code teaches you what the spec should have been.</p><p>So code-first is very tempting. It keeps speed. It keeps flow. It works especially well when the person steering the agent has the whole picture in their head.</p><p>But that is also the problem.</p><p>In that case, the real source of truth is not the code and not the spec. It is the experienced person.</p><p>They know the edge cases. They know the dependencies. They know which interface is fragile. They know why some strange behaviour exists. They know when the generated code is technically correct but still wrong. They are continuously filling the gaps.</p><p>The agent is not really working from a complete spec. It is working with a human who carries the missing context.</p><p>This works while the system fits inside one brain.</p><p>But real systems do not stay like this. They grow. They get delegated. Teams split. People leave. New people join. Some engineers understand the product but not the architecture. Some understand the architecture but not the domain. Some are junior. Some are moving fast. And the agent only knows what we gave it, plus whatever it can infer.</p><p>At that point, <strong>memory becomes a bad source of truth</strong>.</p><p>We forget dependencies. We forget edge cases. We forget why something was built in a strange way. We forget which downstream system depends on a behaviour. Not because people are bad, but because we are humans.</p><p>Agents do not magically solve this. If something is not described, they will improvise. They will choose something plausible. And plausible is often enough to pass the first review.</p><p>The same is true for humans. If something is not specified, we should not expect it to behave exactly as we imagined.</p><p>This is where I think the word &#8220;spec&#8221; is not enough.</p><p>In many software teams, a spec is a temporary artifact. You write it to start the task. It helps the engineer or the agent. Then implementation happens, some things change, and the spec is effectively dead. Maybe it still exists in Notion, Linear, Jira, GitHub, or a markdown file. But nobody really trusts it six months later.</p><p>If the spec is temporary, of course code wins.</p><p>But maybe the better word is requirement.</p><p>And this is not a new idea. This is basically how regulated industries already work. In aerospace, medical, automotive, and similar places, requirements are normal. They can produce multi-hundred-page documents explaining the whole system, but usually it is not really one big document in the simple sense. It is a set of requirements, sub-requirements, interfaces, tests, evidence, and links. A graph that can be turned into a document when needed.</p><p>That difference matters.</p><p>A spec often says: here is what we want to build.</p><p>A requirement says: here is what the system must do, what it must not do, how we know it was implemented, and where the evidence is.</p><p>The requirement does not die when the task is done. The code implements it. The test proves it. The evidence is attached to it. If something changes, the requirement becomes part of the review again.</p><p>This is the part I think normal software engineering may need to borrow, but without copying all the heaviness.</p><p>Not because we suddenly want bureaucracy. I don&#8217;t. But because agentic engineering makes implementation faster, and the faster we implement, the easier it becomes to lose intent.</p><p>The dangerous drift is not always a bug. The code can work. The tests can pass. CI can be green. But the product intent may have moved a little. The domain behaviour may not be exactly right. A security assumption may be weaker. An interface may have changed in a way nobody noticed.</p><p>Everything is green, but it is green around slightly wrong intent.</p><p>I also do not think the answer is just &#8220;write a huge spec first.&#8221; That can fail too. A detailed spec can make wrong assumptions before reality pushes back. Then implementation starts, the spec is challenged, and now you have spec drift instead of code drift.</p><p>So for me the real question is not code-first or spec-first.</p><p>The real question is how we manage the drift between intent and implementation.</p><p>If the code does something different from the requirement, it should not silently become the new truth. But if the requirement is wrong, it should not block reality forever either. There should be a stop. The team should ask: is the code wrong, is the requirement wrong, or did we learn something?</p><p>Until that is resolved, something is broken in the process.</p><p>This is where traceability matters. Not as documentation for documentation&#8217;s sake, but as an invalidation mechanism.</p><p>If code changes, the related requirement and tests should become suspicious. If a test changes, the requirement should be checked. If a requirement changes, the code and tests should definitely be reviewed. The system should say: this thing changed, so these other things are no longer fully trusted.</p><p>This is also why evidence matters.</p><p>I do not trust humans. I do not trust AI either. I want the evidence.</p><p>A green CI run is useful, but evidence of what? That the code passes current tests? Or that the system still matches the original intent?</p><p>Those are not the same thing.</p><p>In larger projects, the original intent becomes a spec, then tasks, then subtasks, then test cases. Every step narrows the focus. Everyone implements their piece. Everyone tests their piece. Locally, everything can look correct. But who validates the final system against the original intent?</p><p>Usually not systematically.</p><p>This is the part I want to think more about. Maybe requirement management, in some lighter and more modern form, becomes much more important for normal software teams. Not because requirements are new, but because agentic development changes the cost of not having them.</p><p>Code is still the runtime truth. It tells us what the system does.</p><p>But requirement is the intent truth. It tells us what the system is supposed to mean.</p><p>And evidence is what connects them.</p><p>I do not have the <a href="https://reqproof.com">full answer yet</a>. I still do not know how detailed requirements should be before they become harmful. I do not know which parts should be formal and which should stay flexible. I do not know how to make traceability work inside Git, PRs, CI, and agentic coding tools without making everyone hate it.</p><p>But I think the direction is becoming clearer.</p><blockquote><p>In the old world, code was expensive to produce, so code naturally became the main asset. In the agentic world, code may become cheaper to produce, but intent becomes easier to lose.</p></blockquote><p>And if intent is the thing we can lose, maybe intent is the thing we need to manage much more seriously.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.reqproof.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe to receive new posts and updates on my journey on building <a href="https://reqproof.com">Proof</a></p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Trust Is the Bottleneck]]></title><description><![CDATA[AI can write specs, code, tests, and docs. If all of them agree on the wrong intent, green CI isn&#8217;t enough.]]></description><link>https://blog.reqproof.com/p/engineerings-ai-bottleneck-is-trust</link><guid isPermaLink="false">https://blog.reqproof.com/p/engineerings-ai-bottleneck-is-trust</guid><dc:creator><![CDATA[Leonid Bugaev]]></dc:creator><pubDate>Tue, 05 May 2026 16:01:15 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2d387e0a-8965-470d-87b9-c8743c912f31_1376x768.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Everyone is asking the same question now: if AI can help us create much more code, why aren&#8217;t engineering teams suddenly moving much faster?</p><p>I think the question is right, but the answer usually stops too early.</p><p>AI does make some things dramatically faster. MVPs are faster. Prototypes are faster. The time to validate an idea is reduced a lot. You can explore directions that previously weren&#8217;t worth the effort. This is real, and I don&#8217;t want to pretend otherwise. But creating the first version of something isn&#8217;t the same as maintaining a product, and creating more pull requests isn&#8217;t the same as creating more trusted change.</p><p>This is where the economics breaks. If your team can create ten times more pull requests, your product doesn&#8217;t automatically move ten times faster. Your company doesn&#8217;t become ten times faster. The economy doesn&#8217;t double or triple. Because the expensive part of mature engineering was never only the typing of code.</p><p><strong>The expensive part is trust.</strong></p><p>Can I trust this change? Does it match the intent? Does it break a hidden customer flow? Does it affect backwards compatibility? Are the docs updated? Are the tests proving the right thing? Did we think about security, performance, malformed input, error states, release notes, migration, support?</p><blockquote><p>A pull request doesn&#8217;t answer all of this. <br>A pull request is just something asking to be trusted.</p></blockquote><p>So I don&#8217;t think the interesting question is &#8220;can AI create more code?&#8221; It can. <em>The interesting question is: what needs to exist around the code so we can safely absorb more change?</em></p><p>If we can scale trust, we can unlock the real scaling of AI. But not by sending maintainers ten times more PRs. That only moves the bottleneck. What I want is a pull request that comes with enough context that I can actually believe it: why this change exists, what it affects, which tests prove it, which docs changed, what can break, and what still needs a human decision.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.reqproof.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">AI made implementation cheaper, but trust is still expensive. Join me on journey to find what to trust in new, post AI world.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>  If that is the kind of engineering problem you care about, subscribe.</p><h2>If your trust model is green CI, you are in trouble</h2><p>AI isn&#8217;t going back in the box. Even if you personally didn&#8217;t join the hype train, people around you probably did. Engineers use it to write code. PMs use it to write specs. Someone asks it to validate the plan, then write the code, then write the tests, then check everything against the same plan again.</p><p>I do it too. I ask AI to help me write the spec. Then I ask it to validate the plan. Then I ask it to write the code. Then validate its own code. Then write the tests. Then check everything against the original plan. It&#8217;s tempting because it works surprisingly well. In a lot of cases, it feels almost magical.</p><p><strong>But this is exactly why it becomes dangerous.</strong></p><p>For a long time, our basic engineering trust model was something like this: write the code, write the tests, pass CI/CD, review the pull request, ship. It was never perfect, but it was much better than nothing. Green CI never meant the product was correct. It meant the code passed the checks we had.</p><p>The problem is that those checks don&#8217;t prove intent. They don&#8217;t prove the requirement was correct. They don&#8217;t prove the tests were testing the right thing. They don&#8217;t prove the documentation was complete. They don&#8217;t prove the change matched the real product behavior we needed.</p><blockquote><p>They prove that the current artifacts agreed with the current checks.</p></blockquote><p>With AI, the whole chain can be generated. The spec can be wrong, the code can follow the wrong spec, the tests can validate the wrong code, the docs can describe the wrong behavior, and CI can still be green. Everything agrees with everything, but the intent is wrong.</p><blockquote><p>That&#8217;s not trust. That&#8217;s a consistent mistake.</p></blockquote><p>This is why high coverage isn&#8217;t enough either. In my <a href="https://blog.reqproof.com/p/i-had-near-100-test-coverage-it-didnt">previous article about jsonparser</a>, the painful part wasn&#8217;t that I had no tests. I had near-100% coverage in the area that mattered. The problem was that malformed input behavior was never properly described. So the tests proved what existed, not what should have existed.</p><p><strong>You cannot test what you never described.</strong></p><p>Security makes this even less optional. For years, many teams survived with some quiet version of security by obscurity. Not officially, of course. Everyone says security matters. But in practice, a lot of software depended on nobody looking too closely, or on attackers moving slowly enough that maintainers had time to react.</p><p>That assumption is breaking. <a href="https://www.vulncheck.com/blog/state-of-exploitation-1h-2025">VulnCheck reported</a> that in the first half of 2025, 32.1% of known exploited vulnerabilities had exploitation evidence on or before the day the CVE was issued. This doesn&#8217;t mean every vulnerability becomes an exploit in hours, but it does mean the old time cushion isn&#8217;t something you can build your product around anymore.</p><p>So things that felt optional before become normal engineering requirements: malformed input, authorization boundaries, resource limits, timeout behavior, error states, data exposure, public API behavior. These aren&#8217;t enterprise extras. They&#8217;re product requirements.</p><p>This is the uncomfortable part: the trust problem is now everyone&#8217;s problem. Even if your company hasn&#8217;t &#8220;adopted AI,&#8221; your people probably have. Even if your CI is green, it may be green against the wrong intent. Even if your coverage is high, it may cover the behavior you remembered to describe, not the behavior the product actually needs.</p><p>So we need a different source of truth. Not instead of CI/CD, not instead of tests, not instead of code review. Above them. Something that says what the system is supposed to do, which obligations apply, what evidence proves them, and what becomes suspicious when something changes.</p><blockquote><p>Otherwise AI won&#8217;t only help us move faster. <br>It will help us move faster with a false feeling of safety.</p></blockquote><h2>The outside structure is not the product</h2><p>I know this problem from open source. For the last 12 years at least, I worked a lot in open source. I had my own popular open source projects, and today at Tyk we build an open source API Gateway.</p><p>Open source is hard. Not because people are bad. Usually it&#8217;s the opposite. Someone from the outside sends you a pull request. Maybe it&#8217;s a bug fix. Maybe a new feature. Maybe it&#8217;s useful. Maybe it&#8217;s technically correct. Maybe they spent their evening on it.</p><p>But as a maintainer, you still need to get inside the context. You need to understand what&#8217;s happening and why this person is doing it. You can be fast and accept too much, or stay picky and make people unhappy. Neither option really solves the trust problem.</p><p>The real issue isn&#8217;t that contributors are bad. The issue is that they see the outside structure. They see the code. Maybe they see the tests. Maybe they see the docs. But they don&#8217;t see the intent in the same way the owner of the project sees it. They don&#8217;t know all the small product promises made over the years. They don&#8217;t know which ugly thing is accidental and which ugly thing is load-bearing. They don&#8217;t know which customer flow depends on some behavior that looks strange from the outside.</p><p><em>They are not inside of this bubble.</em></p><p>And this isn&#8217;t only open source. The same thing happens inside a company. Someone from support knows the product very well. They see customer pain every day. They may even be technical enough to raise a pull request. Someone from solutions architecture can do the same. Another team can contribute to your service. AI makes all of this easier.</p><p><strong>But internal doesn&#8217;t automatically mean trusted.</strong></p><p>A support engineer may understand the product from the customer side, but not the architecture. Another team may understand code, but not the local history. AI may generate something that looks clean, but it has no real ownership unless someone gives it context and checks it.</p><p>These contributions can become shallow. Not useless. Shallow. They touch the visible layer of the system, but they aren&#8217;t backed by the deep intent of the people who own this part of the product.</p><p>We tried relaxing quality gates a few times. More people contributing sounds obviously good, especially when every company has more backlog than humans. But we had cases where a simple line, a simple fix, broke everything. We had other cases where the fix was so big in scope that it was too dangerous to move there.</p><blockquote><p>The conclusion wasn&#8217;t &#8220;only engineers can write code.&#8221;<br>The conclusion was: if you want to scale engineering, it&#8217;s always about trust.</p></blockquote><p>This is also why &#8220;move fast&#8221; changes meaning when you have customers. When you&#8217;re still searching for an MVP, you can break things and call it learning. But when customers put your product inside their infrastructure, the product is no longer fully yours. They pay you for stability, security, and predictable behavior. In a way, you give away part of the ownership.</p><p>At Tyk, this is very real. We build software used by banks, governments, and large enterprises. Quality assurance isn&#8217;t some internal slogan. It&#8217;s part of the relationship with customers. Every software has bugs; I don&#8217;t want to pretend otherwise. But the price of a bug isn&#8217;t the same everywhere. Sometimes it&#8217;s legal. Sometimes it&#8217;s regulatory. Sometimes it&#8217;s very big money. Forget even the money for a second: what if the bank goes down? It can become a national-level issue.</p><blockquote><p>Speed isn&#8217;t how quickly you can make a change. <br>Speed is how quickly you can safely absorb change.</p></blockquote><p>Lehman&#8217;s software evolution work has a phrase that fits here: &#8220;The safe rate of change per release is constrained by the process dynamics.&#8221; In the same passage, he says that as the number, size, and architectural distance of changes increase, complexity and fault rate grow more than linearly.</p><p>This sounds academic, but it matches product reality. You can move only as fast as your safety norms allow. Your current team, your architecture, your customer base, your process, your quality gates, your review culture &#8212; all of this defines your real speed.</p><p>If AI gives you more change than your trust system can absorb, you aren&#8217;t scaling engineering. You&#8217;re scaling incoming work.</p><h2>Temporary specs become archaeology</h2><p>One of the deeper problems is how we treat specifications in consumer engineering.</p><p>Most of what we call a specification is a temporary artifact. You start with all the best practices. Maybe an RFC. Then it becomes a detailed Jira ticket. Maybe later there is an ADR. There are comments in GitHub. A Slack thread. A Confluence page. A few decisions made during review because reality was different from the original assumption.</p><p>At the moment, this feels normal. This is how software gets built. But after some time, all these artifacts pile up. If you want to understand how a component works, you need to dig through history. You need to understand why it ended up in this final state. Why this was done and not that. You may be lucky and find the exact explanation. In most cases it&#8217;s lost in someone&#8217;s head.</p><blockquote><p>This is archaeology, not development.</p></blockquote><p>The bigger problem is that these artifacts are independent. The RFC isn&#8217;t connected to all the code. The Jira ticket isn&#8217;t connected to all the tests. The docs are scattered across ten pages. The final implementation isn&#8217;t connected back to the original assumptions. <strong>It&#8217;s not a graph.</strong></p><p>So we trust a person &#8212; engineer, architect, lead, PM &#8212; to hold the high-level picture in their head. We trust them to find all dependencies. We trust them to notice backwards compatibility issues. We trust them to know which docs need updating. We trust them to remember which customer flow can break.</p><p>And of course people forget. Not because they&#8217;re careless. Because this is too much context for one person to carry.</p><p>You fix a bug and break some other flow. You build a feature and forget a dependency with another service. You update two documentation pages and miss the other eight. The feature exists, but it&#8217;s unusable for one group of users. The implementation works, but not in the real production shape.</p><blockquote><p>The spec was supposed to create clarity. <br>But because it was temporary, it becomes one more historical artifact.</p></blockquote><p>This is also why spec-first isn&#8217;t enough. Spec-driven development is better than no spec. Planning before coding is obviously better than jumping into implementation. But if the spec is still treated as a temporary artifact, after a few iterations you end up in the same position, with intent chaos.</p><p>During development, the spec always changes. You start with assumptions. Researched assumptions, but still assumptions. Then implementation begins and reality appears. The architecture doesn&#8217;t work. A limitation appears. A reviewer notices a security issue. QA finds a case. A customer dependency changes the direction. And in many teams, the spec isn&#8217;t updated.</p><p>The real knowledge moves into GitHub comments, Slack messages, review threads, and people&#8217;s heads. The Jira ticket becomes stale. The implementation says one thing. The ticket says another.</p><p>Imagine someone from QA comes back from vacation and needs to test the feature. They see the ticket. They see the implementation. They have no idea what is happening. Why this? Why not that? Is this intended?</p><p>It&#8217;s so common. I bet a lot of you feel the same.</p><h2>How I can know what I don&#8217;t know?</h2><p><strong>A lot of bugs are not in the code first. They are in the missing specification.</strong></p><p>Have you actually described what should happen if the input is malformed? Have you described that this functionality must not allow SQL injection? What happens if the third-party service times out? What is the error state? Have you described authorization boundaries? Resource limits? Performance boundaries? What happens when something goes from ten requests per second in a test environment to thousands in production?</p><p>There are also more subtle cases. Concurrency. Non-deterministic behavior. Map iteration. Merge order. I&#8217;m looking at you, Go.</p><p>Have you described that the behavior should be deterministic? Did you write a test for it? Did the test prove the requirement, or did it just execute the code?</p><p>This is where checklists, obligations, processes, discipline, and all the boring stuff come in. I know people hate boring process. I hate fake process too. Documents nobody reads. Boxes people tick after the fact. Quality theatre.</p><p>But the useful version is different. An obligation is not a test case. It&#8217;s a category of behavior you are required to describe: malformed input, boundary behavior, error handling, access denied, determinism, idempotency, atomicity, nil safety, overflow safety, encoding safety.</p><blockquote><p>The obligation doesn&#8217;t tell you the answer. <br>It forces you to ask the question.</p></blockquote><p>That is why I like it. It turns &#8220;maybe someone remembers&#8221; into a deterministic process. The checklist itself is human judgment. But checking whether the spec covered the checklist can be mechanical.</p><p>This is also where AI can help without pretending to own the product. If the code uses goroutines, the system can ask where cancellation, lifecycle, and error propagation are described. If code depends on map iteration or merge logic, it can ask whether determinism or commutativity matters. If code reads time directly, it can ask whether time is part of the behavior and how this is tested. If code changes a public API, it can ask where compatibility and documentation obligations are.</p><p><strong>This isn&#8217;t AI judging architecture taste. It&#8217;s tooling surfacing missed questions.</strong></p><p>That is the &#8220;how I know what I don&#8217;t know&#8221; loop. Spec obligations force code and test evidence. Code shape can reveal missing spec questions.</p><h2>What regulated industries got right</h2><p>Consumer engineering and regulated engineering live in different worlds. Different tools. Different conferences. Different language. Some of it is archaic. Some of it is bureaucracy. I don&#8217;t want every SaaS team to become an avionics certification team.</p><p><strong>But we shouldn&#8217;t ignore what they learned.</strong></p><p>I expected to find paperwork. Annoyingly, I found a lot of things our world forgot to learn.</p><p>In aviation, automotive, medical devices, space systems, the spec isn&#8217;t treated as a temporary note. It&#8217;s a source of truth that lives together with the software. Requirements have IDs. They have layers. They are linked to documentation, tests, implementation, verification evidence. You can see blast radius. You can see what a change affects. During review, if implementation differs from the spec, the spec must be updated.</p><blockquote><p>The useful idea is not the paperwork. <br>The useful idea is that intent is durable, traceable, and connected to evidence.</p></blockquote><p><a href="https://software.nasa.gov/software/ARC-18066-1">NASA&#8217;s FRET</a> is one example of this direction. It lets users enter hierarchical system requirements in structured natural language, gives those requirements unambiguous semantics, and can show them as natural language, formal logic, diagrams, and interactive simulation.</p><p>That doesn&#8217;t mean every product team needs FRET or formal methods everywhere. It means the requirement is not just a document. It&#8217;s something you can analyze, link, verify, and keep alive.</p><p>This is where requirement management becomes interesting again for consumer engineering. Not the old heavy version copied blindly from regulated industries. Not paperwork for paperwork. But the useful part: a source of truth, cross-links, invalidation, traceability, and evidence.</p><p>Combined with everything consumer engineering learned over the years: CI/CD, pull requests, fast feedback, developer experience, automated tests, observability, docs, release automation.</p><blockquote><p>We should not throw away modern engineering. <br>We should add the missing trust layer.</p></blockquote><h2>From pull request to evidence pack</h2><p>Today a pull request usually gives me code, maybe tests, maybe a description. But it doesn&#8217;t give me the whole chain.</p><p>It doesn&#8217;t tell me the original intent. It doesn&#8217;t tell me which obligations apply. It doesn&#8217;t show the blast radius. It doesn&#8217;t show which docs changed or should have changed. It doesn&#8217;t show which specs this conflicts with. It doesn&#8217;t show what changed during implementation compared to the plan.</p><p>So the reviewer has to reconstruct all of that.</p><p>Again, archaeology.</p><p><strong>What I want instead is an evidence pack.</strong> Not enterprise theatre. Not documents for the sake of documents. A practical package that makes the change reviewable.</p><p>Here is the intent. Here are the requirements. Here are the obligations. Here are the tests that witness them. Here are the docs. Here is the blast radius. Here is how it aligns with existing specs and where we checked for conflicts. Here is what changed during implementation. Here is what still needs human judgment.</p><p>Then the pull request isn&#8217;t only code. It&#8217;s the full chain of development.</p><p>This matters for open source. It matters for support engineers contributing fixes. It matters for other internal teams. It matters for AI agents. You don&#8217;t trust the contributor blindly. You don&#8217;t trust AI blindly. You trust the evidence chain, and then you still apply human judgment where judgment is needed.</p><p>This will feel slower at first. Writing obligations is slower than writing a vague ticket. Linking tests to requirements is slower than writing random tests. Updating docs through the graph is slower than pushing a change and hoping someone remembers. But not all friction is bad.</p><p>The question is whether the friction creates trust. </p><blockquote><p>Bureaucracy gives you friction without trust. <br>Evidence gives you friction that lets more people move safely.</p></blockquote><p>If this trust exists, then AI can actually help us scale. Not by dumping more pull requests into the same review bottleneck, but by making more changes reviewable, traceable, and safe to absorb in parallel.</p><p>Without trust, maintainers become managers of incoming things. Instead of thinking about architecture, future, and vision, they review an endless stream of pull requests, fixes, and generated artifacts.</p><p><strong>That is not the scaling I want.</strong></p><h2>Why I am building Proof</h2><p>This is why I am building <a href="https://reqproof.com">Proof</a>.</p><p>I don&#8217;t want another tool whose main purpose is to create more code. We already have many of those. <strong>The problem isn&#8217;t that we can&#8217;t produce enough artifacts. The problem is that the artifacts don&#8217;t preserve intent.</strong></p><p>I want specs to stop being temporary. I want requirements to live with the software. I want obligations to force the boring questions before they become production bugs. I want code, tests, docs, and requirements to invalidate each other when they drift. I want a reviewer to see the evidence chain instead of rebuilding it from memory.</p><p>AI will make engineering faster. That part is already happening. But faster without trust is not enough.</p><p>For me, the real question is this: how can I end up in the position where it&#8217;s not just a pull request coming from someone from the outside, but a well-thought evidence pack that makes me believe I can merge it as soon as possible?</p><p>That is the scaling I care about.</p><p>Not just more code.</p><p>More trusted change.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.reqproof.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Verification Gap! Subscribe to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[I Had Near 100% Test Coverage. It Didn’t Matter.]]></title><description><![CDATA[You cannot test for what you never described.]]></description><link>https://blog.reqproof.com/p/i-had-near-100-test-coverage-it-didnt</link><guid isPermaLink="false">https://blog.reqproof.com/p/i-had-near-100-test-coverage-it-didnt</guid><dc:creator><![CDATA[Leonid Bugaev]]></dc:creator><pubDate>Wed, 29 Apr 2026 17:06:16 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7de09747-0619-4bc2-aca5-b1c0702247c4_1451x720.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I woke up and saw a wall of emails in my personal account. Then logged into my corporate Slack, and it was filled with Zendesk messages from customers. Everyone was looking for me.</p><p>The library I wrote, <a href="https://github.com/buger/jsonparser">jsonparser</a>, which got used by a lot of projects, got its very own public CVE. So everyone started freaking out looking at their scanners.</p><p>&#8220;That&#8217;s what the fame is,&#8221; was my first thought.</p><p>Now I remember some notifications I kept ignoring from the Google OSS Fuzz project, I signed up multiple years ago.</p><p>This lib was written in the pre-AI-agents era (so weird to say that now!). Every piece was handcrafted manually, using best practices, with full test coverage. </p><p>I checked the function which had the issue, and it literally had near 100% test coverage. But it did not matter, because the issue was in handling of malformed input data. One of the edge cases which was missed. In other words, the issue was in the specification of what this function should do and how it should behave in edge cases.</p><p>But it opened one more can of worms. I wrote this library like 6 years ago. I don&#8217;t remember anything. And my only source of truth is the code and the tests, which is rather cryptic and looks more like archaeology.</p><p>The issue is fixed now. But how do I prevent such issues happening in the future? And if 100% code coverage is not the answer, what is? And what is my source of truth?</p><p>So I started digging. And it went way deeper than I expected, and changed the way I look at software engineering forever.</p><h2><strong>Down the rabbit hole</strong></h2><p>I started thinking about what the gold standard of software quality is. My first answer was NASA. How does NASA solve these kinds of issues?</p><p>AI now produces so much code that I feel like I am losing ownership of it. Not only of the code. Of the intent.</p><p>I wanted to understand how people work when tests passing is still not enough and the price of being wrong is huge.</p><p>The surprising thing is that a lot of NASA&#8217;s work is public. Their software engineering requirements are public. FRET is public. Kind2 is public. A lot of the case studies are public. There are papers about aircraft, Mars rovers, superconducting magnets, and formal requirements that found bugs before code existed.</p><p>I started reading all of this not as an academic exercise, but because I had a very dumb practical problem: my tests were green, my coverage looked fine, and still one missed edge case was enough to create a public CVE.</p><p>Then I went deeper into automotive and aerospace. It opened a whole new world of software engineering for me. For some reason, our world of consumer software engineering and regulated software engineering in those industries almost do not intersect. Different tools, different conferences, different language. Sometimes it feels like they live in a parallel universe.</p><p>Some of it looks archaic. Some methodologies are weird.</p><p>Our engineering progressed a lot too. We got very good at moving fast and catching damage quickly. CI/CD, linters, tests, canaries, observability, rollbacks. I don&#8217;t want to pretend every SaaS product should behave like avionics certification.</p><p>But we optimized for speed. They optimized for evidence.</p><p>Their industry spends much more time asking what evidence they need before they are allowed to trust the change. Some of it is painful. Some of it is bureaucracy. But the idea underneath is not stupid: if you claim the system should behave in some way, you need a durable chain from that statement to tests, code, and evidence.</p><p>There is real proof there, but it is not the fantasy version I had in my head, where every line of every product is mathematically proven end-to-end. They prove specifications. They use model checking. They simulate models, like with Simulink, against many input/output cases. They measure structural coverage. They use formal proof where the criticality justifies it.</p><p>And they still use testing, code review, static analysis, and all the normal engineering work around it. The difference is that proof and evidence are attached to the parts where being wrong is not acceptable.</p><p>That actually made the idea useful for normal engineering.</p><p>This is a huge topic, which I will cover in future articles. But the first concrete thing I found was MC/DC. It is one of the ways safety-critical industries look at coverage, and it made standard line coverage look very weak to me.</p><p><strong>Line coverage says a line was touched at runtime. It does not say that the decision was tested.</strong></p><h2><strong>Why 90% line coverage can still mean 60% real coverage</strong></h2><p>I still use line coverage. I still look at it.</p><p>But line coverage is bullshit. You should not trust it. Not on its own.</p><p>In Go, when you run:</p><pre><code><code>go test -cover ./...</code></code></pre><p>you mostly get statement coverage. The tool tells you whether a statement executed during the test run. That&#8217;s useful. But it doesn&#8217;t tell you whether the decision was tested.</p><p>Take a tiny parser-style example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">func isDigit(c byte) bool {
&#9;return c &gt;= '0' &amp;&amp; c &lt;= '9'
}</code></pre></div><p>Now test it like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;go&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-go">func TestIsDigit(t *testing.T) {
&#9;if !isDigit('5') {
&#9;&#9;t.Fatal("5 should be a digit")
&#9;}
&#9;if isDigit('x') {
&#9;&#9;t.Fatal("x should not be a digit")
&#9;}
}</code></pre></div><p>Looks fine. The line ran. The function returned true once. The function returned false once. Your coverage report can look perfect.</p><p>But what did you actually prove?</p><p>You tested <code>'5'</code>. You tested <code>'x'</code>. You didn&#8217;t prove the lower boundary. You didn&#8217;t prove that <code>'/'</code> fails because it&#8217;s before <code>'0'</code>. You didn&#8217;t prove that <code>':'</code> fails because it&#8217;s after <code>'9'</code>.</p><p>The line is covered. The boundary is not.</p><p>MC/DC stands for Modified Condition/Decision Coverage. It asks the question line coverage does not ask: did each condition independently affect the outcome?</p><p>When your code says <code>if a &amp;&amp; b</code>, line coverage tells you the <code>if</code> was hit. MC/DC asks whether <code>a</code> alone can change the result, and whether <code>b</code> alone can change the result.</p><p>For this line:</p><pre><code><code>return c &gt;= '0' &amp;&amp; c &lt;= '9'</code></code></pre><p>there are two conditions:</p><pre><code><code>c &gt;= '0'
c &lt;= '9'</code></code></pre><p>A simplified MC/DC table looks like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!R0um!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!R0um!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png 424w, https://substackcdn.com/image/fetch/$s_!R0um!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png 848w, https://substackcdn.com/image/fetch/$s_!R0um!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png 1272w, https://substackcdn.com/image/fetch/$s_!R0um!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!R0um!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png" width="1270" height="332" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:332,&quot;width&quot;:1270,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:68532,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.reqproof.com/i/195788861?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!R0um!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png 424w, https://substackcdn.com/image/fetch/$s_!R0um!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png 848w, https://substackcdn.com/image/fetch/$s_!R0um!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png 1272w, https://substackcdn.com/image/fetch/$s_!R0um!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75668db6-dcc9-400f-b6ad-6cd14dc87f9a_1270x332.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The table is just a way to say: these are the cases that matter. This is the part ordinary coverage does not force you to say.</p><p>This used to be mostly a safety-critical tooling conversation. DO-178C requires MC/DC for the highest-criticality aviation software. The tooling was expensive, slow, and hard for normal teams to justify.</p><p>That changed. GCC 14 has <code>-fcondition-coverage</code>. Clang 18 has <code>-fcoverage-mcdc</code>. Rust is moving in the same direction with richer branch and condition coverage work, even if I would not call Rust MC/DC stable yet. Go does not have native MC/DC support, so I ended up adding code-level Go MC/DC measurement to <a href="https://reqproof.com/">Proof</a>, and we have been extending the same direction to JavaScript and TypeScript as well.</p><p>What aerospace and automotive had because they were slow and diligent is now becoming available to normal engineering teams because AI changed the economics. You don&#8217;t need a certification lab to ask a harder question about your tests. You also don&#8217;t need to apply all of this to the whole company on day one. Start with the part where wrong behavior actually hurts.</p><h2><strong>The jsonparser numbers weren&#8217;t subtle</strong></h2><p>After the CVE fix, I wanted to understand why my previous approach didn&#8217;t make this kind of missing behavior obvious enough.</p><p>So I applied the MC/DC and requirements approach to jsonparser in a later public PR: <a href="https://github.com/buger/jsonparser/pull/281">buger/jsonparser#281</a>.</p><p>Again: this PR didn&#8217;t fix the original CVE. It was the follow-up work after the CVE fix. But it was not just a paperwork exercise. The hardening pass found and fixed more real issues and removed dead code that my previous process had not made obvious.</p><p>That was the uncomfortable part for me. I started by asking: what did my tests actually prove?</p><p>On the main branch before that work, ordinary Go statement coverage was already decent:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Smuc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Smuc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png 424w, https://substackcdn.com/image/fetch/$s_!Smuc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png 848w, https://substackcdn.com/image/fetch/$s_!Smuc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png 1272w, https://substackcdn.com/image/fetch/$s_!Smuc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Smuc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png" width="1362" height="338" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:338,&quot;width&quot;:1362,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:75969,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.reqproof.com/i/195788861?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Smuc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png 424w, https://substackcdn.com/image/fetch/$s_!Smuc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png 848w, https://substackcdn.com/image/fetch/$s_!Smuc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png 1272w, https://substackcdn.com/image/fetch/$s_!Smuc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81b395e9-4e39-4a35-9da9-44514b5463b1_1362x338.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p></p><p>85.3% coverage isn&#8217;t bad. Most teams would see that and move on. But decision coverage told a different story: only 66% of decisions were fully covered, and only 69.2% of conditions were proven independently.</p><p>And the more interesting part: some functions already looked perfect by ordinary coverage.</p><p>Examples from the before state:</p><pre><code><code>parseInt                     100% statement coverage
Unescape                     100% statement coverage
decodeSingleUnicodeEscape    100% statement coverage
</code></code></pre><p>But MC/DC still found missing independent-condition evidence:</p><pre><code><code>bytes.go:21   parseInt missing proof for c &lt; '0'
escape.go:148 Unescape missing proof for len(in) &gt; 0
escape.go:47  decodeSingleUnicodeEscape missing proof for h1 == badHex
escape.go:47  decodeSingleUnicodeEscape missing proof for h2 == badHex
escape.go:47  decodeSingleUnicodeEscape missing proof for h3 == badHex
</code></code></pre><p>100% line coverage can still leave a condition unproven.</p><p>The code ran. The decision wasn&#8217;t tested.</p><h2><strong>The bug was in what I forgot to describe</strong></h2><p>Coverage does not paint the whole picture. Even MC/DC. The bug can still be in the spec.</p><p>That is what happened with jsonparser. It was a classical case: you are building something, moving forward, and not looking back. You don&#8217;t know what you don&#8217;t know. I did not think about what would happen if this edge case appeared. I think most of us do not think about it this way.</p><p>I did not have any specs driving development or anything that forced me to think about the edge cases before writing the code. So of course I did not test for them. You cannot test for what you never described.</p><p>Testing assumes the specification is correct. That is the NASA/formal-methods lesson that changed how I think about this. The hard part is not testing the implementation. The hard part is questioning the specification itself.</p><p>This is where I found two different questions that I had been mashing together.</p><p>The first question starts from my specification: if this is what I claim the system should do, which logical cases need to be witnessed?</p><p>Not the code. The intent.</p><p>NASA built an open-source tool called FRET (Formal Requirements Elicitation Tool) that lets you write requirements in structured English and translates them into formal logic.</p><p>FRET includes an algorithm called FLIP (FuLl Independence Pair). FLIP takes a formalized requirement and generates the minimum set of test cases proving each boolean variable independently affects the outcome. Not every possible combination. Just the ones that matter.</p><p>I still have to write the requirement. I still have to decide what malformed input, boundaries, errors, and edge cases mean. FLIP does not do that for me.</p><p>But once the requirement is formalized, FLIP tells me exactly which test cases that requirement needs.</p><p>I built a tool called <a href="https://reqproof.com/">Proof</a> that implements this approach.</p><p>That is the part I care about: how many tests are enough for this requirement?</p><p>Not &#8220;how many tests did I happen to write?&#8221; Enough for what I described.</p><p>The second question starts from my actual code: did my tests exercise every boolean condition in the implementation so each one independently affects the outcome?</p><p>This side does not care what I meant. It looks at what I wrote.</p><p>And sometimes it shows that my code has many more logical cases than my spec. So maybe my spec is not accurate enough.</p><p>Or my spec says this edge case matters, but my tests don&#8217;t witness it.</p><p>Or my tests cover implementation details, but the behavior is under-described.</p><p>I learned this the hard way on jsonparser. The spec side and the code side kept disagreeing in useful ways, and that is where code drift and spec drift become visible.</p><p>The gap goes in both directions. Sometimes the code is wrong. Sometimes the tests are weak. Sometimes the spec is too vague.</p><p>Sometimes all of it combined badly.</p><h2><strong>Checklists, not memory</strong></h2><p>What can be more deterministic than a checklist? In aerospace and automotive, everything has its own checklist. The price of a mistake is too high to rely on someone&#8217;s memory. I think checklists are the driving force behind quality engineering in those industries.</p><p>When you do not have specifications, it is very hard to create a checklist. When you are building a feature, you can have test cases, but that is a moving target. The items are constantly changing. You need something that will be the same all the time.</p><p>In this context, an obligation is not a test case. It is a category of behavior you are required to describe. Malformed input is an obligation. Boundary behavior is an obligation. Error handling is an obligation. For each one that applies to your requirement, you need at least one test case that proves how the system behaves in that category. The obligation does not tell you the answer. It forces you to ask the question.</p><p>You cannot rely on humans here. Even on me, to be frank. I can miss these items too. You need deterministic checklists.</p><p>In practice, the questions are very simple:</p><p>What will happen if this is malformed data? What will happen if this is slow and the request times out? What will happen if the database is down? What will happen if you have a very large object? What will happen if the function returns different values with the same inputs?</p><p>These are the cases where security issues and data bugs tend to live. For jsonparser, these are the exact cases I had not thought about.</p><p>Without obligations, edge cases depend on memory. Maybe I remember to test malformed data. Maybe the AI remembers. Maybe a reviewer notices. Maybe no one does.</p><p>At the moment, it is just a matter of whether someone forgets or not forgets to test it.</p><p>This is where the CVE fix actually changed how I work. The fix itself was mechanical. But the obligations I wrote afterward forced me to think about the cases I had skipped. Every one of those became an explicit question I had to answer. Not &#8220;did someone remember to test this?&#8221; but &#8220;here is the list, and each item needs a witness.&#8221;</p><p><strong>Obligations turn edge cases from &#8220;someone remembered to test this&#8221; into a deterministic process.</strong></p><p>When I first started writing obligations for jsonparser, it was actually quite easy with modern AI tooling. I reviewed all of the specs. The flow is: you cannot pass this check until the checklist is green, until you define obligations for all of those cases, and until you define test cases for all of those cases as well.</p><p>This is what the double link looks like in practice:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// In the code &#8212; annotated with the requirement it implements:
// SYS-REQ-863
func (s *Service) lookupCache(req Request) (*Result, bool) {
    // ...
}

// In the test &#8212; annotated with both the requirement AND the specific MC/DC row:
// Verifies: SYS-REQ-863
// MCDC SYS-REQ-863: cache_lookup_requested=T, component_inputs_unchanged=F,
//                    cached_component_result_reused=F =&gt; TRUE
func TestMCDC_SYS_REQ_863_Row1(t *testing.T) {
    evalVerifyScenario(t, "SYS-REQ-863", map[string]bool{
        "cache_lookup_requested":         true,
        "component_inputs_unchanged":     false,
        "cached_component_result_reused": false,
    }, true)
}</code></pre></div><p>Each test is not just &#8220;test the function.&#8221; Each test is: &#8220;prove that this specific variable independently affects the outcome of this specific requirement.&#8221;</p><p>If I change the spec, I can see exactly which MC/DC rows are affected and which tests need to be reviewed. If I change a test, I can see which spec requirement it was proving and check whether the spec still says the same thing. If I add a new variable to the requirement, FLIP will generate new witness rows, and the missing tests become immediately visible.</p><p>This is the double link. Change the spec, review the tests. Change the tests, review the spec. If you have not touched the spec, why would you touch the test?</p><p>This is where the &#8220;how many tests are enough?&#8221; question changed for me. Before, the answer was always vibes. Write enough tests. Cover important paths. Don&#8217;t overdo it. Be pragmatic.</p><p>All true, and also not very helpful.</p><p>Now I think about it differently. Enough tests means enough evidence that every condition I described, or every condition my code actually contains, can independently affect the behavior I care about.</p><p>It is not about how many tests I have. It is about whether I really, really trust my system and whether it actually does what I described.</p><h2><strong>The true challenge is legacy</strong></h2><p>You can always start a new project and have a really nice experience with all of this. But the true challenge lies in the big legacy projects. They make up like 90% of all software. They bring the majority of the money. And they are the ones where wrong behavior actually hurts.</p><p>I work with very complex software. At Tyk, we build API gateway software used by banks, governments, and other serious enterprise customers. I am a very sceptical person. I always want some proof. At the same time, I understand that software is always about compromises.</p><p>But the game is changing. What was not possible in the past is now possible for small teams in terms of quality and processes. The wind is changing with AI.</p><p>The true power happens when you can apply some of those approaches to legacy large enterprise codebases. If it works there, it will work everywhere.</p><p>I know how challenging it is. You cannot do it in one go. You cannot just make a switch and start using a new process.</p><p>This is not only about the technical part. It is also about the people part. Even at the size of Tyk, with like a hundred people, it is not about the implementation. It is about the processes and the people. The technical part is the easiest one.</p><p>In order to convince people that you can actually make it, you need to be able to do it in parts. Start small, then scale.</p><p>Can you take small parts, turn them into a repeatable process, and then start scaling? That is how it works in the majority of cases.</p><p>So I picked the policy engine. Authorization and gateway policy decisions are obviously critical. If the policy engine behaves incorrectly, you are not talking about a cosmetic bug.</p><p>I applied the same kind of thinking to the Tyk policy package in a public PR: <a href="https://github.com/TykTechnologies/tyk/pull/7932">TykTechnologies/tyk#7932</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LEmi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LEmi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png 424w, https://substackcdn.com/image/fetch/$s_!LEmi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png 848w, https://substackcdn.com/image/fetch/$s_!LEmi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png 1272w, https://substackcdn.com/image/fetch/$s_!LEmi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LEmi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png" width="1362" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1362,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:72886,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.reqproof.com/i/195788861?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LEmi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png 424w, https://substackcdn.com/image/fetch/$s_!LEmi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png 848w, https://substackcdn.com/image/fetch/$s_!LEmi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png 1272w, https://substackcdn.com/image/fetch/$s_!LEmi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab79f2d-481f-4368-b891-eb8af99b9a29_1362x334.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p></p><p>81% ordinary coverage. 64.3% decision coverage. The normal coverage number says most statements ran. The MC/DC number says a lot of policy decisions still do not have independent evidence.</p><p>For a policy engine, the second number is the one I care about.</p><h2><strong>Code coverage is not about a metric</strong></h2><p>It is about trust.</p><p>What do we trust? In classical software engineering, we say: here is the code and here are the tests, the tests are the source of truth. If you want to know how the system works, read the tests.</p><p>I do not believe that anymore. Not with AI writing code. Not with AI writing tests. Not with AI validating its own assumptions.</p><p><strong>The source of truth cannot just be tests anymore. AI can write those too.</strong></p><p>A passing test can prove that the code agrees with the test. It cannot prove that both agree with my intent.</p><p>So I moved the source of truth up. For me, it has to be the specification: the static description of what I expect the system to do.</p><p>Then code implements it. Tests witness it. Coverage measures evidence around it. Traceability keeps the chain from silently rotting.</p><p>I started this whole journey because of one CVE in a library I wrote six years ago. I ended up in a completely different place.</p><p>I thought the problem was in the code. It was in what I forgot to describe.</p><p>I thought coverage was the answer. It was the wrong question.</p><p>The <a href="https://blog.reqproof.com/p/ai-writes-your-code-nobody-verifies">first article</a> was about losing intent. This one is about binding intent back to code.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.reqproof.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Verification Gap! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[AI Writes Your Code. Nobody Verifies the Intent.]]></title><description><![CDATA[AI made implementation faster, but it did not solve trust. In both solo projects and regulated enterprise systems, the real bottleneck is still verification of intent.]]></description><link>https://blog.reqproof.com/p/ai-writes-your-code-nobody-verifies</link><guid isPermaLink="false">https://blog.reqproof.com/p/ai-writes-your-code-nobody-verifies</guid><dc:creator><![CDATA[Leonid Bugaev]]></dc:creator><pubDate>Thu, 23 Apr 2026 15:09:01 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f7ca47e8-c7d7-48e0-80bd-7205cc410018_1451x720.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I live in two different worlds now.</p><p>In one, AI made me more productive than I have ever been.</p><p>I have written more software in the last two years than across the rest of my career. I have barely written any code manually in the last year.</p><p>That part is real.</p><p>The speed boost is real.</p><p>The weird part is what came with it.</p><p>AI helps me ship more.</p><p>But it also asks me to trust more.</p><p>That is the uncomfortable part.</p><p>I am not just delegating typing.</p><p>I am delegating thinking, validation, and judgment too.</p><p>And I am still not sure where the safe line is.</p><p>In the other world, I lead engineering for software used by banks, governments, and other regulated environments, where mistakes are expensive and confidence matters more than speed.</p><p>And if you ask whether AI made us ship features 2x faster there, the honest answer is no.</p><p>Not even close.</p><p>That does not mean AI was useless.</p><p>It helped somewhere else.</p><p>It reduced noise.</p><p>A lot of engineering time in a big system does not go into writing the feature. It goes into interruption-based work: support engineers trying to understand how a feature behaves, PMs trying to figure out whether something is a bug or intended behavior, solution architects pulling in senior engineers just to inspect a corner of the system.</p><p>Tools that let people talk to the codebase, inspect it safely, and even generate tests or benchmarks to validate a hypothesis helped a lot with that.</p><p>People were less interrupted.</p><p>Context switching got better.</p><p>Engineers were happier.</p><p>But the main bottleneck did not move.</p><p>Implementation got dramatically faster. Trust did not.</p><p>That is the wall I keep hitting in both worlds.</p><h2><strong>The part people keep smoothing over</strong></h2><p>The industry keeps talking as if faster code generation automatically means faster engineering.</p><p>It does not.</p><p>In a lot of teams, it just means mistakes can scale faster than judgment.</p><p>As an individual engineer, I can create software much faster than before. Good software too. Clean structure. Tests. Refactors. Nice terminal output.</p><p>And still I trust it less than I want to.</p><p>Maybe less than before, because I know how much invisible reasoning I no longer fully own.</p><p>As a Head of Engineering, I can see the same problem from the other side.</p><p>We can accelerate some parts of the flow.</p><p>But we still have to verify whether the thing we built is actually the right thing, and whether it behaves correctly in the bigger system.</p><p>In a complex product, implementation is a relatively small slice of the work.</p><p>Validation and verification are the bigger slice.</p><p>That is why I keep coming back to the same phrase:</p><p><strong>verification gap</strong></p><p>The verification gap is the distance between what I mean and what I can actually prove.</p><p>Between intended behavior and demonstrated behavior.</p><p>That gap always existed.</p><p>AI did not invent it.</p><p>It just made it wider, faster, and easier to ignore until production forces the issue.</p><h2><strong>Why this got worse with AI</strong></h2><p>When humans wrote the code, the same brain often held the intent, the implementation, and the validation loop together.</p><p>Not perfectly.</p><p>People still shipped bugs. Specs were incomplete. Tests missed things.</p><p>But there was at least one place where the system could be understood as a whole: the person writing it.</p><p>That is no longer the default.</p><p>Now the human writes the prompt.</p><p>The model writes the code.</p><p>The model writes the tests.</p><p>The human skims the diff.</p><p>The model writes the cleanup.</p><p>The CI passes.</p><p>The feature ships.</p><p>And if the original intent was slightly wrong, incomplete, or misunderstood, that mistake does not stay in one place anymore.</p><p>It gets propagated through the whole stack.</p><ul><li><p>The plan is based on the wrong assumption.</p></li><li><p>The implementation is based on the wrong assumption.</p></li><li><p>The tests are based on the wrong assumption.</p></li><li><p>The &#8220;manual validation&#8221; is often you asking the same model to sanity-check itself.</p></li></ul><p>And then you look at the whole thing and it feels solid.</p><p>But it is solid on top of the wrong assumption.</p><p>So what exactly are we proving at that point?</p><p>That the system is internally consistent with the assumption it invented for itself.</p><p>Not that it matches your intent.</p><p>That is why so much AI productivity discourse feels fake to me.</p><p>A lot of teams did not automate engineering.</p><p>They automated typing.</p><p>That difference matters more than most people want to admit.</p><h2><strong>Bug free is not the same as intent-correct</strong></h2><p>People keep saying: just write better tests.</p><p>I do write tests.</p><p>AI writes tests for me too.</p><p>That is not the point.</p><p>Tests verify behavior for cases somebody thought of.</p><p>That somebody used to be a human.</p><p>Now it is often a human plus a model.</p><p>That is still not the same thing as verifying intent.</p><p>You can have 100% line coverage and still completely miss the thing that matters.</p><p>You can have a green CI run and still not know whether the software behaves the way you intended.</p><p>You can even have bug-free code in a narrow sense and still have software that is wrong.</p><p>A green pipeline can still be a polished misunderstanding.</p><p>That is one of the biggest traps in the current AI coding wave.</p><p>We are getting very good at generating artifacts.</p><p>Code.</p><p>Tests.</p><p>Docs.</p><p>Migration scripts.</p><p>Benchmarks.</p><p>RFC drafts.</p><p>None of that answers the deeper question:</p><p>does the system actually do what we mean?</p><h2><strong>Software is not flat. It is layers.</strong></h2><p>The problem gets worse as the software gets bigger.</p><p>Software is not flat.</p><p>It is layers.</p><p>It is wide, deep, and full of interacting components, hidden assumptions, backwards compatibility constraints, old decisions nobody remembers, and behavior that only makes sense if you know four other subsystems.</p><p>Any project that lives long enough eventually reaches a point where one brain is no longer enough.</p><p>That was true before AI.</p><p>It is still true now.</p><p>AI does not remove that limit.</p><p>In some cases it makes you hit it faster, because you can generate change faster than you can understand its consequences.</p><p>That is why the industry created all the layers around engineering in the first place:</p><ul><li><p>CI/CD</p></li><li><p>QA</p></li><li><p>RFCs</p></li><li><p>Architecture reviews</p></li><li><p>Team ownership boundaries</p></li><li><p>Support escalation paths</p></li><li><p>Approval workflows</p></li></ul><p>These are not random rituals.</p><p>They are patches over the same underlying problem:</p><p>software complexity grows beyond what one brain can safely manage.</p><h2><strong>Where does intent live now?</strong></h2><p>I think mainstream software engineering is still missing something fundamental.</p><p>We do not maintain a real source of truth for intent.</p><p>If I ask where the intended behavior of a system lives right now, the honest answer in most teams is:</p><p>all of it combined badly.</p><p>Some of it is in source code.</p><p>Some of it is in tests.</p><p>Some of it is in RFCs.</p><p>Some of it is in Jira tickets.</p><p>Some of it is in Confluence.</p><p>Some of it is in the heads of senior engineers.</p><p>None of those is the place where I can go and see, clearly, how the system is supposed to behave right now.</p><p>That is not a source of truth.</p><p>That is archaeology.</p><p>And that feels like a drastic difference from fields like aerospace or automotive.</p><p>They have their own fragmentation problems too. Different groups write requirements, validate them, implement them, monitor them. Those worlds often barely talk to each other.</p><p>But at least intended behavior is treated as a first-class artifact.</p><p>There is an SRS.</p><p>There are explicit requirements.</p><p>There is a recognized place where intent is supposed to live.</p><p>In mainstream software, especially for something complex like an API gateway, that still feels almost unimaginable.</p><p>We mostly reconstruct intent after the fact from scattered artifacts.</p><p>And then we act surprised when regressions keep happening.</p><h2><strong>Why enterprise teams do not get the full AI payoff</strong></h2><p>This is also why the conversation about AI productivity is often too shallow.</p><p>Yes, implementation is faster.</p><p>Sometimes dramatically faster.</p><p>But if speed of implementation is no longer the hard part, then what is?</p><p>That is the real question.</p><p>If a feature can be implemented in hours instead of weeks, why have so many teams not seen the full payoff?</p><p>Because implementation was never the only bottleneck.</p><p>The harder part is deciding what should be built, making that intent explicit enough, and then verifying that the resulting system still matches it after the code, tests, and surrounding context have all changed.</p><p>That is where the time goes.</p><p>That is also where a lot of current AI hype becomes unserious.</p><p>People showcase how fast a model can produce code.</p><p>Fine.</p><p>Show me how fast your team can decide what is correct, verify that the behavior matches the intent, and avoid turning six months of hyperproductivity into twelve months of regression cleanup.</p><p>At work, we effectively built a zero-trust environment.</p><p>We do not blindly trust humans.</p><p>We do not blindly trust AI.</p><p>We review the code.</p><p>We validate the assumptions.</p><p>We check the tests.</p><p>That posture protected quality when AI adoption accelerated.</p><p>But it also meant we did not suddenly become 10x faster.</p><p>We became less noisy.</p><p>More focused.</p><p>Better at answering questions.</p><p>Faster in implementation.</p><p>Still constrained by verification.</p><h2><strong>Not everyone needs safety. Everyone needs trust.</strong></h2><p>As an individual engineer, the same tension shows up in a different shape.</p><p>I can move incredibly fast.</p><p>But I know that if I let trust slide too far, I eventually stop building and start doing bug fixing and regression management full-time.</p><p>The software turns into glue and patches.</p><p>You can feel your taste slipping if you are not careful.</p><p>It all kind of works, but you are no longer fully sure why.</p><p>Safety bar differs. Obviously.</p><p>A bank flow is not the same thing as a weekend prototype.</p><p>One component inside a product may deserve a much stricter baseline than another.</p><p>But trust? Everyone needs that.</p><p>If I built a website, a product, a service, an internal tool, whatever it is, I need to trust that it actually follows my intent closely enough for the context it lives in.</p><p>That is the standard I care about.</p><p>Not some abstract perfection.</p><p>Not a fantasy of zero bugs.</p><p>Not a productivity screenshot.</p><p>Trust.</p><p>Can I tell how my software behaves right now?</p><p>Do my docs, specs, tests, and code align with each other?</p><p>Do I know which parts are intentional, which parts are accidental, and which parts are cargo cult left over from earlier decisions?</p><p>When I change something, am I making the system better, or just shifting uncertainty around?</p><p>So what is engineering now, exactly?</p><p>Where is the place of the human?</p><p>Where is the place of judgment?</p><p>And which part should I never offload, even if AI is very good at pretending it can carry it for me?</p><p>Those were already hard questions before AI.</p><p>AI did not create them.</p><p>It amplified them.</p><p>It exposed how incomplete our current software practices already were.</p><h2><strong>Why I am writing this</strong></h2><p>That is why I do not think a smarter model or a shinier coding assistant will solve this by itself.</p><p>The missing layer is verification.</p><p>Not just whether the code runs.</p><p>Not just whether the tests pass.</p><p>Not just whether the reviewer approved.</p><p>I mean verification of intent.</p><p>That is what I have been thinking about for a long time now, and why I am starting this newsletter.</p><p>I want to write about the gap itself, what causes it, why it compounds, why mainstream software and regulated engineering barely learn from each other, and what it would take to close it.</p><p>Not with slogans.</p><p>With examples, systems, failures, tools, and uncomfortable questions.</p><p>AI did not remove the hard part of engineering.</p><p>It moved it from writing to verification.</p><p>If this problem feels familiar, subscribe.</p><p>This is what I am writing about now.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.reqproof.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Verification Gap! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>