Skip to content

review: merge one source's identical finding across files into one pattern-level comment - #348

Merged
jwbron merged 4 commits into
mainfrom
jwies/review-crossfile-cluster
Aug 13, 2026
Merged

review: merge one source's identical finding across files into one pattern-level comment#348
jwbron merged 4 commits into
mainfrom
jwies/review-crossfile-cluster

Conversation

@jwbron

@jwbron jwbron commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What

A new dedup pass merges one source's identical (or near-identical) finding on several files into a single pattern-level comment. Both existing tiers require the same path and different sources by design: they merge agreement between reviewers, not repetition by one reviewer. The pass runs after the cross-source tiers settle (dedupeClaimsWithCrossFile in dedup-crossfile.ts, the entry point dispatch now calls), so it can only remove comments the tiers left standing, never a merge they made. Merged copies skip validation and posting and are recorded in the run artifact under crossFileMerges.

Why

Measured on Khan/webapp#41440, the first post-deploy window for v1.13.0: inline comments 3764122555 and 3764122558 are byte-identical 566-character suggestion (non-blocking, documentation) bodies on two sibling eval YAML files, posted by one source in one run. In-run clustering (#312) could not reach the pair; every rule it has forbids exactly this shape (same path, different sources), and #336 already had to fix a clusterer example that promised the grouping the path rule forbids.

Semantics

  • Same source and same label, exactly. Merging across sources is the other tiers' job, with their own calibration. Label equality makes the verdict arithmetic trivial: a merged blocking group keeps one blocking claim, so it floors the verdict exactly once, and a wrong merge can never soften a verdict.
  • Identical text, or near-identical above the STRICT floor. Identical subject/discussion/failure-scenario text merges outright (the measured 41440 shape; short identical bodies must not fail on a bigram count). Anything else must clear describesSameDefect with the lines stripped, which forces the stricter OTHER_LINE_FLOOR: equal line numbers in different files are coincidence, not anchor evidence, and never buy the laxer exact-anchor tier.
  • Survivor is the first occurrence in diff order (staged files.json; claim order when the staging is absent or does not know a path). Its discussion gains one trailing "Also applies to" line naming the other occurrences (path, and line where known). Those occurrences are prose, not anchors, so the change-provenance gate applies to the survivor's own anchor only; every copy already passed the gate individually before dedup ran.
  • Star guard, as in tier 1: only a copy mergeable against the survivor itself merges, so a bridging claim cannot chain distinct findings.
  • Fail toward not merging. Any doubt in similarity posts separately: a missed merge costs a duplicate comment, a wrong one drops a finding.

Tests

New dedup-crossfile.test.ts (13 tests): the 41440 exact-duplicate shape; diff-order survivor election and the claim-order fallback; near-identical above the strict floor merges; a between-floors pair (clears exact-anchor, fails different-line) does not merge on coincidentally equal lines; label, source, same-path, and unanchored exclusions; a blocking group keeps exactly one blocking claim; a three-file group lists both other occurrences; the star guard. New dispatch-crossfile.test.ts (2 tests): end-to-end collapse with the merged copy skipping validation and the artifact recording the merge, and a differing pair posting both. Full workflows/review suite: 1341 tests pass; pnpm lint and pnpm typecheck clean.

… finding across files into one pattern-level comment
@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 53059ea

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
review Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review live A/B

No reviewable delta: review.md is byte-identical in both arms (baseline origin/main, sha e8782e43bf23), so the extracted prompts and the orchestrator body match and no arms were run. Pass --force-arms for a deliberate wobble control.

@khan-actions-bot
khan-actions-bot requested review from a team, jeresig and somewhatabstract and removed request for a team August 12, 2026 20:43

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested — see inline comments.

Comment thread workflows/review/lib/dispatch.ts Outdated
Comment thread workflows/review/lib/dedup-crossfile.ts Outdated
…er open-thread suppression

Review finding on #348: suppression only matches a thread to a claim on
the thread's own path, so merging first let an open thread on the
survivor's file swallow every other file's occurrence (author copies
flawed a.ts to b.ts, a.ts tracked by an open thread: b.ts never posts,
on any run). Order is now suppress-then-merge, composed and documented
in dedup-crossfile.ts (suppressThenMergeCrossFile) with a dispatch-level
regression test. Also documents the accepted validation asymmetry the
re-review asked about (survivor-only validation across files).
@khan-actions-bot
khan-actions-bot requested a review from a team August 12, 2026 22:47
@github-actions

