fix(B08): 갈래 키를 조립하지 않고 원본값으로 보냄 + 특수문자 깃발

⚠ 계약 변경(2026-09-07 3자). 품셈 원문이 규격 표기를 한 종류로 안 씀 —
**물결표만 셋**(`∼` U+223C 662회 · `~` U+FF5E 459회 · `~` U+007E 2회),
곱셈표도 둘(`×` 97회 · `x` 4회). 같은 절 안에서도 갈림(13-06-01·02 는 `∼`,
13-06-03 은 `~`). **두 창이 각자 키를 조립하면 글자 하나로 영영 안 맞음.**

- `#55cm이하`·`#직경60㎝이상∼80㎝미만` 같은 키를 **우리가 만들지 않음**.
  `variant_axis`(어느 축) + `variant_value`(저장 원본값 그대로)만 보내고,
  원문을 읽는 쪽이 표기를 흡수함. 「…㎝ 이하」 구간 나누기도 그쪽 몫 —
  그 구간이 품셈 표의 구조이기 때문.
- ⚠ **메/찰은 예외** — 키 표기가 아니라 **공종 자체가 갈리는 의미 판정**이라
  코드가 `FP-13-06-01`/`-02` 로 통째로 달라짐. 그것은 우리 몫으로 유지.
- 계약 시험에 못 박음 — 코드에 `#`·`㎝`·`cm` 가 들어가면 깨짐. 원본값을
  가공해도 깨짐(물결표 종류를 바꾸거나 단위를 붙이면 안 됨).
- `data_masonry` 의 갈래표는 지우지 않고 **참고**로 남김 — 서브 판정과
  어긋나면 그것이 곧 신호.

마스터에 `special_glyphs` 깃발 — 규격 표기에 쓰인 특수문자 종류. 값은 안 고치고
표시만 함. 물결표가 두 종류 섞인 표가 실제로 5개 있는 것을 시험으로 못 박음
(계약 근거가 사라지면 깨짐).

