Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
88 lines
3.4 KiB
Python
88 lines
3.4 KiB
Python
"""연결고리 표(`work_item_link`)를 읽는 **한 자리** — 산림·건설 어느 품셈 품을 쓸지 **표가 정함**
|
|
(2026-09-17 브레인 · 코덱스 크로스체크 ② 「연결고리 미소비」).
|
|
|
|
고시 총칙 7 「본 품셈에 명시되지 않은 품은 타 부문 표준품셈 적용 · 유사 공종은 본 품셈 우선」 —
|
|
그 규칙은 표의 `policy`(`both_precedence` · `unconfirmed_action`)에 있고 코드는 **읽기만** 함(「산림 우선」을 코드에 안 박음).
|
|
|
|
확정 · both → `both_precedence` 쪽 열쇠(지금 표는 forest)
|
|
확정 · 한쪽만 → 그 열쇠 그대로
|
|
미판정 → **안 고름** · 사유를 붙여 막음(`unconfirmed_action: do_not_link`)
|
|
표에 없음 → 제 품셈 그대로(품셈에 명시된 품)
|
|
⚠ 미판정 행 가운데 열쇠가 빈 행(여러 절 묶음)은 줄에 못 닿음 — `undecided_links()` 로 목록만 드러냄.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
FOLDER = Path(__file__).resolve().parent.parent / "resources" / "data_work_item_link"
|
|
CONFIRMED = "확정"
|
|
#: 표 `policy.both_precedence` 낱말 → 그 행의 열쇠 칸
|
|
_KEY_COLUMN = {"forest": "forest_key", "construction": "const_key"}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Choice:
|
|
"""쓸 열쇠 · 막힌 까닭(있으면 금액을 안 세움)."""
|
|
|
|
key: str | None
|
|
blocked: str = ""
|
|
|
|
|
|
@lru_cache(maxsize=4)
|
|
def _read(path: str, _mtime_ns: int) -> dict[str, Any]:
|
|
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
|
|
|
|
def _doc() -> dict[str, Any]:
|
|
found = sorted(FOLDER.glob("work_item_link_*.json")) if FOLDER.is_dir() else []
|
|
return _read(str(found[-1]), found[-1].stat().st_mtime_ns) if found else {}
|
|
|
|
|
|
def choose(key: str | None) -> Choice:
|
|
"""열쇠 하나(FW-·CW-) → 실제로 쓸 열쇠. 미판정 행에 걸리면 안 고르고 막음."""
|
|
if not key:
|
|
return Choice(key)
|
|
doc = _doc()
|
|
policy = doc.get("policy") or {}
|
|
rows = [
|
|
link
|
|
for link in doc.get("links") or []
|
|
if key in (link.get("forest_key"), link.get("const_key"))
|
|
]
|
|
for link in rows:
|
|
if link.get("status") != CONFIRMED:
|
|
return Choice(
|
|
None,
|
|
"연결고리 미판정 — 산림·건설 어느 품을 쓸지 근거가 없어 고르지 않음"
|
|
f"({link.get('evidence') or '근거 없음'})",
|
|
)
|
|
for link in rows:
|
|
if link.get("branch") == "both":
|
|
column = _KEY_COLUMN.get(str(policy.get("both_precedence") or ""))
|
|
chosen = link.get(column) if column else None
|
|
if not chosen:
|
|
return Choice(
|
|
None, "연결고리 표에 둘 다 있는 줄의 우선 품셈이 안 적혔음 — 고르지 않음"
|
|
)
|
|
return Choice(chosen)
|
|
return Choice(key)
|
|
|
|
|
|
def undecided_links() -> list[dict[str, str]]:
|
|
"""미판정 행 — 열쇠가 비어 줄에 못 닿는 행도 드러냄(조용히 안 버림)."""
|
|
return [
|
|
{
|
|
"branch": str(link.get("branch") or ""),
|
|
"forest_key": str(link.get("forest_key") or ""),
|
|
"const_key": str(link.get("const_key") or ""),
|
|
"evidence": str(link.get("evidence") or ""),
|
|
}
|
|
for link in _doc().get("links") or []
|
|
if link.get("status") != CONFIRMED
|
|
]
|