- 새 M02_San_Basis_Map — 옛 번호 대조표(건설 옛 11-3 → 8-2-3 · 3-7 → 3-9-1 · 8-2-35 → 8-2-32 · 근거 = 원문 표 값 · 실무 글) · 사방(옛) = 판 대응표 2018 통합 판 · 기계 상수 줄 글 읽기(여러 대입 한 줄 · 괄호 설명 · 끝값 없는 나눗셈) · 작업량당 기계경비 로직 낱말 표 - 엔진 — 같은 말(휠라 = 채움재 · 평떼 = 평잔디 · 육샹 = 육상) · 건설(옛) 같은 번호는 같은 절 + 제목 뜻 겹침만 · Q 식 절 기호 채움(고르는 차례는 채우기 전) · 값 · 제목 길이 품 검산으로 안 받쳐지면 그 기계 작업량당 기계경비(차례 「기계」 · 번호 길은 그대로) - 검산 probe — 고르기가 범위뿐이면 용량 · 규격 값이 드는 범위 하나로(버킷용량 → 규격구분) - 459 블록: 없음 85 → 58 · 확정 125 → 164 · 근접 135 → 145 · 사람 114 → 92 · 확정 · 근접 → 없음 0 · 섞기(GF000132) 오인 넷 → GC001148 · GC001189 - 시험 test_793_3a 넓힘 — 없음 ≤ 58 · 확정 ≥ 164 · 손 확인 견본 열 · 1/3 적용 지어 넣지 않음 · 상수 읽기 · 대조표 - 근거후보 꼴 그대로 · M01 · 지식DB 고침 없음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
603 lines
25 KiB
Python
603 lines
25 KiB
Python
"""근거 제안 엔진(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 근거).
|
||
자동 바꿈 없음 · 지어 넣는 값 없음(못 읽은 칸 · 글에 없는 감률 · 할증 = 입력필요).
|
||
794-2c — 같은 말 묶음(SYN) · 옛 번호 → 현행 절(`old_cands` · `M02_San_Basis_Map.OLD`) 번호 후보 ·
|
||
Q 식 절은 줄 글에서 기호 끝값을 더 읽고(`_fill`) 값 · 제목 길이 품 검산으로 안 받쳐지면 그 기계
|
||
「작업량당 기계경비」 로직(`by_machine` · 차례 「기계」).
|
||
"""
|
||
|
||
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_Map as bm
|
||
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()}
|
||
self.mach = bm.machines(self.logic)
|
||
|
||
|
||
_CTX: list = [None, None]
|
||
|
||
|
||
def ctx() -> Ctx:
|
||
idx = bi.get()
|
||
if _CTX[0] is not idx:
|
||
_CTX[:] = [idx, Ctx(idx)]
|
||
return _CTX[1]
|
||
|
||
|
||
# ── 제목 → 로직 이름 후보 ───────────────────────────────────────────
|
||
def syn(text: Any) -> str:
|
||
t = str(text or "")
|
||
for a, b in SYN:
|
||
t = re.sub(re.escape(a), b, t, flags=re.I)
|
||
return t
|
||
|
||
|
||
def grams(text: Any) -> set:
|
||
return clause._grams(re.sub(r"\d", " ", syn(text)))
|
||
|
||
|
||
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)
|
||
lines = [str(t) for t in blk.get("글") or []]
|
||
whole = {k: top[k] + solve._gather(nodes, k) for k in ("상수", "식", "선택값")}
|
||
_fill(whole, lines)
|
||
if len(works) < 2:
|
||
one = {"제목": "(블록 통째)", "번": None, "길": None, "항목": usable, "글줄": lines}
|
||
one |= 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],
|
||
"글줄": lines[w["글"][0] : _end(w) + 1] if w.get("글") else [],
|
||
}
|
||
part |= {k: top[k] + solve._gather([w], k) for k in ("상수", "식", "선택값")}
|
||
_fill(part, part["글줄"])
|
||
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 _end(node: dict) -> int:
|
||
return max([node["글"][1], *(_end(n) for n in node.get("아래") or [] if n.get("글"))])
|
||
|
||
|
||
def _fill(part: dict, lines: list[str]) -> None:
|
||
"""기계 작업량 절(Q 식 줄) — 못 읽은 기호(q · K · f · E · Cm)를 줄 글 끝값으로 더함 · 고르는
|
||
차례(`is_machine`)는 채우기 전 상수로."""
|
||
if bm.has_q(lines):
|
||
part["기계식"] = is_machine(part)
|
||
have = {chk.base_sym(x.get("기호") or "") for x in part["상수"]}
|
||
part["상수"] = part["상수"] + [
|
||
x for x in bm.consts(lines) if chk.base_sym(x["기호"]) not in have
|
||
]
|
||
|
||
|
||
def old_cands(c: Ctx, basis: dict, title: str, unit: Any) -> list[dict]:
|
||
"""옛 번호 → 현행 절 로직(`M02_San_Basis_Map`) · 건설품셈(옛) 같은 번호는 같은 절 · 제목과 뜻
|
||
겹침만 · 결과단위 성격 거름."""
|
||
mine, have, out = clause.nature(unit), {x["키"] for x in basis["후보"]}, []
|
||
for book, no in basis["출처"]:
|
||
if not book.endswith(clause.OLD):
|
||
continue
|
||
rows = bm.old_rows(book, no)
|
||
if not rows and book == "건설품셈" + clause.OLD:
|
||
rows = [
|
||
o
|
||
for o in clause.lookup("건설품셈", no)
|
||
if o["맞음"] == "같음" and dice(c, o["키"], title) > 0
|
||
]
|
||
for o in rows:
|
||
theirs = clause.nature(o["결과단위"])
|
||
if o["키"] not in have and not (mine and theirs and mine != theirs):
|
||
have.add(o["키"])
|
||
out.append({**o, "절": None})
|
||
return out
|
||
|
||
|
||
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 by_machine(c: Ctx, p: dict, S: list, how: str, title: str, check) -> tuple[list, str]:
|
||
"""기계 작업량 절(Q 식) — 번호 길이 아니고 고른 기초가 품 검산으로 안 받쳐지면 그 기계
|
||
「작업량당 기계경비」 로직(품 같음 · 또는 고른 것 없고 품 짝 있음)."""
|
||
if how.startswith("번호") or not bm.has_q(p["글줄"]) or not is_machine(p):
|
||
return S, how
|
||
names = " ".join(str(i.get("이름") or "") for i in p["항목"])
|
||
mk = bm.machine_key(c.mach, syn(f"{p['제목']} {title} {names}"))
|
||
if not mk or mk in S or (len(S) == 1 and _good(check(S[0]))):
|
||
return S, how
|
||
v = check(mk)
|
||
cnt = v.get("셈") or {}
|
||
if _good(v) or ("오류" not in v and (cnt.get(solve.SAME) or cnt.get(solve.DIFF))):
|
||
return [mk], "기계"
|
||
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()
|
||
basis["후보"] += old_cands(c, basis, title_b, blk.get("단위"))
|
||
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)), p.get("기계식", 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)))
|
||
S, how = by_machine(
|
||
c, p, S, how, title_b, 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
|