Files
Aislo/resources/tester/test_work_item_master_const.py
T
eomsangdonandClaude Opus 5 73f373a40d Merge: 데스크탑 서브(c0b22442 기계 규격 맞대기)와 한 점 — 자료는 다시 뽑음
- 시험 장부는 양쪽을 다 읽어 합침(서브 줄 25 살림) · 자료는 추출기로 다시 뽑음
- `_fuel` 은 내 판(겸용 연료·전력)을 취함 — 서브의 「㎾ 전용은 기름 값 없음」 뜻도 그 안에 담김
- 칸 토막은 서브 `_cells`(빈 칸 = 없음 · `동력` 뗌) 를 쓰되 **코드가 하나면 칸 통째로**
  (`1.87㎾ 휘발유1.3` 처럼 한 값 안에 공백)
- 8-1-7 손료 가산비율 이름 셋은 원문이 글로 뭉개져 이름 칸에 수가 들어앉음 — 목록에 사유로 둠
- 셈 — 건설 표 2,274 · 형태 미판정 166 · 밑수 확보 713 · 미확보 438 · 기계 손료 611 → 620
  · PDF↔md 구멍은 결손 749 · 허구 547 그대로(md 고치는 창 몫)
- 전체 시험 2,445 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-18 15:00:39 +09:00

