Files
Aislo/M02_MasterTemplete/M02_Hopyo_Verdict.py
T
eomsangdonandClaude Opus 5.5 57af9be22f fix(784-1): 「우리 품」 잘못 보임 뿌리 고침 — 못 맞춤 빈칸 · 가드 칸 자동값 0 · 수 칸 짝 · 자재 단위 배 1 · 절 하나 덧 없음 · 엑셀 값 표지
- A 옵션 찾기 못 맞춤(수준 9 · 거리 ≥ 1) = 우리 품 · 입력 안 냄 · 「못맞춤」 표지 · 꼬리표 「→ –(우리 셈 조건 못 찾음)」
- B 자동값 「만약(X > 0, X, 기본)」 칸 = 나눗수 퍼뜨림에서 뺌(0 = 원문 기본) · 옵션 후보엔 옛 1 도 둠
- C 하나씩 조합 — 후보 ≤ 4 수 칸도 둘씩 짝
- D 엑셀이 값으로 넣은 「0 = 식」 칸 출처 「엑셀 값 · 식 아님」 · 꼬리표 「원문 식이면 품 · 금액(조건)」
- E 결과단위 「자재」 = 배 1 · 공/㎥ ≡ 공㎥
- F 품 비교 우리 품 × 대상 몫
- G 절 하나 블록 = 덧 로직은 후보만(이중 계상 막음)
- H 1위 옵션 수준 ≤ 2 면 그 입력을 대상 입력값으로
- I 규격 「L=…cm」 를 거리 칸에 안 넣음 · 수 고르기는 제 단위 붙은 수만 · 엑셀 수 「1 ㎡」 를 m 로 안 읽음
- 우리지문 · 맞춤 캐시 판 올림(FP 2 · CALC 3)
- 새 지킴 시험 test_635_hopyo_ours_qty
- 낡은 시험 둘(test_628 표지 수 18 · test_621 S1 울진소광 해시)은 HEAD 에서도 실패 — 782-3 마스터 합침 뒤 값 · 손대지 않음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 20:41:09 +09:00

