PR #633 follow-ups: --why misses the new unresolved-thread gate, and three doc/contract residues nf-0dj ← Beads

open priority 2 task unassigned

Twinned from https://github.com/cfsh/ninefold/issues/660 by tools/beads/import_github.py's reconcile pass.

Suggestions from ninefold-reviewer's review of #633.

#633 merged at 15:12:01Z, ~90s after its final head 6f55057 was pushed. That head carries bfe9c50f — *"#646: pre-empt the unresolved-thread rejection"* — a fourth merge gate (Gh.unresolved_threads() + a new MergeRefused in merge(), ahead of the ledger) that landed after the last verdict and is described in no PR comment. The gate itself is correct and wanted: required_review_thread_resolution: true is live on ruleset 19101787. These are the surfaces that did not keep up. Filed unclaimed.

F17 — --why does not check the new gate

tools/pr/merge_pr.py:explain() reproduces merge()'s refusals in order (state, CHANGES_REQUESTED, authority, mergeable, base, ledger) under a comment stating the invariant: *"⚠ Every refusal merge() makes, in the order it makes them."* bfe9c50f added a refusal to merge() (the gh.unresolved_threads() block) and not to explain(), so that comment is now false.

This is the F4 / F9 error direction a third time, and it costs more now: CLAUDE.md step 6 was rewritten by #633 to read *"⚠️ Unless merge_pr.py --why <n> says TIER 1"*, so --why is the documented entry point to the unattended path, not a diagnostic. A PR with one unresolved thread prints -> would merge on TIER 1, with no human approval, and the merge then refuses.

Fix: one gh.unresolved_threads() call between the base check and ledger_check() in explain(). WhyTest's _T1Gh already carries the threads knob, so the test is a two-liner.

F18 — the fourth gate is in the module and in no durable doc

notes/merging.md is where CLAUDE.md step 6 sends a session, and its "The three gates" section still enumerates three — ledger, presubmit, UI gate — with no mention of thread resolution. merge_pr.py's own ## The flow header is likewise unchanged. Same shape as F3 → F7 → F13 → F14: fixed in the module, absent from the file that outlives the thread.

Two things a reader needs there:

F19 — unresolved_threads()'s fail-soft contract does not exist in production

tools/nflib/gh.py:unresolved_threads() and its call site both rest on *"None means the query failed … Proceed on an unreadable count rather than blocking every merge on a GraphQL hiccup"*.

Gh.json does return default on not result.ok — but raw() keeps check=True and merge_pr.main() builds Gh() on the strict runner, so a failing gh api graphql raises ProcError two frames earlier and main() prints refuse: a GitHub call failed, so the merge cannot be gated safely. The merge is blocked, the opposite of what the comment promises.

The behaviour is fail-*closed* and therefore safe; the claim is wrong, and a future reader could simplify against a false premise. test_an_UNREADABLE_count_proceeds_rather_than_blocking cannot catch it — _T1Gh overrides unresolved_threads to return None directly rather than exercising Gh.unresolved_threads through a failing runner. main()'s own except ProcError comment, 170 lines below, names this exact unreachability class for ledger_check's fail-closed login branch.

Fix: either say the strict runner already covers it and delete the promise, or make the branch real (a lenient runner for this one call).

Smaller
Body residues on the merged PR (editable post-merge; it is now the permanent record)

Refs #633, #646, #651.

Dependencies

None.

Comments

No comments.

Add a comment