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>
This commit is contained in:
2026-11-05 18:19:26 +09:00
co-authored by Claude Opus 5.5
parent e3262d2b49
commit 532cc52c8e
10 changed files with 2499 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
"""793-3a 지킴 — 근거 제안 엔진(`M02_San_Basis*`) · 실무 여덟 파일 산근 459 블록.
판정 수 ≥ 분석 648-D2 합친 길(기초 짚음 260 · 옵션 전부 52 · 품 검산 맞음 141 · 없음 94) ·
손 확인 견본(합판거푸집 넷 · 철근 Type-Ⅱ 가공 + 조립 · 레미콘 소량감률 50 · 터파기 #51 ·
야면석소운반 GC001140 석재류 · 불량 · 큰돌쌓기 ÷10 근접) · 지어 넣은 값 0 · 보정 자료 = 여덟 파일 ·
저장본 길(임시 폴더). 엑셀 = `helper_m02_hopyo_golden.FILES`(읽기만 · 없으면 skip) ·
STmate 숨은 G 식 = 올림 길과 같은 `M02_San_Basis_Store.hidden`.
"""
from __future__ import annotations
import json
import time
from collections import Counter
from functools import lru_cache
import helper_m02_hopyo_golden as golden
import pytest
from M02_MasterTemplete import M02_Hopyo_Judge as judge
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_San_Basis as sb
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_Store as store
from M02_MasterTemplete import M02_San_Book as san_book
WAYS = {
"B1",
"B2",
"B3",
"B4",
"B5",
"B6",
"V",
"검산",
"검산(같은 품 여럿)",
"근접",
"근접(같은 품 여럿)",
}
@lru_cache(maxsize=1)
def books() -> list[tuple]:
"""[(파일, HP 꼴 책 + 숨은식)] — 없는 파일이면 skip."""
out = []
for tag, path in golden.FILES.items():
book = golden.read(tag) if path.exists() else None
if book is None:
pytest.skip(f"실무 엑셀 없음 {tag}")
sheets = san_book.sheets(path.name, path.read_bytes()) if book["꼴"] == "STmate" else {}
out.append((tag, {**book, "숨은식": store.hidden(book, sheets)}))
return out
@lru_cache(maxsize=1)
def results() -> tuple[dict, float]:
sb.ctx()
t0 = time.time()
got = {}
for tag, book in books():
for blk in book["단산"]:
got[f"{tag}#{blk['번']}"] = store.one(book, int(blk["번"]))
return got, time.time() - t0
def test_counts_at_least_d2():
got, sec = results()
kind = Counter(r["꼴"] for r in got.values())
tier = Counter(r["단"] for r in got.values())
same = sum(
1
for r in got.values()
if r["꼴"].startswith("확정")
and "다름" not in (st := [sb.checked(s) for s in sb.chosen_units(r)])
and "같음" in st
)
print(f"\n블록 {len(got)} · 꼴 {dict(kind)} · 단 {dict(tier)} · 품 같음 {same} · {sec:.1f} 초")
assert len(got) == 459
assert sum(v for k, v in kind.items() if k.startswith("확정")) >= 260
assert kind["확정·전부"] >= 52
assert same >= 141
assert tier["없음"] <= 94 and kind["없음"] <= 94
def _first(bid: str) -> dict:
return results()[0][bid]["근거후보"]
def test_plywood_four_same():
for bid in ("북부청임도#24", "북부청사방#66", "거창#41", "동부청영월#1841"):
(c,) = _first(bid)
assert (c["기초"], c["단"], c["품검산"]) == ("GC000211", "확정", "같음"), bid
assert {c["옵션"][k] for k in ("사용횟수", "유형")} == {"4회", "보통"}, bid
assert {"높이감률", "구멍땜"} <= set(c["입력필요"]), bid # 글에 없는 감률 = 입력 필요
def test_rebar_type2_cut_and_join():
got = {c["기초"]: c for c in _first("북부청임도#72")}
assert set(got) == {"GC000206", "GC000207"}
assert got["GC000206"]["옵션"]["Type"] == got["GC000207"]["옵션"]["Type"] == "Type-Ⅱ"
assert all(c["단"] == "확정" for c in got.values())
def test_remicon_small_50():
(c,) = _first("북부청임도#21")
assert c["기초"] == "GC000198" and c["단"] == "확정"
assert c["옵션"]["소량감률"] == 50 and c["옵션길"]["소량감률"] == "B5"
def test_excavation_51():
(c,) = _first("북부청임도#51")
assert (c["기초"], c["단"], c["옵션"]["Type"], c["옵션"]["깊이"]) == (
"GC000089",
"확정",
"TypeⅢ",
"5m 이하",
)
def test_rubble_carry_gc001140():
r = results()[0]["동부청영월#1466"]
c = r["근거후보"][0]
assert r["단"] == "사람" and c["기초"] == "GC001140"
assert c["옵션"] == {"재료갈래": "석재류", "운반로": "불량"}
assert set(c["옵션길"].values()) == {"V"}
def test_big_stone_near_unit_basis():
r = results()[0]["북부청임도#32"]
(c,) = r["근거후보"]
assert (r["단"], c["기초"], c["단"], c["품검산"]) == ("근접", "GF000226", "근접", "다름")
assert {t for t, _ in c["까닭"]} == {"단위 기준 차이"} and "÷10" in c["까닭"][0][1]
def test_no_made_up_values():
"""옵션마다 출처 꼬리표 · 입력 필요 · 0 간주 칸은 옵션에 없음 ·
없음 단은 근거후보 없이 보강 까닭만."""
for bid, r in results()[0].items():
for c in r["근거후보"]:
assert set(c["옵션"]) <= set(c.get("옵션길") or {}), bid
assert set((c.get("옵션길") or {}).values()) <= WAYS, bid
assert not set(c["옵션"]) & set(c["입력필요"]), bid
assert all(v is not None for v in c["옵션"].values()), bid
assert c["단"] in ("확정", "근접", "사람")
if r["단"] == "없음":
assert r["근거후보"] == [] and r["보강"]["까닭"], bid
def test_corpus_is_eight_files():
"""보정 자료 = 여덟 파일 상수(일반 뺌) — 뽑기 규칙을 바꾸면 다시 지어 넣음."""
preps = [
bc.prep(blk, book["꼴"], (book["숨은식"] or {}).get(str(blk["번"])))
for _, book in books()
for blk in book["단산"]
]
shipped = json.loads(bi.CORPUS.read_text(encoding="utf-8"))
assert sorted(map(json.dumps, bc.corpus(preps))) == sorted(map(json.dumps, shipped))
def test_store_round_trip(tmp_path):
"""임시 폴더 HP → 뒤 셈 저장본 · 목록 · 블록 하나 · 끝내기."""
_, book = books()[0]
small = {**book, "단산": [b for b in book["단산"] if b["번"] in (21, 51)]}
row = hopyo_store.save(tmp_path, "시험.xls", small, {"id": 0, "이름": "시험"})
key = row["키"]
assert store.solved(tmp_path, key) is None
got = store.solve_all(tmp_path, key)
assert set(got) == {"21", "51"} and store.solved(tmp_path, key) == json.loads(json.dumps(got))
assert store.listing(tmp_path, key)["셈중"] is False
assert store.block(tmp_path, key, 51)["근거후보"][0]["기초"] == "GC000089"
with pytest.raises(Exception):
store.block(tmp_path, key, 999)
store.forget(tmp_path, key)
assert store.solved(tmp_path, key) is None
judge.forget(tmp_path, key)
def test_hidden_g_lines():
book = {"꼴": "STmate", "단산": [{"번": "3", "행": [2, 4]}]}
rows = [[None] * 7, [None] * 6 + ["_x0007_L=20"], [None] * 6 + ["_x0007_"], [None] * 6 + ["x"]]
assert store.hidden(book, {store.SHEET: rows}) == {"3": ["L=20"]}
assert store.hidden({"꼴": "엑셀꼴", "단산": []}, {}) == {}