Files
Aislo/M02_MasterTemplete/M02_Hopyo_Verdict.py
T
eomsangdonandClaude Opus 5.5 a8acaed6f5 feat(781-1 · 781-2): 출처 책 · 부문 · 옛 책 가림 · 「근거 조항 우리 기초에 없음」 표지(요구 628 · 629 · 630)
- 책 낱말 표(ReadEst.book_of) — 임도 · 사방 = 옛 책 「(옛)」 · 공통 · 토목 · 건축 · 기계설비 · 유지관리 = 「건설품셈 <부문>」 · 그 밖 건설품셈(부문 모름) 그대로
- 출처 풀이(Basis.settle · Excel.read_book 끝 한 곳 · 두 형식) — 건설품셈(부문 모름) 번호 → 블록 이름(안 맞으면 글 두 묶음)이 원문 제목 · 같은 번호 로직 이름과 뜻 낱말을 나누는 부문 하나 = 그 부문 · 둘 이상 = 모름 · 없음 = (옛) · 임도 옛 번호 → 지식DB 대응표 확정 한 대응(가 · 나 항 = 현행 윗마디 · 일대다 안 옮김)
- 조항 견줌(Basis.clause) 한 줄 — 책 또는 구분 같음 · (옛) = 없음
- 기초 없음 표지(Basis.no_basis) — 출처 조항(윗 · 아랫마디 포함) 로직 0 = 「근거 조항 우리 기초에 없음 — 건설품셈 공통 1-2-7 운반」 · 옛 번호 = 「옛 번호 — 대응 미확인」 · 맞춤셈 꼬리표 · 맞음 아닌 후보 글 「참고 후보 — 근거 조항 아님」 · 단가산출 근거 꼬리표 1위 대신 표지(1위 맞음이면 1위)
- 시험: 새 test_628 · test_621 단계 해시 다시 굳힘(달라진 칸 = 출처뿐 · S1 수 · 그래프 그대로) · test_m02_hopyo_basis · finger · stmate · excel 출처 기대값 새 책 꼴로(낡은 기대 「임도 = 산림품셈」 · 「건설품셈」 부문 없음)

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

222 lines
11 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
_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:
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 _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 = []
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)
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