검증 — 전체 653 passed, tsc 오류 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 04:57:54 +09:00
co-authored by Claude Opus 5
parent 089f80b550
commit 5bd129bae4
5 changed files with 818 additions and 12 deletions
@@ -407,6 +407,29 @@ def formula_rows(table: dict[str, Any]) -> list[str]:
return found
# ⚠ **규격 표기에 쓰이는 특수문자** — 원문이 한 종류로 안 쓴다(2026-09-07 실측).
# 물결표만 셋이다: ``(U+223C) 662회 · ``(U+FF5E) 459회 · `~`(U+007E) 2회.
# 곱셈표도 `×`(U+00D7) 97회 · `x`(U+0078) 4회로 갈린다.
# **두 창이 각자 갈래 키를 조립하면 글자 하나로 영영 안 맞는다** — 그래서 우리는
# 키를 조립하지 않고 **저장 원본값만** 보낸다(인계 계약). 여기서는 **표시만** 한다.
# ⚠ 값을 고치지 않는다 — 원문 표기를 흡수하는 것은 **원문을 읽는 쪽**의 몫이다.
SPECIAL_GLYPHS = {
"": "(U+223C)",
"": "(U+FF5E)",
"~": "~(U+007E)",
"×": "×(U+00D7)",
"x": "x(U+0078)",
"": "(U+2013)",
"": "(U+2010)",
}
def special_glyphs(table: dict[str, Any]) -> list[str]:
"""규격 표기에 쓰인 특수문자 종류. 갈래 키를 맞출 때 대조할 자리."""
text = " ".join(norm(cell) for row in table.get("rows", []) for cell in row)
return sorted({SPECIAL_GLYPHS[ch] for ch in text if ch in SPECIAL_GLYPHS})
def crew_table(table: dict[str, Any]) -> bool:
"""작업조 + 시공량으로 적힌 표인가."""
hay = " ".join(norm(h) for h in table.get("headers", []))
@@ -532,6 +555,8 @@ def build() -> dict[str, Any]:
"spaced_names": spaced_names(table),
# ⚠ 공식 기호 줄 — 소요량이 아니다. 자원으로 세면 이중계상.
"formula_rows": formula_rows(table),
# ⚠ 규격 표기 특수문자 — 갈래 키를 맞출 때 대조할 자리(값은 안 고침).
"special_glyphs": special_glyphs(table),
# 공식 기호가 이 표에 직접 있는가 — 없으면 앞 표에서 물려받는 모양이다.
"capacity_formula_here": capacity_formula_pending(table),
"variant_key": variant_axis(table),
+17 -2
View File
@@ -499,6 +499,9 @@ def _earthwork_rows(
"spec_detail": "",
"composite_parts": None,
"structure_kind": None,
# 토공·운반 줄에는 갈래 축이 없다 — 그래도 **칸은 둔다**(계약이 한 모양).
"variant_axis": None,
"variant_value": None,
"spec_class": None,
"spec_class_basis": "",
# 토공 줄은 막힐 자리가 없다 — 그래도 **칸은 둔다**(계약이 한 모양이어야 한다).
@@ -551,6 +554,9 @@ def _haul_rows(
"spec_detail": "",
"composite_parts": None,
"structure_kind": None,
# 토공·운반 줄에는 갈래 축이 없다 — 그래도 **칸은 둔다**(계약이 한 모양).
"variant_axis": None,
"variant_value": None,
"spec_class": None,
"spec_class_basis": "",
"blocked_kind": None,
@@ -610,6 +616,7 @@ def _structure_rows(
bond = str((structure.get("options") or {}).get("bond") or "").strip()
bond_codes = entry.get("bond_codes") or {}
if bond in bond_codes:
# 메/찰은 **의미 판정**이라 우리 몫이다 — 공종 자체가 갈린다.
code = bond_codes[bond]
class_key = bond
class_basis = f"쌓기 방식 「{bond}」 → 품셈 13-6 {code.split('-')[-1]}"
@@ -620,8 +627,7 @@ def _structure_rows(
)
if code and entry.get("class_from") == "back_length":
class_key, class_basis = masonry_class(structure.get("options") or {})
if class_key:
code = f"{code}#{normalize_kind_key(class_key)}"
composite = mapping.composite_for(type_id) if code is None else None
kind = structure_kind(structure) if composite else None
parts: list[dict[str, Any]] | None = None
@@ -629,6 +635,9 @@ def _structure_rows(
if composite:
parts, parts_missing = composite_quantities(structure, composite, mapping)
blocked_kind, blocked_reason = blocked_of(structure, class_basis)
# 갈래 축과 **저장 제원 원본값**. 가공하지 않는다.
variant_axis = str(entry.get("variant_axis") or "") or None
variant_value = (structure.get("options") or {}).get(variant_axis) if variant_axis else None
if code is None and composite is None:
unmatched.append(f"{wording_type_label(type_id)} — 품셈 공종을 아직 못 이었습니다")
elif entry.get("class_from") in ("back_length", "bond") and class_key is None:
@@ -660,6 +669,12 @@ def _structure_rows(
"blocked_kind": blocked_kind,
"blocked_reason": blocked_reason,
# 규격 갈래(뒷길이 …㎝ 이하) — 못 고르면 사유가 남는다.
# ⚠ **갈래 키 문자열을 우리가 조립하지 않는다** (2026-09-07 계약 변경).
# 품셈 원문이 물결표를 섞어 쓴다(`` U+223C / `` U+FF5E). 두 창이 각자
# 키를 조립하면 **글자 하나로 영영 안 맞는다.** 우리는 **어느 축인지와
# 저장 원본값**만 보내고, 원문을 읽는 쪽이 그 표기를 흡수한다.
"variant_axis": variant_axis,
"variant_value": variant_value,
"spec_class": class_key,
"spec_class_basis": class_basis,
# ⚠ 물량을 못 채운 조각 — 0 으로 적지 않고 사유와 함께 드러낸다.
@@ -119,16 +119,16 @@
"work_item_code": "FP-13-04-05",
"master_name": "돌쌓기 > 찰쌓기(장비)",
"note": "인력 시공이면 FP-13-04-04",
"class_from": "back_length",
"class_note": "저장 제원 `back_len_cm` 으로 「…㎝ 이하」 구간을 고름 — 자동 판정"
"class_note": "⚠ 갈래 키 문자열을 여기서 만들지 않는다 — `variant_axis`+`variant_value`(저장 원본값)만 보내고 「…㎝ 이하」 구간 나누기는 **품셈 원문을 읽는 쪽**이 한다(2026-09-07 계약).",
"variant_axis": "back_len_cm"
},
{
"type_id": "masonry_dry",
"work_item_code": "FP-13-04-02",
"master_name": "돌쌓기 > 메쌓기(장비)",
"note": "인력 시공이면 FP-13-04-01",
"class_from": "back_length",
"class_note": "저장 제원 `back_len_cm` 으로 「…㎝ 이하」 구간을 고름 — 자동 판정"
"class_note": "⚠ 갈래 키 문자열을 여기서 만들지 않는다 — `variant_axis`+`variant_value`(저장 원본값)만 보내고 「…㎝ 이하」 구간 나누기는 **품셈 원문을 읽는 쪽**이 한다(2026-09-07 계약).",
"variant_axis": "back_len_cm"
},
{
"type_id": "pipe_inlet_basin",
@@ -148,7 +148,8 @@
"메쌓기": "FP-13-06-01",
"찰쌓기": "FP-13-06-02"
},
"class_note": "저장 제원 `bond`(메쌓기/찰쌓기)로 품셈 13-6-1·13-6-2 를 고름 — 자동 판정"
"class_note": "메/찰(`bond`)은 **공종 자체가 갈리는 의미 판정**이라 여기서 고른다. 직경 갈래는 `variant_value` 로 원본값만 보낸다 — 원문이 물결표를 섞어 써서.",
"variant_axis": "stone_cm"
}
],
"pending_user": {
@@ -264,5 +265,17 @@
"hand_mixed": 408327
}
}
}
},
"variant_contract": {
"note": "갈래 계약(2026-09-07 3자) — **키 문자열을 두 창이 각자 조립하지 않는다.**",
"why": "품셈 원문이 물결표를 섞어 쓴다: 13-06-01·02 는 ``(U+223C), 13-06-03 은 ``(U+FF5E). 각자 조립하면 글자 하나로 영영 안 맞는다.",
"b08_sends": [
"work_item_code",
"variant_axis",
"variant_value",
"kind_basis"
],
"b09_does": "원문 표기(물결표·공백·괄호)를 흡수해 자기 키로 옮긴다. 「…㎝ 이하」 구간 나누기도 그쪽 몫 — 그 구간이 **품셈 표의 구조**이기 때문."
},
"masonry_class_reference": "resources/data_masonry/masonry_class_2026-01-01.json — ⚠ 이제 **참고용**이다. 서브 판정과 어긋나면 그것이 곧 신호다."
}
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-08T01:52:34+09:00",
"generated_at": "2026-09-08T04:57:04+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": {
"dataset_id": "pum_forest",
@@ -12,8 +12,8 @@
"files": [
{
"file": "work_item_master_2026-01-01.json",
"sha256": "08b0c7c26c4aa569ace3b13490b8cbd00606254abb1439894eac750d387bfb03",
"size_bytes": 839464
"sha256": "1c49012f9f50d721676ad8def15c87c96b77aad83123286dd6981b7d9cefcd81",
"size_bytes": 856450
},
{
"file": "form_undetermined_2026-01-01.json",
File diff suppressed because it is too large Load Diff