<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Revin</title>
    <description>The latest articles on DEV Community by Revin (@revinsoftware).</description>
    <link>https://dev.to/revinsoftware</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4015647%2F4c05d90d-38b0-4993-8f2b-2eac6bbdbff5.png</url>
      <title>DEV Community: Revin</title>
      <link>https://dev.to/revinsoftware</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/revinsoftware"/>
    <language>en</language>
    <item>
      <title>An OpenAI model went looking for exposed API keys in testing. I assume my coding agent will too</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:42:10 +0000</pubDate>
      <link>https://dev.to/revinsoftware/an-openai-model-went-looking-for-exposed-api-keys-in-testing-i-assume-my-coding-agent-will-too-3k8o</link>
      <guid>https://dev.to/revinsoftware/an-openai-model-went-looking-for-exposed-api-keys-in-testing-i-assume-my-coding-agent-will-too-3k8o</guid>
      <description>&lt;p&gt;CIO reported today that OpenAI published six new reports on model misalignment under a new reporting framework. The cases come from internal evaluations and describe models acting beyond the constraints they were given: hidden instructions, unauthorized communication with external services, modified intermediate outputs, shared environments used in ways nobody planned. One of them is an attempt to locate exposed API keys. OpenAI called the behavior "unexpected or concerning".&lt;/p&gt;

&lt;p&gt;Most reactions treat this as news about OpenAI. I read it as a spec sheet for the thing running in my terminal. A coding agent with shell access has the same shape as the models in those reports: a goal, a set of tools and a filesystem. If a key sits within reach and the task gets easier with it, using the key is just another path to finishing the task. No intent is required for that to happen.&lt;/p&gt;

&lt;p&gt;So I stopped asking whether my agent would open &lt;code&gt;.env&lt;/code&gt;. I assume it will, and I set things up so that opening it finds nothing worth having.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: a contractor on day one
&lt;/h2&gt;

&lt;p&gt;A new contractor gets the access their task needs, for as long as the task lasts, and someone keeps a record of what they touched. Nobody hands them the master key because they seem careful. That is the frame I use for agents, and it breaks down into four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What can it read right now?&lt;/li&gt;
&lt;li&gt;Where do the secrets live, and can they leave the workspace?&lt;/li&gt;
&lt;li&gt;Which token does it hold, and what can that token do?&lt;/li&gt;
&lt;li&gt;Is there a record of what it ran?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Find what the agent can read
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt; protects your git history. It does nothing against a process that opens files, and the working directory is exactly where the &lt;code&gt;.env&lt;/code&gt; lives.&lt;/p&gt;

&lt;p&gt;Start with the files git ignores but that are sitting on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ignored by git, readable by anything running in this directory&lt;/span&gt;
git ls-files &lt;span class="nt"&gt;--others&lt;/span&gt; &lt;span class="nt"&gt;--ignored&lt;/span&gt; &lt;span class="nt"&gt;--exclude-standard&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'\.env|secret|credential|\.pem$|\.key$'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then look at what the agent inherits without reading any file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# names only, never print the values&lt;/span&gt;
&lt;span class="nb"&gt;env&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'key|token|secret|password'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command matters more than people expect. Agents usually inherit the shell you launched them from. If you exported &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; or &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; in your profile months ago, the agent has it already.&lt;/p&gt;

&lt;p&gt;Last, scan contents, history included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitleaks &lt;span class="nb"&gt;dir&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--redact&lt;/span&gt; &lt;span class="nt"&gt;--no-banner&lt;/span&gt;
gitleaks git &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--redact&lt;/span&gt; &lt;span class="nt"&gt;--no-banner&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A key that was committed once and deleted later still lives in &lt;code&gt;git log -p&lt;/code&gt;. An agent asked to find out why something broke in March will read that history.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Move secrets out of the workspace
&lt;/h2&gt;

&lt;p&gt;The tempting first fix is telling the agent to ignore &lt;code&gt;.env&lt;/code&gt;, through its ignore file or a line in the instructions. That is a request. The models in the OpenAI reports were operating under instructions too.&lt;/p&gt;

&lt;p&gt;What holds is the secret not being on disk. Replace the file with references that a secret manager resolves at runtime. With the 1Password CLI, the template looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.tpl (safe to commit, safe for the agent to read)&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;op://dev/app-db/url
&lt;span class="nv"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;op://dev/stripe-test/secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;op run &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.tpl &lt;span class="nt"&gt;--&lt;/span&gt; npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can read &lt;code&gt;.env.tpl&lt;/code&gt; all day and it will see vault paths. Doppler, Infisical and the cloud secret managers do the same job with different syntax, so pick whichever your team already pays for.&lt;/p&gt;

&lt;p&gt;One catch I don't have a clean answer for: if the agent's shell can call &lt;code&gt;op run&lt;/code&gt; with your unlocked session, the values are one command away. I start the agent from a shell where the secret manager is not signed in and run the app from a different one. It is clunky, and I'd like to hear a better arrangement.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Its own token, with the smallest scope
&lt;/h2&gt;

&lt;p&gt;When the agent needs credentials, to push a branch or read an issue, it gets its own and never yours. On GitHub that means a fine-grained personal access token limited to one repository, contents read and write, pull requests write, nothing under administration, expiring in days instead of a year.&lt;/p&gt;

&lt;p&gt;Then run the agent where that token is the only thing in the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/work &lt;span class="nt"&gt;-w&lt;/span&gt; /work &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env-file&lt;/span&gt; agent.env &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; agent-net &lt;span class="se"&gt;\&lt;/span&gt;
  agent-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agent.env&lt;/code&gt; has exactly one line. The container sees the repo and nothing from your home directory: no &lt;code&gt;~/.aws&lt;/code&gt;, no &lt;code&gt;~/.ssh&lt;/code&gt;, no shell history with a token someone pasted once. If you can route &lt;code&gt;agent-net&lt;/code&gt; through a proxy that logs hostnames, do it. Interacting with external services was one of the behaviors in the reports, and outbound traffic is where you would see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Log every command it runs
&lt;/h2&gt;

&lt;p&gt;When something odd happens, the first question is what the agent executed. Most agents now expose hooks around tool calls. In Claude Code, a &lt;code&gt;PreToolUse&lt;/code&gt; hook on &lt;code&gt;Bash&lt;/code&gt; receives the command as JSON on stdin, and exit code 2 blocks it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# ~/.claude/hooks/audit-bash.sh&lt;/span&gt;
&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.command // empty'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\t%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.agent-audit.log"&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'(\.env|id_rsa|\.aws/credentials)'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"blocked: command touches a secret path"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/hooks/audit-bash.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be honest about the block. It is a speed bump. &lt;code&gt;cat .e""nv&lt;/code&gt; walks right past that regex, and so does a Python one-liner. I keep it because it turns the obvious attempts into a line I will notice. The log is the part that earns its keep: a plain text file with timestamps that I can grep after a long session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would skip
&lt;/h2&gt;

&lt;p&gt;Two recommendations I keep seeing and don't think pay off. Asking the agent in the system prompt to never touch credentials costs nothing and guarantees nothing. The reports are, quite literally, about models acting past stated constraints. And rotating every key after every session sounds disciplined, but nobody keeps it up for more than a week. Short expiry and narrow scope do the same job without depending on someone remembering.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the reports don't settle
&lt;/h2&gt;

&lt;p&gt;These were OpenAI's internal evaluations, designed to surface this kind of behavior. I don't know how often a coding agent on an ordinary Tuesday task goes near a credentials file, and I would distrust anyone who gave me a precise number. What I do know is the price on each side: the setup above takes an afternoon, and being wrong means a production key that someone else finds before you do.&lt;/p&gt;

&lt;p&gt;Security as a habit looks boring up close. The file that isn't there, the token that expires, the log nobody reads until the day it matters.&lt;/p&gt;

&lt;p&gt;How are you keeping coding agents away from secrets today? Container, separate OS user, a remote dev box, or trust plus a good &lt;code&gt;.gitignore&lt;/code&gt;?&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I summed CPU time by user agent for 7 days: 63% went to clients that never load a second page</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Wed, 09 Sep 2026 15:00:57 +0000</pubDate>
      <link>https://dev.to/revinsoftware/i-summed-cpu-time-by-user-agent-for-7-days-63-went-to-clients-that-never-load-a-second-page-3dog</link>
      <guid>https://dev.to/revinsoftware/i-summed-cpu-time-by-user-agent-for-7-days-63-went-to-clients-that-never-load-a-second-page-3dog</guid>
      <description>&lt;p&gt;The invoice went up around 38% in one quarter and the meeting already had a culprit: the AI feature that shipped in April. Two more instances were on the table. Before approving them, I asked for something cheaper than a server, which was read access to seven days of access logs.&lt;/p&gt;

&lt;p&gt;The answer took an afternoon. About 63% of the summed backend time in that week went to clients that arrived with a Chrome user agent, requested only HTML, never fetched a single stylesheet, and never came back to the same address twice.&lt;/p&gt;

&lt;p&gt;Here is exactly what I ran, including the two things that did not work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your log format is probably missing the only field that matters
&lt;/h2&gt;

&lt;p&gt;Most nginx setups still run the default &lt;code&gt;combined&lt;/code&gt; format, which gives you status, bytes and user agent. It does not give you time. Without time you can only count hits, and hit count is the metric that hides this problem, because the cheap route wins on volume and the expensive one hides at the bottom of the list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;log_format&lt;/span&gt; &lt;span class="s"&gt;timed&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;
                 &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;$body_bytes_sent&lt;/span&gt; &lt;span class="nv"&gt;$request_time&lt;/span&gt; &lt;span class="nv"&gt;$upstream_response_time&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;
                 &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;$upstream_cache_status&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$http_user_agent&lt;/span&gt;&lt;span class="s"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/access.log&lt;/span&gt; &lt;span class="s"&gt;timed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One reload and you start collecting. Everything below assumes that format.&lt;/p&gt;

&lt;h2&gt;
  
  
  First pass: classify by user agent, and watch it fail
&lt;/h2&gt;

&lt;p&gt;The obvious first cut is by declared identity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="s1"&gt;'
{
  split($3, m, " ")
  ua = tolower($4)
  if (ua ~ /bot|crawl|spider|slurp/) k = "declared-bot"
  else if (ua ~ /mozilla\/5\.0/)     k = "browser-ua"
  else                               k = "other"
  hits[k]++; secs[k] += m[4]
}
END { for (k in hits) printf "%-13s %9d hits %11.1f s\n", k, hits[k], secs[k] }
'&lt;/span&gt; access.log-2026090&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;declared-bot     412918 hits     38140.6 s
browser-ua      9130477 hits    201773.4 s
other            286042 hits     11962.0 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that and you conclude the declared bots cost you about 15% of the time and the rest is customers. That conclusion is wrong, and it is the reason most teams stop here and buy the instance. A browser user agent is a string. Anyone can send it, and large scale collection stopped identifying itself a while ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second pass: classify by behaviour instead
&lt;/h2&gt;

