"""찰쌓기 양식 = 지금 전개 — 값이 한 줄도 안 갈리는지 (2026-09-13, PLAN 3장 ② ③). 판정 Ⓑ 「첫 양식 값은 지금 전개와 같게」. 양식(`resources/library_structure/masonry_wet.json`)을 식 풀이기(TS → Node)로 풀어 낸 값이 `build_table`(돌쌓기 전개 + 기초잡석)과 같아야 함. 어긋나면 **양식이 틀린 것**. ⚠ 전개는 부동소수, 양식은 분수 — 상대 1e-9 안이면 같은 값으로 봄. ⚠ 줄이 「안 서는」 경우(버림 안 넣음 · 채집석 구입)는 `when` 으로 — 안 선 줄은 전개에도 없어야 함. ⚠ 원문 「-」 칸은 일부러 다름(양식 `differs_from_engine`) — 여기서는 그 규격을 안 넣음. """ from __future__ import annotations import itertools import shutil import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B08_Quantity.B08_Quantity_Engine_Formula import evaluate_sheets # noqa: E402 from B08_Quantity.B08_Quantity_Engine_StructureTemplate import ( # noqa: E402 load_template, template_sheet, template_vars, ) from B08_Quantity.B08_Quantity_Engine_UnitQuantity import ( # noqa: E402 build_table, face_slope_ratio, ) pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") def _structure(height: float, length: float, **options) -> dict: return { "structure_id": "s1", "type_id": "masonry_wet", "name": "돌쌓기(찰)", "start_m": 100.0, "end_m": 100.0 + length, "length_m": length, "height_m": height, "options": {"height_m": height, "length_m": length, **options}, } CASES = [ dict(height=h, length=10.0, back_len_cm=l3, stone_kind=kind, foundation=found) for h, l3, kind, found in itertools.product( (1.0, 1.5, 2.5, 3.5), (35, 45, 55), ("", "야면석·호박돌", "깬돌"), ("기초유", "기초버림", ""), ) ] + [ # 사용자가 정한 칸 — 기울기 · 두께 · 실무 관행 계수 · 물구멍 면적 · 잡석 두께 · 짧은 연장 dict(height=2.0, length=7.5, back_len_cm=45, face_slope_ratio=0.35, foundation="기초유"), dict(height=2.0, length=10.0, back_len_cm=55, thickness_top_m=0.9, foundation="기초버림"), dict(height=2.0, length=10.0, back_len_cm=45, thickness_bottom_m="1.4", stone_kind="깬잡석"), # 뒷길이 45 — 안 고르면 표준 하한 30㎝ 라 야면석 관측 무게(35·45·55)가 없어 양식만 오류 줄(명세 13장 Ⓐ) dict( height=2.0, length=10.0, back_len_cm=45, stone_kind="야면석·호박돌", stone_coeff_basis="실무 관행", ), # 뒷길이 안 고름 — 전개·양식이 같은 표준 하한(찰 ≤3m 30㎝, 브레인 판정 ㉮) dict(height=2.0, length=10.0, stone_kind="깬돌", foundation="기초유"), dict(height=1.5, length=10.0, back_len_cm=35, weep_hole_area_m2=3, foundation="기초유"), dict(height=2.5, length=12.0, back_len_cm=60, stone_kind="견치돌", rubble=0.3), # 줄이 안 서는 갈래 — `when` dict(height=2.0, length=10.0, blinding_concrete="안 넣음", foundation="기초유"), dict(height=2.0, length=10.0, stone_supply="구입", stone_kind="깬돌"), dict(height=2.0, length=10.0, blinding_concrete="안 넣음", stone_supply="구입"), dict(height=2.0, length=10.0, rubble=0.0), # 규격 칸 — 강도 · 물구멍 지름 dict(height=2.0, length=10.0, fill_concrete_mpa="180", weep_hole_diameter_mm=75), ] @pytest.mark.parametrize("case", CASES, ids=lambda c: "-".join(f"{v}" for v in c.values())) def test_양식_값이_전개와_같다(case: dict) -> None: case = dict(case) rubble = case.pop("rubble", None) height = case.pop("height") length = case.pop("length") structure = _structure(height, length, **case) # ⚠ 전개만 봄 — `build_table` 은 이제 양식으로 갈음하므로(④-2) 끄지 않으면 자기와 대조가 됨. engine = build_table( [structure], {"masonry_wet": "돌쌓기(찰)"}, None, None, rubble, use_templates=False ) components = engine["structures"][0]["components"] template = load_template("masonry_wet") assert template is not None slope, _basis = face_slope_ratio(structure["options"], wet=True, height_m=height) settings = {} if rubble is None else {"rubble_base_thickness_m": rubble} values = template_vars(template, structure, slope, settings) solved = evaluate_sheets([template_sheet(template, values)]) assert solved is not None, "Node 풀이가 안 돌았다" assert [row["error"] for row in solved[0]] == [None] * len(solved[0]), solved[0] rows = [row for row in solved[0] if not row["skipped"]] assert len(rows) == len(components), ( [r["name"] for r in rows], [c["name"] for c in components], ) # ⓘ 「실무 관행」 계수가 돌종류를 지우던 전개 결함은 2026-09-13 고침(브레인 판정) — 차이 없음. for row, component in zip(rows, components): # 명세 13장 Ⓒ — 돌 줄도 양식·전개 모두 이름 고정 「돌」 + 규격에 종류(2026-09-13). assert row["name"] == component["name"] assert row["spec"] == component["spec"], (row, component) assert template["rows"][row["seq"] - 1]["destination"] == component["destination"], row assert float(row["amount"]) == pytest.approx(component["amount"], rel=1e-9, abs=1e-12), ( row["name"], row["amount"], component["amount"], ) def test_실무_관측값은_대안_후보로_보인다() -> None: """판정 Ⓑ — 지금 전개가 기본, 실무 관측(물구멍 2.5㎡)은 칸 옆 대안 후보.""" hole = load_template("masonry_wet")["vars"]["HOLE_AREA"] assert hole["default"] == 2 assert any(item["value"] == 2.5 for item in hole["candidates"]) def test_표는_키_오름차순() -> None: """근사 LOOKUP 의 전제(명세 13장) — 정확 일치만 쓰더라도 표 모양은 한 규칙으로.""" for name, table in load_template("masonry_wet")["tables"].items(): assert table["keys"] == sorted(table["keys"]), name for column, cells in table["columns"].items(): assert len(cells) == len(table["keys"]), (name, column)