Skip to content

fix(iterate-pr): stop content-classifying review summaries, fix nit punctuation - #311

Open
thecodedrift wants to merge 2 commits into
mainfrom
fix/logaf-fallback-classifier
Open

fix(iterate-pr): stop content-classifying review summaries, fix nit punctuation#311
thecodedrift wants to merge 2 commits into
mainfrom
fix/logaf-fallback-classifier

Conversation

@thecodedrift

Copy link
Copy Markdown
Member

Summary

categorizeComment in .agents/skills/iterate-pr/scripts/fetch_pr_feedback.cjs had two measured defects in its content fallback (used when no explicit h:/m:/l: marker is present):

  1. Negation blindness. HIGH patterns (blocker, security issue, critical, …) matched anywhere in the body with no regard for what preceded them, so a clean review's own closing sentence — "not a blocker", "no security issue" — got filed high. Measured on the real review summary of test(cli): cover the tested/failed/refused split on a mixed verify/test run #307.
  2. Punctuation-sensitive tokens. nit[:\s] required a colon or whitespace immediately after the word, so the common Nit, spelling missed and landed in medium, the bucket the skill auto-fixes without prompting. Measured on a real inline comment from test(cli): cover PlatformBinaryResolution.source and asMatchMode's refusal #304.

Fix

  • Review summaries are now bucketed structurally, not by content. A summary narrates a review's findings, so it will always contain finding vocabulary, frequently negated, and no content-pattern list survives that. CHANGES_REQUESTED from a non-author reviewer still stays high (unchanged); an explicit h:/m:/l: marker still wins; otherwise the summary is surfaced but filed low so it doesn't inflate needs_attention. This applies to both human and review-bot summaries — the test(cli): cover the tested/failed/refused split on a mixed verify/test run #307 misfire was a bot's own review summary. The findings themselves still arrive separately as inline review-thread comments and are classified individually, so nothing is lost.
  • The inline-comment fallback (still used for review threads and issue comments) now gives the bracketed-class LOW tokens (nit, suggestion, optional, minor, style) word boundaries (\bnit\b) so punctuation stops mattering, and refuses a HIGH/LOW match preceded by a negator (not/no/non/without/isn't/is not) within a 24-character window, so "not a blocker" and "no security issue" stop counting as findings.
  • detectLogaf (the explicit-marker path) is untouched, and an explicit marker still wins over everything, including in review summaries — covered by a new test.

Self-review summaries stay surfaced and flagged self_review: true (a PR author can't formally request changes on their own PR); review_in_progress and its precedence in action_required are untouched.

Test plan

  • NODE_OPTIONS= pnpm test:scripts — 364 tests pass (was 356; added tests for negation, Nit, punctuation, marker precedence, and structural review-summary bucketing; updated one existing test whose expected bucket intentionally changed from medium to low)
  • NODE_OPTIONS= pnpm lint — passes (typecheck + eslint + built-CLI house-style check)
  • NODE_OPTIONS= node .agents/skills/iterate-pr/scripts/fetch_pr_feedback.cjs --pr 307 against the real, merged PR — summary.high: 0, summary.needs_attention: 0, confirming the clean review no longer reads as high-priority

Fixes #309

…unctuation

categorizeComment's content fallback had two measured defects: HIGH patterns
matched a word anywhere with no regard for negation ("not a blocker" filed
high), and nit[:\s]/suggestion[:\s]/etc. required a colon or space right
after the word, so "Nit," missed and landed in medium.

Review summaries are now bucketed structurally instead of by content: a
CHANGES_REQUESTED from a non-author reviewer stays high, an explicit
h:/m:/l: marker still wins, otherwise the summary is surfaced but filed low
so it doesn't inflate needs_attention. Summaries narrate findings and will
always contain finding vocabulary, frequently negated, so no pattern list
survives being run over them — the findings themselves still arrive
separately as inline threads and are classified individually.

The inline-comment fallback (still used for review threads and issue
comments) gets word boundaries on the bracketed-class LOW tokens
(nit/suggestion/optional/minor/style) so punctuation stops mattering, plus a
short negation window before HIGH/LOW pattern matches so "not a blocker" and
"no security issue" stop counting as findings.

detectLogaf (the explicit-marker path) is untouched and still wins over
content everywhere, including in review summaries.

Fixes #309
Filing a marker-less summary as `low` fixed the false `high` and introduced a
false prompt. `low` means "an optional suggestion, ask the user which to
address": step 3 presents low items as a numbered list and `action_required`
says so. Measured before this change, a review whose own conclusion was "I did
not find any correctness bugs" produced:

  action_required: "Review low-priority suggestions - ask user which to address"

which asks someone to triage a summary that proposes no work.

Summaries now go to a `review_summary` bucket, counted as
`summary.review_summaries` and reported in `action_required` as nothing at all.
Surfaced and countable without being actionable, the same shape
`review_in_progress` already uses, and the same reason: some things a caller
needs to see are not things a caller needs to act on.

`review_summary` is included in the `review_bot_feedback` and
`self_review_feedback` tallies even though it is not a priority bucket. Those
answer "where did this come from", not "how urgent is it", and leaving it out
silently zeroed `self_review_feedback` for an author whose only note is a
summary, which is the ordinary shape of a self-review.

An explicit `h:`/`m:`/`l:` marker still wins, in a summary as anywhere else, and
a reviewer's CHANGES_REQUESTED is still `high` on the review state.
@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 5m 22s —— View job


Review complete

Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json contained no reviewThreads, reviews, or standalone comments beyond the trigger comment itself, so this is the first review of this PR; nothing was treated as already-addressed.

  • Read .prior-review.json
  • Fetch PR diff (gh pr diff) and metadata (gh pr view)
  • Read changed files in full (fetch_pr_feedback.cjs, SKILL.md)
  • Run finder angles (correctness, removed-behavior, cross-file, cleanup, conventions)
  • Verify candidates against the code
  • Post inline comments

Findings

Posted as inline comments (all [New], first pass on this PR):

  1. .agents/skills/iterate-pr/scripts/fetch_pr_feedback.cjs:258NEGATORS (not|no|non|without|isn't|is not) omits common contractions like won't, doesn't, wasn't, can't, never, nothing. A body like "Nothing critical here, just a heads up" or "This won't be a blocker" still gets filed high — the exact class of bug this PR sets out to fix, just phrased differently.
  2. fetch_pr_feedback.cjs:275matchesUnnegated uses a non-global pattern.exec(body), which always returns the first match. If the first occurrence of a HIGH/LOW word is negated but a later, genuine occurrence of the same word exists in the same body, the pattern is treated as fully negated and the real finding is silently dropped (e.g. "not a blocker overall, but there's a real blocker in the retry logic").
  3. fetch_pr_feedback.cjs:41 (and the "Categories" list around line 15-24) — the module's top-of-file doc comment is stale: it says a review summary "defaults to low" absent a marker, but the second commit changed the actual default to the new review_summary bucket (categorizeReviewSummary returns detectLogaf(body) ?? "review_summary"). SKILL.md and the categorizeReviewSummary JSDoc were updated correctly; this block wasn't.
  4. .agents/skills/iterate-pr/SKILL.md, step 3 "Handle Feedback by LOGAF Priority" (~line 165-204; couldn't anchor an inline comment there since it's outside this diff's hunk context) — the Auto-fix / Prompt / Skip-silently / Wait categorization never mentions the new review_summary bucket, even though "Replying to Comments" further down assumes review_summary items exist and need an acknowledgement reply. An agent following step 3 literally would have no cue to ever process them.

Nothing else surfaced past verification — the negation-window sizing (24 chars), the word-boundary fix for nit/suggestion/etc., and the review_summary vs high/marker precedence all look correct and are exercised by the new tests.

// as findings. Measured on the real review summary of #307: "…so it's a
// 'worth a look,' not a blocker" matched `blocker` with nothing to say the
// word was negated.
const NEGATORS = /\b(?:not|no|non|without|isn't|is not)\b/i;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[New] NEGATORS is missing common negation contractions, so this fix still misses the exact class of bug it's meant to close.

const NEGATORS = /\b(?:not|no|non|without|isn't|is not)\b/i;

won't, doesn't, wasn't, wouldn't, can't, shouldn't, aren't, never, and nothing are all absent. Concretely, "Nothing critical here, just a heads up." and "This won't be a blocker." both still hit HIGH_PATTERNS (critical, blocker) unnegated, because "nothing" and "won't" aren't recognized as negators, and would still be filed high.

Worth extending the alternation (e.g. adding won't|doesn't|wasn't|wouldn't|can't|shouldn't|aren't|never|nothing) rather than shipping a negator list that only covers the two phrasings used in the #307 repro.

/**
* Whether any pattern matches `body` at a position not preceded by a negator.
*/
const matchesUnnegated = (patterns, body) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[New] matchesUnnegated only ever inspects the first occurrence of each pattern in the body.

const matchesUnnegated = (patterns, body) =>
  patterns.some((pattern) => {
    const match = pattern.exec(body);
    return match !== null && !isNegated(body, match.index);
  });

pattern.exec(body) on a non-global regex always returns the left-most match. If that first occurrence happens to be negated but a second, real occurrence of the same word exists later in the same body, the pattern is skipped entirely — the later occurrence is never checked.

Example: "It's not a blocker overall, but there's a real blocker in the retry logic that needs fixing." — the first blocker (negated) causes .some to treat the /blocker/i pattern as fully negated, so the second, genuine blocker a few words later is never classified as high. This drops a real finding silently, which is the opposite direction from the bug this PR fixes. Consider matching with a global flag and checking whether every match is negated, or re-searching from match.index + match[0].length when the first hit is negated.

* inline comment) is bucketed by content like any other, defaulting to
* `medium` absent a `h:/m:/l:` prefix. A self-review **summary** is bucketed
* structurally, not by content — see "Review summaries" below — defaulting
* to `low` absent a marker, so it stays visible without inflating

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[New] This module-level doc comment is now stale and contradicts the actual implementation.

Line 41 says a self-review summary defaults "to low absent a marker", and line 51 says a review summary "is surfaced but filed low". But categorizeReviewSummary (added in this same PR) returns detectLogaf(body) ?? "review_summary" — the actual default bucket is review_summary, not low. That bucket rename happened in the second commit ("give review summaries their own bucket, not low"), which updated SKILL.md and the categorizeReviewSummary JSDoc correctly but missed this top-of-file block.

Also, the "Categories" list near the top of the file (lines ~15-24) never mentions review_summary at all, even though it's now a real bucket in feedback and summary.review_summaries.

Since this is the first doc a maintainer reads when opening the file, it should match the SKILL.md wording ("its own review_summary bucket... never a priority bucket") rather than the pre-second-commit low default.

// as findings. Measured on the real review summary of #307: "…so it's a
// 'worth a look,' not a blocker" matched `blocker` with nothing to say the
// word was negated.
const NEGATORS = /\b(?:not|no|non|without|isn't|is not)\b/i;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[New] NEGATORS is missing common negation contractions, so this fix still misses the exact class of bug it's meant to close.

const NEGATORS = /\b(?:not|no|non|without|isn't|is not)\b/i;

won't, doesn't, wasn't, wouldn't, can't, shouldn't, aren't, never, and nothing are all absent. Concretely:

  • "This won't break anything, looks good."will\s+(break|cause|fail) fires on "will break" (inside "won't"... actually on the literal "break" via a different pattern, or directly if phrased "This will not break" the direct-adjacency patterns don't match, but "nothing critical here" and "this won't be a blocker" both still hit HIGH_PATTERNS (critical, blocker) unnegated, because "nothing" and "won't" aren't recognized as negators.
  • A review body like "Nothing critical here, just a heads up" would still be filed high.

Worth extending the alternation (e.g. adding won't|doesn't|wasn't|wouldn't|can't|shouldn't|aren't|never|nothing) rather than shipping a negator list that only covers the two phrasings used in the #307 repro.

/**
* Whether any pattern matches `body` at a position not preceded by a negator.
*/
const matchesUnnegated = (patterns, body) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[New] matchesUnnegated only ever inspects the first occurrence of each pattern in the body.

const matchesUnnegated = (patterns, body) =>
  patterns.some((pattern) => {
    const match = pattern.exec(body);
    return match !== null && !isNegated(body, match.index);
  });

pattern.exec(body) on a non-global regex always returns the left-most match. If that first occurrence happens to be negated but a second, real occurrence of the same word exists later in the same body, the pattern is skipped entirely — the later occurrence is never checked.

Example: "It's not a blocker overall, but there's a real blocker in the retry logic that needs fixing." — the first blocker (negated) causes .some to treat the /blocker/i pattern as fully negated, so the second, genuine blocker a few words later is never classified as high. This is a real finding getting silently dropped, which is the opposite direction from the bug this PR fixes. Consider matching with a global flag and checking whether every match is negated, or at least searching from match.index + match[0].length when the first hit is negated.

* inline comment) is bucketed by content like any other, defaulting to
* `medium` absent a `h:/m:/l:` prefix. A self-review **summary** is bucketed
* structurally, not by content — see "Review summaries" below — defaulting
* to `low` absent a marker, so it stays visible without inflating

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[New] This module-level doc comment is now stale and contradicts the actual implementation.

Line 41 says a self-review summary defaults "to low absent a marker", and line 51 says a review summary "is surfaced but filed low". But categorizeReviewSummary (added in this same PR, see below) returns detectLogaf(body) ?? "review_summary" — the actual default bucket is review_summary, not low. That bucket rename happened in the second commit ("give review summaries their own bucket, not low"), which updated SKILL.md and the categorizeReviewSummary JSDoc correctly but missed this top-of-file block.

Also, the "Categories" list near the top of the file (lines ~15-24) never mentions review_summary at all, even though it's now a real bucket in feedback and summary.review_summaries.

Since this is the first doc a maintainer reads when opening the file, it should match the SKILL.md wording ("its own review_summary bucket... never a priority bucket") rather than the pre-second-commit low default.

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.

LOGAF fallback classifier reads negated prose and punctuation wrong, inflating needs_attention

1 participant