fold/bench/run_summary_ab.py
trvon c4e42cec0a
Add scan router and MCP job queue
Split source and binary scan routing with merge-only hybrid support, binary provenance, and router-backed MCP scan contexts.

Add async MCP scan jobs for long-running scans, detector precision fixes, benchmark scripts, docs, and integration coverage.
2026-05-19 00:24:12 -04:00

727 lines
28 KiB
Python

#!/usr/bin/env python3
"""A/B benchmark fold with and without symbolic summaries.
The harness is intentionally small and reproducible: it builds a temporary
compile_commands.json for a corpus, runs `fold scan` in profile mode for each
variant, captures SARIF/profile/log artifacts, and writes a compact delta report.
Default corpus: bench/corpora/interproc, which exercises guarded helper chains
and cross-file callsites without requiring downloads.
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import os
import shlex
import subprocess
import sys
import time
from pathlib import Path
from statistics import mean
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_FOLD = REPO_ROOT / "build" / "release" / "src" / "cli" / "fold"
DEFAULT_CORPUS = REPO_ROOT / "bench" / "corpora" / "interproc"
RESULTS_ROOT = REPO_ROOT / "bench" / "results"
LLVM_BIN = Path(os.environ.get("LLVM_BIN", "/opt/homebrew/opt/llvm@22/bin"))
def now_iso() -> str:
return dt.datetime.now(dt.UTC).strftime("%Y-%m-%dT%H-%M-%SZ")
def tool_path(name: str) -> str:
candidate = LLVM_BIN / name
return str(candidate if candidate.exists() else name)
def write_compile_db(
corpus_dir: Path, db_path: Path, c_standard: str, cxx_standard: str
) -> list[dict[str, str]]:
include_dirs: set[str] = set()
for path in sorted(corpus_dir.rglob("*")):
if path.is_dir() and path.name == "testcasesupport":
include_dirs.add(str(path))
if path.is_dir() and any(path.glob("*.h")):
include_dirs.add(str(path))
include_args = [arg for inc in sorted(include_dirs) for arg in ("-I", inc)]
entries: list[dict[str, str]] = []
for src in sorted(corpus_dir.rglob("*.c")):
command = [tool_path("clang"), f"-std={c_standard}", *include_args, "-c", str(src)]
entries.append({
"directory": str(corpus_dir),
"command": shlex.join(command),
"file": str(src),
})
for src in sorted(corpus_dir.rglob("*.cpp")):
command = [tool_path("clang++"), f"-std={cxx_standard}", *include_args, "-c", str(src)]
entries.append({
"directory": str(corpus_dir),
"command": shlex.join(command),
"file": str(src),
})
db_path.write_text(json.dumps(entries, indent=2) + "\n")
return entries
def normalize_kind(rule_id: str) -> str:
kind = rule_id.removeprefix("fold.")
return {
"dead-branch": "dead",
"duplicate-class": "class-dup",
}.get(kind, kind)
def parse_sarif(path: Path) -> list[dict[str, Any]]:
if not path.exists():
return []
doc = json.loads(path.read_text())
findings: list[dict[str, Any]] = []
for run in doc.get("runs", []):
for result in run.get("results", []):
kind = normalize_kind(str(result.get("ruleId", "fold.unknown")))
fid = str(result.get("properties", {}).get("id", ""))
message = str(result.get("message", {}).get("text", ""))
locations = []
for loc in result.get("locations", []):
physical = loc.get("physicalLocation", {})
artifact = physical.get("artifactLocation", {})
region = physical.get("region", {})
locations.append({
"file": artifact.get("uri", ""),
"line": int(region.get("startLine", 0) or 0),
"column": int(region.get("startColumn", 0) or 0),
})
findings.append({
"kind": kind,
"id": fid,
"message": message,
"locations": locations,
})
return sorted(findings, key=finding_key)
def finding_key(finding: dict[str, Any]) -> tuple[Any, ...]:
locs = finding.get("locations") or []
loc_key = tuple(
(str(loc.get("file", "")), int(loc.get("line", 0) or 0), int(loc.get("column", 0) or 0))
for loc in locs
)
return (finding.get("kind", ""), finding.get("id", ""), loc_key)
def compact_finding_key(finding: dict[str, Any]) -> str:
kind, fid, loc_key = finding_key(finding)
loc_text = ";".join(f"{Path(file).name}:{line}:{col}" for file, line, col in loc_key)
return f"{kind}|{fid}|{loc_text}"
def detector_totals(profile: dict[str, Any]) -> dict[str, int]:
totals = {
"wall_ms": 0,
"z3_queries": 0,
"z3_sat": 0,
"z3_unsat": 0,
"z3_unknown": 0,
"z3_timeouts": 0,
"abstract_sat": 0,
"abstract_unsat": 0,
"states_merged": 0,
"states_kept_separate": 0,
"findings_emitted": 0,
"functions_analyzed": 0,
"functions_skipped": 0,
}
for detector in profile.get("detectors", []):
for key in totals:
totals[key] += int(detector.get(key, 0) or 0)
return totals
def profile_summary(path: Path) -> dict[str, Any]:
if not path.exists():
return {"scan": {}, "detectors_total": detector_totals({}), "detectors": []}
profile = json.loads(path.read_text())
return {
"scan": profile.get("scan", {}),
"detectors_total": detector_totals(profile),
"detectors": profile.get("detectors", []),
}
def suffix_match(path: str, expected: str) -> bool:
return path.replace("\\", "/").endswith(expected.replace("\\", "/"))
def score_against_gold(
findings: list[dict[str, Any]], gold_path: Path | None, score_mode: str
) -> dict[str, Any] | None:
if gold_path is None or not gold_path.exists():
return None
gold = json.loads(gold_path.read_text())
expected = gold.get("expected", [])
if score_mode == "file":
return score_file_level(findings, expected, gold.get("name", gold_path.stem))
matched_gold: set[int] = set()
matched_findings: set[int] = set()
for gi, item in enumerate(expected):
kind = item.get("kind", "any")
files = [str(file) for file in item.get("files", [])]
for fi, finding in enumerate(findings):
if kind != "any" and finding.get("kind") != kind:
continue
finding_files = [str(loc.get("file", "")) for loc in finding.get("locations", [])]
if all(any(suffix_match(file, expected_file) for file in finding_files) for expected_file in files):
matched_gold.add(gi)
matched_findings.add(fi)
break
tp = len(matched_gold)
fp = max(0, len(findings) - len(matched_findings))
fn = len(expected) - tp
precision = tp / (tp + fp) if tp + fp else 0.0
recall = tp / len(expected) if expected else 0.0
f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0
return {
"gold_name": gold.get("name", gold_path.stem),
"true_positive": tp,
"false_positive": fp,
"false_negative": fn,
"gold_total": len(expected),
"precision": precision,
"recall": recall,
"f1": f1,
"matched_gold_indices": sorted(matched_gold),
"missed_gold_indices": sorted(set(range(len(expected))) - matched_gold),
}
def score_file_level(
findings: list[dict[str, Any]], expected: list[dict[str, Any]], gold_name: str
) -> dict[str, Any]:
finding_groups: set[tuple[str, str]] = set()
for finding in findings:
kind = str(finding.get("kind", "unknown"))
for loc in finding.get("locations", []):
file = str(loc.get("file", ""))
if file:
finding_groups.add((kind, file))
matched_gold: set[int] = set()
matched_groups: set[tuple[str, str]] = set()
for gi, item in enumerate(expected):
kind = str(item.get("kind", "any"))
files = [str(file) for file in item.get("files", [])]
for group_kind, group_file in finding_groups:
if kind != "any" and group_kind != kind:
continue
if any(suffix_match(group_file, expected_file) for expected_file in files):
matched_gold.add(gi)
matched_groups.add((group_kind, group_file))
break
tp = len(matched_gold)
fp = max(0, len(finding_groups) - len(matched_groups))
fn = len(expected) - tp
precision = tp / (tp + fp) if tp + fp else 0.0
recall = tp / len(expected) if expected else 0.0
f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0
return {
"gold_name": gold_name,
"true_positive": tp,
"false_positive": fp,
"false_negative": fn,
"gold_total": len(expected),
"finding_groups": len(finding_groups),
"precision": precision,
"recall": recall,
"f1": f1,
"matched_gold_indices": sorted(matched_gold),
"missed_gold_indices": sorted(set(range(len(expected))) - matched_gold),
}
def variant_args(variant: str) -> list[str]:
if variant == "baseline":
return []
if variant == "summaries":
return ["--build-summaries"]
raise ValueError(f"unknown variant: {variant}")
def run_variant(
fold_bin: Path,
db_path: Path,
out_dir: Path,
detectors: list[str],
variant: str,
repeat: int,
processes: int,
exclude_globs: list[str],
extra_args: list[str],
gold_path: Path | None,
score_mode: str,
) -> dict[str, Any]:
run_dir = out_dir / variant / f"repeat-{repeat}"
run_dir.mkdir(parents=True, exist_ok=True)
sarif_path = run_dir / "fold.sarif"
profile_path = run_dir / "profile.json"
stdout_path = run_dir / "stdout.log"
stderr_path = run_dir / "stderr.log"
command = [
str(fold_bin),
"scan",
"--compile-db",
str(db_path),
"--format",
"sarif",
"--output",
str(sarif_path),
"--min-body-stmts",
"1",
"--processes",
str(processes),
"--profile",
"--profile-output",
str(profile_path),
]
for detector in detectors:
command.extend(["--detectors", detector])
for pattern in exclude_globs:
command.extend(["--exclude-glob", pattern])
command.extend(variant_args(variant))
command.extend(extra_args)
env = os.environ.copy()
env["PATH"] = f"{LLVM_BIN}:{env.get('PATH', '')}"
started = time.monotonic()
proc = subprocess.run(command, capture_output=True, text=True, env=env)
elapsed_s = time.monotonic() - started
stdout_path.write_text(proc.stdout)
stderr_path.write_text(proc.stderr)
findings = parse_sarif(sarif_path)
score = score_against_gold(findings, gold_path, score_mode)
payload = {
"variant": variant,
"repeat": repeat,
"command": command,
"elapsed_s": elapsed_s,
"exit_code": proc.returncode,
"findings_count": len(findings),
"findings": findings,
"finding_keys": [compact_finding_key(finding) for finding in findings],
"profile": profile_summary(profile_path),
"score": score,
"artifacts": {
"sarif": str(sarif_path),
"profile": str(profile_path),
"stdout": str(stdout_path),
"stderr": str(stderr_path),
},
}
(run_dir / "result.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
if proc.returncode not in (0, 1):
raise RuntimeError(
f"fold scan failed for {variant} repeat {repeat} with rc={proc.returncode}; see {stderr_path}"
)
return payload
def refresh_result_from_artifacts(
payload: dict[str, Any], gold_path: Path | None, score_mode: str
) -> dict[str, Any]:
raw_artifacts = payload.get("artifacts")
artifacts: dict[str, Any] = raw_artifacts if isinstance(raw_artifacts, dict) else {}
sarif = artifacts.get("sarif")
if not sarif:
return payload
sarif_path = Path(str(sarif))
if not sarif_path.exists():
return payload
findings = parse_sarif(sarif_path)
payload["findings_count"] = len(findings)
payload["findings"] = findings
payload["finding_keys"] = [compact_finding_key(finding) for finding in findings]
payload["score"] = score_against_gold(findings, gold_path, score_mode)
return payload
def average_runs(runs: list[dict[str, Any]]) -> dict[str, Any]:
if not runs:
return {}
total_keys = [
"wall_ms",
"z3_queries",
"z3_sat",
"z3_unsat",
"z3_unknown",
"z3_timeouts",
"abstract_sat",
"abstract_unsat",
"states_merged",
"states_kept_separate",
"findings_emitted",
]
profile_avg = {
key: mean(run["profile"]["detectors_total"].get(key, 0) for run in runs) for key in total_keys
}
scan_avg = {
key: mean(run["profile"].get("scan", {}).get(key, 0) for run in runs)
for key in ["total_wall_ms", "parse_ms", "analyze_ms", "report_ms", "num_tus"]
}
score = runs[-1].get("score")
return {
"variant": runs[-1]["variant"],
"repeats": len(runs),
"elapsed_s_avg": mean(run["elapsed_s"] for run in runs),
"findings_count": runs[-1]["findings_count"],
"finding_keys": runs[-1]["finding_keys"],
"profile_avg": profile_avg,
"scan_avg": scan_avg,
"score": score,
}
def pct_delta(new: float, old: float) -> str:
if old == 0:
return "n/a"
return f"{((new - old) / old) * 100:+.1f}%"
def signed_int(value: int) -> str:
return f"{value:+d}"
def safe_ratio(new: float, old: float) -> float | None:
if old == 0:
return None
return new / old
def seconds_per_tp(elapsed_delta_s: float, tp_delta: int) -> float | None:
if tp_delta <= 0 or elapsed_delta_s <= 0:
return None
return elapsed_delta_s / tp_delta
def matched_indices(score: dict[str, Any]) -> set[int]:
if isinstance(score.get("matched_gold_indices"), list):
return {int(idx) for idx in score.get("matched_gold_indices", [])}
gold_total = int(score.get("gold_total", 0) or 0)
missed = {int(idx) for idx in score.get("missed_gold_indices", []) or []}
return set(range(gold_total)) - missed
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 = float(signal.get("elapsed_delta_s", 0.0) or 0.0)
f1_delta = float(signal.get("f1_delta", 0.0) or 0.0)
if tp_delta > 0:
return "preserve finding gain; optimize summary precompute/caching around this path"
if tp_delta < 0:
return "fix recall regression before treating this as an optimization win"
if fp_delta > 0 and f1_delta < 0:
return "precision-only regression; tighten feasibility before adding more search"
if elapsed_delta_s > 10:
return "no gold gain; reduce implementation overhead or make this path lazy"
return "neutral; keep as regression guard"
def compute_optimization_signals(variants: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
baseline = variants.get("baseline")
if not baseline:
return {}
base_score = baseline.get("score") or {}
base_profile = baseline.get("profile_avg") or {}
base_keys = set(baseline.get("finding_keys", []))
signals: dict[str, dict[str, Any]] = {}
for name, data in variants.items():
if name == "baseline":
continue
score = data.get("score") or {}
profile = data.get("profile_avg") or {}
keys = set(data.get("finding_keys", []))
elapsed_delta_s = float(data.get("elapsed_s_avg", 0.0) or 0.0) - float(
baseline.get("elapsed_s_avg", 0.0) or 0.0
)
tp_delta = int(score.get("true_positive", 0) or 0) - int(
base_score.get("true_positive", 0) or 0
)
base_matched = matched_indices(base_score)
variant_matched = matched_indices(score)
signal = {
"variant": name,
"added_findings": len(keys - base_keys),
"removed_findings": len(base_keys - keys),
"finding_delta": int(data.get("findings_count", 0) or 0)
- int(baseline.get("findings_count", 0) or 0),
"tp_delta": tp_delta,
"fp_delta": int(score.get("false_positive", 0) or 0)
- int(base_score.get("false_positive", 0) or 0),
"fn_delta": int(score.get("false_negative", 0) or 0)
- int(base_score.get("false_negative", 0) or 0),
"recall_delta": float(score.get("recall", 0.0) or 0.0)
- float(base_score.get("recall", 0.0) or 0.0),
"f1_delta": float(score.get("f1", 0.0) or 0.0)
- float(base_score.get("f1", 0.0) or 0.0),
"elapsed_delta_s": elapsed_delta_s,
"elapsed_ratio": safe_ratio(
float(data.get("elapsed_s_avg", 0.0) or 0.0),
float(baseline.get("elapsed_s_avg", 0.0) or 0.0),
),
"detector_wall_delta_ms": float(profile.get("wall_ms", 0.0) or 0.0)
- float(base_profile.get("wall_ms", 0.0) or 0.0),
"z3_query_delta": float(profile.get("z3_queries", 0.0) or 0.0)
- float(base_profile.get("z3_queries", 0.0) or 0.0),
"seconds_per_added_tp": seconds_per_tp(elapsed_delta_s, tp_delta),
"gained_gold_indices": sorted(variant_matched - base_matched),
"lost_gold_indices": sorted(base_matched - variant_matched),
}
signal["recommendation"] = signal_recommendation(signal)
signals[name] = signal
return signals
def render_markdown(summary: dict[str, Any]) -> str:
variants = summary["variant_summaries"]
baseline = variants.get("baseline")
max_delta_details = int(summary.get("max_delta_details", 50) or 0)
optimization_signals = summary.get("optimization_signals") or compute_optimization_signals(variants)
lines = [
f"# Summary A/B Benchmark: {summary['name']}",
"",
f"Corpus: `{summary['corpus']}`",
f"Detectors: `{', '.join(summary['detectors'])}`",
f"Repeats: `{summary['repeats']}`",
f"Processes: `{summary['processes']}`",
f"Score Mode: `{summary['score_mode']}`",
"",
"| Variant | Findings | Elapsed Avg | Profile Wall Avg | Z3 Queries | Z3 SAT | Z3 UNSAT | Abstract SAT | Abstract UNSAT | Unknown | States Merged | States Split |",
"|---------|----------|-------------|------------------|------------|--------|----------|--------------|----------------|---------|---------------|--------------|",
]
for name, data in variants.items():
profile = data["profile_avg"]
lines.append(
f"| {name} | {data['findings_count']} | {data['elapsed_s_avg']:.3f}s | "
f"{profile['wall_ms']:.1f}ms | {profile['z3_queries']:.1f} | {profile['z3_sat']:.1f} | "
f"{profile['z3_unsat']:.1f} | {profile['abstract_sat']:.1f} | "
f"{profile['abstract_unsat']:.1f} | {profile['z3_unknown']:.1f} | "
f"{profile['states_merged']:.1f} | {profile['states_kept_separate']:.1f} |"
)
if baseline:
lines.extend(["", "## Delta vs Baseline", ""])
base_keys = set(baseline["finding_keys"])
base_profile = baseline["profile_avg"]
delta_details = []
lines.append("| Variant | Added Findings | Removed Findings | Query Delta | Wall Delta |")
lines.append("|---------|----------------|------------------|-------------|------------|")
for name, data in variants.items():
if name == "baseline":
continue
keys = set(data["finding_keys"])
added = sorted(keys - base_keys)
removed = sorted(base_keys - keys)
delta_details.append((name, added, removed))
query_delta = pct_delta(data["profile_avg"]["z3_queries"], base_profile["z3_queries"])
wall_delta = pct_delta(data["profile_avg"]["wall_ms"], base_profile["wall_ms"])
lines.append(
f"| {name} | {len(added)} | {len(removed)} | {query_delta} | {wall_delta} |"
)
for name, added, removed in delta_details:
if not added and not removed:
continue
lines.extend(["", f"### {name} Finding Changes", ""])
if added:
lines.append("Added:")
shown = added[:max_delta_details]
lines.extend(f"- `{key}`" for key in shown)
if len(added) > len(shown):
lines.append(f"- ... {len(added) - len(shown)} more omitted")
if removed:
lines.append("Removed:")
shown = removed[:max_delta_details]
lines.extend(f"- `{key}`" for key in shown)
if len(removed) > len(shown):
lines.append(f"- ... {len(removed) - len(shown)} more omitted")
if optimization_signals:
lines.extend(["", "## Optimization Signal", ""])
lines.append(
"| Variant | TP Delta | FP Delta | Recall Delta | F1 Delta | Extra Time | Sec / Added TP | Recommendation |"
)
lines.append(
"|---------|----------|----------|--------------|----------|------------|----------------|----------------|"
)
for name, signal in optimization_signals.items():
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"
lines.append(
f"| {name} | {signed_int(int(signal['tp_delta']))} | "
f"{signed_int(int(signal['fp_delta']))} | "
f"{float(signal['recall_delta']):+.1%} | {float(signal['f1_delta']):+.1%} | "
f"{float(signal['elapsed_delta_s']):+.1f}s | {sec_per_tp_text} | "
f"{signal['recommendation']} |"
)
if any(data.get("score") for data in variants.values()):
lines.extend(["", "## Gold Score", ""])
lines.append("| Variant | TP | FP | FN | Precision | Recall | F1 |")
lines.append("|---------|----|----|----|-----------|--------|----|")
for name, data in variants.items():
score = data.get("score")
if not score:
continue
lines.append(
f"| {name} | {score['true_positive']} | {score['false_positive']} | "
f"{score['false_negative']} | {score['precision']:.1%} | "
f"{score['recall']:.1%} | {score['f1']:.1%} |"
)
if baseline and baseline.get("score"):
base_score = baseline["score"]
lines.extend(["", "## Gold Delta vs Baseline", ""])
lines.append("| Variant | TP Delta | FP Delta | FN Delta | Recall Delta | F1 Delta |")
lines.append("|---------|----------|----------|----------|--------------|----------|")
for name, data in variants.items():
if name == "baseline":
continue
score = data.get("score")
if not score:
continue
lines.append(
f"| {name} | "
f"{signed_int(score['true_positive'] - base_score['true_positive'])} | "
f"{signed_int(score['false_positive'] - base_score['false_positive'])} | "
f"{signed_int(score['false_negative'] - base_score['false_negative'])} | "
f"{score['recall'] - base_score['recall']:+.1%} | "
f"{score['f1'] - base_score['f1']:+.1%} |"
)
lines.append("")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--corpus", default=str(DEFAULT_CORPUS))
parser.add_argument("--name", default="summary-ab")
parser.add_argument("--gold", default=None, help="Optional gold JSON; defaults to <corpus>/gold.json when present")
parser.add_argument("--fold-bin", default=str(DEFAULT_FOLD))
parser.add_argument("--out-root", default=str(RESULTS_ROOT))
parser.add_argument("--out-dir", default=None, help="Existing or new exact output directory")
parser.add_argument("--detectors", default="null-deref,div-by-zero,oob,int-overflow")
parser.add_argument("--variants", default="baseline,summaries")
parser.add_argument("--repeats", type=int, default=1)
parser.add_argument("--reuse-existing", action="store_true", help="Reuse existing repeat result.json files")
parser.add_argument("--processes", type=int, default=1)
parser.add_argument("--c-standard", default="c99")
parser.add_argument("--cxx-standard", default="c++23")
parser.add_argument("--exclude-glob", action="append", default=[])
parser.add_argument("--score-mode", choices=("finding", "file"), default="finding")
parser.add_argument("--max-delta-details", type=int, default=50)
parser.add_argument("--extra-arg", action="append", default=[], help="Extra argument passed to fold scan")
args = parser.parse_args()
corpus = Path(args.corpus).resolve()
if not corpus.is_dir():
print(f"corpus path is not a directory: {corpus}", file=sys.stderr)
return 2
fold_bin = Path(args.fold_bin).resolve()
if not fold_bin.exists():
print(f"fold binary not found: {fold_bin}", file=sys.stderr)
return 2
detectors = [detector.strip() for detector in args.detectors.split(",") if detector.strip()]
variants = [variant.strip() for variant in args.variants.split(",") if variant.strip()]
if args.repeats <= 0:
print("--repeats must be positive", file=sys.stderr)
return 2
if args.processes <= 0:
print("--processes must be positive", file=sys.stderr)
return 2
gold_path = Path(args.gold).resolve() if args.gold else corpus / "gold.json"
if not gold_path.exists():
gold_path = None
stamp = now_iso()
out_dir = Path(args.out_dir).resolve() if args.out_dir else Path(args.out_root).resolve() / f"{stamp}_{args.name}"
out_dir.mkdir(parents=True, exist_ok=True)
db_path = out_dir / "compile_commands.json"
entries = write_compile_db(corpus, db_path, args.c_standard, args.cxx_standard)
runs_by_variant: dict[str, list[dict[str, Any]]] = {variant: [] for variant in variants}
for variant in variants:
for repeat in range(1, args.repeats + 1):
existing_result = out_dir / variant / f"repeat-{repeat}" / "result.json"
if args.reuse_existing and existing_result.exists():
print(f"reusing {variant} repeat {repeat}/{args.repeats}")
payload = refresh_result_from_artifacts(
json.loads(existing_result.read_text()), gold_path, args.score_mode
)
existing_result.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
runs_by_variant[variant].append(payload)
else:
print(f"running {variant} repeat {repeat}/{args.repeats}")
runs_by_variant[variant].append(
run_variant(
fold_bin,
db_path,
out_dir,
detectors,
variant,
repeat,
args.processes,
args.exclude_glob,
args.extra_arg,
gold_path,
args.score_mode,
)
)
variant_summaries = {variant: average_runs(runs) for variant, runs in runs_by_variant.items()}
optimization_signals = compute_optimization_signals(variant_summaries)
summary = {
"name": args.name,
"timestamp": stamp,
"corpus": str(corpus),
"compile_db": str(db_path),
"source_entries": len(entries),
"fold_bin": str(fold_bin),
"detectors": detectors,
"variants": variants,
"repeats": args.repeats,
"processes": args.processes,
"c_standard": args.c_standard,
"cxx_standard": args.cxx_standard,
"exclude_globs": args.exclude_glob,
"score_mode": args.score_mode,
"max_delta_details": args.max_delta_details,
"gold": str(gold_path) if gold_path else None,
"runs": runs_by_variant,
"variant_summaries": variant_summaries,
"optimization_signals": optimization_signals,
}
(out_dir / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n")
report = render_markdown(summary)
(out_dir / "summary.md").write_text(report)
print(report)
print(f"wrote {out_dir}")
return 0
if __name__ == "__main__":
raise SystemExit(main())