- 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>
173 lines
8.1 KiB
Python
173 lines
8.1 KiB
Python
from pathlib import Path
|
|
|
|
APP = Path(r"C:\Users\User\Desktop\바이브코딩\pier_T\Code\교각모델링앱")
|
|
|
|
|
|
def patch(rel, reps):
|
|
p = APP / rel
|
|
s = p.read_text(encoding="utf-8")
|
|
for a, b in reps:
|
|
assert s.count(a) == 1, (rel, a[:60])
|
|
s = s.replace(a, b)
|
|
p.write_text(s, encoding="utf-8")
|
|
|
|
|
|
patch("web/app.py", [
|
|
("""import json
|
|
import sys
|
|
import threading
|
|
import traceback""", """import json
|
|
import os
|
|
import sys
|
|
import threading
|
|
import traceback
|
|
import uuid"""),
|
|
("""RHINO_LOCK = threading.Lock() # one Rhino job at a time""",
|
|
"""RHINO_LOCK = threading.Lock() # one Rhino job at a time
|
|
ACTIVE = {} # job id -> thread of this server process"""),
|
|
("""def _gate_job(p, gate):
|
|
jp = p.run_dir / f"{gate}_job.json"
|
|
inputs = p.inputs_sha()
|
|
log = []
|
|
|
|
def note(m):
|
|
log.append(m)
|
|
store.wjson(jp, {"state": "실행 중", "at": store.now(), "log": log})
|
|
with RHINO_LOCK:
|
|
try:
|
|
note("Rhino 작업 시작")
|
|
rep = run_gate(gate, p.dir / "params.json", p.dir / "expected.json", p.run_dir, log=note)
|
|
rep["inputs_sha"] = inputs
|
|
store.wjson(p.run_dir / f"{gate}_gate.json", rep)
|
|
store.wjson(jp, {"state": "완료", "at": store.now(), "log": log})
|
|
except Exception:
|
|
store.wjson(jp, {"state": "오류", "at": store.now(), "log": log, "error": traceback.format_exc()})""",
|
|
"""def job_state(job):
|
|
\"\"\"A queued/running job counts only while its thread lives in this server process; a job left behind by a
|
|
stopped server (or test process) is shown as 중단됨 and may be run again.\"\"\"
|
|
if job and job["state"] in ("대기", "실행 중"):
|
|
t = ACTIVE.get(job.get("id"))
|
|
if job.get("pid") != os.getpid() or not (t and t.is_alive()):
|
|
return "중단됨"
|
|
return job and job["state"]
|
|
|
|
|
|
def _gate_job(p, gate, jid):
|
|
jp = p.run_dir / f"{gate}_job.json"
|
|
inputs = p.inputs_sha()
|
|
log = []
|
|
|
|
def put(state, **kw):
|
|
store.wjson(jp, dict({"id": jid, "pid": os.getpid(), "state": state, "at": store.now(), "log": log}, **kw))
|
|
|
|
def note(m):
|
|
log.append(m)
|
|
put("실행 중")
|
|
with RHINO_LOCK:
|
|
try:
|
|
note("Rhino 작업 시작")
|
|
rep = run_gate(gate, p.dir / "params.json", p.dir / "expected.json", p.run_dir, log=note)
|
|
rep["inputs_sha"], rep["job_id"] = inputs, jid
|
|
store.wjson(p.run_dir / f"{gate}_gate.json", rep)
|
|
put("완료")
|
|
except Exception:
|
|
put("오류", error=traceback.format_exc())"""),
|
|
(""" job = p.gate_status(gate)["job"]
|
|
if job and job["state"] in ("대기", "실행 중"):
|
|
flash(f"{gate}는 이미 실행 중입니다.", "fail")
|
|
return redirect(url_for("gates", name=name))
|
|
# Mark the job as queued before the thread starts, so no request sees the previous run's "완료" state.
|
|
store.wjson(p.run_dir / f"{gate}_job.json", {"state": "대기", "at": store.now(), "log": []})
|
|
threading.Thread(target=_gate_job, args=(p, gate), daemon=True).start()""",
|
|
""" if job_state(p.gate_status(gate)["job"]) in ("대기", "실행 중"):
|
|
flash(f"{gate}는 이미 실행 중입니다.", "fail")
|
|
return redirect(url_for("gates", name=name))
|
|
# Mark the job as queued (with its id) before the thread starts, so no request sees the previous run's state.
|
|
jid = uuid.uuid4().hex[:12]
|
|
store.wjson(p.run_dir / f"{gate}_job.json",
|
|
{"id": jid, "pid": os.getpid(), "state": "대기", "at": store.now(), "log": []})
|
|
t = threading.Thread(target=_gate_job, args=(p, gate, jid), daemon=True)
|
|
ACTIVE[jid] = t
|
|
t.start()"""),
|
|
(""" rows = [dict(id=g, name=GATE_NAMES[g], blockers=gate_blockers(p, g), req=p.gate_requirements(g), **p.gate_status(g))
|
|
for g in gate_ids()]
|
|
running = any(r["job"] and r["job"]["state"] in ("대기", "실행 중") for r in rows)""",
|
|
""" rows = [dict(id=g, name=GATE_NAMES[g], blockers=gate_blockers(p, g), req=p.gate_requirements(g), **p.gate_status(g))
|
|
for g in gate_ids()]
|
|
for r in rows:
|
|
r["state"] = job_state(r["job"])
|
|
running = any(r["state"] in ("대기", "실행 중") for r in rows)"""),
|
|
])
|
|
|
|
patch("web/templates/gates.html", [
|
|
("""{% if g.job and g.job.state in ('대기', '실행 중') %}<p class="warn">{{ g.job.state }}: {{ g.job.log[-1] if g.job.log else '' }}</p>
|
|
{% elif g.job and g.job.state == '오류' %}<p class="fail">오류</p><pre>{{ g.job.error }}</pre>{% endif %}""",
|
|
"""{% if g.state in ('대기', '실행 중') %}<p class="warn">{{ g.state }}: {{ g.job.log[-1] if g.job.log else '' }}</p>
|
|
{% elif g.state == '중단됨' %}<p class="warn">중단됨: 이전 실행이 끝나지 않은 채 서버가 멈췄습니다. 다시 실행하세요.</p>
|
|
{% elif g.state == '오류' %}<p class="fail">오류</p><pre>{{ g.job.error }}</pre>{% endif %}"""),
|
|
])
|
|
|
|
patch("tools/test_web.py", [
|
|
("""def html(r):
|
|
return r.get_data(as_text=True)""",
|
|
"""def html(r):
|
|
return r.get_data(as_text=True)
|
|
|
|
|
|
def run_and_wait(c, P, g, limit=900):
|
|
\"\"\"POST run, then wait until the job with the id written at start-up is finished; returns (job, trace).\"\"\"
|
|
c.post(f"/pier/{P}/gate/{g}/run")
|
|
jid = (store.Pier(P).gate_status(g)["job"] or {}).get("id")
|
|
t0, trace = time.time(), []
|
|
while time.time() - t0 < limit:
|
|
job = store.Pier(P).gate_status(g)["job"] or {}
|
|
trace.append((round(time.time() - t0), job.get("id") == jid, job.get("state")))
|
|
if jid and job.get("id") == jid and job.get("state") in ("완료", "오류"):
|
|
return job, trace
|
|
time.sleep(3)
|
|
return None, trace"""),
|
|
(""" r = c.post(f"/pier/{P}/gate/{g}/run", follow_redirects=True)
|
|
t0 = time.time()
|
|
while True:
|
|
job = store.Pier(P).gate_status(g)["job"]
|
|
if job and job["state"] in ("완료", "오류"):
|
|
break
|
|
if time.time() - t0 > 900:
|
|
break
|
|
time.sleep(3)
|
|
st = store.Pier(P).gate_status(g)
|
|
rep = st["report"] or {}
|
|
check(f"{g} run via app -> PASS", job and job["state"] == "완료" and rep.get("verdict") == "PASS",
|
|
f"{rep.get('summary')} {time.time() - t0:.0f}s {job.get('error', '')[-200:] if job else ''}")""",
|
|
""" job, trace = run_and_wait(c, P, g)
|
|
rep = store.Pier(P).gate_status(g)["report"] or {}
|
|
check(f"{g} run via app -> PASS", bool(job) and job["state"] == "완료" and rep.get("verdict") == "PASS"
|
|
and rep.get("job_id") == job["id"], f"{rep.get('summary')} {trace[-1][0]}s {(job or {}).get('error', '')[-200:]}")"""),
|
|
(""" c.post(f"/pier/{P}/gate/G1/run")
|
|
t0 = time.time()
|
|
while time.time() - t0 < 600:
|
|
job = store.Pier(P).gate_status("G1")["job"]
|
|
if job and job["state"] in ("완료", "오류"):
|
|
break
|
|
time.sleep(3)
|
|
rep = store.Pier(P).gate_status("G1")["report"]
|
|
check("pile length +5 mm -> G1 FAIL (pile length)", rep["verdict"] == "FAIL" and "pile length" in rep["unplanned_fails"],
|
|
rep["unplanned_fails"])""",
|
|
""" job, trace = run_and_wait(c, P, "G1")
|
|
rep = store.Pier(P).gate_status("G1")["report"]
|
|
check("pile length +5 mm -> G1 FAIL (pile length)", bool(job) and rep.get("job_id") == job["id"]
|
|
and rep["verdict"] == "FAIL" and "pile length" in rep["unplanned_fails"],
|
|
f"{rep['unplanned_fails']} trace {trace[:2]}..{trace[-2:]}")"""),
|
|
(""" check("report: other modelled marks match", md.count("| 일치 |") == 35, md.count("| 일치 |"))""",
|
|
""" sec6 = md.split("## 6.")[1].split("## 7.")[0]
|
|
check("report: other modelled marks match (35)", sec6.count("| 일치 |") == 35, sec6.count("| 일치 |"))"""),
|
|
(""" # 8 a wrong input gives a FAIL gate""",
|
|
""" # 7b a job left "실행 중" by a stopped process must not block the gate forever
|
|
jp = TD / P / "runs" / "current" / "G1_job.json"
|
|
store.wjson(jp, {"id": "orphan", "pid": -1, "state": "실행 중", "at": store.now(), "log": ["x"]})
|
|
r = c.get(f"/pier/{P}/gates")
|
|
check("orphaned running job shown as 중단됨", "중단됨: 이전 실행" in html(r))
|
|
# 8 a wrong input gives a FAIL gate"""),
|
|
])
|
|
print("patched")
|