fix: pass-2 review fixes

A test edit routes as tests= only when it adds lines with no gate tag; a removed or changed line or an unreadable test diff goes through the full loop. Diffs skip textconv. A hunkless patch (binary, empty, mode-only) is same when its patch-id matches. Without --reviewed-base test files route as before. finish's packet path and attempt's state directory have fallbacks.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-31 09:15:22 +00:00
1 parent d25fd4a51a
commit 37cfb481be
5 files changed
+59 -13

No files matched your search

+15 -7
View File
@@ -13,7 +13,9 @@
* The merge may still have spliced the two (a JSON entry where the branch changed one key and
* the base the next), so a context path needs a scoped review of its hunks, never a silent pass;
* - changed: anything else, including the same lines moved to another place, a file-mode change, and any path whose
* patches cannot be read (an empty patch on both sides of a path the branch touches).
* patches cannot be read (an empty patch on both sides of a path the branch touches). A binary, empty or mode-only
* patch has no hunks to place, so it is same when its patch-id matches and changed otherwise.
* Diffs skip external drivers and textconv, so a configured filter cannot hide a line.
* Paths reach git as literal pathspecs from NUL-separated listings, so quoted names and glob
* characters cannot widen or empty a comparison.
*/
@@ -39,7 +41,7 @@ export function touchedPaths(repoRoot: string, from: string, to: string): Set<st
}
function pathDiff(repoRoot: string, from: string, to: string, rel: string, context: number): string | null {
return git(repoRoot, ['--literal-pathspecs', 'diff', '--no-color', '--no-ext-diff', '--no-renames', '--binary', '--full-index', `-U${context}`, from, to, '--', rel]);
return git(repoRoot, ['--literal-pathspecs', 'diff', '--no-color', '--no-ext-diff', '--no-textconv', '--no-renames', '--binary', '--full-index', `-U${context}`, from, to, '--', rel]);
}
function patchId(repoRoot: string, diff: string): string | null {
@@ -110,7 +112,7 @@ export function comparePath(repoRoot: string, s: PatchSides, rel: string): PathV
if (ids[0] === null || ids[1] === null) return 'changed';
const r0 = zeroHunks(pathDiff(repoRoot, s.reviewedBase, s.reviewed, rel, 0) ?? '');
const c0 = zeroHunks(pathDiff(repoRoot, s.currentBase, s.current, rel, 0) ?? '');
if (ids[0] === ids[1] && r0 !== null && !r0.length) return 'same';
if (ids[0] === ids[1] && r0 === null) return 'same';
const b0 = zeroHunks(pathDiff(repoRoot, s.reviewedBase, s.currentBase, rel, 0) ?? '');
if (!r0?.length || !c0 || !b0 || r0.length !== c0.length || r0.some((h, i) => h.body !== c0[i]!.body)) return 'changed';
const map = mapper(b0);
@@ -143,13 +145,19 @@ export function isTestCode(rel: string): boolean {
return matchesAny(CARRY_TEST_CODE_GLOBS, rel) && !/(^|\/)(helpers|fixtures|__fixtures__|__snapshots__)\//.test(rel);
}
/** Test paths whose change from..to carries a gate edit (lib/gate-diff RH tags: a removed or skipped test, a snapshot, a relaxed gate value). */
/**
* Test paths whose change from..to needs review: any removed or changed line (a weakened assertion or a changed
* expected value carries no gate tag), a gate edit lib/gate-diff tags (a skipped test, a snapshot, a relaxed gate
* value), or a diff the scan cannot read. Only added lines with no tag stay a lanes-only rerun.
*/
export function gateEditedPaths(repoRoot: string, from: string, to: string, paths: readonly string[]): Set<string> | null {
if (!paths.length) return new Set();
const diff = git(repoRoot, ['--literal-pathspecs', 'diff', '--no-color', '--no-ext-diff', '--no-renames', from, to, '--', ...paths]);
const diff = git(repoRoot, ['--literal-pathspecs', 'diff', '--no-color', '--no-ext-diff', '--no-textconv', '--no-renames', from, to, '--', ...paths]);
if (diff === null) return null;
const scan = scanCandidate({ files: parseUnifiedDiff(diff), commits: [], project: EMPTY_PROJECT_CONFIG, specIsTestPath: true, candidate: 'route', artifact: '' });
return new Set(scan.hunks.filter(h => h.tags.length > 0).map(h => h.path));
const files = parseUnifiedDiff(diff);
const scan = scanCandidate({ files, commits: [], project: EMPTY_PROJECT_CONFIG, specIsTestPath: true, candidate: 'route', artifact: '' });
const removed = paths.filter(p => (pathDiff(repoRoot, from, to, p, 0) ?? '-').split('\n').some(l => (l.startsWith('-') && !l.startsWith('--- ')) || l.startsWith('Binary files ') || l.startsWith('GIT binary patch')));
return new Set([...scan.hunks.filter(h => h.tags.length > 0).map(h => h.path), ...scan.unsupportedPaths.map(u => u.path), ...removed]);
}
export interface ScopedReview { paths: string[]; reviewer: string; verdict: 'clean'; head?: string; tree?: string }
+2 -2
View File
@@ -179,7 +179,7 @@ function route(cwd: string, base: string, reviewed: string, reviewedBase: string
const by: Routed = { behavior: [], docs: [], metadata: [], tests: [], base: [], context: [] };
for (const p of [...changedSet].sort()) {
const verdict = sides ? (touched.has(p) ? comparePath(cwd, sides, p) : 'same') : 'changed';
by[verdict === 'same' ? 'base' : release.includes(p) ? 'metadata' : isTestCode(p) ? 'tests' : classifyPath(p, cwd) === 'docs' ? 'docs' : verdict === 'context' ? 'context' : 'behavior'].push(p);
by[verdict === 'same' ? 'base' : release.includes(p) ? 'metadata' : sides && isTestCode(p) ? 'tests' : classifyPath(p, cwd) === 'docs' ? 'docs' : verdict === 'context' ? 'context' : 'behavior'].push(p);
}
const gated = gateEditedPaths(cwd, reviewed, now, by.tests) ?? new Set(by.tests);
by.behavior.push(...by.tests.filter(p => gated.has(p)));
@@ -188,7 +188,7 @@ function route(cwd: string, base: string, reviewed: string, reviewedBase: string
if (by.behavior.length) { r.stop(`behavior, tests or build inputs changed since review (${short(by.behavior.sort())}): insert 5–11.5 → 12–14 → 16 before Step 17 (stage 2.1)`); return null; }
if (by.context.length && sides) {
if (!opts.contextReviewed) {
const packet = path.resolve(cwd, git(cwd, ['rev-parse', '--git-path', `gstack/scoped-review-${now.slice(0, 12)}.md`]).stdout.trim());
const packet = path.resolve(cwd, git(cwd, ['rev-parse', '--git-path', `gstack/scoped-review-${now.slice(0, 12)}.md`]).stdout.trim() || `.git/gstack/scoped-review-${now.slice(0, 12)}.md`);
fs.mkdirSync(path.dirname(packet), { recursive: true });
fs.writeFileSync(packet, contextPacket(cwd, sides, by.context));
r.stop(`base context moved next to the reviewed hunks in ${short(by.context)}: one fix-check reviewer reads ${packet}, then rerun with --context-reviewed <record.json> written beside the packet, outside the worktree ({"tree":"${now}","paths":[…],"reviewer":"<model>","verdict":"clean"}); a blocking finding is a behavior change (stage 2.1)`);