Files
Aislo/M02_MasterTemplete/M02_San_Basis.py
T
eomsangdonandClaude Opus 5.5 532cc52c8e feat(793-3a): 근거 제안 엔진 — 번호 ∩ 값 ∩ 제목 ∩ 기계 상수 → M01 기초 + 옵션 · 품 검산 · 세 단
- M01 값 역색인 · 숫자 정규화 한 벌(N1~N10 · 단위 기준 · % ↔ 소수 · t ↔ kg · 인/양 몫) · M01 지문마다 한 번 · 서버 시작 때 뒤에서 지음
- 값 길(상수 지문 → 로직 · 우연 보정 유의 4) · 합친 길(분석 648-D2 차례 · 후보 2~6 엔진 품 대조) · 검산 · 옵션(B1~B6 → 값 길 V → 고르기 맞댐 → 입력 필요)
- 세 단 확정 · 근접(판 차이 · 현장 값 · 감률 · 할증 배수 · 단위 기준 차이) · 사람(+ 값 약한 1 등) · 없음(M01 보강 목록)
- 길 GET hopyo-basis/{키} · hopyo-basis/{키}/{번} · 파생 저장본 · 올림 때 HP 숨은식(STmate G 식) · 끝내기 때 지움 · 판정 길 그대로
- 보정 자료 = 여덟 파일 상수 1,094 · 시험 test_793_3a 11(459 블록 기초 260 · 옵션 전부 52 · 품 같음 141 · 없음 85)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:19:26 +09:00

