DEV Community

Dakota Huang
Dakota Huang

Posted on

Freeze the Repo Seam Before You Cut a Single File

A messy repo fails at the seam, not the function. Freeze that observable seam before any file move. Then apply exactly one mechanical cut this session.

Rewrites collapse because they change many files at once. Callers outside your editor never appear in diffs. The freeze is the oracle those callers needed.

This workflow does not pin a single function. Function pinning belongs to a different tutorial. This one pins the process boundary of a whole tree.

Use it when mess spans files, folders, and re-exports. Skip it when one helper is the only risk.

The core conclusion

Treat every refactor as a cut against a frozen seam. If the seam hash moves, the cut is too large. If the seam hash holds, stop for the day.

Large language models make huge diffs cheap to propose. Cheap diffs are not automatically safe diffs. Safety lives in unchanged bytes at the seam.

What counts as a seam

A seam is any byte stream a stranger already consumes. Pick one seam. Do not freeze private helpers.

  1. CLI stdout and stderr for a fixed argv matrix.
  2. HTTP JSON for a recorded request cassette.
  3. Package exports that other repos already import.

File-private helpers are not a seam. Changing them stays allowed during a cut. Changing seam bytes is a product change.

Constructed example: a tangled inventory CLI

The tree below is a teaching fixture, not production code. It mimics a repo that grew one file at a time. Pricing, tax, and discounts share one god module.

// inventory_cli.js — constructed messy entry
const { quote } = require("./quote_engine");

function main(argv) {
  const sku = argv[2];
  const qty = Number(argv[3] || "1");
  const region = argv[4] || "US";
  const result = quote(sku, qty, region);
  process.stdout.write(JSON.stringify(result) + "\n");
}

if (require.main === module) {
  main(process.argv);
}

module.exports = { main };
Enter fullscreen mode Exit fullscreen mode
// quote_engine.js — constructed god module
const catalog = {
  A100: { base: 1299, category: "hw" },
  B200: { base: 499, category: "sw" },
  C300: { base: 50, category: "addon" }
};

let coupon = null;

function setCoupon(code) {
  coupon = code;
}

function taxRate(region) {
  if (region === "US") return 0.08;
  if (region === "EU") return 0.2;
  if (region === "UK") return 0.2;
  return 0.0;
}

function discount(sku, qty) {
  let off = 0;
  if (qty >= 10) off += 0.1;
  if (catalog[sku] && catalog[sku].category === "sw") off += 0.05;
  if (coupon === "SAVE10") off += 0.1;
  return Math.min(off, 0.25);
}

function quote(sku, qty, region) {
  const item = catalog[sku];
  if (!item) return { ok: false, error: "unknown_sku" };
  const off = discount(sku, qty);
  const net = Math.round(item.base * qty * (1 - off));
  const tax = Math.round(net * taxRate(region));
  return { ok: true, sku, qty, region, net, tax, total: net + tax };
}

module.exports = { quote, setCoupon, taxRate, discount, catalog };
Enter fullscreen mode Exit fullscreen mode

The public seam is CLI JSON only. Internal helpers are not the seam. Your first job is recording that JSON.

Artifact: the seam freeze harness

Build a fixture corpus before you touch product files. Each line is one argv tuple. The harness runs the CLI and stores canonical JSON.

