From 1c4e8807f4f98cad02a6a353f8f9f2a405090d9e Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 12 Aug 2026 15:05:03 -0700 Subject: [PATCH 1/8] fix(scan): reject committed symlinks in diff inventories --- .../scripts/generate_in_scope_files.py | 18 +++++++ .../tests-ts/compact-diff-scan.test.ts | 53 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index b69c6b08..65b4009a 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -144,6 +144,24 @@ def generate_diff_in_scope_files( relative = path.relative_to(repository) if path_is_excluded(relative) or path.suffix.lower() not in TEXT_CODE_EXTENSIONS: continue + if mode == "revisions": + revision = base if status == "D" else head + entry = subprocess.run( + [ + "git", + "-C", + str(repository), + "ls-tree", + "-z", + revision, + "--", + relative.as_posix(), + ], + capture_output=True, + check=True, + ).stdout + if entry.startswith(b"120000 "): + continue if status != "D": if mode == "revisions": contents = subprocess.run( diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index 87fcae93..04d87ba9 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -206,6 +206,59 @@ describe("compact diff scan", () => { ]); }); + test("excludes committed symlinks using their selected revision modes", () => { + const { root, repository } = createRepository(); + writeSource(repository, "src/handler.py", "value = 1\n"); + writeSource(repository, "src/deleted-link.py", "handler.py"); + git(repository, "add", "."); + const deletedLink = git(repository, "hash-object", "src/deleted-link.py"); + git( + repository, + "update-index", + "--cacheinfo", + `120000,${deletedLink},src/deleted-link.py`, + ); + git(repository, "commit", "-qm", "base"); + const base = git(repository, "rev-parse", "HEAD"); + + rmSync(join(repository, "src", "deleted-link.py")); + writeSource(repository, "src/handler.py", "value = 2\n"); + writeSource(repository, "src/added-link.py", "handler.py"); + git(repository, "add", "."); + const addedLink = git(repository, "hash-object", "src/added-link.py"); + git( + repository, + "update-index", + "--cacheinfo", + `120000,${addedLink},src/added-link.py`, + ); + git(repository, "commit", "-qm", "selected changes"); + const head = git(repository, "rev-parse", "HEAD"); + const output = join(root, "in-scope.txt"); + + const result = python( + "generate_in_scope_files.py", + "--repo", + repository, + "--scope", + ".", + "--diff-base", + base, + "--diff-head", + head, + "--out", + output, + ); + + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(output, "utf8").split("\n").filter(Boolean)).toEqual([ + "src/handler.py", + ]); + expect(readFileSync(join(repository, "src", "added-link.py"), "utf8")).toBe( + "handler.py", + ); + }); + test("includes staged, unstaged, and untracked working-tree changes", () => { const { root, repository } = createRepository(); writeSource(repository, "src/handler.py", "value = 1\n"); From de69cc5531e8a4dbe354e2e0ba2f3c769a71f415 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 12 Aug 2026 15:11:38 -0700 Subject: [PATCH 2/8] fix(scan): batch committed symlink mode checks --- .../scripts/generate_in_scope_files.py | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index 65b4009a..59a32924 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -105,6 +105,40 @@ def generate_in_scope_files(repository: Path, scope: str, output: Path) -> int: return write_inventory(output, rows) +def committed_symlink_paths(repository: Path, base: str, head: str) -> set[Path]: + """Read selected Git modes together without interpreting changed paths.""" + result = subprocess.run( + [ + "git", + "-C", + str(repository), + "diff", + "--raw", + "-z", + "--diff-filter=ACMRD", + f"{base}..{head}", + ], + capture_output=True, + text=True, + check=True, + ) + fields = result.stdout.split("\0") + symlinks: set[Path] = set() + index = 0 + while index < len(fields) - 1: + metadata = fields[index].split() + status = metadata[-1][0] + index += 1 + if status in {"C", "R"}: + index += 1 + path = repository / fields[index] + index += 1 + selected_mode = metadata[0].removeprefix(":") if status == "D" else metadata[1] + if selected_mode == "120000": + symlinks.add(path) + return symlinks + + def generate_diff_in_scope_files( repository: Path, base: str, @@ -137,31 +171,19 @@ def generate_diff_in_scope_files( for relative in untracked.stdout.split("\0") if relative ) + symlinks: set[Path] = set() else: changed = git_changed_paths(repository, base, head, mode) + symlinks = committed_symlink_paths(repository, base, head) for path, status in changed: relative = path.relative_to(repository) - if path_is_excluded(relative) or path.suffix.lower() not in TEXT_CODE_EXTENSIONS: + if ( + path in symlinks + or path_is_excluded(relative) + or path.suffix.lower() not in TEXT_CODE_EXTENSIONS + ): continue - if mode == "revisions": - revision = base if status == "D" else head - entry = subprocess.run( - [ - "git", - "-C", - str(repository), - "ls-tree", - "-z", - revision, - "--", - relative.as_posix(), - ], - capture_output=True, - check=True, - ).stdout - if entry.startswith(b"120000 "): - continue if status != "D": if mode == "revisions": contents = subprocess.run( From 7c49fe1689fd07e40689998bea84b8cc69d6e4ca Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 12 Aug 2026 15:18:07 -0700 Subject: [PATCH 3/8] fix(scan): preserve exact Git path casing in diff inventory --- .../_bundled_plugin/scripts/generate_in_scope_files.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index 59a32924..75295356 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -105,7 +105,7 @@ def generate_in_scope_files(repository: Path, scope: str, output: Path) -> int: return write_inventory(output, rows) -def committed_symlink_paths(repository: Path, base: str, head: str) -> set[Path]: +def committed_symlink_paths(repository: Path, base: str, head: str) -> set[str]: """Read selected Git modes together without interpreting changed paths.""" result = subprocess.run( [ @@ -123,7 +123,7 @@ def committed_symlink_paths(repository: Path, base: str, head: str) -> set[Path] check=True, ) fields = result.stdout.split("\0") - symlinks: set[Path] = set() + symlinks: set[str] = set() index = 0 while index < len(fields) - 1: metadata = fields[index].split() @@ -131,7 +131,7 @@ def committed_symlink_paths(repository: Path, base: str, head: str) -> set[Path] index += 1 if status in {"C", "R"}: index += 1 - path = repository / fields[index] + path = fields[index] index += 1 selected_mode = metadata[0].removeprefix(":") if status == "D" else metadata[1] if selected_mode == "120000": @@ -171,7 +171,7 @@ def generate_diff_in_scope_files( for relative in untracked.stdout.split("\0") if relative ) - symlinks: set[Path] = set() + symlinks: set[str] = set() else: changed = git_changed_paths(repository, base, head, mode) symlinks = committed_symlink_paths(repository, base, head) @@ -179,7 +179,7 @@ def generate_diff_in_scope_files( for path, status in changed: relative = path.relative_to(repository) if ( - path in symlinks + relative.as_posix() in symlinks or path_is_excluded(relative) or path.suffix.lower() not in TEXT_CODE_EXTENSIONS ): From c6b1dc030b8dbbcb8816d238511a7818311979c4 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 12 Aug 2026 15:34:37 -0700 Subject: [PATCH 4/8] fix(scan): reject symlinks across diff review workflows --- .../scripts/generate_in_scope_files.py | 42 +------------------ .../scripts/generate_rank_input.py | 33 ++++++++++++++- .../tests-ts/compact-diff-scan.test.ts | 22 ++++++++++ 3 files changed, 55 insertions(+), 42 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index 75295356..b69c6b08 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -105,40 +105,6 @@ def generate_in_scope_files(repository: Path, scope: str, output: Path) -> int: return write_inventory(output, rows) -def committed_symlink_paths(repository: Path, base: str, head: str) -> set[str]: - """Read selected Git modes together without interpreting changed paths.""" - result = subprocess.run( - [ - "git", - "-C", - str(repository), - "diff", - "--raw", - "-z", - "--diff-filter=ACMRD", - f"{base}..{head}", - ], - capture_output=True, - text=True, - check=True, - ) - fields = result.stdout.split("\0") - symlinks: set[str] = set() - index = 0 - while index < len(fields) - 1: - metadata = fields[index].split() - status = metadata[-1][0] - index += 1 - if status in {"C", "R"}: - index += 1 - path = fields[index] - index += 1 - selected_mode = metadata[0].removeprefix(":") if status == "D" else metadata[1] - if selected_mode == "120000": - symlinks.add(path) - return symlinks - - def generate_diff_in_scope_files( repository: Path, base: str, @@ -171,18 +137,12 @@ def generate_diff_in_scope_files( for relative in untracked.stdout.split("\0") if relative ) - symlinks: set[str] = set() else: changed = git_changed_paths(repository, base, head, mode) - symlinks = committed_symlink_paths(repository, base, head) for path, status in changed: relative = path.relative_to(repository) - if ( - relative.as_posix() in symlinks - or path_is_excluded(relative) - or path.suffix.lower() not in TEXT_CODE_EXTENSIONS - ): + if path_is_excluded(relative) or path.suffix.lower() not in TEXT_CODE_EXTENSIONS: continue if status != "D": if mode == "revisions": diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 535fe341..7c0581c2 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -637,7 +637,36 @@ def run_git_changed_paths(repo: Path, diff_args: list[str]) -> list[tuple[Path, def git_changed_paths(repo: Path, base: str, head: str, mode: str) -> list[tuple[Path, str]]: if mode == "revisions": - return run_git_changed_paths(repo, [f"{base}..{head}"]) + result = subprocess.run( + [ + "git", + "-C", + str(repo), + "diff", + "--raw", + "-z", + "--diff-filter=ACMRD", + f"{base}..{head}", + ], + check=True, + capture_output=True, + text=True, + ) + fields = result.stdout.split("\0") + changed: list[tuple[Path, str]] = [] + index = 0 + while index < len(fields) - 1: + metadata = fields[index].split() + status = metadata[-1][0] + index += 1 + if status in {"C", "R"}: + index += 1 + path = fields[index] + index += 1 + selected_mode = metadata[0].removeprefix(":") if status == "D" else metadata[1] + if selected_mode != "120000": + changed.append((repo / path, status)) + return changed if mode == "local-patch": unstaged = run_git_changed_paths(repo, [base]) staged = run_git_changed_paths(repo, ["--cached", base]) @@ -660,6 +689,8 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: if status == "D": preview = "" + elif path.is_symlink(): + continue elif path.is_file(): preview, is_binary = preview_for(path, args.preview_bytes) if is_binary: diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index 04d87ba9..4a6f4fef 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -257,6 +257,28 @@ describe("compact diff scan", () => { expect(readFileSync(join(repository, "src", "added-link.py"), "utf8")).toBe( "handler.py", ); + + const rankInput = join(root, "rank-input.jsonl"); + const legacyResult = python( + "generate_rank_input.py", + "make-diff-rank-input", + "--repo", + repository, + "--base", + base, + "--head", + head, + "--out", + rankInput, + ); + + expect(legacyResult.status, legacyResult.stderr).toBe(0); + expect( + readFileSync(rankInput, "utf8") + .trim() + .split("\n") + .map((row) => JSON.parse(row).path), + ).toEqual(["src/handler.py"]); }); test("includes staged, unstaged, and untracked working-tree changes", () => { From 6e813093b4ce8afb54dce556d43bd8075cf8fc78 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 12 Aug 2026 15:40:45 -0700 Subject: [PATCH 5/8] fix(scan): read diff previews from selected git revisions --- .../scripts/generate_rank_input.py | 91 +++++++++++++++++-- .../tests-ts/compact-diff-scan.test.ts | 28 ++++++ 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 7c0581c2..4aa99aff 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -37,7 +37,15 @@ # Some plugin hosts launch Python with safe-path isolation enabled. sys.path.insert(0, str(Path(__file__).resolve().parent)) -from rank_preview import DEFAULT_PREVIEW_BYTES, TEXT_CODE_EXTENSIONS, preview_for +from rank_preview import ( + DEFAULT_PREVIEW_BYTES, + TEXT_CODE_EXTENSIONS, + fit_preview_lines, + is_binary_sample, + preview_for, + select_preview_lines, + structural_outline, +) from workbench_target import git_directory_snapshot_paths EXCLUDED_DIRS = { @@ -635,7 +643,14 @@ def run_git_changed_paths(repo: Path, diff_args: list[str]) -> list[tuple[Path, return changed -def git_changed_paths(repo: Path, base: str, head: str, mode: str) -> list[tuple[Path, str]]: +def git_changed_paths( + repo: Path, + base: str, + head: str, + mode: str, + *, + revision_blob_ids: dict[str, str] | None = None, +) -> list[tuple[Path, str]]: if mode == "revisions": result = subprocess.run( [ @@ -666,6 +681,8 @@ def git_changed_paths(repo: Path, base: str, head: str, mode: str) -> list[tuple selected_mode = metadata[0].removeprefix(":") if status == "D" else metadata[1] if selected_mode != "120000": changed.append((repo / path, status)) + if revision_blob_ids is not None and status != "D": + revision_blob_ids[path] = metadata[3] return changed if mode == "local-patch": unstaged = run_git_changed_paths(repo, [base]) @@ -676,19 +693,81 @@ def git_changed_paths(repo: Path, base: str, head: str, mode: str) -> list[tuple raise SystemExit(f"Unknown diff mode: {mode}") +def committed_blob_contents(repo: Path, blob_ids: dict[str, str]) -> dict[str, bytes]: + if not blob_ids: + return {} + + requested = "".join(f"{object_id}\n" for object_id in blob_ids.values()).encode("ascii") + result = subprocess.run( + ["git", "-C", str(repo), "cat-file", "--batch"], + input=requested, + capture_output=True, + check=True, + ) + contents: dict[str, bytes] = {} + offset = 0 + for path in blob_ids: + header_end = result.stdout.index(b"\n", offset) + header = result.stdout[offset:header_end].split() + if len(header) != 3 or header[1] != b"blob": + raise ValueError("Git returned an unexpected object for a changed source file") + data_start = header_end + 1 + data_end = data_start + int(header[2]) + if result.stdout[data_end : data_end + 1] != b"\n": + raise ValueError("Git returned incomplete changed source contents") + contents[path] = result.stdout[data_start:data_end] + offset = data_end + 1 + return contents + + def make_diff_rank_input(args: argparse.Namespace) -> None: repo = Path(args.repo).expanduser().resolve() if not repo.is_dir(): raise SystemExit(f"Repo path not found: {repo}") + revision_blob_ids: dict[str, str] = {} + changed = git_changed_paths( + repo, + args.base, + args.head, + args.mode, + revision_blob_ids=revision_blob_ids, + ) + selected = [ + (path, status) + for path, status in changed + if not path_is_excluded(path.relative_to(repo)) + and path.suffix.lower() in TEXT_CODE_EXTENSIONS + ] + committed_contents = ( + committed_blob_contents( + repo, + { + path.relative_to(repo).as_posix(): revision_blob_ids[ + path.relative_to(repo).as_posix() + ] + for path, status in selected + if status != "D" + }, + ) + if args.mode == "revisions" + else {} + ) + rows: list[JsonRow] = [] - for path, status in git_changed_paths(repo, args.base, args.head, args.mode): + for path, status in selected: rel = path.relative_to(repo) - if path_is_excluded(rel) or path.suffix.lower() not in TEXT_CODE_EXTENSIONS: - continue - if status == "D": preview = "" + elif args.mode == "revisions": + contents = committed_contents[rel.as_posix()] + if is_binary_sample(contents): + continue + text = contents.decode("utf-8", errors="ignore") + outline = structural_outline(path, text) + preview = fit_preview_lines( + select_preview_lines(outline or text.splitlines()), args.preview_bytes + ) elif path.is_symlink(): continue elif path.is_file(): diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index 4a6f4fef..17401943 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -204,6 +204,34 @@ describe("compact diff scan", () => { "src/handler.py", "src/new handler.py", ]); + + const rankInput = join(root, "rank-input.jsonl"); + const legacyResult = python( + "generate_rank_input.py", + "make-diff-rank-input", + "--repo", + repository, + "--base", + base, + "--head", + head, + "--out", + rankInput, + ); + + expect(legacyResult.status, legacyResult.stderr).toBe(0); + const rows = readFileSync(rankInput, "utf8") + .trim() + .split("\n") + .map((row) => JSON.parse(row)); + expect(rows.map((row) => row.path)).toEqual([ + "src/guard.py", + "src/handler.py", + "src/new handler.py", + ]); + expect(rows.find((row) => row.path === "src/handler.py")?.preview).toBe( + "value = 2", + ); }); test("excludes committed symlinks using their selected revision modes", () => { From 106b1c0f96c79fded6e996aadd8836c438e47a2c Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 12 Aug 2026 15:46:02 -0700 Subject: [PATCH 6/8] fix(scan): stream regular committed source blobs --- .../scripts/generate_rank_input.py | 73 +++++++++++-------- .../tests-ts/compact-diff-scan.test.ts | 7 ++ 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 4aa99aff..4005820a 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -679,7 +679,7 @@ def git_changed_paths( path = fields[index] index += 1 selected_mode = metadata[0].removeprefix(":") if status == "D" else metadata[1] - if selected_mode != "120000": + if selected_mode in {"100644", "100755"}: changed.append((repo / path, status)) if revision_blob_ids is not None and status != "D": revision_blob_ids[path] = metadata[3] @@ -693,31 +693,45 @@ def git_changed_paths( raise SystemExit(f"Unknown diff mode: {mode}") -def committed_blob_contents(repo: Path, blob_ids: dict[str, str]) -> dict[str, bytes]: +def committed_blob_previews( + repo: Path, blob_ids: dict[str, str], preview_bytes: int +) -> dict[str, str | None]: if not blob_ids: return {} - requested = "".join(f"{object_id}\n" for object_id in blob_ids.values()).encode("ascii") - result = subprocess.run( + previews: dict[str, str | None] = {} + with subprocess.Popen( ["git", "-C", str(repo), "cat-file", "--batch"], - input=requested, - capture_output=True, - check=True, - ) - contents: dict[str, bytes] = {} - offset = 0 - for path in blob_ids: - header_end = result.stdout.index(b"\n", offset) - header = result.stdout[offset:header_end].split() - if len(header) != 3 or header[1] != b"blob": - raise ValueError("Git returned an unexpected object for a changed source file") - data_start = header_end + 1 - data_end = data_start + int(header[2]) - if result.stdout[data_end : data_end + 1] != b"\n": - raise ValueError("Git returned incomplete changed source contents") - contents[path] = result.stdout[data_start:data_end] - offset = data_end + 1 - return contents + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) as process: + assert process.stdin is not None + assert process.stdout is not None + assert process.stderr is not None + for path, object_id in blob_ids.items(): + process.stdin.write(f"{object_id}\n".encode("ascii")) + process.stdin.flush() + header = process.stdout.readline().split() + if len(header) != 3 or header[1] != b"blob": + raise ValueError("Git returned an unexpected object for a changed source file") + size = int(header[2]) + contents = process.stdout.read(size) + if len(contents) != size or process.stdout.read(1) != b"\n": + raise ValueError("Git returned incomplete changed source contents") + if is_binary_sample(contents): + previews[path] = None + continue + text = contents.decode("utf-8", errors="ignore") + outline = structural_outline(repo / path, text) + previews[path] = fit_preview_lines( + select_preview_lines(outline or text.splitlines()), preview_bytes + ) + process.stdin.close() + stderr = process.stderr.read() + if (returncode := process.wait()) != 0: + raise subprocess.CalledProcessError(returncode, process.args, stderr=stderr) + return previews def make_diff_rank_input(args: argparse.Namespace) -> None: @@ -739,8 +753,8 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: if not path_is_excluded(path.relative_to(repo)) and path.suffix.lower() in TEXT_CODE_EXTENSIONS ] - committed_contents = ( - committed_blob_contents( + committed_previews = ( + committed_blob_previews( repo, { path.relative_to(repo).as_posix(): revision_blob_ids[ @@ -749,6 +763,7 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: for path, status in selected if status != "D" }, + args.preview_bytes, ) if args.mode == "revisions" else {} @@ -760,14 +775,10 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: if status == "D": preview = "" elif args.mode == "revisions": - contents = committed_contents[rel.as_posix()] - if is_binary_sample(contents): + selected_preview = committed_previews[rel.as_posix()] + if selected_preview is None: continue - text = contents.decode("utf-8", errors="ignore") - outline = structural_outline(path, text) - preview = fit_preview_lines( - select_preview_lines(outline or text.splitlines()), args.preview_bytes - ) + preview = selected_preview elif path.is_symlink(): continue elif path.is_file(): diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index 17401943..5eadf32a 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -260,6 +260,13 @@ describe("compact diff scan", () => { "--cacheinfo", `120000,${addedLink},src/added-link.py`, ); + git( + repository, + "update-index", + "--add", + "--cacheinfo", + `160000,${base},src/nested-module.py`, + ); git(repository, "commit", "-qm", "selected changes"); const head = git(repository, "rev-parse", "HEAD"); const output = join(root, "in-scope.txt"); From 367dee5cb0b78d0c866826765176b2de997657d7 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 12 Aug 2026 15:50:29 -0700 Subject: [PATCH 7/8] fix(scan): preserve revision paths as git posix names --- .../scripts/generate_in_scope_files.py | 2 +- .../scripts/generate_rank_input.py | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index b69c6b08..a1618b0b 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -141,7 +141,7 @@ def generate_diff_in_scope_files( changed = git_changed_paths(repository, base, head, mode) for path, status in changed: - relative = path.relative_to(repository) + relative = path if mode == "revisions" else path.relative_to(repository) if path_is_excluded(relative) or path.suffix.lower() not in TEXT_CODE_EXTENSIONS: continue if status != "D": diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 4005820a..6a8d6c5d 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -33,7 +33,7 @@ import sys from collections import Counter from collections.abc import Callable -from pathlib import Path +from pathlib import Path, PurePosixPath # Some plugin hosts launch Python with safe-path isolation enabled. sys.path.insert(0, str(Path(__file__).resolve().parent)) @@ -288,7 +288,7 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def path_is_excluded(path: Path) -> bool: +def path_is_excluded(path: Path | PurePosixPath) -> bool: if any(part in EXCLUDED_DIRS for part in path.parts): return True if path.name in EXCLUDED_FILENAMES: @@ -650,7 +650,7 @@ def git_changed_paths( mode: str, *, revision_blob_ids: dict[str, str] | None = None, -) -> list[tuple[Path, str]]: +) -> list[tuple[Path | PurePosixPath, str]]: if mode == "revisions": result = subprocess.run( [ @@ -668,7 +668,7 @@ def git_changed_paths( text=True, ) fields = result.stdout.split("\0") - changed: list[tuple[Path, str]] = [] + changed: list[tuple[Path | PurePosixPath, str]] = [] index = 0 while index < len(fields) - 1: metadata = fields[index].split() @@ -680,7 +680,7 @@ def git_changed_paths( index += 1 selected_mode = metadata[0].removeprefix(":") if status == "D" else metadata[1] if selected_mode in {"100644", "100755"}: - changed.append((repo / path, status)) + changed.append((PurePosixPath(path), status)) if revision_blob_ids is not None and status != "D": revision_blob_ids[path] = metadata[3] return changed @@ -723,7 +723,7 @@ def committed_blob_previews( previews[path] = None continue text = contents.decode("utf-8", errors="ignore") - outline = structural_outline(repo / path, text) + outline = structural_outline(PurePosixPath(path), text) previews[path] = fit_preview_lines( select_preview_lines(outline or text.splitlines()), preview_bytes ) @@ -750,16 +750,14 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: selected = [ (path, status) for path, status in changed - if not path_is_excluded(path.relative_to(repo)) + if not path_is_excluded(path if args.mode == "revisions" else path.relative_to(repo)) and path.suffix.lower() in TEXT_CODE_EXTENSIONS ] committed_previews = ( committed_blob_previews( repo, { - path.relative_to(repo).as_posix(): revision_blob_ids[ - path.relative_to(repo).as_posix() - ] + path.as_posix(): revision_blob_ids[path.as_posix()] for path, status in selected if status != "D" }, @@ -771,7 +769,7 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: rows: list[JsonRow] = [] for path, status in selected: - rel = path.relative_to(repo) + rel = path if args.mode == "revisions" else path.relative_to(repo) if status == "D": preview = "" elif args.mode == "revisions": From 8a5b2ec84f1b7319f19a4d4432f00508b925f689 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 12 Aug 2026 16:09:38 -0700 Subject: [PATCH 8/8] refactor(scan): narrow committed symlink inventory handling --- .../scripts/generate_in_scope_files.py | 39 ++++- .../scripts/generate_rank_input.py | 139 ++---------------- .../tests-ts/compact-diff-scan.test.ts | 66 +-------- 3 files changed, 48 insertions(+), 196 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index a1618b0b..f2a224ac 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -105,6 +105,39 @@ def generate_in_scope_files(repository: Path, scope: str, output: Path) -> int: return write_inventory(output, rows) +def committed_changed_paths(repository: Path, base: str, head: str) -> list[tuple[Path, str]]: + result = subprocess.run( + [ + "git", + "-C", + str(repository), + "diff", + "--raw", + "-z", + "--diff-filter=ACMRD", + f"{base}..{head}", + ], + capture_output=True, + text=True, + check=True, + ) + fields = result.stdout.split("\0") + changed: list[tuple[Path, str]] = [] + index = 0 + while index < len(fields) - 1: + metadata = fields[index].split() + status = metadata[-1][0] + index += 1 + if status in {"C", "R"}: + index += 1 + path = fields[index] + index += 1 + selected_mode = metadata[0].removeprefix(":") if status == "D" else metadata[1] + if selected_mode != "120000": + changed.append((repository / path, status)) + return changed + + def generate_diff_in_scope_files( repository: Path, base: str, @@ -114,7 +147,7 @@ def generate_diff_in_scope_files( ) -> int: """Reuse the existing diff selection without generating previews or duplicate worklists.""" sys.path.insert(0, str(Path(__file__).resolve().parent)) - from generate_rank_input import git_changed_paths, path_is_excluded, run_git_changed_paths + from generate_rank_input import path_is_excluded, run_git_changed_paths from rank_preview import ( DEFAULT_PREVIEW_BYTES, TEXT_CODE_EXTENSIONS, @@ -138,10 +171,10 @@ def generate_diff_in_scope_files( if relative ) else: - changed = git_changed_paths(repository, base, head, mode) + changed = committed_changed_paths(repository, base, head) for path, status in changed: - relative = path if mode == "revisions" else path.relative_to(repository) + relative = path.relative_to(repository) if path_is_excluded(relative) or path.suffix.lower() not in TEXT_CODE_EXTENSIONS: continue if status != "D": diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 6a8d6c5d..535fe341 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -33,19 +33,11 @@ import sys from collections import Counter from collections.abc import Callable -from pathlib import Path, PurePosixPath +from pathlib import Path # Some plugin hosts launch Python with safe-path isolation enabled. sys.path.insert(0, str(Path(__file__).resolve().parent)) -from rank_preview import ( - DEFAULT_PREVIEW_BYTES, - TEXT_CODE_EXTENSIONS, - fit_preview_lines, - is_binary_sample, - preview_for, - select_preview_lines, - structural_outline, -) +from rank_preview import DEFAULT_PREVIEW_BYTES, TEXT_CODE_EXTENSIONS, preview_for from workbench_target import git_directory_snapshot_paths EXCLUDED_DIRS = { @@ -288,7 +280,7 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def path_is_excluded(path: Path | PurePosixPath) -> bool: +def path_is_excluded(path: Path) -> bool: if any(part in EXCLUDED_DIRS for part in path.parts): return True if path.name in EXCLUDED_FILENAMES: @@ -643,47 +635,9 @@ def run_git_changed_paths(repo: Path, diff_args: list[str]) -> list[tuple[Path, return changed -def git_changed_paths( - repo: Path, - base: str, - head: str, - mode: str, - *, - revision_blob_ids: dict[str, str] | None = None, -) -> list[tuple[Path | PurePosixPath, str]]: +def git_changed_paths(repo: Path, base: str, head: str, mode: str) -> list[tuple[Path, str]]: if mode == "revisions": - result = subprocess.run( - [ - "git", - "-C", - str(repo), - "diff", - "--raw", - "-z", - "--diff-filter=ACMRD", - f"{base}..{head}", - ], - check=True, - capture_output=True, - text=True, - ) - fields = result.stdout.split("\0") - changed: list[tuple[Path | PurePosixPath, str]] = [] - index = 0 - while index < len(fields) - 1: - metadata = fields[index].split() - status = metadata[-1][0] - index += 1 - if status in {"C", "R"}: - index += 1 - path = fields[index] - index += 1 - selected_mode = metadata[0].removeprefix(":") if status == "D" else metadata[1] - if selected_mode in {"100644", "100755"}: - changed.append((PurePosixPath(path), status)) - if revision_blob_ids is not None and status != "D": - revision_blob_ids[path] = metadata[3] - return changed + return run_git_changed_paths(repo, [f"{base}..{head}"]) if mode == "local-patch": unstaged = run_git_changed_paths(repo, [base]) staged = run_git_changed_paths(repo, ["--cached", base]) @@ -693,92 +647,19 @@ def git_changed_paths( raise SystemExit(f"Unknown diff mode: {mode}") -def committed_blob_previews( - repo: Path, blob_ids: dict[str, str], preview_bytes: int -) -> dict[str, str | None]: - if not blob_ids: - return {} - - previews: dict[str, str | None] = {} - with subprocess.Popen( - ["git", "-C", str(repo), "cat-file", "--batch"], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) as process: - assert process.stdin is not None - assert process.stdout is not None - assert process.stderr is not None - for path, object_id in blob_ids.items(): - process.stdin.write(f"{object_id}\n".encode("ascii")) - process.stdin.flush() - header = process.stdout.readline().split() - if len(header) != 3 or header[1] != b"blob": - raise ValueError("Git returned an unexpected object for a changed source file") - size = int(header[2]) - contents = process.stdout.read(size) - if len(contents) != size or process.stdout.read(1) != b"\n": - raise ValueError("Git returned incomplete changed source contents") - if is_binary_sample(contents): - previews[path] = None - continue - text = contents.decode("utf-8", errors="ignore") - outline = structural_outline(PurePosixPath(path), text) - previews[path] = fit_preview_lines( - select_preview_lines(outline or text.splitlines()), preview_bytes - ) - process.stdin.close() - stderr = process.stderr.read() - if (returncode := process.wait()) != 0: - raise subprocess.CalledProcessError(returncode, process.args, stderr=stderr) - return previews - - def make_diff_rank_input(args: argparse.Namespace) -> None: repo = Path(args.repo).expanduser().resolve() if not repo.is_dir(): raise SystemExit(f"Repo path not found: {repo}") - revision_blob_ids: dict[str, str] = {} - changed = git_changed_paths( - repo, - args.base, - args.head, - args.mode, - revision_blob_ids=revision_blob_ids, - ) - selected = [ - (path, status) - for path, status in changed - if not path_is_excluded(path if args.mode == "revisions" else path.relative_to(repo)) - and path.suffix.lower() in TEXT_CODE_EXTENSIONS - ] - committed_previews = ( - committed_blob_previews( - repo, - { - path.as_posix(): revision_blob_ids[path.as_posix()] - for path, status in selected - if status != "D" - }, - args.preview_bytes, - ) - if args.mode == "revisions" - else {} - ) - rows: list[JsonRow] = [] - for path, status in selected: - rel = path if args.mode == "revisions" else path.relative_to(repo) + for path, status in git_changed_paths(repo, args.base, args.head, args.mode): + rel = path.relative_to(repo) + if path_is_excluded(rel) or path.suffix.lower() not in TEXT_CODE_EXTENSIONS: + continue + if status == "D": preview = "" - elif args.mode == "revisions": - selected_preview = committed_previews[rel.as_posix()] - if selected_preview is None: - continue - preview = selected_preview - elif path.is_symlink(): - continue elif path.is_file(): preview, is_binary = preview_for(path, args.preview_bytes) if is_binary: diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index 5eadf32a..cadcbe05 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -204,37 +204,9 @@ describe("compact diff scan", () => { "src/handler.py", "src/new handler.py", ]); - - const rankInput = join(root, "rank-input.jsonl"); - const legacyResult = python( - "generate_rank_input.py", - "make-diff-rank-input", - "--repo", - repository, - "--base", - base, - "--head", - head, - "--out", - rankInput, - ); - - expect(legacyResult.status, legacyResult.stderr).toBe(0); - const rows = readFileSync(rankInput, "utf8") - .trim() - .split("\n") - .map((row) => JSON.parse(row)); - expect(rows.map((row) => row.path)).toEqual([ - "src/guard.py", - "src/handler.py", - "src/new handler.py", - ]); - expect(rows.find((row) => row.path === "src/handler.py")?.preview).toBe( - "value = 2", - ); }); - test("excludes committed symlinks using their selected revision modes", () => { + test("omits committed symlinks from the revision inventory", () => { const { root, repository } = createRepository(); writeSource(repository, "src/handler.py", "value = 1\n"); writeSource(repository, "src/deleted-link.py", "handler.py"); @@ -260,13 +232,6 @@ describe("compact diff scan", () => { "--cacheinfo", `120000,${addedLink},src/added-link.py`, ); - git( - repository, - "update-index", - "--add", - "--cacheinfo", - `160000,${base},src/nested-module.py`, - ); git(repository, "commit", "-qm", "selected changes"); const head = git(repository, "rev-parse", "HEAD"); const output = join(root, "in-scope.txt"); @@ -286,34 +251,7 @@ describe("compact diff scan", () => { ); expect(result.status, result.stderr).toBe(0); - expect(readFileSync(output, "utf8").split("\n").filter(Boolean)).toEqual([ - "src/handler.py", - ]); - expect(readFileSync(join(repository, "src", "added-link.py"), "utf8")).toBe( - "handler.py", - ); - - const rankInput = join(root, "rank-input.jsonl"); - const legacyResult = python( - "generate_rank_input.py", - "make-diff-rank-input", - "--repo", - repository, - "--base", - base, - "--head", - head, - "--out", - rankInput, - ); - - expect(legacyResult.status, legacyResult.stderr).toBe(0); - expect( - readFileSync(rankInput, "utf8") - .trim() - .split("\n") - .map((row) => JSON.parse(row).path), - ).toEqual(["src/handler.py"]); + expect(readFileSync(output, "utf8").trim()).toBe("src/handler.py"); }); test("includes staged, unstaged, and untracked working-tree changes", () => {