chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- 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>
This commit is contained in:
commit
964a841de1
3978 files changed
+58013356
No files matched your search
@@ -0,0 +1,198 @@
|
||||
"""Gate runner: build a gate model, re-open it, check it, run negative tests and judge the gate.
|
||||
|
||||
A gate Gk passes when all of:
|
||||
checks every FAIL of checker Gk is an approved planned exception (expected.json `planned_fail`)
|
||||
negative a horizontal and a vertical move of one gate object each raise the FAIL count
|
||||
reload the re-opened file has the same object count as the built document
|
||||
unchanged objects of earlier gates are identical to the previous gate model (fast fingerprint)
|
||||
Files go to <run_dir>: Gk.3dm, Gk.json, Gk_negH.json, Gk_negV.json, Gk_fp.json, Gk_gate.json.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from engine.pipeline import (CHECKERS, GATE_PREFIXES, GATES, REBAR_GATES, RHINO_DIR, TOLERANCE_MM, build_steps,
|
||||
builder_of, check_steps, checker_of, gate_ids, scope_of)
|
||||
|
||||
APP = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(APP))
|
||||
sys.path.insert(0, str(APP / "tools"))
|
||||
from compare_fingerprints import diff_obj # noqa: E402
|
||||
from runner import run_job # noqa: E402
|
||||
|
||||
# Negative tests per gate: one horizontal and one vertical move of a gate object (mm).
|
||||
NEGATIVE = {
|
||||
"G1": ({"id": "BS08", "d": [3, 0, 0]}, {"id": "BP03", "d": [0, 0, 2]}),
|
||||
"G2": ({"mark": "P2", "d": [5, 0, 0]}, {"mark": "P3", "d": [0, 0, 5]}),
|
||||
"G3": ({"mark": "B4", "d": [5, 0, 0]}, {"mark": "B9", "d": [0, 0, 5]}),
|
||||
"G4": ({"mark": "C11", "d": [5, 0, 0]}, {"mark": "S1", "d": [0, 0, 5]}),
|
||||
"G5": ({"id": "BB05", "d": [5, 0, 0]}, {"id": "SP08", "d": [0, 0, 5]}),
|
||||
}
|
||||
|
||||
|
||||
def _load(p):
|
||||
return json.loads(Path(p).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def planned(expected, gate, item):
|
||||
for pf in expected.get("planned_fail", []):
|
||||
if pf["gate"] != gate:
|
||||
continue
|
||||
if pf.get("item") == item or (pf.get("item_prefix") and item.startswith(pf["item_prefix"])):
|
||||
return pf
|
||||
return None
|
||||
|
||||
|
||||
def _fails(res):
|
||||
return [r for r in res["results"] if r["result"] == "FAIL"]
|
||||
|
||||
|
||||
def negative_moves(X, gate):
|
||||
"""Negative-test moves of a gate: expected["negative"][gate] (pier specific) or the Ungok default.
|
||||
A default bearing id beyond this pier's bearings (BS08 / SP08 with 4 bearings) is replaced by the last bearing
|
||||
number of expected G5 el_x (OPEN #19 A7); with 10 bearings the default is unchanged."""
|
||||
ov = X.get("negative", {}).get(gate)
|
||||
if ov:
|
||||
return tuple(ov)
|
||||
nos = sorted(int(k) for k in (X.get("G5", {}).get("el_x") or {}) if str(k).isdigit())
|
||||
return tuple(dict(mv, id=f"{mv['id'][:2]}{nos[-1]:02d}") if "id" in mv and nos and int(mv["id"][2:]) not in nos
|
||||
else mv for mv in NEGATIVE[gate])
|
||||
|
||||
|
||||
def _negative_steps(gate, pid, expected, run_dir):
|
||||
"""Rhino steps: checker, then horizontal / vertical move -> checker -> undo (same document)."""
|
||||
neg_h, neg_v = negative_moves(_load(expected), 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_moves(X, 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 = []
|
||||
scope = scope_of(params)
|
||||
for g, _, _ in GATES:
|
||||
if g not in scope:
|
||||
continue
|
||||
script = builder_of(params, g)
|
||||
steps.append({"name": f"{g}:{script}", "script": RHINO_DIR / script, "globals": dict(glb, GATE=g)})
|
||||
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": checker_of(expected, 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):
|
||||
run_dir = Path(run_dir)
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
X = _load(expected)
|
||||
pid = X["pier_id"]
|
||||
t0 = time.time()
|
||||
model = run_dir / f"{gate}.3dm"
|
||||
log(f"[{gate}] build G1..{gate}")
|
||||
built = run_job(build_steps(params, gate), save=model, units_mm_tolerance=TOLERANCE_MM)
|
||||
|
||||
steps = _negative_steps(gate, pid, expected, run_dir)
|
||||
steps.append({"name": "fingerprint", "script": RHINO_DIR / "fingerprint.py",
|
||||
"globals": {"MODEL_PATH": str(model), "OUT_PATH": str(run_dir / f"{gate}_fp.json"), "FAST": True}})
|
||||
log(f"[{gate}] re-open, check, negative tests, fingerprint")
|
||||
checked = run_job(steps, open_path=model)
|
||||
|
||||
base, unplanned, planned_seen, neg = _judge(gate, X, run_dir)
|
||||
|
||||
fp = _load(run_dir / f"{gate}_fp.json")
|
||||
unchanged = {"compared": 0, "diffs": [], "missing": [], "ok": True, "against": None}
|
||||
ids = scope_of(params) # previous gate = previous gate in this pier's scope
|
||||
k = ids.index(gate)
|
||||
if k > 0:
|
||||
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)
|
||||
earlier = tuple(f"{pid}-{p}" for g in ids[:k] for p in GATE_PREFIXES[g])
|
||||
keys = [oid for oid in prev["objects"] if oid.startswith(earlier)]
|
||||
unchanged["against"] = prev_fp.name
|
||||
unchanged["compared"] = len(keys)
|
||||
unchanged["missing"] = [oid for oid in keys if oid not in fp["objects"]][:10]
|
||||
for oid in keys:
|
||||
if oid in fp["objects"]:
|
||||
d = diff_obj(prev["objects"][oid], fp["objects"][oid])
|
||||
if d:
|
||||
unchanged["diffs"].append([oid, d[:3]])
|
||||
unchanged["diffs"] = unchanged["diffs"][:10]
|
||||
unchanged["ok"] = not unchanged["missing"] and not unchanged["diffs"]
|
||||
|
||||
reload_ok = built["objects"] == checked["objects"] == fp["count"]
|
||||
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")
|
||||
rep = {"gate": gate, "checker": checker_of(expected, gate), "verdict": verdict, "summary": base["summary"],
|
||||
"unplanned_fails": unplanned, "planned_fails": planned_seen,
|
||||
"negative": neg, "reload": {"built": built["objects"], "reopened": checked["objects"],
|
||||
"fingerprint": fp["count"], "ok": reload_ok},
|
||||
"unchanged": unchanged, "model": str(model), "mode": "single", "secs": round(time.time() - t0, 1),
|
||||
"build_steps": [{k_: s[k_] for k_ in ("name", "secs")} for s in built["steps"]]}
|
||||
(run_dir / f"{gate}_gate.json").write_text(json.dumps(rep, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
log(f"[{gate}] {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']} reload {reload_ok} "
|
||||
f"unchanged {unchanged['ok']} ({unchanged['compared']}) {rep['secs']}s")
|
||||
return rep
|
||||
Reference in new issue
Block a user