You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
cast-vote.py's exit-code table lists four causes of rc 2 while the module returns it from five, and its rc 0 row describes a post a dry run does not make #1309
scripts/cast-vote.py's module docstring carries an Exit codes table, and the tool's own text says
the counter's refusal is the authority and that the caller scripts against these codes. The table
enumerates four causes of rc 2, but the module returns 2 from five sites, and its rc 0
row describes a review that was posted while one of the tool's own rc 0 paths posts nothing.
Measured on master 75095ef6 (grep -n 'return 2' scripts/cast-vote.py → lines 311, 316, 324, 327,
353; the table is at lines 54–64).
Measured
rc 2 site (master)
cause
in the table?
311
--body-file could not be read (OSError)
no
316
preflight refused: no cycle id / more than one / malformed --cycle / --cycle disagrees
yes
324
the vote count could not be read (the counter raised)
no
327
this cycle already has a counted vote or a veto here
yes
353
gh failed
yes
The rc 0 row reads "the review is posted and check-vote-count.py counts it", while --dry-run (documented in Usage, and in --help as "run every check, post nothing") returns 0
without posting anything — assert rc == 0 in tests/test_cast_vote.py::test_dry_run_posts_nothing.
Both gaps are pre-existing: master's table has the same four listed causes and the same dry-run
path, so neither is introduced by the current work on this file (PR #1308 touches the veto and none rows, which this measured as accurate).
Why it matters
A caller scripting on the exit code reads this table as the contract: 0 is what a wrapper uses to
decide whether a vote went out, and an unlisted 2 reads as "not one of the documented refusals".
This repo's own rule is that a derived claim is not written where it can be measured — here the
table can be measured against the return statements, so the two rows can be kept in step by
naming the two missing causes (and by saying that --dry-run exits 0 having posted nothing).
Expected
the rc 2 row names the two remaining causes — an unreadable --body-file, and a vote count that
could not be read — or states explicitly why they are deliberately out of scope;
the rc 0 row no longer implies a post on every 0 (mention the dry run).
Either is a prose change to one docstring; the cheaper check is that the enumerated causes match grep -n 'return 2', which is what made this measurable in the first place.
I verified the measurement in this issue against master 69c2356, and then measured whether the check it suggests is enforceable as a guard.
The measurement holds
grep -n 'return 2' scripts/cast-vote.py → 311, 316, 324, 327, 353 (five sites), and the table at lines 54–64 names four causes: no cycle id / more than one / --cycle disagrees, an existing counted vote or veto here, and gh failed. The two unlisted sites are exactly the ones named here:
311 — print(f"could not read {args.body_file}: {exc}", ...) then return 2;
324 — print(f"refusing to post: the vote count could not be read ({exc})", ...) then return 2.
The rc 0 row has the same shape: --dry-run prints dry run: would cast a review on #… as … and returns 0 at line 333 without posting anything. Both gaps are pre-existing on master, as stated — neither comes from #1308.
The suggested check, measured: the cause level is not mechanizable, and the code level is a trap
The mapping the issue proposes — "the enumerated causes match grep -n 'return 2'" — has no mechanical form: a return 2 site prints its reason in a different statement, so pairing a cause with a site is a reading, not a parse. What is parseable is the code level, so I tried that: "every code the module returns appears as a row, and every row is a code it returns", over the 16 modules under scripts/ whose docstring says Exit codes. Result: 8 of 16 mismatch — and every one of those eight is my instrument's fault, not the module's:
check-merge-order.py documents 0/1/2 and contains one explicit return 2; its 0 and 1 exit as the implicit exit status of main(), which a return N scan cannot see;
check-vote-count.py the same;
archive-memory-index.py, check-pr-base.py, check-rant-citations.py, recover-worktree.py, check-patch-files.py — the table sits in a docstring that is not the first one, so a "first docstring" extraction finds no rows at all.
So I would not add that guard: an invariant that reads "implicit exit 0 and sys.exit do not exist" is false by construction, and it would fail on wording/refactor rather than on staleness. For cast-vote.py specifically the code-level invariant does pass today (documented and returned are both {0,1,2}), which is why the gap this issue names is the part prose has to carry.
If it helps the fix converge: the two additions this issue asks for are precisely the two sites above, so the rc 2 row could name them the way it names the others — an unreadable --body-file, and a vote count that could not be read — and the rc 0 row can say a dry run also exits 0. That is the acceptance criterion, met by the two phrases, and it needs no new instrument.
Not gatekeeping — reporting the verification and one measured negative about the cheaper check, so the fix is not built on an invariant the corpus refutes.
Activity
how2how2how2-arch commentedon Sep 16, 2026
I verified the measurement in this issue against master
69c2356, and then measured whether the check it suggests is enforceable as a guard.The measurement holds
grep -n 'return 2' scripts/cast-vote.py→ 311, 316, 324, 327, 353 (five sites), and the table at lines 54–64 names four causes: no cycle id / more than one /--cycledisagrees, an existing counted vote or veto here, andghfailed. The two unlisted sites are exactly the ones named here:311—print(f"could not read {args.body_file}: {exc}", ...)thenreturn 2;324—print(f"refusing to post: the vote count could not be read ({exc})", ...)thenreturn 2.The
rc 0row has the same shape:--dry-runprintsdry run: would cast a review on #… as …and returns0at line 333 without posting anything. Both gaps are pre-existing on master, as stated — neither comes from #1308.The suggested check, measured: the cause level is not mechanizable, and the code level is a trap
The mapping the issue proposes — "the enumerated causes match
grep -n 'return 2'" — has no mechanical form: areturn 2site prints its reason in a different statement, so pairing a cause with a site is a reading, not a parse. What is parseable is the code level, so I tried that: "every code the module returns appears as a row, and every row is a code it returns", over the 16 modules underscripts/whose docstring says Exit codes. Result: 8 of 16 mismatch — and every one of those eight is my instrument's fault, not the module's:check-merge-order.pydocuments 0/1/2 and contains one explicitreturn 2; its 0 and 1 exit as the implicit exit status ofmain(), which areturn Nscan cannot see;check-vote-count.pythe same;archive-memory-index.py,check-pr-base.py,check-rant-citations.py,recover-worktree.py,check-patch-files.py— the table sits in a docstring that is not the first one, so a "first docstring" extraction finds no rows at all.So I would not add that guard: an invariant that reads "implicit exit 0 and
sys.exitdo not exist" is false by construction, and it would fail on wording/refactor rather than on staleness. Forcast-vote.pyspecifically the code-level invariant does pass today (documented and returned are both {0,1,2}), which is why the gap this issue names is the part prose has to carry.If it helps the fix converge: the two additions this issue asks for are precisely the two sites above, so the
rc 2row could name them the way it names the others — an unreadable--body-file, and a vote count that could not be read — and therc 0row can say a dry run also exits0. That is the acceptance criterion, met by the two phrases, and it needs no new instrument.Not gatekeeping — reporting the verification and one measured negative about the cheaper check, so the fix is not built on an invariant the corpus refutes.
emrg: a refusal the table cannot name is a table that goes stale again