Skip to content

Build the census before opening the file it overwrites - #106

Merged
realmarcin merged 1 commit into
mainfrom
fix/census-write-after-scan
Sep 22, 2026
Merged

realmarcin merged 1 commit into
mainfrom
fix/census-write-after-scan

Conversation

@realmarcin

Copy link
Copy Markdown
Contributor

Closes #102.

json.dump(census(), open(DATA + "/prefix_census.json", "w"), indent=1)

Two problems in one line. The handle is never closed, so every run emits ResourceWarning: unclosed file — and the obvious fix for that would destroy the census.

Arguments evaluate left to right, so census() runs to completion before open() truncates. Write it the idiomatic way —

with open(DATA + "/prefix_census.json", "w") as fh:
    json.dump(census(), fh, indent=1)

— and the order reverses: a scan that raises leaves the committed file at zero bytes. And census() raises for ordinary reasons: roots.mech_root when a checkout is absent, record_paths when a glob matches nothing, both deliberately fail-closed.

So the line is safe by accident, and the accident is one refactor away from losing data precisely when something else has already gone wrong. This was not hypothetical — an adversarial review measured it, truncating the tracked census to 0 bytes with that refactor in place.

Verified both directions

result
failing scan (MECHS_ROOT=/nonexistent) exits with the checkout message, census byte-identical
successful scan (10-file synthetic corpus) 9 Mechs written, all non-empty, valid JSON, exit 0

The successful run was done in a temp tree, so the committed census was never a participant in the test.

Not done here

Writing to a temp file and renaming would be stronger — an interrupted write could not leave a partial census either. That is a larger change across the four scripts that write derived data, and this PR removes the trap without it.

27 tests pass; the only changed file is scripts/fleet/prefix_census.py.

🤖 Generated with Claude Code

    json.dump(census(), open(DATA + "/prefix_census.json", "w"), indent=1)

Two problems in one line. The handle is never closed, so every run emits
ResourceWarning — and the obvious fix for that would destroy the census.

Arguments evaluate left to right, so `census()` runs to completion before
`open()` truncates. Write it the idiomatic way instead, with the `with open`
first, and the order reverses: a scan that raises leaves the committed file at
zero bytes. census() raises for ordinary reasons — roots.mech_root when a
checkout is absent, record_paths when a glob matches nothing — both deliberate
fail-closed behaviour. So the current line is safe by accident, and the accident
is one refactor away from losing data exactly when something else already went
wrong (#102).

Building the document first keeps the ordering guarantee explicit rather than
incidental, and closes the handle.

Verified both directions. A failing scan (`MECHS_ROOT=/nonexistent`) exits with
the checkout message and leaves the census byte-identical. A successful scan
against a ten-file synthetic corpus writes all nine Mechs, non-empty, valid
JSON — run in a temp tree so the committed census was never a participant.

Writing to a temp file and renaming would be stronger still, since an
interrupted write could not leave a partial census either. Not done here: this
removes the trap, and atomic replacement is a larger change to make across the
four scripts that write derived data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@realmarcin
realmarcin merged commit 13d4f92 into main Sep 22, 2026
1 check passed
@realmarcin
realmarcin deleted the fix/census-write-after-scan branch September 22, 2026 07:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prefix_census.main() truncates its output before it knows the scan will succeed

1 participant