Files
Pier_v1/Code/교각모델링앱/engine/review.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

94 lines
5.0 KiB
Python

"""Review summary for the one-screen confirmation (2026-10-20): what changed in a bundle, where its values came from, and the reason text the app
writes into the confirmation record. Pure functions (no I/O); the web layer (web/app.py `review`) supplies the data.
Why: confirming 20+ bundles one by one took too long, the reasons written under time pressure were empty ('12', 'asf'), and it was hard to see what
changed. The confirmation stays a HUMAN act (nothing is confirmed without the person's ticks and name); the app only prepares what the person looks at
and writes an informative reason from facts it knows (기획서 4.3 / 6.9).
"""
import json
from engine import verify
MAX_EXAMPLES = 3
def _short(v):
s = json.dumps(v, ensure_ascii=False) if not isinstance(v, (int, float, str)) else str(v)
return s if len(s) <= 24 else s[:21] + "…"
def bundle_change(value, origin_value):
"""Change of a bundle against the pier's creation snapshot: {mode, n_changed, n_total, examples:[(path, old, new)], text}.
mode 'none' when there is no snapshot (older piers), 'same' when nothing changed."""
if origin_value is None:
return {"mode": "none", "n_changed": None, "n_total": None, "examples": [], "text": "생성 시점 값 기록이 없어 무엇이 바뀌었는지 비교하지 못함"}
old, new = dict(verify._walk(origin_value)), dict(verify._walk(value))
paths = sorted(set(old) | set(new))
diff = [p for p in paths if old.get(p) != new.get(p)]
if not diff:
return {"mode": "same", "n_changed": 0, "n_total": len(paths), "examples": [], "text": f"생성 시점 값 그대로({len(paths)}개 값)"}
ex = [(".".join(p), old.get(p), new.get(p)) for p in diff[:MAX_EXAMPLES]]
txt = f"생성 시점 대비 {len(diff)}/{len(paths)}개 값이 바뀜 — " + "; ".join(f"{p}: {_short(o)} → {_short(n)}" for p, o, n in ex) + (" …" if len(diff) > MAX_EXAMPLES else "")
return {"mode": "changed", "n_changed": len(diff), "n_total": len(paths), "examples": ex, "text": txt}
def provenance(which, key, read_log=None, anchor_log=None, candidates_log=None):
"""Facts about where values of the bundle were written from (tools of the app) -> list of short sentences."""
out = []
pre = f"{which}:{key}."
for e in read_log or []:
n = sum(1 for f in e.get("fields", []) if f.startswith(pre))
if n:
out.append(f"도면에서 읽기 {n}건 반영 ({e.get('at', '')[:16]} {e.get('by') or '-'})")
for e in anchor_log or []:
n = sum(1 for f in e.get("fields", []) if isinstance(f, dict) and f.get("field", "").startswith(pre))
if n:
out.append(f"표 밖 치수 후보 {n}건 반영 ({e.get('at', '')[:16]})")
return out
def not_checked(checks, st=None):
"""What the app did NOT verify, one sentence each: the checks that are 근거 없음 / 주의 / 불일치 (id + text); when none of the checks says so
(a status that comes from 'counts only' or from the lack of any evidence) the general statement."""
out = [f"{c['id']} {c['text']}" for c in checks if c["st"] in ("n", "w", "b")]
if not out and st == "w":
out = ["수량만 확인했고 위치·좌표·치수는 도면과 직접 대조하지 못함"]
if not out and st == "n":
out = ["앱이 독립적으로 대조할 근거가 없음 — 도면과 직접 대조"]
return out
def flags(item):
"""Things the person should know before ticking a bundle in bulk (shown as chips, written into the reason): the bundle is still the template's
original, or it carries a provisional registration that waits for the designer."""
out = []
if item["change"]["mode"] == "same" and item["st"] in ("n", "w"):
out.append("템플릿 원본 그대로(새 도면 근거 없음)")
if any("설계자 확인" in d for d in item.get("detail") or []):
out.append("설계자 확인 전 임시 등록 포함")
return out
def group_of(st):
"""review group: A 일치, B 주의·근거 없음, D 사람 결정 기록 / 참고, C 불일치 (cannot be confirmed)"""
return {"g": "A", "w": "B", "n": "B", "x": "D", "b": "C"}.get(st, "B")
def auto_reason(item, note=""):
"""The reason text written into the confirmation record: facts the app knows + the person's note. Never empty."""
st = item["st"]
head = "[검토 요약 일괄 확정] "
if st == "g":
why = head + f"앱 확인 일치 — {item['summary']}"
else:
why = head + f"사용자가 도면과 대조했다고 표시함. 앱 확인 {verify.ST_NAME.get(st, st)} — {item['summary']}"
if item["change"]["mode"] in ("changed", "same"):
why += f" | 변경: {item['change']['text']}"
if item["prov"]:
why += " | 출처: " + "; ".join(item["prov"])
if item.get("flags"):
why += " | 주의 표시: " + "; ".join(item["flags"])
if note.strip():
why += f" | 의견: {note.strip()}"
return why[:900]