- 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>
163 lines
8.6 KiB
Python
163 lines
8.6 KiB
Python
"""OPEN #19 test (2026-10-13): A3 range text in schedules, A4 params rows for bearing / sole plate tables,
|
|
A5 dimension-chain input, A6 handle lookup, A7 default negative-test ids. No Rhino.
|
|
|
|
Needs 검증/open19_snapshot_before.json (tools/open19_snapshot.py before, taken before the changes).
|
|
Web part uses its own data root 검증/open19_test_data (recreated) with a copy of data/운곡교p2.
|
|
"""
|
|
import copy
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
APP = Path(__file__).resolve().parents[1]
|
|
TD = APP / "검증" / "open19_test_data"
|
|
if TD.exists():
|
|
shutil.rmtree(TD)
|
|
sys.path.insert(0, str(APP / "tools"))
|
|
from testlib import pier_dir # noqa: E402
|
|
shutil.copytree(pier_dir("운곡교p2"), TD / "p2", ignore=shutil.ignore_patterns("runs", "backup"))
|
|
os.environ["PIERAPP_DATA"] = str(TD)
|
|
sys.path.insert(0, str(APP))
|
|
sys.path.insert(0, str(APP / "tools"))
|
|
sys.path.insert(0, str(APP / "web"))
|
|
from engine import reader # noqa: E402
|
|
from engine.gates import NEGATIVE, negative_moves # noqa: E402
|
|
import app as webapp # noqa: E402
|
|
import store # noqa: E402
|
|
from open19_snapshot import snapshot # noqa: E402
|
|
|
|
ok = True
|
|
|
|
|
|
def check(name, cond, detail=""):
|
|
global ok
|
|
ok &= bool(cond)
|
|
print("PASS" if cond else "FAIL", name, str(detail)[:300], flush=True)
|
|
|
|
|
|
before = json.loads((APP / "검증" / "open19_snapshot_before.json").read_text(encoding="utf-8"))
|
|
after = snapshot()
|
|
(APP / "검증" / "open19_snapshot_after.json").write_text(json.dumps(after, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
|
|
# ---- A3: records only gain cells; Ungok P2 C12 / C13 range texts now land in column A
|
|
for pier in before:
|
|
added, changed = [], []
|
|
b_rec, a_rec = before[pier]["records"], after[pier]["records"]
|
|
for k in b_rec:
|
|
for rb, ra in zip(b_rec[k], a_rec.get(k, [])):
|
|
changed += [(k, rb["_key"], f) for f in rb if ra.get(f) != rb[f]]
|
|
added += [(rb["_key"], f, ra[f]) for f in ra if f not in rb]
|
|
check(f"A3 {pier}: same tables / rows", b_rec.keys() == a_rec.keys()
|
|
and all(len(b_rec[k]) == len(a_rec[k]) for k in b_rec))
|
|
check(f"A3 {pier}: no existing cell changed or lost", not changed, changed[:5])
|
|
print(" added cells:", added)
|
|
if pier == "운곡교p2":
|
|
got = {m: v for m, f, v in added if f == "A"}
|
|
check("A3 p2: C12 / C13 range text in column A",
|
|
got.get("C12") == "12.228~3.151" and got.get("C13") == "12.167~12.044", added)
|
|
check("A3 p2: nothing else added", len(added) == 2, added)
|
|
|
|
# ---- A4: expected-side rows identical to before; new params rows all equal current params
|
|
for pier in before:
|
|
old = before[pier]["proposals"]
|
|
new = after[pier]["proposals"]
|
|
new_old = [r for r in new if r["file"] == "expected" or r["path"][0] == "rebar_marks"]
|
|
check(f"A4 {pier}: expected / rebar_marks rows unchanged ({len(old)})", new_old == old)
|
|
pr = [r for r in new if r["file"] == "params" and r["path"][0] != "rebar_marks"]
|
|
diff = [r for r in pr if not r["same"]]
|
|
kinds = sorted({".".join(str(x) for x in r["path"] if not isinstance(x, int)) for r in pr})
|
|
check(f"A4 {pier}: params rows found, none differ ({len(pr)})", pr and not diff, diff[:3] or kinds)
|
|
|
|
texts = json.loads((pier_dir("운곡교p2") / "drawings/texts.json").read_text(encoding="utf-8"))
|
|
P0 = json.loads((pier_dir("운곡교p2") / "params.json").read_text(encoding="utf-8"))
|
|
X0 = json.loads((pier_dir("운곡교p2") / "expected.json").read_text(encoding="utf-8"))
|
|
cands = reader.field_candidates(reader.table_candidates(texts))
|
|
P = copy.deepcopy(P0)
|
|
P["sole_plates"]["table"][2]["W"] = 999 # NO.3 W (table 750)
|
|
P["bearing_seats"]["bearings"][6]["h1"] += 7 # 7th entry
|
|
rows = reader.proposals(cands, P, X0)
|
|
d = [r for r in rows if not r["same"]]
|
|
no7 = P["bearing_seats"]["bearings"][6]["no"]
|
|
check("A4 mutation: only the two changed params cells differ",
|
|
sorted(tuple(r["path"]) for r in d) == sorted([("sole_plates", "table", 2, "W"),
|
|
("bearing_seats", "bearings", 6, "h1")]), [r["path"] for r in d])
|
|
check("A4 mutation: expected untouched by the params rows", all(r["file"] == "params" for r in d))
|
|
reader.apply_proposals(d, P, X0)
|
|
check("A4 apply restores exactly the original params", P == P0)
|
|
check("A4 h3 common value proposed once (Ungok)",
|
|
sum(r["path"] == ["sole_plates", "h3"] for r in rows) == 1)
|
|
|
|
# ---- A5: dimension chain
|
|
pos = json.loads((APP / "data/운곡교P1/params.json").read_text(encoding="utf-8"))["footing_rebar"]["layers"]["bottom_Y1"]["pos"]
|
|
incs = [b - a for a, b in zip(pos, pos[1:])]
|
|
parts, i = [], 0
|
|
while i < len(incs):
|
|
j = i
|
|
while j < len(incs) and incs[j] == incs[i]:
|
|
j += 1
|
|
parts.append(f"{j - i}@{incs[i]:g}" if j - i > 1 else f"{incs[i]:g}")
|
|
i = j
|
|
text = ", ".join([f"{pos[0]:g}"] + parts)
|
|
print(" chain:", text)
|
|
check("A5 chain of P1 bottom_Y1.pos (73) expands to the same list", store.chain(text) == pos, len(store.chain(text)))
|
|
check("A5 drawing notation", store.chain("0, 8@120") == [0] + [120 * k for k in range(1, 9)])
|
|
check("A5 decimals kept", store.chain("-1.5, 2@0.25") == [-1.5, -1.25, -1.0])
|
|
for bad in ("", "a, 1", "2@100", "0, 0@5", "0, 3@x"):
|
|
try:
|
|
store.chain(bad)
|
|
check(f"A5 rejects {bad!r}", False)
|
|
except ValueError as e:
|
|
check(f"A5 rejects {bad!r}", True, e)
|
|
nested = {"a": {"chain": "0, 2@5"}, "b": [{"chain": "1, 1"}, 3], "c": {"chain": "0", "x": 1}}
|
|
check("A5 expand_chains nested / other dicts untouched",
|
|
store.expand_chains(nested) == {"a": [0, 5, 10], "b": [[1, 2], 3], "c": {"chain": "0", "x": 1}})
|
|
|
|
c = webapp.app.test_client()
|
|
fr = store.Pier("p2").load("params")["footing_rebar"]
|
|
fr2 = copy.deepcopy(fr)
|
|
fr2["b9"]["grid_x"] = {"chain": f"{fr['b9']['grid_x'][0]}, {len(fr['b9']['grid_x']) - 1}@250"}
|
|
r = c.post("/pier/p2/section/params/footing_rebar", data={"value": json.dumps(fr2, ensure_ascii=False)})
|
|
saved = store.Pier("p2").load("params")["footing_rebar"]
|
|
check("A5 web save expands the chain to the same list", r.status_code == 302 and saved == fr,
|
|
(r.status_code, saved["b9"]["grid_x"][:3]))
|
|
fr2["b9"]["grid_x"] = {"chain": "0, 3@abc"}
|
|
h = c.post("/pier/p2/section/params/footing_rebar", data={"value": json.dumps(fr2, ensure_ascii=False)},
|
|
follow_redirects=True).get_data(as_text=True)
|
|
check("A5 web rejects a bad chain, params unchanged",
|
|
"치수 사슬 형식 오류" in h and store.Pier("p2").load("params")["footing_rebar"] == fr)
|
|
check("A5 section page shows the chain hint", "치수 사슬" in c.get("/pier/p2/section/params/footing_rebar").get_data(as_text=True))
|
|
|
|
# ---- A6: handle lookup (values written in the P2 reading report / params src)
|
|
dxf = pier_dir("운곡교p2") / "drawings/dxf"
|
|
d1 = next(a["dxf"] for a in json.loads((pier_dir("운곡교p2") / "drawings/audit.json").read_text(encoding="utf-8"))
|
|
if a["label"] == "D1")
|
|
e = reader.entity_info(dxf / d1, "21A76")
|
|
print(" 21A76:", e)
|
|
check("A6 21A76 (footing, report: 10.350) found", e and abs(max(e["values"].get("bbox_size", [0])) - 10350) < 1, e)
|
|
e = reader.entity_info(dxf / d1, "20e89")
|
|
print(" 20E89:", e)
|
|
check("A6 20E89 (coping soffit arc, params R12,000) is an ARC R12000",
|
|
e and e["type"] == "ARC" and abs(e["values"]["radius"] - 12000) < 1, e)
|
|
check("A6 unknown handle -> None", reader.entity_info(dxf / d1, "FFFFFFF") is None)
|
|
h = c.get("/pier/p2/search?dwg=D1&h=20E89").get_data(as_text=True)
|
|
check("A6 web shows the object", "ARC" in h and "radius" in h and "12000" in h)
|
|
h = c.get("/pier/p2/search?dwg=D1&h=ZZZ").get_data(as_text=True)
|
|
check("A6 web reports a missing handle", "핸들 'ZZZ' 객체가 없습니다" in h)
|
|
|
|
# ---- A7: default negative ids follow the bearings of the pier
|
|
X10 = json.loads((APP / "data/운곡교P1/expected.json").read_text(encoding="utf-8"))
|
|
check("A7 10 bearings: defaults unchanged", all(negative_moves(X10, g) == NEGATIVE[g] for g in NEGATIVE))
|
|
X4 = copy.deepcopy(X10)
|
|
X4["G5"]["el_x"] = {k: v for k, v in X4["G5"]["el_x"].items() if int(k) <= 4}
|
|
m = {g: [mv.get("id") or mv.get("mark") for mv in negative_moves(X4, g)] for g in NEGATIVE}
|
|
check("A7 4 bearings: ids within 1..4", m["G1"] == ["BS04", "BP03"] and m["G5"] == ["BB04", "SP04"]
|
|
and m["G2"] == ["P2", "P3"], m)
|
|
X4["negative"] = {"G1": [{"id": "BS01", "d": [3, 0, 0]}, {"id": "BP01", "d": [0, 0, 2]}]}
|
|
check("A7 pier override still wins", [mv["id"] for mv in negative_moves(X4, "G1")] == ["BS01", "BP01"])
|
|
|
|
shutil.rmtree(TD)
|
|
print("RESULT", "PASS" if ok else "FAIL")
|
|
sys.exit(0 if ok else 1)
|