mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #274] [MERGED] fix(cursor-review): budget the success path's demoted-findings sentinel so the clamp never cuts inside it #277
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#277
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/274
Author: @mattmillerai
Created: 9/8/2026
Status: ✅ Merged
Merged: 9/9/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-12535-success-path-sentinel-budget📝 Commits (6)
7b5579bfix(cursor-review): budget the success path's demoted-findings sentinel (BE-12535)4787286fix(cursor-review): make a budget-truncated sentinel disclose its own loss (BE-12535)2d9eac1fix(cursor-review): neutralize planted comment openers and stop the ceiling overcharging (BE-12535)b570aabMerge remote-tracking branch 'origin/main' into matt/be-12535-success-path-sentinel-budgetb1fdb78test(cursor-review): pin the companion's line anchor at the reader, not via the writer's defang (BE-12535)bc133eefix(cursor-review): spell the defang's zero-width space as an escape (BE-12535)📊 Changes
4 files changed (+944 additions, -55 deletions)
View changed files
📝
.github/cursor-review/build-ledger.py(+66 -3)📝
.github/cursor-review/post-review.py(+320 -50)📝
.github/cursor-review/tests/test_build_ledger.py(+98 -0)📝
.github/cursor-review/tests/test_post_review.py(+460 -2)📄 Description
ELI-5
When a review round has too many findings to anchor to lines in the diff,
post-review.pylists them in the review body and hides a machine-readable copy of them in an HTML comment (the "sentinel") so the next round's ledger can read them back. That sentinel is nearly as long as the prose it duplicates, so it competes with the findings a human actually sees — and on the success path it was allowed to take the whole body. Past a certain number of findings it overran the size limit, GitHub's clamp cut it mid-JSON, the unterminated-comment backstop rewound it to its opener and swallowed every finding below it, and the review posted a 420-character header with nothing in it. The 422 fallback path already had a guard for exactly this; this gives the success path the same one.What changed
FALLBACK_SENTINEL_MAX_CHARS→SENTINEL_MAX_CHARS(same value,MAX_REVIEW_BODY_CHARS // 2), now documented as the prose floor on both finding-carrying paths rather than the fallback's alone.git grepfound no reader outside this repo, so no back-compat alias was added; the one stale mention was a test docstring, updated in place.render_body_only_findings(items, budget=None).budgetis how many characters the whole section may occupy before the clamp's cut point.Nonekeeps today's behaviour byte-identical (the round-trip tests intest_post_review.py/test_build_ledger.pyall call it unbudgeted and pin that). With a budget it first checks whether the whole section fits — if it does, nothing is charged, because the clamp will not cut it — and otherwise handssentinel_share(available, len(prose))to the existingfit_sentinel_items, keeping the most-urgent prefix that fits — or dropping the sentinel entirely and posting the marker alone. Both outcomes print the same kind of stderr disclosure the fallback path already prints. The prose loop still renders every item either way: the budget governs what the ledger recovers, never what a reader is shown.main()passesbudget=MAX_REVIEW_BODY_CHARS - len(CLAMP_TRUNCATION_NOTE) - len(review_head) - len(FINDINGS_SEPARATOR), computed wherereview_headhas actually been measured. The posted body isreview_head + FINDINGS_SEPARATOR + marker + sentinel + "\n\n" + proseand the clamp cuts atlimit - len(note), so the sentinel's closing-->now always sits ahead of the cut.drop_unterminated_commentis scoped to CommonMark's HTML-block start condition, and its docstring no longer claims a render-time budget "cannot promise this" — both paths now derive one from the measured head, so it is reworded as the backstop for any HTML comment in any posted body (a miscounted head, a comment some future path adds) rather than as the primary guard..github/cursor-review/README.mdneeded no change:grep -ni "sentinel\|size guard\|body-only"returns nothing there, so the size guard is not described at that level.Evidence
Reproduced on the base commit through the repo's stubbed-
ghharness — one anchorable finding plus n demoted findings of ~700 chars, reading the success-path payload (posted[0]):unrecovered_rounds=1)Then swept the shape space rather than only the shapes being fixed: 42 combinations of body length (100–3,000 chars) × finding count (20–200), before vs after. 19 unchanged, 23 changed, 0 regressed on either ledger entries or visible findings. Ten of the changed cells were the total collapse above (0 entries, 0 visible, a 422-char body), now carrying 45–73 ledger entries and 10–68 visible findings; the other 13 keep the same entry count and show a reader more findings (e.g. 700-char bodies at n=80: 45 entries either way, visible findings 10 → 41). The only shapes where the new cap could cost ledger entries are ones where the sentinel exceeds half the limit while the prose does not, and none exist in the swept range because a sentinel entry is always shorter than the prose entry it duplicates.
Edge cases checked by hand beyond the suite: a review head larger than the whole limit drives
budgetnegative —fit_sentinel_itemsalready guardsbudget <= 0, so it degrades to the marker-alone branch and the clamp takes it from there, with no traceback.This change denies no capability and adds no dead-end path: at every measured shape the new code posts strictly more recoverable findings and strictly more visible findings than the old code, never fewer. The two new stderr messages are degradation disclosures on a path that previously degraded silently.
Tests
Five added next to the fallback's equivalents in
BodyBudgetTest: the success-path prose floor across n ∈ {60, 90, 91, 100, 130}; the clamp-boundary sweep across a paddedpath; the most-urgent-prefix assertion (reading the sentinel payload, not the ledger —build_ledgerre-sorts its entries, so the write order the test is about only survives in the payload);budget=Noneequivalence; and the marker-alone floor. Every existing test named in the plan stays green unchanged, includingtest_the_sentinel_survives_the_clamp_that_cuts_the_prose,test_the_marker_still_outlives_a_cut_that_takes_the_whole_sentinel,test_the_sentinel_follows_the_marker_precedes_the_findings_and_round_trips, and the fallback prose-floor / reserve tests.Mutation check as specified: removing
budget=from themain()call fails all 11 subtests of the two new success-path tests (including every n ≥ 91) plus the prefix test, then restored to green.Judgment calls
FALLBACK_SENTINEL_MAX_CHARSalias.git grepacross the repo found the old name only in one test docstring; nothing outside this repo importspost-review.py(the scripts are loaded at run time by workflows in this repo, never vendored), so an alias would be dead code. Renaming it outright is the reversible choice.budget: int | Noneis the file's first PEP 604 union. Valid on 3.10+; CI pins 3.12 andcursor-review.ymlruns onubuntu-latest's systempython3with nosetup-python, so there is no interpreter here older than that.budget=Nonedefault rather than making the argument required. Three test modules and the round-trip suite call this function unbudgeted and pin its exact output; a required argument would have forced churn in tests that are deliberately pinning the unbudgeted render.Residual
review_headitself on either path. Past ~59,660 characters of head the clamp cuts inside the head, and the whole findings section (marker, sentinel, companion and prose alike) lands past the cut no matter what this code emits. Not reachable from today's inputs: every head contributor is a short workflow-generated string, and it took a synthetic 54,000-character model name to reach that regime in the new sweep test. Worth a ticket only if a caller ever ships a head that large.repeat_ofkey to the sentinel payload. It does not conflict —fit_sentinel_itemsmeasures the real render, so the budget stays correct whichever lands first — but if that merges first this branch should be rebased rather than re-derived, since the payload's per-entry length changes.mainregardless.mainat52b4fe6and reported above. Thecursor-review.ymljudge-failed raw-panel branch — the only route that produces an uncapped finding union in production — was exercised only throughpost-review.py's own harness, not by running the workflow end to end, which would require a live PR and the review panel's model spend.Review round (
4787286)The panel raised five findings on
7b5579b. Four were valid and are fixed; the fifth is answered on its thread. Every fix is pinned by a test verified to fail against the pre-fix code and pass after.build-ledger.pyread 12-of-89 as a complete recovery — strictly worse than the all-or-nothing rule the budget replaced, which degraded loudly because a dropped sentinel does not parse. The writer now emits<!-- cursor-review:body-only-truncated v1 kept=N total=M -->on a real cut, and_body_only_entriesreturns entries anddegradedindependently, so a truncated round both contributes what it recovered and discloses what it did not. A second comment rather than a payload key, because the reader pins the sentinel to a single-spaced opener directly below the prose marker: anything inserted between them breaks the recovery it annotates, and an older pinned reader ignores an unknown trailing comment instead of failing to parse. Applied on both budgeted paths, defanged in quoted text alongside the other two literals, and the suffix is deliberately unpinned so an unexpected count shape still discloses.min(SENTINEL_MAX_CHARS, available)bought the ceiling only while the first term won; past a head of roughly half the limit theminstopped binding. Confirmed by reverting the one expression: the new sweep fails at head shares 0.5/0.6/0.75/0.9 with one visible finding. Nowsentinel_share(available, prose_len) = min(SENTINEL_MAX_CHARS, max(available - prose_len, available // 2))— one step past the reviewer's flat half, so a big-head round with small prose is not charged a floor nothing was going to use. Line 1681 got the same treatment, as flagged.SENTINEL_MAX_CHARSon-→\u002dalone while the whole body stays well under the limit. The 422 fallback gets the same skip.drop_unterminated_commentrewound too far (1/6). Scoped the rewind to CommonMark's HTML-block start condition,(?:\A|(?<=\n)|(?<=\r)) {0,3}<!--(bare\rexplicitly, since cmark-gfm ends a line on one andre.MULTILINEdoes not). The reasoning behind this one was wrong and round 2 overturned it — I argued a<!--inside a finding's blockquote could not swallow anything because the>prefix indents it out of the start condition. cmark-gfm strips the blockquote marker before parsing the contents, so it does. Superseded below; the note is kept rather than edited away because the round-2 fix only makes sense against it.budgetto fall underlen(marker),review_headmust exceed ~59,660 of 60,000 characters, at which point the clamp is already cutting inside the head and every byte of this section is past the cut regardless. Only bounding the head would help, and the head is not this function to bound. Recorded as an in-code concern rather than a follow-up ticket, since it is not reachable under current code.Review round 2 (
b1fdb78)Five more findings on
4787286. All five were valid; all five are fixed, plus one bug the fuzz written for the third turned up. Each fix is pinned by a test verified to fail against the pre-fix code and pass after.Two of them turned on how GitHub actually renders a body, so I stopped reasoning about CommonMark and measured it —
POST /markdownwithmode: gfm, which is the same renderer the PR page uses.<!--DOES swallow the findings below it (1/6, a re-raise). My round-1 answer was wrong.> <!-- openerreturns a document ending at that finding: everything below it, including the clamp's own truncation note, is gone. The blockquote marker is stripped before its contents are parsed, so> <!--opens a type-2 HTML block exactly as a column-0 opener does. Re-terminating with a column-0-->does not help either — that one is escaped to-->and closes nothing — so the opener has to die at the writer.render_finding_entrynow neutralizes every<!--in a rendered entry with a zero-width space (DEFANGED_COMMENT_OPENER), the same trick and house style asdefang_body_only_contract; the defanged form renders as<!--and swallows nothing, and mid-line openers were already escaped by cmark-gfm, so nothing a human reads changes.defang_body_only_contractis called only frompost_error_review, which_body_only_entriesrefuses outright, so the writer-side defang gave this line zero coverage on the success and 422-fallback bodies where it is actually read. Unanchored, the literal was plantable from the PR under review and flipped a fully recovered round todegraded, fabricating anunrecovered_roundsentry and a "may repeat" warning in the next prompt._BODY_ONLY_TRUNCATED_REis now line-anchored with the same\r-aware lookbehind_ERROR_REVIEW_REuses, and_body_only_truncatedadditionally requires it to sit below the parsed sentinel — taking the reviewer's "and ideally". Both are properties of what the writer emits byte-for-byte, so neither can turn a disclosed loss back into a silent one.<!--inside a fence is not an opener (2/6). Confirmed against the same endpoint: prose after the fence and the clamp note both survive it. It lands on the error path, where the fenced judge/CLI text is unbounded, so the over-deletion would have been large.html_block_openersnow walks the cut a line at a time tracking fence state and returns only openers outside a fence, with character- and length-correct fence matching. On the parenthetical: a fence the cut left open is now detected but deliberately still not closed — closing it means appending characters past a cut point whose slack is already spent onCLAMP_TRUNCATION_NOTE, trading a cosmetic problem (the note renders as a last line of code rather than prose) for a 422.budgethas already subtractedCLAMP_TRUNCATION_NOTE, whileclamp_review_bodyleaves any body up toMAX_REVIEW_BODY_CHARSuntouched, so a section in the ~140-char window between them was truncated and flagged degraded with no clamp behind it. Nowlen(whole) <= budget + len(CLAMP_TRUNCATION_NOTE), which is exactly the fallback'swhole_fallback_len <= MAX_REVIEW_BODY_CHARS. Checked at the edge: sized solen(whole)equalsbudget + len(note)to the character, the section is posted whole and the body lands on 60,000 exactly, so the widened check cannot itself produce a 422.available // 2alone already guarantees the prosemin(prose_len, ceil(available/2))— everything it wants, up to half — at every head size, so capping the wholemaxonly ever reserved space nothing would use.SENTINEL_MAX_CHARSnow bounds the floor, not the leftover:max(available - prose_len, min(SENTINEL_MAX_CHARS, available // 2)). Their numbers reproduce —sentinel_share(59_000, 17_000)was 30,000 and is now 42,000, roughly a hundred ledger entries recovered, with the prose still getting all 17,000 it asked for.-->, so a later<!--is already inside the earlier comment and is not an opener at all. It now rewinds to the first unterminated opener.Round 1's fifth finding (the marker-fits check) stands answered on its thread and is unchanged: it is still unreachable without a ~59,660-character head, at which point the clamp is cutting inside the head and this section is past the cut whatever it emits.
Provenance
b1fdb78(which mergesorigin/mainat1d37fe8into the branch — see below):python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py': 484 passed, 0 failed;.github/groom/tests: 374 passed, 1 skipped;.github/agents-md-integrity/tests: 46 passed;shellcheck -x .github/bump-callers/bump-callers.sh .github/bump-callers/tests/test_bump_callers.sh: clean;check_agents_md.py --root .: passed with the same 2 pre-existing warnings (AGENTS.md 155 lines vs the 150 target, no CODEOWNERS), neither touched here. Falsification: each of the six round-2 fixes was individually reverted and the test pinning it confirmed to fail, then restored to green. That pass is whyb1fdb78exists — reverting the companion's line anchor left every test green, because the end-to-end fixture reached the reader throughrender_finding_entryand so was covered by the new writer-side defang rather than by the anchor it claimed to pin. The reader-side control is now pinned directly, which is the half that covers consumer repos still on older SHAs. Two rendering claims were checked empirically againstPOST /markdown(mode: gfm) rather than argued from the spec, anddrop_unterminated_commentwas fuzzed over 20,000 mixed fence/blockquote/terminated/dangling bodies: no dangling opener survives it and it never grows the cut. No workflow or shell files were edited, so the pin-validation surface is unchanged.origin/main(1d37fe8), which had moved substantially under this PR in the same two files. It conflicted textually inbuild-ledger.pyandtest_post_review.py— both pure adjacency (each side inserted a new function or class at the same point), resolved by keeping both sides, with_body_only_entriestaking main's newerresolve_lineage=signature. The merged tree runs 484 tests green, so the merge-queue result is exercised rather than assumed. Otherwise as planned, with one deliberate step past a reviewer's suggestion carried over from round 1 (the prose floor ismax(available - prose_len, …)rather than a flat half). One round-1 finding remains answered rather than fixed, with the reasoning on its thread; the round-1 rationale that round 2 overturned is marked as such above rather than quietly rewritten.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.