"""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//section///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//section///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//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//evidence//") 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//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")