v1.4.0: precision-filter post-processing between engine and merge gate - #9
Conversation
New pipeline stage between `ocr review` and check-result.mjs, gated by a new `precision-filter` input (default true). Targets the FP pattern seen on large PRs where the reviewer copies the same finding text onto multiple files that do not contain the referenced code (adjudicated at ~23% FP rate on orca-cyber-harness#13; the same pattern reproduced in bakeoff runs on minimax as well). L1 — `scripts/postfilter.mjs` (deterministic, no LLM): Uses each finding's `existing_code` snippet as a locator. git-greps the snippet in the reviewed commit's tree; if found in exactly one other file, re-homes the finding there; if found nowhere and the claimed path is a non-code file, drops the finding. Also dedupes by normalized content. L2 — `scripts/judge.mjs` (one LLM call, independent vendor): Clusters findings by root cause and scores each cluster 0–1 for "concrete, correct, high-value defect in this change". Keeps one representative per cluster above `judge-threshold` (default 0.7). Uses an independent model (default `deepseek/deepseek-v4-pro`); the same-vendor guardrail from earlier testing showed a same-family judge under-catches its own errors. Configurable via new `judge-model` and `judge-threshold` inputs. LLM connection reads OCR_LLM_URL / OCR_LLM_TOKEN env vars (production) with a ~/.opencodereview/config.json fallback for the local harness. Both stages are soft-fail: any error keeps the prior stage's findings and never aborts the review. Also skipped when the engine timed out or produced no findings — nothing to filter. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…uncated) Observed on the minimax 80bffaa72 bakeoff (48 findings): the JSON response exceeded 8000 output tokens mid-cluster, parseFail → judge exited 1 → the action's fail-safe kept the L1 output (47) instead of the intended judge cut. 32k gives headroom well past 100 findings; measured usage at 47 in was ~20k. Independent of model — same fix in testbed/judge.mjs.
…shold 0.5 Scrub the action's precision-filter, judge-model, and judge-threshold input descriptions (and matching README rows) to plain mechanism-only language; describe what each input does, not why or how it was tuned. Adjust judge-threshold default from 0.7 to 0.5.
There was a problem hiding this comment.
🐳 Orca-Code-Review
Found 2 issues in this PR: 🟠 2 P1.
Reviewed via OrcaRouter — Route Smarter. Ship Safer. Spend Less.
| let out = null; | ||
| for (let i = 0; i < rest.length; i += 1) if (rest[i] === "--out") out = rest[++i]; | ||
| if (!file || !repo || !commit) { | ||
| console.error("usage: node postfilter.mjs <result.json> <repo> <commit> [--out f]"); |
There was a problem hiding this comment.
🟠 P1 There are no checks to validate whether the provided file path exists or whether the format of the input arguments is correct. This might lead to runtime errors or unhandled exceptions. Implement checks at the start of your script to ensure that the file, repo, and commit arguments exist and are formatted correctly.
| const jsonText = content.replace(/^```(?:json)?/m, "").replace(/```$/m, "").trim(); | ||
| let parsed; | ||
| try { parsed = JSON.parse(jsonText); } | ||
| catch (e) { console.error("judge did not return JSON:\n" + content.slice(0, 600)); process.exit(1); } |
There was a problem hiding this comment.
🟠 P1 Improve error handling during JSON parsing by including detailed error messages. Current error handling lacks context, which makes debugging difficult without specific error information.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 024746874d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const covered = new Set(); | ||
| for (const g of groups) for (const id of g.member_ids || []) covered.add(id); | ||
| for (let i = 0; i < findings.length; i += 1) if (!covered.has(i)) | ||
| groups.push({ member_ids: [i], representative_id: i, confidence: 0.5, keep: true, root_cause: "(uncovered)", reason: "not classified by judge; kept fail-open" }); |
There was a problem hiding this comment.
Keep unclassified findings above custom judge thresholds
When the judge response omits a finding id and users set judge-threshold above the default 0.5, this fail-open fallback still assigns confidence: 0.5, so the later g.confidence >= threshold check drops the finding even though the reason says it was kept. In any incomplete judge response, raising the documented threshold can therefore silently remove findings because the judge failed to classify them; fallback groups should bypass the threshold or use a confidence that always survives.
Useful? React with 👍 / 👎.
| let action = "keep"; | ||
| if (trueFiles.length) { | ||
| if (trueFiles.includes(c.path)) action = "keep (correct)"; | ||
| else if (trueFiles.length === 1) { path = trueFiles[0]; action = `REHOME ${c.path} -> ${path}`; } |
There was a problem hiding this comment.
Recompute line numbers when re-homing findings
When this branch re-homes a finding to the single file that actually contains the quoted snippet, it changes only path; the original start_line/end_line still belong to the old path and are later used unchanged by the review-posting step. For any re-homed finding whose old line is not a valid/commentable line in the new file, the inline review batch can be rejected or point at unrelated code; the filter should also locate the snippet line in the target file before rewriting the comment.
Useful? React with 👍 / 👎.
| const rep = comments[g.representative_id] ?? comments[g.member_ids[0]]; | ||
| const others = (g.member_ids || []).filter((id) => id !== (g.representative_id ?? g.member_ids[0])); | ||
| const line = `[conf ${g.confidence?.toFixed(2)}] ${(rep?.content || "").match(/\[(P[0-3])\]/)?.[0] || ""} ${rep?.path} :: ${g.root_cause} ${others.length ? "(merged " + others.length + ")" : ""}`; | ||
| if (surv) { kept.push(rep); console.error("KEEP " + line); } |
There was a problem hiding this comment.
Validate judge ids before keeping comments
If the judge returns a kept group whose representative_id (or fallback member_ids[0]) is outside the input comments array, this pushes undefined, which serializes as null in comments; check-result.mjs still accepts the array, but the later posting step dereferences c.content and fails the job. Since L2 is supposed to be soft-fail, reject out-of-range ids or ignore the malformed group before writing the filtered result.
Useful? React with 👍 / 👎.
| else | ||
| echo "::warning::L2 judge failed — keeping L1 findings" |
There was a problem hiding this comment.
Clear policy blocks from soft-failed judge calls
When a repo has a guardrail/firewall attached and the L2 judge request is blocked by the same proxy, policy-block.json is written even though this branch only warns and keeps the L1 findings. The later Surface guardrail / firewall block step will then post a false “merge is blocked” policy comment for a review that actually completed; clear that marker on soft-failed judge calls or isolate the judge from the block-surfacing file.
Useful? React with 👍 / 👎.
| let out = null, threshold = 0.7, modelOverride = null; | ||
| for (let i = 0; i < rest.length; i += 1) { | ||
| if (rest[i] === "--out") out = rest[++i]; | ||
| else if (rest[i] === "--threshold") threshold = parseFloat(rest[++i]); |
There was a problem hiding this comment.
Validate judge thresholds before filtering
If a workflow passes a nonnumeric judge-threshold, parseFloat produces NaN, and every later g.confidence >= threshold comparison is false, so L2 writes an empty comments array and the merge gate can pass despite blocking findings from the engine/L1. Since action inputs are strings and this knob is documented for users, invalid thresholds should default or soft-fail back to the prior-stage findings instead of filtering everything out.
Useful? React with 👍 / 👎.
| } else if (!CODE_EXT.test(c.path)) { | ||
| action = "DROP (code snippet, filed on non-code file, not found)"; |
There was a problem hiding this comment.
Keep unmatched findings on extensionless code files
When a legitimate finding targets code/config that does not match this extension allowlist, such as a Dockerfile or Makefile, and its quoted existing_code line is too short to search or otherwise not found, this branch drops it solely as “non-code”. That can remove P0/P1 findings before the gate; unmatched paths that are not known generated/locale files should fail open instead of being discarded by extension.
Useful? React with 👍 / 👎.
Post-merge review of the two self-review findingsSelf-review on this PR reported FAILURE with 2 P1 findings. I squash-merged before opening them — a process mistake I've made once before and explicitly ruled against. Recording the assessment here for the audit trail. Judgment: both findings are false positives. v1.4.0 does not need to be reverted. P1 —
|
Orca-Code-Review — push 1
Tier: STRONG (final pass) — blocked
❌ 2 findings block merge
Summary
Adds a precision-filter stage between the engine's raw findings and the merge gate:
existing_codesnippet against the reviewed commit; re-homes findings whose snippet points at a different file and drops findings whose snippet cannot be located, then dedupes by normalized content.New inputs (all optional)
precision-filter(defaulttrue) — toggle the pipelinejudge-model(defaultdeepseek/deepseek-v4-pro) — which model runs the L2 judgejudge-threshold(default0.5) — L2 confidence gate for cluster keep/dropBoth L1 and L2 are soft-fail: errors keep the prior stage's findings intact and never abort the review.
Compatibility
The
precision-filter: "true"default alters what consumers on@v1see — findings are consolidated by root cause and low-confidence entries are dropped. Setprecision-filter: "false"to preserve pre-v1.4 output.Test plan
🤖 Generated with Claude Code