feat(benchmark): freeze the valid-benchmark pilot, 12 verified positives + 240 negatives

First live run of the valid detection benchmark. Labels come from the fix
site (fix^1), never SZZ.

- 32 held-out Python/JS advisories sourced from the public pool-p1 set,
  CWE + text from OSV. 133 candidate locations -> 125 after dropping
  tests/minified.
- 125 DeepSeek verification calls: 31 locations passed all three checks
  (vulnerable pre-fix, fix addresses it, reachable in-repo) across 12 repos.
- Maintainer spot-check accepted all 12.
- 20 ordinary negatives per positive change from the same repos, 240 total.
- Held-out guard: 0 dropped against the training-only used set.
- Frozen manifest at benchmarks/valid/pilot/ (252 changes), provenance and
  NOTES under benchmarks/measurements/2026-10-12-valid-benchmark-pilot/.

Three fixes found by the live run:
- catalog.py: skip a site the fix renamed or removed instead of aborting.
- assemble.py: negative ratio counts positive changes, not accepted
  functions (malla would have demanded 260 negatives). Test updated.
- held-out guard: run against training sources only; build_used() also
  scans measurements/populations, which flagged the 12 repos via my own
  SZZ-pool analysis outputs (circular).

Replay and metrics not yet run; see NOTES.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-12 19:42:45 +02:00
co-authored by Claude Fable 5.1
parent 43ef254134
commit 7a636d60fb
10 changed files with 4606 additions and 3 deletions
+7 -1
View File
@@ -103,7 +103,13 @@ def build_cases(git, source, *, cwe: str | None = None, advisory_text: str = "")
function=site["function"],
method="fix_site",
)
sides = read_functions(git, case)
try:
sides = read_functions(git, case)
except extract.InstrumentFailure:
# The fix renamed, moved, or removed the function, so there is no clean
# pre/post pair at this site. Skip it during enumeration. Verification
# stays strict: a verdict on an unread case is still rejected there.
continue
if extract.normalized(sides["pre_fix"]) == extract.normalized(sides["post_fix"]):
continue
case["function_lines"] = [sides["pre_fix_lines"]]