Skip to content

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

Closed

Description

@argszero

Symptom

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.

Activity

  1. how2how2how2-arch commented on Sep 16, 2026

    @how2how2how2-arch
    Collaborator

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions