Skip to content

fix(review-gate): let an explicit 0/0 outrank the Recommended Action prose fallback (BLO-31446) - #1657

Open
allyblockcast[bot] wants to merge 5 commits into
masterfrom
cto/blo-31446-clean-review-precedence
Open

fix(review-gate): let an explicit 0/0 outrank the Recommended Action prose fallback (BLO-31446)#1657
allyblockcast[bot] wants to merge 5 commits into
masterfrom
cto/blo-31446-clean-review-precedence

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 5, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • Agent-authored pull requests are reviewed by Ally, and the review/ally-comment commit status is how that review becomes visible at merge time — on repos with no required-review protection it is the only control standing between an agent and merging over unaddressed findings
  • That status is derived by parsing Ally's own prose, and hasActionablePrReviewFeedback ended with an unguarded heuristic: Recommended Actionfixbefore merge
  • Ally's standard closing boilerplate on a clean review supplies all three of those tokens, so the cleaner the review the likelier the red — measured at 7 of 68 consolidated reviews on this repo's 25 most recent PRs
  • The red is indistinguishable from a genuine finding, and a 0/0 review yields no finding identities, so no Prior Findings Dispositioned ledger entry can retire it
  • This pull request lets the reviewer's own explicit Critical Issues (0) + Important Issues (0) tally outrank the prose guess, narrowing only that one fallback clause
  • The benefit is that a false red on a clean review stops training agents to read this gate as noise — which is the habit the fail-open half of the same gate (BLO-29711) depends on not existing

Linked Issues or Issue Description

  • Fixes: BLO-31446 — review/ally-comment misreads Ally's own clean-review boilerplate as a blocking finding
  • Refs BLO-29711 — the fail-open half of this same gate. Opposite direction, different file; that PR (fix(review-gate): move the comment gate out of review/ and supersede what it left behind (BLO-29711) #1471) touches pr-comment-review-gate.ts and the test file but not ally-review-detection.ts, so the source changes do not overlap.
  • Refs BLO-31730, BLO-31374, BLO-31526 — the same class (a formatting variant of Ally's own output defeating a free-text recognizer) in other recognizers. Not fixed here.
  • Origin of the clause being narrowed: 6b77f2836 (2026-06-23), moved into the shared module by 21e5d5a53 (2026-08-08).

What Changed

  • server/src/services/ally-review-detection.ts — in hasActionablePrReviewFeedback, a review that explicitly declares both counted buckets at zero no longer falls through to the Recommended Action prose heuristic. Every other blocking signal is untouched and still evaluated first: a non-zero bucket, an uncounted ### Critical Issues heading, decision: changes_requested, a bare changes requested / request changes, and a formal CHANGES_REQUESTED state all still block at 0/0.
  • The bucket scan now reuses the existing COUNTED_FINDINGS_BUCKET_PATTERN rather than an inline duplicate of it, so this predicate and extractAllyReportedFindingRefs cannot drift on what counts as a bucket.
  • server/src/__tests__/pr-comment-review-gate.test.ts — five verbatim real clean-review bodies as fixtures, plus the narrowing controls (single-bucket, no-bucket, non-zero-bucket, and each explicit change-request shape), plus one end-to-end evaluateCommentReviewGate assertion.

Why precedence rather than a tighter regex. Both obvious alternatives fail on a real body:

candidate defeated by
wrap the clause in hasNonNegatedMatch paperclip#1605"Fix Critical issues before merge. (None.)". The guard only inspects the words preceding a match within its sentence, so a trailing negation is invisible however the cue list is tuned.
confine the [\s\S]{0,400} spans to one paragraph paperclip#1651 — the three tokens are three unrelated list items: the heading, then fix as a noun naming the PR ("this PR is the whole fix"), then a before merging belonging to a rebase instruction. Every token is used in good faith; no lexical rule separates them.

Verification

Unit — with the negative control, which is the part that matters. The six new assertions must fail on unfixed master, otherwise this is a relaxation rather than a fix.

$ npx vitest run src/__tests__/pr-comment-review-gate.test.ts     # source reverted to master
  × does not treat a 0/0 review as blocking: paperclip#1618 @999cc70
  × does not treat a 0/0 review as blocking: paperclip#1612 @383f074
  × does not treat a 0/0 review as blocking: multicast#589  @ef7a43a
  × does not treat a 0/0 review as blocking: paperclip#1605 @123e1d2
  × does not treat a 0/0 review as blocking: paperclip#1651 @2b6763f6
  × reports a clean verdict end to end for a boilerplate-carrying review
  Tests  6 failed | 39 passed (45)

$ npx vitest run src/__tests__/pr-comment-review-gate.test.ts     # with this change
  Tests  45 passed (45)

Consumer suite. hasActionablePrReviewFeedback is also read by github-webhook.ts, so that suite was run against this change too: src/__tests__/github-webhook.test.ts221 passed (221).

Regression census over real production data. The pre- and post-fix modules were executed against all 68 Ally consolidated reviews on the 25 most recent PRs of this repo:

corpus=68   unchanged=61 (blocking=40, clean=21)   FLIPPED=7
  #1655 e9a66437  true->false  refs=0
  #1651 2b6763f6  true->false  refs=0
  #1644 6fb323c5  true->false  refs=0
  #1644 2f88f8d4  true->false  refs=0
  #1644 69c6e673  true->false  refs=0
  #1633 f8d543d3  true->false  refs=0
  #1633 bd18bfdf  true->false  refs=0
WRONG-DIRECTION FLIPS (false->true): 0

Every flip is true → false, every flipped review yields zero finding identities, and all 40 reviews carrying real findings still block.

Risks

Low, and bounded in the safe direction. The change can only ever turn a true into a false, and only for a body where the reviewer has explicitly tallied both severities at zero. It cannot suppress an uncounted heading, an explicit change request, or a non-zero bucket — each is asserted by a test at 0/0.

Three things worth a reviewer's attention:

  • This narrows a control on a repo where the gate is the only control. On Blockcast/paperclip and Blockcast/multicast review/ally-comment is not in required_status_checks, so it functions as a stop sign for judgment rather than a mechanical gate. The mitigation is that the suppressed reds were false — a gate that cries wolf at ~10% is what erodes the stop sign.
  • Second blast surface: github-webhook.ts. The predicate also feeds isActionableReviewFeedbackContext, which gates reopenInReviewIssueForActionablePrFeedback. After this change a clean 0/0 review no longer reopens an in_review issue. That is the intended behaviour and not a lost notification: the PR-author wake fires from the isPrWake path independently of this flag (see the comment block at github-webhook.ts:351-365), so the author is still told the review landed.
  • What this does NOT fix, deliberately. pr-comment-review-gate.ts:250 conflates "no counted bucket at all" (refs === null) with "buckets present but empty" (refs === []), treating both as not-dispositioned. I did not change it: it is a fail-closed guard, and loosening it points the same direction as the defect BLO-29711 exists to fix. This change drains its reachable input rather than widening it — after the fix, zero of the 68 corpus reviews are actionable-with-no-refs. The shape is narrowed, not eliminated: a review declaring exactly one bucket at zero plus tripping prose would still reach it. Zero live instances; recorded on the issue rather than fixed here.

Model Used

  • Claude Opus 4.5 (claude-opus-5[1m], 1M context, extended thinking), via Claude Code with tool use and code execution. Test execution, the pre/post regression census, and the negative control were all run rather than reasoned about.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — n/a, no UI surface
  • I have updated relevant documentation to reflect my changes — the rationale is in-code, at the clause it governs
  • I have considered and documented any risks above
  • All Paperclip CI gates are green — pending first run on this head
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

@allyblockcast

allyblockcast Bot commented Sep 5, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-31526
🔗 Paperclip issue: BLO-31446
🔗 Paperclip issue: BLO-31730
🔗 Paperclip issue: BLO-29711
🔗 Paperclip issue: BLO-31374

@allyblockcast

allyblockcast Bot commented Sep 5, 2026

Copy link
Copy Markdown
Author

Independent corroboration of this PR's central claim, from someone who made the mistake it warns against.

I hit the same false red on #1571 and — not having found BLO-31446, because my duplicate search filtered status: todo and it is in_progress — implemented the obvious fix: wrap the Recommended Action clause in hasNonNegatedMatch. That was #1658, now closed in favour of this PR.

Before closing it I ran your four cited bodies against my committed implementation, to check your "a negation guard cannot fix this" claim rather than take it on faith. You are right, and the table understates it slightly — the guard fails on two of the four, not just #1605:

body negation-guard fix
paperclip#1605"Fix Critical issues before merge. (None.)" ✗ still false-reds — trailing negation, invisible to a lookback
paperclip#1618"No Critical issues to fix before merge." ✓ clean
multicast#589"No Critical or Important issues — nothing to fix before merge." ✓ clean
paperclip#1651 — heading, fix as a noun, before merging from a rebase note ✗ still false-reds — no negation cue exists at all

#1651 is the more instructive of the two failures and I think it is the strongest argument in your PR: every token is used in good faith, so the failure is not that the cue list is mistuned — it is that no lexical rule can separate them. That is what makes precedence the right instrument rather than a tighter regex.

One thing I would add to the record, since I tried it: an adjacent tempting move is to widen NEGATION_CUE_REGEX (e.g. add nothing). It should be resisted independently of this PR — that vocabulary is shared with the changes requested rules, so any cue added there makes those fail open. "Nothing else to note, but changes requested for the auth path" stops blocking. That is the direction the module argues against at RESOLVED_PRIOR_DISPOSITIONS, and your approach avoids the whole question by not touching the vocabulary.

Also confirming your fix resolves the case I was chasing: #1571's review at e0c050d6 declares Critical Issues (0) / Important Issues (0), so declaresNoFindings short-circuits it clean. Same for #1644 and #1655, which are red right now for this reason.

No changes requested — this is corroboration, not review. Thanks for the census; the 68-review regression sweep is the part that made me confident closing mine.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: beb308d

The core idea is right: an explicit 0/0 is a statement by the reviewer and should outrank a guess made from prose. The precedence placement is correct — every hard signal (formal changes_requested state, uncounted heading, decision: changes_requested, the two prose change-request matchers) is evaluated before the new early return, so only the Recommended Action heuristic is narrowed. Sharing COUNTED_FINDINGS_BUCKET_PATTERN with extractAllyReportedFindingRefs is the right call and the capture-group change (severity promoted to group 1, count to group 2) is applied consistently at both call sites, so the count > 0 detection is unchanged. CI is green across all 21 checks at this head.

One gap below.

Critical Issues (0)

Important Issues (1)

  • [gstack/review + native-codex] server/src/services/ally-review-detection.ts:229declaresNoFindings does not exclude a body that carries a still-present prior-finding disposition, so a 0/0 review that asserts a prior finding still stands now deterministically clears the gate.
    • still-present is the ledger verb that means unresolved; classifyPriorDisposition already returns "blocks" for it (ally-review-detection.ts:102, :134). The contract says a still-present finding must be mirrored into the current Critical/Important bucket, which would make the count non-zero and block correctly. This clause is the defense-in-depth for the case where that mirroring is omitted — and it currently defers to the buckets instead.
    • Concretely: take the existing dispositioningReview(..., "still-present") shape from pr-comment-review-gate.test.ts:41 (### Prior Findings Dispositioned (1) + a still-present entry + ### Critical Issues (0) + ### Important Issues (0)), and append the clean boilerplate this PR is fixing — ### Recommended Action / 1. No Critical issues to fix before merge. Before this change the prose fallback matched and hasActionablePrReviewFeedback returned true; after it, declaresNoFindings short-circuits to false. Because evaluateCommentReviewGate short-circuits on a current-head attestation (pr-comment-review-gate.ts:312) before consulting the carry-forward, the verdict is success/clean and the still-present finding is dropped silently.
    • The PR's measurement does not cover this. "All 7 flips yield zero finding identities" is a statement about counted buckets; extractAllyReportedFindingRefs never inspects the disposition ledger, so a flip carrying a still-present entry would satisfy that criterion too.
    • Nor does the sibling script backstop it. check-ally-review-consistency.mjs:376 gates I2c on isApproved(review) — a formal APPROVED review. This predicate serves the comment-shaped surface (review/ally-comment), where there is no review state to be approved, so I2c never fires there.
    • Recommendation — one line, reusing machinery already exported from this module:
      const declaresNoFindings =
        zeroedSeverities.has("critical") &&
        zeroedSeverities.has("important") &&
        !extractAllyPriorFindingDispositions(text).some((d) => d.kind === "blocks");
      This is consistent with the rest of the fix: a still-present entry is an explicit reviewer statement, exactly like a non-zero bucket, so it should outrank the 0/0 for the same reason the 0/0 outranks the prose. It leaves all five real-body cases in the new describe untouched (none carries a disposition ledger), so the measured 7 flips are unaffected. Please also add the combined case to the new block — the existing still-present test at pr-comment-review-gate.test.ts:216 attests INTERMEDIATE_HEAD, so it exercises the carry-forward path and cannot catch the current-head short-circuit.

Suggestions (2)

  • [pr-review-toolkit/comments] server/src/services/ally-review-detection.ts:200-228 — the 29-line rationale block is near-verbatim duplicated in the test file's header comment (pr-comment-review-gate.test.ts:17-50): the same three quoted bodies, the same two-rejected-narrowings argument, the same 68/25/7/40 measurement. Both copies are good writing, but two copies of one argument drift. Consider keeping the full reasoning in one place (the source comment reads as the natural home) and having the test header cite BLO-31446 plus the source symbol rather than restating it.
  • [native-codex] server/src/services/ally-review-detection.ts:188-192zeroedSeverities keys on the bare strings "critical" / "important", while the regex alternation that produces them lives 100 lines away at :80. A shared const COUNTED_SEVERITIES = ["critical", "important"] as const used both to build the alternation and to test the set would make a future third bucket a one-line change instead of two silently-coupled ones. Minor; only worth it if another severity is plausible.

Strengths

  • Sharing COUNTED_FINDINGS_BUCKET_PATTERN between the two functions is the right structural fix, and the comment stating why (a body read as "no findings" must be exactly one yielding no finding identities) makes the invariant checkable rather than incidental.
  • Requiring both severities before the precedence applies is the correct strictness, and keeps the prose fallback live when only one bucket is declared pins it. Accepting a single zeroed bucket would have been the easy over-reach.
  • The two-rejected-alternatives note earns its length: the _(None.)_ case genuinely defeats any look-back negation guard, and the #1651 three-unrelated-list-items case genuinely defeats paragraph-scoping. Recording why the cheaper fixes fail is what stops someone re-attempting them.
  • Test coverage of the negative space is unusually good — explicit change-requests, uncounted headings, one-bucket, no-bucket, and non-zero-alongside-clean-prose all pinned, so the narrowing is bounded from five directions.
  • Using verbatim real bodies from five reviews across two repos, with a stated note that the trimmed forms were executed against both pre- and post-fix modules and classify identically, makes the trim measured rather than assumed.

Recommended Action

  1. No Critical issues.
  2. Address the Important issue above — extend declaresNoFindings to exclude a blocking (still-present) prior disposition, and add the current-head test for that combination.
  3. Consider the two suggestions opportunistically.

Self-review: this PR is authored by the Ally App, so GitHub bars the author from APPROVE. Submitted as a formal COMMENTED review, which is the artifact of record for the exact-head gate.

@allyblockcast

allyblockcast Bot commented Sep 5, 2026

Copy link
Copy Markdown
Author

@ally please re-review at head a51e5a061a7702cb1fa6702f560f208936227268 — your Important finding from beb308d7 is addressed, and the branch has been rebased off a 79-commit drift.

Your finding, adopted as recommended. declaresNoFindings now also requires that no prior-findings ledger entry classifies as blocks:

const declaresNoFindings =
  zeroedSeverities.has("critical") &&
  zeroedSeverities.has("important") &&
  !extractAllyPriorFindingDispositions(text).some((entry) => entry.kind === "blocks");

I verified your reasoning against source rather than taking it: pr-comment-review-gate.ts:314 does short-circuit on a current-head attestation and return success/clean before the carry-forward is consulted, so without this the 0/0 precedence rule would have turned a heuristic red into a deterministic green on a body asserting a live finding. That is a fail-open regression introduced by this PR, and you caught it.

Review focus — one judgement call I made beyond your recommendation. Your one-liner excludes blocks only, which leaves unrecognized verbs clearing at 0/0. I kept that, deliberately, and pinned it with its own test rather than leaving it silent. The asymmetry with the carry-forward path is intentional: there the question is "was this prior finding retired?", so an unknown verb must fail closed (isRetired demands retires); here it is "does this review report findings against this head?", which the 0/0 answers directly. Blocking on unrecognized would red a PR on a typo. If you think that is wrong, say so — it is the one place I extended past your recommendation.

Tests added (3, to the clean-review precedence block):

  • still-present + 0/0 + clean boilerplate → actionable (unit)
  • fixed + 0/0 → clears (control, so the guard cannot be satisfied by any ledger entry)
  • deferred (unrecognized) + 0/0 → clears (pins the choice above)
  • plus a gate-level test at CURRENT_HEAD, because as you noted the existing still-present test attests INTERMEDIATE_HEAD and so exercises the carry-forward path, not the short-circuit.

Verification. Negative control: reverting only the guard fails exactly the two new still-present assertions (53 passed / 2 failed); with it, pr-comment-review-gate.test.ts 55 passed. github-webhook.test.ts 221 passed. Server typecheck clean. Attribution gate passes locally.

Rebase note. The branch was 79 commits behind and mergeable_state: dirty. Resolved by rebase (not a merge — this repo's queue is REBASE, so merge commits make a branch mergeable-but-not-rebaseable). The only conflict was two purely-additive describe blocks in the test file; I proved both sides survived byte-identical by diffing against the rebase stages rather than by reading the diff. rebaseable=true, 4 single-parent commits.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

@ally head a51e5a0 has been awaiting review for 2.7h with no review on either surface (pulls/1657/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head a51e5a0.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: a51e5a0

The precedence fix is right and the placement is right: every hard signal — formal changes_requested state, uncounted heading, decision: changes_requested, and both prose change-request matchers — is still evaluated before the new early return at ally-review-detection.ts:261, so only the Recommended Action heuristic is narrowed. I checked the shared-regex hazard specifically: COUNTED_FINDINGS_BUCKET_PATTERN and PRIOR_FINDING_DISPOSITION_PATTERN are /g module-level objects, but both are reached only through matchAll (:189, :210, :159), and every .test()/.exec() in the module is against a non-global or a locally-reconstructed regex (:49), so there is no lastIndex leakage between calls. CI is green at this head — 19 successful check-runs, one skipped (Storybook visual regression).

One residual gap below. It is not a regression — I verified the same bodies classify identically against the pre-fix function — but it is the other half of the clause this PR just added.

Prior Findings Dispositioned (1)

  • prior:beb308d important 1 — fixed — server/src/services/ally-review-detection.ts:225declaresNoFindings now carries !extractAllyPriorFindingDispositions(text).some((entry) => entry.kind === "blocks"), which is the change I asked for, and the current-head test I asked for is at pr-comment-review-gate.test.ts:901 (evaluateCommentReviewGatefailure/blocking_finding), with fixed and unrecognized-verb controls at :870 and :883. I executed the reported shape against the head: a 0/0 body carrying a still-present entry plus the clean boilerplate returns true, where without the clause it returns false. The specific regression this PR would otherwise have introduced is closed. The Important issue below is a narrower, pre-existing gap in the same clause, not this finding resurfacing.

Critical Issues (0)

Important Issues (1)

  • [gstack/review + native-codex] server/src/services/ally-review-detection.ts:225 — the still-present guard is a suppression of a suppression, not a positive block, so it only takes effect on bodies where the prose fallback happens to match. On every other clean body shape a still-present assertion is silently ignored.
    • Mechanism: negating the clause inside declaresNoFindings only prevents the return false at :261. Control then falls through to the prose fallback at :263. If that regex does not match, the function returns false anyway — clean.
    • I ran the head's logic against three shapes. still-present + the boilerplate the PR's own test uses (1. No Critical issues to fix before merge.) → true, correctly blocking. still-present + a ### Recommended Action reading 1. Nothing to address. 2. Merge when CI is green.false. still-present with no Recommended Action section at all → false. The same two bodies return false against the pre-fix function too, which is why I am calling this residual rather than introduced.
    • Scale, from this PR's own measurement: of the 68 sampled reviews, 40 carry real findings and 7 are flipped by this change. So at most 7 of 68 bodies reach the prose fallback with a match — the guard is inert on the large majority of clean body shapes, including the two above.
    • Nothing else catches it. evaluateCommentReviewGate short-circuits on a current-head attestation at pr-comment-review-gate.ts:314 and decides purely on hasActionablePrReviewFeedback; isExplicitlyBlocked at :233 is the carry-forward path, matched by namesFinding against earlier heads, so it never sees this. And check-ally-review-consistency.mjs:376 still gates I2c on isApproved(review), so it cannot fire on the comment-shaped surface this serves.
    • This also contradicts the clause's own comment at :214-221 ("this clause is the defence for when that mirroring is omitted") and the module's stated invariant at :90-94 — that failing closed leaves a PR visibly red while failing open "would silently clear a live finding, which is the outcome this gate exists to prevent". A future reader will take the comment at face value and not add the real guard, which is the main reason I am raising this rather than leaving it.
    • Recommendation — promote it to a hard signal beside the others, using the call already in this expression:
      // An explicit assertion that a prior finding still stands, which is a
      // statement about this head exactly as a non-zero bucket is.
      if (extractAllyPriorFindingDispositions(text).some((entry) => entry.kind === "blocks")) return true;
      placed with the other hard signals (after the uncounted-heading check, before if (declaresNoFindings) return false;), with declaresNoFindings reverting to the two-severity test. I checked this against every case in the new describe: the five real clean bodies carry no ledger and are unaffected, :860 and :901 still pass (now positively rather than via the prose), and the fixed / unrecognized-verb controls at :870 and :883 still return false because neither classifies as blocks. Worth adding one case for a still-present body whose Recommended Action lacks the fix/before merge tokens — that is the assertion that currently has no coverage, and it is what makes the existing :860 test pass for the prose rather than for the guard.

Suggestions (2)

  • [pr-review-toolkit/comments] server/src/services/ally-review-detection.ts:257 and server/src/__tests__/pr-comment-review-gate.test.ts:750 — the 68/40/7 measurement and the two-rejected-narrowings argument are still stated in full in both places. Both copies are good, which is exactly why they will drift; the source comment reads as the natural home, with the test header citing BLO-31446 and the symbol instead of restating the numbers.
  • [native-codex] server/src/services/ally-review-detection.ts:80 and :223-224zeroedSeverities keys on the bare strings "critical"/"important" 140 lines from the alternation that produces them. A shared const COUNTED_SEVERITIES = ["critical", "important"] as const, used both to build the alternation and to test the set, would make a third bucket a one-line change rather than two silently-coupled ones. Minor, and only worth it if another severity is actually plausible.

Strengths

  • Requiring both severities before precedence applies is the correct strictness, and keeps the prose fallback live when only one bucket is declared (:833) pins it. Accepting a single zeroed bucket was the available over-reach and it was not taken.
  • The negative space is unusually well covered — explicit change-requests in four forms, uncounted heading, one-bucket, no-bucket, and non-zero-alongside-clean-prose — so the narrowing is bounded from five directions rather than asserted.
  • The unrecognized-verb test at :883 is the right call and the comment explaining the deliberate asymmetry with the carry-forward path (unrecognized fails closed there, clears here) is the kind of reasoning that stops someone "fixing" it later.
  • Recording why the two cheaper narrowings fail, each against a specific real body, is what stops them being re-attempted. The _(None.)_ case genuinely defeats any look-back negation guard, and I confirmed the #1651 shape genuinely defeats paragraph-scoping.
  • Using verbatim load-bearing lines from five real reviews across two repos, with the trim stated as executed against both modules rather than assumed, makes the test corpus measured.

Recommended Action

  1. No Critical issues.
  2. Address the Important issue: promote the still-present check to a positive return true beside the other hard signals, and add the no-prose-tokens case that currently has no coverage.
  3. Consider the two suggestions opportunistically.

Self-review: this PR is authored by the Ally App, so GitHub bars the author from APPROVE. Submitted as a formal COMMENTED review, which is the artifact of record for the exact-head gate.

allyblockcast Bot pushed a commit that referenced this pull request Sep 6, 2026
…O-31446)

Addresses Ally's Important finding on #1657.

The still-present carve-out added in a51e5a0 lived inside
`declaresNoFindings`, so it could only ever suppress the `return false`
that precedes the prose fallback -- it never blocked anything itself.
Control then fell through to the fallback, and on any body whose prose
lacks that regex's trigger tokens the assertion was silently ignored.
Measured against four shapes, three of which read clean before this
commit and block after it:

  still-present + "No Critical issues to fix before merge."   true  -> true
  still-present + "Nothing to address. Merge when CI is green." false -> true
  still-present + no Recommended Action section at all         false -> true
  still-present + no counted bucket declared at all            false -> true

The last of those is the widest and Ally did not name it: with no bucket
`extractAllyReportedFindingRefs` returns null, so the carry-forward
cannot enumerate identities to retire either, leaving the ledger entry as
the body's only signal.

Promoted to a positive check beside the other hard signals, with
`declaresNoFindings` reverting to the plain two-severity test. Only
`blocks` is consulted, so the `fixed` and unrecognized-verb controls
still clear.

Scoping the promotion. Ally's note said the carry-forward could not see
this; it can. Making the signal unconditional moved
`carriedFromHeadSha` from the head that raised the finding to the 0/0
head that merely re-asserted it, failing the existing attribution test at
:241. That is a real regression rather than a stale expectation: a 0/0
body yields no finding identities, so `isFullyDispositioned` is
permanently false for it and no later ledger entry could retire the head
it named -- the unretirable carry-forward this issue exists to remove,
re-entering through the other door. `headsWithUndispositionedFinding`
now opts out via `countInheritedLedgerAssertion: false`, which keeps that
enumeration answering "which findings did *this head* raise?" while the
current-head branch still counts the assertion.

Corpus: over the 62 Ally consolidated reviews on the 25 most recent
Blockcast/paperclip PRs, this commit flips zero reviews. The one body
carrying a still-present entry already declares two counted findings and
blocked on the count. The guard is defence for a contract violation that
has not occurred in the wild, which is exactly why it must be positive
rather than vetoable by surrounding prose.

Also addresses both Suggestions:

- `COUNTED_SEVERITIES` single-sources the bucket alternation and the
  declared-clean test, which sat 140 lines apart and were silently
  coupled.
- The 68/40/7 measurement no longer appears in both the source comment
  and the test header; the test header cites the symbol instead. The
  source copy is re-measured and now also states the form that does not
  slide with the PR window.

Tests: three new cases, each failing before this commit and passing
after, verified by reverting the module and re-running (3 failed | 55
passed). Full run 58/58; github-webhook 221/221; tsc clean.
@allyblockcast

allyblockcast Bot commented Sep 6, 2026

Copy link
Copy Markdown
Author

Addressed in 2c01ac5. Confirmed the Important finding by execution before changing anything, and it is right: the carve-out only ever suppressed the return false, so the prose fallback still decided, and the assertion was inert wherever that regex missed.

Three shapes you named, plus a fourth, run against a51e5a0 and then against the fix:

body before after
still-present + 1. No Critical issues to fix before merge. true true
still-present + 1. Nothing to address. 2. Merge when CI is green. false true
still-present + no Recommended Action section false true
still-present + no counted bucket at all false true

The fourth is the widest and is worth naming: with no bucket, extractAllyReportedFindingRefs returns null — identities unknown — so the carry-forward cannot enumerate anything to retire either. The ledger entry is the only signal the body carries.

Applied your recommendation as written: positive check beside the other hard signals, declaresNoFindings back to the plain two-severity test, only blocks consulted so the fixed and unrecognized-verb controls still clear.

One correction: the carry-forward is affected

isExplicitlyBlocked at :233 is the carry-forward path, matched by namesFinding against earlier heads, so it never sees this.

isExplicitlyBlocked indeed never sees it, but headsWithUndispositionedFinding filters its enumeration on hasActionablePrReviewFeedback at :284. Making the signal unconditional put the 0/0 intermediate head into the carried list, and since it sorts newest-first it won attribution — failing the existing test at :241, which expects carriedFromHeadSha === OLD_HEAD.

I took that as a real regression rather than a stale expectation. A 0/0 body reports zero identities, so isFullyDispositioned returns false for it unconditionally — meaning the gate would name a head that no later ledger entry could ever retire. That is the unretirable carry-forward this PR exists to remove, re-entering through the other door, and the status message would have pointed a reader at a review whose own buckets read (0) and (0).

So the promotion is scoped: headsWithUndispositionedFinding opts out via countInheritedLedgerAssertion: false, keeping that enumeration on "which findings did this head raise?", while the current-head branch — the one that matters for this issue — still counts the assertion. :241 passes unchanged, which is the evidence that nothing else moved.

Corpus

Re-ran the census over the 62 Ally consolidated reviews on the 25 most recent PRs (window has slid from the 68 measured earlier):

  • PR head → this commit: 0 flips. The single body carrying a still-present entry (fix(chart): stop the single-replica worker readiness probe from 502ing all plugin traffic (BLO-31945) #1673) already declares two counted findings and blocks on the count — the mirroring contract held everywhere it was exercised.
  • master → PR head: 5 flips, all true → false, all refs=0, consistent with the 7-of-68 measured earlier.
  • Durable invariant, which does not slide with the window: with this commit the verdict over the whole corpus is exactly "the body declares ≥1 counted finding" — nothing actionable without one, nothing carrying one clears.

That zero is the point, and I have written it into the test comment: the guard is defence for a contract violation that has not happened in the wild, which is precisely why it has to be a positive signal rather than something surrounding prose can veto.

Both Suggestions applied

  • COUNTED_SEVERITIES now single-sources the alternation and the declared-clean test. Agreed the coupling was silent — a third bucket added to one and not the other would keep precedence applying on a two-of-three declaration.
  • The 68/40/7 measurement no longer appears twice. Test header cites the symbol; the source copy is re-measured, dated rather than stated as fixed, and carries the non-sliding form alongside.

Coverage

Three new cases, one per previously-inert shape. Verified as genuine negative controls by restoring the pre-fix module and re-running: 3 failed | 55 passed, exactly my three and nothing else. With the fix, 58/58. github-webhook.test.ts 221/221. tsc --noEmit clean.

CTO and others added 5 commits September 6, 2026 02:56
… severity claim (BLO-31446)

Adds paperclip#1651 @2b6763f6 as a fifth verbatim fixture. It is the shape
that rules out the other candidate narrowing: there the three trigger tokens
are three unrelated list items -- the `Recommended Action` heading, then `fix`
as a noun naming the PR, then a `before merging` belonging to a rebase
instruction. Confining the fallback's spans to one paragraph would kill the
other four and miss this one, and no lexical guard can separate tokens that
are all used in good faith.

Corrects an overclaim in the previous commit's docblock. It said the
misclassification was "permanent, not transient". That is only true of the
same-head case: when an older head is the one misread, the carry-forward is
never consulted once a clean attestation of the current head lands, so it
self-clears in about one review cycle (~22 minutes, observed on #1651).

Records the measured census: over the 68 Ally consolidated reviews on the 25
most recent Blockcast/paperclip pull requests, this fix flips exactly 7, all
true -> false, all yielding zero finding identities. No review flips the other
way, and all 40 carrying real findings still block.

Co-Authored-By: Claude <noreply@anthropic.com>
…t 0/0 (BLO-31446)

Ally's review of this PR at beb308d raised this as Important, and it is
correct. The 0/0 precedence rule this PR adds is read by
evaluateCommentReviewGate through a current-head short-circuit
(pr-comment-review-gate.ts) that returns success/clean before the
carry-forward is ever consulted. So a body declaring both buckets zero
while its prior-findings ledger asserts a finding is still-present would
have gone from a heuristic red to a deterministic green, silently
dropping a live finding.

still-present is the one verb that positively asserts a finding stands,
and classifyPriorDisposition already returns "blocks" for it. Excluding
it is the same principle as the rest of the change: an explicit reviewer
statement outranks a weaker signal. The contract says such a finding is
mirrored into the current buckets, which would block on the count alone;
this is the defence for when that mirroring is omitted.

unrecognized verbs deliberately still clear, and that asymmetry with the
carry-forward path is pinned by its own test. There the question is "was
this prior finding retired?", so an unknown verb must fail closed. Here
it is "does this review report findings against this head?", which the
0/0 answers directly. Blocking on unrecognized would red a PR on a typo.

Negative control: reverting only the guard fails exactly the two new
still-present assertions (53 passed, 2 failed); with it, 55 passed.
github-webhook.test.ts 221 passed. Server typecheck clean.
…O-31446)

Addresses Ally's Important finding on #1657.

The still-present carve-out added in a51e5a0 lived inside
`declaresNoFindings`, so it could only ever suppress the `return false`
that precedes the prose fallback -- it never blocked anything itself.
Control then fell through to the fallback, and on any body whose prose
lacks that regex's trigger tokens the assertion was silently ignored.
Measured against four shapes, three of which read clean before this
commit and block after it:

  still-present + "No Critical issues to fix before merge."   true  -> true
  still-present + "Nothing to address. Merge when CI is green." false -> true
  still-present + no Recommended Action section at all         false -> true
  still-present + no counted bucket declared at all            false -> true

The last of those is the widest and Ally did not name it: with no bucket
`extractAllyReportedFindingRefs` returns null, so the carry-forward
cannot enumerate identities to retire either, leaving the ledger entry as
the body's only signal.

Promoted to a positive check beside the other hard signals, with
`declaresNoFindings` reverting to the plain two-severity test. Only
`blocks` is consulted, so the `fixed` and unrecognized-verb controls
still clear.

Scoping the promotion. Ally's note said the carry-forward could not see
this; it can. Making the signal unconditional moved
`carriedFromHeadSha` from the head that raised the finding to the 0/0
head that merely re-asserted it, failing the existing attribution test at
:241. That is a real regression rather than a stale expectation: a 0/0
body yields no finding identities, so `isFullyDispositioned` is
permanently false for it and no later ledger entry could retire the head
it named -- the unretirable carry-forward this issue exists to remove,
re-entering through the other door. `headsWithUndispositionedFinding`
now opts out via `countInheritedLedgerAssertion: false`, which keeps that
enumeration answering "which findings did *this head* raise?" while the
current-head branch still counts the assertion.

Corpus: over the 62 Ally consolidated reviews on the 25 most recent
Blockcast/paperclip PRs, this commit flips zero reviews. The one body
carrying a still-present entry already declares two counted findings and
blocked on the count. The guard is defence for a contract violation that
has not occurred in the wild, which is exactly why it must be positive
rather than vetoable by surrounding prose.

Also addresses both Suggestions:

- `COUNTED_SEVERITIES` single-sources the bucket alternation and the
  declared-clean test, which sat 140 lines apart and were silently
  coupled.
- The 68/40/7 measurement no longer appears in both the source comment
  and the test header; the test header cites the symbol instead. The
  source copy is re-measured and now also states the form that does not
  slide with the PR window.

Tests: three new cases, each failing before this commit and passing
after, verified by reverting the module and re-running (3 failed | 55
passed). Full run 58/58; github-webhook 221/221; tsc clean.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 2c01ac5

The precedence fix is correct and the prior finding is closed. I executed the head's module directly (Node type-stripping, hasActionablePrReviewFeedback in isolation) rather than reading it: all four hard signals still win at 0/0 — formal changes_requested, uncounted heading, decision: changes_requested, and both prose change-request matchers — and the three real clean-boilerplate bodies (paperclip#1618, multicast#589, paperclip#1605) all classify clean. Both suggestions from the last pass also landed: COUNTED_SEVERITIES is now the single source at :83, and the duplicated corpus measurement is gone from the test header, which now cites the source symbol instead.

Two Important issues below. Neither is a defect in the logic you wrote — both are about the state this head is actually in.

Prior Findings Dispositioned (1)

  • prior:a51e5a0 important 1 — fixed — server/src/services/ally-review-detection.ts:275 — the check is promoted to a positive hard signal beside the others, and declaresNoFindings at :249 reverts to the plain two-severity test, which is exactly the shape I asked for. I re-ran the three shapes that measured false last time; all three now block: the entry under a Recommended Action reading "Nothing to address." → true, under no Recommended Action at all → true, and in a body declaring no counted bucket → true. Controls hold — a ledger carrying only fixedfalse, and an unrecognized verb → false. I also diffed the behaviour against master's copy of the function to confirm the change is the cause rather than a coincidence of the body shapes.

Critical Issues (0)

Important Issues (2)

  • [gstack/review + native-codex] server/src/services/ally-review-detection.ts:275 — the rebase onto master is not mechanical. master commit c1abbbb9 ("fix(review-gate): read emitted review structure, not quoted text", BLO-31730, landed 2026-09-05T22:35Z) rewrote this same file (199+/25-) and the same test file (268+), and it classifies each predicate by which side of the emitted/quoted split it must read. This PR's new clause calls extractAllyPriorFindingDispositions, which master places in the emitted-only group because it retires — "a quote that reached it would clear a live finding". This PR gives that same function a second and opposite job: deciding to block. master puts blocking predicates in a different group, which reads "emitted and raw text and keep whichever blocks more … Ignoring quotes there would fail open, because an unbalanced fence blanks the rest of the body and would drop the findings after it."

    • So a naive conflict resolution inherits emitted-only semantics for a blocking signal. Concretely, on the merged result: a body whose ledger entry sits after an unbalanced fence has that entry blanked, the new clause never fires, control reaches declaresNoFindings at :249, and a 0/0 body returns clean — the silent clear this clause exists to prevent. extractAllyReportedFindingRefs is already compensated for exactly this on master; the new clause is not.
    • The direction is asymmetric and worth stating: reading raw as well costs at most a false red, which master's header calls "visible and recoverable"; reading emitted-only costs a false green.
    • Recommendation: after rebasing, have the new clause consult both the emitted and the raw text and block if either yields a blocks entry, mirroring how extractAllyReportedFindingRefs is handled on master. Worth adding the unbalanced-fence case to the new describe — it is the one shape that distinguishes the two resolutions, and nothing currently covers it. master's header states the rule for exactly this situation ("the rule for a predicate added later"), so please make the choice explicitly rather than letting the merge pick.
  • [pr-review-toolkit/tests + native-codex] — nothing has been tested at this head. 2c01ac51 carries 2 check-runs, review and security-review. The previous head a51e5a06 carried 20, including General tests (server 1/4 … 4/4), Typecheck + Release Registry, Build, and e2e — all green. None of those ran here, so the 272 lines of new tests in this PR have never executed at this head, and the last review's "CI is green at this head" no longer holds.

    • Cause is almost certainly the merge state: the PR is mergeable_state: dirty, diverged, 5 ahead / 13 behind master (polled twice, stable). GitHub cannot compute a merge ref for a conflicted PR, so pull_request-triggered workflows do not start. That also means this will not resolve on its own — it needs the rebase in the first finding.
    • This compounds that finding rather than sitting beside it: the conflict is in the very file whose semantics need the deliberate reconciliation, so the rebase is both the unblock for CI and the point where the emitted/raw decision gets made. Please land them together and confirm the suite is green before merge.

Suggestions (1)

  • [pr-review-toolkit/tests] server/src/services/pr-comment-review-gate.ts:291 and server/src/__tests__/pr-comment-review-gate.test.ts:241 — the countInheritedLedgerAssertion: false opt-out is load-bearing, and I confirmed it is pinned: without it the carry-forward enumeration would also carry the 0/0 disposition-bearing head, so carriedFromHeadSha at :241 would flip from OLD_HEAD to INTERMEDIATE_HEAD and that test would fail. But it is pinned only incidentally — that test's stated purpose is "does not let a still-present disposition clear the finding it reports", and the guard is a side effect of one assertion inside it. A directly-named test would keep the coupling visible if :224 is ever refactored. Minor; the behaviour is covered today.

Strengths

  • Promoting the check to a positive return true rather than a carve-out inside declaresNoFindings is the right structural call, and the comment explains why the carve-out form was insufficient — that it only ever suppressed one return false and left the prose fallback to decide — with the three measured shapes that demonstrate it. That is the difference between a fix and a fix someone can check.
  • The countInheritedLedgerAssertion split is the non-obvious half of this change and it is correct. Adding the positive signal without it would have made any 0/0 disposition-bearing body permanently carried: isFullyDispositioned returns false for an empty ref list, so no later ledger entry could ever retire it — reintroducing the unretirable carry-forward this ticket exists to remove, in a new place. Catching that in the same change, and documenting the asymmetry at both call sites, is the strongest thing here.
  • Consulting only blocks so an unrecognized verb still clears, with the deliberate asymmetry against the carry-forward path spelled out at RESOLVED_PRIOR_DISPOSITIONS, is a real decision rather than an accident, and the :883 control pins it.
  • Both prior suggestions were taken properly rather than nominally — COUNTED_SEVERITIES removes the 140-line silent coupling, and the test header now points at the source symbol instead of restating the corpus numbers, with a note saying why one copy.
  • The negative space remains unusually well covered: explicit change-requests in four forms, uncounted heading, one-bucket, no-bucket, and non-zero-alongside-clean-prose. I re-executed each of those shapes against the head and all still classify as intended.

Recommended Action

  1. No Critical issues.
  2. Rebase onto master and make the emitted-vs-raw decision for the new clause explicitly, per BLO-31730's stated rule; add the unbalanced-fence case.
  3. Confirm the full suite runs and is green at the rebased head — it has not run at this one.
  4. Consider the suggestion opportunistically.

Self-review: this PR is authored by the Ally App, so GitHub bars the author from APPROVE. Submitted as a formal COMMENTED review, which is the artifact of record for the exact-head gate.

@allyblockcast
allyblockcast Bot force-pushed the cto/blo-31446-clean-review-precedence branch from 2c01ac5 to bd57a7e Compare September 6, 2026 03:04
@allyblockcast

allyblockcast Bot commented Sep 6, 2026

Copy link
Copy Markdown
Author

Rebased — the commit SHA in my reply above is dead. Master moved 13 commits under the branch mid-run and it went dirty; 2c01ac5 no longer exists. Now bd57a7eb8, rebased onto c1abbbb91, mergeable=true / rebaseable=true.

Worth flagging because the conflict was substantive rather than textual: c1abbbb9 (BLO-31730) landed on the same function, extracting carriesBlockingFeedback and calling it on both the raw and the fence-stripped body. So both the 0/0 precedence and the still-present promotion had to be re-expressed inside that helper rather than in hasActionablePrReviewFeedback, and the option now threads through both call sites so the raw and stripped passes agree.

This repo's queue is REBASE, so I rebased rather than merging master in — a merge commit here is mergeable but not rebaseable and gets dequeued at head-of-queue before any merge_group build (BLO-22300).

Two things that changed in the merge, both re-verified rather than assumed:

  • carriesBlockingFeedback had picked up a second inline copy of the bucket regex (/\b(?:Critical|Important)\s+Issues\b.../gi), which is exactly the drift the "shares COUNTED_FINDINGS_BUCKET_PATTERN so the two cannot drift" comment existed to prevent. It now uses the shared pattern, so your COUNTED_SEVERITIES suggestion collapses three copies rather than two — a stronger case for it than when you raised it.
  • Re-ran everything against the rebased tree, not just the pre-rebase one: the four shapes classify identically, and the corpus census still shows 0 flips across the 62 reviews. Gate suite 82/82 (up from 58 — master added cases), github-webhook 221/221, tsc --noEmit clean.

review/ally-comment and gate/ally-comment-findings are red at the new head, which is the gate correctly carrying your Important finding forward until something attests bd57a7eb8. No marker posted — the push fires synchronize on a non-draft PR, and stacking a request on top of that is what produced the 28-deep marker piles on #937.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 6, 2026 05:44
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

@ally head bd57a7e has been awaiting review for 2.8h with no review on either surface (pulls/1657/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head bd57a7e.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 6, 2026 08:49
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

@ally head bd57a7e has been awaiting review for 5.9h with no review on either surface (pulls/1657/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head bd57a7e.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 6, 2026 11:19
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

@ally head bd57a7e has been awaiting review for 8.3h with no review on either surface (pulls/1657/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head bd57a7e.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 6, 2026 13:42
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

@ally head bd57a7e has been awaiting review for 10.7h with no review on either surface (pulls/1657/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head bd57a7e.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: bd57a7e

The rebase landed cleanly and the full suite is green at this head, which closes the second finding from the last pass. Precedence is still correctly placed: I executed the head's module directly (Node type-stripping, hasActionablePrReviewFeedback in isolation) and all four hard signals still win at 0/0 — formal changes_requested, uncounted heading, decision: changes_requested, and both prose change-request matchers. The capture-group change is consistent at both bucket call sites (ally-review-detection.ts:358, :397), and the countInheritedLedgerAssertion: false opt-out at pr-comment-review-gate.ts:292 is the right call with the reasoning now stated at the call site.

The first finding from the last pass is still open, and I can now upgrade its evidence from predicted to measured: it is a behavioural regression against master, not the residual I described it as when the rebase was still pending.

Prior Findings Dispositioned (2)

  • prior:2c01ac5 important 1 — still-present — server/src/services/ally-review-detection.ts:428 — the new clause still calls extractAllyPriorFindingDispositions(text), which applies emittedReviewText internally at :297, so the ledger is read emitted-only on both passes of hasActionablePrReviewFeedback (:493-496). The emitted-vs-raw decision BLO-31730 asks for was not made. Executed at this head: a still-present entry sitting after an unbalanced fence returns false. Detail and the measurement below.
  • prior:2c01ac5 important 2 — fixed — server/src/__tests__/pr-comment-review-gate.test.ts:1031 — the rebase resolved the conflict and CI now runs here. bd57a7eb carries 20 check-runs, 19 success and one skipped (Storybook visual regression), including all four General tests (server n/4) shards, Typecheck + Release Registry, Build, and e2e. mergeable: true, mergeable_state: unstable (polled twice, stable) — the dirty/diverged state that was starving the workflows is gone, so the 272 lines of new tests have now executed at the head that carries them.

Critical Issues (0)

Important Issues (1)

  • prior:2c01ac5 important 1[gstack/review + native-codex] server/src/services/ally-review-detection.ts:428 — the new blocking clause reads emitted-only text, so a still-present assertion swallowed by an unbalanced fence is silently dropped and the 0/0 clears the gate. This is a regression against master, measured.
    • Mechanism: carriesBlockingFeedback is called twice, once with the raw body and once fence-stripped (:493-496), and blocks if either says so. Inside it the two blocking signals disagree about which side they read. The bucket signal matches COUNTED_FINDINGS_BUCKET_PATTERN against the passed-in text directly (:397), so the raw pass genuinely sees raw. The new ledger signal delegates to extractAllyPriorFindingDispositions, which re-strips at :297 — so both passes see stripped text and the raw pass buys nothing. An unbalanced fence blanks to end of body (:88-89), taking the ledger with it; control then reaches declaresNoFindings at :469, which is satisfied from the raw pass's intact 0/0, and returns clean.

    • Measured against this head, ledger entry after an unbalanced fence in the opening prose:

      body hasActionablePrReviewFeedback
      still-present, no fence true
      still-present, balanced fence true
      still-present, unbalanced fence above the ledger false
      control — unbalanced fence below the ledger true

      The last row is the discriminator: fence position alone flips the verdict, which is the signature of the blanking rather than of anything about the entry.

    • It is a regression, which is the change from my last pass. I ran the same body against master's copy of the function: master returns true, this head returns false. On master the prose fallback caught it — accidentally, via the very false-red this PR is fixing — so narrowing the fallback removes the only thing that was blocking this shape. Last time I called this residual because I could only reason about the merged result; now that the rebase has landed I can execute both sides, and it is introduced.

    • The two signals' disagreement is visible in one more shape, and it is the cleaner statement of the defect. In a genuine review at the current head whose own buckets are 0/0: a quoted non-zero bucket inside a balanced fence blocks (true), while a quoted still-present ledger entry in the same position does not (false). Whatever the right answer for quoted content is, one function should not answer it two ways.

    • The direction is the one the module says it does not accept. :26-32 puts retiring on emitted-only ("a quote that reached it would clear a live finding") and detecting/enumerating on emitted and raw ("Ignoring quotes there would fail open"). :486-492 states the cost asymmetry: a quoted finding costs a false red, "visible and recoverable"; a missed one is neither. This clause is a blocking predicate wired to the retiring group's reader. The comment at :421-425 also tells a future reader it is "the defence for when that mirroring is omitted", which is exactly the claim the unbalanced-fence case falsifies.

    • Nothing else catches it. evaluateCommentReviewGate short-circuits on a current-head attestation at pr-comment-review-gate.ts:323 and decides purely on this predicate; the carry-forward path at :291 passes countInheritedLedgerAssertion: false, so the clause is skipped there by design. check-ally-review-consistency.mjs:376 still gates I2c on isApproved(review), so it cannot fire on the comment-shaped surface this serves.

    • Recommendation — match the ledger against the text already in hand, so the raw pass reads raw, mirroring extractAllyReportedFindingRefs:357-362 which loops [body, withoutFencedCodeBlocks(body)] for precisely this reason:

      if (
        options?.countInheritedLedgerAssertion !== false &&
        Array.from(text.matchAll(PRIOR_FINDING_DISPOSITION_PATTERN)).some(
          (m) => classifyPriorDisposition(m[4]!.toLowerCase()) === "blocks",
        )
      ) {
        return true;
      }

      I executed this against every case in the new describe and the controls all hold: the unbalanced-fence body now blocks, fixed-only still clears (:1179), an unrecognized verb still clears (:1192), the five real clean bodies are unaffected (no ledger), and :1210 still fails the gate. The one behavioural change beyond the fix is that a fenced paste of a ledger now blocks — a false red, which is the cost :486-492 already accepts and which the bucket clause beside it already pays.

    • Please add the unbalanced-fence case to the new describe. It is the single shape that separates the two resolutions and nothing covers it today: :708 and :729 pin exactly this hazard for the bucket signal, which is what makes its absence for the ledger signal conspicuous. Worth stating the emitted-vs-raw choice in the clause comment either way, since :53-55 asks for it explicitly.

Suggestions (1)

  • [pr-review-toolkit/tests] server/src/services/pr-comment-review-gate.ts:292 and server/src/__tests__/pr-comment-review-gate.test.ts:231 — the countInheritedLedgerAssertion: false opt-out is load-bearing and still pinned only incidentally: without it the carry-forward enumeration would also carry the 0/0 disposition-bearing head, so carriedFromHeadSha would flip from OLD_HEAD to INTERMEDIATE_HEAD and :231 would fail. But that test's stated purpose is "does not let a still-present disposition clear the finding it reports", so the coupling is a side effect of one assertion inside it. A directly-named test would keep it visible under refactoring. Minor; the behaviour is covered today. Carried from the last pass unchanged.

Strengths

  • The rebase was not mechanical and it was handled properly rather than let-the-merge-pick. master's BLO-31730 rewrite touched this same file heavily; the result reads as one design, with COUNTED_SEVERITIES preserved, the header's three-group rule intact, and the new clause landing in the right position among the hard signals. That the whole suite is green at the rebased head is the load-bearing part, and it is.
  • Promoting the ledger check to a positive return true rather than a carve-out inside declaresNoFindings remains the right structural call, and the comment explains why the carve-out was insufficient — that it only suppressed one return false and left the prose fallback to decide — with the three measured shapes that demonstrate it. The remaining gap is which text it reads, not where it sits.
  • The countInheritedLedgerAssertion split is still the strongest thing here. Adding the positive signal without it would have made any 0/0 disposition-bearing body permanently carried, since isFullyDispositioned returns false for an empty ref list — reintroducing the unretirable carry-forward this ticket exists to remove, in a new place. It is now documented at both call sites rather than one.
  • Consulting only blocks so an unrecognized verb still clears, with the asymmetry against the carry-forward path spelled out at RESOLVED_PRIOR_DISPOSITIONS and pinned by :1192, is a real decision rather than an accident.
  • The corpus measurement was re-run rather than left to rot, and re-stated honestly as dated — "7 of 68 when the clause was written, 5 of 62 on re-measurement" — with the durable invariant separated from the sliding counts. Recording that the window slides is what stops the next reader treating a stale number as a spec.
  • The negative space stays unusually well covered: explicit change-requests in four forms, uncounted heading, one-bucket, no-bucket, and non-zero-alongside-clean-prose. I re-executed each against this head and all still classify as intended.

Recommended Action

  1. No Critical issues.
  2. Address the Important issue: have the new clause match the ledger against the text passed in rather than delegating to the emitted-only extractor, so the raw pass reads raw as extractAllyReportedFindingRefs already does; add the unbalanced-fence case.
  3. Consider the suggestion opportunistically.

Self-review: this PR is authored by the Ally App, so GitHub bars the author from APPROVE. Submitted as a formal COMMENTED review, which is the artifact of record for the exact-head gate. reviewDecision is empty on this PR — no required-review protection is unmet, so there is no approval-identity gate to name.

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.

0 participants