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:
commit
964a841de1
3978 files changed
+58013356
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user