pipeline: 원장 검사기를 문서 계약에 맞추고 터미널 증거 회귀를 잇는다
R2 — `stage-contracts.md` 「관문 요약」이 정본인데 `STAGES` 가 넷을 빠뜨리고 있었다. S4 `preview-figure.py` · S5 `check_evidence.mjs` · S6 `check_body.mjs`·`check_evidence.mjs` · S7 `build-tech-log-tree.py` 를 더한다. 그리고 `style_profile.mjs` 는 관문이 아니라 측정이다 — 문서 계약이 「error 0」을 붙인 것은 `check_prose` 뿐이다(`stage-contracts.md:178`·`:252`). 그런데 원장 검사기가 exit≠0 을 전부 error 로 세어서, 정직하게 exit 1 로 적은 원장은 무조건 실패하고 0 으로 고쳐 적으면 그건 지어낸 것이 된다. `MEASUREMENT_GATES` 로 갈라 **돌았다는 것만 요구하고 종료 코드 0 은 요구하지 않는다.** 대신 `exit` 칸이 없으면 error 다 — 안 돌리고 넘어가는 것을 막는다. 곁증명(`_side_proof`)에도 같은 규칙을 넣는다. 대조군 셋으로 확인했다. preview-figure 를 뺀 원장 → FAIL 「관문이 빠졌다」 style_profile 을 exit 1 로 적은 원장 → PASS style_profile 의 exit 칸을 지운 원장 → FAIL 「측정 관문의 종료 코드가 없다」 이 변경으로 `runs/virtualization/2026-09-08-1958/run.json` 이 빨개진다. S6 에서 `check_evidence.mjs` 를 실제로 안 돌린 원장이라 맞는 결과다. 낮춰서 초록으로 만들지 않는다. R10 — `scripts/terminal-evidence/tests/` 가 문서에 적힌 `unittest discover -s scripts/tests` 범위 밖이고, 직접 `discover` 를 걸면 `render_terminal` import 경로가 `sys.path` 에 없어 깨진다. 회귀가 초록인데 아무도 안 부르는 상태였다. `scripts/tests/test_terminal_evidence.py` 가 `load_tests` 로 경로를 얹고 끌어온다 (86 → 92). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wp9jNbePAmWc5jQwCYhK9v
This commit is contained in:
co-authored by
Claude Opus 5
parent
43e1aadef0
commit
c2742a66cc
@@ -0,0 +1,42 @@
|
||||
"""`scripts/terminal-evidence/tests/` 를 문서에 적힌 한 명령이 함께 돌게 잇는다.
|
||||
|
||||
`python3 -m unittest discover -s scripts/tests` 는 그 폴더만 훑는다. 터미널 증거 렌더러의
|
||||
테스트는 `scripts/terminal-evidence/tests/` 에 따로 있어서 그 명령에 안 잡혔고,
|
||||
`verify-pipeline.py` 도 unittest 를 부르지 않는다. 그래서 **초록인데 아무도 안 부르는 회귀**가 됐다.
|
||||
|
||||
`discover -s scripts/terminal-evidence/tests` 를 직접 돌려도 깨진다 — 그 테스트가
|
||||
`render_terminal` 을 같은 폴더 기준으로 부르는데 그 경로가 `sys.path` 에 없다. 여기서
|
||||
경로를 얹고 파일을 직접 불러 `load_tests` 로 끌어온다. 중첩 `discover` 는 쓰지 않는다 —
|
||||
`top_level_dir` 이 바깥을 가리키면 unittest 가 거절한다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_TE = os.path.join(os.path.dirname(_HERE), "terminal-evidence")
|
||||
_TE_TESTS = os.path.join(_TE, "tests")
|
||||
|
||||
|
||||
def load_tests(loader: unittest.TestLoader, tests: unittest.TestSuite,
|
||||
pattern: str | None) -> unittest.TestSuite:
|
||||
if not os.path.isdir(_TE_TESTS):
|
||||
return tests
|
||||
if _TE not in sys.path:
|
||||
sys.path.insert(0, _TE)
|
||||
for name in sorted(os.listdir(_TE_TESTS)):
|
||||
if not (name.startswith("test") and name.endswith(".py")):
|
||||
continue
|
||||
mod_name = "terminal_evidence_" + name[:-3]
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
mod_name, os.path.join(_TE_TESTS, name))
|
||||
if spec is None or spec.loader is None:
|
||||
continue
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[mod_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
tests.addTests(loader.loadTestsFromModule(module))
|
||||
return tests
|
||||
@@ -44,16 +44,29 @@ STAGES = {
|
||||
"gates": ["check_body.mjs", "check_prose.mjs", "check_evidence.mjs"],
|
||||
"skippable": False},
|
||||
"S4": {"skill": "technical-visualizer",
|
||||
"gates": ["lint", "check-figure-text.py", "check-figure-overlap.py"],
|
||||
"gates": ["lint", "check-figure-text.py", "check-figure-overlap.py",
|
||||
"preview-figure.py"],
|
||||
"skippable": True},
|
||||
# S5·S6 은 문장을 고친 뒤라 S3 관문을 다시 돈다 (stage-contracts.md 「관문 요약」)
|
||||
"S5": {"skill": "rewriting-technical-prose-naturally",
|
||||
"gates": ["check_prose.mjs", "style_profile.mjs", "check_body.mjs"],
|
||||
"gates": ["check_prose.mjs", "style_profile.mjs", "check_body.mjs",
|
||||
"check_evidence.mjs"],
|
||||
"skippable": False},
|
||||
"S6": {"skill": "writing-as-the-person-who-did-it",
|
||||
"gates": ["check_voice.mjs", "check_prose.mjs"], "skippable": False},
|
||||
"gates": ["check_voice.mjs", "check_prose.mjs", "check_body.mjs",
|
||||
"check_evidence.mjs"],
|
||||
"skippable": False},
|
||||
"S7": {"skill": "publishing-tech-log-to-studio",
|
||||
"gates": ["저장됨", "verify-tech-log-tree.py"], "skippable": True},
|
||||
"gates": ["저장됨", "build-tech-log-tree.py", "verify-tech-log-tree.py"],
|
||||
"skippable": True},
|
||||
}
|
||||
|
||||
# 측정 관문 — 돌았다는 것은 요구하지만 종료 코드 0 은 요구하지 않는다.
|
||||
# 문서 계약이 「error 0」을 붙인 것은 check_prose 뿐이고 style_profile 은 문체 수치를 보여 주는
|
||||
# 측정이다 (stage-contracts.md:178·:252). 여기에 0 을 요구하면 정직하게 적은 원장이 실패하고,
|
||||
# 0 으로 고쳐 적으면 그건 지어낸 것이 된다.
|
||||
MEASUREMENT_GATES = ("style_profile.mjs",)
|
||||
|
||||
ORDER = ["S1", "S2", "S3", "S4", "S5", "S6", "S7"]
|
||||
|
||||
|
||||
@@ -130,9 +143,14 @@ def _side_proof(rep: Report, st: dict, sid: str, spec: dict, where: str) -> None
|
||||
if not gates:
|
||||
rep.error("곁증명에 관문이 없다", f"{where} — {out}")
|
||||
for g in gates:
|
||||
cmd = str(g.get("cmd") or "")
|
||||
if any(tok in cmd for tok in MEASUREMENT_GATES):
|
||||
if g.get("exit") is None:
|
||||
rep.error("곁증명의 측정 관문에 종료 코드가 없다", f"{where} — {cmd[:70]}")
|
||||
continue
|
||||
if g.get("exit") not in (0, "0"):
|
||||
rep.error("곁증명의 관문이 통과하지 못했다",
|
||||
f"{where} — {str(g.get('cmd'))[:70]} → exit {g.get('exit')}")
|
||||
f"{where} — {cmd[:70]} → exit {g.get('exit')}")
|
||||
rep.facts.setdefault("곁증명", []).append(f"{sid}:{os.path.basename(out)}")
|
||||
|
||||
|
||||
@@ -214,9 +232,16 @@ def verify(path: str) -> Report:
|
||||
if token not in cmds:
|
||||
rep.error("관문이 빠졌다", f"{where} — {token}")
|
||||
for g in gates:
|
||||
cmd = str(g.get("cmd") or "")
|
||||
if any(tok in cmd for tok in MEASUREMENT_GATES):
|
||||
# 측정 관문 — 돌았는지만 본다. exit 칸이 아예 없으면 안 돌린 것이다
|
||||
if g.get("exit") is None:
|
||||
rep.error("측정 관문의 종료 코드가 없다",
|
||||
f"{where} — {cmd[:70]} — 돌렸다는 기록이 없다")
|
||||
continue
|
||||
if g.get("exit") not in (0, "0"):
|
||||
rep.error("관문이 통과하지 못했다",
|
||||
f"{where} — {str(g.get('cmd'))[:70]} → exit {g.get('exit')}")
|
||||
f"{where} — {cmd[:70]} → exit {g.get('exit')}")
|
||||
|
||||
for out in st.get("outputs") or []:
|
||||
if not os.path.exists(os.path.join(ROOT, out)):
|
||||
|
||||
Reference in New Issue
Block a user