&lt;p&gt;A browser that renders a page also asks for the CSS, the JS bundle, the fonts and a few images. A collector asks for the HTML and leaves. That difference is in the log already, and it does not require a new tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="s1"&gt;'
{
  ip = $1; sub(/ .*/, "", ip)
  split($2, r, " "); u = r[2]
  split($3, m, " ")
  if (u ~ /\.(css|js|woff2?|png|svg|jpg|ico)(\?|$)/) asset[ip]++; else page[ip]++
  secs[ip] += m[4]
}
END {
  for (i in page)
    if (page[i] &amp;gt; 50 &amp;amp;&amp;amp; asset[i] / page[i] &amp;lt; 0.1)
      printf "%-15s %7d pages %6d assets %9.1f s\n", i, page[i], asset[i], secs[i]
}
'&lt;/span&gt; access.log-2026090&lt;span class="k"&gt;*&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-k6&lt;/span&gt; &lt;span class="nt"&gt;-nr&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;198.51.100.24      41207 pages     18   14822.9 s
203.0.113.91       28644 pages      0   10310.4 s
198.51.100.77      19855 pages      3    7412.0 s
203.0.113.140      12038 pages      0    4488.7 s
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sum that bucket against the total and you get the number from the title. In this system it came out at 63.4% of all backend seconds in the week. None of those addresses appeared in more than a couple of hundred requests each, because the ranges rotate. There was no name to block, which is the part that makes the usual answer useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third pass: the twenty most expensive routes, by time and not by hits
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="s1"&gt;'
{
  split($2, r, " "); u = r[2]; sub(/\?.*/, "", u)
  split($3, m, " ")
  secs[u] += m[4]; n[u]++
  if (m[5] == "HIT") hit[u]++
}
END { for (u in secs) printf "%10.1f s %9d %6.1f%% %s\n", secs[u], n[u], 100 * hit[u] / n[u], u }
'&lt;/span&gt; access.log-2026090&lt;span class="k"&gt;*&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-nr&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   61402.8 s     67341    1.8% /reports/export
   38915.3 s    204882    0.4% /catalog
   22107.6 s     91120   11.2% /calendar/day
    9044.1 s   1980433   96.7% /api/session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route with 1.9 million hits was the cheapest thing on the list. The one costing the most ran 67 thousand times at roughly 900 ms each, with a cache hit rate under 2%, because every request carried a different date range in the query string. Every filter combination that became a public address is a page that is generated once, served once, and cached for nobody.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two things that did not work
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;robots.txt&lt;/code&gt;. Whoever respects it was already respecting it, and the traffic in that top bucket does not read it. Adding rules there changed nothing measurable in the following week.&lt;/li&gt;
&lt;li&gt;Blocking inside the application. The first attempt was a middleware that inspected the request and returned 403. In that stack the session middleware ran first, so by the time the rule said no, the request had already checked out a database connection. A 403 from the app measured around 34 ms and held a pool slot. The same 403 from nginx measured 0.4 ms. Saying no is not free, and where you say it is worth roughly two orders of magnitude.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the deny moved to the edge, per range rather than per address, since single IPs rotate too fast to matter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=perip:16m&lt;/span&gt; &lt;span class="s"&gt;rate=3r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/catalog&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;limit_req&lt;/span&gt;  &lt;span class="s"&gt;zone=perip&lt;/span&gt; &lt;span class="s"&gt;burst=10&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;limit_req_status&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Honest limit on that one: rate limiting by range punishes offices behind NAT, and I have no clean answer for telling those two apart at the edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moved the number
&lt;/h2&gt;

&lt;p&gt;The expensive route got a cheap variant for anything that had not requested an asset in the same connection window: same content, none of the aggregation queries that build the side panels. Query count per render went from 41 to 3. The cache key dropped the tracking parameters and the TTL went up on pages born from parameter combinations, which changed little between requests anyway. Filter combinations left the index, with canonical pointing at the unfiltered version. Two weeks later, summed upstream time was down about 44% with no new instance, and p95 improved on the routes humans actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is not a niche problem, and it is worse on public code hosting
&lt;/h2&gt;

&lt;p&gt;While I was writing this up, Konstantin Ryabitsev, who runs kernel.org infrastructure, reported that git.kernel.org burns more CPU rendering commits as HTML for scrapers than it spends on every other kind of legitimate access combined, git clones included. Across five geo distributed nodes, fourteen cores are doing nothing else at any given moment. Simon Willison published the account and it landed on Hacker News.&lt;/p&gt;

&lt;p&gt;A git web frontend is the extreme version of the shape: commits times files times views, diff, blame, raw, tree, an address space that grows by multiplication. A person opens the same handful of those. A crawler opens all of them, once each, which defeats caching by design, because for every address the crawler request is both the first and the last.&lt;/p&gt;

&lt;p&gt;One thing the log does not answer. It will not tell you whether the collector eating your CPU feeds something that sends you users. Behaviour separates robots from humans, not wanted from unwanted, and that second call is not an infrastructure call.&lt;/p&gt;

&lt;p&gt;So, two questions for people who have been through this. How do you separate the crawlers you want from the ones you do not, when both arrive with a Chrome user agent from residential ranges? And has anyone found a rate limit granularity that survives corporate NAT without whitelisting by hand?&lt;/p&gt;

&lt;p&gt;Originally published on the Revin blog: &lt;a href="https://revin.com.br/en/blog/scraper-traffic-cloud-bill" rel="noopener noreferrer"&gt;https://revin.com.br/en/blog/scraper-traffic-cloud-bill&lt;/a&gt;&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>backend</category>
      <category>performance</category>
    </item>
    <item>
      <title>One copy change touched 12 files: I grepped the repo and found the same discount rule written 5 times</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Fri, 04 Sep 2026 13:37:59 +0000</pubDate>
      <link>https://dev.to/revinsoftware/one-copy-change-touched-12-files-i-grepped-the-repo-and-found-the-same-discount-rule-written-5-4fpn</link>
      <guid>https://dev.to/revinsoftware/one-copy-change-touched-12-files-i-grepped-the-repo-and-found-the-same-discount-rule-written-5-4fpn</guid>
      <description>&lt;p&gt;Someone gave me read access last month to a product built almost entirely with an AI assistant. Node, TypeScript, roughly 30k lines, six months old, paying customers. The question was not about architecture. It was much more practical: why does changing one sentence in an email take a week?&lt;/p&gt;

&lt;p&gt;The ticket behind the call was tiny. Reword the order confirmation email and fix the line that shows the applied discount. The diff that came back touched 12 files.&lt;/p&gt;

&lt;p&gt;I did not read the codebase. I ran one search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start from the number, not from the folder tree
&lt;/h2&gt;

&lt;p&gt;Business rules leave fingerprints, and the fingerprint is usually a literal: a threshold, a rate, a status string. Here it was a 15% discount above a 500 subtotal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--no-heading&lt;/span&gt; &lt;span class="s1"&gt;'\b500\b'&lt;/span&gt; src/ | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="c"&gt;# 19&lt;/span&gt;
rg &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;'0\.15|0\.85'&lt;/span&gt; src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/pricing/calculateTotal.ts
src/services/checkout.ts
src/emails/orderConfirmation.ts
src/reports/monthlyRevenue.ts
src/admin/orderPreview.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five files carrying the same rule. Four minutes of work, no context needed about the domain.&lt;/p&gt;

&lt;p&gt;Then I opened them side by side, and the copies were not identical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pricing/calculateTotal.ts&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// src/services/checkout.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;// src/reports/monthlyRevenue.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;discounted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At exactly 500 the checkout applies 75 off and the pricing module applies nothing. The report counts that order as undiscounted either way. One boundary, three answers.&lt;/p&gt;

&lt;p&gt;I asked how often that happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- 23&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;23 orders since launch got their total decided by whichever code path answered first. Nobody had opened a bug, because both numbers look plausible on a screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool I expected to catch this caught nothing
&lt;/h2&gt;

&lt;p&gt;My first move was the obvious one, and it failed. I ran a copy paste detector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx jscpd src &lt;span class="nt"&gt;--min-tokens&lt;/span&gt; 50 &lt;span class="nt"&gt;--reporters&lt;/span&gt; console
&lt;span class="c"&gt;# Clones found: 3&lt;/span&gt;
&lt;span class="c"&gt;# Duplicated lines: 1.4% (all inside __fixtures__)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1.4%, and every hit was test fixture noise. By that metric the repo looks clean.&lt;/p&gt;

&lt;p&gt;The reason took me a while to accept. Clone detectors are built for code that was copied. This code was never copied, it was regenerated. Someone opened a new session, described the discount again in slightly different words, and got a fresh implementation with a different variable name, a different comparison operator and a different shape. Token level similarity is low. Semantic duplication is total.&lt;/p&gt;

&lt;p&gt;That is the part I had not internalized before this repo. Old style duplication announces itself, because the two blocks read the same. Regenerated duplication hides, because each version reads like it was written by a different person on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git says how it got there
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%ad  %s'&lt;/span&gt; &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  src/pricing/calculateTotal.ts src/services/checkout.ts &lt;span class="se"&gt;\&lt;/span&gt;
  src/reports/monthlyRevenue.ts | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-03-12  feat: order total with tier discount
2026-03-29  feat: checkout summary
2026-04-17  feat: monthly revenue report
2026-05-02  fix: discount line on confirmation email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four weeks apart, one author, four separate sessions. Every commit was correct on the day it landed. None of them was wrong in isolation, and that is exactly why review never flagged anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ten minute check that says whether the tests are real
&lt;/h2&gt;

&lt;p&gt;Before touching anything I broke the rule on purpose. Changed the threshold from 500 to 5000 in the pricing module and ran the suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;span class="c"&gt;# Tests:       118 passed, 118 total&lt;/span&gt;
&lt;span class="c"&gt;# Time:        11.4 s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;118 green tests while pricing is wrong. Coverage was reported at 71%, so the number on the badge was fine. The tests exercised the functions, they just never asserted on the money.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did instead of proposing a rewrite
&lt;/h2&gt;

&lt;p&gt;A rewrite was the first thing suggested on the call, and it was the most expensive option on the table. Three cheaper steps went in first.&lt;/p&gt;

