feat(b08): 줄 구실 잣대를 자료로 — axis_policy.json · 건설 축 자리와 덜어낼 잣대 미리 세움

- 총칙 뿌리를 코드에서 꺼냄 — `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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsGw1Dz9HmhuAxGisxmDPF
This commit is contained in:
2026-09-17 12:17:04 +09:00
co-authored by Claude Opus 5
parent 7b6038b524
commit d4f75212e2
2 changed files with 71 additions and 8 deletions
@@ -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)
@@ -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": "토공"},