From e762ac40eb92c25c83eb481c5d15293eb4dcbb89 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Tue, 8 Sep 2026 23:52:34 +0000 Subject: [PATCH 1/4] refactor(plugin): port deep-review worklists to TypeScript --- .../codex-security/mcp-app/helpers-main.ts | 8 +- .../mcp-app/src/helpers/deep-review-input.ts | 277 +++++++++++ .../mcp-app/src/helpers/helper-files.ts | 47 +- plugins/codex-security/native/README.md | 2 +- .../native/examples/windows-wide-launcher.rs | 51 ++- .../scripts/generate_rank_input.py | 53 --- .../skills/finding-discovery/SKILL.md | 2 +- .../references/scan-artifacts-and-ledger.md | 2 +- .../tests/test_generate_rank_input.py | 125 ----- .../tests-ts/deep-review-input.test.ts | 430 ++++++++++++++++++ 10 files changed, 813 insertions(+), 184 deletions(-) create mode 100644 plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts create mode 100644 sdk/typescript/tests-ts/deep-review-input.test.ts diff --git a/plugins/codex-security/mcp-app/helpers-main.ts b/plugins/codex-security/mcp-app/helpers-main.ts index 8aad95453..63920a951 100644 --- a/plugins/codex-security/mcp-app/helpers-main.ts +++ b/plugins/codex-security/mcp-app/helpers-main.ts @@ -4,6 +4,7 @@ import { decodePosixBytes } from "./src/helpers/posix-path"; import { windowsBinding } from "./src/native"; import { normalizeCandidatesCommand } from "./src/helpers/normalize-candidates"; import { validatePatchRiskAssessmentCommand } from "./src/helpers/validate-patch-risk-assessment"; +import { deepReviewInputCommand } from "./src/helpers/deep-review-input"; let commandLine = process.argv.slice(2); if (process.platform === "win32") { @@ -35,9 +36,14 @@ if (command === "resolve-security-md") { process.exitCode = normalizeCandidatesCommand(args, posixHome); } else if (command === "validate-patch-risk-assessment") { process.exitCode = validatePatchRiskAssessmentCommand(args); +} else if ( + command === "copy-deep-review-input" || + command === "select-deep-review-input" +) { + process.exitCode = deepReviewInputCommand(command, args, posixHome); } else { console.error( - "Usage: launch_codex_security_mcp[.cmd] --helper [options]", + "Usage: launch_codex_security_mcp[.cmd] --helper [options]", ); process.exitCode = 2; } diff --git a/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts b/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts new file mode 100644 index 000000000..7466b5844 --- /dev/null +++ b/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts @@ -0,0 +1,277 @@ +import { decodeUtf8 } from "./utf8"; +import { dirname } from "node:path"; +import { exists, mkdir, readFile, writeFile } from "./helper-files"; +import { encodePosixPath } from "./posix-path"; +import { JsonSyntaxError, object, parseJson, pythonRepr } from "./python-json"; +import { expandHome, parsedPath } from "./resolve-security-md"; + +type Command = "copy-deep-review-input" | "select-deep-review-input"; +interface RankRow { + path: string; + area: string; + score?: bigint; + include?: boolean; +} +const trim = (value: string) => + value.replace( + /^[\p{White_Space}\u001c-\u001f]+|[\p{White_Space}\u001c-\u001f]+$/gu, + "", + ); + +function compare(left: string, right: string): number { + const a = Array.from(left, (character) => character.codePointAt(0)!); + const b = Array.from(right, (character) => character.codePointAt(0)!); + for (let index = 0; index < Math.min(a.length, b.length); index++) { + if (a[index] !== b[index]) return a[index]! - b[index]!; + } + return a.length - b.length; +} + +function loadRows(path: string, selection: boolean): RankRow[] { + const label = selection ? "Rank output" : "Rank input"; + if (!exists(path)) throw new Error(`${label} missing: ${path}`); + const contents = decodeUtf8(readFile(path)); + const lines = contents === "" ? [] : contents.split(/\r\n|[\r\n]/u); + if (lines.at(-1) === "") lines.pop(); + const fields = selection + ? ["path", "area", "score", "include", "reason"] + : ["path", "area", "preview"]; + const rows = lines.map((line, index) => { + const fail = (message: string): never => { + throw new Error(`${path}:${index + 1}: ${message}`); + }; + if (trim(line) === "") fail("blank JSONL rows are not allowed"); + let row: unknown; + try { + row = parseJson(line); + } catch (error) { + if (error instanceof JsonSyntaxError) + fail( + `invalid JSON: ${error.message.replace(/: line \d+ column \d+ \(char \d+\)$/u, "")}`, + ); + throw error; + } + if (!object(row)) return fail("expected a JSON object"); + const missing = fields + .filter((field) => !Object.hasOwn(row, field)) + .sort(compare); + const unexpected = Object.keys(row) + .filter((field) => !fields.includes(field)) + .sort(compare); + const details: string[] = []; + if (missing.length) details.push(`missing fields ${pythonRepr(missing)}`); + if (unexpected.length) + details.push(`unexpected fields ${pythonRepr(unexpected)}`); + if (details.length) fail(details.join("; ")); + for (const field of selection + ? ["path", "area"] + : ["path", "area", "preview"]) { + if ( + typeof row[field] !== "string" || + (field === "path" && trim(row[field]) === "") + ) + fail( + `${field} must be ${field === "path" ? "a non-empty string" : "a string"}`, + ); + } + if (selection) { + if (typeof row.score !== "bigint") + fail("score must be an integer from 1 through 10"); + if ((row.score as bigint) < 1n || (row.score as bigint) > 10n) + fail("score must be from 1 through 10"); + if (typeof row.include !== "boolean") fail("include must be a boolean"); + if (typeof row.reason !== "string" || trim(row.reason) === "") + fail("reason must be a non-empty string"); + } + return row as unknown as RankRow; + }); + const seen = new Set(), + duplicates = new Set(); + for (const row of rows) { + if (seen.has(row.path)) duplicates.add(row.path); + seen.add(row.path); + } + if (duplicates.size) + throw new Error( + `${label} contains duplicate paths: ${pythonRepr([...duplicates].sort(compare))}`, + ); + return rows; +} + +function writeRows(output: string, rows: RankRow[]): void { + mkdir(dirname(output)); + function* contents(): Iterable { + for (const row of rows) { + const json = JSON.stringify({ path: row.path, area: row.area }).replace( + /[\u007f-\uffff]/g, + (character) => + `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`, + ); + yield Buffer.from(json + (process.platform === "win32" ? "\r\n" : "\n")); + } + } + writeFile(output, contents()); +} + +class ArgumentError extends Error {} +function integer(value: string): bigint { + const text = value.replace(/^\p{White_Space}+|\p{White_Space}+$/gu, ""); + if (!/^[+-]?\p{Decimal_Number}+(?:_\p{Decimal_Number}+)*$/u.test(text)) + throw new ArgumentError( + `argument --top-percent: invalid int value: ${pythonRepr(value)}`, + ); + return BigInt( + Array.from(text.replaceAll("_", ""), (character) => { + if (!/\p{Decimal_Number}/u.test(character)) return character; + const point = character.codePointAt(0)!; + let start = point; + while (/\p{Decimal_Number}/u.test(String.fromCodePoint(start - 1))) + start--; + return String((point - start) % 10); + }).join(""), + ); +} + +function argumentsFor( + args: string[], + selection: boolean, +): Record { + const input = selection ? "rank-output" : "rank-input"; + const names = [input, "out", "help", ...(selection ? ["top-percent"] : [])]; + const values: Record = {}; + const extra: string[] = []; + const looksOptional = (arg: string) => + arg.startsWith("-") && + arg !== "-" && + !arg.includes(" ") && + !/^-(?:\p{Decimal_Number}+|\p{Decimal_Number}*\.\p{Decimal_Number}+)\n?$/u.test( + arg, + ); + for (let index = 0; index < args.length; index++) { + const arg = args[index]!; + if (arg === "--") { + extra.push(...args.slice(index)); + break; + } + const equals = arg.indexOf("="); + const option = equals === -1 ? arg : arg.slice(0, equals); + const matches = option.startsWith("--") + ? names.filter((name) => `--${name}`.startsWith(option)) + : []; + const name = + names.find((name) => option === `--${name}`) ?? + (arg.startsWith("-h") + ? "help" + : matches.length === 1 + ? matches[0] + : undefined); + if (matches.length > 1 && !name) + throw new ArgumentError( + `ambiguous option: ${arg} could match ${matches.map((name) => `--${name}`).join(", ")}`, + ); + if (!name) { + extra.push(arg); + continue; + } + if (name === "help") { + if (equals !== -1) + throw new ArgumentError( + `argument -h/--help: ignored explicit argument ${pythonRepr(arg.slice(equals + 1))}`, + ); + return { help: true }; + } + let value: string; + if (equals !== -1) value = arg.slice(equals + 1); + else { + if (index + 1 === args.length || looksOptional(args[index + 1]!)) + throw new ArgumentError(`argument --${name}: expected one argument`); + value = args[++index]!; + } + values[name] = name === "top-percent" ? integer(value) : value; + } + const missing = [input, "out"].filter((name) => values[name] === undefined); + if (missing.length) + throw new ArgumentError( + `the following arguments are required: ${missing.map((name) => `--${name}`).join(", ")}`, + ); + if (extra.length) + throw new ArgumentError(`unrecognized arguments: ${extra.join(" ")}`); + return values; +} + +function print(message: string, stderr = false): void { + let text = message + "\n"; + if (stderr) + text = text.replace( + /[\ud800-\udfff]/gu, + (character) => + `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`, + ); + (stderr ? process.stderr : process.stdout).write( + process.platform === "win32" + ? text.replaceAll("\n", "\r\n") + : stderr + ? text + : encodePosixPath(text), + ); +} + +export function deepReviewInputCommand( + command: Command, + args: string[], + posixHome = process.env.HOME, +): number { + const selection = command === "select-deep-review-input"; + const input = selection ? "rank-output" : "rank-input"; + const usage = `usage: launch_codex_security_mcp[.cmd] --helper ${command} [-h] --${input} PATH --out PATH${selection ? " [--top-percent INT]" : ""}`; + try { + const values = argumentsFor(args, selection); + if (values.help) { + print( + `${usage}\n\nCreate deep_review_input.jsonl from ${selection ? "worker-produced rank_output.jsonl" : "rank_input.jsonl"}.\n\noptions:\n -h, --help show this help message and exit\n --${input} PATH ${selection ? "Worker ranking output" : "Deterministic rank input"} JSONL.\n --out PATH Output deep_review_input.jsonl path.${selection ? "\n --top-percent INT Percent of included files to keep for deep review. Defaults to 100." : ""}`, + ); + return 0; + } + const path = (name: string) => + parsedPath(expandHome(parsedPath(values[name] as string), posixHome)); + const rows = loadRows(path(input), selection); + let selected = rows, + total = rows.length; + if (selection) { + const included = rows.filter((row) => row.include); + const base = included.length ? included : rows; + base.sort( + (left, right) => + Number(right.score! - left.score!) || compare(left.path, right.path), + ); + total = base.length; + let keep = 0; + if (total) { + const percent = Number(values["top-percent"] ?? 100n); + if (!Number.isFinite(percent)) + throw new Error("int too large to convert to float"); + const count = total * (percent / 100); + if (!Number.isFinite(count)) + throw new Error("cannot convert float infinity to integer"); + keep = Math.max(1, Math.trunc(count)); + } + selected = base.slice(0, keep); + } + const output = path("out"); + writeRows(output, selected); + const message = selection + ? `Selected ${selected.length} of ${total} rows into ${output}` + : `Copied ${selected.length} rows into ${output}`; + print(message); + return 0; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + print( + error instanceof ArgumentError + ? `${usage}\n${command}: error: ${message}` + : message, + true, + ); + return error instanceof ArgumentError ? 2 : 1; + } +} diff --git a/plugins/codex-security/mcp-app/src/helpers/helper-files.ts b/plugins/codex-security/mcp-app/src/helpers/helper-files.ts index 37ddcf4fd..cdec4be24 100644 --- a/plugins/codex-security/mcp-app/src/helpers/helper-files.ts +++ b/plugins/codex-security/mcp-app/src/helpers/helper-files.ts @@ -1,4 +1,11 @@ -import { readFileSync } from "node:fs"; +import { + closeSync, + existsSync, + mkdirSync, + openSync, + readFileSync, + writeFileSync, +} from "node:fs"; import { windowsBinding } from "../native"; import { widePath, windowsFileSystem } from "../../../native/windows-files.mjs"; import { encodePosixPath } from "./posix-path"; @@ -9,3 +16,41 @@ export function readFile(path: string | number): Buffer { ? windowsFileSystem(windowsBinding()).readFile(widePath(path)) : readFileSync(encodePosixPath(path)); } + +export function exists(path: string): boolean { + if (process.platform !== "win32") return existsSync(encodePosixPath(path)); + try { + windowsFileSystem(windowsBinding()).stat(widePath(path)); + return true; + } catch (error) { + const { code, winerror } = error as NodeJS.ErrnoException & { + winerror?: number; + }; + if ( + ["ENOENT", "ENOTDIR", "ELOOP"].includes(code ?? "") || + winerror === 21 || + winerror === 123 + ) + return false; + throw error; + } +} + +export function mkdir(path: string): void { + if (process.platform === "win32") + windowsFileSystem(windowsBinding()).mkdir(widePath(path)); + else mkdirSync(encodePosixPath(path), { recursive: true }); +} + +export function writeFile(path: string, chunks: Iterable): void { + if (process.platform === "win32") { + windowsFileSystem(windowsBinding()).writeFile(widePath(path), chunks); + return; + } + const descriptor = openSync(encodePosixPath(path), "w"); + try { + for (const chunk of chunks) writeFileSync(descriptor, chunk); + } finally { + closeSync(descriptor); + } +} diff --git a/plugins/codex-security/native/README.md b/plugins/codex-security/native/README.md index 50034bb04..585aa5c91 100644 --- a/plugins/codex-security/native/README.md +++ b/plugins/codex-security/native/README.md @@ -39,7 +39,7 @@ Windows uses `windows-binding.mts` and the same Rust crate. `WindowsHandle` owns The binding exposes synchronous file and directory creation, attributes and reparse tags, identity and final/opened names, read/write/seek/size/EOF/flush, exact-handle rename and deletion, and exclusive whole-file locking. Rust's `File` supplies ordinary I/O, cursor-preserving truncation, `sync_all` for flush, and locks. Calls return numeric Windows errors, including 6 for closed handles and 33 for nonblocking lock contention. Buffer ranges, path encoding, and 64-bit seek arguments are checked before use. Overlapped handles are unsupported because pending operations could retain native buffers beyond the call. Path authorization, ancestor traversal, and reparse-point policy remain the caller's responsibility. -Five additional operations preserve Windows strings at the Node boundary. `windowsArguments` returns the complete OS argument vector, including the executable and Node options, using Rust's CRT-compatible parser. `windowsEnvironment` reads one wide environment name and distinguishes an absent value (`null`) from an empty buffer. `windowsAbsolutePath` resolves against the native current directory and drive directories without requiring the destination to exist. `windowsDirectoryEntries` uses `std::fs::read_dir` and cached `DirEntry::file_type()` values without opening each child; names remain UTF-16LE, and construction or iteration failures return their numeric Windows error and an empty array. Directory symlinks and junctions have both directory and symbolic-link flags. The typed adapter exposes this enumerator through `entriesWithTypes`, which `resolve-security-md --list` uses on Windows. `windowsReadLink` returns a UTF-16LE link target or its numeric Windows error; candidate normalization uses it to resolve missing paths without losing raw filenames. Assessment validation shares the typed wide-path reader. +Five additional operations preserve Windows strings at the Node boundary. `windowsArguments` returns the complete OS argument vector, including the executable and Node options, using Rust's CRT-compatible parser. `windowsEnvironment` reads one wide environment name and distinguishes an absent value (`null`) from an empty buffer. `windowsAbsolutePath` resolves against the native current directory and drive directories without requiring the destination to exist. `windowsDirectoryEntries` uses `std::fs::read_dir` and cached `DirEntry::file_type()` values without opening each child; names remain UTF-16LE, and construction or iteration failures return their numeric Windows error and an empty array. Directory symlinks and junctions have both directory and symbolic-link flags. The typed adapter exposes this enumerator through `entriesWithTypes`, which `resolve-security-md --list` uses on Windows. `windowsReadLink` returns a UTF-16LE link target or its numeric Windows error; candidate normalization uses it to resolve missing paths without losing raw filenames. Assessment validation and deep-review worklists share the typed wide-path reader and writer. `windows-files.mts` leaves ordinary absolute-path resolution and canonicalization to `GetFullPathNameW` and `GetFinalPathNameByHandleW`, trimming trailing separators below the root. Its small verbatim-path normalizer preserves drive and UNC share roots when resolving dot segments, including literal trailing dots and spaces. Non-strict `realpath` can retain unresolved components; callers must check containment independently. It also supports missing output paths. `stat(path, false)` retains exact symbolic-link and reparse-point metadata so callers can reject junction traversal independently of the enumerator's link label. The SDK's public runtime floor remains Node 22.13.0. Node 20.0.0 is an additional native-foundation compatibility proof; it does not change the SDK engine requirement. diff --git a/plugins/codex-security/native/examples/windows-wide-launcher.rs b/plugins/codex-security/native/examples/windows-wide-launcher.rs index 4f8fd74aa..8fd307e1d 100644 --- a/plugins/codex-security/native/examples/windows-wide-launcher.rs +++ b/plugins/codex-security/native/examples/windows-wide-launcher.rs @@ -376,7 +376,56 @@ fn main() -> std::io::Result<()> { { return Err(io::Error::other("Assessment validation changed its input")); } - println!("{{\"policyHelperRawPaths\":true,\"candidateHelperRawPaths\":true,\"assessmentHelperRawPaths\":true,\"directoryIdentity\":true}}"); + let worklist_dir = raw("worklist-", 0xdc80); + let worklist_output = repo.join(&worklist_dir).join(&output_name); + let worklist_sentinel = repo + .join(raw("worklist-", 0xfffd)) + .join(&replacement_output); + fs::create_dir_all(worklist_sentinel.parent().unwrap())?; + fs::write(&worklist_sentinel, "worklist output sentinel")?; + for (command, input_flag, row) in [ + ( + "copy-deep-review-input", + "--rank-input", + "{\"path\":\"source.py\",\"area\":\"src\",\"preview\":\"source line\"}\n", + ), + ( + "select-deep-review-input", + "--rank-output", + "{\"path\":\"source.py\",\"area\":\"src\",\"score\":5,\"include\":true,\"reason\":\"source line\"}\n", + ), + ] { + fs::write(repo.join(&input_name), row)?; + for prefix in [repo.clone(), PathBuf::from("~"), PathBuf::from(".")] { + let child = Command::new(&node) + .arg(&script) + .args(["--helper", command, input_flag]) + .arg(prefix.join(&input_name)) + .arg("--out") + .arg(prefix.join(&worklist_dir).join(&output_name)) + .current_dir(&repo) + .env("USERPROFILE", &repo) + .output()?; + if !child.status.success() + || !child.stderr.is_empty() + || fs::read(&worklist_output)? != b"{\"path\":\"source.py\",\"area\":\"src\"}\r\n" + { + return Err(io::Error::other(format!( + "Wide deep-review helper failed: {}", + String::from_utf8_lossy(&child.stderr) + ))); + } + fs::remove_file(&worklist_output)?; + } + } + if fs::read(&worklist_sentinel)? != b"worklist output sentinel" + || fs::read(repo.join(raw("input-", 0xfffd)))? != b"invalid replacement input" + { + return Err(io::Error::other( + "Deep-review helper changed a replacement path", + )); + } + println!("{{\"policyHelperRawPaths\":true,\"candidateHelperRawPaths\":true,\"assessmentHelperRawPaths\":true,\"deepReviewHelperRawPaths\":true,\"directoryIdentity\":true}}"); Ok(()) } diff --git a/plugins/codex-security/scripts/generate_rank_input.py b/plugins/codex-security/scripts/generate_rank_input.py index 221e3d7c8..a9cfa2add 100644 --- a/plugins/codex-security/scripts/generate_rank_input.py +++ b/plugins/codex-security/scripts/generate_rank_input.py @@ -17,9 +17,6 @@ coordinator accepts it. - `validate-rank-pool` validates the pool plan and every assigned shard output. - `merge-rank-outputs` validates and combines worker-local shard outputs. -- `copy-deep-review-input` copies every candidate into the deep-review worklist - for exhaustive mode. -- `select-deep-review-input` selects the ranked rows for deep review. """ from __future__ import annotations @@ -263,25 +260,6 @@ def parse_args() -> argparse.Namespace: merge.add_argument("--shard-dir", required=True, help="Directory of input and output shards.") merge.add_argument("--out", required=True, help="Output rank_output.jsonl path.") - copy = subparsers.add_parser( - "copy-deep-review-input", - help="Create deep_review_input.jsonl directly from rank_input.jsonl.", - ) - copy.add_argument("--rank-input", required=True, help="Deterministic rank input JSONL.") - copy.add_argument("--out", required=True, help="Output deep_review_input.jsonl path.") - - select = subparsers.add_parser( - "select-deep-review-input", - help="Create deep_review_input.jsonl from worker-produced rank_output.jsonl.", - ) - select.add_argument("--rank-output", required=True, help="Worker ranking output JSONL.") - select.add_argument("--out", required=True, help="Output deep_review_input.jsonl path.") - select.add_argument( - "--top-percent", - type=int, - default=100, - help="Percent of included files to keep for deep review.", - ) return parser.parse_args() @@ -1132,33 +1110,6 @@ def merge_rank_outputs(args: argparse.Namespace) -> None: print(f"Merged {len(merged)} ranking rows into {output}") -def copy_deep_review_input(args: argparse.Namespace) -> None: - rank_input = Path(args.rank_input).expanduser() - rows = load_jsonl(rank_input, "Rank input", validate_rank_input_row) - require_unique_paths(rows, "Rank input") - selected = [{"path": row["path"], "area": row["area"]} for row in rows] - - output = Path(args.out).expanduser() - write_jsonl(output, selected) - print(f"Copied {len(selected)} rows into {output}") - - -def select_deep_review_input(args: argparse.Namespace) -> None: - rank_output = Path(args.rank_output).expanduser() - rows = load_jsonl(rank_output, "Rank output", validate_rank_output_row) - require_unique_paths(rows, "Rank output") - - included = [row for row in rows if row["include"]] - base_rows = included if included else rows - base_rows.sort(key=lambda row: (-int(row["score"]), str(row["path"]))) - keep = max(1, int(len(base_rows) * (args.top_percent / 100.0))) if base_rows else 0 - selected = [{"path": row["path"], "area": row["area"]} for row in base_rows[:keep]] - - output = Path(args.out).expanduser() - write_jsonl(output, selected) - print(f"Selected {len(selected)} of {len(base_rows)} rows into {output}") - - def main() -> None: args = parse_args() if args.command == "make-repo-rank-input": @@ -1181,10 +1132,6 @@ def main() -> None: validate_rank_pool_command(args) elif args.command == "merge-rank-outputs": merge_rank_outputs(args) - elif args.command == "copy-deep-review-input": - copy_deep_review_input(args) - elif args.command == "select-deep-review-input": - select_deep_review_input(args) else: raise SystemExit(f"Unknown command: {args.command}") diff --git a/plugins/codex-security/skills/finding-discovery/SKILL.md b/plugins/codex-security/skills/finding-discovery/SKILL.md index 5ce12c24c..0a337fd9a 100644 --- a/plugins/codex-security/skills/finding-discovery/SKILL.md +++ b/plugins/codex-security/skills/finding-discovery/SKILL.md @@ -31,7 +31,7 @@ For a targeted code diff without an existing compact inventory: - Read `../security-scan/references/scan-artifacts-and-ledger.md`. - Generate `rank_input.jsonl` deterministically from changed source-like files with ` /scripts/generate_rank_input.py make-diff-rank-input --repo --base --mode revisions --head --out /rank_input.jsonl` for PR, commit, and branch diffs, or ` /scripts/generate_rank_input.py make-diff-rank-input --repo --base --mode local-patch --out /rank_input.jsonl` for a local patch. -- Copy every diff row into `deep_review_input.jsonl` with ` /scripts/generate_rank_input.py copy-deep-review-input --rank-input /rank_input.jsonl --out /deep_review_input.jsonl`. Diff scans do not rank or drop changed files before deep review. +- Copy every diff row into `deep_review_input.jsonl` with `/scripts/launch_codex_security_mcp[.cmd] --helper copy-deep-review-input --rank-input /rank_input.jsonl --out /deep_review_input.jsonl`. Diff scans do not rank or drop changed files before deep review. - Add directly supporting files required to understand the changed security behavior only when repository evidence shows they are needed. Do not use them to broaden into unrelated repository-wide enumeration. - Deep-review every file in `deep_review_input.jsonl` using the shared scoped file-review rules. - Stay anchored to the changed code and directly supporting files. Unchanged siblings are context or negative controls unless the diff newly reaches them, weakens their shared control, or changes a shared sink/helper they depend on. diff --git a/plugins/codex-security/skills/security-scan/references/scan-artifacts-and-ledger.md b/plugins/codex-security/skills/security-scan/references/scan-artifacts-and-ledger.md index cd2face07..a541da2d7 100644 --- a/plugins/codex-security/skills/security-scan/references/scan-artifacts-and-ledger.md +++ b/plugins/codex-security/skills/security-scan/references/scan-artifacts-and-ledger.md @@ -56,7 +56,7 @@ The parent agent must reconcile validation and attack-path subagent outputs befo ## Scoped Deep Review - Use `deep_review_input.jsonl` as the canonical changed-file review worklist for diff scans. -- For diff-scoped scans, generate `rank_input.jsonl` deterministically from changed source-like files with ` /scripts/generate_rank_input.py make-diff-rank-input --repo --base --mode revisions --head --out /rank_input.jsonl` for PR, commit, and branch diffs, or ` /scripts/generate_rank_input.py make-diff-rank-input --repo --base --mode local-patch --out /rank_input.jsonl` for a local patch, then copy every row into `deep_review_input.jsonl` with ` /scripts/generate_rank_input.py copy-deep-review-input --rank-input /rank_input.jsonl --out /deep_review_input.jsonl`. +- For diff-scoped scans, generate `rank_input.jsonl` deterministically from changed source-like files with ` /scripts/generate_rank_input.py make-diff-rank-input --repo --base --mode revisions --head --out /rank_input.jsonl` for PR, commit, and branch diffs, or ` /scripts/generate_rank_input.py make-diff-rank-input --repo --base --mode local-patch --out /rank_input.jsonl` for a local patch, then copy every row into `deep_review_input.jsonl` with `/scripts/launch_codex_security_mcp[.cmd] --helper copy-deep-review-input --rank-input /rank_input.jsonl --out /deep_review_input.jsonl`. - Diff-scoped scans do not rank or drop changed files before deep review. Every row in diff `rank_input.jsonl` must be copied into `deep_review_input.jsonl` and receive a full-file review receipt. - Add directly supporting files required to understand the changed security behavior only when repository evidence shows they are needed; record the add-back reason in the work ledger or per-file result. - Deep-review every file selected into `deep_review_input.jsonl`. diff --git a/plugins/codex-security/tests/test_generate_rank_input.py b/plugins/codex-security/tests/test_generate_rank_input.py index 4858f9c1a..cb7dce59e 100644 --- a/plugins/codex-security/tests/test_generate_rank_input.py +++ b/plugins/codex-security/tests/test_generate_rank_input.py @@ -860,121 +860,6 @@ def test_make_rank_input_decodes_bom_marked_utf16_source(tmp_path: Path, mode: s assert {row["path"]: row["preview"] for row in read_jsonl(output)} == expected -def test_copy_and_select_deep_review_inputs(tmp_path: Path) -> None: - rank_input = tmp_path / "rank_input.jsonl" - write_jsonl( - rank_input, - [ - {"path": "a.py", "area": "core", "preview": "a"}, - {"path": "b.py", "area": "api", "preview": "b"}, - ], - ) - copied = tmp_path / "copied.jsonl" - run_cli( - "copy-deep-review-input", - "--rank-input", - str(rank_input), - "--out", - str(copied), - ) - assert read_jsonl(copied) == [ - {"path": "a.py", "area": "core"}, - {"path": "b.py", "area": "api"}, - ] - - rank_output = tmp_path / "rank_output.jsonl" - write_jsonl( - rank_output, - [ - {"path": "c.py", "area": "api", "score": 8, "include": True, "reason": "c"}, - {"path": "a.py", "area": "core", "score": 10, "include": True, "reason": "a"}, - {"path": "b.py", "area": "api", "score": 8, "include": True, "reason": "b"}, - {"path": "d.py", "area": "core", "score": 2, "include": False, "reason": "d"}, - ], - ) - selected = tmp_path / "selected.jsonl" - run_cli( - "select-deep-review-input", - "--rank-output", - str(rank_output), - "--top-percent", - "67", - "--out", - str(selected), - ) - assert read_jsonl(selected) == [ - {"path": "a.py", "area": "core"}, - {"path": "b.py", "area": "api"}, - ] - - -def test_select_honors_explicit_top_percent_20(tmp_path: Path) -> None: - rank_output = tmp_path / "rank_output.jsonl" - rows = make_rank_rows(5) - write_jsonl( - rank_output, - [rank_result(row, score=10 - index) for index, row in enumerate(rows)], - ) - selected = tmp_path / "selected.jsonl" - - run_cli( - "select-deep-review-input", - "--rank-output", - str(rank_output), - "--top-percent", - "20", - "--out", - str(selected), - ) - - assert read_jsonl(selected) == [{"path": "src/file_00.py", "area": "src"}] - - -def test_select_defaults_to_top_percent_100(tmp_path: Path) -> None: - rank_output = tmp_path / "rank_output.jsonl" - rows = make_rank_rows(5) - write_jsonl( - rank_output, - [rank_result(row, score=10 - index) for index, row in enumerate(rows)], - ) - selected = tmp_path / "selected.jsonl" - - run_cli( - "select-deep-review-input", - "--rank-output", - str(rank_output), - "--out", - str(selected), - ) - - assert len(read_jsonl(selected)) == 5 - - -def test_select_falls_back_to_all_rows_when_workers_exclude_everything(tmp_path: Path) -> None: - rank_output = tmp_path / "rank_output.jsonl" - write_jsonl( - rank_output, - [ - {"path": "b.py", "area": "api", "score": 2, "include": False, "reason": "b"}, - {"path": "a.py", "area": "core", "score": 9, "include": False, "reason": "a"}, - ], - ) - selected = tmp_path / "selected.jsonl" - - run_cli( - "select-deep-review-input", - "--rank-output", - str(rank_output), - "--out", - str(selected), - ) - - assert read_jsonl(selected) == [ - {"path": "a.py", "area": "core"}, - {"path": "b.py", "area": "api"}, - ] - - def test_make_rank_shards_is_deterministic_and_bounded(tmp_path: Path) -> None: rank_input = tmp_path / "rank_input.jsonl" rows = make_rank_rows(312) @@ -1144,16 +1029,6 @@ def test_empty_rank_input_closes_with_zero_shards_and_workers(tmp_path: Path) -> assert merge_result.stdout == f"Merged 0 ranking rows into {rank_output}\n" assert rank_output.read_bytes() == b"" - deep_review_input = tmp_path / "deep_review_input.jsonl" - run_cli( - "select-deep-review-input", - "--rank-output", - str(rank_output), - "--out", - str(deep_review_input), - ) - assert deep_review_input.read_bytes() == b"" - def test_make_rank_pool_plan_requires_sibling_rank_shards_directory(tmp_path: Path) -> None: rank_input = tmp_path / "rank_input.jsonl" diff --git a/sdk/typescript/tests-ts/deep-review-input.test.ts b/sdk/typescript/tests-ts/deep-review-input.test.ts new file mode 100644 index 000000000..5840499f5 --- /dev/null +++ b/sdk/typescript/tests-ts/deep-review-input.test.ts @@ -0,0 +1,430 @@ +import { spawnSync } from "node:child_process"; +import { + existsSync, + mkdtempSync, + readFileSync, + realpathSync, + rmSync, + statSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test } from "bun:test"; +import { PLUGIN_ROOT } from "./plugin-root.js"; + +const node = Bun.which("node")!; +const helper = join(PLUGIN_ROOT, "mcp", "helpers.mjs"); +const roots: string[] = []; +const newline = process.platform === "win32" ? "\r\n" : "\n"; +type Row = Record; +const candidate = (path: string, area = "src") => ({ + path, + area, + preview: "source preview", +}); +const ranked = (path: string, score = 5, include = true, area = "src") => ({ + path, + area, + score, + include, + reason: "runtime surface", +}); +function fixture() { + const root = realpathSync(mkdtempSync(join(tmpdir(), "deep-review-input-"))); + roots.push(root); + return { + root, + input: join(root, "input.jsonl"), + output: join(root, "output.jsonl"), + }; +} +function write(path: string, rows: Row[]) { + writeFileSync(path, rows.map((row) => JSON.stringify(row) + "\n").join("")); +} +function read(path: string) { + const text = readFileSync(path, "utf8").trim(); + return text === "" + ? [] + : text.split(/\r?\n/u).map((line) => JSON.parse(line)); +} +function run( + f: ReturnType, + selection: boolean, + args: string[] = [], + env = process.env, +) { + return spawnSync( + node, + [ + helper, + selection ? "select-deep-review-input" : "copy-deep-review-input", + selection ? "--rank-output" : "--rank-input", + f.input, + "--out", + f.output, + ...args, + ], + { cwd: f.root, encoding: "utf8", env }, + ); +} +afterEach(() => { + for (const root of roots.splice(0)) + rmSync(root, { recursive: true, force: true }); +}); + +describe("deep-review worklists", () => { + test("copies all candidates in input order and selects included ranked rows", () => { + const f = fixture(); + write(f.input, [candidate("a.py", "core"), candidate("b.py", "api")]); + expect(run(f, false).status).toBe(0); + expect(read(f.output)).toEqual([ + { path: "a.py", area: "core" }, + { path: "b.py", area: "api" }, + ]); + write(f.input, [ + ranked("c.py", 8, true, "api"), + ranked("a.py", 10, true, "core"), + ranked("b.py", 8, true, "api"), + ranked("d.py", 2, false, "core"), + ]); + const result = run(f, true, ["--top-percent", "67"]); + expect(result.status).toBe(0); + expect(result.stdout).toBe( + `Selected 2 of 3 rows into ${f.output}${newline}`, + ); + expect(read(f.output)).toEqual([ + { path: "a.py", area: "core" }, + { path: "b.py", area: "api" }, + ]); + }); + test("honors explicit 20 percent and defaults to 100 percent", () => { + const f = fixture(); + write( + f.input, + Array.from({ length: 5 }, (_, index) => + ranked(`src/file_${index}.py`, 10 - index), + ), + ); + expect(run(f, true, ["--top-percent", "20"]).status).toBe(0); + expect(read(f.output)).toEqual([{ path: "src/file_0.py", area: "src" }]); + expect(run(f, true).status).toBe(0); + expect(read(f.output)).toHaveLength(5); + }); + test("falls back to all rows when workers exclude everything", () => { + const f = fixture(); + write(f.input, [ + ranked("b.py", 2, false, "api"), + ranked("a.py", 9, false, "core"), + ]); + expect(run(f, true).status).toBe(0); + expect(read(f.output)).toEqual([ + { path: "a.py", area: "core" }, + { path: "b.py", area: "api" }, + ]); + }); + test.each([false, true])( + "closes an empty worklist with an empty output (selection=%s)", + (selection) => { + const f = fixture(); + write(f.input, []); + writeFileSync(f.output, "previous contents"); + expect(run(f, selection).status).toBe(0); + expect(readFileSync(f.output)).toHaveLength(0); + }, + ); + test.each([ + ["0", 1], + ["-10", 1], + ["1", 1], + ["200", 3], + ["+2_0", 1], + ["20", 1], + ["٢٠", 1], + ["9".repeat(308), 3], + ] as const)("preserves accepted integer percentage %s", (percent, count) => { + const f = fixture(); + write(f.input, [ranked("a.py"), ranked("b.py"), ranked("c.py")]); + expect(run(f, true, ["--top-percent", percent]).status).toBe(0); + expect(read(f.output)).toHaveLength(count); + }); + test("reports float conversion overflow only for nonempty selected worklists", () => { + const f = fixture(); + write(f.input, [ranked("a.py")]); + expect(run(f, true, ["--top-percent", "9".repeat(400)]).status).toBe(1); + expect(existsSync(f.output)).toBe(false); + write(f.input, []); + expect(run(f, true, ["--top-percent", "9".repeat(400)]).status).toBe(0); + }); + test("orders tied paths by Unicode code points and writes compact ASCII JSON", () => { + const f = fixture(); + write(f.input, [ + ranked("𐀀.py", 5, true, "é"), + ranked("\ue000.py", 5, true, "\udcff"), + ranked("\u007f.py", 5, true, "\t"), + ]); + expect(run(f, true).status).toBe(0); + expect(readFileSync(f.output, "utf8")).toBe( + [ + '{"path":"\\u007f.py","area":"\\t"}', + '{"path":"\\ue000.py","area":"\\udcff"}', + '{"path":"\\ud800\\udc00.py","area":"\\u00e9"}', + "", + ].join(newline), + ); + }); + test.each(["\n", "\r\n", "\r"])( + "accepts universal input newline %j with an unterminated last row", + (separator) => { + const f = fixture(); + writeFileSync( + f.input, + JSON.stringify(candidate("a.py")) + + separator + + JSON.stringify(candidate("b.py")), + ); + expect(run(f, false).status).toBe(0); + expect(read(f.output)).toHaveLength(2); + }, + ); + const invalid: Array<[boolean, string | Buffer, string]> = [ + [false, "\n", "blank JSONL rows are not allowed"], + [false, "\u001c\n", "blank JSONL rows are not allowed"], + [false, "{}\n\n", "missing fields"], + [false, "{not json}\n", "invalid JSON"], + [false, "[]\n", "expected a JSON object"], + [ + false, + JSON.stringify({ ...candidate("a.py"), extra: true }), + "unexpected fields ['extra']", + ], + [ + false, + JSON.stringify(candidate("\u001f")), + "path must be a non-empty string", + ], + [ + false, + JSON.stringify({ ...candidate("a.py"), preview: 1 }), + "preview must be a string", + ], + [ + false, + JSON.stringify({ ...candidate("a.py"), area: null }), + "area must be a string", + ], + [ + true, + JSON.stringify({ ...ranked("a.py"), score: true }), + "score must be an integer", + ], + [ + true, + '{"path":"a.py","area":"src","score":5.0,"include":true,"reason":"x"}', + "score must be an integer", + ], + [ + true, + '{"path":"a.py","area":"src","score":1e1,"include":true,"reason":"x"}', + "score must be an integer", + ], + [ + true, + JSON.stringify(ranked("a.py", 11)), + "score must be from 1 through 10", + ], + [ + true, + JSON.stringify({ ...ranked("a.py"), include: 1 }), + "include must be a boolean", + ], + [ + true, + JSON.stringify({ ...ranked("a.py"), reason: "\t" }), + "reason must be a non-empty string", + ], + [ + false, + "\ufeff" + JSON.stringify(candidate("a.py")), + "invalid JSON: Unexpected UTF-8 BOM", + ], + [false, Buffer.from([0xff]), "encoded data"], + ]; + test.each(invalid)( + "rejects invalid worklist data (selection=%s, data=%j)", + (selection, text, message) => { + const f = fixture(); + writeFileSync(f.input, text); + writeFileSync(f.output, "preserve existing output"); + const result = run(f, selection); + expect(result.status).toBe(1); + expect(result.stderr).toContain(message); + expect(readFileSync(f.output, "utf8")).toBe("preserve existing output"); + }, + ); + test.each([false, true])( + "rejects repeated paths before output (selection=%s)", + (selection) => { + const f = fixture(); + write( + f.input, + selection + ? [ranked("a.py"), ranked("a.py")] + : [candidate("a.py"), candidate("a.py")], + ); + const result = run(f, selection); + expect(result.status).toBe(1); + expect(result.stderr).toContain("contains duplicate paths: ['a.py']"); + expect(existsSync(f.output)).toBe(false); + }, + ); + test("accepts duplicate JSON object keys and empty area/preview as before", () => { + const f = fixture(); + writeFileSync( + f.input, + '{"path":"unused.py","path":"a.py","area":"","preview":""}', + ); + expect(run(f, false).status).toBe(0); + expect(read(f.output)).toEqual([{ path: "a.py", area: "" }]); + }); + test("allows replacing the input after all rows have been read", () => { + const f = fixture(); + write(f.input, [candidate("b.py"), candidate("a.py")]); + expect(run({ ...f, output: f.input }, false).status).toBe(0); + expect(read(f.input)).toEqual([ + { path: "b.py", area: "src" }, + { path: "a.py", area: "src" }, + ]); + }); + test("expands homes, creates output parents, and preserves path spelling in status", () => { + const f = fixture(); + write(f.input, [candidate("a.py")]); + const env = { ...process.env, HOME: f.root, USERPROFILE: f.root }; + const result = run( + { ...f, input: "~/input.jsonl", output: "./nested/./output.jsonl" }, + false, + [], + env, + ); + expect(result.status).toBe(0); + expect(result.stdout).toBe( + `Copied 1 rows into ${join("nested", "output.jsonl")}${newline}`, + ); + expect(read(join(f.root, "nested", "output.jsonl"))).toHaveLength(1); + }); + test("reports missing input and preserves argument parsing status", () => { + const f = fixture(); + expect(run(f, false).stderr).toContain(`Rank input missing: ${f.input}`); + expect(run(f, true).stderr).toContain(`Rank output missing: ${f.input}`); + write(f.input, [ranked("a.py")]); + expect(run(f, true, ["--top-percent=20"]).status).toBe(0); + expect(run(f, true, ["--top-p", "20"]).status).toBe(0); + for (const args of [ + ["--top-percent", "1.0"], + ["--top-percent"], + ["--unknown"], + ["extra"], + ["--top-percent", "1__0"], + ["--top-percent", "\u001c20"], + ]) + expect(run(f, true, args).status).toBe(2); + expect(run(f, true, ["--help"]).stdout).toContain("Defaults to 100"); + }); + test.skipIf(process.platform === "win32")( + "follows existing input/output symlinks and preserves existing output permissions", + () => { + const f = fixture(); + const target = join(f.root, "target.jsonl"); + writeFileSync(target, "existing", { mode: 0o640 }); + symlinkSync(target, f.output); + write(f.input, [candidate("a.py")]); + const inputLink = join(f.root, "input-link"); + symlinkSync(f.input, inputLink); + expect(run({ ...f, input: inputLink }, false).status).toBe(0); + expect(read(target)).toHaveLength(1); + expect(statSync(target).mode & 0o777).toBe(0o640); + }, + ); + test.skipIf(process.platform === "win32")( + "launcher preserves POSIX input and output path bytes", + () => { + const f = fixture(); + // APFS requires valid UTF-8 filenames; Linux also permits undecodable bytes. + const [inputBytes, outputBytes] = + process.platform === "darwin" + ? [Buffer.from("é"), Buffer.from("ö")] + : [Buffer.from([0xff]), Buffer.from([0xfe])]; + const shellBytes = (bytes: Buffer) => + Array.from( + bytes, + (byte) => `\\${byte.toString(8).padStart(3, "0")}`, + ).join(""); + const input = Buffer.concat([Buffer.from(f.root + "/in-"), inputBytes!]); + const output = Buffer.concat([ + Buffer.from(f.root + "/nested-"), + inputBytes!, + Buffer.from("/out-"), + outputBytes!, + ]); + writeFileSync(input, JSON.stringify(candidate("a.py")) + "\n"); + const launcher = join( + PLUGIN_ROOT, + "scripts", + "launch_codex_security_mcp", + ); + const result = spawnSync( + "sh", + [ + "-c", + `exec "$1" --helper copy-deep-review-input --rank-input "$2/in-$(printf '${shellBytes(inputBytes!)}')" --out "$2/nested-$(printf '${shellBytes(inputBytes!)}')/out-$(printf '${shellBytes(outputBytes!)}')"`, + "sh", + launcher, + f.root, + ], + { env: { ...process.env, CODEX_MCP_NODE_PATH: node } }, + ); + expect(result.status).toBe(0); + expect(readFileSync(output, "utf8")).toBe( + '{"path":"a.py","area":"src"}\n', + ); + expect(result.stdout).toEqual( + Buffer.concat([ + Buffer.from("Copied 1 rows into "), + output, + Buffer.from("\n"), + ]), + ); + }, + ); + test.skipIf(process.platform !== "win32")( + "Windows launcher reads and writes paths with spaces", + () => { + const f = fixture(); + const input = join(f.root, "rank input.jsonl"), + output = join(f.root, "deep review.jsonl"); + write(input, [candidate("a.py")]); + const caller = join(f.root, "caller.cmd"); + const launcher = join( + PLUGIN_ROOT, + "scripts", + "launch_codex_security_mcp.cmd", + ); + writeFileSync( + caller, + `@echo off\r\ncall "${launcher}" --helper copy-deep-review-input --rank-input "${input}" --out "${output}"\r\nexit /b %errorlevel%\r\n`, + ); + const result = spawnSync( + process.env["ComSpec"] ?? "cmd.exe", + ["/d", "/s", "/c", `""${caller}""`], + { + env: { ...process.env, CODEX_MCP_NODE_PATH: node }, + encoding: "utf8", + windowsVerbatimArguments: true, + }, + ); + expect(result.status, result.stderr || result.error?.message).toBe(0); + expect(read(output)).toEqual([{ path: "a.py", area: "src" }]); + }, + ); +}); From e25b86f50e2f6df49ff510a6398bc438f04ef640 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 10 Sep 2026 23:18:46 +0000 Subject: [PATCH 2/4] refactor: read worklist inputs without existence prechecks --- .../mcp-app/src/helpers/deep-review-input.ts | 3 +-- .../mcp-app/src/helpers/helper-files.ts | 20 ------------------- .../tests-ts/deep-review-input.test.ts | 9 +++++++-- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts b/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts index 7466b5844..5f73d6acf 100644 --- a/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts +++ b/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts @@ -1,6 +1,6 @@ import { decodeUtf8 } from "./utf8"; import { dirname } from "node:path"; -import { exists, mkdir, readFile, writeFile } from "./helper-files"; +import { mkdir, readFile, writeFile } from "./helper-files"; import { encodePosixPath } from "./posix-path"; import { JsonSyntaxError, object, parseJson, pythonRepr } from "./python-json"; import { expandHome, parsedPath } from "./resolve-security-md"; @@ -29,7 +29,6 @@ function compare(left: string, right: string): number { function loadRows(path: string, selection: boolean): RankRow[] { const label = selection ? "Rank output" : "Rank input"; - if (!exists(path)) throw new Error(`${label} missing: ${path}`); const contents = decodeUtf8(readFile(path)); const lines = contents === "" ? [] : contents.split(/\r\n|[\r\n]/u); if (lines.at(-1) === "") lines.pop(); diff --git a/plugins/codex-security/mcp-app/src/helpers/helper-files.ts b/plugins/codex-security/mcp-app/src/helpers/helper-files.ts index cdec4be24..920468fc5 100644 --- a/plugins/codex-security/mcp-app/src/helpers/helper-files.ts +++ b/plugins/codex-security/mcp-app/src/helpers/helper-files.ts @@ -1,6 +1,5 @@ import { closeSync, - existsSync, mkdirSync, openSync, readFileSync, @@ -17,25 +16,6 @@ export function readFile(path: string | number): Buffer { : readFileSync(encodePosixPath(path)); } -export function exists(path: string): boolean { - if (process.platform !== "win32") return existsSync(encodePosixPath(path)); - try { - windowsFileSystem(windowsBinding()).stat(widePath(path)); - return true; - } catch (error) { - const { code, winerror } = error as NodeJS.ErrnoException & { - winerror?: number; - }; - if ( - ["ENOENT", "ENOTDIR", "ELOOP"].includes(code ?? "") || - winerror === 21 || - winerror === 123 - ) - return false; - throw error; - } -} - export function mkdir(path: string): void { if (process.platform === "win32") windowsFileSystem(windowsBinding()).mkdir(widePath(path)); diff --git a/sdk/typescript/tests-ts/deep-review-input.test.ts b/sdk/typescript/tests-ts/deep-review-input.test.ts index 5840499f5..78762ee62 100644 --- a/sdk/typescript/tests-ts/deep-review-input.test.ts +++ b/sdk/typescript/tests-ts/deep-review-input.test.ts @@ -315,8 +315,13 @@ describe("deep-review worklists", () => { }); test("reports missing input and preserves argument parsing status", () => { const f = fixture(); - expect(run(f, false).stderr).toContain(`Rank input missing: ${f.input}`); - expect(run(f, true).stderr).toContain(`Rank output missing: ${f.input}`); + writeFileSync(f.output, "previous output\n"); + for (const selection of [false, true]) { + const result = run(f, selection); + expect(result.status).toBe(1); + expect(result.stderr).toContain(f.input); + expect(readFileSync(f.output, "utf8")).toBe("previous output\n"); + } write(f.input, [ranked("a.py")]); expect(run(f, true, ["--top-percent=20"]).status).toBe(0); expect(run(f, true, ["--top-p", "20"]).status).toBe(0); From eac169fcd08a6c7e486aaa6117c85b084f57f244 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 10 Sep 2026 23:20:18 +0000 Subject: [PATCH 3/4] refactor: drop unused worklist diagnostic label --- plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts b/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts index 5f73d6acf..6220809a6 100644 --- a/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts +++ b/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts @@ -28,7 +28,6 @@ function compare(left: string, right: string): number { } function loadRows(path: string, selection: boolean): RankRow[] { - const label = selection ? "Rank output" : "Rank input"; const contents = decodeUtf8(readFile(path)); const lines = contents === "" ? [] : contents.split(/\r\n|[\r\n]/u); if (lines.at(-1) === "") lines.pop(); From b7f638d626bd7240cabac5350cb2d5ba846296a2 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 10 Sep 2026 23:58:20 +0000 Subject: [PATCH 4/4] fix: retain duplicate worklist diagnostics in staged migration --- plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts b/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts index 6220809a6..5f73d6acf 100644 --- a/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts +++ b/plugins/codex-security/mcp-app/src/helpers/deep-review-input.ts @@ -28,6 +28,7 @@ function compare(left: string, right: string): number { } function loadRows(path: string, selection: boolean): RankRow[] { + const label = selection ? "Rank output" : "Rank input"; const contents = decodeUtf8(readFile(path)); const lines = contents === "" ? [] : contents.split(/\r\n|[\r\n]/u); if (lines.at(-1) === "") lines.pop();