output_encoding had exactly ONE pair in the whole corpus. It now has 41. 238 pairs were harvested from OWASP BenchmarkJava (2740 generated servlets, GPL-2.0, with a ground-truth CSV) by matching each really-vulnerable test to the safe test of the same category it most resembles -- adjacent tests come from one template, so they differ in the sink or the sanitiser rather than being unrelated programs. Median similarity 0.91, against the 64% of the older `sast` OWASP entries that the group-4 stratification flagged as different programs. Only 40 are vendored. One representative of each of the seven categories was run through the model BEFORE vendoring, and only xss distinguished: its fix adds ESAPI encoding, which the Java sanitizer facts recognise, so the vulnerable side entails and the fixed side drops to corroborated. cmdi, sqli, ldapi and xpathi put the sink in a helper class outside the servlet and resolved no flow at all; pathtraver found the flow but its "safe" twin carries no distinguishing call, so text similarity had matched a twin that is not semantically the fix -- a subtler form of the very defect the harvest was designed to avoid. Those 198 pairs are deliberately left out: adding pairs the model cannot read would inflate the corpus while making every number less interpretable. Evidence for the vendored slice is 3 of 3 distinguished, which is a small sample and is stated as such in the catalog header rather than implied to be more. The slice is marked SYNTHETIC in its header: it measures breadth across a weakness class and must never stand in for real-world performance. The corpus already shows a +16% overfitting gap, and a large generated slice blended into a headline would widen it invisibly. Two things recorded rather than hidden: BenchmarkJava's LICENSE is GPL-2.0 while the older sast entries declare GPL-3.0 for OWASP, which looks like an error and is not copied; and OWASP-Benchmark/BenchmarkJava already straddled the train/holdout split before these pairs (owasp-java-cmdi train, owasp-java-sqli holdout), so the new all-holdout pairs enlarge an existing straddle rather than create one. The two vendored-count guards did their job -- a corpus change could not land silently -- and are updated as the acknowledgement. Hygiene artifact regenerated: 277 -> 317 vendored pairs. 720 passing, mypy/ruff clean, gates byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
Vuln vs fixed pair corpus
This corpus measures harness efficiency, not cheat-sheet recall.
The stage-1 smoke gate (python -m openultrasast.gate) scans one tree that
already contains the planted sinks. A pair eval instead scans two isolated
trees:
- vuln — only the vulnerable file. The harness should fire (true positive).
- fixed — only the patched file, same relative path. The harness should stay silent on that CWE (true negative).
pair_correct = detected on vuln and silent on the fix. That is the signal
the improvement loop consumes (miss on the vuln side, fp on the fix side).
Layout
| Path | Role |
|---|---|
catalog.toml |
Runnable offline pairs (local fixtures + vendored GitHub functions) |
datasets.toml |
Pointers to public VFC datasets — do not vendor the dumps |
datasets/cwe-bench-java-project_info.csv |
120 vetted Java CVE buggy/fix commits |
github/ |
Isolated function excerpts with provenance headers |
Pairs in this directory must not join LANGUAGE_MANIFESTS.
Slices
- local — in-tree fixture counterparts (
app.pyvssafe.py, …). CI gate: every local pair must pass (python -m openultrasast.pair_gate). - github — real commit/function pairs (SVEN, CWE-Bench-Java, NVD patterns). Measured, not a 90% smoke gate. Misses here are the honesty / evolve-the-harness dashboard.
- sast — OWASP Benchmark Java/Python true-vs-false files and Juliet
bad-vs-good sinks (
benchmarks/pairs/sast/). Score is Youden (TPR − FPR). Honesty dashboard, not a CI fail. - vfc — reviewed isolated functions from OpenSSL, Firefox, Chromium, and
curl (
benchmarks/pairs/vfc/). Overlay vs inventory scoreboard. Honesty dashboard, not a CI fail. Training labels (vuln vs fixed) arevfc/training/manifest.jsonl(vuln vs fixed, one snapshot per line). Grow with a bloblessgit log --grep=CVE-202mine plusvfc/harvest.py, thenvfc/generate_catalog.py(maintainer, not CI). - vibe-py — Real-Vuln-Benchmark functions (
benchmarks/pairs/vibe-py/): Python web repos pinned by commit, one vulnerable function per pair with a false-positive trap from the same repo as the fixed twin. Today only the 15 licensed human-authored repos are vendored (provenance = "human"); the 40 LLM-generated repos carry no license file and are excluded until one is stated upstream. Educational/CTF apps, not production. Honesty dashboard. - vfc-js — server-side JavaScript function pairs harvested from the upstream
fix commits that SecBench.js records (
benchmarks/pairs/vfc-js/). Command injection, path traversal, and code injection first; prototype pollution and ReDoS carry their mechanism and may beknown_limit. No SecBench.js file is vendored. Honesty dashboard. - agent-vfc — security-fix commits carrying an AI agent co-author trailer or
generation marker, in JavaScript, TypeScript, or Python
(
benchmarks/pairs/agent-vfc/).provenance = "agent". Rows withreviewer = "pending"load atreview_tier = "title"(the review queue) and never gate the improve loop (task 8.2); a human setsreviewerto promote a row toreviewed. Honesty dashboard.
Review tiers
Every pair carries review_tier (Req 9): seeded (ground truth reviewed by the
upstream benchmark), advisory (public CVE or GHSA with a fix commit), title
(mechanism inferred from a commit message, unreviewed), reviewed (a named
reviewer in this repository; the loader rejects reviewed without one).
Defaults per slice: local reviewed, vibe-py seeded, sast/vfc/vfc-js/github
advisory, agent-vfc title. pairs --json reports per_tier; evolve.evaluate_profiles scores only seeded
and reviewed pairs, so advisory and title pairs never gate the improve loop (task 8.2).
Pointer pairs (not vendored)
Current pointer rows: vibe-py 80 (the 40 LLM-generated Real-Vuln repositories, agent,
seeded) and agent-vfc (unlicensed agent-authored fixes, title).
A row with vendored = false carries its harvest recipe (repo, parent, commit,
path, mode, line/fix_path/fix_line, ...) and no excerpt files (Req 10). The
repository never redistributes that code: pairs --pointers (or
OPENULTRASAST_PAIRS_NETWORK=1) harvests both sides into
OPENULTRASAST_PAIR_CACHE (default ~/.cache/openultrasast/pairs/<slice>/<name>/,
git-ignored) and scores them exactly like a vendored pair; without network the pair is
skipped with a pointer_pair_skipped degradation and a failed fetch is
pointer_pair_fetch_failed, never an error. The default pytest run and every gate
score vendored pairs only (select_vendored); the pointer run is the nightly command.
Labels, profiles, and mechanisms
Every [[pair.expected]] row names a mechanism from the closed vocabulary in
benchmarks/pairs/mechanisms.toml; every pair carries provenance
(human|agent|mixed|synthetic), a declared split (train|holdout), and an
optional known_limit reason. The scorer counts overlay coverage records with
a source as detections and as leaks, matches by function when a label names one,
never matches a CWE-only row (weak_label), and reports per_profile,
per_mechanism, achievable (known-limit pairs excluded), and loss
(parse-failed files, unadjudicated proposals). ousast pairs --profile agent --split holdout --json filters; --hunter adds the LLM hunter as a third scorer
when a hunter model is configured. The improve loop rejects a change that
regresses any profile on the holdout split.
Harvest for every slice goes through benchmarks/pairs/harvest.py --slice <name>
(modes name, line_range, hunk, enclosing, handler_context; comment
and string mentions never anchor; handler_context keeps the labeled handler
with its decorators and the statements that register it) and benchmarks/pairs/catalog_gen.py --slice <name> regenerates
the catalog from recipes.toml. Maintainer only; CI never fetches.
Commands
uv run ousast pairs
uv run ousast pairs --slice github --json
uv run ousast pairs --slice vfc --json
uv run python -m openultrasast.pair_gate
Growing the corpus
Prefer function-level before/after from a reviewed source (SVEN jsonl, a
CWE-Bench-Java fix_info.csv row, a GHSA with a tiny patch). Isolate the
changed function into vuln / fixed files with the same relpath. Leave
whole-repo clones to --fetch later; CI stays offline.