test(b08): 원문 맞대기 표 성격 사각 좁힘 — 품 표 276 → 305 알아봄 · 못 보는 12 와 비목 구성표 7 을 목록으로
브레인 일감 ①②. 시험이 못 보는 자리를 좁히고, 못 좁히는 자리는 까닭과 함께 남김.
- ① 표 성격 — 품 표 317 중 못 알아보던 41 의 원인을 가름
· 넓힘: 단위 붙은 수(`10ℓ`·`5인/km`) · 범위 값(`3.3∼5.9`) · 「인력구분」「인원(명)」「시멘트(kg)」 표지 ·
단위만 늘어선 줄(`| | ㎥ | ㎥ |`) → 305 알아봄(성격이 뒤집히면 빨강 — 10가지 뒤집어 확인)
· 못 봄 12 → 목록 `form_blind` 에 까닭: 머리 소실 4 · 값 칸 비어 있음 4 · 뭉친 칸·식 글 2 ·
연료표 1(F0046 — 같은 모양 셋은 참조인데 이것만 품) · 증가율표 1(13-4-1)
· ⚠ 12-1-3 한중콘크리트 표(F0333)는 머리가 「온도 품종 0℃때…」인데 몸 줄이 앞 시멘트 표와 똑같음 — 원문 md 의심
· 표지를 넓혀 새로 걸린 참조표 둘(8-6-1 F0227 · 13-4-4 F0410)은 사람 판정 참조라 까닭 달아 목록에
- ② 「재료비의 5%」 꼴 — 원문에 표 전체가 비목 구성(`재료비 | 설치비 재료비의 5%`)인 것은 7표(12장)
· 모양은 시험으로 잡힘(첫 칸이 모두 비목 이름) · 참조냐 품이냐는 원문으로 못 가림 → `cost_rule` 「사람이 봐야 함」
· 같은 꼴인데 12-29 만 requirement, 나머지 6 은 reference — 목록에 드러남
· 「…비의 N%」 가 든 나머지 15표는 품 표 안의 한 칸(잡품 「주재료비의 5%」 · 감률 「굴취품의 20%」)이라 따로 안 셈
- 목록 칸 `why`(사람이 적은 까닭)는 맞대기에서 뺌 · --prune 도 까닭을 지킴
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -7,7 +7,10 @@
|
||||
"basis: md=원문에서 읽은 밑수 [수, 단위] · master=마스터 밑수 · 둘 중 하나가 null 이면 한쪽만 있음.",
|
||||
"notes: md_only=원문 [주]에 있는데 마스터에 없는 줄 · master_only=마스터에만 있는 줄.",
|
||||
"form: 원문 표에 품 표 표지(직종 이름·소요인력·(인)·수량 단위 칸 + 숫자)가 있는데 마스터가 reference·coefficient 로 둔 표 · md_mark=그 표지 칸.",
|
||||
"basis 의 12-7-1·12-7-2·12-8·12-38-3·13-16-2(F0339·F0340·F0341·F0395·F0449)는 일부러 다름 — 작업조 표라 밑수가 시공량 열(c3da333e)."
|
||||
"basis 의 12-7-1·12-7-2·12-8·12-38-3·13-16-2(F0339·F0340·F0341·F0395·F0449)는 일부러 다름 — 작업조 표라 밑수가 시공량 열(c3da333e).",
|
||||
"form_blind: 마스터는 품 표(requirement·productivity)인데 원문에서 품 표지를 못 찾은 표 — 시험이 성격 뒤집힘을 못 잡는 자리.",
|
||||
"cost_rule: 첫 칸이 모두 비목(재료비·설치비 …)인 비목 구성표 — 모양은 잡히나 참조냐 품이냐는 원문으로 못 가림 · 사람이 봐야 함.",
|
||||
"why: 사람이 적은 까닭 — 시험은 맞대기에서 뺌."
|
||||
],
|
||||
"place": {
|
||||
"F0452": {
|
||||
@@ -539,6 +542,12 @@
|
||||
"master": "reference",
|
||||
"md_mark": "적용시공량"
|
||||
},
|
||||
"F0227": {
|
||||
"section": "8-6-1",
|
||||
"master": "reference",
|
||||
"md_mark": "유효 살포량/1회",
|
||||
"why": "사람 판정 reference(Forms.py) — 기종별 희석배수별 1회 살포량 계산표"
|
||||
},
|
||||
"F0257": {
|
||||
"section": "9-11-1",
|
||||
"master": "coefficient",
|
||||
@@ -574,10 +583,184 @@
|
||||
"master": "reference",
|
||||
"md_mark": "매"
|
||||
},
|
||||
"F0410": {
|
||||
"section": "13-4-4",
|
||||
"master": "reference",
|
||||
"md_mark": "야면석(㎥) 호박돌(㎥)",
|
||||
"why": "사람 판정 reference(Forms.py) — 채움 콘크리트 소요량 자재 원단위표, B08 채움표가 씀(품이면 두 번 셈) · 안티그래비티 2바퀴는 requirement 주장"
|
||||
},
|
||||
"F0454": {
|
||||
"section": "14-2",
|
||||
"master": "reference",
|
||||
"md_mark": "작업량"
|
||||
}
|
||||
},
|
||||
"form_blind": {
|
||||
"F0046": {
|
||||
"section": "2-1-1",
|
||||
"master": "requirement",
|
||||
"why": "기계별 연료표(천공기 주연료 3ℓ·잡품 95%) — 품 낱말 없음. 같은 모양 F0044·F0045·F0047 은 사람 판정 reference 인데 이것만 requirement"
|
||||
},
|
||||
"F0062": {
|
||||
"section": "2-1-12",
|
||||
"master": "requirement",
|
||||
"why": "값 칸이 「층적부피에 따라 별도계산」·「〃」 — 숫자 없음"
|
||||
},
|
||||
"F0140": {
|
||||
"section": "5-26-1",
|
||||
"master": "requirement",
|
||||
"why": "머리 소실 — 토성×깊이 숫자만 남음(사람 판정: 100㎡당 보통인부)"
|
||||
},
|
||||
"F0141": {
|
||||
"section": "5-26-2",
|
||||
"master": "requirement",
|
||||
"why": "머리 소실 — 위와 같음"
|
||||
},
|
||||
"F0142": {
|
||||
"section": "5-26-3",
|
||||
"master": "requirement",
|
||||
"why": "머리 소실 — 위와 같음"
|
||||
},
|
||||
"F0195": {
|
||||
"section": "7-14",
|
||||
"master": "productivity",
|
||||
"why": "칸이 뭉침(「㎥ 0.06∼0.07 4.8∼6.0」) — 원문 md 가 병합 칸을 못 풂"
|
||||
},
|
||||
"F0237": {
|
||||
"section": "8-11",
|
||||
"master": "productivity",
|
||||
"why": "작업량이 식 글(「Q = 3.5 ㎥/hr」)"
|
||||
},
|
||||
"F0333": {
|
||||
"section": "12-1-3",
|
||||
"master": "requirement",
|
||||
"why": "⚠ 원문 md 의심 — 머리는 한중콘크리트 「온도 품종 | 0℃때 …」인데 몸 줄이 앞 표 F0332(시멘트·모래·자갈 kg)와 똑같음 · PDF 대조 필요"
|
||||
},
|
||||
"F0342": {
|
||||
"section": "12-9-1",
|
||||
"master": "requirement",
|
||||
"why": "수량 칸이 비어 있음 — 원문에 값 없음(「반중력식 옹벽 참조」)"
|
||||
},
|
||||
"F0343": {
|
||||
"section": "12-9-2",
|
||||
"master": "requirement",
|
||||
"why": "수량 칸이 비어 있음 — 원문에 값 없음"
|
||||
},
|
||||
"F0344": {
|
||||
"section": "12-9-3",
|
||||
"master": "requirement",
|
||||
"why": "수량 칸이 비어 있음 — 원문에 값 없음"
|
||||
},
|
||||
"F0405": {
|
||||
"section": "13-4-1",
|
||||
"master": "requirement",
|
||||
"why": "증가율(%) 표(높이별 30·40·60%) — 품이 아니라 할증률 모양 · requirement 가 맞는지 사람 확인"
|
||||
}
|
||||
},
|
||||
"cost_rule": {
|
||||
"F0369": {
|
||||
"section": "12-22",
|
||||
"master": "reference",
|
||||
"md_rows": [
|
||||
[
|
||||
"재료비"
|
||||
],
|
||||
[
|
||||
"설치비",
|
||||
"재료비의 5%"
|
||||
]
|
||||
],
|
||||
"why": "설치비 = 재료비의 5% — 참조(비율 지시)로 둘지 비율 자원 줄로 세울지는 설계 방식 결정"
|
||||
},
|
||||
"F0379": {
|
||||
"section": "12-29",
|
||||
"master": "requirement",
|
||||
"md_rows": [
|
||||
[
|
||||
"재료비",
|
||||
"",
|
||||
"필요수량"
|
||||
],
|
||||
[
|
||||
"설치비",
|
||||
"재료비의 5%"
|
||||
]
|
||||
],
|
||||
"why": "⚠ 같은 꼴 6표는 reference 인데 이것만 requirement — 설치비 = 재료비의 5%"
|
||||
},
|
||||
"F0382": {
|
||||
"section": "12-31",
|
||||
"master": "reference",
|
||||
"md_rows": [
|
||||
[
|
||||
"재료비"
|
||||
],
|
||||
[
|
||||
"설치비",
|
||||
"주재료비의 5%"
|
||||
]
|
||||
],
|
||||
"why": "설치비 = 주재료비의 5% — F0369 와 같은 물음"
|
||||
},
|
||||
"F0383": {
|
||||
"section": "12-32",
|
||||
"master": "reference",
|
||||
"md_rows": [
|
||||
[
|
||||
"재료비",
|
||||
"P.V.C ∅54m/m"
|
||||
],
|
||||
[
|
||||
"설치비",
|
||||
"주재료비의 10%"
|
||||
]
|
||||
],
|
||||
"why": "설치비 = 주재료비의 10% — F0369 와 같은 물음"
|
||||
},
|
||||
"F0386": {
|
||||
"section": "12-34-2",
|
||||
"master": "reference",
|
||||
"md_rows": [
|
||||
[
|
||||
"재료비",
|
||||
"1회 기준 46.1%"
|
||||
],
|
||||
[
|
||||
"노무비",
|
||||
"1회 기준 47.1%"
|
||||
]
|
||||
],
|
||||
"why": "재료비·노무비 「1회 기준 46.1%·47.1%」 — 거푸집 사용횟수 비율"
|
||||
},
|
||||
"F0388": {
|
||||
"section": "12-34-4",
|
||||
"master": "reference",
|
||||
"md_rows": [
|
||||
[
|
||||
"재료비",
|
||||
"JOINT FILLER"
|
||||
]
|
||||
],
|
||||
"why": "재료비 칸에 자재 이름(JOINT FILLER)만 — 값 없음"
|
||||
},
|
||||
"F0390": {
|
||||
"section": "12-36",
|
||||
"master": "reference",
|
||||
"md_rows": [
|
||||
[
|
||||
"제작비",
|
||||
"견적처리"
|
||||
],
|
||||
[
|
||||
"운송비",
|
||||
"견적처리"
|
||||
],
|
||||
[
|
||||
"설치비",
|
||||
"견적처리"
|
||||
]
|
||||
],
|
||||
"why": "제작비·운송비·설치비 모두 「견적처리」 — 값 없음"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,15 @@ KNOWN = Path(__file__).resolve().parent / "fixtures/work_item_master_source_know
|
||||
TOC_TABLE = "F0001"
|
||||
#: 이 줄 뒤는 부록(할인·할증 공종표 · 적용 공종 · 단가산출서 예시) — 공종 품이 아니라 `orphan_tables` 몫.
|
||||
APPENDIX = "〔부록 1〕"
|
||||
#: 알려진 목록 갈래. `form_blind`·`cost_rule` 은 어긋남이 아니라 **시험이 못 가리는 자리**를 적어 둔 것.
|
||||
KIND_NAMES = {
|
||||
"place": "표 자리",
|
||||
"basis": "밑수",
|
||||
"notes": "[주]",
|
||||
"form": "표 성격",
|
||||
"form_blind": "표 성격 — 품 표지를 못 알아봄",
|
||||
"cost_rule": "비목 구성표 — 사람이 봐야 함",
|
||||
}
|
||||
|
||||
# ── 원문 읽기 ────────────────────────────────────────────────────────────
|
||||
_SEP = re.compile(r"\s*\|(\s*:?-+:?\s*\|)+\s*")
|
||||
@@ -168,35 +177,68 @@ _WORKER = re.compile(
|
||||
r"인\s*부|기능공|운전|목\s*공|석\s*공|철근공|콘크리트공|기\s*사|벌목부|작업반장|기술자|"
|
||||
r"용접공|미장공|방수공|배관공|비계공"
|
||||
)
|
||||
#: 품 낱말 — 공백을 지운 칸에서 찾음(`주 재 료` → `주재료`).
|
||||
_WORK_MARK = re.compile(
|
||||
r"\(인\)|소요인력|소요량|주입량|작업능력|작업량|수집량|집재량|공정량|시공량|인/|/인|\(시간\)|\(hr\)"
|
||||
r"\(인\)|소요인력|소요량|주입량|작업능력|작업량|수집량|집재량|공정량|시공량|살포량|처리량|"
|
||||
r"인력구분|인력구성|적용인부|인원\(명\)|^주재료$|인/|/인|\(시간\)|\(hr\)|[가-힣]\((kg|㎏|g|㎥|ℓ)\)"
|
||||
)
|
||||
_QTY_UNIT = {"인", "매", "개", "본", "kg", "㎏", "㎥", "㎡", "m", "ℓ", "대", "톤", "hr", "시간"}
|
||||
#: 숫자 칸 — `0.16` · `1.5인` · `0.5ℓ` · 한 칸에 여러 값 `0.04 0.06 0.10`.
|
||||
_NUMBER = re.compile(r"^\d[\d,]*(\.\d+)?(\s*(인|ℓ|시간|㎥|개|본|매|kg|m|%))?(\s+[-\d,.]+)*$")
|
||||
#: 숫자 칸 — `0.16` · 범위 `3.3∼5.9` · 단위 붙음 `1.5인`·`10ℓ`·`5인/km`·`2.18Roll` ·
|
||||
#: 한 칸에 여러 값 `0.04 0.06 0.10`.
|
||||
_NUMBER = re.compile(
|
||||
r"^\d[\d,]*(\.\d+)?(\s*[~∼~-]\s*\d[\d,]*(\.\d+)?)?"
|
||||
r"(\s*(인|ℓ|시간|㎥|㎡|개|본|매|kg|m|%|Roll)(/[a-z㎞㏊]+)?)?(\s+[-\d,.]+)*$"
|
||||
)
|
||||
|
||||
|
||||
def md_work_mark(table: dict) -> str | None:
|
||||
"""품 표 표지 칸 — 있으면 그 칸 글, 없으면 `None`.
|
||||
"""품 표 표지 칸 — 있으면 그 칸 글, 없으면 `None`. 표에 숫자 칸이 있을 때만 봄.
|
||||
|
||||
① 표 어딘가에 **짧은 칸(20자 이하)** 으로 직종 이름·품 낱말(`소요인력`·`(인)`·`작업능력` …)이 있고
|
||||
숫자 칸도 있음 · ② 한 줄에 수량 단위 칸(`인`·`매`·`㎥` …)과 숫자 칸이 함께 있음.
|
||||
① **짧은 칸(20자 이하)** 에 직종 이름·품 낱말(`소요인력`·`인력구분`·`(인)`·`시멘트(kg)` …)
|
||||
② 한 줄에 수량 단위 칸(`인`·`매`·`㎥` …)과 숫자 칸이 함께 있음
|
||||
③ 단위만 늘어선 줄(`| | ㎥ | ㎥ | ㎥ |`) — 값은 아래 줄에 있음(7-1-1 수확)
|
||||
긴 설명 글 속 「보통인부」는 이름이 아님(손료계수표 적용기준 칸).
|
||||
ponytail: 품 표 317 중 276 만 알아봄 — 값이 범위(`3.3∼5.9`)거나 머리가 여러 줄로 갈린 표는 못 봄.
|
||||
⚠ 못 알아보는 품 표는 알려진 목록 `form_blind` 에 까닭과 함께 둠 — 그 표는 성격이 뒤집혀도 못 잡음.
|
||||
"""
|
||||
rows = [table["headers"], *table["rows"]]
|
||||
cells = [c for row in rows for c in row]
|
||||
if any(_NUMBER.match(c) for c in cells):
|
||||
for c in cells:
|
||||
if len(c) <= 20 and (_WORKER.search(c) or _WORK_MARK.search(c)):
|
||||
return c
|
||||
if not any(_NUMBER.match(c) for c in cells):
|
||||
return None
|
||||
for c in cells:
|
||||
if len(c) <= 20 and (_WORKER.search(c) or _WORK_MARK.search(c.replace(" ", ""))):
|
||||
return c
|
||||
for row in rows:
|
||||
unit = next((c for c in row if c in _QTY_UNIT), None)
|
||||
if unit and any(_NUMBER.match(c) for c in row):
|
||||
return unit
|
||||
filled = {c for c in row[1:] if c}
|
||||
if len(filled) == 1 and filled <= _QTY_UNIT:
|
||||
return next(iter(filled))
|
||||
return None
|
||||
|
||||
|
||||
#: 비목 이름 — 이것만으로 첫 칸이 채워진 표가 비목 구성표(`재료비 | 설치비 재료비의 5%`).
|
||||
_COST_ITEMS = {"재료비", "설치비", "노무비", "경비", "제작비", "운송비", "자재비", "주재료비"}
|
||||
|
||||
|
||||
def md_cost_rows(table: dict) -> list[list[str]] | None:
|
||||
"""비목 구성표면 그 줄들(뒤 빈 칸 뗌), 아니면 `None`.
|
||||
|
||||
⚠ 이 꼴은 **모양은 잡히나 성격은 원문으로 못 가림** — 「설치비 = 재료비의 5%」 가 품(자원 줄)인지
|
||||
참조(비율 지시)인지는 설계 방식의 결정이라 사람이 봐야 함(알려진 목록 `cost_rule`).
|
||||
"""
|
||||
rows = [r for r in table["rows"] if r and r[0].strip()]
|
||||
if not rows or any(r[0].replace(" ", "") not in _COST_ITEMS for r in rows):
|
||||
return None
|
||||
out = []
|
||||
for r in rows:
|
||||
cut = list(r)
|
||||
while cut and not cut[-1]:
|
||||
cut.pop()
|
||||
out.append(cut)
|
||||
return out
|
||||
|
||||
|
||||
# ── 맞대기 ──────────────────────────────────────────────────────────────
|
||||
def _norm(text: str) -> str:
|
||||
return " ".join(str(text).split())
|
||||
@@ -225,7 +267,7 @@ def load_sources() -> dict:
|
||||
def current_mismatches(src: dict) -> dict[str, dict]:
|
||||
"""표 자리·밑수·[주]·표 성격 어긋남 — 알려진 목록과 같은 꼴. 밑수·[주]·성격은 공종에 붙은 표만 봄."""
|
||||
lines = src["lines"]
|
||||
out: dict[str, dict] = {"place": {}, "basis": {}, "notes": {}, "form": {}}
|
||||
out: dict[str, dict] = {kind: {} for kind in KIND_NAMES}
|
||||
appendix = next(i for i, x in enumerate(lines, 1) if x.strip().startswith(APPENDIX))
|
||||
orphans = {o["pum_table_id"] for o in src["master"]["orphan_tables"]}
|
||||
for table in src["tables"]:
|
||||
@@ -267,12 +309,19 @@ def current_mismatches(src: dict) -> dict[str, dict]:
|
||||
"master_only": [x for x in entry["notes"] if x not in md_note],
|
||||
}
|
||||
|
||||
if entry["pum_form"] in ("reference", "coefficient") and (mark := md_work_mark(table)):
|
||||
out["form"][table["id"]] = {
|
||||
form = entry["pum_form"]
|
||||
if cost_rows := md_cost_rows(table):
|
||||
out["cost_rule"][table["id"]] = {
|
||||
"section": section,
|
||||
"master": entry["pum_form"],
|
||||
"md_mark": mark,
|
||||
"master": form,
|
||||
"md_rows": cost_rows,
|
||||
}
|
||||
continue # 성격은 사람 몫 — 품 표지로 재지 않음
|
||||
mark = md_work_mark(table)
|
||||
if form in ("reference", "coefficient") and mark:
|
||||
out["form"][table["id"]] = {"section": section, "master": form, "md_mark": mark}
|
||||
elif form in ("requirement", "productivity") and not mark:
|
||||
out["form_blind"][table["id"]] = {"section": section, "master": form}
|
||||
return out
|
||||
|
||||
|
||||
@@ -280,6 +329,10 @@ def load_known() -> dict[str, dict]:
|
||||
return json.loads(KNOWN.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _without_why(entry: dict) -> dict:
|
||||
return {k: v for k, v in entry.items() if k != "why"}
|
||||
|
||||
|
||||
# ── 시험 ────────────────────────────────────────────────────────────────
|
||||
def test_표_수와_자리(src: dict) -> None:
|
||||
"""원문 표가 원본(`pum_forest`)에 같은 차례·같은 줄로 있고, 마스터에 빠짐없이 한 번씩 있음."""
|
||||
@@ -340,14 +393,14 @@ def test_셀_값이_글자까지_같음(src: dict) -> None:
|
||||
assert not diffs, f"셀 어긋남 {len(diffs)}건 — 앞 30:\n" + "\n".join(diffs[:30])
|
||||
|
||||
|
||||
KIND_NAMES = {"place": "표 자리", "basis": "밑수", "notes": "[주]", "form": "표 성격"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", list(KIND_NAMES))
|
||||
def test_알려진_어긋남_밖은_없음(src: dict, kind: str) -> None:
|
||||
"""표 자리·밑수·[주]·표 성격 — 알려진 목록과 똑같아야 함. 새로 어긋남 · 모습 바뀜 · 고쳐짐 셋 다 빨강."""
|
||||
"""알려진 목록과 똑같아야 함 — 새로 어긋남 · 모습 바뀜 · 고쳐짐 셋 다 빨강.
|
||||
|
||||
목록 칸 `why` 는 사람이 적은 까닭 — 맞대기에서 뺌.
|
||||
"""
|
||||
now = current_mismatches(src)[kind]
|
||||
known = load_known()[kind]
|
||||
known = {k: _without_why(v) for k, v in load_known()[kind].items()}
|
||||
lines: list[str] = []
|
||||
for tid in sorted(set(now) - set(known)):
|
||||
lines.append(f" 새로 어긋남 {tid}: {json.dumps(now[tid], ensure_ascii=False)}")
|
||||
@@ -368,6 +421,8 @@ if __name__ == "__main__" and "--prune" in sys.argv:
|
||||
data = load_known()
|
||||
for kind in KIND_NAMES:
|
||||
before = len(data[kind])
|
||||
data[kind] = {k: v for k, v in data[kind].items() if current[kind].get(k) == v}
|
||||
data[kind] = {
|
||||
k: v for k, v in data[kind].items() if current[kind].get(k) == _without_why(v)
|
||||
}
|
||||
print(f"{KIND_NAMES[kind]}: {before} → {len(data[kind])}")
|
||||
KNOWN.write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user