mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #321] [MERGED] fix(cursor-review): build the incremental hunks block as a subset of the reviewed diff #321
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#321
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/321
Author: @mattmillerai
Created: 9/18/2026
Status: ✅ Merged
Merged: 9/20/2026
Merged by: @mattmillerai
Base:
main← Head:be-15558-incremental-diff-subset📝 Commits (2)
da6cbc8fix(cursor-review): build the incremental hunks block as a subset of the reviewed diffa2da829fix(cursor-review): close seven gaps the panel found in the incremental block📊 Changes
5 files changed (+1095 additions, -7 deletions)
View changed files
📝
.github/cursor-review/README.md(+5 -0)➕
.github/cursor-review/incremental-diff.py(+282 -0)➕
.github/cursor-review/tests/test_incremental_diff.py(+687 -0)📝
.github/workflows/cursor-review.yml(+117 -7)📝
docs/callers/cursor-review.md(+4 -0)📄 Description
ELI-5
Each review round after the first gets a bonus block in the prompt saying "here are the hunks that are new since last time — look here first". The prompt calls that block "the subset of the diff above". It wasn't one. It was computed as
git diff <last-reviewed-commit>...<head>, and if you mergemaininto your branch, your last-reviewed commit becomes an ancestor of head — so that range quietly swallows every commit the merge pulled in frommain. The reviewer was handed a mountain of other people's code, labelled as your PR.The fix stops diffing two commits and starts diffing two PR patches: what the last round saw, versus what this round is reviewing. Both are three-dot diffs against the base, so neither can contain a base-branch commit, and every section emitted is copied verbatim out of the diff being reviewed. Then it's checked anyway, and thrown away if the check fails.
What was wrong
diff-size→ Build incremental diff since last review built the block withgit diff "${LAST_REVIEWED_SHA}...${HEAD_SHA}". With a merge commit at HEAD the merge base of those two commits isLAST_REVIEWED_SHA, so the range expands to the whole merge. Measured on one consumer repo: a round built a 9,800-line block against a 1,234-line reviewed diff, 119 of the block's 133 files outside the PR; two other PRs built ~1.16M-line blocks against 481- and 1,476-line reviewed diffs. Every leg's prompt grew from ~98 KB to ~826 KB, four of six legs timed out, and the two that finished reviewed base-branch files.What changed
.github/cursor-review/incremental-diff.py(new).build --old --new --outsplits both patches into per-file sections on^diff --gitand emits every NEW section whose hunk content differs from the same file's section in OLD, plus every NEW section for a file OLD lacks. A file OLD had and NEW doesn't contributes nothing — it's no longer in the PR.@@ -a,b +c,d @@headers are normalised to@@ @@before comparison, so a pure rebase (identical hunks, shifted line numbers) produces an empty block. Lines that track the base blob rather than the branch's own edit —index <blob>..<blob>, the similarity percentages — are excluded for the same reason: they move whenever the base blob does. The pre-hunk metadata that does not (old mode/new mode,rename from/rename to, typechange) is compared, so a round that only adds the executable bit to an already-edited script still shows up; and a binary section keeps itsindexline, because without--binarytheBinary files a/X and b/X differline is a constant andindexis the only content-dependent line git emits. Splitting is LF-only throughout —str.splitlinesalso breaks on\v,\f,\x85, U+2028 and friends, which would let a content line forge adiff --gitsection boundary.check --new --fullis the fail-safe, returning the foreign-section count and both line counts.The workflow step. OLD is
git -c core.quotePath=false diff "${BASE_SHA}...${LAST_REVIEWED_SHA}" -- . $DIFF_EXCLUDES— thequotePathflag matchingwriteReviewedDiff, which builds NEW, so a non-ASCII path keys the same on both sides. A build that fails, returns an EMPTY patch, or returns one over 32 MiB is declined rather than used. NEW ispr-diff.patchitself rather than a freshly computedBASE...HEADdiff — see the judgment call below. The helper is loaded from_pr_size_tool/, the pinned checkout of this repo the classifier is already built from, never the PR's own tree. After it runs, the step verifies the result and — if any section of the block is not carried byte for byte bypr-diff.patch, or the block has more lines thanpr-diff.patch— truncates it to empty and emits::warning::Incremental diff discarded: it was not a subset of the reviewed diff (<n> foreign file(s), <a> vs <b> lines).TheIncremental diff since round N (<sha>): <n> lineslog line is unchanged, so the sweep that sized this problem can be re-run against it.New
incremental_subsetoutput on theincrementalstep and on thediff-sizejob.truewhen the block passed the check or when there is no block at all (round 1, an unreachable last-reviewed SHA, an empty reviewed diff, an over-cap run where the step never ran);falseonly when a block that had been built was discarded. Written as!= 'false'so a skipped step reads astruerather than''.Docs. Two paragraphs in the ledger section of
.github/cursor-review/README.mdplus a scripts-table row, and two in the Gotchas ofdocs/callers/cursor-review.md— both state that the block is derived from the two PR patches and can never carry a hunk the PR doesn't, and both quote the fail-safe warning verbatim and say what a caller seeing it should conclude (the panel ran on the full diff; nothing was skipped or suppressed).Judgment calls
NEW is
pr-diff.patch, not a freshgit diff BASE...HEAD. The plan I worked from specified the latter.pr-diff.patchis already exactlygit diff mergeBase...head -- . <excludes>minus the generated-file sections the classifier stripped (writeReviewedDiff,scripts/check-pr-size/main.go:166), and using it buys two things a fresh diff doesn't. The subset property becomes structural — every emitted section is a verbatim byte-slice of the file the fail-safe compares against — instead of merely checked after the fact. And a generated file touched since the last round can't reappear in the block: with a freshBASE...HEADdiff it would, it isn't inpr-diff.patch, the fail-safe would count it foreign, and the entire block would be discarded over a lockfile. The fail-safe is kept regardless, as defence in depth.The two patches can resolve to different merge bases. OLD resolves to
merge-base(BASE, LAST), NEW tomerge-base(BASE, HEAD), and a round that merged the base branch in moves the second one forward. A file both the base branch and the PR touched then reads as changed even if the author's own edit didn't move. That is over-inclusion strictly within the PR's own files — it costs a little prompt budget and can't break the subset property. Pinning both to one merge base would hide a hunk whose surrounding code really did change under it, which is worse. Noted in the step comment.Every "couldn't build one" path leaves the block empty and
incremental_subsettrue. Nothing was discarded in those cases; there was simply nothing to prioritize, and the panel behaves as it does on round 1. Reportingfalsethere would make the integrity signal fire on a non-event. A helper crash duringbuildis the one in-between case: it logs its own::warning::, empties the block and leaves the outputtrue, because again no block was discarded — one was never produced.No
Skip-caller-bump: truetrailer. This is a behavioural change inside a watched surface; the fleet should bump.Verification
Reproduced against real git rather than fixtures.
TestMergeCommitHeadbuilds an actual repository — base branch advances by 400 lines plus a new file, the PR branch merges it in and edits its own file — and pins four things: the old formulation really does dragunrelated.pyin (424 lines vs a 10-line reviewed diff), the new one emits onlyapp.py, the result passes the subset check, and the old formulation would have failed it. I also extracted the step'srun:block straight out ofcursor-review.ymland ran it verbatim against that repository for every branch: merge-commit head (10-line block,app.pyonly,incremental_subset=true), a realgit rebaseonto the advanced base (empty block), round 1 /LAST == HEAD/ unreachable LAST / empty reviewed diff (all skip cleanly),diff_excludesword-splitting through the new wrapper function, a missing helper, and a deliberately sabotaged builder that re-emitted the old commit-range diff — which produced exactly::warning::Incremental diff discarded: it was not a subset of the reviewed diff (2 foreign file(s), 424 vs 10 lines)., truncated the block to 0 lines, and appendedincremental_subset=false.python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py'(the commandtest-cursor-review-scripts.ymlruns, and itstest_*.pyglob picks the new file up): 636 tests, OK — 36 of them new.shellcheck -x .github/cursor-review/install-cursor-cli.sh .github/cursor-review/slack-notify.sh: clean. The new step'srun:block, extracted and shellchecked separately: clean — theelifchain is whybuild_old_patchis a function, since a# shellcheck disable=directive is rejected in front of anelifbranch.python3 .github/workflow-pins/check_workflow_pins.py: OK, 11 workflows, 155 pinned refs..github/workflow-pins/tests: 499 tests, OK.python3 .github/agents-md-integrity/check_agents_md.py --root .: passed (2 pre-existing warnings — AGENTS.md at 156 lines and no CODEOWNERS; neither touched here).INCREMENTAL_PYat the PR's own checkout makestest_the_helper_is_loaded_from_the_pinned_checkout_not_the_prfail as intended.contextlib,importlib,io,os,shutil,subprocess,tempfile,unittest), matching the repo rule and the sibling workflow suites' explicit note that ayamlimport would not run in this job.Residual
The
pytestinvocation named in the plan was not run. The plan's verification step ispython3 -m pytest .github/cursor-review/tests/; this host has neitherpytestnorpip, so I ran theunittest discovercommandtest-cursor-review-scripts.ymlactually uses — which AGENTS.md names as the authority, and which is what CI will run. Every new test is a plainunittest.TestCase, so pytest would collect them identically, but I did not empirically confirm that.The live end-to-end reproduction on a test PR was not performed. The plan asks for it: open a PR here, trigger the review label, push a merge of
maininto the branch, re-trigger, and read thediff-sizelog. That needs two labelled review rounds against a real panel (billedcursor-reviewruns), so it is out of reach of a non-mutating check from this session. What I substituted is described under Verification: the step's ownrun:block, extracted verbatim fromcursor-review.yml, executed against a real repository containing a real merge commit, for each of its branches. That exercises the same shell but not the surrounding job — in particular it does not prove that_pr_size_tool/is still populated at this point in a real run (a drift guard now asserts the path matches a declared checkoutpath:, which is the static half of that claim), nor thatneeds.ledger.outputs.last_reviewed_shacarries what this step assumes on a live PR. This PR itself is the natural place to confirm it: it is labelledcursor-review, so pushing a merge ofmaininto this branch and re-triggering the label would produce exactly the log the plan asks for.The evidence this change is built on was read, not re-measured. The 9,800-line / ~1.16M-line / ~826 KB figures, the 119-of-133 foreign-file count and the four-of-six-legs-timed-out result come from a prior read-only investigation on a private consumer repository. I have no access to that repository or to those runs, so I could not re-derive any of those numbers; they are quoted in the step comment and the docs as measurements, and this PR does not independently corroborate them. The mechanism they describe is independently reproduced here in
TestMergeCommitHead, which is the part the fix actually depends on.Not swept: how many consumer PRs were affected. The investigation this came from sized the problem on specific PRs; I did not and could not sweep the rest of the fleet's review history to count how many rounds shipped a non-subset block, because those runs and their logs are on repositories this session cannot read. Anyone with that access can now count them directly — the
incremental_subsetoutput and the discard warning make it observable going forward, but neither is retroactive.A consumer already reading
diff-sizejob outputs sees a new one.incremental_subsetis additive, so no existing caller breaks, but note that when the wholediff-sizejob is skipped (gate says don't run) the output is'', not'true'— the!= 'false'default only covers the skipped-step case within a running job. The sibling ticket that turns this into a check-run conclusion should treat''as "no panel ran" rather than as a verdict.Two pre-existing
agents-md-integritywarnings remain unfixed and are unrelated to this change:AGENTS.mdis 156 lines against an aspirational 150 (hard ceiling 200, so it passes), and the repo has no CODEOWNERS file, soAGENTS.mdhas no DRI. I deliberately did not add toAGENTS.mdgiven the first.Review round
The panel raised nine findings. Eight are fixed in
a2da829; each was reproduced before it was fixed, and each has a regression test.! build_old_patchonly caught a non-zero exit.BASE...LASTcomes back empty — with no error — whenevermerge-base(BASE, LAST)isLASTitself (the base advanced past the last-reviewed commit, e.g. a retarget) or$DIFF_EXCLUDESfilters everything out, and the block then became a byte-for-byte copy of the whole reviewed diff: the doubled prompt the guard's own comment says it prevents. The fail-safe could not catch it either — nothing foreign, andnew_linesequalsfull_linesrather than exceeding it. Guard is now! build_old_patch || [ ! -s "$OLD_PATCH" ].core.quotePath. NEW comes from check-pr-size's-c core.quotePath=falsediff; OLD used the default, so a non-ASCII path arrived C-quoted on one side and plain on the other and the two could never key alike — re-emitting the file in full every round, including a pure rebase. The suite's own_diffhelper already passed the flag, which is why nothing caught it.+xto an already-edited script compared equal and vanished from the block.str.splitlineslet the diff forge its own section boundaries.+x\x0cdiff --git a/lib/auth.py b/lib/auth.pyproduced two sections, andchecklearned the forged path from the same bad split, so it did not count as foreign — the guard disarming itself. All splitting is LF-only now.indexis right for a text file and wrong for a binary, whose other lines are constant. The test that covered this pitted aBinary files … differstanza against aGIT binary patchone — two formats onegit diffinvocation never mixes — so it passed while the gap was open.checknow requires every section to appear byte for byte in the reviewed diff, matched as a multiset. Both docs now describe it as the byte test it is.a/x b/c b/dreads two ways;candidates[0]took('x', 'c b/d'), which no later round's plaindiff --git a/d b/dcould match. A newsection_pathsprefers the section's ownrename from/rename tolines — one path per line, so unambiguous. (The finding's first example,a/old name.py b/new name.py, in fact parses correctly today: the split needs a literalb/.)diff_size_cap, butBASE...LASTkeeps the generated-file sections the classifier strips out of the reviewed diff, and those cost nothing against that cap. Now declined over 32 MiB, on the same fail-closed path as every other "can't build one" branch.Deferred, with a follow-up filed:
BASE_SHAis the current base, not the base the previous round diffed against. After a retarget,BASE...LAST_REVIEWEDcan carry hunks the previous reviewed diff never showed, and this round then suppresses them — under-inclusion, the one direction the "different merge bases" judgment call above does not accept. It cannot be fixed here: the ledger recordslast_reviewed_shaand no base SHA, so fixing it means a per-round base SHA inbuild-ledger.pyand in the ledger's rendered-and-reparsed comment format, plus a compatibility path for entries already live on open PRs. Blast radius is bounded — the block is only a prioritization hint and the panel always sees the full reviewed diff beside it, so no hunk goes unreviewed.Provenance
python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py': 657 passed, 0 failed (57 intest_incremental_diff.py, 21 added this round);.github/workflow-pins/tests: 499 passed, 0 failed; groom 405 / public-repo-hygiene 204 / linear-ticket 116 / refresh-reviewers 56 / agents-md-integrity 46 passed;python3 .github/workflow-pins/check_workflow_pins.py: OK (11 workflows, 155 pinned refs);python3 .github/agents-md-integrity/check_agents_md.py --root .: passed, 2 pre-existing warnings;shellcheck -xon the two cursor-review shell scripts and on the step's extractedrun:block: clean, with its four guard branches (failed / empty / oversized / usable OLD) exercised directly; every one of the eight fixed findings reproduced against the pre-fix code before being fixedpr-diff.patchrather than a freshgit diff BASE...HEAD(rationale under Judgment calls); on the fail-safe finding I took byte-exact section matching rather than the narrower "require the NEW-side path to be carried, relaxing for a rename pair" the reviewer proposed — it subsumes that case and needs no rename special-casing; the base-drift finding is deferred to a follow-up rather than fixed (see Review round);pytestand the live two-round reproduction on a test PR were still not run (both under Residual)🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.