Files
Aislo/M02_MasterTemplete/M02_Hopyo_Verdict.py
T
eomsangdonandClaude Opus 5.5 e6cdcb2952 feat(일위대가실무): 338-1 맞춤 핵심 서버 2 단계 — 줄 새 상태 · 체크 제안 · 대상 목록 · 품 비교 · 호표 통째 · 셈 · 만든 · guess 목록 + 골든 시험
- 새 Line — 상태 차례(% · 호표 · 노임 · 장부 뺌 · 별칭 · 맞춤셈 판정 · 이름 근거 후보) · 체크 제안(맞음 · 별칭 규칙 셋 · 확인된 하위 호표) · 대상(입력값 출처 별칭 > 블록 > 규격 · 환산 · 몫 · 배) · 품 비교 · 직접 줄 호표 통째 · guess(대상 목록 · 낱개도 받음 · 엔진 한 번)
- Match — view(to = 만들 층 · 셈) · picks · guess 를 Line 으로 · 옛 층 기억 · confirm · 들어감 없앰 · b_order 후보에 근거 칸
- Verdict — 위 다섯 딱 맞음 먼저(조합 없는 줄) · 후보 근거 = 근거 맞춤 꼴 · 지문차례(조합 전)
- Fit — 맞춤셈 캐시 이름에 셈 판(CALC_VERSION)
- 골든 시험(느림 · 1 분 54 초) — 사람 126 1순위 118 · 다섯 안 122 · 맞음 57 틀림 0 · 다른 점 틀림 1 · 옵션까지 57 · 조합 맞음 62/83 · 새 파일 51 맞음 틀림 2 · 씨앗 끔 · 북부청 조합 전 통과
- 근거 시험(나 창) 세 곳 — 옛 match_line → b_order(새 API)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-16 00:00:56 +09:00

210 lines
9.7 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위(조합 없는 줄) →
판정 맞음 · 다른 점 · 후보 + 후보 다섯(로직마다 옵션 판정 꼬리표).
글 · 꼬리표는 숫자 사실만(엑셀 · 우리 품) — 「옛 판」 같은 까닭은 짓지 않음.
"""
from __future__ import annotations
from typing import Any
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
def _num(x: float) -> str:
return f"{x:.4g}"
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 = 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 {})
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