"""Patch (2026-10-12, user request): batch run G1..G5 in ONE Rhino session with gate checks only — no per-gate save, no reload; negative tests kept (moved and restored inside the session); the final G5 model is saved once. Reload / unchanged are reported as skipped for batch-run gates. Individual gate runs keep save + reload; when the previous gate has no fingerprint (it was batch-run), the unchanged check is skipped instead of raising.""" from pathlib import Path APP = Path(__file__).resolve().parents[2] def patch(rel, reps): p = APP / rel s = p.read_text(encoding="utf-8") for a, b in reps: assert s.count(a) == 1, (rel, a[:70]) s = s.replace(a, b) p.write_text(s, encoding="utf-8") patch("engine/gates.py", [ # 1. shared judgement of checks + negative tests ("""def run_gate(gate, params, expected, run_dir, log=print):""", """def _negative_steps(gate, pid, expected, run_dir): \"\"\"Rhino steps: checker, then horizontal / vertical move -> checker -> undo (same document).\"\"\" neg_h, neg_v = NEGATIVE[gate] pert = RHINO_DIR / "perturb.py" steps = check_steps(expected, run_dir, [gate]) for tag, mv in (("_negH", neg_h), ("_negV", neg_v)): glb = {"PIER_ID": pid, "MOVES": [mv]} steps.append({"name": f"{gate}:perturb{tag}", "script": pert, "globals": glb}) steps += check_steps(expected, run_dir, [gate], tag) steps.append({"name": f"{gate}:undo{tag}", "script": pert, "globals": dict(glb, UNDO=True)}) return steps def _judge(gate, X, run_dir): \"\"\"Read checker + negative-test results: (base, unplanned, planned_seen, neg).\"\"\" base = _load(run_dir / f"{gate}.json") fails = _fails(base) unplanned = [f["item"] for f in fails if not planned(X, gate, f["item"])] planned_seen = [{"item": f["item"], "reason": planned(X, gate, f["item"])["reason"]} for f in fails if planned(X, gate, f["item"])] neg = {} for tag, mv in zip(("negH", "negV"), NEGATIVE[gate]): r = _load(run_dir / f"{gate}_{tag}.json") new = [f["item"] for f in _fails(r) if f["item"] not in {x["item"] for x in fails}] neg[tag] = {"move": mv, "new_fails": new[:10], "detected": len(_fails(r)) > len(fails)} return base, unplanned, planned_seen, neg def run_batch_session(gates, params, expected, run_dir, log=print): \"\"\"Batch mode: build G1..G5 in ONE Rhino session; after each gate in `gates` is built, run its checker and negative tests in the same document. No per-gate save and no reload; the final model is saved once as G5.3dm. Writes _gate.json for each gate in `gates` (reload / unchanged = skipped) and returns {gate: report}.\"\"\" run_dir = Path(run_dir) run_dir.mkdir(parents=True, exist_ok=True) X = _load(expected) pid = X["pier_id"] ids = gate_ids() for g in gates: # results / models of these gates are replaced: drop the old ones for suffix in ("_fp.json",) + ((".3dm",) if g != "G5" else ()): (run_dir / f"{g}{suffix}").unlink(missing_ok=True) glb = {"PARAMS_PATH": str(Path(params).resolve())} steps = [] for g, _, script in GATES: steps.append({"name": f"{g}:{script}", "script": RHINO_DIR / script, "globals": glb}) if g in REBAR_GATES: steps.append({"name": f"{g}:rebar_colours.py", "script": RHINO_DIR / "rebar_colours.py", "globals": glb}) if g in gates: steps += _negative_steps(g, pid, expected, run_dir) steps.append({"name": "display.py", "script": RHINO_DIR / "display.py", "globals": glb}) t0 = time.time() log(f"[batch] one Rhino session: build G1..G5, check {', '.join(gates)} (no per-gate save / reload)") res = run_job(steps, save=run_dir / "G5.3dm", units_mm_tolerance=TOLERANCE_MM) secs = {} for st in res["steps"]: g = st["name"].split(":")[0] secs[g] = secs.get(g, 0) + st["secs"] out = {} for g in gates: base, unplanned, planned_seen, neg = _judge(g, X, run_dir) verdict = "PASS" if not unplanned and all(v["detected"] for v in neg.values()) else "FAIL" rep = {"gate": g, "checker": CHECKERS[g], "verdict": verdict, "summary": base["summary"], "unplanned_fails": unplanned, "planned_fails": planned_seen, "negative": neg, "reload": {"ok": None, "skipped": "일괄 실행(단일 세션) — 저장·재로드 안 함"}, "unchanged": {"ok": None, "compared": 0, "diffs": [], "missing": [], "against": None, "skipped": "일괄 실행(단일 세션) — 앞 게이트 객체를 다시 만들지 않음"}, "model": str(run_dir / "G5.3dm") if g == "G5" else None, "mode": "batch", "secs": round(secs.get(g, 0), 1), "session_secs": res["wall_secs"]} (run_dir / f"{g}_gate.json").write_text(json.dumps(rep, ensure_ascii=False, indent=1), encoding="utf-8") log(f"[{g}] {verdict} checks {base['summary']['passed']}/{base['summary']['total']} " f"(planned FAIL {len(planned_seen)}, unplanned {len(unplanned)}) negative H/V " f"{neg['negH']['detected']}/{neg['negV']['detected']} (batch)") out[g] = rep log(f"[batch] done in {round(time.time() - t0, 1)}s, objects {res['objects']}, model {run_dir / 'G5.3dm'}") return out def run_gate(gate, params, expected, run_dir, log=print):"""), # 2. run_gate uses the shared helpers (""" neg_h, neg_v = NEGATIVE[gate] pert = RHINO_DIR / "perturb.py" steps = check_steps(expected, run_dir, [gate]) for tag, mv in (("_negH", neg_h), ("_negV", neg_v)): glb = {"PIER_ID": pid, "MOVES": [mv]} steps.append({"name": f"perturb{tag}", "script": pert, "globals": glb}) steps += check_steps(expected, run_dir, [gate], tag) steps.append({"name": f"undo{tag}", "script": pert, "globals": dict(glb, UNDO=True)}) steps.append""", """ steps = _negative_steps(gate, pid, expected, run_dir) steps.append"""), (""" base = _load(run_dir / f"{gate}.json") fails = _fails(base) unplanned = [f["item"] for f in fails if not planned(X, gate, f["item"])] planned_seen = [{"item": f["item"], "reason": planned(X, gate, f["item"])["reason"]} for f in fails if planned(X, gate, f["item"])] neg = {} for tag, mv in (("negH", neg_h), ("negV", neg_v)): r = _load(run_dir / f"{gate}_{tag}.json") new = [f["item"] for f in _fails(r) if f["item"] not in {x["item"] for x in fails}] neg[tag] = {"move": mv, "new_fails": new[:10], "detected": len(_fails(r)) > len(fails)} """, """ base, unplanned, planned_seen, neg = _judge(gate, X, run_dir) """), # 3. previous gate without fingerprint (batch-run) -> unchanged skipped (""" prev_fp = run_dir / f"{ids[k - 1]}_fp.json" if not prev_fp.exists(): raise FileNotFoundError(f"run {ids[k - 1]} first (needs {prev_fp.name})") prev = _load(prev_fp)""", """ prev_fp = run_dir / f"{ids[k - 1]}_fp.json" if k > 0 and not prev_fp.exists(): unchanged.update(ok=None, skipped=f"{ids[k - 1]} 지문 없음(일괄 실행 결과) — 비교 생략") elif k > 0: prev = _load(prev_fp)"""), (""" verdict = ("PASS" if not unplanned and all(v["detected"] for v in neg.values()) and reload_ok and unchanged["ok"] else "FAIL")""", """ verdict = ("PASS" if not unplanned and all(v["detected"] for v in neg.values()) and reload_ok and unchanged["ok"] is not False else "FAIL")"""), (""" "unchanged": unchanged, "model": str(model), "secs": round(time.time() - t0, 1),""", """ "unchanged": unchanged, "model": str(model), "mode": "single", "secs": round(time.time() - t0, 1),"""), ("""from engine.pipeline import (CHECKERS, GATE_PREFIXES, RHINO_DIR, TOLERANCE_MM, build_steps, check_steps, gate_ids)""", """from engine.pipeline import (CHECKERS, GATE_PREFIXES, GATES, REBAR_GATES, RHINO_DIR, TOLERANCE_MM, build_steps, check_steps, gate_ids)"""), ]) print("engine patched")