{"slug": "the-golden-file-refactor-loop-record-verify-move-commit", "title": "The Golden-File Refactor Loop: Record, Verify, Move, Commit", "summary": "A developer introduced a golden-file refactor loop to safely modify a legacy 600-line function by recording its behavior and verifying each change against snapshots. The technique uses four commands—record, verify, move, commit—and a diff budget to enforce small, semantic changes. The approach replaces unit tests with recorded actual behavior, which is more honest when intent is unclear.", "body_md": "You do not understand the messy function. That is fine. The snapshot does not care about your understanding. Golden files turn \"I think this is safe\" into \"the diff says so.\" This loop has four commands: record, verify, move, commit. Each move is one semantic change. The snapshot judges every move.\n\nA 600-line function hides its contract. Callers see the return value. They also see database writes, emails, and exceptions. Human reviewers guess about those side effects. AI reviewers guess with more confidence. Neither can prove the behavior is identical. A golden file can.\n\nThe golden file is a recorded behavior. It stores the return value and side effects for a fixed input. It is not a unit test. It encodes no intent. It only says: this is what the code did on this input. That is enough for a refactor.\n\nChoose one entry point. This walkthrough uses `import_orders(raw_rows)`\n\nfrom a legacy module. The function parses rows, writes to a database, sends emails, and returns a list. That is the boundary contract.\n\n``` python\ndef import_orders(raw_rows):\n    orders = []\n    for row in raw_rows:\n        if not row.get(\"sku\"):\n            continue\n        qty = int(row.get(\"qty\") or 1)  # legacy default: None -> 1\n        if qty < 0:\n            qty = 1\n        order = {\"sku\": row[\"sku\"], \"qty\": qty}\n        # 200 more lines: logging, dedup, DB writes, mail\n        orders.append(order)\n    return orders\n```\n\nRecord four things per input: the return value, the DB writes, the emails, and the exception type. The recorder replaces the module's side effects with logging stubs.\n\n``` python\n# tools/characterize.py\nimport json\nimport sys\nfrom pathlib import Path\n\nimport legacy  # the messy module\n\ndef run_case(payload):\n    events = []\n    legacy.db.insert = lambda row: events.append((\"db\", row))\n    legacy.mailer.send = lambda mail: events.append((\"mail\", mail))\n    try:\n        result = legacy.import_orders(payload)\n        return {\"return\": result, \"events\": events, \"error\": None}\n    except Exception as exc:\n        return {\"return\": None, \"events\": events, \"error\": type(exc).__name__}\n\ndef main(action, snapshot_dir, fixture_dir):\n    snap = Path(snapshot_dir)\n    for case in sorted(Path(fixture_dir).glob(\"*.json\")):\n        record = run_case(json.loads(case.read_text()))\n        golden = snap / f\"{case.stem}.golden.json\"\n        if action == \"record\":\n            golden.write_text(json.dumps(record, sort_keys=True, indent=2))\n            continue\n        if golden.read_text() != json.dumps(record, sort_keys=True, indent=2):\n            print(f\"BEHAVIOR CHANGED: {case.stem}\", file=sys.stderr)\n            sys.exit(1)\n    print(\"snapshot verified\")\n\nif __name__ == \"__main__\":\n    main(sys.argv[1], sys.argv[2], sys.argv[3])\n```\n\nThe stubs assume the function references module attributes. If it imports helpers directly, patch those helpers instead. The principle stays the same: record, do not trust.\n\nThe fixtures determine the value of the whole loop. Replay logged production payloads if you have them. One week of real rows beats ten hand-written cases. Weak fixtures create fake confidence. Make sure the corpus hits every branch, every error path, and the `None`\n\nquantity default. The snapshot can only judge what the corpus exercises.\n\n```\npython tools/characterize.py record snapshots fixtures\npython tools/characterize.py verify snapshots fixtures\n# -> snapshot verified\n```\n\nWhy not write unit tests instead? Unit tests encode expected behavior. Golden files record actual behavior. For a messy function, intent is exactly what you lack. Recording is faster and more honest than guessing the contract in advance.\n\nNow the current behavior exists as files on disk. From this point, the snapshot is the specification.\n\n\"Smallest\" needs a measurable definition. Use a diff budget. The budget allows one file and a bounded number of changed lines per move. This shell gate enforces it:\n\n``` bash\n# tools/refactor.sh\n#!/usr/bin/env bash\nset -euo pipefail\nMAX_CHURN=80\n\nverify() { python tools/characterize.py verify snapshots fixtures; }\n\nbudget() {\n  local churn files\n  churn=$(git diff --numstat -- '*.py' | awk '{a+=$1; d+=$2} END {print a+d}')\n  files=$(git diff --name-only -- '*.py' | wc -l)\n  if (( churn > MAX_CHURN )); then\n    echo \"budget exceeded: $churn lines\" >&2; exit 1\n  fi\n  if (( files > 1 )); then\n    echo \"one file per move\" >&2; exit 1\n  fi\n}\n\n\"$@\"\n```\n\nThe loop becomes mechanical:\n\n```\ngit checkout -b refactor/validate-first\n# apply one model-proposed move\n./tools/refactor.sh verify\n./tools/refactor.sh budget\ngit add legacy.py\ngit commit -m \"extract row validation into a pure function\"\n```\n\nEach commit message names exactly one move. \"Extract validation.\" \"Rename `order`\n\nto `parsed`\n\n.\" \"Split the import loop.\" One verb per commit. Never \"cleanup.\" Never \"refactor import_orders.\"\n\nEvery move starts as a narrow prompt. One prompt asks for exactly one change: \"Extract the validation block into a pure function. Change nothing else.\" This loop runs dozens of prompts per refactor. Free model access keeps that prompt cost at zero. MonkeyCode's free model access covers these small proposals. The model proposes; the snapshot disposes.\n\nDisclosure: This article was prepared as part of MonkeyCode's product outreach.\n\nIf verify turns red, the patch is wrong for this codebase. Model confidence does not matter. The golden file does. If you want the experiment fully isolated, MonkeyCode's free server option offers a clean environment for the verify step. Apply the candidate patch there, run the check, inspect the diff. Promote the accepted move to your branch only after it passes. Keep the prompt narrow. One move per prompt. If the model returns a multi-move patch, split it or reject it. The budget gate will reject it anyway.\n\nHere is a failure mode this loop catches reliably. The legacy code treats `qty=None`\n\nas 1. The model's extraction \"fixed\" it to raise `ValueError`\n\n. Return-value tests stayed green. The golden file caught the new exception immediately. The bug was in the patch, not in the code. This is the whole point of the loop.\n\n| Move | Snapshot sensitivity | Risk |\n|---|---|---|\n| Extract a pure block | low | low |\n| Rename a local variable | none | negligible |\n| Reorder two DB writes | high | medium |\n| Change an exception path | high | high |\n| Inline a cached value | medium | medium |\n\nPure extraction and renames are cheap. Reordering side effects is not. Exception paths are the most dangerous. The snapshot records all of them.\n\nGolden files freeze bugs too. If the refactor must fix a bug, update the golden file deliberately. Write the reason in the commit message. Do not let the snapshot bless the bug and the fix at the same time.\n\nFlaky behavior breaks the loop. Timestamps, random IDs, and network calls poison golden files. Seed randomness and stub time before recording. If the boundary is genuinely nondeterministic, this loop is the wrong tool.\n\nThe diff budget is a proxy, not a proof. An 80-line change can still break behavior. A 400-line move can be perfectly safe. The budget enforces discipline, not correctness. Run the verify command in CI. Add it to the pre-commit hook. The snapshot becomes a regression net for the whole branch.\n\nWho should not use this? Greenfield code. Tiny functions with two callers. Urgent behavior changes. In those cases, ordinary tests beat golden files. Snapshot-first is for code bases where fear is the dominant emotion.\n\n```\ngit checkout -b refactor/one-move\n./tools/refactor.sh verify    # red before you start? fix the harness first\n# apply one model-proposed move\n./tools/refactor.sh verify    # green? the move is safe\n./tools/refactor.sh budget    # within budget? commit it\ngit commit -m \"one move\"\n```\n\nPick the function you avoid opening. Run this loop once. The golden files will argue with you. Let them win.", "url": "https://wpnews.pro/news/the-golden-file-refactor-loop-record-verify-move-commit", "canonical_source": "https://dev.to/hackrs_6393/the-golden-file-refactor-loop-record-verify-move-commit-48bm", "published_at": "2026-08-29 06:01:23+00:00", "updated_at": "2026-08-29 06:18:46.511615+00:00", "lang": "en", "topics": ["developer-tools"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/the-golden-file-refactor-loop-record-verify-move-commit", "markdown": "https://wpnews.pro/news/the-golden-file-refactor-loop-record-verify-move-commit.md", "text": "https://wpnews.pro/news/the-golden-file-refactor-loop-record-verify-move-commit.txt", "jsonld": "https://wpnews.pro/news/the-golden-file-refactor-loop-record-verify-move-commit.jsonld"}}