- 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>
104 lines
6.8 KiB
Python
104 lines
6.8 KiB
Python
"""Stage A test of the 입력 묶음 확인·확정 개편 (2026-10-16), no Rhino: variability classes, template-origin marking, F-field edit rule (D3),
|
|
V3 edit block. Own data root 검증/variability_test_data (recreated); real piers are not touched.
|
|
Checks: every bundle of the 6 protected templates has a class (100 %), grades follow 기획서 부록 A, a new pier from a template shows all
|
|
required bundles as "템플릿 원본 그대로" and an edited bundle drops out, an old pier without origin record claims nothing, F edits need a
|
|
reason and are logged, V3 edits are refused, saved templates get a variability.json."""
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
APP = Path(__file__).resolve().parents[1]
|
|
TD = APP / "검증" / "variability_test_data"
|
|
if TD.exists():
|
|
shutil.rmtree(TD)
|
|
shutil.copytree(APP / "data" / "_템플릿", TD / "_템플릿", ignore=shutil.ignore_patterns("_이전"))
|
|
shutil.copytree(APP / "data" / "옥성교P1", TD / "옥성교P1", ignore=shutil.ignore_patterns("runs", "backup", "drawings"))
|
|
os.environ["PIERAPP_DATA"] = str(TD)
|
|
sys.path.insert(0, str(APP / "web"))
|
|
sys.path.insert(0, str(APP))
|
|
import app as webapp # noqa: E402
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from testlib import confirm_required # noqa: E402
|
|
import store # noqa: E402
|
|
from engine import variability # 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)
|
|
|
|
|
|
# ---- 1. classes of the protected templates
|
|
for t in store.templates():
|
|
d = TD / "_템플릿" / t["name"]
|
|
P, E = (json.loads((d / f).read_text(encoding="utf-8")) for f in ("params.json", "expected.json"))
|
|
v = json.loads((d / "variability.json").read_text(encoding="utf-8"))["sections"]
|
|
want = {f"params:{k}" for k in P} | {f"expected:{k}" for k in E}
|
|
check(f"{t['name']}: every bundle classified ({len(want)})", set(v) == want and all(s["grade"] in variability.GRADES for s in v.values()),
|
|
sorted(want ^ set(v))[:5])
|
|
v3 = sorted(k for k, s in v.items() if s["grade"] == "V3")
|
|
exp_v3 = {"운곡교P1형": [], "산외교울산P1형": ["params:coping_rebar"], "산외교울산P2형": ["params:coping_rebar"], "Ramp-A교P1형": ["params:coping_rebar"],
|
|
"옥성교P1형": ["params:coping_rebar", "params:footing_rebar"], "새재1교P1형": ["params:coping_rebar", "params:footing_rebar"]}[t["name"]]
|
|
check(f"{t['name']}: V3 = {exp_v3} (explicit bar lists, 부록 A)", v3 == exp_v3, v3)
|
|
check(f"{t['name']}: run settings / pier_id are F", all(v[k]["grade"] == "F" for k in ("params:pier_id", "expected:pier_id", "expected:checkers") if k in v))
|
|
|
|
# ---- 2. new pier from a template: all required bundles are "원본 그대로"
|
|
c = webapp.app.test_client()
|
|
r = c.post("/pier/new", data={"name": "원본시험", "type": "T형", "template": "tpl:새재1교P1형"})
|
|
check("new pier created", r.status_code == 302, r.status_code)
|
|
p = store.Pier("원본시험")
|
|
req = webapp.required(p)
|
|
same = {(s["file"], s["key"]) for s in p.sections() if p.section_meta(s["file"], s["key"])["origin"]}
|
|
check(f"all {len(req)} required bundles are still the template's original", req <= same and len(req) > 20, f"{len(req & same)}/{len(req)}")
|
|
html = c.get("/pier/원본시험").get_data(as_text=True)
|
|
check("pier page warns about original values and shows grade / source columns", f"템플릿 원본 그대로인 필수 묶음 {len(req)}개" in html
|
|
and "변수 등급" in html and "값 출처" in html and "V1 값 변수" in html)
|
|
check("pier folder holds its own variability.json and the origin record", (TD / "원본시험" / "variability.json").exists() and p.info.get("origin", {}).get("sha"))
|
|
|
|
# ---- 3. editing a V1 bundle drops it from the original list
|
|
col = json.loads(json.dumps(p.load("params")["column"]))
|
|
col["height"] = col["height"] + 100
|
|
r = c.post("/pier/원본시험/section/params/column", data={"value": json.dumps(col)})
|
|
check("V1 edit saved without reason", r.status_code == 302 and p.load("params")["column"]["height"] == col["height"])
|
|
check("edited bundle is no longer 'original'", p.section_meta("params", "column")["origin"] is False and p.section_meta("params", "footing")["origin"] is True)
|
|
|
|
# ---- 4. F fields need a reason (D3), the reason is logged
|
|
col2 = dict(col, shape="rectangular")
|
|
r = c.post("/pier/원본시험/section/params/column", data={"value": json.dumps(col2)})
|
|
check("F-field change without reason refused", "고정(F) 항목" in r.get_data(as_text=True) and p.load("params")["column"]["shape"] == "circular")
|
|
r = c.post("/pier/원본시험/section/params/column", data={"value": json.dumps(col2), "reason": "사각 기둥 시험"})
|
|
log = store.rjson(TD / "원본시험" / "edit_log.json", [])
|
|
check("F-field change with reason saved and logged", p.load("params")["column"]["shape"] == "rectangular" and log and log[-1]["reason"] == "사각 기둥 시험"
|
|
and log[-1]["fixed_fields"] == ["shape"], log[-1:] )
|
|
sc = p.load("params")["scope"]
|
|
r = c.post("/pier/원본시험/section/params/scope", data={"value": json.dumps(sc[:-1])})
|
|
check("F bundle (scope) change without reason refused", "고정(F) 항목" in r.get_data(as_text=True) and p.load("params")["scope"] == sc)
|
|
|
|
# ---- 5. V3 bundles cannot be edited by hand
|
|
before = p.load("params")["coping_rebar"]
|
|
r = c.post("/pier/원본시험/section/params/coping_rebar", data={"value": json.dumps(before)})
|
|
check("V3 bundle edit refused", "생성 변수(V3)" in r.get_data(as_text=True) and p.load("params")["coping_rebar"] == before)
|
|
check("V3 section page explains", "판독 스크립트를 다시 실행" in c.get("/pier/원본시험/section/params/coping_rebar").get_data(as_text=True))
|
|
|
|
# ---- 6. an old pier (no origin record) claims nothing
|
|
q = store.Pier("옥성교P1")
|
|
check("old pier: origin unknown (None), no 'original' claim", q.info.get("origin") is None and all(q.section_meta(s["file"], s["key"])["origin"] is None for s in q.sections()))
|
|
html = c.get("/pier/옥성교P1").get_data(as_text=True)
|
|
check("old pier page says the origin is not recorded", "템플릿 복사 기록이 없어" in html and "▲ 템플릿 원본 그대로" not in html)
|
|
|
|
# ---- 7. saving a template writes variability.json
|
|
store.save_template("원본시험", "시험형", "시험")
|
|
check("save_template writes variability.json", (TD / "_템플릿" / "시험형" / "variability.json").exists())
|
|
|
|
# ---- 8. confirmation still works and the confirm records are untouched by the new metadata
|
|
confirm_required(c, webapp, store, "원본시험", "시험자")
|
|
check("confirming every required bundle one by one works", not webapp.unconfirmed(store.Pier("원본시험")))
|
|
print("RESULT", "PASS" if ok else "FAIL")
|
|
sys.exit(0 if ok else 1)
|