529 lines
21 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.
"""근거 제안 엔진(PLAN 793-3a · 요구 648 · 649 · 652 갈래 C · 분석 648-F H1~H5 · 648-D2) — 서버
단독 · 읽기만.
산근 블록(HP `단산` 꼴) → 절마다 번호 후보(`M02_Hopyo_Clause.basis`) ∩ 값
후보(`M02_San_Basis_Value` ·
역색인 · 유의 4) ∩ 제목 후보(이름 두 글자 묶음 Dice ≥ 0.35 · 같은 말 묶음) · 기계 작업량 절(q · K
· f · E · Cm
둘+)은 일 갈래(번호 · 제목) 먼저 · 차례 = 분석 648-D2 1 장 · 후보 2~6 이면 엔진 품 대조로 하나만
맞으면 확정 ·
같은 기초 절은 묶어 검산(`M02_San_Basis_Check`) · 절 2+ 블록은 블록 통째 길도 셈해 나은 꼴.
꼴(648-D2) = 확정·전부 · 확정·일부 · 확정·일부(절 미확정) · 후보 2~3 · 없음.
단(근거후보 · 화면 · 793-2 SK) = 확정(기초 확정 · 엔진 품 = 엑셀 품 · 품 다름 0) · 근접(기초 확정
· 품 다름 ·
품 대조 못 함 · 까닭 표지) · 사람(후보 2~3) · 없음(M01 없음 의심 → `보강` = 794-2 근거).
자동 바꿈 없음 · 지어 넣는 값 없음(못 읽은 칸 · 글에 없는 감률 · 할증 = 입력필요).
"""
from __future__ import annotations
import re
from typing import Any
from M02_MasterTemplete import M02_Hopyo_Bind as hb
from M02_MasterTemplete import M02_Hopyo_Clause as clause
from M02_MasterTemplete import M02_Hopyo_Formula as formula
from M02_MasterTemplete import M02_Hopyo_Solve as solve
from M02_MasterTemplete import M02_San_Basis_Check as chk
from M02_MasterTemplete import M02_San_Basis_Const as bc
from M02_MasterTemplete import M02_San_Basis_Index as bi
from M02_MasterTemplete import M02_San_Basis_Value as bv
TITLE_MIN, TITLE_GAP, TITLE_TOP = 0.35, 0.15, 5
SYN = [
("B/H", "굴착기"),
("BACK-HOE", "굴착기"),
("BACKHOE", "굴착기"),
("백호", "굴착기"),
("굴삭", "굴착"),
("D/Z", "불도저"),
("도자", "불도저"),
("불도우저", "불도저"),
("D/P", "덤프"),
("브레카", "브레이커"),
("브레이카", "브레이커"),
("깍기", "깎기"),
("뒷채움", "뒤채움"),
("로울러", "롤러"),
("콤펙터", "콤팩터"),
("레미콘", "레디믹스트콘크리트"),
("컨테이너", "콘테이너"),
]
ORDER = ["확정·전부", "확정·일부", "확정·일부(절 미확정)", "후보 2~3", "없음"]
NONE = {"확정": [], "동점": [], "약함": []}
class Ctx:
"""역색인 + M01 Master + 로직 이름 묶음(역색인 지문마다 한 벌)."""
def __init__(self, idx: bi.Index):
self.idx, self.m = idx, hb.m01.cm.master()
self.logic = {
**self.m.index.get("GC", {}),
**self.m.index.get("GF", {}),
**self.m.index.get("GX", {}),
}
self.grams = {k: grams(r.get("이름")) for k, r in self.logic.items()}
_CTX: list = [None, None]
def ctx() -> Ctx:
idx = bi.get()
if _CTX[0] is not idx:
_CTX[:] = [idx, Ctx(idx)]
return _CTX[1]
# ── 제목 → 로직 이름 후보 ───────────────────────────────────────────
def grams(text: Any) -> set:
t = str(text or "")
for a, b in SYN:
t = re.sub(re.escape(a), b, t, flags=re.I)
return clause._grams(re.sub(r"\d", " ", t))
def title_cands(c: Ctx, text: str, unit: Any) -> list[tuple[str, float]]:
"""제목 ↔ 로직 이름 Dice ≥ 0.35 · 1등과 0.15 안 · 다섯까지 · 결과단위 성격 다르면 뺌."""
mine = grams(text)
if not mine:
return []
nat, scored = clause.nature(unit), []
for k, gs in c.grams.items():
theirs = clause.nature(c.logic[k].get("결과단위"))
if not gs or nat and theirs and nat != theirs:
continue
s = 2 * len(mine & gs) / (len(mine) + len(gs))
if s >= TITLE_MIN:
scored.append((k, round(s, 3)))
scored.sort(key=lambda x: (-x[1], x[0]))
return [x for x in scored if x[1] >= scored[0][1] - TITLE_GAP][:TITLE_TOP] if scored else []
def dice(c: Ctx, key: str, text: str) -> float:
a, b = grams(text), c.grams.get(key, set())
return 2 * len(a & b) / (len(a) + len(b)) if a and b else 0.0
# ── 블록 · 절 ───────────────────────────────────────────────────────
def _flat(t: Any) -> str:
return re.sub(r"[\s.()\[\]:·,]", "", str(t or ""))
def parts_of(blk: dict) -> tuple[list[dict], dict, list]:
"""일 절(`Formula.works`)마다 {제목, 번, 길, 항목, 상수, 식, 선택값, 몫} · 블록 통째 상수 · 쓸
항목.
"""
nodes = blk.get("절") or formula.tree(
blk.get("글") or [], blk.get("글행"), blk.get("항목") or []
)
top = formula.head(blk.get("글") or [], blk.get("글행"))
works = formula.works(nodes)
paths = {id(n): p for p, n in clause._paths(nodes)}
items = blk.get("항목") or []
usable = solve._usable(items)
whole = {k: top[k] + solve._gather(nodes, k) for k in ("상수", "식", "선택값")}
if len(works) < 2:
one = {"제목": "(블록 통째)", "번": None, "길": None, "항목": usable, **whole}
one["몫"] = works[0].get("몫") if works else None
return [one], whole, usable
out = []
for w in works:
at = [i for i in solve._gather([w], "항목") if not items[i].get("밖")]
part = {
"제목": f"{w['번']}. {w['제목']}",
"번": w["번"],
"길": paths.get(id(w)),
"항목": [items[i] for i in at],
}
part |= {k: top[k] + solve._gather([w], k) for k in ("상수", "식", "선택값")}
part["몫"] = w.get("몫")
out.append(part)
if not any(p["항목"] for p in out): # STmate — 글행 없음 → 항목 절 이름으로 붙임
for it in usable:
name = _flat(it.get("윗절") or it.get("절"))
hit = next(
(
p
for p in out
if name and (_flat(p["제목"]).endswith(name) or name in _flat(p["제목"]))
),
None,
)
if hit:
hit["항목"].append(it)
return out, whole, usable
def value_ev(rec: dict | None, parts: list[dict]) -> tuple[dict, list[dict], dict]:
"""값 길 — 블록 {확정, 동점, 약함} · 절마다 · 로직별 옵션."""
if rec is None:
return dict(NONE), [dict(NONE) for _ in parts], {}
opts: dict = {}
for b in rec.get("기초", []) + rec.get("동점", []):
opts.setdefault(b["로직"], {}).update(b.get("옵션") or {})
blk = {
"확정": [b["로직"] for b in rec.get("기초", [])] if rec["판정"] in ("U", "P") else [],
"동점": [b["로직"] for b in rec.get("동점", [])] if rec["판정"] == "A" else [],
"약함": [b["로직"] for b in rec.get("약한후보", [])[:1]],
}
secs, out = rec.get("절") or [], []
for i, p in enumerate(parts):
s = None
if len(parts) > 1:
head = lambda x: _flat(x["절"]).split(".")[-1][:6] # noqa: E731
s = (
secs[i]
if len(secs) == len(parts)
else next((x for x in secs if head(x) and head(x) in _flat(p["제목"])), None)
)
one = {"확정": [], "동점": [], "약함": []}
if s and s["판정"] in ("U", "P") and s.get("기초"):
one["확정"] = [s["기초"]["로직"]]
opts.setdefault(s["기초"]["로직"], {}).update(s["기초"].get("옵션") or {})
elif s and s["판정"] == "A":
one["동점"] = list(s.get("동점") or [])
out.append(one)
return blk, out, opts
def is_machine(part: dict) -> bool:
syms = {chk.base_sym(c.get("기호") or "") for c in part["상수"]} | {
chk.base_sym(f.get("기호") or "") for f in part["식"]
}
return len(syms - {""}) >= 2
def pick(
c: Ctx, N: list, T: list, Vb: dict, Vs: dict, machine: bool, single: bool, title: str
) -> tuple[list, str]:
"""일반: 번호∩값 › 값 › 번호∩제목 › 값동점∩제목 › 번호 › 제목 /
기계 절: 번호∩제목 › 번호∩값 › 제목∩값 › 번호 › 제목 › 값."""
Tk = [k for k, _ in T]
sure = [
k
for k in (Vs["확정"] or (Vb["확정"] if single else []))
if not title or dice(c, k, title) >= 0.2
]
anyv = list(dict.fromkeys(Vs["확정"] + Vs["동점"] + Vb["확정"] + Vb["동점"] + Vb["약함"]))
def inter(a, b):
return [x for x in a if x in set(b)]
if machine:
steps = [
("번호∩제목", inter(N, Tk)),
("번호∩값", inter(N, anyv)),
("제목∩값", inter(Tk, anyv)),
("번호", N),
("제목", Tk),
("값", sure),
]
else:
steps = [
("번호∩값", inter(N, anyv)),
("값", sure),
("번호∩제목", inter(N, Tk)),
("값동점∩제목", inter(Vs["동점"] + Vb["동점"], Tk)),
("번호", N),
("제목", Tk),
]
for name, s in steps:
if s:
return list(dict.fromkeys(s)), name
return [], "없음"
def _good(v: dict) -> bool:
return "오류" not in v and not v["셈"][solve.DIFF] and bool(v["셈"][solve.SAME])
def resolve(S: list, how: str, check) -> tuple[list, str]:
"""후보 2~6 → 검산(품 같음 있고 품 다름 없는 후보 하나)으로 거름."""
if 2 <= len(S) <= 6:
good = [k for k in S if _good(check(k))]
if len(good) == 1:
return good, how + "+검산"
return S, how
def kind_of(S: list) -> str:
return "확정" if len(S) == 1 else "후보 2~3" if 2 <= len(S) <= 3 else "없음"
def _found(c: Ctx, key: str, v: dict, mc: list, rec: dict | None, how: str) -> dict:
kind, missing, differ = chk.judge_one(v, mc)
return {
"꼴": kind,
"기초": key,
"기초이름": c.logic.get(key, {}).get("이름", ""),
"차례": how,
"옵션": v.get("입력값"),
"옵션길": v.get("길"),
"입력필요": v.get("입력필요"),
"0간주": v.get("0간주"),
"글에없음": missing,
"값다름": differ,
"기계상수": mc,
"셈": v.get("셈"),
"다름까닭": chk.causes(v, mc, rec) if differ else [],
}
def run_block(c: Ctx, blk: dict, line: dict | None, same: list, rec: dict | None) -> dict:
"""블록 하나 합친 길 — {꼴, 블록길, 절 [...], 통째, 기초 [키]} (648-D2 꼴)."""
basis = clause.basis(blk)
parts, whole, usable = parts_of(blk)
title_b = f"{blk.get('이름') or ''} {blk.get('규격') or ''}".strip()
src = {"줄": line, "같은줄": same, "블록": blk}
src["제목줄"] = {
"글": re.sub(r"[-_/()\[\],~]+", " ", f"{blk.get('이름') or ''} {blk.get('규격') or ''}"),
"행": None,
}
Vb, Vsecs, vopts = value_ev(rec, parts)
allN = list(dict.fromkeys(x["키"] for x in basis["후보"]))
memo: dict = {}
def check(key, items, cons, share):
tag = (key, id(items), share, repr(cons)) # 값에 items 를 쥐어 id 를 다시 안 씀
if tag not in memo:
memo[tag] = (items, chk.verify(c.m, key, items, cons, src, share, vopts.get(key, {})))
return memo[tag][1]
def share_of(p):
s = p.get("몫")
return (s.get("값") if isinstance(s, dict) else s) or 1
secs = []
for p, Vs in zip(parts, Vsecs, strict=True):
if p["길"]:
N = [
x["키"]
for x in basis["후보"]
if x.get("절") in (None, p["길"]) or str(p["길"]).startswith(f"{x.get('절')}.")
]
text = f"{p['제목']} {title_b}"
else:
N, text = allN, title_b
N, mach = list(dict.fromkeys(N)), is_machine(p)
T = title_cands(c, text, blk.get("단위"))
S, how = pick(c, N, T, Vb, Vs, mach, len(parts) == 1, text)
cons = {x: p[x] for x in ("상수", "식", "선택값")}
S, how = resolve(S, how, lambda k, p=p, cons=cons: check(k, p["항목"], cons, share_of(p)))
secs.append(
{
"절": p["제목"],
"길": p["길"],
"항목수": len(p["항목"]),
"기계식": mach,
"번호": N,
"제목후보": [k for k, _ in T],
"차례": how,
"후보": S,
"_p": p,
}
)
chosen = {i: s["후보"][0] for i, s in enumerate(secs) if len(s["후보"]) == 1}
bases = list(dict.fromkeys(chosen.values()))
group = {}
for b in bases: # 같은 기초 절끼리 묶어 검산(기초 하나뿐이면 블록 통째)
if len(bases) == 1:
items, cons, ps = usable, whole, list(parts)
else:
ps = [secs[i]["_p"] for i, k in chosen.items() if k == b]
items = [x for p in ps for x in p["항목"]]
cons = {x: [y for p in ps for y in p[x]] for x in ("상수", "식", "선택값")}
v = check(b, items, cons, share_of(ps[0]) if len(ps) == 1 else 1)
mc = (
chk.mach_check(c.idx, b, chk.mach_consts(cons))
if any(s["기계식"] for s in secs)
else []
)
group[b] = (v, mc)
for i, s in enumerate(secs):
s.pop("_p")
if (
i in chosen
and s["차례"] == "제목"
and not _good({"셈": group[chosen[i]][0].get("셈") or {solve.DIFF: 0, solve.SAME: 0}})
):
s |= {"꼴": "후보 2~3", "제목단독": True} # 제목 하나뿐 · 품 대조로 못 받침 = 사람 확인
elif i in chosen:
s |= _found(c, chosen[i], *group[chosen[i]], rec, s["차례"])
else:
s["꼴"] = kind_of(s["후보"])
kinds = [s["꼴"] for s in ([s for s in secs if s["항목수"]] or secs)]
if all(k == "확정·전부" for k in kinds):
bk = "확정·전부"
elif all(k.startswith("확정") for k in kinds):
bk = "확정·일부"
elif any(k.startswith("확정") for k in kinds):
bk = "확정·일부(절 미확정)"
else:
bk = "후보 2~3" if "후보 2~3" in kinds else "없음"
out = {"절": secs, "표지": basis["표지"], "출처": basis["출처"], "번호후보": allN}
wb = None
if len(parts) > 1: # 블록 통째 길 — 블록 제목 · 번호 · 블록 값으로 기초 하나 → 블록 전부로 검산
mach = any(s["기계식"] for s in secs)
S, how = pick(
c, allN, title_cands(c, title_b, blk.get("단위")), Vb, dict(NONE), mach, True, title_b
)
S, how = resolve(S, how, lambda k: check(k, usable, whole, 1))
if len(S) == 1:
v = check(S[0], usable, whole, 1)
mc = chk.mach_check(c.idx, S[0], chk.mach_consts(whole)) if mach else []
wb = _found(c, S[0], v, mc, rec, how)
if how == "제목" and not _good({"셈": v.get("셈") or {solve.DIFF: 0, solve.SAME: 0}}):
wb["꼴"] = "후보 2~3"
else:
wb = {"꼴": kind_of(S), "후보": S, "차례": how}
if wb and ORDER.index(wb["꼴"]) < ORDER.index(bk) and (wb.get("셈") or {}).get(solve.SAME):
out |= {"꼴": wb["꼴"], "블록길": "통째", "통째": wb, "기초": [wb["기초"]]}
else:
out |= {"꼴": bk, "블록길": "절 모음" if len(parts) > 1 else "통째(절 하나)", "통째": wb}
out["기초"] = list(dict.fromkeys(s["기초"] for s in secs if s.get("기초")))
return out
# ── 세 단 · 근거후보 ────────────────────────────────────────────────
def chosen_units(r: dict) -> list[dict]:
"""블록 판정에 쓰인 단위 — 통째 길이면 통째 하나 · 아니면 기초마다 첫 절."""
if r["블록길"] == "통째" and r.get("통째"):
return [r["통째"]]
out, seen = [], set()
for s in r["절"]:
if s.get("기초") and s["기초"] not in seen:
seen.add(s["기초"])
out.append(s)
return out
def checked(s: dict) -> str:
"""품 검산 — 다름 · 같음 · 짝 없음."""
cnt = s.get("셈") or {}
return "다름" if cnt.get(solve.DIFF) else "같음" if cnt.get(solve.SAME) else "짝 없음"
def _tier(s: dict) -> str:
return "확정" if checked(s) == "같음" and not s["값다름"] else "근접"
def cands(c: Ctx, r: dict) -> list[dict]:
"""근거후보 [{기초, 이름, 원문번호, 옵션, 옵션길, 입력필요, 품검산, 단, 까닭, 글에없음,
값다름}] ·
사람 = 후보 2~3(각 후보 · 옵션 없음) · 없음 = []."""
if r["꼴"].startswith("확정"):
out = []
for s in chosen_units(r):
opts = {k: v for k, v in (s.get("옵션") or {}).items()}
row = c.logic.get(s["기초"], {})
out.append(
{
"기초": s["기초"],
"이름": row.get("이름", ""),
"원문번호": row.get("원문번호"),
"차례": s["차례"],
"옵션": opts,
"옵션길": s.get("옵션길") or {},
"입력필요": list(s["입력필요"] or []) + list(s["0간주"] or []),
"품검산": checked(s),
"단": _tier(s),
"까닭": [[chk.CAUSE[t], x] for t, x in s["다름까닭"]],
"글에없음": s["글에없음"],
"값다름": s["값다름"],
}
)
return out
if r["꼴"] == "후보 2~3":
keys = list(
dict.fromkeys(
k for s in r["절"] if s["꼴"] == "후보 2~3" for k in (s.get("후보") or [])
)
)
return [
{
"기초": k,
"이름": c.logic.get(k, {}).get("이름", ""),
"원문번호": c.logic.get(k, {}).get("원문번호"),
"옵션": {},
"입력필요": [],
"품검산": None,
"단": "사람",
"까닭": [],
}
for k in keys[:6]
]
return []
def lack(blk: dict, r: dict) -> dict:
"""없음 까닭 — a 기초 없음(번호 확인) · a′ 품목 없음 의심 · f 후보 4+ · 증거 흩어짐 · 794-2
보강 근거.
"""
if r["표지"] == "기초 없음":
why = "a 기초 없음(번호 확인)"
elif any(s.get("후보") for s in r["절"]):
why = "f 후보 4+ · 증거 흩어짐"
else:
why = "a′ 품목 없음 의심"
name = re.sub(r"\(.*?\)|\[.*?\]|[^가-힣]", "", f"{blk.get('이름') or ''}")[:8]
return {"까닭": why, "이름": name, "품셈": [f"{b} {n}" for b, n in r["출처"]]}
def suggest(
blk: dict,
kind: str,
line: dict | None = None,
same: list | None = None,
g: list[str] | None = None,
) -> dict:
"""산근 블록 하나 → 근거 제안 {단, 꼴, 근거후보, 절, 보강?, 값판정}.
`kind` = 책 꼴(STmate · 엑셀꼴) · `line` · `same` = 부르는 첫 줄 · 그 호표 다른 줄(B1 · B6) ·
`g` = STmate 숨은 G 식(없으면 보이는 글)."""
c = ctx()
pr = bc.prep(blk, kind, g)
rec = bv.value(c.idx, pr)
r = run_block(c, blk, line, same or [], rec)
out = {**r, "값판정": rec["판정"], "근거후보": cands(c, r)}
if r["꼴"].startswith("확정"):
tiers = {x["단"] for x in out["근거후보"]}
outside = [
m[6:] for x in out["근거후보"] for m in x["글에없음"] if m.startswith("기초 밖 줄 ")
]
out["까닭"] = [["기초 밖 줄", ", ".join(outside)]] if outside else [] # 엑셀에만 있는 줄
sure = tiers == {"확정"} and r["꼴"] != "확정·일부(절 미확정)"
out["단"] = "확정" if sure else "근접"
else:
out["단"] = {"후보 2~3": "사람"}.get(r["꼴"], "없음")
weak = (rec.get("약한후보") or [None])[0]
title = f"{blk.get('이름') or ''} {blk.get('규격') or ''}"
if (
weak
and weak["로직"] not in {x["기초"] for x in out["근거후보"]}
and dice(c, weak["로직"], title) >= 0.2
):
row = c.logic.get(
weak["로직"], {}
) # 값 길 약한 1 등(유의 4 미만) — 제목과 뜻 겹치면 사람 후보 맨 앞
opts = {k: v for k, v in weak["옵션"].items()}
out["근거후보"].insert(
0,
{
"기초": weak["로직"],
"이름": row.get("이름", ""),
"원문번호": row.get("원문번호"),
"차례": "값약함",
"옵션": opts,
"옵션길": dict.fromkeys(opts, "V"),
"입력필요": [],
"품검산": None,
"단": "사람",
"까닭": [],
},
)
out["단"] = "사람"
if out["단"] == "없음":
out["보강"] = lack(blk, r)
return out