Files
Pier_v1/Code/교각모델링앱/tools/patches/2026-10-16_autocheck_web_patch.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

163 lines
8.3 KiB
Python

"""Stage B (2026-10-16): confirmation records carry the reason and the app-check status, routes for the app check, the evidence page and the
"일치 묶음 일괄 확정" (D4), the report gets the app-check summary. The old 필요한 묶음 일괄 확정 route stays (legacy, stage D decides).
Backups of the stage-A state are made by the caller. Refuses to run twice."""
from pathlib import Path
APP = Path(__file__).resolve().parents[2]
def patch(path, pairs):
s = path.read_text(encoding="utf-8")
assert "# stage B 2026-10-16" not in s, f"{path.name} already patched"
for a, b in pairs:
assert s.count(a) == 1, (path.name, a[:70])
s = s.replace(a, b)
path.write_text(s, encoding="utf-8")
STORE = [
(''' return {"exists": True, "confirmed": ok, "by": c and c["by"], "at": c and c["at"],
"stale": bool(c) and not ok, "info": is_info(key)}''',
''' return {"exists": True, "confirmed": ok, "by": c and c["by"], "at": c and c["at"], "why": c and c.get("why"),
"app_st": c and c.get("app_st"), "stale": bool(c) and not ok, "info": is_info(key)}'''),
(''' def confirm(self, which, key, by):
d = self.load(which)
conf = self.confirmations()
conf[f"{which}:{key}"] = {"sha": sha(d[key]), "by": by, "at": now()}''',
''' def confirm(self, which, key, by, why="", app_st=""):
"""Human confirmation record (stage B 2026-10-16: + the reason `why` and the app-check status at that time `app_st`)."""
d = self.load(which)
conf = self.confirmations()
rec = {"sha": sha(d[key]), "by": by, "at": now()}
if why:
rec["why"] = why
if app_st:
rec["app_st"] = app_st
conf[f"{which}:{key}"] = rec'''),
]
APPPY = [
("from engine import recommend, reader, report_files, variability # noqa: E402",
"from engine import recommend, reader, report_files, variability, verify # noqa: E402"),
('''@app.post("/pier/<name>/section/<which>/<key>/confirm")
def section_confirm(name, which, key):
p = pier_or_404(name)
by = request.form.get("by", "").strip()
if not by:
flash("확정자 이름을 입력하세요.", "fail")
else:
p.confirm(which, key, by)
flash(f"{which}.{key} 확정 ({by})", "ok")
return redirect(request.form.get("next") or url_for("pier", name=name))''',
'''@app.post("/pier/<name>/section/<which>/<key>/confirm")
def section_confirm(name, which, key):
"""Confirm one bundle. A bundle the app check does not report as 일치 (주의 / 불일치 / 근거 없음) needs a reason (D3, stage B 2026-10-16)."""
p = pier_or_404(name)
by = request.form.get("by", "").strip()
why = request.form.get("why", "").strip()
p.run_autocheck()
st = p.section_check(which, key)["st"]
if not by:
flash("확정자 이름을 입력하세요.", "fail")
elif st in ("w", "b", "n") and not why:
flash(f"{which}.{key}는 앱 확인이 '{verify.ST_NAME[st]}'입니다. 사유를 입력해야 확정할 수 있습니다.", "fail")
else:
p.confirm(which, key, by, why, st)
flash(f"{which}.{key} 확정 ({by})" + (f" — 앱 확인 {verify.ST_NAME.get(st, st)}, 사유 기록" if why else ""), "ok")
return redirect(request.form.get("next") or url_for("pier", name=name))
# ---------------- app check (stage B 2026-10-16)
@app.post("/pier/<name>/autocheck")
def autocheck_run(name):
p = pier_or_404(name)
a = p.run_autocheck(force=True)
flash(f"앱 확인을 실행했습니다 ({len(a['sections'])}개 묶음). 앱은 확정하지 않습니다.", "ok")
return redirect(request.form.get("next") or url_for("pier", name=name))
@app.route("/pier/<name>/evidence/<which>/<key>")
def evidence(name, which, key):
p = pier_or_404(name)
if which not in ("params", "expected") or key not in p.load(which):
abort(404)
p.run_autocheck()
return render_template("evidence.html", p=p, which=which, key=key, chk=p.section_check(which, key), st=p.section_state(which, key),
meta=p.section_meta(which, key), grades=variability.GRADES, ST=verify.ST_NAME)
def matched_targets(p):
"""Required bundles the app check reports as 일치 and nobody has confirmed (current): the D4 bulk-confirm targets."""
p.run_autocheck()
out = []
for f, k in sorted(required(p)):
c, s = p.section_check(f, k), p.section_state(f, k)
if c["st"] == "g" and not c["stale"] and not s["confirmed"]:
out.append((f, k))
return out
def _targets_sha(p, targets):
return store.sha([[f, k, p.section_check(f, k)["sha"], p.section_check(f, k)["st"]] for f, k in targets])
@app.route("/pier/<name>/confirm_matched", methods=["GET", "POST"])
def confirm_matched(name):
"""D4: bulk confirmation of the bundles the app check reports as 일치. The evidence table must have been opened (a marker holds the
hash of the target list), a name is required; anything the app does not report as 일치 is not touched."""
p = pier_or_404(name)
targets = matched_targets(p)
if request.method == "POST":
by = request.form.get("by", "").strip()
mark = store.rjson(p.dir / "confirm_view.json", {})
if not targets:
flash("확정할 일치 묶음이 없습니다.", "fail")
elif mark.get("sha") != _targets_sha(p, targets):
flash("근거표를 열어 끝까지 읽은 뒤에 확정할 수 있습니다(대상 목록이 바뀌었으면 다시 여세요).", "fail")
elif not by:
flash("확정자 이름을 입력하세요.", "fail")
else:
for f, k in targets:
p.confirm(f, k, by, "", "g")
flash(f"앱 확인 '일치' 묶음 {len(targets)}개를 {by}이(가) 확정했습니다. 확정 기록에는 입력한 이름만 남습니다.", "ok")
return redirect(url_for("pier", name=name))
return redirect(url_for("confirm_matched", name=name))
store.wjson(p.dir / "confirm_view.json", {"sha": _targets_sha(p, targets), "at": store.now(), "n": len(targets)})
rest = []
for f, k in sorted(required(p)):
if (f, k) in targets:
continue
s = p.section_state(f, k)
if not s["confirmed"]:
rest.append(dict(file=f, key=k, chk=p.section_check(f, k)))
return render_template("confirm_matched.html", p=p, targets=[dict(file=f, key=k, chk=p.section_check(f, k)) for f, k in targets],
rest=rest, ST=verify.ST_NAME)'''),
(''' secs = []
req = required(p)
for s in p.sections():
secs.append(dict(s, **p.section_state(s["file"], s["key"]), meta=p.section_meta(s["file"], s["key"]),
required=(s["file"], s["key"]) in req))''',
''' secs = []
req = required(p)
p.run_autocheck() # stage B: skipped when nothing changed
for s in p.sections():
secs.append(dict(s, **p.section_state(s["file"], s["key"]), meta=p.section_meta(s["file"], s["key"]),
required=(s["file"], s["key"]) in req, chk=p.section_check(s["file"], s["key"])))'''),
(''' origin_same = [s for s in secs if s["required"] and s["meta"]["origin"]] # stage A 2026-10-16''',
''' origin_same = [s for s in secs if s["required"] and s["meta"]["origin"]] # stage A 2026-10-16
cnt = {k: 0 for k in "gwbnx"} # stage B 2026-10-16
for s in secs:
if s["required"]:
cnt[s["chk"]["st"] if s["chk"]["st"] in cnt else "n"] += 1
human = sum(1 for s in secs if s["required"] and s["confirmed"])
nmatch = len(matched_targets(p))'''),
(''' has_origin=bool(p.info.get("origin")), grades=variability.GRADES,''',
''' has_origin=bool(p.info.get("origin")), grades=variability.GRADES, cnt=cnt, human=human, nreq=len(req),
nmatch=nmatch, ST=verify.ST_NAME,'''),
]
if __name__ == "__main__":
patch(APP / "web" / "store.py", STORE)
patch(APP / "web" / "app.py", APPPY)
print("patched store.py, app.py")