Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Written by the release flow's update-changelog.sh, not by hand — its
# formatting is the generator's business, and checking it turns every PR
# red when a release commit lands unformatted on main
CHANGELOG.md
# npm's output format is the source of truth; reformatting a lockfile
# invites churn against what the tool regenerates
package-lock.json
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ src/
logger.ts # structured JSON logger — levels, child contexts, lazy props
github/ # GitHub I/O: event payload → PrContext, octokit wrappers (diff fetch, review posting)
openrouter/ # OpenRouter I/O: @openrouter/sdk wrapper, per-attempt deadline, structured-output retry ladder, cost summary
diff/ # pure transforms over parse-diff output
context/ # workspace I/O: conventions file, changed files, import-trace scan, doc-mention scan, priority docs
diff/ # pure transforms over parse-diff output + diff-level exclusion (patterns, gitattributes linguist rules, wildcard safety cap)
context/ # workspace I/O: conventions file, root .gitattributes, changed files, import-trace scan, doc-mention scan, priority docs
review/ # pure review logic: finding schema, phases + stage dispatch, prompt, non-finding filter, unknown-file filter, cross-phase merge, path normalization, selection, comment mapping, title similarity, context notes, summary
orchestrate.ts # pipeline + createPromptedGenerateFindings — fully testable with stub clients
```
Expand Down
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Changelog


## [0.4.0] — 2026-09-05

### Features
Expand Down
44 changes: 23 additions & 21 deletions README.md

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,36 @@ inputs:
__snapshots__
required: false
default: ""
diff_exclude_paths:
description: >-
Comma-separated folder prefixes or globs removed from the review diff
before the token budget check — excluded files are listed by name in
the review output but their content is not reviewed (excluded is not
vetted). 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); a leading
"none" drops the defaults, so "none" alone disables the built-in
list (linguist-generated exclusions are governed separately by
respect_linguist_generated) and "none, evals/**" replaces the list
outright. Empty = the default
list (unlike exclude_paths, where empty means no exclusions), so
workflows can wire an unset repo variable directly. Patterns with
more than 2 "*" in one path segment are rejected ("**" segments are
exempt) — glob matching backtracks exponentially on such shapes.
Example: none, **/__snapshots__/**
required: false
default: ""
respect_linguist_generated:
description: >-
Also exclude changed files the repo's root .gitattributes marks
linguist-generated=true (nested .gitattributes files are not read).
Negated entries (-linguist-generated) keep a file reviewable even
when the default diff_exclude_paths list matches it; explicitly
supplied diff_exclude_paths patterns always win. Rules are read from
the PR head, so a PR changing .gitattributes reviews under its own
rules — exclusions are always named in the review output
required: false
default: "true"
cost_summary:
description: Write a per-run cost report (model, prompt/completion tokens, USD) to the workflow step summary
required: false
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@actions/github": "9.1.1",
"@openrouter/sdk": "1.2.85",
"env-var": "7.5.0",
"ignore": "5.3.2",
"luxon": "3.7.2",
"parse-diff": "0.12.0",
"zod": "4.5.4"
Expand Down
102 changes: 101 additions & 1 deletion src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest"
import { parseConfig, type RawInputs } from "../config.js"
import {
DEFAULT_DIFF_EXCLUDE_PATTERNS,
parseConfig,
type RawInputs,
} from "../config.js"

const makeRawInputs = (overrides: Partial<RawInputs> = {}): RawInputs => ({
githubToken: "ghs_testtoken",
Expand All @@ -19,6 +23,8 @@ const makeRawInputs = (overrides: Partial<RawInputs> = {}): RawInputs => ({
maxRelatedDocs: "4",
priorityDocs: "README.md",
excludePaths: "",
diffExcludePaths: "",
respectLinguistGenerated: true,
costSummary: true,
prNumberOverride: "",
...overrides,
Expand Down Expand Up @@ -46,6 +52,28 @@ describe("parseConfig", () => {
maxRelatedDocs: 4,
priorityDocs: ["README.md"],
excludePaths: [],
diffExcludePaths: {
defaultPatterns: [
"**/package-lock.json",
"**/npm-shrinkwrap.json",
"**/yarn.lock",
"**/pnpm-lock.yaml",
"**/bun.lock",
"**/bun.lockb",
"**/deno.lock",
"**/composer.lock",
"**/Cargo.lock",
"**/Gemfile.lock",
"**/poetry.lock",
"**/uv.lock",
"**/go.sum",
"**/*.min.js",
"**/*.min.css",
"**/*.map",
],
operatorPatterns: [],
},
respectLinguistGenerated: true,
costSummary: true,
prNumberOverride: undefined,
})
Expand Down Expand Up @@ -230,6 +258,78 @@ describe("parseConfig", () => {
expect(config.excludePaths).toEqual(["evals", "fixtures", "nested/deep"])
})

it("extends the default diff_exclude_paths list with supplied patterns", () => {
const config = parseConfig(
makeRawInputs({ diffExcludePaths: "evals/**, **/*.snap" }),
)

expect(config.diffExcludePaths.operatorPatterns).toEqual([
"evals/**",
"**/*.snap",
])
// The behavioral claim is tier preservation — supplied patterns must not
// replace the built-in list, so identity with the constant is the spec
expect(config.diffExcludePaths.defaultPatterns).toEqual(
DEFAULT_DIFF_EXCLUDE_PATTERNS,
)
})

it("disables the default list with a leading none", () => {
const config = parseConfig(makeRawInputs({ diffExcludePaths: "none" }))

expect(config.diffExcludePaths).toEqual({
defaultPatterns: [],
operatorPatterns: [],
})
})

it("replaces the default list via none followed by patterns", () => {
const config = parseConfig(
makeRawInputs({ diffExcludePaths: "none, evals/**" }),
)

expect(config.diffExcludePaths).toEqual({
defaultPatterns: [],
operatorPatterns: ["evals/**"],
})
})

it("rejects a non-leading none in diff_exclude_paths", () => {
expect(() =>
parseConfig(makeRawInputs({ diffExcludePaths: "evals/**, none" })),
).toThrow(
'diffExcludePaths: "none" disables the default list only in leading position — move it first or remove it',
)
})

it("normalizes diff_exclude_paths pattern spellings", () => {
const config = parseConfig(
makeRawInputs({ diffExcludePaths: "/evals, ./fixtures/, generated//" }),
)

expect(config.diffExcludePaths.operatorPatterns).toEqual([
"evals",
"fixtures",
"generated",
])
})

it("rejects a diff_exclude_paths pattern over the wildcard cap", () => {
expect(() =>
parseConfig(makeRawInputs({ diffExcludePaths: "*a*a*a*b" })),
).toThrow(
'diffExcludePaths: pattern(s) exceed the wildcard cap (at most 2 "*" per path segment; "**" segments exempt): *a*a*a*b',
)
})

it("passes a false respect_linguist_generated through unchanged", () => {
const config = parseConfig(
makeRawInputs({ respectLinguistGenerated: false }),
)

expect(config.respectLinguistGenerated).toBe(false)
})

it("rejects a zero max_related_files", () => {
expect(() => parseConfig(makeRawInputs({ maxRelatedFiles: "0" }))).toThrow(
'maxRelatedFiles: "0" is not a positive integer',
Expand Down
Loading