238 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""M02 일위대가 맞춤 — 블록 하나 셈 · 판정(PLAN 338-1 · 설계 2-1 · 2-6 · 3-2).
블록 + 줄 → 엑셀 지문(`Finger`) → 이름 · 근거 차례(`Match.b_order`) → 지문 순위 섞기(`Fit`) ·
같음 1위 고정(딱 맞음이 이김) → 섞인 줄 몫 먼저 · 조합(`Combine.solve` · 근거 안 닿은 덧 = 후보 끝)
→ 위 다섯 옵션 거꾸로(`Option`) →
위 다섯 가운데 딱 맞음(옵션까지)이 있는데 1위가 아니면 그것이 1위(조합 없는 줄) →
판정 맞음 · 다른 점 · 후보 + 후보 다섯(로직마다 옵션 판정 꼬리표) ·
출처 조항이 우리 기초 밖(781-2 · `Basis.no_basis`) = `기초없음` 표지 · 판정 꼬리표 ·
맞음 아닌 후보 글 「참고 후보」.
글 · 꼬리표는 숫자 사실만(엑셀 · 우리 품) — 「옛 판」 같은 까닭은 짓지 않음.
"""
from __future__ import annotations
from typing import Any
from common_util import common_util_money as money_fmt
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
from M02_MasterTemplete import M02_Hopyo_Combine as combine
from M02_MasterTemplete import M02_Hopyo_Finger as finger
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
from M02_MasterTemplete import M02_Hopyo_Option as option
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
_num = money_fmt.fmt # 꼬리표 수 — 공용 수량 꼴(1만 넘어도 지수 꼴 아님)
OUTSIDE = "참고 후보 — 근거 조항 아님"
def excel_qty(efp: dict[str, Any]) -> dict[str, Any]:
"""엑셀 품(화면 꼴) — {인력: {직종: 품}, 장비: [{갈래, 시간}], 자재: [{이름, 수량}]}."""
return {
"인력": {k: round(x["품"], 6) for k, x in efp["인력"].items() if x.get("품")},
"장비": [{"갈래": m["갈래"], "시간": m["시간"]} for m in efp["장비"] if m.get("시간")],
"자재": [{"이름": m["이름"], "수량": m["품"]} for m in efp["자재"] if m.get("품")],
}
def _public(efp: dict[str, Any]) -> dict[str, Any]:
keep = ("인력", "장비", "자재", "출처", "조건", "옛판", "율", "계수")
return {k: efp.get(k) for k in keep}
def _label(code: str, values: dict[str, Any]) -> str:
"""옵션 요약 — 고르기 글은 그대로 · 수 칸은 「칸 값」 · 기계 코드는 고르기이름."""
specs = {s.get("이름"): s for s in hopyo_match._logic_specs(code)}
out = []
for name, v in values.items():
names = (specs.get(name) or {}).get("고르기이름") or {}
out.append(str(names.get(v) or v) if isinstance(v, str) else f"{name} {v}")
return " · ".join(out)
def _part_fp(part: dict[str, Any], x: float, line: dict[str, Any]) -> dict[str, Any]:
"""조합 몫 → 옵션 찾기용 지문(몫 x 를 풀어 100% 품)."""
fp = finger.empty(line)
k = x or 1.0
for t, q in (part.get("노임") or {}).items():
fp["인력"][t] = {"품": q / k, "밑품": q / k, "율": [], "어떻게": ["몫"]}
return fp
def _diffs(efp: dict, found: dict) -> list[str]:
"""숫자 사실 꼬리표 — 직종 품 다름 · 섞음 · 율 · 엑셀만 · 우리만 장비."""
tags = []
ours_l = (found.get("우리품") or {}).get("인력") or {}
for t, x in efp["인력"].items():
a, b = x.get("품"), ours_l.get(t)
if a and b and not option.near(a, b, fit.TOL[2]):
tags.append(f"{t} 엑셀 {_num(a)} · 우리 {_num(b)}")
elif a and not b and found.get("못맞춤"):
tags.append(f"{t} 엑셀 {_num(a)} → –(우리 셈 조건 못 찾음)")
elif a and not b:
tags.append(f"{t} 엑셀만 {_num(a)}")
tags += [f"{t} 우리만 {_num(q)}" for t, q in ours_l.items() if t not in efp["인력"]]
for x in efp["인력"].values():
for v, note in x.get("율") or []:
tag = f"섞음 — 엑셀이 × {_num(v)}" if note == "섞음" else f"율 {note} × {_num(v)}"
if v and tag not in tags:
tags.append(tag)
tags += [d for d in ((found.get("견줌") or {}).get("덧") or []) if d not in tags]
return tags
def formula_tag(code: str, found: dict | None) -> str | None:
"""「운반회수 42 = 엑셀 값(식 아님) · 원문 식이면 보통인부 0.17 · 30,077원(운반거리 6 · …)」."""
f = (found or {}).get("식아님")
if not f:
return None
cells = " · ".join(f"{k} {_num(v)}" for k, v in f["칸"].items())
qty = " · ".join(f"{t} {_num(q)}" for t, q in f["우리품"]["인력"].items()) or "–"
won = (labor_doc.run_line(option.ours.whole(), code, f["입력"]).get("기준") or {}).get("계")
won_text = f" · {money_fmt.fmt(won)}원" if won is not None else ""
mine = {**f["칸"], **((found or {}).get("입력") or {})} # 맞춘 칸 뺀 원문 식 조건
used = " · ".join(f"{k} {_num(v)}" for k, v in f["입력"].items() if k not in mine)
return f"{cells} = 엑셀 값(식 아님) · 원문 식이면 {qty}{won_text}({used})"
def _state(efp: dict, code: str, found: dict | None, theirs: list) -> str:
lvl = 9 if (found is None or fit.single(efp)) else found["수준"]
if lvl <= 1:
return "맞음"
our = hopyo_basis.ours().get(code)
clause_ok = bool(our and theirs and hopyo_match.clause(theirs, our)[0] >= 0.9)
if lvl == 2 or (found and found.get("직종같음") and clause_ok):
return "다른 점"
return "후보"
def _no_numbers(entry: dict, top: list[str], why: str) -> dict[str, Any]:
entry["순위"] = [{"로직": k, "점수": None, "B": i} for i, k in enumerate(top[: fit.TOP_OPT])]
entry["차례"] = top[: fit.TOP_OPT]
entry["판정"] = {"상태": "후보", "글": why, "꼬리표": [why]}
return entry
def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dict[str, Any]:
"""블록(또는 `{_지문}` 직접 줄 호표) + 줄 → 맞춤셈 한 칸(설계 3-2)."""
if block_ and "_지문" in block_:
efp, text = block_["_지문"], []
else:
efp, text = finger.excel(block_, line), (block_ or {}).get("글") or []
border = hopyo_match.b_order(line, got, text)
entry: dict[str, Any] = {"지문": _public(efp), "품": excel_qty(efp), "B": border["상태"]}
entry.update(옵션={}, 조합=None)
if not fit.has_numbers(efp):
return _no_numbers(entry, border["top"], fit.NO_NUMBER)
if fit.prepared() is None:
return _no_numbers(entry, border["top"], "우리 로직 준비 중")
e = fit.ex_prep(efp)
pool = fit.rank(e)
btop = border["top"]
scores = {k: s for s, k, _, _ in pool}
fused = fit.fuse(e, pool, btop)
if border["상태"] == "같음": # 같음 1위 고정 — 지문 딱 맞음이 다른 로직이면 지문 1위
first = pool[0][1] if fit.sure(e, pool) and pool[0][1] != btop[0] else btop[0]
top = [first] + [k for k in fused if k != first][:4]
else:
top = fused
entry["지문차례"] = top[: fit.TOP_OPT] # 조합 · 딱 맞음 먼저 전(시험 지표)
parts = efp.get("몫") or []
if parts and btop and combine.is_mixed(parts, line): # 섞인 줄 — 이름 · 근거 차례로 몫 먼저
top = btop[: fit.TOP_OPT]
top, combo, spare = combine.solve(top, parts, line, got, border["합침"])
opts = {k: option.find(k, efp, text) for k in top[: fit.TOP_OPT]}
for c in combo:
if c["로직"] not in opts and c["체크"]:
part = next((p for p in parts if p.get("제목") == c["어디"]), None)
pfp = _part_fp(part, c["몫"], line) if part else efp
opts[c["로직"]] = option.find(c["로직"], pfp, text) if pfp["인력"] else None
theirs = efp["출처"]
exact = [k for k in top[: fit.TOP_OPT] if _state(efp, k, opts.get(k), theirs) == "맞음"]
if (
not combo and exact and exact[0] != top[0]
): # 딱 맞음 먼저(K5) — 1위가 못 맞추면 위 다섯 딱 맞음
top = [exact[0]] + [k for k in top if k != exact[0]]
chans = {k: ch for _, k, ch, _ in pool}
entry["순위"] = [
{
"로직": k,
"점수": round(scores[k], 3) if k in scores else None,
"채널": {a: round(b, 3) for a, b in (chans.get(k) or {}).items() if b is not None},
"B": btop.index(k) if k in btop else None,
}
for k in top[: fit.TOP_OPT]
]
entry["차례"] = top[: fit.TOP_OPT]
entry["옵션"] = opts
entry["조합"] = combo or None
entry["판정"] = judge(efp, top, opts, theirs, got)
entry["후보"] = candidates(efp, top, opts, theirs, got, scores, border.get("근거") or {})
if outside := hopyo_basis.no_basis(theirs): # 781-2 — 근거 조항이 우리 기초 밖
entry["기초없음"] = outside
entry["판정"]["꼬리표"].append(outside["글"])
for c in entry["후보"]:
c["글"] = c["글"] if c["판정"] == "맞음" else OUTSIDE
names = {x["로직"]: x for x in got["로직"]}
for k in (k for k in spare if k not in top[: fit.TOP_OPT] and k in names): # 근거 안 닿은 덧
one = {key: names[k][key] for key in ("로직", "이름", "단위")}
entry["후보"].append({**one, "점수": None, "판정": "후보", "글": "덧 후보 — 근거 안 닿음"})
return entry
def judge(efp: dict, top: list[str], opts: dict, theirs: list, got: dict) -> dict[str, Any]:
"""1위 판정 {상태, 글, 꼬리표}."""
if not top:
return {"상태": "후보", "글": "후보 없음", "꼬리표": []}
names = {x["로직"]: x["이름"] for x in got["로직"]}
t1, found = top[0], opts.get(top[0]) or {}
state = _state(efp, t1, opts.get(t1), theirs)
exact = [k for k in top[: fit.TOP_OPT] if _state(efp, k, opts.get(k), theirs) == "맞음"]
tags = [t for t in [formula_tag(t1, found)] if t and state in ("맞음", "다른 점")]
if state == "맞음":
label = _label(t1, found.get("입력") or {})
text = " · ".join(x for x in ("맞음 — " + t1, names.get(t1, ""), label) if x)
if len(exact) > 1:
tags.append("맞음 둘")
if found.get("갈리는칸"):
tags.append(f"{' · '.join(found['갈리는칸'])} 못 가름")
return {"상태": state, "글": text, "꼬리표": tags}
if state == "다른 점":
tags = _diffs(efp, found) + tags
return {"상태": state, "글": " · ".join(["다른 점 — " + t1, *tags[:2]]), "꼬리표": tags}
return {"상태": "후보", "글": f"후보 {len(top[: fit.TOP_OPT])}", "꼬리표": []}
def candidates(
efp: dict, top: list[str], opts: dict, theirs: list, got: dict, scores: dict, why: dict
) -> list[dict[str, Any]]:
"""후보 다섯 — 로직마다 옵션 판정 꼬리표 · 「맞음 둘」이면 둘째 맞음이 맨 앞."""
by = {x["로직"]: x for x in got["로직"]}
table = hopyo_basis.ours()
out = []
for k in top[: fit.TOP_OPT]:
found = opts.get(k) or {}
state = _state(efp, k, opts.get(k), theirs)
our = table.get(k)
sc, clauses, tag = hopyo_match.clause(theirs, our) if (our and theirs) else (0, [], "")
one = by.get(k) or {"로직": k, "이름": "", "단위": ""}
unit = hopyo_match.fits(efp["단위"], one["단위"])
basis = why.get(k) or {"조항": clauses, "직종": [], "단위": unit, "점수": sc, "까닭": tag}
text = state if state != "다른 점" else " · ".join(["다른 점", *_diffs(efp, found)[:1]])
out.append(
{
"로직": k,
"이름": one["이름"],
"단위": one["단위"],
"점수": round(scores[k], 3) if k in scores else None,
"판정": state,
"글": text,
"근거": basis,
}
)
first = [c for c in out[1:] if c["판정"] == "맞음"][:1]
if out and out[0]["판정"] == "맞음" and first:
out = [out[0], *first, *[c for c in out[1:] if c is not first[0]]]
return out