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>
This commit is contained in:
ahnyongjooandClaude Sonnet 5.5 committed 2026-10-28 11:20:18 +09:00
commit 964a841de1
3978 files changed
+58013356

No files matched your search

+217
View File
@@ -0,0 +1,217 @@
"""Review report draft (Korean Markdown) from gate results, drawing tables and approval records.
Chapters follow the requirements review (기획 v0.1.01 §3.7): 개요, 오류·불일치, 개선방안, 추가 사항, 누락,
철근재료표(원본 vs 모델), 철근상세, plus 검증 방법. Verdict vocabulary is fixed (오류·불일치·누락·확인 필요);
automatic entries use only 불일치 (approved planned FAIL) and 확인 필요 (everything else). Final verdicts are
confirmed by a person, so the report is always issued as a draft.
"""
import json
import re
import time
from pathlib import Path
from engine import reader
from engine.pipeline import GATES, gate_ids
VOCAB = ("오류", "불일치", "누락", "확인 필요")
REBAR_GATES = {"G2": "기둥", "G3": "확대기초", "G4": "코핑"}
def _rj(p, default=None):
p = Path(p)
return json.loads(p.read_text(encoding="utf-8")) if p.exists() else default
def _cell(v):
return str(v).replace("|", "/").replace("\n", " ")
def _table(head, rows):
out = ["| " + " | ".join(head) + " |", "|" + "|".join(" :--- " for _ in head) + "|"]
out += ["| " + " | ".join(_cell(c) for c in r) + " |" for r in rows]
return out
def collect(pier_dir, run_dir):
pier_dir, run_dir = Path(pier_dir), Path(run_dir)
d = {"info": _rj(pier_dir / "pier.json", {"name": pier_dir.name, "type": "T형"}),
"audit": _rj(pier_dir / "drawings" / "audit.json", []),
"approvals": _rj(run_dir / "approvals.json", {}),
"expected": _rj(pier_dir / "expected.json", {}), "params": _rj(pier_dir / "params.json", {}),
"gates": {}, "results": {}, "autocheck": _rj(pier_dir / "autocheck.json", {}), "confirm": _rj(pier_dir / "confirm.json", {})}
for g in gate_ids():
d["gates"][g] = _rj(run_dir / f"{g}_gate.json")
d["results"][g] = _rj(run_dir / f"{g}.json")
texts = _rj(pier_dir / "drawings" / "texts.json", [])
tables = reader.table_candidates(texts) if texts else []
cands = reader.field_candidates(tables)
d["schedule"] = [c for c in cands if c["target"].startswith("schedule.")]
d["diffs"] = [r for r in reader.proposals(cands, d["params"], d["expected"]) if not r["same"]] if cands else []
return d
def _n(v):
"""Number for a table cell: '-' when missing, no trailing .0 for integral values."""
if v is None:
return "-"
return int(v) if isinstance(v, float) and v.is_integer() else v
def _bar_counts(results):
"""Model bar count per mark from the checker results (items '<mark> bar count...')."""
out = {}
for g in REBAR_GATES:
for r in (results.get(g) or {}).get("results", []):
m = re.match(r"^(\S+) bar count", r["item"])
if m and "actual" in r:
out[m.group(1)] = (int(r["actual"]), int(r["expected"]), r["result"], g)
return out
def build(pier_dir, run_dir, author="", today=None, stale=(), out_of_scope=()):
"""stale: gates whose result predates the current inputs (shown as void, not counted as done)."""
d = collect(pier_dir, run_dir)
for g in stale:
d["gates"][g] = None
d["results"][g] = None
today = today or time.strftime("%Y-%m-%d")
name, gates, appr = d["info"]["name"], d["gates"], d["approvals"]
done = [g for g in gate_ids() if gates[g]]
all_ok = all(gates[g] and gates[g]["verdict"] == "PASS" and appr.get(g, {}).get("decision") == "승인"
for g in gate_ids() if g not in out_of_scope)
L = [f"# {name} 교각 모델링 검토보고서 (초안)", "",
f"> 앱이 게이트 결과·도면 표·승인 기록으로 자동 작성한 **초안**입니다. '오류'·'불일치' 판정과 3~5장의 서술은 "
f"담당자가 확인·확정해야 합니다. 작성일 {today}{' · 작성 ' + author if author else ''}.", ""]
# 1 overview
L += ["## 1. 개요", "", f"- 교각: {name} ({d['info'].get('type', 'T형')}) · 좌표: 로컬(교각 중심 원점, X 교축직각, Y 교축, "
f"Z = EL×1000, mm) · 산출: .3dm", f"- 게이트 진행: {len(done)}/{5 - len(out_of_scope)} 실행{(' (범위 밖: ' + ', '.join(out_of_scope) + ')') if out_of_scope else ''}, "
+ ("**전 게이트 PASS·승인**" if all_ok else "**미완료 — 실행·승인되지 않은 게이트가 있음**"), ""]
if d["audit"]:
L += ["### 1.1 입력 도면", ""] + _table(["구분", "DWG", "SHA-256(앞 16자)"],
[[a.get("label", ""), a["source"], a["source_sha256"][:16]] for a in d["audit"]]) + [""]
rows = []
for g, gname, _ in GATES:
r, a = gates[g], appr.get(g, {})
if not r:
rows.append([g, gname, "범위 밖" if g in out_of_scope else
("입력 변경 — 무효(재실행 필요)" if g in stale else "미실행"), "-", "-", "-"])
continue
s = r["summary"]
rows.append([g, gname, r["verdict"], f"{s['passed']}/{s['total']}",
f"{a.get('decision', '-')} {a.get('by', '')} {a.get('at', '')}".strip(), a.get("note", "") or "-"])
L += ["### 1.2 게이트 결과", ""] + _table(["게이트", "내용", "판정", "검사 PASS", "승인", "의견"], rows) + [""]
# 2 errors / mismatches
finds = []
for g in done:
for pf in gates[g]["planned_fails"]:
ex = next((x for x in d["expected"].get("planned_fail", []) if x["gate"] == g and
(x.get("item") == pf["item"] or (x.get("item_prefix") and pf["item"].startswith(x["item_prefix"])))), {})
finds.append(["불일치", g, pf["item"], pf["reason"], f"예외 승인: {ex.get('approved', '-')}"])
for item in gates[g]["unplanned_fails"]:
finds.append(["확인 필요", g, item, "계획되지 않은 FAIL", "게이트 미승인 사유"])
for r in d["diffs"]:
finds.append(["확인 필요", "-", ".".join([r["file"]] + r["path"]),
f"도면 표 값 {json.dumps(r['candidate'], ensure_ascii=False)} ≠ 입력값 {json.dumps(r['current'], ensure_ascii=False)}",
r["src"]])
L += ["## 2. 도면 오류·불일치", "",
"자동 등재 기준: 승인된 계획 FAIL → '불일치', 계획되지 않은 FAIL과 도면 표 값·입력값 차이 → '확인 필요'.", ""]
L += (_table(["판정", "게이트", "항목", "내용", "근거"], finds) if finds else ["자동으로 찾은 항목이 없습니다."]) + [""]
# 3-5 human chapters with automatic hints
L += ["## 3. 개선방안", "", "[담당자 작성] 2장 항목별로 도면 수정·설계자 확인 방안을 적습니다.", "",
"## 4. 추가되어야 할 사항", "", "[담당자 작성] 도면에 없어 추정·가정으로 모델링한 사항(입력 묶음의 `[추론]` 표기)을 정리합니다.", ""]
inferred = sorted({k for k, v in d["params"].items() if "[추론]" in json.dumps(v, ensure_ascii=False)})
if inferred:
L += [f"- 자동 확인: `[추론]` 표기가 있는 입력 묶음 — {', '.join(inferred)}", ""]
L += ["## 5. 누락사항", "", "[담당자 작성] 도면에서 빠진 치수·표·상세를 적습니다.", ""]
nomodel = [c["target"][9:] for c in d["schedule"] if c["target"][9:] not in d["params"].get("rebar_marks", {})]
if nomodel:
L += [f"- 자동 확인: 재료표에 있으나 모델 대상이 아닌 마크 — {', '.join(sorted(set(nomodel)))} (범위 결정 확인)", ""]
# 6 schedule vs model
bc = _bar_counts(d["results"])
rows, seen = [], set()
for c in d["schedule"]:
m, v = c["target"][9:], c["value"]
if m in seen:
continue
seen.add(m)
mc = bc.get(m)
if m not in d["params"].get("rebar_marks", {}):
model, judge = "모델 외", "-"
elif not mc:
model, judge = "미검사", "-" # its rebar gate has not run (or is void)
else:
model, judge = mc[0], ("일치" if mc[0] == v["n"] else "불일치")
rows.append([m, v["shape"] or "-", v["dia"] or "-", _n(v["n"]), _n(v["L"]), model, judge, c["src"]])
L += ["## 6. 철근재료표 (원본 vs 모델)", "",
"원본 = 도면 재료표 판독값(동일 기호 \" 해석 포함), 모델 = 게이트 검사의 본수 실측값.", ""]
L += (_table(["마크", "형식", "직경", "재료표 수량", "재료표 L(mm)", "모델 본수", "대조", "근거"], rows)
if rows else ["도면 재료표를 인식하지 못했습니다(도면 미등록 또는 표 형식 상이)."]) + [""]
# 7 rebar detail (records)
L += ["## 7. 철근상세 (기록)", "", "원길이·곡선길이·피복은 판정이 아닌 기록 값입니다(요구사항 §6.2).", ""]
for g, part in REBAR_GATES.items():
res = d["results"].get(g)
if not res:
continue
recs = [[r["item"], r.get("detail", "")] for r in res["results"]
if r["result"] == "INFO" and ("length" in r["item"] or "cover" in r["item"])]
L += [f"### 7.{list(REBAR_GATES).index(g) + 1} {part} ({g})", ""] + _table(["항목", "값"], recs) + [""]
# 8 verification method
rows = []
for g in done:
r = gates[g]
n = r["negative"]
ok = lambda v, yes, no: "생략(일괄 실행)" if v is None else (yes if v else no)
rows.append([g, ok(r["reload"]["ok"], "일치", "불일치"),
" / ".join(f"{'수평' if k == 'negH' else '수직'} {'검출' if v['detected'] else '미검출'}" for k, v in n.items()),
ok(r["unchanged"]["ok"], f"불변 ({r['unchanged']['compared']})", f"변경 ({r['unchanged']['compared']})")])
L += ["## 8. 검증 방법", "",
"- 검사기는 생성 입력(params)이 아니라 도면 문자·표에서 옮긴 독립 기대값(expected)만 읽습니다.",
"- 게이트마다 G1부터 새로 생성 → 저장 파일을 다시 열어 검사(재로드) → 객체를 수평·수직으로 옮겨 FAIL이 나는지 확인(음성 시험) → 앞 게이트 객체 지문 비교.",
"- 일괄 실행(단일 Rhino 세션)으로 검사한 게이트는 중간 저장·재로드와 앞 게이트 비교를 하지 않으며, 표에 '생략(일괄 실행)'으로 적습니다. 모델은 마지막 G5만 저장합니다.",
""] + _table(["게이트", "재로드", "음성 시험", "앞 게이트 객체"], rows) + [""]
ac = (d.get("autocheck") or {}).get("sections") or {}
if ac:
names = {"g": "일치", "w": "주의", "b": "불일치", "n": "근거 없음", "x": "해당 없음"}
rows = []
for b, r in sorted(ac.items(), key=lambda kv: ("gwbnx".index(kv[1]["st"]) if kv[1]["st"] in "gwbnx" else 9, kv[0])):
if r["st"] == "x" and b.split(":")[1].startswith("_"):
continue
c = d["confirm"].get(b) or {}
human = f"{c['by']} {c['at']}" + (f" (앱 확인 {names.get(c.get('app_st'), '-')}, 사유: {c['why']})" if c.get("why") else "") if c else "미확정"
rows.append([b, names.get(r["st"], r["st"]), r["summary"][:90], human])
n = {k: sum(1 for r in ac.values() if r["st"] == k) for k in "gwbnx"}
L += ["### 8.1 입력 묶음 앱 확인 결과 요약", "",
f"- 앱 확인(검사 버전 {d['autocheck'].get('version', '-')}, {d['autocheck'].get('at', '-')}): 일치 {n['g']} · 주의 {n['w']} · 불일치 {n['b']} · 근거 없음 {n['n']} · 해당 없음 {n['x']} (전체 묶음 {len(ac)}개).",
"- 앱 확인은 도면 표·재료표 수량·고정 항목을 앱이 대조한 결과입니다. '일치'는 독립 근거와 값이 같다는 뜻이며 도면이 맞다는 보증이 아닙니다. 확정은 사람이 했고, 앱 확인이 일치가 아닌 묶음은 사유가 기록되어 있습니다.",
""] + _table(["묶음", "앱 확인", "근거", "사람 확정"], rows) + [""]
L += ["## 변경 이력", "", "| 버전 | 일자 | 내용 |", "| :--- | :--- | :--- |", f"| v0.1.00 | {today} | 앱 자동 생성 초안 |", ""]
return "\n".join(L), d
def check(md, d):
"""Report self-check (pattern of check_report.py): chapters, vocabulary, schedule rows, gate table."""
need = ["## 1. 개요", "## 2. 도면 오류·불일치", "## 3. 개선방안", "## 4. 추가되어야 할 사항", "## 5. 누락사항",
"## 6. 철근재료표", "## 7. 철근상세", "## 8. 검증 방법"]
res = {"chapters_missing": [h for h in need if h not in md]}
sec2 = md.split("## 2.")[1].split("## 3.")[0]
words = re.findall(r"^\| ([^|]+?) \| (?:G\d|-) \|", sec2, re.M)
res["vocab_bad"] = [w for w in words if w not in VOCAB]
res["finding_rows"] = len(words)
res["finding_rows_without_source"] = len([l for l in sec2.splitlines() if re.match(r"^\| (오류|불일치|누락|확인 필요) \|", l)
and l.rstrip().endswith("| |")])
marks = {c["target"][9:] for c in d["schedule"]}
sec6 = md.split("## 6.")[1].split("## 7.")[0]
res["schedule_rows"] = len([l for l in sec6.splitlines() if re.match(r"^\| [A-Z]{1,2}\d", l)])
res["schedule_marks"] = len(marks)
res["gate_rows"] = len(re.findall(r"^\| G\d \|", md.split("### 1.2")[1].split("## 2.")[0], re.M)) if "### 1.2" in md else 0
# a pier with rebar marks must have its schedule read (Ulsan 2026-10-13: 0 of 0 rows passed silently)
res["schedule_unread"] = bool(d["params"].get("rebar_marks")) and not marks
res["ok"] = (not res["chapters_missing"] and not res["vocab_bad"] and res["schedule_rows"] == res["schedule_marks"]
and not res["schedule_unread"] and res["gate_rows"] == 5 and res["finding_rows_without_source"] == 0)
return res