# fixtures/argv.tsv
A100    1   US
A100    10  US
B200    1   EU
B200    10  UK
C300    3   US
Z999    1   US
A100    1   XX
Enter fullscreen mode Exit fullscreen mode
// scripts/freeze.js — constructed harness
const { spawnSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");

const root = path.join(__dirname, "..");
const check = process.argv.includes("--check");
const lines = fs
  .readFileSync(path.join(root, "fixtures/argv.tsv"), "utf8")
  .trim()
  .split("\n");

const rows = [];
for (const line of lines) {
  const args = line.split("\t");
  const run = spawnSync("node", ["inventory_cli.js", ...args], {
    cwd: root,
    encoding: "utf8"
  });
  rows.push({
    args,
    status: run.status,
    stdout: run.stdout,
    stderr: run.stderr
  });
}

const canonical = JSON.stringify(rows, null, 2);
const digest = crypto.createHash("sha256").update(canonical).digest("hex");
const freezeDir = path.join(root, "freeze");
const digestPath = path.join(freezeDir, "seam.sha256");

if (check) {
  const expected = fs.readFileSync(digestPath, "utf8").trim();
  if (digest !== expected) {
    process.stderr.write("seam moved\n");
    process.stderr.write(canonical);
    process.exit(1);
  }
  process.stdout.write("seam held\n");
  process.exit(0);
}

fs.mkdirSync(freezeDir, { recursive: true });
fs.writeFileSync(path.join(freezeDir, "seam.json"), canonical);
fs.writeFileSync(digestPath, digest + "\n");
process.stdout.write(digest + "\n");
Enter fullscreen mode Exit fullscreen mode

Record once on the current tree. Commit freeze/seam.json and freeze/seam.sha256. After that, every cut must keep the digest stable.

node scripts/freeze.js
git add fixtures freeze scripts inventory_cli.js quote_engine.js
git commit -m "freeze inventory CLI seam"
Enter fullscreen mode Exit fullscreen mode

Later checks must not rewrite the committed snapshot. Use the check flag instead.

node scripts/freeze.js --check
Enter fullscreen mode Exit fullscreen mode

Decision table: the smallest safe cut

Score the next edit against this table. Pick the lowest rank that still reduces mess. Reject anything that alters seam bytes.

Rank Cut Allowed if Reject if
1 Delete a file-private helper Freeze still holds The helper formats seam JSON
2 Extract a function in-file Signature stays local New arguments change quotes
3 Move a helper to a new file Old file re-exports behavior Callers must change import paths
4 Split a god module Public CLI JSON is identical Rounding or tax math changes
5 Rename an internal symbol No exported name changes The name is a JSON key
6 Change algorithms or rates Never during a refactor You want a product change

Ranks 1 through 5 are refactors. Rank 6 is a release. Do not mix them in one commit.

Numbered workflow

1. Map the seam in writing

List the command, the argv matrix, and the output encoding. One short paragraph is enough. If two seams exist, freeze them in two commits.

Mixing seams hides the failing cut. Name the process you will run. Leave every other process out.

2. Record the freeze on a clean tree

Do not edit product files in this step. Only add fixtures and the harness. The digest is your baseline.

Time, Math.random, and live HTTP break hashes. Stub those at the boundary before recording. If you cannot stub them, stop here.

This method does not apply to noisy CLIs. Fix determinism first. Then record the freeze.

3. Classify one cut with the table

Write the rank in the commit message. Example: rank-3 extract taxRate into tax.js. If you cannot name a single rank, the cut is too large.

Split the work until one rank remains. Two ranks in one diff hide the mover. The checker cannot tell you which edit failed.

4. Execute the cut with re-exports

Keep old import paths working. New files are allowed. Deleted paths are not allowed yet.

Re-export from the previous module so silent callers survive. That is the whole point of rank 3.

// tax.js — constructed extract
function taxRate(region) {
  if (region === "US") return 0.08;
  if (region === "EU") return 0.2;
  if (region === "UK") return 0.2;
  return 0.0;
}

module.exports = { taxRate };
Enter fullscreen mode Exit fullscreen mode
// quote_engine.js — after rank-3 cut (excerpt)
const { taxRate } = require("./tax");
// discount, quote, and catalog remain in this file
module.exports = { quote, setCoupon, taxRate, discount, catalog };
Enter fullscreen mode Exit fullscreen mode

This is a file cut, not a rewrite. quote still rounds the same way. JSON keys do not change.

5. Re-run the freeze before any second cut

node scripts/freeze.js --check
Enter fullscreen mode Exit fullscreen mode

If the checker prints seam held, keep the commit. If it prints seam moved, revert the cut. Do not edit fixtures to match a new digest.

Fixture edits are product decisions. They belong in a rank-6 commit. A refactor commit must not rotate goldens.

6. Expand the corpus only on a separate commit

New argv rows are not a refactor. They are test design. Add them, regenerate the freeze, and commit that alone.

A thin corpus lies to you. Seven rows cannot see a hidden branch. Add rows for every region, quantity band, and unknown SKU.

Using a free model only for extra fixtures

You can ask a coding model for missing argv combinations. Do not ask it to rewrite quote_engine.js. The model has no oracle. The freeze does.

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

MonkeyCode provides free model access and a free server option. Those two facts matter here for one reason. You can draft extra TSV rows with a model, then run scripts/freeze.js on a spare box if the laptop is busy.

The checker still decides pass or fail. The model does not. Label every model-suggested row as unverified until the current CLI emits stable JSON twice.

Drop rows that embed the current time. Drop rows that embed a random token. Keep rows that hit a branch the corpus missed.

Failure analysis: when the digest moves

When --check fails, do not open a formatter first. Diff freeze/seam.json against a fresh freeze printed on stderr. Classify the delta with this list.

  1. Key order changed. Canonicalize with a sorted JSON replacer.
  2. Float noise appeared. The code started using raw division.
  3. Error strings changed. That is a seam change. Revert.
  4. Status code changed. Treat it like a JSON change. Revert.
  5. stderr grew a warning. Callers may already parse that stream.

Most harmless cleanups fail item 3. Teams rename unknown_sku to SKU_NOT_FOUND and call it polish. External scripts break on that polish. The freeze exists to stop it.

Limitations

This method does not prove design quality. A held seam can still hide a worse module graph. It also misses branches the corpus never calls.

Mutation testing is a different tool. Do not pretend a SHA-256 file replaces it. Use mutation later on the extracted module, not during the first cut.

Non-deterministic CLIs cannot use this freeze. Pin clocks and randomness first. Binary output needs a hex dump, not pretty JSON.

Multi-service repos need one freeze per process. A single digest for five services will thrash. Golden files rot when product intent changes. Rotate them in a product commit only.

Who should not use this approach

Skip this workflow if contract tests already cover the same seam. Skip it if the change must alter prices or tax. Skip it for greenfield code with no callers.

Skip it when the CLI talks to a live network you cannot stub. Solo spikes can move files freely. The freeze cost exceeds the rewrite cost there.

Wait until a second consumer exists. Two consumers turn a spike into a seam. Then the ranked cut starts to pay.

Stop after one held cut

The smallest safe change is one ranked cut. Two cuts hide which edit moved the seam. Three cuts become a rewrite with extra steps.

Commit. Run the checker. Stop. Tomorrow, rank the next cut against the same table.

The repo gets smaller because the seam stayed still. It does not get safer because a model dumped a new tree.

Top comments (0)