fold/bench/compare_literature.py
trvon 2dcbdbc75d
feat: harden Fold quality guard
Preserve source, binary, and runtime evidence boundaries while making precision and performance regressions reproducible.

- decompose analysis orchestration into bounded modules
- add differential, metamorphic, organic, and Yams change-time gates
- fix detector, P-Code, fuzz sanitizer, and report identity precision
2026-07-13 15:05:35 -04:00

533 lines
21 KiB
Python

#!/usr/bin/env python3
"""Compare Fold benchmark outputs with literature baselines and F1 ceilings."""
from __future__ import annotations
import argparse
import datetime as dt
import json
import re
import sys
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
RESULTS_ROOT = REPO_ROOT / "bench" / "results"
DEFAULT_LITERATURE = REPO_ROOT / "bench" / "literature_baselines.json"
sys.path.insert(0, str(REPO_ROOT / "scripts" / "benchmarks"))
from fold_eval.metrics import classification_metrics
SOLVER_RE = re.compile(
r"^\[solver-profile\]\s+(?P<detector>\S+)\s+"
r"queries=(?P<queries>\d+)\s+sat=(?P<sat>\d+)\s+unsat=(?P<unsat>\d+)\s+"
r"unknown=(?P<unknown>\d+)\s+decisions=(?P<decisions>\d+)\s+"
r"conflicts=(?P<conflicts>\d+)\s+restarts=(?P<restarts>\d+)\s+"
r"propagations=(?P<propagations>\d+)\s+z3_time_ms=(?P<z3_time_ms>\d+)\s+"
r"wall_ms=(?P<wall_ms>\d+)"
)
def now_iso() -> str:
return dt.datetime.now(dt.UTC).strftime("%Y-%m-%dT%H-%M-%SZ")
def pct(value: float) -> str:
return f"{100.0 * value:.1f}%"
def signed_int(value: int) -> str:
return f"{value:+d}"
def signed_seconds(value: float | None) -> str:
if value is None:
return ""
return f"{value:+.1f}s"
def score_metrics(tp: int, fp: int, fn: int) -> dict[str, float]:
metrics = classification_metrics(tp, fp, fn)
return {key: float(metrics[key]) for key in ("precision", "recall", "f1")}
def infer_cwe(*values: str) -> str:
for value in values:
match = re.search(r"(?:juliet[-_/]|CWE)(\d{3})", value)
if match:
return match.group(1)
match = re.search(r"[/_-](\d{3})(?:[/_-]|$)", value)
if match:
return match.group(1)
return "unknown"
def profile_aliases(kind: str) -> set[str]:
aliases = {part.strip() for part in kind.split(",") if part.strip()}
if "oob-access" in aliases:
aliases.add("oob")
if "dead" in aliases:
aliases.add("dead-branch")
return aliases
def parse_solver_profile_log(path: Path, aliases: set[str]) -> dict[str, int] | None:
if not path.exists():
return None
totals = {
"queries": 0,
"sat": 0,
"unsat": 0,
"unknown": 0,
"decisions": 0,
"conflicts": 0,
"restarts": 0,
"propagations": 0,
"z3_time_ms": 0,
"wall_ms": 0,
}
matched = False
for line in path.read_text(errors="replace").splitlines():
match = SOLVER_RE.match(line.strip())
if not match or match.group("detector") not in aliases:
continue
matched = True
for key in totals:
totals[key] += int(match.group(key))
return totals if matched else None
def row_from_score(
*,
source_path: Path,
tool: str,
variant: str,
cwe: str,
kind: str,
score: dict[str, Any],
findings_count: int | None,
elapsed_s: float | None,
z3_queries: float | None,
) -> dict[str, Any]:
tp = int(score.get("true_positive", 0) or 0)
fp = int(score.get("false_positive", 0) or 0)
fn = int(score.get("false_negative", 0) or 0)
gold_total = int(score.get("gold_total", tp + fn) or 0)
finding_groups = int(score.get("finding_groups", tp + fp) or 0)
metrics = score_metrics(tp, fp, fn)
return {
"source": str(source_path),
"tool": tool,
"variant": variant,
"cwe": cwe,
"kind": kind,
"true_positive": tp,
"false_positive": fp,
"false_negative": fn,
"gold_total": gold_total,
"finding_groups": finding_groups,
"findings_count": findings_count,
"elapsed_s": elapsed_s,
"z3_queries": z3_queries,
**metrics,
}
def rows_from_summary(path: Path, doc: dict[str, Any]) -> list[dict[str, Any]]:
cwe = infer_cwe(str(doc.get("name", "")), str(doc.get("gold", "")), str(doc.get("corpus", "")))
kind = ",".join(str(item) for item in doc.get("detectors", [])) or "unknown"
rows: list[dict[str, Any]] = []
for variant, data in doc.get("variant_summaries", {}).items():
score = data.get("score")
if not isinstance(score, dict):
continue
profile = data.get("profile_avg", {}) if isinstance(data.get("profile_avg"), dict) else {}
rows.append(row_from_score(
source_path=path,
tool="fold",
variant=str(variant),
cwe=cwe,
kind=kind,
score=score,
findings_count=int(data.get("findings_count", 0) or 0),
elapsed_s=float(data.get("elapsed_s_avg", 0.0) or 0.0),
z3_queries=float(profile.get("z3_queries", 0.0) or 0.0),
))
return rows
def rows_from_tool_payload(path: Path, doc: dict[str, Any]) -> list[dict[str, Any]]:
score = doc.get("score")
if not isinstance(score, dict):
return []
profile = doc.get("profile", {}) if isinstance(doc.get("profile"), dict) else {}
solver = profile.get("solver", {}) if isinstance(profile.get("solver"), dict) else {}
artifacts = doc.get("artifacts", {}) if isinstance(doc.get("artifacts"), dict) else {}
kind = str(doc.get("kind") or doc.get("detector") or "unknown")
if float(solver.get("queries", 0.0) or 0.0) == 0.0 and artifacts.get("stderr_log"):
recovered = parse_solver_profile_log(Path(str(artifacts["stderr_log"])), profile_aliases(kind))
if recovered is not None:
solver = recovered
return [row_from_score(
source_path=path,
tool=str(doc.get("tool", path.stem)),
variant=str(doc.get("variant", "default")),
cwe=str(doc.get("cwe") or infer_cwe(str(path))),
kind=kind,
score=score,
findings_count=len(doc.get("findings", []) or []),
elapsed_s=float(doc.get("elapsed_s", 0.0) or 0.0),
z3_queries=float(solver.get("queries", 0.0) or 0.0),
)]
def load_rows_from_path(path: Path) -> list[dict[str, Any]]:
if path.is_dir():
rows: list[dict[str, Any]] = []
if (path / "summary.json").exists():
rows.extend(load_rows_from_path(path / "summary.json"))
for payload_name in ("fold.json", "clang-sa.json", "esbmc.json"):
for child in sorted(path.glob(f"*/{payload_name}")):
rows.extend(load_rows_from_path(child))
return rows
doc = json.loads(path.read_text())
if not isinstance(doc, dict):
return []
if "variant_summaries" in doc:
return rows_from_summary(path, doc)
return rows_from_tool_payload(path, doc)
def discover_default_runs() -> list[Path]:
latest_by_name: dict[str, Path] = {}
for path in sorted(RESULTS_ROOT.glob("*summary-ab/summary.json")):
try:
name = str(json.loads(path.read_text()).get("name", path.parent.name))
except Exception:
name = path.parent.name
latest_by_name[name] = path
return list(latest_by_name.values())
def compute_ceiling(row: dict[str, Any]) -> dict[str, Any]:
tp = int(row["true_positive"])
fp = int(row["false_positive"])
fn = int(row["false_negative"])
gold_total = int(row["gold_total"])
finding_groups = int(row.get("finding_groups", tp + fp) or 0)
current = score_metrics(tp, fp, fn)
precision_only = score_metrics(tp, 0, fn)
recall_only = score_metrics(gold_total, fp, 0)
budget_tp = min(gold_total, max(finding_groups, tp))
alert_budget = score_metrics(budget_tp, 0, max(0, gold_total - budget_tp))
oracle = score_metrics(gold_total, 0, 0)
precision_gain = precision_only["f1"] - current["f1"]
recall_gain = recall_only["f1"] - current["f1"]
if recall_gain > precision_gain * 1.1:
bottleneck = "recall"
next_action = "prioritize missed patterns, interprocedural coverage, and path/model completeness"
elif precision_gain > recall_gain * 1.1:
bottleneck = "precision"
next_action = "prioritize false-positive pruning, stronger feasibility checks, and post-filters"
else:
bottleneck = "mixed"
next_action = "split work between missed patterns and false-positive pruning"
return {
"current": current,
"precision_only_ceiling": precision_only,
"recall_only_ceiling": recall_only,
"alert_budget_ceiling": alert_budget,
"oracle_ceiling": oracle,
"precision_gain_f1": precision_gain,
"recall_gain_f1": recall_gain,
"oracle_gap_f1": oracle["f1"] - current["f1"],
"bottleneck": bottleneck,
"next_action": next_action,
}
def best_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
best: dict[tuple[str, str], dict[str, Any]] = {}
def is_better(candidate: dict[str, Any], current: dict[str, Any]) -> bool:
# Favor recall/finding gains when F1 differences are within report
# rounding noise. This keeps ceilings aligned with discovery value
# instead of hiding useful extra TPs behind tiny precision tradeoffs.
f1_delta = float(candidate["f1"]) - float(current["f1"])
if abs(f1_delta) > 0.005:
return f1_delta > 0
candidate_rank = (
int(candidate["true_positive"]),
float(candidate["recall"]),
int(candidate.get("finding_groups", 0) or 0),
-int(candidate["false_negative"]),
-int(candidate["false_positive"]),
)
current_rank = (
int(current["true_positive"]),
float(current["recall"]),
int(current.get("finding_groups", 0) or 0),
-int(current["false_negative"]),
-int(current["false_positive"]),
)
return candidate_rank > current_rank
for row in rows:
key = (str(row["cwe"]), str(row["kind"]))
if key not in best or is_better(row, best[key]):
best[key] = row
return [best[key] for key in sorted(best)]
def signal_recommendation(signal: dict[str, Any]) -> str:
tp_delta = int(signal.get("tp_delta", 0) or 0)
fp_delta = int(signal.get("fp_delta", 0) or 0)
elapsed_delta_s = signal.get("elapsed_delta_s")
f1_delta = float(signal.get("f1_delta", 0.0) or 0.0)
if tp_delta > 0:
return "preserve finding gain; optimize implementation cost around this path"
if tp_delta < 0:
return "fix recall regression before optimizing speed"
if fp_delta > 0 and f1_delta < 0:
return "precision regression; tighten feasibility or scoring before expanding search"
if elapsed_delta_s is not None and float(elapsed_delta_s) > 10:
return "no discovery gain; reduce overhead or make this path lazy"
return "neutral regression guard"
def seconds_per_added_tp(elapsed_delta_s: float | None, tp_delta: int) -> float | None:
if elapsed_delta_s is None or elapsed_delta_s <= 0 or tp_delta <= 0:
return None
return elapsed_delta_s / tp_delta
def optimization_signals(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
groups: dict[tuple[str, str, str], list[dict[str, Any]]] = {}
for row in rows:
groups.setdefault((str(row["tool"]), str(row["cwe"]), str(row["kind"])), []).append(row)
signals: list[dict[str, Any]] = []
for (tool, cwe, kind), group in groups.items():
baseline = next((row for row in group if row.get("variant") == "baseline"), None)
if baseline is None:
continue
for row in group:
if row.get("variant") == "baseline":
continue
elapsed_delta_s = None
if row.get("elapsed_s") is not None and baseline.get("elapsed_s") is not None:
elapsed_delta_s = float(row["elapsed_s"]) - float(baseline["elapsed_s"])
z3_query_delta = None
if row.get("z3_queries") is not None and baseline.get("z3_queries") is not None:
z3_query_delta = float(row["z3_queries"]) - float(baseline["z3_queries"])
tp_delta = int(row["true_positive"]) - int(baseline["true_positive"])
signal = {
"tool": tool,
"cwe": cwe,
"kind": kind,
"variant": row["variant"],
"source": row.get("source"),
"tp_delta": tp_delta,
"fp_delta": int(row["false_positive"]) - int(baseline["false_positive"]),
"fn_delta": int(row["false_negative"]) - int(baseline["false_negative"]),
"recall_delta": float(row["recall"]) - float(baseline["recall"]),
"f1_delta": float(row["f1"]) - float(baseline["f1"]),
"finding_group_delta": int(row.get("finding_groups", 0) or 0)
- int(baseline.get("finding_groups", 0) or 0),
"elapsed_delta_s": elapsed_delta_s,
"z3_query_delta": z3_query_delta,
"seconds_per_added_tp": seconds_per_added_tp(elapsed_delta_s, tp_delta),
}
signal["recommendation"] = signal_recommendation(signal)
signals.append(signal)
def sort_key(signal: dict[str, Any]) -> tuple[Any, ...]:
tp_delta = int(signal.get("tp_delta", 0) or 0)
elapsed_delta = float(signal.get("elapsed_delta_s", 0.0) or 0.0)
if tp_delta > 0:
bucket = 0
elif tp_delta < 0:
bucket = 1
elif elapsed_delta > 10:
bucket = 2
else:
bucket = 3
return (bucket, -tp_delta, -elapsed_delta, str(signal["cwe"]), str(signal["kind"]))
return sorted(signals, key=sort_key)
def metric_text(metric: dict[str, Any]) -> str:
name = str(metric["name"]).replace("_", " ")
unit = metric.get("unit")
if "value_min" in metric and "value_max" in metric:
lo = float(metric["value_min"])
hi = float(metric["value_max"])
value = f"{pct(lo)}-{pct(hi)}" if unit == "ratio" else f"{lo:g}-{hi:g}"
else:
raw = float(metric.get("value", 0.0))
value = pct(raw) if unit == "ratio" else f"{raw:g}"
qualifier = str(metric.get("qualifier", ""))
if qualifier:
value = f"{qualifier} {value}"
return f"{name}: {value}"
def render_markdown(rows: list[dict[str, Any]], literature: dict[str, Any]) -> str:
best = best_rows(rows)
signals = optimization_signals(rows)
lines = [
"# Literature and Ceiling Comparison",
"",
"## Fold Runs",
"",
"| CWE | Kind | Variant | TP | FP | FN | Precision | Recall | F1 | Findings | Time | Z3 Queries |",
"|-----|------|---------|----|----|----|-----------|--------|----|----------|------|------------|",
]
for row in sorted(rows, key=lambda item: (item["cwe"], item["kind"], item["variant"])):
elapsed = "" if row.get("elapsed_s") is None else f"{float(row['elapsed_s']):.1f}s"
z3 = "" if row.get("z3_queries") is None else f"{float(row['z3_queries']):.0f}"
lines.append(
f"| CWE-{row['cwe']} | {row['kind']} | {row['variant']} | {row['true_positive']} | "
f"{row['false_positive']} | {row['false_negative']} | {pct(row['precision'])} | "
f"{pct(row['recall'])} | {pct(row['f1'])} | {row['finding_groups']} | {elapsed} | {z3} |"
)
lines.extend([
"",
"## Best Fold Variant vs Ceilings",
"",
"| CWE | Variant | TP | Recall | Current F1 | No-FP F1 | Full-Recall F1 | Alert-Budget F1 | Oracle Gap | Bottleneck | Next Action |",
"|-----|---------|----|--------|------------|----------|----------------|-----------------|------------|------------|-------------|",
])
for row in best:
ceiling = compute_ceiling(row)
lines.append(
f"| CWE-{row['cwe']} | {row['variant']} | {row['true_positive']} | "
f"{pct(row['recall'])} | {pct(ceiling['current']['f1'])} | "
f"{pct(ceiling['precision_only_ceiling']['f1'])} | {pct(ceiling['recall_only_ceiling']['f1'])} | "
f"{pct(ceiling['alert_budget_ceiling']['f1'])} | {pct(ceiling['oracle_gap_f1'])} | "
f"{ceiling['bottleneck']} | {ceiling['next_action']} |"
)
if signals:
lines.extend([
"",
"## Optimization Signals",
"",
"| CWE | Kind | Variant | TP Delta | FP Delta | Recall Delta | F1 Delta | Time Delta | Sec / Added TP | Query Delta | Recommendation |",
"|-----|------|---------|----------|----------|--------------|----------|------------|----------------|-------------|----------------|",
])
for signal in signals:
sec_per_tp = signal.get("seconds_per_added_tp")
sec_per_tp_text = "n/a" if sec_per_tp is None else f"{float(sec_per_tp):.1f}s"
query_delta = signal.get("z3_query_delta")
query_delta_text = "" if query_delta is None else signed_int(int(round(float(query_delta))))
lines.append(
f"| CWE-{signal['cwe']} | {signal['kind']} | {signal['variant']} | "
f"{signed_int(int(signal['tp_delta']))} | {signed_int(int(signal['fp_delta']))} | "
f"{float(signal['recall_delta']):+.1%} | {float(signal['f1_delta']):+.1%} | "
f"{signed_seconds(signal.get('elapsed_delta_s'))} | {sec_per_tp_text} | "
f"{query_delta_text} | {signal['recommendation']} |"
)
lines.extend([
"",
"## Literature Baselines",
"",
"| Baseline | Benchmark | Scope | Reported Metrics | Comparability |",
"|----------|-----------|-------|------------------|---------------|",
])
for baseline in literature.get("baselines", []):
metrics = "; ".join(metric_text(metric) for metric in baseline.get("metrics", [])) or "methodology only"
lines.append(
f"| {baseline['id']} | {baseline['benchmark']} | {baseline['tool_scope']} | "
f"{metrics} | {baseline['comparability']} |"
)
lines.extend([
"",
"## Benchmark Expansion Targets",
"",
"| Target | Status | Runner | Scope | Next Action |",
"|--------|--------|--------|-------|-------------|",
])
for target in literature.get("benchmark_targets", []):
lines.append(
f"| {target['id']} | {target['status']} | `{target['runner']}` | {target['scope']} | {target['next_action']} |"
)
lines.extend([
"",
"## Sources",
"",
"| ID | Year | Title | DOI |",
"|----|------|-------|-----|",
])
for ref in literature.get("references", []):
lines.append(f"| {ref['id']} | {ref['year']} | {ref['title']} | {ref['doi']} |")
lines.append("")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--fold-run", action="append", default=[], help="summary.json, tool payload JSON, or result directory")
parser.add_argument("--literature", default=str(DEFAULT_LITERATURE))
parser.add_argument("--out-dir", default="", help="Output directory; defaults to bench/results/<timestamp>_literature-comparison")
parser.add_argument("--no-write", action="store_true", help="Print only; do not write result artifacts")
args = parser.parse_args()
literature_path = Path(args.literature).resolve()
if not literature_path.exists():
print(f"literature baseline file not found: {literature_path}", file=sys.stderr)
return 2
literature = json.loads(literature_path.read_text())
input_paths = [Path(item).resolve() for item in args.fold_run]
if not input_paths:
input_paths = discover_default_runs()
if not input_paths:
print("no benchmark summaries found; pass --fold-run", file=sys.stderr)
return 2
rows: list[dict[str, Any]] = []
for path in input_paths:
if not path.exists():
print(f"benchmark input not found: {path}", file=sys.stderr)
return 2
rows.extend(load_rows_from_path(path))
rows = [row for row in rows if row.get("gold_total", 0)]
if not rows:
print("no scored benchmark rows found", file=sys.stderr)
return 2
payload = {
"timestamp": now_iso(),
"inputs": [str(path) for path in input_paths],
"literature": str(literature_path),
"fold_rows": rows,
"ceilings": [{**row, "ceiling": compute_ceiling(row)} for row in best_rows(rows)],
"optimization_signals": optimization_signals(rows),
"literature_baselines": literature.get("baselines", []),
"benchmark_targets": literature.get("benchmark_targets", []),
}
report = render_markdown(rows, literature)
if not args.no_write:
out_dir = Path(args.out_dir).resolve() if args.out_dir else RESULTS_ROOT / f"{payload['timestamp']}_literature-comparison"
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "summary.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
(out_dir / "summary.md").write_text(report + "\n")
print(report)
print(f"wrote {out_dir}")
else:
print(report)
return 0
if __name__ == "__main__":
raise SystemExit(main())