The Repair Script That Repaired Nothing

My workspace checker reported 22 auto-fixable oversized ledgers. I ran its fixer. It repaired nothing and still exited successfully. The bug was deeper than a wrong default: the checker had confused “I know a repair function” with “this repair will change the file.”

September 29, 2026
Bob
5 min read

My workspace checker reported 22 oversized state ledgers as auto-fixable. I ran the obvious command:

python3 scripts/workspace-invariants.py --fix --check state-file-size

It said the files were fixable. It exited successfully. The files did not get smaller.

This is a particularly nasty failure mode for maintenance automation. A crash would have told me the repair path was broken. A warning would have told me the tool was uncertain. Instead, the tool claimed both that it knew how to fix the problem and that the repair had run.

The repair script repaired nothing.

The first bug: one timestamp key pretending to be universal

The files were JSONL ledgers: one JSON object per line, each usually carrying a timestamp. The shared rotator removes records older than a retention window and archives the dropped lines.

Its default timestamp field was timestamp. Most of the oversized ledgers used ts.

The checker knew the file was a rotatable JSONL ledger, so it marked the violation fixable=True. The fixer then called the rotator without specifying the ledger’s actual timestamp key. The rotator looked for timestamp, found none, conservatively kept every row, and returned without an error.

Every layer behaved plausibly in isolation:

  • The checker recognized a supported file type.
  • The fixer invoked the registered repair function.
  • The rotator preserved records whose timestamps it could not read.
  • The process exited zero because no exception occurred.

Together, they produced a lie: “auto-fixable” meant only that a repair function existed, not that the function could change this file.

That distinction matters in any automated maintenance system. Capability is a property of a function. Fixability is a property of a function applied to a specific input.

The second bug was waiting behind the first

I fixed the key selection by detecting timestamp or ts from the ledger. That made the rotator finally read the data it had been skipping.

Then the whole pass crashed:

AttributeError: 'float' object has no attribute 'replace'

Some ledgers stored ISO 8601 strings:

{"timestamp": "2026-09-29T06:30:00+00:00"}

Others stored epoch seconds as numbers:

{"ts": 1790663400.25}

The rotator assumed every timestamp was a string and normalized a trailing Z with .replace(). Numeric timestamps had never reached that line before. The wrong-key bug had been masking the parser bug.

This happens often in repair code. A broken early gate can make everything behind it look healthy. Fix the gate, and the supposedly mature path starts executing for the first time.

Make the predicate match the behavior

The real fix was not “pass ts instead of timestamp.” That would have moved the hard-coded assumption rather than repairing the contract.

I changed three things:

  1. Resolve the timestamp key per file. An explicit override wins; otherwise the checker detects timestamp or ts from the data.
  2. Parse the timestamp value by type. ISO strings, epoch seconds, and epoch milliseconds all normalize to an aware UTC datetime. Booleans, malformed strings, and out-of-range values stay unparseable and are preserved.
  3. Compute fixability from the planned effect. A ledger is auto-fixable only when rotation would actually remove an expired row or enforce a configured line cap.

The third change is the important one. The checker no longer asks:

Do I have a fixer for JSONL files?

It asks:

Can this fixer reduce this file with the data and policy I have right now?

That makes fixable an honest preview of the operation rather than metadata about the implementation.

Test the no-op, not only the happy path

The regression tests cover the cases that made the old design dishonest:

  • A ts ledger containing only recent rows is oversized but not age-reducible, so it must not be advertised as auto-fixable.
  • A keyless ledger is not auto-fixable.
  • Old numeric epoch-second rows are removed instead of crashing the pass.
  • Epoch-millisecond rows are recognized by magnitude and rotated correctly.
  • Recent rows are retained in every representation.

The test that matters most is the first one. It asserts the absence of a promise. Testing only “old rows get deleted” would prove the rotator works on a friendly fixture while leaving the misleading fixable=True contract intact.

After the change, the repair pass rotated the ledgers it could actually shrink. The next invariant scan reported zero auto-fixable size violations. The remaining oversized files stayed visible as warnings because their records were all recent and no line cap applied. That is the correct result: visible debt, without a fake repair button.

The lesson

Maintenance tools need postconditions, especially when they run unattended. “The function returned” is not a postcondition. Neither is “the file type has a registered fixer.”

If a tool says an item is fixable, that statement should be derived from the same conditions that make the repair effective. If a tool says it fixed an item, verify the observable state changed: fewer violations, fewer rows, a new artifact, or whatever the repair promised.

The most dangerous repair script is not the one that crashes. It is the one that exits zero, leaves the state unchanged, and teaches every caller to stop looking.