feat(z01): 기초단가 다섯을 한 표로 모으는 이름표 · 줄 열쇠·null 판정

- `merged_tables` 다섯(노임·자재·유가·제비율·기계) — 열 62. **값은 안 옮김**, 열 이름·단위·숨김과
  `editable`(고칠 수 있나)만 담음. 계산값 넷은 기계에만 있고 `editable: false`
- `machine_cost_chain` 다섯 걸음 — 손료(취득가×1,000×손료계수) · 연료(연료량×유가×(1+잡재료)) ·
  조종원(일 노임÷8×제수당) · 암 할증 · 시간당 기계경비. **밑값 11 / 계산값 4** 를 가름 ·
  기계가 유가·노임 표를 물어 갱신 차례가 갈림을 적음
- `merge_findings` 네 갈래 — 이름 다른 같은 열 2 · 한쪽에만 있는 열 4 · 단위·축 어긋남 4 · 부딪힘 2
- `row_keys` 다섯 판정(브레인 ①) — 자재 `item_code`·기계 `machine_code` 는 전수 겹침 0 으로 **있음** ·
  노임은 두 코드 공간이라 `출처+직종코드` 로 묶어야 함 · 유가·제비율은 **줄 열쇠 없음**(지어내지 않음)
- `null_is_real` 여섯(브레인 ②) — ⭐ 노임 일 노임이 **30 줄에 아예 없음**(미조사가 제값) ·
  기계 운전경비에 **명시 null 12** · 되돌리기 null 과 뜻이 겹치는 자리를 규칙과 함께 적음
- 시험 36건(8건 늘) — 계산값이 기계 밖으로 새거나 줄 열쇠·null 판정이 빠지면 빨강

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFCnEYNH4tsS2MbHvzBhZk
This commit is contained in:
2026-09-15 21:47:43 +09:00
co-authored by Claude Opus 5
parent 5ae246146c
commit d8f4ac0240
2 changed files with 912 additions and 1 deletions
@@ -447,3 +447,94 @@ def test_미결은_미판정으로_표시된다(labels):
column = labels["synthetic_tables"]["@values"]["columns"][3]
assert column["key"] == "@undecided"
assert column["name_ko"] == "미판정"
MERGED_KEYS = ("labor", "material", "oil", "rates", "machine")
def test_기초단가_다섯이_다_있다(labels):
merged = {m["key"]: m for m in labels["merged_tables"]}
assert tuple(merged) == MERGED_KEYS
for entry in merged.values():
assert entry["name_ko"].strip()
assert entry["summary"].strip()
assert entry["columns"], entry["key"]
assert entry["sources"], entry["key"]
def test_합친_표의_열마다_이름과_고칠수있나가_있다(labels):
bad = []
for entry in labels["merged_tables"]:
for column in entry["columns"]:
if not column["name_ko"].strip():
bad.append(f"{entry['key']}/{column['key']} — 이름 없음")
if not isinstance(column.get("editable"), bool):
bad.append(f"{entry['key']}/{column['key']} — editable 없음")
assert not bad, bad
def test_계산값은_기계에만_있고_못_고친다(labels):
"""⭐ 밑값만 고칠 수 있다 — 계산값을 손으로 고치면 밑값과 어긋나 되짚을 수 없다."""
derived = {
entry["key"]: [c["key"] for c in entry["columns"] if not c["editable"]]
for entry in labels["merged_tables"]
}
assert derived["machine"] == [
"hourly_loss_krw",
"hourly_fuel_krw",
"hourly_operator_krw",
"hourly_total_krw",
]
for key in ("labor", "material", "oil", "rates"):
assert derived[key] == [], f"{key} 에 계산값이 생겼다"
def test_기계_계산_사슬이_다섯_걸음으로_적혀_있다(labels):
chain = labels["machine_cost_chain"]
assert len(chain["steps"]) == 5
for step in chain["steps"]:
assert step["step"].strip() and step["formula"].strip()
assert "밑값" in chain["editable_rule"] and "계산값" in chain["editable_rule"]
# 기계는 유가·노임 표를 문다 — 갱신 차례가 여기서 갈린다.
assert "유가" in chain["cross_table"] and "노임" in chain["cross_table"]
def test_열_맞대기_어긋남이_네_갈래로_적혀_있다(labels):
findings = labels["merge_findings"]
assert set(findings) == {
"renamed_same_column",
"one_side_only",
"unit_or_axis_mismatch",
"collision",
}
assert all(findings[k] for k in findings)
def test_줄_열쇠가_다섯_다_판정돼_있다(labels):
"""덮개가 원본 줄을 붙드는 끈 — 없는 자료는 「없음」으로 내고 지어내지 않는다."""
keys = {r["table"]: r for r in labels["row_keys"]}
assert set(keys) == set(MERGED_KEYS)
for entry in keys.values():
assert entry["verdict"].strip() and entry["key"] and entry["checked"].strip()
assert keys["material"]["key"] == ["item_code"]
assert keys["machine"]["key"] == ["machine_code"]
# 한 칸으로는 안 되는 둘 — 묶어야 한다.
assert len(keys["labor"]["key"]) > 1
assert keys["oil"]["verdict"].startswith("없음")
assert keys["rates"]["verdict"].startswith("없음")
def test_null_이_제값인_열이_적혀_있다(labels):
"""되돌리기 null 과 원본의 「값 없음」이 겹치는 자리 — 겹치면 미조사가 값 지움으로 바뀐다."""
block = labels["null_is_real"]
assert block["columns"]
for row in block["columns"]:
assert row["table"] in MERGED_KEYS
assert row["column"].strip() and row["why"].strip()
assert isinstance(row["rows"], int)
# 가장 위험한 자리 — 노임 일 노임이 없는 줄(미조사)
wage = [
r for r in block["columns"] if r["table"] == "labor" and "daily_wage_krw" in r["column"]
]
assert wage and wage[0]["rows"] == 30
assert "되돌리기" in block["rule"]