diff --git a/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Keys.py b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Keys.py index ea40953f..741bb08a 100644 --- a/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Keys.py +++ b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Keys.py @@ -27,8 +27,27 @@ KEY_PREFIX = "FW" # Forest Work item — 산림 공종 열쇠 KEY_DIGITS = 5 PATH_SEP = " › " -#: 총칙 — 값표는 붙어 있으나 공종이 아닌 장. 뿌리 코드로 가른다. -GENERAL_PROVISION_ROOTS = ("FP-01", "FP-02") +#: 줄 구실 잣대는 **자료에 둔다** — 총칙 범위는 기관이 정하는 것이라 코드에 박으면 관리자가 못 고친다. +AXIS_POLICY = ( + Path(__file__).resolve().parent.parent + / "resources" + / "data_work_item_master" + / "axis_policy.json" +) + + +def load_policy(axis: str, path: Path = AXIS_POLICY) -> dict[str, Any]: + """`axis_policy.json` 의 한 축(`forest`·`const`). 없는 축을 부르면 멈춘다.""" + data = json.loads(path.read_text(encoding="utf-8")) + try: + return data["axes"][axis] + except KeyError: + raise ValueError(f"공종 축 잣대에 `{axis}` 가 없다 — {path}") from None + + +def general_provision_roots(axis: str = "forest", path: Path = AXIS_POLICY) -> tuple[str, ...]: + """총칙 뿌리 — 값표는 붙어 있으나 공종이 아닌 장. ⚠ 지우지 않고 구실로만 가른다.""" + return tuple(load_policy(axis, path).get("general_provision_roots") or ()) #: 원문 경로에 박힌 고시 번호 — 「(산림청고시 제2025-82호)」. @@ -173,27 +192,37 @@ def path_names(nodes: list[dict[str, Any]]) -> list[str]: return out -def axis_role(node: dict[str, Any], *, has_children: bool) -> str: - """이 줄이 공종인가 문서 구역인가. +def axis_role( + node: dict[str, Any], *, has_children: bool, roots: tuple[str, ...] | None = None +) -> str: + """이 줄이 공종인가 문서 구역인가. 차례가 뜻을 가진다(총칙 먼저). `work_item` 품셈 표가 붙은 줄 — 값이 있는 자리 `group` 표는 없고 아래를 묶는 마디 — `parent_mode` 가 계산 규칙을 지닌다(지우면 안 된다) - `general_provision` 총칙(1·2장) — 문서 구역이나 **값표가 붙어 있어 B09 가 읽는다** + `general_provision` 총칙 — 문서 구역이나 **값표가 붙어 있어 B09 가 읽는다** `empty` 표도 아래도 없는 잎 — 원문에 표가 안 붙은 자리 + + ⚠ **어느 구실이든 지우지 않는다.** 가름은 화면·조합이 무엇을 볼지 고르는 것뿐이다. """ + if roots is None: + roots = general_provision_roots() code = node["work_item_code"] - if any(code == root or code.startswith(root + "-") for root in GENERAL_PROVISION_ROOTS): + if any(code == root or code.startswith(root + "-") for root in roots): return "general_provision" if node.get("tables"): return "work_item" return "group" if has_children else "empty" -def decorate(nodes: list[dict[str, Any]], *, edition: str) -> None: +def decorate( + nodes: list[dict[str, Any]], *, edition: str, roots: tuple[str, ...] | None = None +) -> None: """각 줄에 목차 칸·경로 이름·구실을 얹는다. 열쇠는 `assign_keys` 가 이미 박았다. 기존 칸은 손대지 않는다 — 금액을 낳는 값은 그대로다. """ + if roots is None: + roots = general_provision_roots() paths = path_names(nodes) has_kid = {n.get("parent_code") for n in nodes} for node, path in zip(nodes, paths): @@ -202,4 +231,4 @@ def decorate(nodes: list[dict[str, Any]], *, edition: str) -> None: node["toc_number"] = node["number"] node["toc_edition"] = edition node["path_name"] = path - node["axis_role"] = axis_role(node, has_children=code in has_kid) + node["axis_role"] = axis_role(node, has_children=code in has_kid, roots=roots) diff --git a/resources/tester/test_work_item_key_registry.py b/resources/tester/test_work_item_key_registry.py index a51bb09b..a2d752f4 100644 --- a/resources/tester/test_work_item_key_registry.py +++ b/resources/tester/test_work_item_key_registry.py @@ -28,6 +28,8 @@ from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Keys import ( # noqa: E402 axis_role, empty_registry, format_key, + general_provision_roots, + load_policy, path_names, toc_edition, toc_slot, @@ -197,6 +199,38 @@ def test_구실_가름_규칙() -> None: assert axis_role({"work_item_code": "FP-09-01", "tables": []}, has_children=False) == "empty" +def test_총칙_범위는_코드가_아니라_자료가_정함() -> None: + """⚠ 뿌리를 코드에 박으면 관리자가 못 고친다 — `axis_policy.json` 이 정본.""" + assert general_provision_roots("forest") == ("FP-01", "FP-02") + const = load_policy("const") + assert const["key_prefix"] == "CW" + assert const["settled"] is False + assert const["general_provision_roots"] == ["01_공통부문/제1장_적용기준.md"] + assert [p["chapter"] for p in const["general_provision_pending"]] == [ + "01_공통부문/제8장_건설기계.md", + "05_유지관리부문/제1장_공통.md", + ] + # 뿌리를 바꿔 주면 구실도 따라 바뀐다 — 잣대가 자료에 있다는 증거 + assert ( + axis_role({"work_item_code": "FP-09-03", "tables": []}, has_children=True, roots=("FP-09",)) + == "general_provision" + ) + + +def test_어느_구실도_지우지_않음() -> None: + """브레인 승인(2026-09-17) — 「덜어내기」가 아니라 「가름만」.""" + import json as _json + from pathlib import Path as _Path + + policy = _json.loads( + _Path("resources/data_work_item_master/axis_policy.json").read_text(encoding="utf-8") + ) + assert policy["policy"]["never_delete"] is True + assert policy["policy"]["flat_list_filter"] == "pum_tables > 0" + assert all(role["keep"] for role in policy["roles"].values()) + assert list(policy["roles"]) == ["work_item", "group", "general_provision", "empty"] + + def test_경로_이름_잇기() -> None: nodes = [ {"work_item_code": "A", "parent_code": None, "name": "토공"},