Skip to content

feat: exclude generated files from the review diff by default - #88

Open
aliasunder wants to merge 5 commits into
mainfrom
worktree-diff-exclude-paths
Open

feat: exclude generated files from the review diff by default#88
aliasunder wants to merge 5 commits into
mainfrom
worktree-diff-exclude-paths

Conversation

@aliasunder

@aliasunder aliasunder commented Sep 6, 2026

Copy link
Copy Markdown
Owner

What

A PR dominated by regenerated artifacts (test snapshots, lockfiles, minified bundles) could blow the diff token budget and skip the entire review — the hand-written changes got nothing. And even under budget, generated hunks waste model attention and cost on every run.

This adds diff-level exclusion with three tiers:

  • diff_exclude_paths input — folder prefixes or globs removed from the parsed diff before annotation and the budget check. Supplied patterns extend a built-in default list of generated artifacts (ecosystem lockfiles, *.min.js, *.min.css, *.map — the classes GitHub's linguist auto-collapses via built-in rules no .gitattributes can express). A leading none drops the defaults: none alone disables exclusion, none, evals/** replaces the list. Empty means the default list (documented divergence from exclude_paths, where empty means no exclusions), so bare repo-variable wiring works.
  • respect_linguist_generated input (default on) — changed files the repo's root .gitattributes marks linguist-generated=true are excluded too; negated entries (-linguist-generated) keep a file reviewable even when the default list matches it. Explicitly supplied diff_exclude_paths patterns always win. Rules are read from the PR head deliberately: a PR that introduces the .gitattributes marks reviews under its own rules.
  • Snapshots are deliberately NOT in the default list — linguist has no built-in snapshot rule and GitHub renders .snap/__snapshots__ diffs expanded, so collapsing them is a repo-level decision (.gitattributes) or workflow-level one (the input), never ours.

Behavior

  • Excluded files leave the review subject entirely: no diff budget, no changed-file reads, no import-trace seeds, no commentable lines — and the related-file and doc scans skip them, so their content cannot re-enter the prompt as a traced importer or mention-matched doc. A snapshot-heavy PR that used to skip now reviews its hand-written files.
  • Visibility, not disappearance: excluded files are named — with change counts and their exclusion source — in a trailer after the annotated diff (so the model knows they changed), in the status comment's context notes, and in the run log. Excluded ≠ vetted.
  • The trailer's per-file lines deliberately do not resemble === path === diff headers; the anchoring contract already routes findings about un-rendered files onto a provided file, and a finding naming an excluded file is dropped by the unknown-file filter (tested).
  • When every changed file matches, the run posts a skip review (existing neutral check conclusion) whose reason counts exclusions per source — all N changed file(s) excluded from review (2 by default exclusion, 1 by linguist-generated) — and whose body lists every excluded file with its source, so the cause is never misattributed to an input the operator didn't set.
  • Backtracking guard on both channels: glob engines backtrack exponentially on multi-star segments (measured in whole minutes against crafted 40+-char filenames). Operator patterns with more than 2 * in one segment are rejected at config parse; a violating .gitattributes rule — untrusted, PR-author-controlled input — is dropped with a warn and never fails the run.

Implementation notes

  • New pure modules in src/diff/: exclude-diff-files.ts (partition + trailer renderer), gitattributes.ts (linguist-rule parser + evaluator), pattern-safety.ts (the shared wildcard cap). context/workspace.ts gains readGitAttributes() (root file only — documented v1 boundary; symlink-escape and read failures degrade to no rules with a warn).
  • Input patterns match as root-anchored folder prefixes (the exclude_paths rule) or globs via node:path matchesGlob — union, no new dependency. Gitattributes patterns use gitignore semantics (slash-less patterns match basenames at any depth) via the ignore package (new runtime dependency, pinned to the probed 5.x major), instantiated per pattern — load-bearing: a shared instance would activate gitignore !-negation semantics the gitattributes format forbids. Directory-style entries (dir/, bare dir) over-approximate toward exclusion relative to the gitattributes spec — ruled deliberate, pinned in tests.
  • Files are judged by their new path (deletions by the old path): a rename out of an excluded folder into reviewable source is reviewed; a rename into one is excluded.

Testing

  • 676 tests pass (was 624 on main + release); lint, build, and Prettier clean.
  • New coverage: config tier semantics (none positions, extend-not-replace, wildcard-cap rejection), gitattributes parser/evaluator (all four attribute spellings, last-match-wins, cap-violation warn-drop, forbidden ! patterns, escaped spaces), partition precedence (negation exempts defaults, operator beats negation, rename directions, deletions, root-level files against the shipped **/ defaults), trailer full-string format, exclusion-source summaries, and orchestrate wiring (all-excluded skip with per-source attribution and file list, budget pass with oversized files excluded, context note, unknown-file drop of a finding naming an excluded file, diff-excluded paths forwarded to the related-file/doc scans, respect_linguist_generated: false short-circuit).
  • Mutation-verified: swapping the precedence order, dropping the trailer, disabling the related-scan exclusion, and hard-coding the skip attribution each fail exactly the tests that pin them.

🤖 Generated with Claude Code

New diff_exclude_paths input: folder prefixes or globs removed from the
review diff before the token budget check, so one oversized generated
artifact no longer skips the whole review. Supplied patterns extend a
built-in default list (ecosystem lockfiles, minified sources, source
maps); a leading "none" drops the defaults. Patterns over 2 stars per
segment are rejected — glob matching backtracks exponentially.

New respect_linguist_generated input (default on): files the repo's root
.gitattributes marks linguist-generated=true are excluded too; negated
entries keep a default-list match reviewable. Excluded files stay
visible — named with their source in the annotated diff trailer, the
status comment's context notes, and the run log. When every changed file
matches, the run posts a skip review naming the mechanism.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/orchestrate.ts
Comment thread src/orchestrate.ts Outdated
Comment thread src/config.ts
@umm-actually

umm-actually Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

umm-actually re-reviewed at 9d873a0

3 new finding(s) posted (6 tracked finding(s) across all runs).

Context notes
  • Priority docs already in context: README.md
  • 1 changed file(s) excluded from review: package-lock.json (default exclusion)

umm-actually · deepseek/deepseek-v4-flash-0731

- Diff-excluded changed files are excluded from the related-file and
  related-doc scans so their content cannot re-enter the prompt after
  the partition removed them from the review subject
- The all-excluded skip attributes each exclusion to the layer that
  actually excluded it and lists every excluded file in the skip body
- Port the Prettier CHANGELOG.md fix from main (the release flow
  committed an unformatted entry, failing the checks job on every PR)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/diff/exclude-diff-files.ts
matchesGlob's globstar matches zero directories, so the shipped
**/-prefixed defaults catch the standard npm layout's root lockfile —
pinned so a future matcher swap cannot regress it silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The release flow's update-changelog.sh writes it directly to main
without a format pass, so checking it fails every PR's checks job
after a release until someone hand-formats a generated file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm's own output format is the source of truth for the lockfile;
formatting it invites churn against what the tool regenerates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@umm-actually

umm-actually Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Name the real exclusion sources and files in the all-excluded skip
Medium severity · correctness · high confidence

src/config.ts:214 — beyond the diff's line ranges, in code the changes touch or depend on.

The all-excluded skip path hard-codes 'diff_exclude_paths' as the exclusion source, so a PR touching only package-lock.json (matched by the built-in default list or a repo .gitattributes linguist rule) is reported as caused by an input the operator never configured. On this path no trailer and no status comment are built, so the excluded file and its true source appear nowhere user-visible, contradicting the README/action.yml claim that every exclusion is named.

Failure scenario: A repo with default inputs receives a PR changing only root-level package-lock.json: the run posts 'Skipped — all 1 changed files match diff_exclude_paths' in the skip review and check title, although no diff_exclude_paths pattern was configured (the file matched the built-in default list); the file itself is named nowhere in the review output.

Suggested fix
Build the skip reason (and a per-file list) from the actual excludedDiffFiles results, naming each file and its exclusion source (e.g. 'all 1 changed file(s) excluded from review: src/package-lock.json (default exclusion)') instead of hard-coding 'match diff_exclude_paths'; if the reason string must stay generic, append the per-file source list so the skip body matches the documented visibility promise.

umm-actually · deepseek/deepseek-v4-flash-0731

Comment thread src/diff/exclude-diff-files.ts
@umm-actually

umm-actually Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Reject malformed globs in diff_exclude_paths at config parse
Low severity · correctness · high confidence

src/config.ts:312 — beyond the diff's line ranges, in code the changes touch or depend on.

Config validation for diff_exclude_paths checks only the per-segment wildcard count, so a malformed glob such as '[' passes parse and throws ERR_INVALID_GLOB_PATTERN at runtime inside matchesGlob, failing every review until the config is fixed instead of rejecting the input at startup like other config errors.

Failure scenario: An operator sets diff_exclude_paths: '[' (comma-split yields one pattern '['). Config parsing succeeds (0 stars), and the next PR review crashes mid-pipeline in matchesExcludePattern with an invalid-glob exception, failing the check run and the action instead of rejecting the input at boot.

Suggested fix
After the wildcard-cap check in diffExcludePathsInput, probe each pattern with posix.matchesGlob('probe', pattern) inside a try/catch and ctx.addIssue(...) on throw, so malformed globs fail at config parse like the existing wildcard-cap violations.

umm-actually · deepseek/deepseek-v4-flash-0731

Copy link
Copy Markdown
Owner Author

The two beyond-diff comments from the 9d873a0 run are re-posts of findings already dispositioned on earlier heads, re-anchored outside the content-dedup window (one cross-file, one at src/config.ts:312 — a line the ~200-line file doesn't have):

  • "Name the real exclusion sources and files in the all-excluded skip" — fixed in 268a239: the skip reason is built from the partition result with per-source counts, and the skip body lists every excluded file with its source. The claim that the source is hard-coded is false on this head.
  • "Reject malformed globs in diff_exclude_paths at config parse" — rebutted on the original thread with runtime evidence: on Node v24.20.0 (node:24-slim), posix.matchesGlob never throws on malformed patterns ([, unclosed classes/braces, [z-a], trailing \); minimatch parses them as literals, so the crash path doesn't exist. No change.

Generated by Claude 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