"""Stage A (2026-10-16): web/app.py pier page metadata, F-field edit rule (D3) and V3 edit block; web/store.py edit log. Backup in backup/2026-10-16_묶음개편A전/. Refuses to run twice.""" from pathlib import Path APP = Path(__file__).resolve().parents[2] A, S = APP / "web" / "app.py", APP / "web" / "store.py" def patch(path, pairs): s = path.read_text(encoding="utf-8") assert "# stage A 2026-10-16" not in s, f"{path.name} already patched" for a, b in pairs: assert s.count(a) == 1, (path.name, a[:60]) s = s.replace(a, b) path.write_text(s, encoding="utf-8") patch(S, [( ' def set_section(self, which, key, value):', ''' def log_fixed_edit(self, which, key, paths, reason): """Record of an edit of fixed (F) fields with its reason (D3) # stage A 2026-10-16""" log = rjson(self.dir / "edit_log.json", []) log.append({"at": now(), "section": f"{which}:{key}", "fixed_fields": paths, "reason": reason}) wjson(self.dir / "edit_log.json", log) def set_section(self, which, key, value):''')]) patch(A, [ ("from engine import recommend, reader, report_files # noqa: E402", "from engine import recommend, reader, report_files, variability # noqa: E402"), (''' secs = [] for s in p.sections(): secs.append(dict(s, **p.section_state(s["file"], s["key"]))) ''', ''' 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)) origin_same = [s for s in secs if s["required"] and s["meta"]["origin"]] # stage A 2026-10-16 '''), (''' return render_template("pier.html", p=p, sections=secs, gates=gates, need=unconfirmed(p),''', ''' return render_template("pier.html", p=p, sections=secs, gates=gates, need=unconfirmed(p), origin_same=origin_same, has_origin=bool(p.info.get("origin")), grades=variability.GRADES,'''), (''' text = json.dumps(d[key], ensure_ascii=False, indent=1) if request.method == "POST": text = request.form["value"] try: p.set_section(which, key, json.loads(text)) flash("저장했습니다. 확정하려면 '확정'을 누르세요.", "ok") return redirect(url_for("section", name=name, which=which, key=key)) except (ValueError, json.JSONDecodeError) as e: flash(f"저장 안 됨: {e}", "fail") return render_template("section.html", p=p, which=which, key=key, text=text, st=p.section_state(which, key))''', ''' text = json.dumps(d[key], ensure_ascii=False, indent=1) meta = p.section_meta(which, key) if request.method == "POST": text = request.form["value"] reason = request.form.get("reason", "").strip() try: new = store.expand_chains(json.loads(text)) if meta["grade"] == "V3": raise ValueError("생성 변수(V3)는 화면에서 직접 고칠 수 없습니다. 판독 스크립트를 다시 실행해 목록을 만든 뒤 반영하세요") fixed = ["(묶음 전체)"] if meta["grade"] == "F" and new != d[key] else variability.changed_fixed(d[key], new, meta["fixed_fields"]) if fixed and not reason: raise ValueError(f"고정(F) 항목({', '.join(fixed)})을 바꾸면 같은 템플릿이 아닐 수 있습니다. 바꾸는 사유를 입력하세요") p.set_section(which, key, new) if fixed: p.log_fixed_edit(which, key, fixed, reason) flash("저장했습니다. 확정하려면 '확정'을 누르세요." + (" 고정 항목 변경 사유를 기록했습니다." if fixed else ""), "ok") return redirect(url_for("section", name=name, which=which, key=key)) except (ValueError, json.JSONDecodeError) as e: flash(f"저장 안 됨: {e}", "fail") return render_template("section.html", p=p, which=which, key=key, text=text, st=p.section_state(which, key), meta=meta, grades=variability.GRADES)'''), ('''def unconfirmed(p):''', '''def required(p): """Bundles any gate of the pier's scope needs (existing ones).""" return {(r["file"], r["key"]) for g in p.scope() for r in p.gate_requirements(g) if r["exists"]} def unconfirmed(p):'''), ]) print("patched app.py, store.py")