&lt;p&gt;One, a characterization test on the boundary, written before any refactor, pinning the behavior we decided was correct (&lt;code&gt;&amp;gt;=&lt;/code&gt;, discount applies at 500):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tier discount boundary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;499.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;500.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;75.0015&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;])(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subtotal %p gives discount %p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;discountFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeCloseTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two, one exported &lt;code&gt;discountFor&lt;/code&gt; in &lt;code&gt;src/pricing&lt;/code&gt;, then deleting the four copies one at a time, running the new test between each deletion.&lt;/p&gt;

&lt;p&gt;Three, a crude guard in CI so the sixth copy does not get generated next month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# ci/check-pricing-literals.sh&lt;/span&gt;
&lt;span class="nv"&gt;hits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;rg &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;--glob&lt;/span&gt; &lt;span class="s1"&gt;'!src/pricing/**'&lt;/span&gt; &lt;span class="nt"&gt;--glob&lt;/span&gt; &lt;span class="s1"&gt;'!**/*.test.ts'&lt;/span&gt; &lt;span class="s1"&gt;'0\.15|0\.85|\b500\b'&lt;/span&gt; src | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$hits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pricing literal outside src/pricing:"&lt;/span&gt;
  rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--glob&lt;/span&gt; &lt;span class="s1"&gt;'!src/pricing/**'&lt;/span&gt; &lt;span class="s1"&gt;'0\.15|0\.85|\b500\b'&lt;/span&gt; src
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is blunt and it produces false positives, which we handle with an allowlist file. It also caught two new attempts in the following weeks, both from fresh AI sessions that had no idea the rule already existed. The next copy change on that email touched 2 files instead of 12.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this does not apply
&lt;/h2&gt;

&lt;p&gt;If you are three weeks from finding out whether anyone wants the product, skip all of it. A prototype does not need a single source of truth for a discount, it needs proof that someone will pay. The moment there are real orders in a database and a boundary that decides money, five phrasings of the same rule stop being a style problem.&lt;/p&gt;

&lt;p&gt;I also do not know how this scales past one rule. Grepping literals works because pricing hides behind numbers. Rules expressed as prose in a status machine, or as a chain of booleans, do not leave that fingerprint, and I have no cheap check for those yet.&lt;/p&gt;

&lt;p&gt;So the honest question for whoever has been through this: what do you use to catch the same rule regenerated in different wording, when clone detection is blind to it? Structural patterns with ast-grep, embedding search over the codebase, architecture tests that fail when a module reaches for something it should not know about? I would rather learn a method than write more grep scripts.&lt;/p&gt;

&lt;p&gt;Originally published on the Revin blog: &lt;a href="https://revin.com.br/en/blog/building-with-ai-without-architecture" rel="noopener noreferrer"&gt;https://revin.com.br/en/blog/building-with-ai-without-architecture&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I stopped Postgres and /health kept returning 200 in 4 of 6 services</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Wed, 02 Sep 2026 15:06:43 +0000</pubDate>
      <link>https://dev.to/revinsoftware/i-stopped-postgres-and-health-kept-returning-200-in-4-of-6-services-8cj</link>
      <guid>https://dev.to/revinsoftware/i-stopped-postgres-and-health-kept-returning-200-in-4-of-6-services-8cj</guid>
      <description>&lt;p&gt;Last week I read that companies are putting AI agents to work maintaining mainframes and that confidence in the platform held up while modernization runs alongside (CIO Dive, on BMC's annual survey). My first reaction had nothing to do with agents. It was that a platform older than most of the people operating it can tell you what it did at 3:12am on a Tuesday, and several Node services I have opened this year cannot.&lt;/p&gt;

&lt;p&gt;So I stopped arguing about it in meetings and ran two boring tests on a codebase I had read access to. Six HTTP services, Node and TypeScript, Postgres, Redis, one external payments API. Both tests took an afternoon and neither needed anything beyond docker, curl, grep and jq.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 1: stop the database, then ask the service if it is healthy
&lt;/h2&gt;

&lt;p&gt;The health endpoint in four of the six services looked like this. Same shape, different indentation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;uptime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uptime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads nothing. It answers because the process is alive and the event loop got to it. Here is the run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose stop postgres
&lt;span class="go"&gt;[+] Stopping 1/1
 ✔ Container app-postgres  Stopped

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; localhost:3000/health
&lt;span class="go"&gt;200

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:3000/orders/8814 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 80
&lt;span class="go"&gt;{"statusCode":500,"message":"Internal server error"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four services returned 200 with the database down. One returned 503, and it was the only one whose author had wired an actual query into the check. The sixth had no health route at all, which is at least honest.&lt;/p&gt;

&lt;p&gt;The dashboard on the wall reads that endpoint every 30 seconds and draws availability at 99 point something. During the two minutes Postgres was stopped, the graph stayed green and every order request returned 500. The metric existed. The guarantee did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 2: rebuild one order from the logs
&lt;/h2&gt;

&lt;p&gt;Second test, and this is the one that changed my opinion about what legacy means. Pick an order that behaved oddly yesterday and reconstruct its path from the log file, without opening a psql session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; app-2026-08-31.log
&lt;span class="go"&gt;41823 app-2026-08-31.log

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 8814 app-2026-08-31.log
&lt;span class="go"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines for an order that went through six code paths. One was the access log line with the URL, the other was a stack trace. Everything in between was logged like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-08-31T14:02:12.114Z info  discount applied
2026-08-31T14:02:12.119Z warn  fallback rate used
2026-08-31T14:02:12.240Z info  order persisted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I counted 31 lines in that time window that plausibly belonged to the request. Twenty two of them carried no id of any kind, so "plausibly" was the best I could do. Under concurrency, that window contained three other orders. There is no way to tell which one used the fallback rate. Not a hard question, an unanswerable one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tried first and had to undo
&lt;/h2&gt;

&lt;p&gt;My first move was the obvious one: put the dependency checks inside &lt;code&gt;/health&lt;/code&gt;, the same route the Kubernetes liveness probe already pointed at. It worked for about a day. Then the payments API had a slow minute, three checks went past the probe timeout, and kubelet restarted pods that were serving traffic fine. I turned a downstream hiccup into a restart storm of my own making.&lt;/p&gt;

&lt;p&gt;The split that stuck: &lt;code&gt;/health&lt;/code&gt; answers for the process only and stays on liveness. &lt;code&gt;/ready&lt;/code&gt; answers for dependencies and goes on the readiness probe, with a per check timeout well under the probe timeout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHECK_TIMEOUT_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;CHECK_TIMEOUT_MS&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;race&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payments&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PAYMENTS_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/ping`&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;checks&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same run as before, with the database stopped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; localhost:3000/health
&lt;span class="go"&gt;200
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:3000/ready | jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'.checks[] | select(.ok == false)'&lt;/span&gt;
&lt;span class="go"&gt;{"name":"postgres","ok":false,"ms":802,"error":"timeout"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Giving the log lines an owner
&lt;/h2&gt;

&lt;p&gt;For the second problem I did not adopt a tracing stack. I added a request id in async local storage and a log function that always merges it in, so nobody has to remember to pass context down five call levels.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsyncLocalStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:async_hooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AsyncLocalStorage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// at the call site, the order id rides along with the value that matters&lt;/span&gt;
&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fallback rate used&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The replay of the same question, one command instead of an afternoon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'select(.orderId == "8814") | [.ts, .level, .msg, .rate // ""] | @tsv'&lt;/span&gt; app.log
&lt;span class="go"&gt;2026-09-01T11:40:02.101Z  info  order created
2026-09-01T11:40:02.118Z  info  discount applied      0.15
2026-09-01T11:40:02.121Z  warn  fallback rate used    0.09
2026-09-01T11:40:02.244Z  info  order persisted
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thirty one of thirty one lines now carry the id. The whole change was under 60 lines across the four services, plus the tedious part, which was rewriting roughly 90 log call sites that were passing strings built with template literals.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;The date of the first commit told me nothing. Two of these services are from 2019, two are from last year, and they failed the same two checks. The 1970s platform in that survey is auditable because someone paid to keep it that way for decades, with records, procedures and a named owner per change window. An agent reading its logs has something to read. Pointed at a service that logs "discount applied" with no id, any agent, human or otherwise, produces a confident guess about code that moves money.&lt;/p&gt;

&lt;p&gt;One thing I am still unsure about: whether the split between liveness and readiness is worth it for a single instance service with no orchestrator, where a failing dependency and a failing process end in the same restart either way.&lt;/p&gt;

&lt;p&gt;How do you draw the line on your readiness checks, and do you let a slow third party API mark you as not ready?&lt;/p&gt;

&lt;p&gt;Originally published on the &lt;a href="https://revin.com.br/en/blog/legacy-is-not-age-mainframe-ai-agents" rel="noopener noreferrer"&gt;Revin blog&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>1,043 branches in a pricing service: 34 changed last year, 9 disagree with the spreadsheet</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Mon, 31 Aug 2026 12:57:51 +0000</pubDate>
      <link>https://dev.to/revinsoftware/1043-branches-in-a-pricing-service-34-changed-last-year-9-disagree-with-the-spreadsheet-2g69</link>
      <guid>https://dev.to/revinsoftware/1043-branches-in-a-pricing-service-34-changed-last-year-9-disagree-with-the-spreadsheet-2g69</guid>
      <description>&lt;p&gt;Someone gave me read access to a pricing service last month with a question I could not answer by opinion: should we buy a rules engine? Node and TypeScript, roughly 40k lines, six files under &lt;code&gt;src/pricing&lt;/code&gt;, three developers in its history and one of them still around.&lt;/p&gt;

&lt;p&gt;The vendor deck said hundreds of rules. The team said thousands. Nobody had counted, and the count was never the interesting number anyway. What decides that purchase is how many of those rules move, how often, and who has to be in the room when they do.&lt;/p&gt;

&lt;p&gt;So I spent an afternoon measuring three things: how many branches exist, how many of them changed in the last twelve months, and how many of the same rules also live in the finance spreadsheet with a different value. The third number was the one that mattered, and it was the only one nobody had asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting branches, and the first attempt that lied to me
&lt;/h2&gt;

&lt;p&gt;My first pass was the lazy one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rg &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'\bif\s*\('&lt;/span&gt; src/pricing
&lt;span class="go"&gt;src/pricing/commission.ts:274
src/pricing/discount.ts:141
src/pricing/creditLimit.ts:98
src/pricing/tax.ts:87
src/pricing/legacyPartner.ts:61
src/pricing/shipping.ts:28
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;689 total, and wrong in both directions. It counts &lt;code&gt;if&lt;/code&gt; inside comments and string literals, and it misses every ternary and every &lt;code&gt;case&lt;/code&gt;. In this codebase the ternaries were not decoration, half the tier selection was written as chained &lt;code&gt;? :&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A parse gives an honest number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// count-branches.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SyntaxKind&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ts-morph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tsConfigFilePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tsconfig.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kinds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;SyntaxKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IfStatement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SyntaxKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConditionalExpression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SyntaxKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CaseClause&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSourceFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/pricing/**/*.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;kinds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDescendantsOfKind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFilePath&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node count-branches.js | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;span class="go"&gt;412 src/pricing/commission.ts
196 src/pricing/discount.ts
143 src/pricing/creditLimit.ts
121 src/pricing/tax.ts
116 src/pricing/legacyPartner.ts
 55 src/pricing/shipping.ts

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node count-branches.js | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{ s += $1 } END { print s }'&lt;/span&gt;
&lt;span class="go"&gt;1043
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1,043 decision points. Big enough to scare anyone in a planning meeting, and useless on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which of them actually moved
&lt;/h2&gt;

&lt;p&gt;File level granularity is crude, and it was enough to change the conversation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;git ls-files &lt;span class="s1"&gt;'src/pricing/*.ts'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="gp"&gt;    printf "%3d %s\n" "$&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;git log &lt;span class="nt"&gt;--since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'12 months ago'&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;span class="go"&gt;  done | sort -rn
 31 src/pricing/commission.ts
 12 src/pricing/discount.ts
  4 src/pricing/creditLimit.ts
  0 src/pricing/tax.ts
  0 src/pricing/legacyPartner.ts
  0 src/pricing/shipping.ts
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop the commits that only touched imports or formatting and 47 becomes 34 real rule changes in a year, 31 of them in one file. Three files with 292 branches between them had not been touched since 2021.&lt;/p&gt;

&lt;p&gt;For the hot file I narrowed it down with &lt;code&gt;git log -L&lt;/code&gt;, which follows a function across renames well enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git log &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s1"&gt;'/function calculateCommission/'&lt;/span&gt;,+60:src/pricing/commission.ts &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    --since='12 months ago' --format='%h %ad %an' --date=short | grep -c '^[0-9a-f]\{7\}'
19
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nineteen changes to one function in twelve months, and every one of them went through a ticket, a review and a deploy. That is the real argument for pulling something out of the code, and it applies to about 3% of the branches in that repo. The other 97% are fine where they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 0.125 nobody could explain
&lt;/h2&gt;

&lt;p&gt;While I was in there, I picked the strangest constant and asked git who put it there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git log &lt;span class="nt"&gt;-S&lt;/span&gt;&lt;span class="s1"&gt;'0.125'&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%h %ad %an %s'&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; src/pricing/commission.ts
&lt;span class="gp"&gt;a3f19c2 2019-11-08 &amp;lt;redacted&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fix commission &lt;span class="k"&gt;for &lt;/span&gt;partner channel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One commit, one line of message, author no longer at the company. The diff shows what happened. Nothing anywhere shows why the partner channel got half a point more, or whether that was meant to expire.&lt;/p&gt;

&lt;p&gt;Version control is the best record of what was done that most companies own, and a poor record of why it was done. A rules engine does not fix that either. It just moves the undocumented number to a different screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spreadsheet nobody put in the architecture diagram
&lt;/h2&gt;

&lt;p&gt;Then I asked for the workbook finance uses to close the month. Formula cells are business rules running in production without tests, history or an owner, so I counted them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# formulas.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openpyxl&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_workbook&lt;/span&gt;

&lt;span class="n"&gt;wb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_workbook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;month-close.xlsx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0\.\d+|\d+(?:\.\d+)?%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;wb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worksheets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_rows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python formulas.py | &lt;span class="nb"&gt;tee &lt;/span&gt;sheet-rates.txt | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-4&lt;/span&gt;
&lt;span class="gp"&gt;Commissions D14 ['0.125'] =IF(C14&amp;gt;&lt;/span&gt;45000,B14&lt;span class="k"&gt;*&lt;/span&gt;0.125,B14&lt;span class="k"&gt;*&lt;/span&gt;0.1&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;Commissions D15 ['0.125'] =IF(C15&amp;gt;&lt;/span&gt;45000,B15&lt;span class="k"&gt;*&lt;/span&gt;0.125,B15&lt;span class="k"&gt;*&lt;/span&gt;0.1&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;Discounts   H8  ['0.08']  =IF(AND(F8&amp;gt;&lt;/span&gt;50,G8&amp;lt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"distributor"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;,E8&lt;span class="k"&gt;*&lt;/span&gt;0.08,E8&lt;span class="k"&gt;*&lt;/span&gt;0.05&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;Discounts   H9  ['0.05']  =IF(AND(F9&amp;gt;&lt;/span&gt;50,G9&amp;lt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"distributor"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;,E9&lt;span class="k"&gt;*&lt;/span&gt;0.08,E9&lt;span class="k"&gt;*&lt;/span&gt;0.05&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python formulas.py | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;148
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python formulas.py | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;'[^]]*&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;148 formula cells, 23 distinct rates. I pulled the numeric literals out of the six pricing files with the same AST script and compared the two lists. Fourteen matched. Nine did not.&lt;/p&gt;

&lt;p&gt;The first line of that output is the expensive one. The service pays the higher commission tier above 50,000 and the spreadsheet pays it above 45,000. Same rate, different threshold. Invoices come out of the system, commissions get paid from the sheet, and the gap only shows up when a rep notices their own number.&lt;/p&gt;

&lt;p&gt;That comparison took about forty minutes to write and it is the measurement I would run first if I did this again, before counting a single branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The table that turned into an interpreter
&lt;/h2&gt;

&lt;p&gt;The obvious move is to pull the rates out of the code and into data. It works, right up to the point where it does not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;commission_rule&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="nb"&gt;serial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;channel&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;min_amount&lt;/span&gt;    &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;max_amount&lt;/span&gt;    &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rate&lt;/span&gt;          &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;effective_from&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;owner&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tiers, caps and thresholds fit here without complaint, and most requests from the business are exactly that: change a number. Then I hit this one, which exists in that codebase almost word for word:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 8% off above 50 units, except distributors, who get 5%,&lt;/span&gt;
&lt;span class="c1"&gt;// unless the contract predates 2023&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;units&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;distributor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;contractSignedBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2023-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To express that in columns I added &lt;code&gt;customer_type&lt;/code&gt;, then &lt;code&gt;contract_signed_before&lt;/code&gt;, then a &lt;code&gt;priority&lt;/code&gt; column, then a first match wins evaluator to read the priority. Two hours in, I was writing a rules engine inside the product, with no debugger, no tests and exactly one person who would ever understand it. I threw it away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the line ended up
&lt;/h2&gt;

&lt;p&gt;Numbers go to the table, with an owner and an effective date. Chained conditions stay in the code, with a test whose name spells out the business case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;distributor keeps 5% above 50 units when the contract predates 2023&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customerType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;distributor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contractSignedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2022-04-19&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;discountRateFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;23 constants left the code. The nine mismatches got reconciled against the sheet, which was a finance conversation and not an engineering one. The chained rules stayed exactly where they were, and nobody has asked to change them since.&lt;/p&gt;

&lt;p&gt;On the original question: 34 changes a year, concentrated in one file, with no separate business function operating the rules and no auditor asking who changed what. Buying an engine there would have added an environment to run, test and roll back, in exchange for removing a deploy that happens about three times a month.&lt;/p&gt;

&lt;p&gt;Two things I still do not have a good answer for. The AST comparison between spreadsheet constants and code constants was a throwaway script, and I would like it running in CI, but matching a formula to the function that implements it is fuzzy work and I stopped at eyeballing 23 lines. And I have no idea where this line sits for a team of two with twenty rules, where the whole exercise probably costs more than the problem.&lt;/p&gt;

&lt;p&gt;How do you keep rate constants from forking between the system and whatever the business uses to close the month? Has anyone automated that diff in a way that survives a column being renamed?&lt;/p&gt;

&lt;p&gt;Originally published on the &lt;a href="https://revin.com.br/en/blog/thousands-if-then-else-rules-code-engine-spreadsheet" rel="noopener noreferrer"&gt;Revin blog&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I counted scope changes with gh and jq: 41 of 118 issues arrived after the freeze, 3 got a new date</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Fri, 28 Aug 2026 13:22:47 +0000</pubDate>
      <link>https://dev.to/revinsoftware/i-counted-scope-changes-with-gh-and-jq-41-of-118-issues-arrived-after-the-freeze-3-got-a-new-date-3n19</link>
      <guid>https://dev.to/revinsoftware/i-counted-scope-changes-with-gh-and-jq-41-of-118-issues-arrived-after-the-freeze-3-got-a-new-date-3n19</guid>
      <description>&lt;p&gt;A delivery I estimated at three weeks landed closer to seven. The code the ticket actually described was finished in the first week, more or less on time. The other four weeks went somewhere, and for a while the only account I had of them was a feeling.&lt;/p&gt;

&lt;p&gt;Before I wrote software for a living I signed off on steel structures: cement plants, one racetrack. Sites run late too, sometimes badly. What a site has that my repository did not is the monthly measurement: a document listing what went up, what did not go up and why, signed by both sides long before the schedule blows. The delay gets assembled in pieces. In my project the delay showed up finished, in one meeting, at the end.&lt;/p&gt;

&lt;p&gt;A 2012 question on Software Engineering Stack Exchange asking why IT cannot deliver large projects quickly like other industries has 123,583 views, a score of 543 and 31 answers. Most of them talk about estimation and essential complexity. I wanted a number instead of an opinion, so I went looking for the four missing weeks in data I already had: the issue tracker.&lt;/p&gt;

&lt;h2&gt;
  
  
  First attempt: a label nobody applied
&lt;/h2&gt;

&lt;p&gt;At kickoff we agreed on a label called &lt;code&gt;scope-change&lt;/code&gt;. Anything requested after the scope was agreed would carry it. Simple, free, and it failed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh issue list &lt;span class="nt"&gt;--milestone&lt;/span&gt; &lt;span class="s2"&gt;"Release 2"&lt;/span&gt; &lt;span class="nt"&gt;--state&lt;/span&gt; all &lt;span class="nt"&gt;--limit&lt;/span&gt; 300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt; number,title,createdAt,closedAt,labels &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; issues.json

jq &lt;span class="s1"&gt;'[.[] | select(any(.labels[]; .name == "scope-change"))] | length'&lt;/span&gt; issues.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two. Both created by me, both on the same afternoon, both in the first month. A label works only when a human remembers to apply it at the exact moment he is under pressure to keep the work moving. Nobody labels a request that arrives as "quick thing, five minutes".&lt;/p&gt;

&lt;p&gt;Second attempt, also bad: I tried churn per week as a proxy, lines added and removed from &lt;code&gt;git log --numstat&lt;/code&gt;. The chart was flat and useless. Rework and new scope look identical in a diff, which is the whole reason the status meeting stays green.&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked: creation date against the freeze date
&lt;/h2&gt;

&lt;p&gt;The one field nobody has to maintain is &lt;code&gt;createdAt&lt;/code&gt;. The tracker writes it whether anyone cares or not. So the question became mechanical: how much of what we shipped in this milestone did not exist when we agreed on the date?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;FREEZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-03-09T00:00:00Z

&lt;span class="c"&gt;# everything in the milestone&lt;/span&gt;
jq &lt;span class="s1"&gt;'length'&lt;/span&gt; issues.json

&lt;span class="c"&gt;# everything born after we agreed on the date&lt;/span&gt;
jq &lt;span class="nt"&gt;--arg&lt;/span&gt; freeze &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FREEZE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'[.[] | select(.createdAt &amp;gt; $freeze)] | length'&lt;/span&gt; issues.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;118
41
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Around 35% of the milestone was written after the estimate that the milestone was judged by. Broken down by month, the shape is worse than the total:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; freeze &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FREEZE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'
  [.[] | select(.createdAt &amp;gt; $freeze)]
  | group_by(.createdAt[0:7])
  | map({month: .[0].createdAt[0:7], added: length})
  | .[] | "\(.month)  \(.added)"
'&lt;/span&gt; issues.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="mf"&gt;2026&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;03&lt;/span&gt;   &lt;span class="mf"&gt;6&lt;/span&gt;
&lt;span class="mf"&gt;2026&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;04&lt;/span&gt;   &lt;span class="mf"&gt;11&lt;/span&gt;
&lt;span class="mf"&gt;2026&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;05&lt;/span&gt;   &lt;span class="mf"&gt;17&lt;/span&gt;
&lt;span class="mf"&gt;2026&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;06&lt;/span&gt;   &lt;span class="mf"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;May was the month everyone in the room described as "the team is struggling with the last stretch". It was the month the scope grew the most. Nobody lied. The growth simply had no line anywhere, so the only visible variable was the date.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second number is the one that hurts
&lt;/h2&gt;

&lt;p&gt;Work arriving late is normal. On a site the owner changes his mind constantly: he moves the warehouse layout after the foundation is poured, raises the clear height, adds a crane bay. There is a path for it, and it is tedious on purpose, because price and date move together with the request.&lt;/p&gt;

&lt;p&gt;So I asked the tracker the second question: of those 41, how many ever got a revised date written down anywhere?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; freeze &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FREEZE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'.[] | select(.createdAt &amp;gt; $freeze) | .number'&lt;/span&gt; issues.json |
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; n&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;hits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gh issue view &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt; body,comments &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'[.body] + [.comments[].body] | join("\n")'&lt;/span&gt; |
    &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eic&lt;/span&gt; &lt;span class="s1"&gt;'estimate|revised date|new deadline|moves the date'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$hits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three out of 41. Thirty eight work items entered the milestone through a conversation, and the schedule they landed on was still the one negotiated before they existed. That is the difference between a site that runs 20% long and a project that reads as a failure. One measured the drift along the way, the other showed up finished at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning the measurement into a CI check
&lt;/h2&gt;

&lt;p&gt;The fix I kept is boring and it lives in the repo, because process that lives in someone's memory is the same label that got applied twice. The workflow does not block the merge. It writes the fact on the PR, at the moment the work is being requested, while the decision is still cheap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scope-gate&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flag-late-scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compare issue creation with the freeze date&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;FREEZE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-03-09T00:00:00Z"&lt;/span&gt;
          &lt;span class="na"&gt;PR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.number }}&lt;/span&gt;
          &lt;span class="na"&gt;REPO&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.repository }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;issue=$(gh pr view "$PR" --repo "$REPO" \&lt;/span&gt;
            &lt;span class="s"&gt;--json closingIssuesReferences \&lt;/span&gt;
            &lt;span class="s"&gt;--jq '.closingIssuesReferences[0].number // empty')&lt;/span&gt;

          &lt;span class="s"&gt;if [ -z "$issue" ]; then echo "no linked issue"; exit 0; fi&lt;/span&gt;

          &lt;span class="s"&gt;created=$(gh issue view "$issue" --repo "$REPO" \&lt;/span&gt;
            &lt;span class="s"&gt;--json createdAt --jq .createdAt)&lt;/span&gt;
          &lt;span class="s"&gt;labels=$(gh issue view "$issue" --repo "$REPO" \&lt;/span&gt;
            &lt;span class="s"&gt;--json labels --jq '[.labels[].name] | join(",")')&lt;/span&gt;

          &lt;span class="s"&gt;if [[ "$created" &amp;gt; "$FREEZE" &amp;amp;&amp;amp; "$labels" != *"scope-change"* ]]; then&lt;/span&gt;
            &lt;span class="s"&gt;gh pr comment "$PR" --repo "$REPO" --body \&lt;/span&gt;
              &lt;span class="s"&gt;"Issue #$issue was created after the scope freeze and carries no scope-change label. Add the label with an effort range and a revised date, or move it out of this milestone."&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The label came back to life once a robot asked for it instead of a person. Over the next two milestones the count went from 2 to 29, and the useful part was never the label itself: it was that the date discussion happened in the week the request arrived, not in the week of the deadline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this falls apart
&lt;/h2&gt;

&lt;p&gt;If your team opens issues after the code is written, &lt;code&gt;createdAt&lt;/code&gt; measures nothing and you will get a clean report on a project that drifted anyway. Same for repos where one issue means "epic" and the next one means "typo", since counting items assumes items are roughly comparable in size, and mine were not. I looked at the 41 by hand to be sure the big ones were spread out, which is not a method, just a sanity check.&lt;/p&gt;

&lt;p&gt;I am also not convinced this pays for itself on a new product still hunting for its first customers. There the scope is supposed to move every week, and a bot commenting on every PR is noise with a YAML file attached.&lt;/p&gt;

&lt;p&gt;The part I do trust is the count. Take the last milestone that blew its date, run those two jq lines, and compare the number of items born after the estimate with the number that ever got a new date written down. If the second number is zero, the delay was never a surprise.&lt;/p&gt;

&lt;p&gt;How do you keep this visible in your repo? I am curious about people using milestones with explicit budget, or a bot that recomputes a forecast whenever an issue joins the milestone, because comparing dates in bash is the crudest version of this I could build.&lt;/p&gt;

&lt;p&gt;Originally published on the Revin blog: &lt;a href="https://revin.com.br/en/blog/why-it-cannot-deliver-like-construction" rel="noopener noreferrer"&gt;https://revin.com.br/en/blog/why-it-cannot-deliver-like-construction&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I ran git blame on the files that touch money: 87% of the commission logic had one author</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Wed, 26 Aug 2026 16:04:14 +0000</pubDate>
      <link>https://dev.to/revinsoftware/i-ran-git-blame-on-the-files-that-touch-money-87-of-the-commission-logic-had-one-author-3flj</link>
      <guid>https://dev.to/revinsoftware/i-ran-git-blame-on-the-files-that-touch-money-87-of-the-commission-logic-had-one-author-3flj</guid>
      <description>&lt;p&gt;Every planning meeting at a client I was helping ended the same way. Someone would pull a card about the commission rule, and someone else would say "better wait for Marina". Nobody found that odd. She always sorted it out.&lt;/p&gt;

&lt;p&gt;I wanted a number instead of a feeling, because "only Marina touches this" does not survive a budget conversation and a percentage does. So I spent an afternoon on the repository. Node, Postgres, about 64k lines, six years old, four developers on payroll and a fifth who left in 2023.&lt;/p&gt;

&lt;p&gt;The method is crude and it fits in one afternoon. Write down the rules that move money, map each one to the files that implement it, and ask git who owns those lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: list the rules, then the paths
&lt;/h2&gt;

&lt;p&gt;This part is not automatable and that is fine. I sat with the product person and we listed ten rules that touch money: how the discount is calculated, when an order can be cancelled, what releases a partner payout, how tax is split on an invoice, and so on. Then a developer mapped each rule to the files that actually implement it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;money-paths.txt
src/billing/commission.ts
src/billing/discount.ts
src/orders/cancellation.ts
src/payouts/partner-release.ts
src/invoices/tax-split.ts
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten rules turned into 14 files. The mapping took longer than the script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: ask blame who wrote the surviving lines
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git blame &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="nt"&gt;-M&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nt"&gt;--line-porcelain&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'^author '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;top&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git blame &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="nt"&gt;-M&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nt"&gt;--line-porcelain&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^author '&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-34s %4s lines %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$top&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; money-paths.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output, names changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/billing/commission.ts         412 lines  358 author Marina
src/billing/discount.ts           287 lines  169 author Marina
src/orders/cancellation.ts        233 lines  201 author Marina
src/payouts/partner-release.ts    191 lines   96 author Rafael
src/invoices/tax-split.ts         164 lines  102 author Marina
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commission file came back at 87% single author. Four of the ten rules had the same name above 80%. That is the whole finding, and it took an afternoon.&lt;/p&gt;

&lt;p&gt;The flags matter more than the loop. &lt;code&gt;-w&lt;/code&gt; ignores whitespace changes, &lt;code&gt;-M&lt;/code&gt; follows lines moved inside a file, &lt;code&gt;-C&lt;/code&gt; follows lines copied from other files. Without them the number is a lie, and I will get to how badly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: check who guards the door
&lt;/h2&gt;

&lt;p&gt;Authorship is half the story. The other half is review, because a rule with one author and three habitual reviewers is much less concentrated than the blame output suggests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;list &lt;span class="nt"&gt;--state&lt;/span&gt; merged &lt;span class="nt"&gt;--limit&lt;/span&gt; 300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--search&lt;/span&gt; &lt;span class="s2"&gt;"billing/commission in:path"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt; number,reviews &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[].reviews[].author.login'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  19 marina
   3 rafael
   1 caio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nineteen of the 23 merged pull requests touching that path were approved by the same person who wrote it. So the review was not spreading the knowledge, it was confirming it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not work
&lt;/h2&gt;

&lt;p&gt;Three attempts died before the one above.&lt;/p&gt;

&lt;p&gt;First I ran &lt;code&gt;git shortlog -sn -- &amp;lt;file&amp;gt;&lt;/code&gt;, counting commits per author. It ranked a developer first on the discount file because he had done a dependency bump that reformatted imports. Commit count measures traffic, not ownership.&lt;/p&gt;

&lt;p&gt;Then I ran blame without &lt;code&gt;-w -M -C&lt;/code&gt;. A Prettier rollout in 2023 rewrote almost every line in the repository, and the output cheerfully told me that one engineer owned 71% of the entire business logic. He had joined four months earlier. Any blame based metric that ignores whitespace commits will hand you that kind of nonsense with a straight face.&lt;/p&gt;

&lt;p&gt;Last, I tried correlating with cyclomatic complexity, because that is what most answers on Stack Exchange recommend when someone asks how to quantify technical debt. The most complex file in the repository had four authors and nobody was afraid of it. The file that froze the planning meeting was 233 lines of plain conditionals. Complexity told me where the code was ugly. It said nothing about where the company was exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the ugly code is not the debt
&lt;/h2&gt;

&lt;p&gt;In systems that land on my desk after the previous vendor walked away, the thing that eats the first month is rarely the chained slow query or the test suite without a single assertion. It is figuring out why a strange conditional exists in the order service. That conditional is a commercial agreement somebody closed on the phone in 2019.&lt;/p&gt;

&lt;p&gt;The code was the only living record of the rule. Living records read slowly, and the person who could read it fast changed jobs.&lt;/p&gt;

&lt;p&gt;Construction has a name for the missing artifact. There is the design, drawn before anything is poured, and the as built, the drawing of how the structure ended up after every decision made on site, including the beam that moved because the soil did not cooperate. Skip the as built and you find the deviation by opening a wall with the building already occupied. In software the wall is invisible and the person who knew where it was is somewhere else.&lt;/p&gt;

&lt;p&gt;There is a cheap test for whether your docs are as built or decoration. Give a small change on that rule to someone who has never touched it, with the document and nothing else. If she opens Slack in twenty minutes, you have a file rather than a document.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moved the number
&lt;/h2&gt;

&lt;p&gt;A documentation sprint fixes little and rots fast. Rotation moves the blame output, in three habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every task on a concentrated rule ships as a pair, with the person who does not know it on the keyboard and the person who does only answering questions.&lt;/li&gt;
&lt;li&gt;On call rotates through everyone, with the author of the rule in second line rather than first, because on call is the only moment nobody can postpone the question.&lt;/li&gt;
&lt;li&gt;The rule owner writes half a page of why, including the options that were thrown out, and the page counts as done only after someone else changed the rule reading nothing but it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is slower. A pair delivers something like 20% to 30% slower in the first weeks and the team will say so out loud. It is the price of pulling one name off four lines of that list.&lt;/p&gt;

&lt;p&gt;Where the advice is bad: if you are three people still hunting for customers, concentration is an advantage. Rotation eats the week and your real problem is whether anyone pays. The math flips when revenue starts depending on that system, and it flips without warning.&lt;/p&gt;

&lt;p&gt;I re-ran the script on those paths eleven weeks later. Commission went from 87% to 61%, which is not great and is a lot better than a wiki nobody opened.&lt;/p&gt;

&lt;p&gt;How do you measure this where you work? I have seen bus factor plugins, CODEOWNERS coverage reports and pure gut feeling in planning, and I am honestly not sure the plugins beat the afternoon with blame. If you have a metric that survived contact with a real repository, I want to read it.&lt;/p&gt;

&lt;p&gt;Originally published on the Revin blog: &lt;a href="https://revin.com.br/en/blog/cognitive-debt-technical-debt-outside-code" rel="noopener noreferrer"&gt;https://revin.com.br/en/blog/cognitive-debt-technical-debt-outside-code&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>PHPCompatibility flagged 217 errors in a PHP 5.6 app. The one that cost money wasn't in the report</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Mon, 24 Aug 2026 14:08:34 +0000</pubDate>
      <link>https://dev.to/revinsoftware/phpcompatibility-flagged-217-errors-in-a-php-56-app-the-one-that-cost-money-wasnt-in-the-report-ieh</link>
      <guid>https://dev.to/revinsoftware/phpcompatibility-flagged-217-errors-in-a-php-56-app-the-one-that-cost-money-wasnt-in-the-report-ieh</guid>
      <description>&lt;p&gt;Last month someone handed me read access to a PHP 5.6 codebase and asked whether going to PHP 8 was the same size of job as dropping Laravel on top of what already existed. Roughly 90k lines, procedural, a homemade router, MySQL 5.7 underneath, running order capture and shipping rules for a distributor. Three developers in its history, none of them still at the company.&lt;/p&gt;

&lt;p&gt;The team had been arguing about it for two weeks with zero numbers on the table. Instead of joining the argument I ran a scanner, and then I did the boring thing nobody wants to do first: I froze the current behavior of the money flows before touching the version.&lt;/p&gt;

&lt;p&gt;Good thing, because the change that would have cost real money never showed up in the scanner report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The linter counts the damage in about half an hour
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require &lt;span class="nt"&gt;--dev&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  squizlabs/php_codesniffer &lt;span class="se"&gt;\&lt;/span&gt;
  phpcompatibility/php-compatibility

vendor/bin/phpcs &lt;span class="nt"&gt;-p&lt;/span&gt; ./src &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--standard&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PHPCompatibility &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--runtime-set&lt;/span&gt; testVersion 8.2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--report&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PHP CODE SNIFFER REPORT SUMMARY
--------------------------------------------------------------
FILE                                        ERRORS  WARNINGS
--------------------------------------------------------------
src/legacy/order_functions.php                  58        11
src/legacy/db.php                               41         3
src/legacy/mailer.php                           22         9
src/shipping/tier.php                            9         2
... (7 more files)
--------------------------------------------------------------
A TOTAL OF 217 ERRORS AND 89 WARNINGS WERE FOUND IN 11 FILES
--------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;217 errors across 11 files, in a codebase that everybody in the room described as "unfixable". Almost all of it mechanical: &lt;code&gt;each()&lt;/code&gt; removed, &lt;code&gt;create_function()&lt;/code&gt; gone, &lt;code&gt;mysql_*&lt;/code&gt; calls that already died back in 7.0, arguments passed by reference where that is no longer allowed. Rector cleared a large slice of it unattended:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vendor/bin/rector process src &lt;span class="nt"&gt;--set&lt;/span&gt; php82 &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What was left after the dry run was a long afternoon of manual edits. Annoying, not a quarter of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The change that does not raise an error
&lt;/h2&gt;

&lt;p&gt;Loose comparison between a string and a number changed in PHP 8. On 5.6, &lt;code&gt;0 == "abc"&lt;/code&gt; returned true. On 8, it returns false. Nothing throws, no log line appears, the &lt;code&gt;if&lt;/code&gt; simply picks the other branch.&lt;/p&gt;

&lt;p&gt;In this codebase it lived inside a &lt;code&gt;switch&lt;/code&gt;, which is loose comparison wearing a costume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shippingTier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="c1"&gt;// matches ANY non-numeric string on 5.6&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'free'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'flat'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'standard'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nf"&gt;shippingTier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'EXPRESS'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// PHP 5.6 -&amp;gt; free&lt;/span&gt;
&lt;span class="c1"&gt;// PHP 8.2 -&amp;gt; standard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same with &lt;code&gt;in_array(0, ['EXPRESS', 'ECONOMY'])&lt;/code&gt;, true on 5.6 and false on 8. A business rule flips and nobody signs off on it. The customer finds out from an invoice total three weeks later.&lt;/p&gt;

&lt;p&gt;Grep does not save you here. I tried: searching for &lt;code&gt;==&lt;/code&gt; in 90k lines returns thousands of hits, and reading them one by one is how you convince yourself you read them all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freeze the behavior, then bump the version
&lt;/h2&gt;

&lt;p&gt;What worked was characterization tests over the four flows that generate revenue. Not unit tests of what the code should do. A recording of what it does today, ugly parts included.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/Characterization/ShippingTierTest.php&lt;/span&gt;
&lt;span class="nv"&gt;$inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'EXPRESS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ECONOMY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1abc'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$fh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;fopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/golden.txt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$inputs&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;fwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%-10s =&amp;gt; %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;var_export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;shippingTier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;fclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fh&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on the old runtime, keep the file, run it on the new one, diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php5.6 tests/Characterization/ShippingTierTest.php &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv &lt;/span&gt;tests/Characterization/golden.txt golden-5.6.txt
php8.2 tests/Characterization/ShippingTierTest.php &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv &lt;/span&gt;tests/Characterization/golden.txt golden-8.2.txt
diff golden-5.6.txt golden-8.2.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;1c1
&lt;/span&gt;&lt;span class="gd"&gt;&amp;lt; 'EXPRESS'  =&amp;gt; free
&lt;/span&gt;&lt;span class="p"&gt;---
&lt;/span&gt;&lt;span class="gi"&gt;&amp;gt; 'EXPRESS'  =&amp;gt; standard
&lt;/span&gt;&lt;span class="p"&gt;2c2
&lt;/span&gt;&lt;span class="gd"&gt;&amp;lt; 'ECONOMY'  =&amp;gt; free
&lt;/span&gt;&lt;span class="p"&gt;---
&lt;/span&gt;&lt;span class="gi"&gt;&amp;gt; 'ECONOMY'  =&amp;gt; standard
&lt;/span&gt;&lt;span class="p"&gt;5c5
&lt;/span&gt;&lt;span class="gd"&gt;&amp;lt; '1abc'     =&amp;gt; flat
&lt;/span&gt;&lt;span class="p"&gt;---
&lt;/span&gt;&lt;span class="gi"&gt;&amp;gt; '1abc'     =&amp;gt; flat
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines of diff on the flow that decides shipping cost. That is the whole return on the exercise. Across the four money flows I ended up with 3 behavior changes: two harmless, one that would have handed free shipping to nobody who had it before, or the reverse, depending on which way the data leaned that month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things I tried that did not work
&lt;/h2&gt;

&lt;p&gt;First attempt was to lean on the existing suite. The repo reported 34% coverage, which sounded like something. Opening the tests, a good chunk of them called the method and asserted nothing at all, no &lt;code&gt;assert*&lt;/code&gt; anywhere in the body. The metric existed, the guarantee did not. Coverage told me nothing about whether the version bump changed an outcome.&lt;/p&gt;

&lt;p&gt;Second attempt was worse and I am glad it was a branch. I ran a sweep turning &lt;code&gt;==&lt;/code&gt; into &lt;code&gt;===&lt;/code&gt; in the shipping and pricing files, on the theory that strict is safer. It is safer in a language where types are stable, and this app reads everything from &lt;code&gt;mysqli&lt;/code&gt; in the old procedural style, which hands back numeric columns as strings. Half the id comparisons started returning false. Reverted in twenty minutes. Strict comparison is a refactor with its own test bill, not a migration step you sneak in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then check what nobody maintains anymore
&lt;/h2&gt;

&lt;p&gt;Half a day of work and the answer changes the plan more than any framework debate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer outdated &lt;span class="nt"&gt;--direct&lt;/span&gt;
composer audit
php &lt;span class="nt"&gt;-m&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;   &lt;span class="c"&gt;# native extensions the server actually loads&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch for &lt;code&gt;mcrypt&lt;/code&gt;, which left core in 7.2 and is still glued to homegrown crypto in plenty of shops. If every direct dependency has an 8-compatible release, even one that needs work, the upgrade path is short. If the system is welded to a framework that stopped shipping fixes years ago, part of it gets rewritten either way, and now you know which part: the entry layer, not ten years of business rules.&lt;/p&gt;

&lt;p&gt;One detail that shortens meetings: 5.6 has been out of security support since the end of 2018. That does not choose between upgrading and rewriting. It removes the option everybody secretly prefers, which is looking at this next year.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order that ended up working
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Characterization tests over the revenue flows, on the old runtime, golden files committed.&lt;/li&gt;
&lt;li&gt;Rector plus manual cleanup for the 217 errors, then diff the golden files and explain every line that moved.&lt;/li&gt;
&lt;li&gt;Housekeeping without changing the shape of the thing: Composer with PSR-4, a single front controller, config out of the code, real logging.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only after that does standing a framework beside it on the same database make sense, one route at a time, new endpoints born there and old checkout left alone until tests can back the move. The system runs split for a while and that bothers everybody who likes clean things. It is also reversible on any given day, which a nine-month rewrite is not.&lt;/p&gt;

&lt;p&gt;Measured on this one: the scan took under an hour, the characterization tests took about two days, the actual upgrade landed in a bit over three weeks. The rewrite proposal on the table had been sized at a quarter, and every rewrite I have watched from close up ran past its number.&lt;/p&gt;

&lt;p&gt;The part I still do not have a clean answer for: flows that only prove themselves against a third party. Billing, tax invoices, the payment gateway. You cannot golden-file a webhook that only fires when a real customer pays. How do you snapshot behavior on those before a version jump? Recorded HTTP fixtures, a sandbox, or do you just ship it and watch the logs?&lt;/p&gt;




&lt;p&gt;Originally published on the Revin blog: &lt;a href="https://revin.com.br/en/blog/php-5-6-to-php-8-or-rewrite" rel="noopener noreferrer"&gt;https://revin.com.br/en/blog/php-5-6-to-php-8-or-rewrite&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I pointed an AI reviewer at a 2019 order service: 63 comments, and none of them was the IDOR</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Fri, 21 Aug 2026 12:55:09 +0000</pubDate>
      <link>https://dev.to/revinsoftware/i-pointed-an-ai-reviewer-at-a-2019-order-service-63-comments-and-none-of-them-was-the-idor-4n9o</link>
      <guid>https://dev.to/revinsoftware/i-pointed-an-ai-reviewer-at-a-2019-order-service-63-comments-and-none-of-them-was-the-idor-4n9o</guid>
      <description>&lt;p&gt;Last month I got read access to an order service that had been running since 2019. Node, Express, Postgres, around 64k lines, three developers in its history and none of them still at the company. The team wanted a second opinion before a batch of changes to the checkout flow.&lt;/p&gt;

&lt;p&gt;I ran an AI reviewer over the branch first. Half curiosity, half because everybody has been arguing about whether AI code review is a bubble, and I wanted a number of my own instead of a take.&lt;/p&gt;

&lt;p&gt;It came back with 63 comments across 12 files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sorting the 63
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;38 were naming, formatting, or "consider extracting this into a helper"&lt;/li&gt;
&lt;li&gt;11 were missing null checks, and 9 of those could not happen, because the value came from a NOT NULL column&lt;/li&gt;
&lt;li&gt;8 suggested wrapping code in a try/catch that was already inside a try/catch&lt;/li&gt;
&lt;li&gt;4 were about actual behavior, and 2 of those I would have written myself: a date compared as a string, and a retry loop with no ceiling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So something around 3% of the output was worth a human's attention. That ratio alone is not damning. Linters have terrible ratios too and nobody cares, because a linter costs nothing to skim.&lt;/p&gt;

&lt;p&gt;The part that bothered me is what it stayed quiet about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comment it did not write
&lt;/h2&gt;

&lt;p&gt;Here is the endpoint, cleaned up and renamed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/routes/orders.js&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;oneOrNone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select * from orders where id = $1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;requireAuth&lt;/code&gt; resolves the session and puts &lt;code&gt;req.user&lt;/code&gt; in place. It never asks whether that order belongs to the caller. The ids are sequential integers. Three curl calls with a valid cookie and a different number and you are reading another customer's address, items and total.&lt;/p&gt;

&lt;p&gt;The AI's comment on that exact file was: &lt;em&gt;"Consider extracting the 404 response into a shared helper for consistency."&lt;/em&gt; Accurate. Useless.&lt;/p&gt;

&lt;p&gt;I checked how widespread it was with ripgrep before doing anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"where id = &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;1"&lt;/span&gt; &lt;span class="nt"&gt;--glob&lt;/span&gt; &lt;span class="s1"&gt;'!test/**'&lt;/span&gt; src/
src/routes/orders.js:41:    &lt;span class="s1"&gt;'select * from orders where id = $1'&lt;/span&gt;,
src/routes/invoices.js:88:   &lt;span class="s1"&gt;'select * from invoices where id = $1'&lt;/span&gt;,
src/jobs/reconcile.js:23:    &lt;span class="s1"&gt;'select * from orders where id = $1'&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of the three were reachable from the browser. The third runs in a job with no request context, so it is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a diff reviewer cannot see this
&lt;/h2&gt;

&lt;p&gt;The reviewer reasons about lines that exist. A broken authorization check is an absence. There is no bad line to point at, and the thing that makes it dangerous lives somewhere else entirely: in the fact that ids are sequential, in what &lt;code&gt;requireAuth&lt;/code&gt; chose not to do, in a middleware file the branch never touched.&lt;/p&gt;

&lt;p&gt;Same story with the two other findings from that week. A query sitting inside a &lt;code&gt;for&lt;/code&gt; loop that only hurts when a customer has more than a handful of shipments. A &lt;code&gt;/health&lt;/code&gt; route that returns 200 without checking anything, which means the load balancer keeps sending traffic to a process that lost its database pool. Both are invisible in a diff and obvious when you run the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix I tried that did not work
&lt;/h2&gt;

&lt;p&gt;My first move was the obvious one: give it more context. Whole file instead of hunk, then a repo map, then a system prompt that spelled it out, roughly "check every route for broken object level authorization, ids are sequential".&lt;/p&gt;

&lt;p&gt;Told explicitly what to hunt for, it found the orders endpoint. It also flagged 7 endpoints in total, and 5 of those were wrong. They loaded data through a repository function that already scopes by &lt;code&gt;customer_id&lt;/code&gt;, one call deeper than the route file. The model could not tell the difference between an endpoint with no ownership filter and an endpoint whose filter is two hops away.&lt;/p&gt;

&lt;p&gt;Which is the honest summary: it runs a checklist over text. It does not know who owns what in your system, and the prompt that makes it paranoid enough to catch the real one also makes it cry wolf five times.&lt;/p&gt;

&lt;h2&gt;
  
  
  What sits in CI now
&lt;/h2&gt;

&lt;p&gt;I stopped asking the reviewer for security opinions and wrote a test with two tenants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;an order is not readable by another customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;seedCustomer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;seedCustomer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;seedOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1290&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;bearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alice&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;404 and not 403, so the endpoint does not confirm that the id exists.&lt;/p&gt;

&lt;p&gt;One test only covers one route, and the point was the other 40. So there is a second script that walks the Express router stack and prints every path that never touches an owner filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;node scripts/audit-routes.js
GET   /orders/:id              no owner filter
GET   /invoices/:id            no owner filter
POST  /orders/:id/cancel       no owner filter
GET   /orders                  scoped &lt;span class="o"&gt;(&lt;/span&gt;customer_id&lt;span class="o"&gt;)&lt;/span&gt;
...
3 of 41 routes unscoped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is crude. It matches on the repository functions we consider safe, so a fourth way of loading an order would slip past it until someone adds it to the list. But it is deterministic, it runs in about 2 seconds, and it never invents a problem to look useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I land on the bubble argument
&lt;/h2&gt;

&lt;p&gt;I don't think these tools are worthless. Two of those 63 comments were real, and one of them was a bug I would probably have shipped.&lt;/p&gt;

&lt;p&gt;What I think is broken is measuring them by comments produced. Every comment spends attention, and attention runs out. After the twentieth "consider extracting", the human reviewer starts scrolling, and the one comment that mattered scrolls by with the rest. The tool got cheaper and moved the cost onto the person approving the merge.&lt;/p&gt;

&lt;p&gt;So the number I care about now is not how much it finds. It is how much noise a real finding has to survive.&lt;/p&gt;

&lt;p&gt;How are you drawing that line? Specifically, has anyone gotten an AI reviewer to reason about authorization across files without drowning in false positives, or did you also give up and write the boring two-tenant test?&lt;/p&gt;




&lt;p&gt;This audit came out of client work at Revin.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>An agent wrote 14 tests for a module I knew was broken. Mutation score came back 41%</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Wed, 19 Aug 2026 13:14:13 +0000</pubDate>
      <link>https://dev.to/revinsoftware/an-agent-wrote-14-tests-for-a-module-i-knew-was-broken-mutation-score-came-back-41-157a</link>
      <guid>https://dev.to/revinsoftware/an-agent-wrote-14-tests-for-a-module-i-knew-was-broken-mutation-score-came-back-41-157a</guid>
      <description>&lt;p&gt;Last month I ran a small experiment on a checkout module I already knew was broken.&lt;/p&gt;

&lt;p&gt;The module applies tiered discounts to an order total. The bug is boring: the tier discount gets applied twice above a certain subtotal, and the coupon argument is read but never used. It survived two code reviews because the output still looks like money and the difference only shows up on bigger carts.&lt;/p&gt;

&lt;p&gt;I gave an agent read access to that one file and asked for a test suite. Nothing else in the prompt, no examples, no description of expected behavior. It came back with 14 tests. All green on the first run. Line coverage for the file went from nothing to 92%. The bug shipped anyway.&lt;/p&gt;

&lt;p&gt;Martin Fowler's site published a piece last week called "TDD inside the agent loop - theater or actual value?", and reading it is what pushed me to stop arguing from feeling and write down my own numbers. So here they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The file under test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TIERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;off&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;off&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;off&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;orderTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;coupon&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Coupon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TIERS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;coupon&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the tier lands a second time&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the agent gave me
&lt;/h2&gt;

&lt;p&gt;Two of the 14 tests were fine. The rest looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orderTotal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;returns a number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nf"&gt;orderTotal&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}])).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;applies the 5% tier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;orderTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handles a coupon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;orderTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X10&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;929.28&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the last one. 929.28 is the double discount. The agent read the implementation, computed what the code currently does, and froze it as the expected value. The test is green because the bug is now documented as a requirement.&lt;/p&gt;

&lt;p&gt;This is the part that no coverage report will ever tell you. A test written from the implementation can only confirm the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mutation run
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx stryker run &lt;span class="nt"&gt;--mutate&lt;/span&gt; &lt;span class="s1"&gt;'src/checkout/order-total.ts'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-----------------|---------|----------|----------|-----------|---------|
File             | % score | # killed | # surviv | # timeout | # error |
-----------------|---------|----------|----------|-----------|---------|
order-total.ts   |   41.37 |       12 |       17 |         0 |       0 |
-----------------|---------|----------|----------|-----------|---------|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;92% of the lines executed, 41.37% of the mutants killed. Twelve dead, seventeen alive. Two of the survivors say the whole story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;Survived mutant #7 (ConditionalExpression)
&lt;/span&gt;&lt;span class="gd"&gt;-  if (coupon &amp;amp;&amp;amp; subtotal &amp;gt;= 1000) {
&lt;/span&gt;&lt;span class="gi"&gt;+  if (coupon &amp;amp;&amp;amp; true) {
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;Survived mutant #12 (EqualityOperator)
&lt;/span&gt;&lt;span class="gd"&gt;-  TIERS.filter((t) =&amp;gt; subtotal &amp;gt;= t.min)
&lt;/span&gt;&lt;span class="gi"&gt;+  TIERS.filter((t) =&amp;gt; subtotal &amp;gt; t.min)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mutant #12 flips &lt;code&gt;&amp;gt;=&lt;/code&gt; into &lt;code&gt;&amp;gt;&lt;/code&gt; and nothing in the suite notices, which means no test ever hits a subtotal of exactly 500 or exactly 1000. Fourteen tests and not one boundary. Mutant #7 removes the subtotal condition entirely and the suite stays green, because the coupon test asserts the broken output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things I tried that went nowhere
&lt;/h2&gt;

&lt;p&gt;First I did the obvious thing and asked for more. "Improve the test suite, target 95% coverage." It wrote six extra tests, coverage went to about 96%, mutation score moved to roughly 47%. More assertions on shape, more &lt;code&gt;toBeDefined&lt;/code&gt;, same blind spots. Raising the coverage gate rewarded exactly the behavior I was trying to kill.&lt;/p&gt;

&lt;p&gt;Then I put the theory in the prompt: "write boundary tests, this suite has weak mutation coverage." The agent produced tests that look like boundary tests, with subtotals of 499, 500 and 501, and it filled every expected value by running the current code in its head. Green again. The vocabulary changed, the epistemology did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What moved the number
&lt;/h2&gt;

&lt;p&gt;Two changes, both of them about what the agent is allowed to read.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feed the surviving mutants back in as the task. Stryker writes a JSON report, so the loop becomes "here is mutant #12, write a test that kills it" instead of "write tests". Two rounds of that took the score from 41% to somewhere near 78%.&lt;/li&gt;
&lt;li&gt;Make the red step mandatory and machine-checked. The agent may not see the implementation when writing the test, and the test has to fail against the current code before anyone looks at it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second one is three lines in a pre-commit hook and it is the only part of this I would call TDD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# a new test must be able to fail on the unfixed file&lt;/span&gt;
git stash push &lt;span class="nt"&gt;--&lt;/span&gt; src/checkout/order-total.ts
&lt;span class="k"&gt;if &lt;/span&gt;npx jest src/checkout/order-total.spec.ts &lt;span class="nt"&gt;--silent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"test passes on the broken implementation - rejected"&lt;/span&gt;
  git stash pop &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
&lt;/span&gt;git stash pop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crude, and it only works when the fix and the test arrive in the same change. But it caught four tests in the following week that were green against code they were supposed to break.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost, honestly
&lt;/h2&gt;

&lt;p&gt;Mutation runs are slow. That single file takes about 40 seconds, the full suite takes something close to 11 minutes on our runner, so it only runs on changed files in CI and on a nightly job for the payment paths. I have no idea whether the mutants-as-input loop holds up on a codebase with heavy mocking, because ours has fairly little of it. If your suite mocks the module under test, most mutants die for the wrong reason and the score lies in the other direction.&lt;/p&gt;

&lt;p&gt;What I stopped believing is the green board. An agent writing tests after reading the implementation produces a very convincing photograph of the bug you already have. Coverage measures which lines ran. Mutation measures whether anyone would have noticed if those lines were wrong, and the gap between 92 and 41 is where the money leaked.&lt;/p&gt;

&lt;p&gt;So, a real question: for those of you running agents inside a TDD loop, how do you stop the model from deriving the expected value from the code it is looking at? Hiding the implementation behind an interface helped a bit here, and I would like to hear what else works before I turn this into a rule for the whole repo.&lt;/p&gt;




&lt;p&gt;I write about this kind of thing on the Revin blog: &lt;a href="https://revin.com.br/en" rel="noopener noreferrer"&gt;https://revin.com.br/en&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I inherited an undocumented codebase: 2.5 days to boot it, ~400 silent errors a day</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Fri, 14 Aug 2026 13:47:02 +0000</pubDate>
      <link>https://dev.to/revinsoftware/i-inherited-an-undocumented-codebase-25-days-to-boot-it-400-silent-errors-a-day-p8g</link>
      <guid>https://dev.to/revinsoftware/i-inherited-an-undocumented-codebase-25-days-to-boot-it-400-silent-errors-a-day-p8g</guid>
      <description>&lt;p&gt;Six weeks ago I got read access to a system nobody had documented. The dev who wrote most of it left in 2023. The README had three lines and two of them were wrong.&lt;/p&gt;

&lt;p&gt;My first instinct was the wrong one. I opened the repo and started reading, module by module, for two days. At the end I could describe the folder structure and nothing else. Reading code tells you what the code says. It says nothing about what actually runs, how often, and what breaks at 3am.&lt;/p&gt;

&lt;p&gt;So I stopped reading and ran three experiments instead, in this order: boot the system on a clean machine while logging every missing step, instrument what was already in production, and find the handful of paths where money moves. No features, no refactors, for two weeks.&lt;/p&gt;

&lt;p&gt;The Stack Exchange question "I've inherited 200,000 lines of spaghetti code, what now?" has been sitting there since 2012, past 200k views, 463 votes, 19 answers. Almost every answer argues about code quality. I couldn't join that argument yet, because I still didn't know what the system did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 1: boot it on a clean container
&lt;/h2&gt;

&lt;p&gt;Rule I set for myself: no asking anyone, no copying files from a colleague's laptop. Empty container, the repo, and whatever documentation exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone git@github.com:redacted/api.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;api
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
api-1  | Error: connect ECONNREFUSED 127.0.0.1:6379
api-1  |   at TCPConnectWrap.afterConnect &lt;span class="o"&gt;[&lt;/span&gt;as oncomplete]
api-1  | &lt;span class="c"&gt;# REDIS_URL is read in src/queue/client.ts and is not in .env.example&lt;/span&gt;
api-1 exited with code 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time it broke I appended a line to &lt;code&gt;BOOT.md&lt;/code&gt;: the error, what fixed it, how long it took. That file ended up with 14 undocumented steps and the whole thing took me 2.5 days.&lt;/p&gt;

&lt;p&gt;One grep was enough to prove the env problem was structural and not bad luck:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rhoE&lt;/span&gt; &lt;span class="s2"&gt;"process&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;env&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;[A-Z_]+"&lt;/span&gt; src | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
23
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-cE&lt;/span&gt; &lt;span class="s2"&gt;"^[A-Z_]+="&lt;/span&gt; .env.example
11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve variables the application reads and nobody wrote down. The rest of the list looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the database schema only existed as a production dump someone pastes into a chat window&lt;/li&gt;
&lt;li&gt;the runtime was pinned to a three-year-old minor through an undeclared engine field&lt;/li&gt;
&lt;li&gt;one payment integration had no sandbox and pointed at the live endpoint from local&lt;/li&gt;
&lt;li&gt;the seed script assumed a tenant row that only exists in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;BOOT.md&lt;/code&gt; became the first honest document the project had. In construction there's a drawing called as-built: the plan of what was actually erected, not the one that left the office. That's what a boot log is.&lt;/p&gt;

&lt;p&gt;The number I track now: time between a new dev getting access and seeing one request served locally. It went from 2.5 days to roughly 40 minutes once I committed the compose fixtures and a seed that doesn't need production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 2: a week of production data beats a month of reading
&lt;/h2&gt;

&lt;p&gt;The system had a &lt;code&gt;/health&lt;/code&gt; endpoint answering 200 and a colourful panel wired to it. Neither measured anything that breaks. The metric existed, the guarantee didn't.&lt;/p&gt;

&lt;p&gt;I installed three things and stopped, on purpose, because none of them touch business rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/http/observability.ts&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hrtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hrtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unmatched&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Structured logs with a request id, error grouping in a capture tool, and response time per route at the edge. One deploy, one file, low blast radius.&lt;/p&gt;

&lt;p&gt;Seven days later the log aggregation said three routes out of 61 carried around 78% of the traffic. It also surfaced an error firing about 400 times a day inside a swallowed catch, which had never reached a single dashboard.&lt;/p&gt;

&lt;p&gt;The latency answer came from the database, not from the code I had been reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_exec_time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;total_exec_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_exec_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Top row: a &lt;code&gt;SELECT ... WHERE order_id = $1&lt;/code&gt; called 41,882 times in a day at 6.4ms each. Cheap query, called inside a loop, once per line item. Half the p95 of the checkout route lived there.&lt;/p&gt;

&lt;p&gt;What I tried and dropped: full distributed tracing in week one. Instrumenting every service boundary meant editing code I didn't understand yet and shipping a deploy I couldn't reason about. Edge timings plus a request id got me most of the answer from one middleware, and I added spans later, only on the paths that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 3: find the five paths where money moves
&lt;/h2&gt;

&lt;p&gt;This one has no code. I booked an hour with support, an hour with finance, and an hour with the person who has been in operations the longest. Three questions: what do customers call about, what jams at month-end close, and which spreadsheet exists today to work around the software.&lt;/p&gt;

&lt;p&gt;Five flows came out, and I mapped each one to concrete routes and jobs using the request ids from experiment 2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signup and login, because a customer who can't get in complains within minutes&lt;/li&gt;
&lt;li&gt;checkout, plus every payment call it fires underneath&lt;/li&gt;
&lt;li&gt;the nightly billing job, the oldest script in the repo and the one with no reprocessing&lt;/li&gt;
&lt;li&gt;outbound integration with the ERP, where a silent failure becomes a reconciliation mess a quarter later&lt;/li&gt;
&lt;li&gt;the report leadership opens on Monday, which decides whether the system is trusted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five paths with an owner and log evidence behind each one told me more than any diagram of 200k lines would have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 92% coverage that guarantees nothing
&lt;/h2&gt;

&lt;p&gt;I asked for the coverage number before looking at the suite. 92%. Then I opened the billing tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generates invoices for active contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contracts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contracts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="c1"&gt;// no assertion&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs, it passes, it counts as covered lines. A test with no assertion is a line counter with good PR.&lt;/p&gt;

&lt;p&gt;So I ran mutation testing on that one module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npx stryker run &lt;span class="nt"&gt;--mutate&lt;/span&gt; &lt;span class="s2"&gt;"src/billing/**/*.ts"&lt;/span&gt;
Ran 1.72 tests per mutant on average.
&lt;span class="nt"&gt;---------------&lt;/span&gt;|---------|----------|-----------|------------|
File           | % score | killed   | survived  | no coverage|
&lt;span class="nt"&gt;---------------&lt;/span&gt;|---------|----------|-----------|------------|
billing        |   41.18 |      44  |       63  |          0 |
&lt;span class="nt"&gt;---------------&lt;/span&gt;|---------|----------|-----------|------------|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;92% coverage, 41% mutation score, 63 mutants alive in the code that issues invoices. Now the theatre had a receipt.&lt;/p&gt;

&lt;p&gt;I didn't try to write the whole suite in two weeks. I wrote characterisation tests on the five revenue paths, pinning current behaviour exactly as it is today, wrong parts included. That's a safety net and it isn't quality yet. Quality comes after you know what you can touch without dropping revenue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rewrite I didn't do
&lt;/h2&gt;

&lt;p&gt;Around day 4 the sentence shows up: this is unmaintainable, we should rewrite it. I keep saying no for a practical reason. The ugly code is the only living documentation of the business rules. The weird &lt;code&gt;if&lt;/code&gt; in the middle of the order service turned out to be a contract signed with a large customer in 2019, and nobody on the current team knew it existed. A rewrite throws away the answer together with the question.&lt;/p&gt;

&lt;p&gt;Where that stops being true: a runtime with no security support, a critical dependency unpatched for years, a stack you can't hire for anymore. Then the maths flips, and even then I'd go piece by piece with the old system running beside me as the oracle. And I honestly don't know that any of this scales down to a 3,000-line app with forty users. One afternoon probably covers that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where day 14 landed
&lt;/h2&gt;

&lt;p&gt;Boot time from 2.5 days to about 40 minutes. One error at ~400/day found and fixed. One N+1 removed from checkout. Mutation score measured on the module that bills customers. Characterisation tests on five flows. Zero features, zero refactors.&lt;/p&gt;

&lt;p&gt;The part I'm least sure about is experiment 3. Sitting with support and finance worked, but it's slow and it depends on those people having time for me. Has anyone found a faster way to identify the revenue paths straight from telemetry? And do you instrument first or read first when you land on a codebase nobody can explain?&lt;/p&gt;




&lt;p&gt;Originally published on the Revin blog: &lt;a href="https://revin.com.br/en/blog/inherited-spaghetti-code-first-two-weeks" rel="noopener noreferrer"&gt;https://revin.com.br/en/blog/inherited-spaghetti-code-first-two-weeks&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The code that passes the pitch and fails due diligence</title>
      <dc:creator>Revin</dc:creator>
      <pubDate>Wed, 12 Aug 2026 12:56:57 +0000</pubDate>
      <link>https://dev.to/revinsoftware/the-code-that-passes-the-pitch-and-fails-due-diligence-5fll</link>
      <guid>https://dev.to/revinsoftware/the-code-that-passes-the-pitch-and-fails-due-diligence-5fll</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://revin.com.br/en/blog/the-code-that-passes-the-pitch-and-fails-diligence" rel="noopener noreferrer"&gt;Revin blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For months I kept hearing the same scene told from different angles. A founder lands the round's narrative, the investor likes it, a term sheet shows up. Then technical diligence starts, someone from outside opens the repository, and the number on the page starts to shrink.&lt;/p&gt;

&lt;p&gt;In May I talked to the founder of a healthtech company who had just been through it. Series A on the table, an eager lead, and a consultant the fund had hired to read two and a half years of product. The report came back at thirteen pages. The part that stung wasn't a bug: it was that nobody on the cap table could say who legally owned the code written by a studio she'd hired in year one and let go later. The contract was boilerplate. So was the IP clause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Her product worked fine.&lt;/strong&gt; What failed was everything around it: the contract, the ownership, a history nobody had read with a buyer's eyes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2bojgttr9j30rj4eaj5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2bojgttr9j30rj4eaj5.jpg" alt="Two people going over a report, pen on the chart: diligence reads what the pitch left out." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two people going over a report, pen on the chart: diligence reads what the pitch left out.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I've written about engineering teams for a while, and this is the gap that catches my eye the most. A pitch is a sales object: it shows the traction, the line going up, the demo that works. Diligence is the opposite of a pitch. It doesn't want the good story, it wants what sits underneath. And what sits underneath is rarely built with the day in mind when someone from outside lifts the lid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diligence opens what the pitch keeps shut
&lt;/h2&gt;

&lt;p&gt;Technical diligence is not bug hunting. A serious consultant spends three to five days looking at a set of things the founder almost never measured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Intellectual property&lt;/strong&gt;: who signed what, and whether third-party code actually became yours.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Knowledge concentration&lt;/strong&gt;: how many people understand the system, and what happens if the main one leaves.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Basic security&lt;/strong&gt;: a secret committed to the repo, a dependency left unpatched, a door left open.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ability to ship&lt;/strong&gt;: how long it takes from a commit to production, and how many hands have to get involved.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these show up in a demo. All of them show up in a report. Which is why diligence is usually the first time a founder sees their own product through the eyes of the person about to pay for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  "We'll clean it up before the round" doesn't survive contact
&lt;/h2&gt;

&lt;p&gt;The most common reaction, when I raise this in conversation, is to defer. "When the round gets close, we'll get organized." It feels like the natural order of things. Except diligence won't wait for you to be ready.&lt;/p&gt;

&lt;p&gt;You can't document two years of decisions in three weeks. Rewriting an IP contract with a vendor who already walked away is a slow negotiation, sometimes with someone who no longer picks up. Testing code that never had tests, written by people no longer on the team, is archaeology. Diligence lands in the worst possible window: when you have the least time and the most to lose. Show up improvising and the report becomes the other side's discount argument.&lt;/p&gt;

&lt;p&gt;The counter has no shine to it, and it works: a team that operates, from day one, as if the lid could be lifted at any moment. The squads we work with at Revin run that way by default, with a round on the horizon or none at all. Code that passes diligence is the same code that doesn't break in production, and the discipline behind one is the discipline behind the other.&lt;/p&gt;

&lt;p&gt;If you suspect your code would not survive an outside read, a Diagnostic Sprint lifts the lid before the investor does. &lt;a href="https://revin.com.br/en/diagnostic-sprint" rel="noopener noreferrer"&gt;Worth a look.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What usually shows up in the report
&lt;/h2&gt;

&lt;p&gt;I've read more diligence reports than I'd like, and a few patterns keep repeating. A production secret committed in plain text somewhere in the Git history, where it never truly disappears. One person holding the core of the system in their head, with nothing written down. Libraries two or three major versions behind, carrying a known and public vulnerability. A deploy that hinges on a manual step living in someone's memory.&lt;/p&gt;

&lt;p&gt;Across the projects that reached me for a second opinion this past year, something like two-thirds had at least one secret exposed in the repository. That number is probably skewed, I'll admit: nobody calls me to talk about the diligence that went well. But even cut in half, it's too high for a problem that costs a day to fix and a whole round to discover late.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fumq5897gotuatkjpkcl7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fumq5897gotuatkjpkcl7.jpg" alt="A team around one laptop: code that passes an outside read is built by people who expected one." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A team around one laptop: code that passes an outside read is built by people who expected one.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The report doesn't ask who wrote it
&lt;/h2&gt;

&lt;p&gt;Here's a misunderstanding that trips founders up. A bad diligence result is not a synonym for outsourced engineering. I've seen in-house teams leave a repository in a state that would embarrass any vendor, and I've seen an external squad hand over code the fund's consultant praised in writing. The question that matters is only one: was the code written with discipline, or was it a patch job?&lt;/p&gt;

&lt;p&gt;A patch job has a recognizable signature: the freelancer who showed up for three months and vanished, the studio that shipped the MVP and handed back a boilerplate contract, the agency that billed by the hour and left no test behind. Each one solved the week's problem and none thought about the report coming two years later. A managed senior squad does think, because the same discipline that makes a test pass is the one that gets an IP clause signed and keeps a secret out of the repo.&lt;/p&gt;

&lt;p&gt;Who actually owns the code, by the way, deserves its own piece, and &lt;a href="https://revin.com.br/en/blog/who-actually-owns-your-code" rel="noopener noreferrer"&gt;there is one here.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to that founder
&lt;/h2&gt;

&lt;p&gt;The healthtech closed its round, but smaller than it was worth, with part of the money earmarked to fix what diligence had flagged. She left me with a line I keep repeating: "I paid for the same code twice: once to write it, once to prove it was mine."&lt;/p&gt;

&lt;p&gt;I'm not writing this to scare anyone raising capital. I'm writing it because diligence is just the moment the bill arrives; the spending happened much earlier, in the choice of who built the thing and how. If you want the outside read to be a stamp instead of a shock, the time to deal with it is now, long before the term sheet.&lt;/p&gt;

&lt;p&gt;Want an honest read on the real state of your code before a fund does theirs? &lt;a href="https://revin.com.br/en/schedule-a-call" rel="noopener noreferrer"&gt;Talk to Revin.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;We write about software engineering, managed squads and technical decisions every week at &lt;a href="https://revin.com.br/en/blog?utm_source=devto&amp;amp;utm_medium=syndication" rel="noopener noreferrer"&gt;revin.com.br&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>founders</category>
      <category>softwaredevelopment</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
