From d4f75212e27f4fb2a5ebc9e790d7b1f2508d4ec5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 17 Sep 2026 12:17:04 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08):=20=EC=A4=84=20=EA=B5=AC=EC=8B=A4=20?= =?UTF-8?q?=EC=9E=A3=EB=8C=80=EB=A5=BC=20=EC=9E=90=EB=A3=8C=EB=A1=9C=20?= =?UTF-8?q?=E2=80=94=20axis=5Fpolicy.json=20=C2=B7=20=EA=B1=B4=EC=84=A4=20?= =?UTF-8?q?=EC=B6=95=20=EC=9E=90=EB=A6=AC=EC=99=80=20=EB=8D=9C=EC=96=B4?= =?UTF-8?q?=EB=82=BC=20=EC=9E=A3=EB=8C=80=20=EB=AF=B8=EB=A6=AC=20=EC=84=B8?= =?UTF-8?q?=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 총칙 뿌리를 코드에서 꺼냄 — `general_provision_roots()` 가 `axis_policy.json` 을 읽음 (기관이 정하는 범위라 코드에 박으면 관리자가 못 고침) - 방침 못박음 — `never_delete: true` · 평평한 목록은 `pum_tables > 0`(「잎」 아님) - 구실 넷에 「지우지 않음」 딱지 — 브레인 승인(2026-09-17) - 건설 축 자리 — `CP`/`CW` · 총칙 뿌리 `01_공통부문/제1장_적용기준.md` · ⬜ 미확정 둘(제8장 건설기계 351표 · 유지관리 제1장 공통 40표 · 안티그래비티 조사 대기) - ⚠ 건설 코드 꼴 경고 — 부문마다 장 번호가 1부터 다시 시작(공통 제1장=적용기준 · 토목 제1장=도로포장) · 장 번호만으로 코드를 지으면 부문이 다른 공종끼리 덮음 계층 근거는 목차표가 아니라 장 파일 46개(`source_file` 칸) - 이름표에 건설 kind 자리 — `pending_merged_tables` (서버가 kind 를 내는 날 옮겨 담을 것) - 시험 둘 더 — 잣대가 자료에 있음 · 어느 구실도 안 지움 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QsGw1Dz9HmhuAxGisxmDPF --- .../B08_Quantity_Build_WorkItemMaster_Keys.py | 45 +++++++++++++++---- .../tester/test_work_item_key_registry.py | 34 ++++++++++++++ 2 files changed, 71 insertions(+), 8 deletions(-) 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": "토공"},