DEV Community

Dakota Huang
Dakota Huang

Posted on

The Behavior Fingerprint: One Command That Gates Every Legacy Refactor

A refactor is safe when the behavior fingerprint is unchanged. The fingerprint is a hash of probe outputs. One command computes it. One command verifies it. Everything else is ceremony.

Local baselines lie. Your laptop has a different locale. Your Python version differs from CI. A missing system library changes an exception type. The baseline you captured at 9 AM fails at 9 PM. The refactor gate must run in a clean environment.

This article shows a three-part workflow: probe, fingerprint, gate. The probe captures current behavior. The fingerprint hashes it. The gate compares baseline and current in one command.

Step 1: Freeze the Messy State

Create a baseline branch. This is your reference point. It never moves during the refactor.

git checkout -b baseline
git push origin baseline
git checkout main
Enter fullscreen mode Exit fullscreen mode

The baseline branch is the truth. Every change on main is measured against it. Do not commit to baseline again.

Step 2: Draft the Probe

A probe calls a legacy function with captured inputs. It records outputs. It records exceptions. It does not judge. It only reports.

# probe.py
import json, sys

cases_path, out_path = sys.argv[1], sys.argv[2]
from legacy import price

cases = json.load(open(cases_path))
results = []
for case in cases:
    try:
        results.append({
            "case": case,
            "out": price(case["item"], case["qty"]),
            "err": None,
        })
    except Exception as exc:
        results.append({
            "case": case,
            "out": None,
            "err": type(exc).__name__,
        })
json.dump(results, open(out_path, "w"), indent=2)
print(f"probed {len(results)} cases -> {out_path}")
Enter fullscreen mode Exit fullscreen mode

Where do cases come from? Production logs. API traces. Old tickets. Call sites in the repo. Start with ten cases. Add more when the gate says "different" and you do not know why.

A free model can draft this harness fast. MonkeyCode's free model access is enough for this task. Give it the function signature and three sample cases. Review the output. The probe is the contract. A wrong probe is worse than no probe.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Step 3: Hash the Behavior

The fingerprint is a SHA-256 hash of the probe output file. Two identical outputs produce two identical hashes. One changed exception type produces a different hash.

python probe.py cases.json /tmp/baseline.json
sha256sum /tmp/baseline.json
Enter fullscreen mode Exit fullscreen mode

Do not sort the probe output. Order is part of the behavior. A reordered loop changes the fingerprint. That is the point.

Step 4: Run the Gate

The gate script runs the probe twice. Once on the baseline branch. Once on your working tree. Then it compares the hashes.

#!/usr/bin/env bash
# refactor_gate.sh — run in a clean environment
set -euo pipefail

REPO_DIR="${1:?usage: refactor_gate.sh <repo-dir>}"
CASES="${2:-cases.json}"
PROBE="${3:-probe.py}"
WORKTREE="$(mktemp -d)"

trap 'git worktree remove "$WORKTREE" --force 2>/dev/null || true' EXIT

cd "$REPO_DIR"
git worktree add "$WORKTREE" baseline >/dev/null

echo "==> baseline fingerprint"
(cd "$WORKTREE" && python "$PROBE" "$CASES" /tmp/baseline.json)

echo "==> current fingerprint"
python "$PROBE" "$CASES" /tmp/current.json

python - <<'PY'
import hashlib, sys
def fp(path):
    return hashlib.sha256(open(path, "rb").read()).hexdigest()
b = fp("/tmp/baseline.json")
c = fp("/tmp/current.json")
print(f"baseline {b}")
print(f"current  {c}")
sys.exit(0 if b == c else 1)
PY
Enter fullscreen mode Exit fullscreen mode

Run it once before any change. It must exit 0. This validates the harness. If it fails, fix the probe. Do not touch the code yet.

Run it on a clean server. MonkeyCode's free server option gives you that. Same code. Same inputs. Same environment. The environment stops being a variable. Local baselines lie. Clean servers do not.

Step 5: Change, Gate, Commit

Now the loop. One small change per cycle.

  1. Make one rename, extraction, or branch inversion.
  2. Run refactor_gate.sh.
  3. Exit 0 means behavior unchanged. Commit.
  4. Exit 1 means behavior changed. Inspect the diff. Revert or add a regression test.
./refactor_gate.sh . cases.json probe.py
Enter fullscreen mode Exit fullscreen mode

The gate is the commit condition. No gate, no commit. This is not a suggestion. It is the workflow.

Decision Table

Gate exit Fingerprint Meaning Action
0 match behavior unchanged commit
1 differ behavior changed revert or test
1 probe error harness broken fix probe

The table is small because the gate is binary. That is its strength. No judgment calls during a refactor. Just a hash.

Limitations

The fingerprint proves stability. It does not prove correctness. A bug in the legacy function is preserved. You are not fixing behavior. You are freezing it.

Do not use this for security fixes. Security fixes change behavior on purpose. The gate will block them. Write a regression test instead.

Do not use this for large rewrites. The gate answers "same or different". It does not answer "better". A 2,000-line rewrite needs design review, not a hash.

Do not trust a probe you did not read. A free model drafts fast. It also drafts wrong. Read the harness. Run it on a copy. Then trust the fingerprint.

The Loop

Freeze. Probe. Fingerprint. Gate. Change. Repeat. Each cycle is small. Each cycle is measured. That is how a messy repo becomes clean without a big-bang rewrite.

The gate is the point. If you cannot prove the change is safe, you are not ready to make it. Measure first. Edit second. Try the gate on your messiest file this week.

Top comments (0)