{"slug": "i-don-t-review-ai-code-my-build-does", "title": "I don't review AI code. My build does.", "summary": "A developer built a 169-line Node.js verification script to automatically catch bugs in AI-generated code, addressing the impracticality of manually reviewing every line. The script checks for dead links, missing alt attributes, duplicate H1s, and other issues, and is integrated into the CI pipeline so failing branches never deploy.", "body_md": "*Originally published on indiecore.net.*\n\nThere's a ratio I spent a while pretending not to know about. Claude produces a plausible\n\ntwo-hundred-line diff in roughly a minute. Reading two hundred lines of plausible code\n\nproperly — not skimming for shape, actually checking it — takes me fifteen or twenty. That\n\ngap is the whole problem with \"never merge AI-generated code without reading it,\" which is\n\nadvice I agree with and could not follow.\n\nFor a few months I scrolled diffs, recognised the shape of things, and merged. It felt like\n\nreviewing.\n\nWhat eventually gave it away was the *kind* of bug getting through. None of them were subtle\n\nlogic errors that careful reading would have caught. They were dull, mechanical things: a\n\ndead internal link left behind by a route rename, an `<img>`\n\nwith no `alt`\n\n, a canonical URL\n\npointing at the wrong host, two cache rules that turned out to concatenate. Every one was\n\nsomething a fifty-line script could find in milliseconds, and none of them were things I was\n\never going to reliably catch by eye at eleven at night.\n\nSo I stopped trying to read faster.\n\n`scripts/verify.mjs`\n\nis 169 lines of dependency-free Node. It walks the built `dist/`\n\ndirectory and exits non-zero on anything I've decided must never ship.\n\n```\nnpm run check      # build + verify — the same gate CI runs\n```\n\nIt runs on every pull request and the deploy job depends on it, so a branch that fails it\n\ndoesn't reach the internet. Not because I'm disciplined; because the pipeline is wired that\n\nway. (That plumbing is in [the Cloudflare Workers post](https://www.indiecore.net/blog/static-site-cloudflare-workers/),\n\nalong with the five traps that cost me an evening.)\n\nThe link and image checks are the whole idea in miniature. Resolve every site-absolute URL\n\nagainst what's actually on disk:\n\n``` js\n/** does a site-absolute URL resolve to a real file? */\nconst resolves = url => {\n  const clean = url.split('#')[0].split('?')[0];\n  if (!clean || clean === '/') return fs.existsSync(path.join(DIST, 'index.html'));\n  const p = path.join(DIST, clean);\n  if (fs.existsSync(p) && fs.statSync(p).isFile()) return true;\n  return fs.existsSync(path.join(p, 'index.html'));\n};\n\nfor (const href of attr(html, /href=\"(\\/[^\"#][^\"]*)\"/g)) {\n  if (!resolves(href)) fail(where, `dead internal link → ${href}`);\n}\n```\n\nNothing clever. It's the kind of check that's tedious for a person to do on every page of\n\nevery build and free for a machine to do forever.\n\nI didn't sit down and write a best-practices linter. Each rule went in on the day something\n\ngot past me:\n\n| The check | What it's remembering |\n|---|---|\nexactly one `<h1>` per page |\na generated template that emitted two |\n`alt` on every `<img>`\n|\naccessibility regressions Lighthouse caught days later |\n| dead internal links | a renamed route that left pages pointing at a 404 |\n`_redirects` targets resolve |\na redirect pointing at a page I'd since deleted |\n| no placeholder or filler copy | draft text left in a page, one merge from production |\n| Blogger markup in output | migration leftovers surfacing weeks after the migration |\nevery page listed in `sitemap.xml`\n|\nnew pages that silently never got submitted |\n\n**Resist the urge to write the exhaustive rulebook up front.** I tried; you end up with forty\n\nrules, six of which ever fire, and the other thirty-four generate enough noise that you stop\n\nreading the output at all. Which is worse than having no verifier, because now you also\n\nbelieve you have one.\n\nThe rules I'd least want to lose guard things I would never notice by looking at the site,\n\nbecause the site looks fine.\n\nThis one validates the IndexNow key file:\n\n```\n// scripts/seo-ping.mjs submits URLs under this key; the crawlers reject the\n// submission unless the matching file is live at the site root.\nconst key = fs.readFileSync(KEY_FILE, 'utf8').trim();\nif (!/^[a-f0-9]{8,128}$/.test(key)) fail('indexnow', `key is not 8-128 hex chars: \"${key}\"`);\nelse if (!fs.existsSync(p))         fail('indexnow', `missing key file /${key}.txt`);\n```\n\nAsk an agent to \"add IndexNow submission\" and you'll get a correct submission script and no\n\nkey file, because nothing in the request mentions one. It isn't being careless. It has no way\n\nto know what the silent failure costs, and a missing key file fails silently by design — the\n\ncrawler just ignores you. Same story with `app-ads.txt`\n\n, which is read by ad networks and\n\nnobody else, and which is now parsed field-by-field at build time against the IAB spec.\n\nAlongside the verifier there's a Lighthouse budget, and it isn't negotiable:\n\n```\nperformance ≥ 90, accessibility ≥ 100, best-practices ≥ 100, seo ≥ 100\n```\n\nThis catches the failure the verifier can't: slow drift. No single change makes a site slow;\n\ntwenty changes, each fine on its own, do, and you never see the moment it happens because you\n\nwere looking at diffs rather than at scores.\n\nA real one: the grey I use for code comments in these posts failed WCAG contrast. It looked\n\ndeliberate. Honestly, it looked good. One-line fix, and I would never have found it by\n\nreading a stylesheet — the budget found it on the pull request (commit `d64bce0`\n\n, if you're\n\ncurious).\n\nPerformance measurement on shared CI runners is noisy, so the gate re-measures instead of\n\nfailing on one bad sample:\n\n```\n// Only performance is re-measured, and only when it misses.\nwhile (scores.performance < BUDGET.performance && attempts < RETRIES) { … }\n```\n\nThat detail matters more than it looks. **A gate that fails at random gets ignored, then\ndisabled, then deleted.** Flaky checks teach you to click through failures, which is the exact\n\nOne thing I'd underrated. Compare:\n\n```\n  ERROR  /blog/index.html: dead internal link → /games/word-slot\n```\n\nwith a generic \"validation failed\". The first I can paste straight into a session and get a\n\ncorrect fix in one turn; the second starts a conversation. Good error messages were always\n\nworth writing. They're worth roughly double now, because the agent reads them too, and a\n\nprecise message is the difference between a fix and a guess.\n\nIt has no opinion about whether the code is any good. It won't tell me a function is a mess,\n\nthat an abstraction is wrong, or that the feature was a bad idea in the first place. That's\n\nstill my job, and now it's the only reviewing job I have — which is a better trade than it\n\nsounds, because the mechanical layer is where all the volume is.\n\nIt also can't tell me the agent touched something it shouldn't have gone near at all. That's\n\na different problem and I handle it differently; it's in\n\n[the blast radius rule](https://www.indiecore.net/blog/blast-radius-rule-ai-coding/).\n\nThe transferable bit isn't \"write a verifier for your static site.\" It's the question I now\n\nask every time I catch something in a diff: could a script have caught this? When the answer\n\nis yes, writing the script beats remembering the lesson, because I don't reliably remember\n\nlessons and the script doesn't get tired.\n\nOriginally published at ** I don't review AI code. My build does.**.\n\nThe code is on GitHub: [Both scripts, ready to drop in](https://gist.github.com/IndieCoreDev/6c707bf89e5d215225f3bdac6e4a4b25)", "url": "https://wpnews.pro/news/i-don-t-review-ai-code-my-build-does", "canonical_source": "https://dev.to/indiecoredev/i-dont-review-ai-code-my-build-does-436d", "published_at": "2026-09-01 22:47:16+00:00", "updated_at": "2026-09-01 23:23:19.387074+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Claude", "Cloudflare Workers", "IndexNow"], "alternates": {"html": "https://wpnews.pro/news/i-don-t-review-ai-code-my-build-does", "markdown": "https://wpnews.pro/news/i-don-t-review-ai-code-my-build-does.md", "text": "https://wpnews.pro/news/i-don-t-review-ai-code-my-build-does.txt", "jsonld": "https://wpnews.pro/news/i-don-t-review-ai-code-my-build-does.jsonld"}}