- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환 - Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용 - pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관) - worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md) - D:\Rhino-MCP 경로 참조를 새 위치로 치환 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
146 lines
8.0 KiB
Python
146 lines
8.0 KiB
Python
"""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>_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")
|