fix(diff): decode git-quoted paths from the PR diff - #125
Conversation
GitHub's diff C-quotes non-ASCII paths and paths with a quote, a backslash, or a control character. parse-diff keeps the escapes, so the workspace read, diff exclusion, and inline comments saw a path that does not exist. Decode from and to right after parsing; a malformed quote is kept as received with a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
umm-actually re-reviewed at No new findings (13 tracked finding(s) across all runs). umm-actually · deepseek/deepseek-v4.1-flash |
…acters A decoded escape can put a real newline, C1 control, or U+2028/U+2029 into a path. The path is rendered raw in the annotated diff header and in markdown, so a filename could forge a header line. Such a path now stays as received, with a warn log naming the code point. The job-summary list now escapes backslashes too, so a decoded backslash before a pipe cannot cancel the pipe's escape. Ship-Check: pr-review · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sites - quoted-paths: document the token regex's optional escaped character, the s flag, and full coverage; state the escape map's key form; compare the escape byte to undefined since 0x00 is a valid byte; narrow the chunks with every() instead of a second null filter; name the annotated diff header; say how a rejected path is reviewed. - orchestrate: state that decoding happens once for every later step, describe the excluded-entry shape, drop the no-op normalize on excluded paths, correct the comment on when parse-diff leaves `from` undefined, and turn the type-only null check into a commented guard. - review-summary: explain the backslash-then-pipe order with an example, document the priority-doc floor and cross-run duplicate fields, and mark the remaining truncated status. Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds decoder cases for an escaped backslash beside octal escapes, an escaped backslash followed by octal-looking digits, a NUL octal escape, and the bell, backspace, vertical-tab, and form-feed escapes. Adds an orchestrate case where a diff path with a doubled slash is excluded and still matches its priority doc and the scan exclusions. Ship-Check: test-audit · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parse-diff strips a trailing `\"` from `---`/`+++` paths as the closing quote, so a quoted path ending in an escaped backslash arrived with a lone trailing backslash. The decoder rejected it and kept a path that names no file, while the same file in a hunk-less diff decoded correctly from the `diff --git` line. A lone backslash can only match at the end of the path, so it now decodes as the escaped backslash parse-diff dropped. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A quoted diff path whose decoding is rejected keeps git's escapes, so it names no file GitHub knows. An inline comment on it made GitHub reject the whole review and reroute every inline finding to issue comments. decodeQuotedFilePaths now returns the rejected paths beside the files, and the orchestrator leaves those files out of the commentable-lines map. Their findings post as standalone comments while the rest of the review still posts inline. The file stays in the diff and the prompt. Ship-Check: triage · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A decoded path is now rejected only when it holds a line break: LF, VT, FF, CR, NEL, U+2028, or U+2029. Those are the characters that can forge an annotated-diff header line or split a markdown row. A file named with a tab or another control character now decodes, so it gets its workspace read and inline comments. The check also runs on unquoted paths, so the guard no longer depends on GitHub quoting every line break. Ship-Check: pr-monitor · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A finding on a file whose diff path was rejected posted with the beyond-diff location note, though it sits in a changed file. It now gets its own note. Each distinct diff path decodes and logs once, so a modified file with a rejected path warns once, and the warning no longer says "quoted", since unquoted paths are checked too. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions exclusion A rejected path was kept exactly as received, so an unquoted path with a raw line separator still reached the annotated-diff header and the job summary. Each raw line break in a kept rejected path now becomes git's octal escape of its UTF-8 bytes, and rejectedPaths carries the same spelling. The doubled-slash exclusion test now expects the conventions file in the scan exclusions, which main started adding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unknown-file filter keeps a finding whose path matches a changed file after normalization, such as ./a\rb.ts, but the rejected-path note was chosen by a raw compare. That finding fell through to the beyond-diff note. The check now compares normalized paths on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Document the standalone route for paths that cannot take an inline comment
This PR adds a third way a finding surfaces — a finding in a changed file whose diff path cannot take an inline comment posts as a standalone issue comment with its own location note — but the README still enumerates only an inline review plus beyond-diff standalone comments. A reader cannot account for why a changed file's finding appeared outside the diff, and AGENTS.md requires docs to update in the same change that alters behavior. Failure scenario: A PR touches a file whose name carries an escape git C-quotes (e.g. a carriage return in the name). The file's finding posts as a standalone issue comment with the rejected-path note and never appears inline, while a user following the README expects changed-file findings to be anchored to the diff or to be described as beyond-diff. Suggested fixExtend the "What it does" bullet and "How it works" step 9 to name the route, e.g. "Findings in a changed file whose diff path cannot take an inline comment also post as standalone comments" — in the same shape as the existing beyond-diff clause.umm-actually · deepseek/deepseek-v4.1-flash |
|
Escape backticks in the finding location line
Pre-existing: the location line wraps Failure scenario: A PR renames a file to src/a Suggested fixSize the code fence to the longest backtick run in the path the way suggestionBlock already sizes its diff fence (or render the location as a fenced block), so a backtick in the filename cannot close the span.umm-actually · deepseek/deepseek-v4.1-flash |
…ent the rejected-path route Finding locations, the cap note, and the context notes wrapped paths in single backticks, so a filename with a backtick closed the span early. A renderCodeSpan helper now sizes the delimiter past the longest backtick run and pads edge backticks or spaces. The README now names findings on a changed file whose diff path cannot take an inline comment among the standalone comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Dispositions for the two umm-actually findings posted as PR-level comments on
🔍 ship-check · pr-monitor · claude-opus-5-5 |
…y and code spans Workspace-scan paths (related files, mention-matched docs, cap-excluded paths) never pass the diff decoder's line-break check, so a raw newline split the job-summary table row or ended a code span in the context notes. The job-summary list and renderCodeSpan now write each line break as its octal escape first, reusing the decoder's escapeLineBreaks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Neutralize
Pre-existing: the anchor HTML comment interpolates the finding's path raw, so a file whose name contains Failure scenario: A PR adds a file named Suggested fixEncode the `--` sequence when building the anchor key and reverse it in parseAnchorKey, or treat a path containing `-->` as uncommentable the way this PR treats rejected diff paths, keeping the raw path only in the escaped display surfaces.umm-actually · deepseek/deepseek-v4.1-flash |
|
Escape the PR's head and base refs in the job summary
Pre-existing: renderReviewSummary now escapes line breaks, backslashes, and pipes for every path list (renderCommaList) but the PR line still interpolates headRef and baseRef raw inside code spans, so a branch name containing a backtick closes the span and lets the remaining text render as loose markdown in the workflow job summary. Failure scenario: A PR from a branch named Suggested fixRun headRef and baseRef through the same escaping renderCommaList applies to paths (line breaks, backslashes, backticks/span delimiters) before interpolating them into the summary line.umm-actually · deepseek/deepseek-v4.1-flash |
# Conflicts: # README.md
Git allows a backtick in a branch name, so the PR line's single-backtick spans around the head and base refs could close early. Both refs now go through renderCodeSpan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Dispositions for the two umm-actually findings posted as PR-level comments on
🔍 ship-check · pr-monitor · claude-opus-5-5 |
|
Update the diff-header spelling claim in the unknown-file filter docs
The Unknown-file filter section states that a diff header prints a path as the diff spells it, but quoted diff paths are now decoded before annotateDiff builds the header, so the header prints the decoded spelling (a diff that spells Failure scenario: A PR changes a file whose quoted diff path decodes to Suggested fixReword the sentence to name the decoded spelling, e.g. "A diff header prints the path in the spelling the filter matches (decoded, without the `"` escaping the prompt tags add), and the filter matches that spelling as written."umm-actually · deepseek/deepseek-v4.1-flash |
… filter section Quoted diff paths are now decoded before the annotated diff is built, so the header shows each double quote as written. A path whose decoding was rejected keeps the escaped spelling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Disposition for umm-actually finding 5922423239 (the diff-header spelling claim in the Unknown-file filter section), posted on Valid, fixed in 1200638. Quoted diff paths are now decoded before the annotated diff is built, so the header shows each 🔍 ship-check · pr-monitor · claude-opus-5-5 |
…d standalone route Git never quotes a backtick, so two paths with one each in a job-summary cell opened a code span. renderCommaList now escapes backticks after backslashes. The README feature list and step 9 now name the in-diff findings that post as standalone comments when GitHub rejects the inline review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
A PR that touches a file with a non-ASCII name got a worse review. The same applied to a name with a
"or a\. GitHub's diff C-quotes those paths ("b/nn/0016_\303\245-f\303\270de.md"). parse-diff removes the quotes but keeps the escapes, so the action worked with a path that does not exist:This PR decodes the quoted paths once, right after parse-diff, so every later step sees the real path.
core.quotePath=false, and every character it quotes becomes a backslash escape. The decoder accepts the escapes git'sunquote_c_styleaccepts:\a \b \t \n \v \f \r \" \\and\ooowith a first digit of 0-3. The resulting bytes decode as UTF-8, andnode:buffer'sisUtf8validates them.\342\200\250).Changes
src/diff/quoted-paths.ts(new):decodeQuotedPathreturnsunquoted,decoded, orrejectedwith a reason.decodeQuotedFilePathsmaps parse-diff files, decodesfromandto, and passes/dev/nulland an absent path through unchanged. It returns the files with the set of rejected paths, each spelled as the diff spelled it with any raw line break escaped. Each distinct path decodes once, so a decoded path logs once at debug and a rejected path logs once at warn.src/orchestrate.ts: wraps the singleparseDiffcall indecodeQuotedFilePaths. A file whose new path was rejected is left out of the commentable-lines map, with a debug log naming the path. Its findings render with their own location note instead of the beyond-diff one. The note is chosen by comparing normalized paths, the same way the unknown-file filter matches, so a finding spelled./a\rb.tsstill gets it. Excluded diff paths are compared to priority docs without a secondposix.normalize, because exclusion already normalizes them.src/review/comment-mapping.ts:renderRejectedPathFindingrenders a finding in a changed file whose path cannot take an inline comment. The finding location line and the max-findings cap note render each path withrenderCodeSpan.src/review/markdown.ts(new):renderCodeSpanwraps a path in an inline code span that a backtick in the path cannot close. It writes each line break as its octal escape, because a blank line ends a code span. The delimiter is one backtick longer than the path's longest backtick run, and a path that starts or ends with a backtick or a space gets one space of padding on each side. Git never quotes a backtick, so such a path reaches these comments as written.src/review/context-notes.ts: the priority-doc, related-file, diff-exclusion, and conventions notes render paths withrenderCodeSpan.src/review/review-summary.ts: the job-summary path lists escape line breaks, then backslashes, then backticks and pipes. Workspace-scan paths (related files, mention-matched docs, cap-excluded paths) never pass the decoder's line-break check, so a raw newline would split the table row; it is written as its octal escape first. A decoded backslash right before a pipe would otherwise cancel the pipe's escape and split the table cell. Two backticks in one cell, which git never quotes, would otherwise open a code span. The PR line renders the head and base branch names withrenderCodeSpan, because a branch name can contain a backtick.src/diff/__tests__/quoted-paths.test.ts(new): decoder tests.\",\\, raw non-ASCII text beside an escape,\\directly before octal escapes,\\followed by octal-looking digits, and a lone trailing backslash./dev/null.\t,\a,\b, an octal NUL, an octal ESC, and an octal DEL.\n,\r,\v,\f, U+0085, U+2028, and U+2029, plus an unquoted path holding a raw U+2028.src/__tests__/orchestrate.test.ts:=== forged.ts ===header leaves exactly one header line in the annotated diff. An unquoted filename with a raw U+2028 does the same when the diff is split on every line-break character../still posts with the rejected-path note.docs/a"b.md. The prompt escapes the quote in path attributes, so the model reportsdocs/a"b.md, and the finding still posts inline under the decoded path.src/review/__tests__/comment-mapping.test.ts: the rejected-path renderer's full body, for a plain path and for a path with a backtick.src/review/__tests__/review-summary.test.ts: a backslash before a pipe renders as\\\|, a path with a newline and pipes stays on one table row, two paths with backticks in one cell render escaped, and a branch name with a backtick keeps its code span whole.AGENTS.md: thediff/structure line lists path decoding, and thereview/line lists markdown code spans.README.md: the feature list and step 9 of How it works name findings in a changed file whose diff path cannot take an inline comment among the standalone comments, along with every in-diff finding when GitHub rejects the inline review. The Unknown-file filter section says a diff header prints the decoded path, and that a path whose decoding was rejected keeps its escaped spelling.src/review/__tests__/markdown.test.ts(new): code spans for a plain path, one backtick, a two-backtick run, an edge backtick at either end, an edge space at either end, and a path holding a blank line.src/review/__tests__/context-notes.test.ts: a diff-excluded path with a backtick keeps its code span whole.Testing
npm test: 899 passed, after merging main.npm run lintandnpm run buildpass.git checkout:orchestrate.tsfails the orchestrate test, becausechangedPathsgetsnn/0016_\\303\\245-f\\303\\270de.md.caf�.md.[0-7]{3}fails the above-one-byte test.\"escape fails the escaped-quote test.\p{Cc}) fails all six break-no-line decode cases, tab included.tofails the absent-new-path test, because the file gainsto: "".\aescape fails the bell case.posix.normalizefrom exclusion's classification path fails the doubled-slash test, becauseassets/guide.mdstays in the priority-doc read.a\rb.ts../orchestrate test.renderCodeSpanfails the one-row table test and the blank-line code-span test.fromandtofor every quoted case:from: "/dev/null",to: "nn/0016_\\303\\245-f\\303\\270de.md"from: "docs/\\341\\213\\265.md",to: "docs/\\341\\213\\265-new.md". Pure renames take their paths from thediff --gitline, because parse-diff ignoresrename fromandrename to.from: "nn/\\303\\245.md",to: "/dev/null"fromandtoare both"say \\\"hi\\\".md"\comes back asends-with\with a lone trailing backslash, because parse-diff's closing-quote regex drops the escaped backslash. The decoder reads that lone trailing backslash as the escaped backslash parse-diff dropped. Thediff --gitline keeps both backslashes, so a hunk-less rename decodes to the same path.indexfield holds only blob hashes and the mode, and nothing reads it.back\slash.md,say "hi".md, a tab, a newline, a C0 byte, and DEL under bothcore.quotePathsettings.å-føde.md,ድ.md, U+0085, and U+2028 only with the default, as octal escapes.|, or a backtick is never quoted.🤖 Generated with Claude Code