feat(guardian): per-finding skeptic judgement + impact scoring (#246) - #269
Conversation
… coverage Finding gains impact_score (0-10, skeptic-owned, None = unjudged) and ReviewResult gains the 'partial' status plus skeptic_judged/skeptic_total. _sanitize_finder_result now wipes impact_score and the counters too: they live on the shared finder schema, and a hallucinated score would hide a real finding behind the impact threshold as quietly as the Plan-2 verdict bug dropped one. Task 1 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two orthogonal axes: verdict answers 'is this true' (only an explicit refuted drops), impact_score 0-10 answers 'does it matter' (below threshold hides, never drops, and stays in the result so metrics still see it). apply_judgements merges positionally, so a None — a failed call — leaves the finding unruled rather than refuted. The batch API stays alive until every call site is ported. Task 2 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The skeptic pass needs per-file diff blocks, and diff_index.py is the module whose stated purpose is pure unified-diff parsing; importing it from chunker (a #154 concept dormant behind a flag) would point the skeptic at the wrong layer. Not a straight move: chunker's _NEW_FILE_RE and diff_index's _NEW_FILE_RE share a name but NOT a pattern — the chunker pair is git-C-quote aware, the indexer pair is not. Merged blindly, one would have silently replaced the other and changed inline-comment anchoring. The quote-aware pair moves as _QUOTED_OLD_FILE_RE/_QUOTED_NEW_FILE_RE, and the module docstring now states that the two families are deliberately different. Self-measurement moved and is re-pinned in tests/self_parsing/test_suggest.py: a shared leaf is imported from more clusters, so guardian's modularity Q fell 0.3516 -> 0.3320 (split -> borderline) while divergence, the metric that tracks the layout, IMPROVED 0.7224 -> 0.6705. The test's load-bearing claim (nested reads below the flat 1.0) holds and strengthened; same re-pin precedent as #255. Task 3 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
build_judgement_prompt shows the claim and its own file's hunks, and withholds the finder's confidence and severity: both are its guess at exactly what this pass re-derives, so showing them anchors the judge on the defendant. The prompt states that a claim depending on code outside those hunks cannot be checked — grounds for 'uncertain', never 'refuted' — so narrowing the context does not become a false-refutation generator. IMPACT_RUBRIC anchors the new axis, including 'if ruff/ruff format/mypy --strict would catch it, score <= 2': safe here because both run as mandatory gates, and placed on the importance axis rather than in the finder prompt, where the same instruction would re-introduce the precision bias that caused recall 0/5 (#249). judge_finding collapses every failure to None so a skeptic that cannot answer can never silence a finding. Task 4 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each finding is judged against only its own file's hunks (split_diff_by_file), concurrently behind a semaphore defaulting to 3 — provider rate limits are the binding constraint, not local CPU. Results are positional, with None where a call failed, so apply_judgements can leave that finding unruled. The concurrency test asserts both bounds: peak <= 3 proves the semaphore holds, peak > 1 proves the gather is actually concurrent and not accidentally serial. Task 5 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
run_review swaps the single batch skeptic call for judge_all + apply_judgements and reports honest counts. Degradation stops being all-or-nothing: 'partial' carries judged/total, and the findings whose calls failed stay visible, because absence of a judgement has never been a refutation. Previously one batch parse failure discarded every verdict in the pass. skeptic_status_for lives in skeptic.py rather than as a private core helper — the chunked orchestrator (T7) reports the same statuses, and two copies of the mapping would be free to drift apart. Task 6 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports run_chunked_review to judge_all/apply_judgements so a second, diverging skeptic call site does not survive into T11's deletion of the batch API. The chunk-scoped context still bounds what the judge sees; judge_all narrows further to each finding's own file. The flag stays off — this is not a re-benchmark of chunking (#154). The scoped-diff test now feeds the per-finding response shape; its batch constant _CONFIRM_ALL is gone. Task 7 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…counts The report now orders by impact_score descending before severity, shows the score next to each finding, and counts below-threshold suppressions in the same never-silent way refutations were already counted. Unjudged findings sort as -1, not 0: no score means no ranking signal, not a score of zero. GUARDIAN_IMPACT_THRESHOLD (runner.impact_threshold) is clamped to 0-10 and defaults to 0 — nothing is hidden until the benchmark shows the distribution, and a garbage value degrades to hiding nothing rather than silently swallowing findings. Threaded through run_guardian, the inline poster and the entry script, so the env var actually reaches the render. Footer gains 'skeptic judged/total' and the metrics JSONL gains skeptic_judged, skeptic_total and impact_threshold — a partial pass must be visible in the record, not inferred. Task 8 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… separation score_separation is the #246 gate metric: median impact of GT-matching findings minus median impact of the rest. Noise counts alone cannot tell 'the skeptic learned to rank' from 'the threshold got lucky'; a flat distribution scores 0, which is exactly what the PR #263 pathology would produce. None when either population is empty — a run with no GT match cannot answer the question. annotate_matches keeps EVERY finding in the record, including ones the skeptic hid, each flagged matched_gt — otherwise 'noise fell' is indistinguishable from 'we went blind'. It maps findings to matcher positions by identity, not equality: two findings can be field-identical while only one matched. Task 9 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--record-finder freezes a finder pass (skeptic forced off for that run); --replay-finder judges that frozen set instead of running the finder, so every skeptic variant sees identical findings. Re-running the stochastic finder per variant measures the sum of two noise sources — the reason a single lucky finding could move a PR's median at n=3. Deviation from the plan, deliberate: the recording carries the DIFF alongside the findings. The plan replayed a bare ReviewResult, which would have forced a worktree + ingest to reconstruct the diff for the skeptic — paying exactly the setup cost replay exists to avoid. A replay run now touches no git at all. load_finder_recording strips any skeptic verdicts the recording carries: a set recorded from a run that had a skeptic would otherwise smuggle old verdicts into the next variant and make the arms incomparable. Scoring is extracted to _score_and_record so both paths share one JSONL writer. Task 10 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes SkepticVerdict, SkepticResult, build_skeptic_prompt and apply_verdicts now that core.py and chunked.py both judge per finding. Verified no other call site remained before deleting. The batch tests are replaced, not just deleted: every behaviour they protected has an equivalent against apply_judgements — refuted is marked but stays in the list, uncertain discounts x0.9 and survives, an absent judgement is not a refutation, visible_findings drops only refuted. The two index-mapping tests (out-of-range, duplicate) have no equivalent by design: without indices those failure modes cannot occur. Sprint spec §5.2 is marked SUPERSEDED with the reasoning: its premises were a 5-finding cap (removed in #249) and a large shared context (each judgement now carries only its own file's hunks, so N calls are individually smaller than the one they replace). Task 11 of docs/specs/plans/2026-07-29-guardian-skeptic-scoring.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Two MAJOR vulnerabilities: --record-finder / --replay-finder take a path from an untrusted CLI flag and hand it straight to the filesystem. Validation now mirrors the #167 precedent — expanduser + resolve, require a .json suffix, and require an existing file when reading — deliberately WITHOUT confining the path under the CWD, which would break the cross-repo and tmpdir workflows the bench runs in. A typo'd replay path now fails with a clear ValueError instead of a raw OSError. CRITICAL smell: '/dev/null' appeared three times in diff_index after the T3 move; extracted as _DEV_NULL. record_review hit 14 parameters (limit 13) because #246 added three. Grouped the four skeptic-related ones into a SkepticRecord model rather than dropping any: they are meaningless apart — judged/total only interpret status, and impact_threshold only interprets what the report then hid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A replay run makes zero finder calls, so recording only the finder provider's usage reported a free run for a pass that cost real tokens — measured 78k prompt tokens on the pr-143 frozen set while the JSONL row said 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/guardian review |
Seven rows: three frozen finder passes (mistral-medium on pr-143/144/140) and four per-finding skeptic runs over them — codestral on pr-143 (phase 1, same-family control) and gemini-2.5-flash on all three (phase 2, cross-family). Each row is tagged bench_arm / bench_phase. The batch-arm numbers quoted in the PR discussion are NOT here: they were produced by a throwaway script against main's batch API, which this branch deletes, so they are not reproducible from this tree and do not belong in the committed record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 1 + 2 measured — gate passes, with two caveatsProtocol per spec §4: one frozen finder pass per PR (mistral-medium), every skeptic variant judging that same set, so the only variable is the skeptic. Raw rows committed in Results across 3 PRs
Per-finding: −33% noise for −10% recall. Batch: −54% noise for −37% recall. Batch buys its lower noise by destroying findings — on pr-143 it refuted 14 of 28 and took two ground-truth findings with them, the exact over-refutation pathology that was reverted once before. Isolation is the better operating point on all three PRs. Phase 1 (same-family control, codestral on pr-143): judged 28/28, refuted 0, noise unchanged at 22. A same-family skeptic contributes nothing in either arm — the cross-family choice is where the entire effect lives, which is what #246 originally proposed. Gate 2 passes at exactly the line: pooled separation 3.0 vs the required ≥3 (GT n=12 median 7.0; non-GT n=58 median 4.0). Per-PR it swings 1.5 / 7.0 / 2.0 — precisely the small-sample instability that made the spec require pooling. 3.0 against a 3.0 bar is not margin. Caveat 1 — a flaw in my own analysis, correctedI first reported "7/7 refutations correct, zero GT killed". That evidence was tautological: The sound evidence is recall before vs after: unchanged on pr-143 (1.00) and pr-144 (0.60), but pr-140 fell 0.40 → 0.20 — per-finding gemini did kill a real finding there. The comparative claim survives (isolation holds recall better than batch on 2 of 3 PRs, ties on the third); the absolute claim "isolation protects recall" does not.
Caveat 2 — the impact axis under-deliversGate 2 passes only via pooling. Per-PR the axis is unstable, and on pr-143 the separation among survivors is 0.0: gemini's low scores sit exactly on the findings it already refuted, while every surviving noise finding scores the same as the GT ones (medians 7.0 vs 7.0). The two axes were designed orthogonal; for both models tested they are largely collinear. Cost: per-finding took 221k skeptic prompt tokens across the three PRs vs ~131k for batch — +69%, not the reduction the spec assumed (the spec's cost claim is refuted: findings cluster by file, so a file's hunks are re-sent once per finding in it). What this changes
🤖 Generated with Claude Code |
|



Implements
docs/specs/2026-07-29-guardian-skeptic-scoring-design.md(merged in #268) via its 11-task plan. Closes the mechanism half of #246; the model matrix is phase 2 and needs a budget decision.What was broken
The recall-lean finder (#249) delegates precision to the skeptic, and the skeptic could not deliver. Live evidence from this repo today, PR #263: 8 findings, all confirmed, all at one severity — one real defect (
Path.is_file()raises on EACCES/ENAMETOOLONG) sitting indistinguishable from two factually wrong claims (for size: int in range(...)is not valid Python;as_posix()would have broken the very Windows-path case the line exists for).Two causes, only one of which is the model:
finding_index. Everything competed for one attention window, and a single parse failure discarded every verdict in the pass.Literal["confirmed","refuted","uncertain"]can say "this is false" but not "this is true and worthless", and cannot be sorted or thresholded.What changed
Two orthogonal axes.
verdict(is it true — only explicitrefuteddrops) and the newimpact_score: int0–10 (does it matter — below threshold hides, never drops, and stays in the JSONL so the benchmark can still see what was cut).One call per finding, carrying only that finding's own hunks, concurrent behind a semaphore (default 3 — mistral's free tier caps tokens per minute).
finding_indexis gone, and with it the out-of-range and duplicate failure modes that previously had to be handled.Partial degradation.
skeptic_statusgainspartialwithjudged/totalin the footer and metrics. One failed call now costs one verdict instead of the whole pass.A previously-reverted lever became usable. Few-shot false-positive examples once swung mistral into refute-all (pr-143 recall 0.7 → 0.0) because with one axis, refutation was the only way to express "bad finding". Calibration now lands on the importance axis and cannot reach the refutation logic. The truth-axis prompt is reused verbatim — it was bought by reverting a refute-by-default version that killed 7/7 findings including 2 GT matches.
"If ruff/ruff format/mypy --strict would catch it, impact ≤ 2" — the Gemini-review advice, placed on the axis where it is safe. In the finder prompt the same instruction re-introduces the precision bias that caused recall 0/5.
Benchmark can now answer the question.
--record-finder/--replay-finderfreeze a finder pass so every skeptic variant judges an identical set;annotate_matchesrecords each finding's score and whether it matched ground truth;score_separationis the gate metric —median(score | GT) − median(score | non-GT), pooled across PRs. A flat distribution scores 0, which is exactly what the #263 pathology would produce.Two deliberate deviations from the plan
T3 was not a straight move.
split_diff_by_filedragged along_block_pathand a_OLD_FILE_RE/_NEW_FILE_REpair — anddiff_index.pyalready had its own_NEW_FILE_REwith a different pattern (the chunker pair is git-C-quote aware, the indexer pair is not). Merged blindly, one would have silently replaced the other and changed inline-comment anchoring. They now coexist as_QUOTED_*, with the module docstring stating the difference is deliberate.T10 records the diff alongside the findings. The plan replayed a bare
ReviewResult, which would have forced a worktree + ingest to reconstruct the diff for the skeptic — paying exactly the setup cost replay exists to avoid. A replay run now touches no git at all.Self-measurement moved, and is re-pinned honestly
diff_indexbecame a shared leaf (chunker, github_poster, skeptic), and a shared leaf adds cross-community edges: guardian's modularity Q fell 0.3516 → 0.3320, flippingsuggest-packagesfromsplittoborderline. The metric that actually tracks the layout improved: divergence 0.7224 → 0.6705.tests/self_parsing/test_suggest.pyis re-pinned with that reasoning; its load-bearing claim (nested reads below the flat 1.0) holds and strengthened. Same precedent as the #255 re-pin.Verification
apply_judgements. The two index-mapping tests have no equivalent by design — without indices those failures cannot occur.Not in this PR
The phase-1 A/B benchmark run (batch arm vs per-finding arm, same codestral model, frozen finder output) — it needs API spend, and the gate is
noise down + zero lost GT + separation ≥ 3. Phase 2 (gemini / local ollama matrix) needs an explicit budget go; the prior gemini run hit the 30 PLN cap.GUARDIAN_IMPACT_THRESHOLDships at 0 — nothing is hidden until that benchmark shows the score distribution.🤖 Generated with Claude Code