fix(B09): 넓은 필터가 정상 자원 70건을 지우고 있었음 — 조회 우선으로 바꿈

메인의 `막자갈`→배합 `자갈` 오탐 사례를 전해 듣고 내 필터를 재 봤더니 **같은 병이 있었음.**

- **실측** — `is_non_resource_label()` 이 머리글 낱말을 **부분일치**로 보고 있어
  `건설기계운전사`·`일반기계운전사`·`작업반장`·`인력운반공`·`비계공`·`계장공` 등
  **정상 자원 70 / 745** 를 「자원 아님」으로 지우고 있었음(「계」·「작업」·「인력」에 걸림).
  그 탓에 **매칭 14건이 조용히 없어졌음**.
- **고침 둘**
  ① 머리글 판정을 **정확 일치 + 머리글 낱말 조합**(「단위작업별」·「위치및면적」)으로 좁힘.
  ② **카탈로그 조회를 필터보다 먼저** 함 — 카탈로그에 있는 이름은 **정의상 자원**이라,
     필터가 넓어져도 정상 자원이 안 사라지는 구조가 됨.
- **결과** — 매칭 91 → **106**(노무 100 · 기종 6). 산출 파일 다시 냄.

**오탐 짝 시험을 함께 넣음** (「걸려야 한다」 + **「걸리면 안 된다」**)
- 머리글 필터 — 정상 자원 8종은 안 먹고 머리글 7종은 잡는지.
- ㉡ 무대 — 이름에 「운반」이 든 `도자운반`·`덤프운반` 줄은 **안 걸리는지**
  (판정을 이름이 아니라 `equipment` 정확 일치로 하는 근거).
- ㉣ 효율 — 손료계수 0.0002085·조종원 0.125 처럼 **0~1 이지만 효율이 아닌 값**은
  안 걸리는지(효율 자리로 들어올 때만 막는 근거).

**덤으로 잡은 것** — 상세가 하나도 안 붙는 일위대가가 **제목만 서서** 「상세 줄이 없어
단가를 못 조립」하는 상태가 있었음. **붙을 상세를 먼저 모으고 없으면 제목도 안 세움**
(일위대가 67 · 건너뜀 1). 전수 시험(`모든 일위대가 총액 > 0`)이 이걸 잡았음.

PLAN 9-6 에 ㉤(열 방향 검사, 이중계상 규칙이 아니라 표 정합 검사)과 이번 필터 교훈 기록.

자체검증 — `pytest tmp/tests/ -q` **123 passed** · ruff 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 23:33:42 +09:00
co-authored by Claude Opus 5
parent f2d12afef7
commit 3762a06b25
4 changed files with 674 additions and 15 deletions
+13 -8
View File
@@ -200,6 +200,18 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
title_code = f"B-{work_item_code}"
if title_code in build.book.titles:
continue
# ⚠ **붙을 상세를 먼저 모으고, 하나도 없으면 제목도 안 세운다.**
# 제목만 세워 두면 「상세 줄이 없어 단가를 못 조립」하는 빈 일위대가가 남는다
# (기계 층이 안 선 기종만 참조하는 공종에서 실제로 생겼음).
attachable = [
(row, row.resource_code if row.resource_kind == "labor" else f"X-{row.resource_code}")
for row in rows
]
attachable = [(row, ref) for row, ref in attachable if ref in build.book.titles]
if not attachable:
build.skipped.append(work_item_code)
continue
unit = next((r.amount_unit for r in rows if r.amount_unit), "")
build.book.add_title(
PriceTitle(
@@ -210,15 +222,8 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
unit=unit,
)
)
added = 0
for row in rows:
ref = row.resource_code if row.resource_kind == "labor" else f"X-{row.resource_code}"
if ref not in build.book.titles:
continue
for row, ref in attachable:
build.book.add_detail(PriceDetail(title_code, ref, row.amount))
added += 1
if added == 0:
build.skipped.append(work_item_code)
return build