Found in the adversarial review of #120.
The revision is read after the records, not when they are read
Where: scripts/fleet/prefix_census.py:40
census() computes _revisions only after all nine scans finish (minutes later for the first Mech scanned). mech_stats.py calls revision(name) after review_census(name) and a network gh call. The docstring says 'The commit a checkout's records were read at'. On a checkout whose HEAD moves mid-run, the recorded SHA belongs to a later tree than the one the prefixes were counted from. The +dirty check does not help, because a fast-forwarded checkout is clean.
Failure scenario. The documented default run (_fleet/README.md: python3 scripts/fleet/prefix_census.py against the live checkouts) is in progress when the nightly fast-forward lands. The reflogs show merge ...: Fast-forward at 23:12–23:13 on HabitatMech, CellStructureMech and TaxonMech. HabitatMech is scanned at commit A, advances to B, and _revisions records B. mech_stats later reads B. The revisions agree, the file counts can agree, and the census prefixes come from A under B's name.
Evidence. prefix_census.py:26-40 (scan loop, then out["_revisions"]=... after it). mech_stats.py:141-148 (revision(name) evaluated after review_census and merged_prs). git reflog on the live checkouts: 960c8a574 HEAD@{2026-09-22 23:13:00}: merge ...: Fast-forward (HabitatMech), and likewise for CellStructureMech and TaxonMech.
Verifier (confirmed, low). The finding reproduces as stated. revision() is sampled after the reads, not when they happen: after all nine scans in prefix_census.py, and after review_census plus a gh call in mech_stats.py. So on a checkout that is fast-forwarded mid-run, the recorded SHA names a later tree than the one that was counted. The +dirty guard misses this because a fast-forward leaves the tree clean. That contradicts the docstring ("The commit a checkout's records were read at") and the commit message ("Record the revision each count was read at"). It also defeats the purpose of RefreshProvenanceTests (#85), since matching SHAs and matching counts can still hide prefixes taken from an older commit. The exposure is real: the default pipeline in _fleet/README.md ("python3 scripts/fleet/prefix_census.py" with the default MECHS_ROOT pointing at the shared live checkouts) reads those checkouts directly, and their reflogs show automated fast-forwards landing across several Mechs within seconds. One small correction: the fast-forwards are recurring but not strictly nightly (09-15 10:4x, 09-19 21:1x, 09-20 17:2x/21:2x/23:1x, 09-22 23:1x). Severity stays low. The skill's procedure runs against detached snapshots at the pins, which avoids the problem. This PR's committed outputs are internally consistent and all tests pass. The window is a race during a 2–4 minute scan. Fix: capture revision(m) both before and after scanning each Mech (and around review_census in mech_stats) and fail if the two differ. That also catches a fast-forward rewriting the working tree partway through a scan.
Suggested fix. Capture revision(m) before scanning m and again after. Raise if they differ, or record the before-value and verify it.
Found in the adversarial review of #120.
The revision is read after the records, not when they are read
Where:
scripts/fleet/prefix_census.py:40census() computes
_revisionsonly after all nine scans finish (minutes later for the first Mech scanned). mech_stats.py calls revision(name) after review_census(name) and a network gh call. The docstring says 'The commit a checkout's records were read at'. On a checkout whose HEAD moves mid-run, the recorded SHA belongs to a later tree than the one the prefixes were counted from. The +dirty check does not help, because a fast-forwarded checkout is clean.Failure scenario. The documented default run (_fleet/README.md:
python3 scripts/fleet/prefix_census.pyagainst the live checkouts) is in progress when the nightly fast-forward lands. The reflogs showmerge ...: Fast-forwardat 23:12–23:13 on HabitatMech, CellStructureMech and TaxonMech. HabitatMech is scanned at commit A, advances to B, and_revisionsrecords B. mech_stats later reads B. The revisions agree, the file counts can agree, and the census prefixes come from A under B's name.Evidence. prefix_census.py:26-40 (scan loop, then
out["_revisions"]=...after it). mech_stats.py:141-148 (revision(name) evaluated after review_census and merged_prs).git reflogon the live checkouts:960c8a574 HEAD@{2026-09-22 23:13:00}: merge ...: Fast-forward(HabitatMech), and likewise for CellStructureMech and TaxonMech.Verifier (confirmed, low). The finding reproduces as stated. revision() is sampled after the reads, not when they happen: after all nine scans in prefix_census.py, and after review_census plus a gh call in mech_stats.py. So on a checkout that is fast-forwarded mid-run, the recorded SHA names a later tree than the one that was counted. The +dirty guard misses this because a fast-forward leaves the tree clean. That contradicts the docstring ("The commit a checkout's records were read at") and the commit message ("Record the revision each count was read at"). It also defeats the purpose of RefreshProvenanceTests (#85), since matching SHAs and matching counts can still hide prefixes taken from an older commit. The exposure is real: the default pipeline in _fleet/README.md ("python3 scripts/fleet/prefix_census.py" with the default MECHS_ROOT pointing at the shared live checkouts) reads those checkouts directly, and their reflogs show automated fast-forwards landing across several Mechs within seconds. One small correction: the fast-forwards are recurring but not strictly nightly (09-15 10:4x, 09-19 21:1x, 09-20 17:2x/21:2x/23:1x, 09-22 23:1x). Severity stays low. The skill's procedure runs against detached snapshots at the pins, which avoids the problem. This PR's committed outputs are internally consistent and all tests pass. The window is a race during a 2–4 minute scan. Fix: capture revision(m) both before and after scanning each Mech (and around review_census in mech_stats) and fail if the two differ. That also catches a fast-forward rewriting the working tree partway through a scan.
Suggested fix. Capture revision(m) before scanning m and again after. Raise if they differ, or record the before-value and verify it.