랩탑 메인의 추출기(줄 번호를 안 쓰고 글자로 맞대는 쪽)가 서서 원천 벌이 다시 뽑혔음. 막혀 있던 「다시 짓기」를 그 위에서 돌림. - 산림 밑수 확보 **238 → 242** · 미확보 92 → 88 (5-19-1 · 5-27 · 5-28-1 · 9-21 되살아남) - 건설 표 **2,192 → 2,202** — 코덱스가 되살린 제8장 손료표 열(C2193~C2202) · 표 귀속 2,202/2,202 - 건설 [주] 실린 표 1,106 → 1,110 · 형태 미판정 881 → 891(새 표 열이 미판정으로 들어옴) ⚠ **5-26-1·2·3 밑수를 사람 판정 100㎡ → 원문 992㎡ 로 물림**(브레인 판정) 원문 L2878·L2892·L2905 가 「(인/992㎡당)」. 앞서 100㎡ 는 **머리가 소실돼 원문을 못 읽던 때** 흔한 밑수로 메운 값이었음 — 이 고리의 잣대가 「원문이 정본」이라 사람 판정이 물러남. ⚠ 100 과 992 는 열 배 가까이 달라 **금액이 크게 갈리는 자리** — 사유를 코드와 시험에 남김 (992㎡ 는 「0.1정(1반)」에 해당하는 옛 면적 단위) 시험·이름표 손질 — 지문 갱신 · 건설 표 수 2,192 → 2,202 · 이름표 줄 수(원천 2,202 · 미확보 88 · 건설 미판정 891) · 「사람 판정은 원문이 나오면 물러난다」를 시험으로 못박음 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsGw1Dz9HmhuAxGisxmDPF
193 lines
9.0 KiB
Python
193 lines
9.0 KiB
Python
"""건설 공종 축(`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)을 되살림
|
||
assert stats["tables_total"] == 2202
|
||
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㎝"
|