{"slug": "write-a-blast-radius-file-before-your-first-ai-patch", "title": "Write a Blast-Radius File Before Your First AI Patch", "summary": "A developer at MonkeyCode outlines a fail-closed strategy for introducing AI-assisted code changes, emphasizing the creation of a blast-radius file before any patch. The approach requires a reversible change, a test proving the legacy path, and a checker script as the sole merge gate.", "body_md": "Your first AI patch should fail closed today. Do not ship a feature on day one. Prove one target file can revert cleanly now.\n\nYou joined a messy repo this morning. The assistant wants a wide rewrite. Your job is a tiny reversible cut only.\n\nThis drill gives you a blast-radius file first. You fill it before any model writes code. Then a short script checks the revert path.\n\nCheap code is not cheap to unwind. One extra import can touch auth. One extra migration can lock deploys.\n\nYou will not know the architecture yet. You also should not pretend otherwise. A blast-radius file makes unknowns explicit fast.\n\nIf the file cannot name a revert, stop. You do not prompt for more code. You shrink the change until revert is boring.\n\nYou will add two artifacts on your branch. Keep both files in the first PR.\n\n`blast_radius.py`\n\n— the contract for this change.`scripts/check_blast_radius.py`\n\n— the fail-closed proof.The contract is the source of truth. The checker is the only merge gate. No green checker means no review yet.\n\nPick one production file you can read. Do not pick a whole folder. Do not pick generated vendor code.\n\n```\ngit ls-files '*.py' '*.ts' '*.go' | head -n 40\nTARGET=src/billing/invoice.py\nwc -l \"$TARGET\"\ngit log -n 5 --oneline -- \"$TARGET\"\n```\n\nRead the last five commits on that file. Write two plain sentences in notes. Pick a smaller file if you cannot yet.\n\nYou now have a hard fence. Everything outside that fence is forbidden. Your assistant may not cross it.\n\nCreate `blast_radius.py`\n\nat the repo root. Keep the dict small. Fill every field with your own hands.\n\n```\n# blast_radius.py\n# Day-one contract. Humans edit this. Models do not.\n\nBLAST = {\n    \"change_id\": \"day-one-001\",\n    \"intent\": \"Add a fail-closed guard on invoice totals.\",\n    \"target_files\": [\n        \"src/billing/invoice.py\",\n        \"tests/billing/test_invoice_flag_off.py\",\n        \"blast_radius.py\",\n        \"scripts/check_blast_radius.py\",\n    ],\n    \"forbidden_globs\": [\n        \"src/auth/**\",\n        \"**/migrations/**\",\n        \"package-lock.json\",\n        \"go.sum\",\n        \"poetry.lock\",\n    ],\n    \"feature_flag\": {\n        \"name\": \"INVOICE_GUARD_V1\",\n        \"default\": \"off\",\n        \"missing_means\": \"old_path\",\n    },\n    \"revert\": {\n        \"strategy\": \"git_revert\",\n        \"notes\": \"Flag off restores the prior totals path.\",\n    },\n    \"tests\": [\n        \"pytest tests/billing/test_invoice_flag_off.py -q\",\n    ],\n    \"max_diff_lines\": 80,\n    \"max_files\": 4,\n    \"base_ref\": \"origin/main\",\n}\n```\n\n`missing_means: old_path`\n\nis the fail-closed rule. A missing flag must not enable new behavior. That single line is the drill.\n\nDo not ask a model for the feature yet. Write the test that proves the old path.\n\n``` python\n# tests/billing/test_invoice_flag_off.py\nimport os\nfrom billing.invoice import compute_total\n\ndef test_missing_flag_uses_legacy_total(monkeypatch):\n    monkeypatch.delenv(\"INVOICE_GUARD_V1\", raising=False)\n    assert \"INVOICE_GUARD_V1\" not in os.environ\n    assert compute_total([100, 20], tax=0.1) == 120.0\n\ndef test_flag_off_uses_legacy_total(monkeypatch):\n    monkeypatch.setenv(\"INVOICE_GUARD_V1\", \"off\")\n    assert compute_total([100, 20], tax=0.1) == 120.0\n```\n\nRun that file once before any patch. Watch it fail for a real reason. Put the exact command in `BLAST[\"tests\"]`\n\n.\n\nIf the test cannot run locally, stop here. Fix the harness before any AI edit. A junior without a test command has no proof.\n\nNow you may use an assistant carefully. Feed it one file, not the tree. Paste `BLAST`\n\nas the hard constraint.\n\nDisclosure: This article was prepared as part of MonkeyCode's product outreach.\n\nMonkeyCode provides free model access and a free server option. Use that loop to draft a patch against `invoice.py`\n\nonly. You still type every revert field yourself.\n\nKeep the prompt short and strict.\n\n```\nRead blast_radius.py.\nEdit only BLAST[\"target_files\"].\nDo not touch BLAST[\"forbidden_globs\"].\nHonor feature_flag default = off.\nA missing flag must use the old path.\nKeep the diff under max_diff_lines.\nReturn a unified diff, nothing else.\n```\n\nReject any answer that adds extra files. Reject lockfile churn without discussion. Reject \"while we are here\" cleanups on sight.\n\nSave this as `scripts/check_blast_radius.py`\n\n. Run it with Python 3.\n\n``` bash\n#!/usr/bin/env python3\n\"\"\"Fail the PR when the diff escapes the blast radius.\"\"\"\n\nfrom __future__ import annotations\n\nimport fnmatch\nimport subprocess\nimport sys\nfrom pathlib import Path\n\nROOT = Path(__file__).resolve().parents[1]\nsys.path.insert(0, str(ROOT))\nfrom blast_radius import BLAST  # noqa: E402\n\ndef git(*args: str) -> str:\n    return subprocess.check_output([\"git\", *args], cwd=ROOT, text=True)\n\ndef main() -> int:\n    base_ref = BLAST[\"base_ref\"]\n    try:\n        base = git(\"merge-base\", \"HEAD\", base_ref).strip()\n    except subprocess.CalledProcessError:\n        print(f\"cannot resolve merge-base with {base_ref}\", file=sys.stderr)\n        return 2\n\n    names = [n for n in git(\"diff\", \"--name-only\", base).splitlines() if n]\n    allowed = set(BLAST[\"target_files\"])\n    forbidden = BLAST[\"forbidden_globs\"]\n\n    for name in names:\n        for pat in forbidden:\n            if fnmatch.fnmatch(name, pat):\n                print(f\"forbidden path in diff: {name}\")\n                return 1\n        if name not in allowed:\n            print(f\"file outside blast radius: {name}\")\n            return 1\n\n    if len(names) > BLAST[\"max_files\"]:\n        print(f\"too many files: {len(names)}\")\n        return 1\n\n    changed = 0\n    for row in git(\"diff\", \"--numstat\", base).splitlines():\n        if not row.strip():\n            continue\n        added, deleted, name = row.split(\"\\t\", 2)\n        if added == \"-\" or deleted == \"-\":\n            print(f\"binary file not allowed: {name}\")\n            return 1\n        changed += int(added) + int(deleted)\n\n    if changed > BLAST[\"max_diff_lines\"]:\n        print(f\"diff too large: {changed} lines\")\n        return 1\n\n    flag = BLAST[\"feature_flag\"]\n    if flag.get(\"default\") != \"off\":\n        print(\"feature flag must default off\")\n        return 1\n    if flag.get(\"missing_means\") != \"old_path\":\n        print(\"missing flag must mean old_path\")\n        return 1\n\n    print(\"blast-radius contract: ok\")\n    for cmd in BLAST[\"tests\"]:\n        print(f\"+ {cmd}\")\n        subprocess.check_call(cmd, shell=True, cwd=ROOT)\n    print(\"fail-closed checks passed\")\n    return 0\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\npython3 scripts/check_blast_radius.py\n```\n\nThe script is strict on purpose. It should fail your first attempt. That failure is the actual lesson here.\n\nDo this on a throwaway branch today. Do not wait for production traffic later.\n\n```\ngit checkout -b drill/day-one-guard\ngit add blast_radius.py scripts/check_blast_radius.py \\\n        src/billing/invoice.py tests/billing/test_invoice_flag_off.py\ngit commit -m \"feat: invoice guard behind fail-closed flag\"\n\npython3 scripts/check_blast_radius.py\n\nSHA=$(git rev-parse HEAD)\ngit revert --no-edit \"$SHA\"\nINVOICE_GUARD_V1=off pytest tests/billing/test_invoice_flag_off.py -q\ngit log -n 3 --oneline\n```\n\nYou must see the old test pass after revert. If it fails, the flag is not fail-closed. Fix that before you open any PR.\n\nReset the rehearsal when the proof is green.\n\n```\ngit checkout -B drill/day-one-guard \"$SHA\"\n```\n\nNever force-push this proof to main. Keep the revert commit on the drill branch. Copy only the lesson into your real PR.\n\nYour PR body should quote the blast-radius fields. Reviewers need the fence, not a long story.\n\n```\n## Blast radius\n- Files: src/billing/invoice.py\n- Flag: INVOICE_GUARD_V1 default off\n- Missing flag: legacy totals\n- Revert: git revert of this SHA\n\n## Proof\n- python3 scripts/check_blast_radius.py\n- pytest tests/billing/test_invoice_flag_off.py -q\n```\n\nAsk the reviewer one question only today. Do not ask them to love the design.\n\nUse this table when the assistant argues for more files.\n\n| Model request | You do | Why it fails closed |\n|---|---|---|\n| Touch auth \"just in case\" | Refuse | Forbidden path |\n| Add a migration | Refuse | Not reversible today |\n| Rename a package | Refuse | Diff too wide |\n| Refresh a lockfile | Refuse | Hidden blast radius |\n| Default the flag on | Refuse | Not fail-closed |\n| Edit one function plus tests | Allow | Inside the fence |\n| Split work into two PRs | Allow | Shrinks revert |\n\nPrint the table beside your editor. Point at it when the model rambles on. Your job is the fence, not speed.\n\nThe checker says it cannot resolve merge-base. Fetch main, then retry the script once.\n\n```\ngit fetch origin main\ngit rev-parse origin/main\npython3 scripts/check_blast_radius.py\n```\n\nThe checker says a file sits outside blast radius. You added a helper file by accident. Either shrink the patch or update `target_files`\n\nwith intent.\n\nThe old test still passes with the flag on. Your new path is not isolated yet. Put the new logic behind the env read first.\n\n``` python\nimport os\n\n# Proposal only. Unexecuted sample for a fail-closed read.\ndef compute_total(items, tax):\n    flag = os.getenv(\"INVOICE_GUARD_V1\", \"off\")\n    if flag != \"on\":\n        return sum(items) * (1 + tax)\n    return guarded_total(items, tax)\n```\n\nTreat that snippet as unexecuted sample code only. Wire it to your real totals function after tests exist.\n\nThis drill will not teach system architecture. It will not catch semantic money bugs. It only bounds files, flags, and revert commands.\n\nThe script trusts `origin/main`\n\nas merge base. Forks with `origin/master`\n\nmust change `base_ref`\n\nnow. You need git, Python 3, and pytest on PATH.\n\nA comment is not a feature flag. An env var is a minimum bar. If your team has a flag service, use that name instead.\n\nFree model access does not replace human review. A free server does not own production. You still run the tests locally.\n\nDo not use this as a senior design review. Do not use it during live incident response. Do not use it to rubber-stamp generated refactors.\n\nSkip it if you cannot run tests locally. Skip it if revert needs a force-push. Skip it if the change is a data migration.\n\nSecurity patches need a different fence. Secret rotation is not a day-one AI drill. Ask a teammate before those changes.\n\nYou leave with a tiny, reversible PR. You also leave with a revert you already ran. That is enough work for day one.\n\nTomorrow you may widen the fence one file. You still start from `blast_radius.py`\n\n. Cheap code stays cheap only when revert is boring.", "url": "https://wpnews.pro/news/write-a-blast-radius-file-before-your-first-ai-patch", "canonical_source": "https://dev.to/gitgo_5662/write-a-blast-radius-file-before-your-first-ai-patch-ho5", "published_at": "2026-09-03 15:58:12+00:00", "updated_at": "2026-09-03 16:27:48.069093+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-safety"], "entities": ["MonkeyCode"], "alternates": {"html": "https://wpnews.pro/news/write-a-blast-radius-file-before-your-first-ai-patch", "markdown": "https://wpnews.pro/news/write-a-blast-radius-file-before-your-first-ai-patch.md", "text": "https://wpnews.pro/news/write-a-blast-radius-file-before-your-first-ai-patch.txt", "jsonld": "https://wpnews.pro/news/write-a-blast-radius-file-before-your-first-ai-patch.jsonld"}}