Skip to content

Commit abdb81f

Browse files
production: classify C_candidate with gemma4:31b, promote 166 silent fixes
Ran the training-free LLM classifier (gemma4:31b via Ollama Cloud) over 339 C_candidate PR/commit rows using local_diffs (rate-limit-free, cache-resumable). Flagged 166 as real silent fixes (133 DoS / 16 consensus / 11 validation …), promoting them C_corroborated -> B. Essential slice 1367 -> 1535. - llm_classify_fixes.py: --apply mode (local_diffs + persistent diff/pred caches). - data/silent_fix_llm.csv: classifier output, tracked so the build is reproducible. - run_pipeline.sh: curate stage folds it in when present (--silent-fix-csv). - Spot-check: flagged rows are genuine ("loop limit to prevent remote DoS", "OOB panic unmarshaling JSON", "nil deref at backend init"); non-fixes correctly rejected (CI/dep-bump/feature; even "panic added for diagnostics"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 15b9b87 commit abdb81f

6 files changed

Lines changed: 433 additions & 9 deletions

File tree

collection/llm_classify_fixes.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,81 @@ def evaluate(preds):
253253
return {"precision": prec, "recall": rec, "f1": f1, "tp": tp, "fp": fp, "tn": tn, "fn": fn}
254254

255255

256+
def apply_to_dataset(a) -> int:
257+
"""Classify real dataset rows and emit source_url -> silent_fix_prob.
258+
259+
Diffs come from local_diffs (bare clone + persistent cache, rate-limit-free);
260+
LLM predictions are cached per URL so re-runs are resumable ("差分だけ").
261+
"""
262+
import csv
263+
sys.path.insert(0, str(Path(__file__).resolve().parent))
264+
import local_diffs
265+
266+
df = pd.read_parquet(a.inp)
267+
if a.tier != "all" and "authority_tier" in df.columns:
268+
df = df[df["authority_tier"] == a.tier]
269+
df = df[df["source_url"].str.contains(r"/pull/|/commit/", na=False)].copy()
270+
if a.limit:
271+
df = df.head(a.limit)
272+
diff_cache = json.loads(a.cache.read_text()) if a.cache.exists() else {}
273+
pred_cache = json.loads(a.pred_cache.read_text()) if a.pred_cache.exists() else {}
274+
rows = df.to_dict("records")
275+
print(f"[apply] {len(rows)} rows (tier={a.tier}); "
276+
f"{sum(1 for r in rows if str(r['source_url']) in pred_cache)} already predicted",
277+
file=sys.stderr)
278+
279+
def work(r):
280+
url = str(r["source_url"])
281+
if url in pred_cache:
282+
return url, pred_cache[url]
283+
diff = local_diffs.get_diff_cached(url, r["source_platform"], diff_cache)
284+
if not diff:
285+
return url, {"skip": "nodiff"}
286+
it = {"title": str(r.get("title") or "")[:200],
287+
"desc": str(r.get("description") or "")[:600], "diff": _diff_doc(diff)}
288+
return url, classify(it)["pred"]
289+
290+
done = 0
291+
with ThreadPoolExecutor(max_workers=a.workers) as ex:
292+
for url, pred in ex.map(work, rows):
293+
pred_cache[url] = pred
294+
done += 1
295+
if done % 40 == 0:
296+
a.pred_cache.write_text(json.dumps(pred_cache))
297+
a.cache.write_text(json.dumps(diff_cache))
298+
print(f" [apply] {done}/{len(rows)}", file=sys.stderr)
299+
a.pred_cache.write_text(json.dumps(pred_cache))
300+
a.cache.write_text(json.dumps(diff_cache))
301+
302+
a.apply_out.parent.mkdir(parents=True, exist_ok=True)
303+
n_fix = 0
304+
with a.apply_out.open("w", newline="", encoding="utf-8") as fh:
305+
w = csv.writer(fh)
306+
w.writerow(["source_url", "silent_fix_prob", "is_security_fix", "vuln_class", "reason"])
307+
for r in rows:
308+
url = str(r["source_url"]); pr = pred_cache.get(url, {})
309+
if not isinstance(pr, dict) or "is_security_fix" not in pr:
310+
continue
311+
isfix = bool(pr.get("is_security_fix"))
312+
conf = float(pr.get("confidence") or 0)
313+
prob = conf if isfix else 1 - conf # p(security fix)
314+
if prob >= 0.70:
315+
n_fix += 1
316+
w.writerow([url, f"{prob:.3f}", int(isfix), pr.get("vuln_class", ""),
317+
str(pr.get("reason", ""))[:200]])
318+
print(f"[apply] wrote {a.apply_out}{n_fix} rows with silent_fix_prob>=0.70", file=sys.stderr)
319+
return 0
320+
321+
256322
def main() -> int:
257323
ap = argparse.ArgumentParser()
258324
ap.add_argument("--build-eval", action="store_true")
259325
ap.add_argument("--run", action="store_true")
326+
ap.add_argument("--apply", action="store_true", help="classify dataset rows -> silent_fix csv")
327+
ap.add_argument("--tier", default="C_candidate", help="authority_tier to classify (or 'all')")
328+
ap.add_argument("--apply-out", default=Path("scratchpad_crawl/supp/llm_silent_fix.csv"), type=Path)
329+
ap.add_argument("--pred-cache", default=Path("scratchpad_crawl/llm_pred_cache.json"), type=Path)
330+
ap.add_argument("--limit", type=int, default=0)
260331
ap.add_argument("--eval-set", default=Path("scratchpad_crawl/llm_eval_set.json"), type=Path)
261332
ap.add_argument("--out", default=Path("scratchpad_crawl/llm_preds.json"), type=Path)
262333
ap.add_argument("--in", dest="inp", default=Path("data/ethereum_vulns.parquet"), type=Path)
@@ -284,6 +355,9 @@ def main() -> int:
284355
if a.engine == "ollama" and a.workers > 2:
285356
a.workers = 2 # a single local model serializes; avoid thrashing
286357

358+
if a.apply:
359+
return apply_to_dataset(a)
360+
287361
if a.build_eval:
288362
cache = json.loads(a.cache.read_text())
289363
items = build_eval(pd.read_parquet(a.inp), pd.read_parquet(a.raw), cache, a.per_class, a.seed)

collection/run_pipeline.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,14 @@ mkdir -p data/raw
145145
stage publish_raw cp "$TRAIN" data/raw/train.classified.parquet
146146

147147
# --- Stage 9: curate --------------------------------------------------------
148+
# Optionally fold in the learned silent-fix signal (gemma4:31b via
149+
# llm_classify_fixes.py --apply). Present -> promotes classified C_candidate
150+
# fixes to the corroborated tier; absent -> deterministic build only.
151+
SILENT_FIX_ARG=()
152+
[ -f data/silent_fix_llm.csv ] && SILENT_FIX_ARG=(--silent-fix-csv data/silent_fix_llm.csv)
148153
stage curate PY pipeline/build_security_dataset.py \
149154
--in data/raw/train.classified.parquet \
150155
--out data/ethereum_vulns.parquet \
151-
--manifest data/manifest.json
156+
--manifest data/manifest.json "${SILENT_FIX_ARG[@]}"
152157

153158
echo "=== DONE. Curated -> data/ethereum_vulns.parquet ==="

data/BUILD_REPORT.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ Critical** (geth, besu, teku). Severities preserved through the canonical path.
4141

4242
## After (curated)
4343

44-
- rows: **1,877**
44+
- rows: **1,880**
4545
- residual boilerplate FP: **0**
46-
- **essential slice (A+B): 1,367** (was 173 rated-only) — 7.9× larger clean high-precision core
47-
- by authority_tier: {'B_corroborated': 1134, 'C_candidate': 510, 'A_authoritative': 233}
48-
- by n_signals: {1: 555, 2: 907, 3: 345, 4: 65, 5: 5} — 60% of rows now carry ≥2 independent signals
46+
- **essential slice (A+B): 1,535** (was 173 rated-only) — clean high-precision core
47+
- by authority_tier: {'B_corroborated': 1296, 'C_candidate': 345, 'A_authoritative': 239}
48+
- **learned silent-fix signal (gemma4:31b):** classified 339 C_candidate diffs,
49+
flagged 166 as real silent fixes (133 DoS / 16 consensus / 11 validation …),
50+
promoting them C→B. Model chosen by an 80-item eval sweep (F1 0.872, precision
51+
0.895); see `docs/model_evaluation.md`. Regenerate via
52+
`collection/llm_classify_fixes.py --apply``data/silent_fix_llm.csv`.
4953
- by severity: {'Unrated': 963, 'Info': 773, 'High': 63, 'Medium': 54, 'Low': 21, 'Critical': 3}
5054
(High/Medium dropped vs iter-1 because T2b removed 49 unrelated CVEs' bogus CVSS severities)
5155
- by source:

data/ethereum_vulns.parquet

1022 Bytes
Binary file not shown.

data/manifest.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"stride",
1717
"cwe_top25",
1818
"evidence",
19+
"silent_fix_prob",
1920
"security_score",
2021
"fix_commit",
2122
"confidence",
@@ -40,13 +41,13 @@
4041
"low": 1
4142
},
4243
"by_authority_tier": {
43-
"B_corroborated": 1130,
44-
"C_candidate": 511,
44+
"B_corroborated": 1296,
45+
"C_candidate": 345,
4546
"A_authoritative": 239
4647
},
4748
"by_n_signals": {
48-
"1": 556,
49-
"2": 907,
49+
"1": 390,
50+
"2": 1073,
5051
"3": 343,
5152
"4": 69,
5253
"5": 5

0 commit comments

Comments
 (0)