| |
| """ |
| Run :class:`~frame.pipeline.formal_model_building_pipeline.FormalModelBuildingPipeline` |
| for a range of ``spec_id`` rows in ``datasets/spec.csv``. |
| |
| Uses UPPAAL IR KB for the Builder **unless** ``--no-rag`` is set. |
| |
| Writes: |
| artifacts/formal_build/batch_formal_<kind>_<stamp>/report.jsonl (one JSON object per spec) |
| artifacts/formal_build/batch_formal_<kind>_<stamp>/S{nn}_{label}/... per spec |
| |
| Usage: |
| python scripts/run_formal_batch_kb.py --start 1 --end 20 --no-rag |
| python scripts/run_formal_batch_kb.py --start 1 --end 20 --rebuild-kb-index # KB on |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import csv |
| import json |
| import re |
| import subprocess |
| import sys |
| import time |
| from datetime import datetime |
| from pathlib import Path |
| from typing import Any, Dict, List, Tuple |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(ROOT / "src")) |
|
|
| from frame.pipeline.formal_model_building_pipeline import FormalModelBuildingPipeline |
|
|
| SPEC_CSV = ROOT / "datasets" / "spec.csv" |
|
|
|
|
| def _safe_label(s: str) -> str: |
| s = (s or "").strip() or "spec" |
| return re.sub(r"[^\w\-]+", "_", s)[:80] |
|
|
|
|
| def load_all_specs(spec_csv: Path) -> List[Dict[str, str]]: |
| rows: List[Dict[str, str]] = [] |
| with spec_csv.open(encoding="utf-8", newline="") as f: |
| for row in csv.DictReader(f): |
| rows.append(row) |
| rows.sort(key=lambda r: int(r["spec_id"])) |
| return rows |
|
|
|
|
| def maybe_rebuild_kb_index() -> None: |
| script = ROOT / "scripts" / "index_uppaal_kb.py" |
| if not script.is_file(): |
| return |
| subprocess.run([sys.executable, str(script), "--rebuild"], cwd=str(ROOT), check=False) |
|
|
|
|
| def quality_grade(rec: Dict[str, Any]) -> str: |
| ok = bool(rec.get("summary_ok")) |
| val = bool(rec.get("validator_ok")) |
| deg = bool(rec.get("degraded_export")) |
| smoke = rec.get("smoke_ok") |
| ir_n = int(rec.get("ir_error_count_final") or 0) |
| if ok and val and smoke is True and not deg: |
| return "excellent" |
| if ok and smoke is True and (not deg or ir_n <= 3): |
| return "good" |
| if ok and smoke is True: |
| return "acceptable" |
| if ok and smoke is False: |
| return "xml_ok_verifyta_bad" |
| if not ok: |
| return "failed_export" |
| return "unknown" |
|
|
|
|
| def main() -> int: |
| ap = argparse.ArgumentParser(description=__doc__) |
| ap.add_argument("--spec-csv", type=Path, default=SPEC_CSV) |
| ap.add_argument("--start", type=int, default=1) |
| ap.add_argument("--end", type=int, default=20) |
| ap.add_argument("--batch-root", type=Path, default=None, help="Reuse an existing batch root instead of creating a new timestamped one") |
| ap.add_argument("--rebuild-kb-index", action="store_true") |
| ap.add_argument( |
| "--no-rag", |
| action="store_true", |
| help="Disable UPPAAL IR KB retrieval for the Builder (baseline / no-RAG run).", |
| ) |
| ap.add_argument("--analyze-model", type=str, default="gpt-4.1") |
| ap.add_argument("--builder-model", type=str, default="gpt-4o-mini") |
| ap.add_argument( |
| "--builder-two-phase", |
| action="store_true", |
| help="IR extractor Phase 1 structure + Phase 2 guards/sync (often better timing/sync)", |
| ) |
| ap.add_argument("--max-validation-rounds", type=int, default=None) |
| ap.add_argument("--disable-verifyta-smoke", action="store_true") |
| args = ap.parse_args() |
|
|
| if args.batch_root: |
| batch_root = Path(args.batch_root).resolve() |
| else: |
| stamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| batch_kind = "norag" if args.no_rag else "kb" |
| batch_root = ROOT / "artifacts" / "formal_build" / f"batch_formal_{batch_kind}_{stamp}" |
| batch_root.mkdir(parents=True, exist_ok=True) |
| report_path = batch_root / "report.jsonl" |
|
|
| if args.rebuild_kb_index and not args.no_rag: |
| maybe_rebuild_kb_index() |
|
|
| all_rows = {int(r["spec_id"]): r for r in load_all_specs(args.spec_csv)} |
| indices = sorted( |
| sid for sid in all_rows.keys() if args.start <= sid <= args.end |
| ) |
|
|
| pipe_kw: Dict[str, Any] = { |
| "stub_analyzer": False, |
| "stub_agents": False, |
| "llm_model": args.analyze_model, |
| "builder_model_name": args.builder_model, |
| "builder_two_phase": bool(args.builder_two_phase), |
| "use_rag_kb": not args.no_rag, |
| "kb_rebuild": False, |
| "enable_verifyta_smoke": not args.disable_verifyta_smoke, |
| } |
| if args.max_validation_rounds is not None: |
| pipe_kw["max_validation_or_refine_rounds"] = args.max_validation_rounds |
|
|
| pipe = FormalModelBuildingPipeline(**pipe_kw) |
|
|
| print(f"Batch root: {batch_root}") |
| kb_on = pipe_kw["use_rag_kb"] |
| print(f"Spec range: {args.start}–{args.end} KB: {'on' if kb_on else 'off'} verifyta: {pipe_kw['enable_verifyta_smoke']}") |
| sys.stdout.flush() |
|
|
| for sid in indices: |
| row = all_rows[sid] |
| label = (row.get("system_name") or "").strip() or f"spec_{sid}" |
| spec_text = (row.get("nl_spec") or "").strip() |
| out = batch_root / f"S{sid:02d}_{_safe_label(label)}" |
| print(f"\n========== SPEC {sid} {label!r} ==========") |
| sys.stdout.flush() |
| t0 = time.monotonic() |
|
|
| summary = pipe.run(spec_text, out) |
|
|
| elapsed = time.monotonic() - t0 |
|
|
| tpl_n = sch_n = None |
| sch_path = out / "formal_schema.json" |
| if sch_path.is_file(): |
| try: |
| sch = json.loads(sch_path.read_text(encoding="utf-8")) |
| tpls = sch.get("templates") or [] |
| tpl_n = len(tpls) |
| sch_n = sum(len(t.get("transitions") or []) for t in tpls if isinstance(t, dict)) |
| except Exception: |
| pass |
|
|
| rec: Dict[str, Any] = { |
| "spec_id": sid, |
| "system_name": label, |
| "elapsed_sec": round(elapsed, 1), |
| "out_dir": str(out.relative_to(ROOT)), |
| "summary_ok": summary.ok, |
| "validator_ok": summary.validator_ok, |
| "degraded_export": summary.degraded_export, |
| "repair_rounds_used": summary.repair_rounds_used, |
| "ir_error_count_final": summary.ir_error_count_final, |
| "schema_error_count_final": summary.schema_error_count_final, |
| "smoke_ok": summary.smoke_ok, |
| "model_check_skipped": summary.model_check_skipped, |
| "error": summary.error, |
| "template_count": tpl_n, |
| "transition_count_total": sch_n, |
| } |
|
|
| tp_path = out / "formal_build_trace.json" |
| if tp_path.is_file(): |
| try: |
| tp = json.loads(tp_path.read_text(encoding="utf-8")) |
| rec["ir_errors_final"] = tp.get("ir_errors_final") or [] |
| except Exception: |
| rec["ir_errors_final"] = [] |
|
|
| rec["quality_grade"] = quality_grade(rec) |
|
|
| with report_path.open("a", encoding="utf-8") as rf: |
| rf.write(json.dumps(rec, ensure_ascii=False) + "\n") |
|
|
| print( |
| f" grade={rec['quality_grade']:18} ok={summary.ok} validator_ok={summary.validator_ok} " |
| f"degraded={summary.degraded_export} smoke={summary.smoke_ok} " |
| f"ir_err={summary.ir_error_count_final} rounds={summary.repair_rounds_used} " |
| f"tpl/tr={tpl_n}/{sch_n} {elapsed:.0f}s" |
| ) |
| if summary.error: |
| print(f" error: {summary.error}") |
| ie = rec.get("ir_errors_final") or [] |
| if isinstance(ie, list) and ie: |
| preview = ie[0][:160] + ("..." if len(ie[0]) > 160 else "") |
| print(f" ir head: {preview!r}") |
| sys.stdout.flush() |
|
|
| print(f"\nWrote report: {report_path}") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|