- 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>
94 lines
5.0 KiB
Python
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]
|