195 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""건설 공종 축(`CW-00001`) 뽑기 — 2026-09-17 브레인 8-2.
못박는 것
① 목차 글줄 가르기 — 쪽 번호가 다음 번호에 붙은 꼴(「131-2-6」 = 13쪽 + 1-2-6) · 부문마다 장 번호 1부터
② 표는 **본문 절 제목**으로 붙음 — 인용(「3-2-4 터파기(기계)’를 참고하여」)은 제목이 아님
③ 목차 오기 없음 — 연혁 표시(「('20년 보완)」)가 붙은 본문 제목이 모두 목차 번호·이름과 같음
(산림에서 목차가 번호를 잘못 적어 표가 딴 줄에 붙은 병 · 건설도 같은 함정)
④ 못 붙인 표는 목록으로 · 열쇠 CW- 안 겹침 · 다시 뽑아도 같은 열쇠
⑤ 산출 파일 = 지금 코드로 뽑은 것(산림도 같이 — 두 뽑기가 표 읽기 한 벌을 나눠 씀 · 금액 불변)
"""
from __future__ import annotations
import json
import re
import pytest
from B08_Quantity import B08_Quantity_Build_WorkItemMaster as forest
from B08_Quantity import B08_Quantity_Build_WorkItemMaster_Const as const
OUT = forest.OUT_DIR
CONST_MASTER = OUT / "const_work_item_master_2026-01-01.json"
@pytest.fixture(scope="module")
def built() -> tuple:
return const.build_const()
@pytest.fixture(scope="module")
def master() -> dict:
return json.loads(CONST_MASTER.read_text(encoding="utf-8"))
def _same_except_time(fresh: dict, saved: dict) -> None:
fresh = json.loads(json.dumps(fresh, ensure_ascii=False))
for doc in (fresh, saved):
doc.pop("generated_at")
doc["stats"].pop("keys_newly_issued") # 첫 뽑기만 발급 · 다시 뽑으면 0
doc["stats"].pop("variant_keys_newly_issued", None) # 갈래 열쇠도 같음
assert fresh == saved
def test_목차_글줄_가르기_쪽_번호가_붙은_꼴() -> None:
text = "제1장적용기준31-1 일반사항····31-1-1 목적····131-2-6 공구 및 경장비····19제2장가설공사352-1 가설물의 한도 ····35"
assert const.toc_entries(text) == [
("1", "1", "적용기준"),
("1", "1-1", "일반사항"),
("1", "1-1-1", "목적"),
("1", "1-2-6", "공구 및 경장비"),
("2", "2", "가설공사"),
("2", "2-1", "가설물의 한도"),
]
assert const.toc_entries("제11장칠공사61111-1 공통사항····611")[1] == ("11", "11-1", "공통사항")
def test_인용은_제목이_아님() -> None:
names = {("공통부문", "3-2-4"): "터파기(기계)", ("공통부문", "3-2-5"): "되메우기"}
chapters = {"2", "3", "4"}
assert (
const.titles_in("3-2-4 터파기(기계)’를 참고하여 적용한다.", "공통부문", chapters, names)
== []
)
assert const.titles_in("…경우3-2-4 터파기(기계)('25년 신설)", "공통부문", chapters, names) == [
"3-2-4"
]
assert const.titles_in("853-2-5 되메우기Q = n·q", "공통부문", chapters, names) == ["3-2-5"]
def test_목차_부문_다섯_장_마흔다섯(built: tuple) -> None:
items = built[0]["work_items"]
roots = [it for it in items if it["parent_code"] is None]
assert [r["name"] for r in roots] == list(const.DIVISIONS)
assert sum(1 for it in items if it["level"] == 2) == 45
codes = [it["work_item_code"] for it in items]
assert len(set(codes)) == len(codes)
def test_표는_다_붙고_못_붙인_표는_목록(built: tuple) -> None:
master = built[0]
stats = master["stats"]
# 2026-09-18 2,192 → 2,202 — 코덱스가 제8장 손료표 열(C2193~C2202)을 되살림
# 2026-09-18 합침 2,202 → 2,270 → 2,272 — 랩탑 서브·안티그래비티·코덱스가 원문 md 의
# 뭉친 줄·표를 잇달아 풂(8-2 시공능력 · 8-4-9 해상기계 …)
assert stats["tables_total"] == 2274
assert stats["tables_attached"] + len(master["orphan_tables"]) == stats["tables_total"]
assert stats["tables_orphan"] == len(master["orphan_tables"]) == 0
def test_목차_오기_없음_연혁_붙은_본문_제목이_목차와_같음(built: tuple) -> None:
"""본문 제목 번호가 목차에 없거나 이름이 다르면 그 절 표가 **앞 절에 조용히 붙음** — 빨강."""
items = built[0]["work_items"]
names = {(it["division"], it["number"]): it["name"] for it in items if it["level"] > 1}
titled = re.compile(r"(\d+(?:-\d+)+)\s*([^'\[\]()]{1,40}?)\s*\(\s*['‘’]\d\d")
data = json.loads(const.SOURCE.read_text(encoding="utf-8"))
files = sorted({t["source_file"] for t in data["variables"]["pum"]["tables"]})
seen, bad = 0, []
for source_file in files:
division, chapter = const.file_place(source_file)
chapters = {str(chapter - 1), str(chapter), str(chapter + 1)}
for line in (const.ROOT / source_file).read_text(encoding="utf-8").splitlines():
for found in titled.finditer(line):
raw, name = found.group(1), "".join(found.group(2).split())
number = next(
(
raw[cut:]
for cut in range(len(raw.split("-")[0]))
if raw[cut:].split("-")[0] in chapters
),
None,
)
if number is None:
continue
seen += 1
toc = "".join(names.get((division, number), "").split())
if not toc or not (name.startswith(toc) or toc.startswith(name)):
bad.append(
f"{division} {number}{found.group(2)}」 목차 「{names.get((division, number))}」"
)
assert seen > 600
assert not bad, bad
def test_열쇠_CW_안_겹침_다시_뽑아도_같음(built: tuple, master: dict) -> None:
keys = [it["work_item_key"] for it in master["work_items"]]
assert all(re.fullmatch(r"CW-\d{5}", k) for k in keys)
assert len(set(keys)) == len(keys)
assert built[3][1] == [] # 장부가 있으니 새로 안 냄
assert [it["work_item_key"] for it in built[0]["work_items"]] == keys
def test_산출_파일이_지금_코드로_뽑은_것(built: tuple, master: dict) -> None:
_same_except_time(built[0], master)
def test_산림_산출도_그대로_금액_불변() -> None:
"""표 읽기를 떼어내고(`_Table`) 건설과 나눠 써도 산림 산출은 한 글자도 안 바뀜."""
saved = json.loads((OUT / "work_item_master_2026-01-01.json").read_text(encoding="utf-8"))
master, undetermined, basis_missing, _ = forest.build()
_same_except_time(master, saved)
for name, fresh in (("form_undetermined", undetermined), ("basis_missing", basis_missing)):
saved_items = json.loads((OUT / f"{name}_2026-01-01.json").read_text(encoding="utf-8"))[
"items"
]
assert json.loads(json.dumps(fresh, ensure_ascii=False)) == saved_items
def test_산림을_고르는_쪽이_건설_파일을_안_집음() -> None:
"""⚠ `work_item_master_*` 로 끝 파일을 고르는 자리가 있음(B08 밑수 대조 · B09 근거표) —
건설 산출을 `work_item_master_const_…` 로 두면 그쪽이 건설을 집어 밑수 대조가 통째로 빠졌음(2026-09-17)."""
from B08_Quantity.B08_Quantity_Engine_BasisUnit import load_master
assert load_master()["dataset_id"] == "work_item_master_forest"
assert not list(OUT.glob("work_item_master_*const*.json"))
def test_건설_갈래도_이름을_다듬어도_같은_열쇠(master: dict) -> None:
"""④ 건설 갈래 펴기(2026-09-17) — 갈래 열쇠는 번호(`CW-00063#01`) · 자간·물결표를 손질해도 새로 안 남."""
import copy
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Keys import (
assign_variant_keys,
load_registry,
)
items = master["work_items"]
spread = [it for it in items if it.get("variants")]
assert len(spread) > 100 and sum(len(it["variants"]) for it in spread) > 400
registry = load_registry(const.CONST_KEY_REGISTRY, const.KEY_PREFIX)
nodes = copy.deepcopy(spread)
for node in nodes: # 사람이 이름을 다듬은 날 — 자간 한 칸 · 물결표 다른 벌
node["variant_keys"] = [
" ".join(k.split()).replace("", "") for k in node["variant_keys"]
]
assert assign_variant_keys(nodes, registry) == [] # 새 번호 0
for before, after in zip(spread, nodes):
assert [v["variant_key"] for v in after["variants"]] == [
v["variant_key"] for v in before["variants"]
]
def test_자간_다듬기는_진짜_띄어쓰기를_안_붙임() -> None:
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Keys import tidy_spacing
assert tidy_spacing("측 량") == "측량"
assert tidy_spacing("평 규 준 틀") == "평규준틀"
assert tidy_spacing("굴 삭 기 (무한궤도)") == "굴삭기 (무한궤도)"
assert tidy_spacing("메 붙 임 · 깬 돌 · 뒷길이 25㎝") == "메붙임 · 깬돌 · 뒷길이 25㎝"
assert tidy_spacing("토목부문 › 측 량 › 기준점 측량") == "토목부문 › 측량 › 기준점 측량"
assert (
tidy_spacing("그 외 자재의 운반품셈") == "그 외 자재의 운반품셈"
) # 한 글자 낱말 둘은 진짜 띄어쓰기
assert tidy_spacing("호박돌 및 야면석 · 뒷길이 25㎝") == "호박돌 및 야면석 · 뒷길이 25㎝"