Copy link
Copy Markdown
Contributor

Review Guidance

github-actions (2 files)
File Reason
dedup-crossfile.ts New dedup pass in the enforcement layer that can drop findings by merging them; a bug changes what gets posted on every consumer PR. It runs after the cross-source tiers and provenance gate and fails toward posting separately, so a wrong decision costs a duplicate comment rather than a softened verdict.
dispatch.ts Swaps the suppression call for a composed suppress-then-merge step whose ordering is load-bearing (merge-first would silently drop a sibling file's finding); the ordering is pinned by an end-to-end regression test.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All 2 prior review threads are resolved.
Note: divergence tripwire re-armed a full review (unreviewed share 0.64).

Comment thread workflows/review/lib/dedup-crossfile.ts
Comment thread workflows/review/lib/dedup-crossfile.ts
* floors above demand identical-or-near-identical text from ONE source, and
* the occurrence list is prose ("Also applies to..."), not an anchored
* claim, so a wrong merge publishes a weaker statement about the other file
* than a separate comment would have. A refuted survivor drops its whole

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question (non-blocking): Since crossFileMerges records every dropped copy, could a refuted survivor un-merge instead of dropping the group? The header accepts that a refuted survivor drops its whole group and argues the same text is unlikely to be valid on only the sibling — but validation verdicts can hinge on the survivor's file specifically, and unlike the cross-source tiers (whose copies share a path, so one verdict genuinely covers them), here the verdict speaks for files the validator never opened. The merge record already carries enough (id, path, line) to resurrect copies for posting-without-revalidation or at least to emit a note line, keeping the failure cost at 'duplicate comment' rather than 'lost finding'.

…ltering branch; pin staging degradation

Re-review notes on #348: the chain test excluded its third claim by
label before grouping, so the star guard never ran as a filter; a
bigram-chain fixture (A~B and B~C above the floor, A~C below) now
reaches it and pins that a bridged member stays its own claim. Direct
tests cover suppressThenMergeCrossFile's malformed-staging fallback.
The un-merge-on-refutation question stays as designed: a refuted
survivor drops its group (documented in the header).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested — see inline comments.
2 of 3 prior review threads resolved; 1 still unaddressed as of dbc211d:

1 non-blocking thread still open
Note: skill-auditor not assessed this run (skill-auditor output unavailable). Note: completeness not assessed this run (completeness output unavailable). Note: holistic not assessed this run (holistic output unavailable). Note: first-principles not assessed this run (first-principles output unavailable). Note: conventions not assessed this run (conventions output unavailable). Note: documentation not assessed this run (documentation output unavailable). Note: re-review ran at scoped depth (re-review mode scoped, blocking-only).

Comment thread workflows/review/lib/dedup-crossfile.ts
…s a corrected discussion erases

Merge resolution: main's suppressTrackedDuplicates (open + adjudicated
corpora, #332/#333) replaces the open-only pass inside
suppressThenMergeCrossFile; the cross-file merge still runs after both
suppression passes, and shapeFailure rides the composed result.

Blocking re-review finding: applyVerifications applies a validator
corrected.discussion by wholesale replacement, which erased the merged
survivor's 'Also applies to' line and lost the sibling file's finding
from all posted output. The line is now one renderer
(alsoAppliesLine) and reapplyCrossFileOccurrences re-builds it from the
merge records after validation; unit and dispatch-level tests cover the
corrected, untouched, and dropped-survivor shapes.
@jwbron
jwbron merged commit 512a2a7 into main Aug 13, 2026
6 checks passed
@jwbron
jwbron deleted the jwies/review-crossfile-cluster branch August 13, 2026 01:31

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested — see inline comments.
1 of 2 prior review threads resolved; 1 still unaddressed as of 53059ea:

1 non-blocking thread still open
Note: divergence tripwire re-armed a full review (unreviewed share 0.50).

exactKey(a) === exactKey(b) ||
describesSameDefect({...a, line: undefined}, {...b, line: undefined});

const mergeableAcrossFiles = (a: Claim, b: Claim): boolean =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (blocking): A cross-file merge into an author-disputed survivor can soften the verdict, contradicting the module's stated invariant. Introduced by this change. mergeableAcrossFiles compares source, label, path, line, and text — but never author_dispute, and disputes are attached per (path, line) (dispatch.ts:784-802, matching the staged author-disputes entries against each candidate's own anchor), so a byte-identical finding on two sibling files can carry a dispute on exactly one copy. When the disputed copy wins survivorship (its file first in diff order), the undisputed sibling's blocking copy is merged away, and applyVerifications (dispatch-contracts.ts:677-680, checked on the checkout) then caps any non-confirmed disputed survivor to question (non-blocking) — including on the validator-unavailable path, and on a plausible verdict. Without the merge, the sibling copy posts and retains its blocking label (an undisputed, unmentioned claim is retained as-is), so the merge silently drops a blocking finding's force. That directly contradicts this header's guarantee that "a wrong merge can never soften a verdict (the survivor carries the same label every copy did)": label equality holds at merge time, but the dispute cap re-labels the survivor afterward. The path is reachable precisely because blocking candidates are exempted from adjudicated-thread suppression so a disputed re-flag stays visible — that disputed claim is then a live merge survivor. The one-line fix keeps the module's fail-toward-not-merging direction: require equal dispute state, so an asymmetric pair posts as two comments (the accepted duplicate-comment cost) instead of one capped comment.

A sketch, not a committable replacement:

const mergeableAcrossFiles = (a: Claim, b: Claim): boolean =>
    a.source === b.source &&
    a.label === b.label &&
    // An asymmetric dispute must not merge: the dispute cap re-labels a
    // non-confirmed disputed survivor, which would soften the verdict the
    // undisputed copy carried (the invariant above forbids exactly that).
    a.author_dispute === b.author_dispute &&
    a.path !== undefined &&
    b.path !== undefined &&
    a.path !== b.path &&
    a.line !== undefined &&
    b.line !== undefined &&
    sameCrossFileFinding(a, b);
Lower-confidence observations (2)
  • workflows/review/lib/dedup-crossfile.ts:107 suggestion (non-blocking): The near-identical branch serves an unmeasured shape and carries most of the module's complexity — consider shipping exact-identical-only first.
  • workflows/review/lib/dedup-crossfile.ts:7 thought (non-blocking): The root cause is upstream: findings carry exactly one anchor, and no reviewer prompt asks a source to report a repeated pattern once.

"A reader trusts the comment and edits the wrong versions entry.",
});

describe("runDispatch cross-file duplicate merge", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

note (non-blocking): PR description undercounts the delivered tests (13→17 and 2→4). The description's Tests section enumerates two dispatch scenarios but the file adds two more (validator-corrected-discussion re-apply and the suppression-ordering regression), and lists 13 dedup cases against 17 present. This is over-delivery, not a gap — flagging only so the description count can be refreshed.

* blocking claim, so it floors the verdict exactly once, and a wrong merge
* can never soften a verdict (the survivor carries the same label every
* copy did).
* - **Different paths, both anchored.** A same-path pair belongs to tier 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

note (non-blocking): "A same-path pair belongs to tier 1" is inaccurate for the same-source pairs this module handles. Tier 1's mergeable requires a.source !== b.source (dedup.ts:618-624, checked on the checkout), so a SAME-source pair on the SAME path — one reviewer stamping identical text twice on one file, or a bridge-chained group whose star guard leaves a same-path member standing beside the survivor — merges in no tier at all and posts as two comments. The docstring here (and the dedup-crossfile.test.ts test name "leaves same-path pairs to tier 1") tells a future maintainer that case is handled elsewhere when it is handled nowhere. Posting duplicates is this module's accepted failure direction, so the behavior is fine; the pointer to tier 1 is what misleads. Consider rewording to say a same-source same-path pair is out of every tier's scope by design.

@@ -0,0 +1,316 @@
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (non-blocking): Module filename "crossfile" is unhyphenated where the repo hyphenates multi-word suffixes. The repo hyphenates word boundaries in multi-word module names (dedup-pr-level.ts, dispatch-gate-plan.ts, submission-blocking-only.test.ts), and this concept is spelled "cross-file" everywhere in the code itself (via: "cross-file", // Cross-file duplicate merge), but the new files are dedup-crossfile.ts and dispatch-crossfile.test.ts.

A sketch, not a committable replacement:

Rename to `dedup-cross-file.ts` and `dispatch-cross-file.test.ts` to match `dedup-pr-level.ts` and the "cross-file" spelling used throughout the code.

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.

2 participants