Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions agents/critic/scripts/check_seal_timeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""회고적 사전등록 탐지 게이트 (Registry-Replay A2 → gate).

"sealed-forward"라 주장한 예측이 실제로는 결과보다 늦게 커밋됐는지를 git 커밋 시각으로 검증한다.
PITFALLS A2("봉인의 진위는 시각이 가른다") + Registry-Replay MISSED 3건 중 최우선 구멍을 메운다.
7종 CI 중 git 이력을 보는 게이트가 없어 이 사고를 못 잡았다 — 이 스크립트가 8번째 게이트다.

입력: seal manifest JSON
{"seals": [
{"claim": "폐 histology sealed-forward",
"prediction_commit": "<sha>", "result_commit": "<sha>"}, # 방식 A: 커밋 SHA
{"claim": "...", "prediction_path": "<file>", "result_path": "<file>"} # 방식 B: 파일 마지막 커밋
]}

판정: 각 seal에 대해 prediction 커밋 시각 < result 커밋 시각 이어야 통과.
prediction >= result 이면 회고적(retrospective)이므로 "sealed-forward" 주장 무효 → FAIL.
규약: 결정론(커밋 시각은 불변). 게이트를 점수 좋게 손대는 것 금지(A5 골대이동).
"""
import argparse, json, subprocess, sys
from pathlib import Path


def commit_ct(sha: str) -> int:
"""커밋 SHA의 committer timestamp(epoch). 실패 시 예외."""
out = subprocess.run(["git", "show", "-s", "--format=%ct", sha],
capture_output=True, text=True)
if out.returncode != 0 or not out.stdout.strip():
raise ValueError(f"커밋 시각 조회 실패: {sha} ({out.stderr.strip()})")
return int(out.stdout.strip().splitlines()[-1])


def path_last_ct(path: str) -> int:
"""파일 경로의 마지막 커밋 committer timestamp(epoch)."""
out = subprocess.run(["git", "log", "-1", "--format=%ct", "--", path],
capture_output=True, text=True)
if out.returncode != 0 or not out.stdout.strip():
raise ValueError(f"경로 커밋 시각 조회 실패: {path} ({out.stderr.strip()})")
return int(out.stdout.strip())


def resolve(seal: dict):
if "prediction_commit" in seal and "result_commit" in seal:
return commit_ct(seal["prediction_commit"]), commit_ct(seal["result_commit"])
if "prediction_path" in seal and "result_path" in seal:
return path_last_ct(seal["prediction_path"]), path_last_ct(seal["result_path"])
raise ValueError(f"seal에 prediction/result (commit 또는 path) 쌍이 없음: {seal.get('claim')}")


def main() -> int:
ap = argparse.ArgumentParser(description="회고적 사전등록 탐지 게이트")
ap.add_argument("manifest", help="seal manifest JSON")
ap.add_argument("--json", help="결과 JSON 출력 경로")
a = ap.parse_args()

data = json.loads(Path(a.manifest).read_text())
seals = data.get("seals", [])
if not seals:
print("[check_seal_timeline] seal 0건 — 검사 대상 없음(통과)")
return 0

results, failed = [], 0
for s in seals:
claim = s.get("claim", "(unnamed)")
try:
pred_ct, res_ct = resolve(s)
except ValueError as e:
print(f" ERROR {claim}: {e}")
results.append({"claim": claim, "verdict": "ERROR", "detail": str(e)})
failed += 1
continue
ok = pred_ct < res_ct
verdict = "SEALED-FORWARD" if ok else "RETROSPECTIVE"
if not ok:
failed += 1
print(f" {'PASS' if ok else 'FAIL'} {claim}: prediction_ct={pred_ct} "
f"{'<' if ok else '>='} result_ct={res_ct} → {verdict}")
results.append({"claim": claim, "prediction_ct": pred_ct,
"result_ct": res_ct, "verdict": verdict})

summary = {"n_seals": len(seals), "n_failed": failed,
"pass": failed == 0, "results": results}
if a.json:
Path(a.json).write_text(json.dumps(summary, ensure_ascii=False, indent=2))
if failed:
print(f"[check_seal_timeline] FAIL — {failed}/{len(seals)}건이 회고적(예측이 결과보다 늦음). "
f"'sealed-forward' 주장 무효.")
return 1
print(f"[check_seal_timeline] PASS — {len(seals)}건 모두 예측이 결과보다 먼저 커밋됨.")
return 0


if __name__ == "__main__":
sys.exit(main())
16 changes: 16 additions & 0 deletions agents/critic/seal_manifests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# seal_manifests — 회고적 사전등록 탐지

Registry-Replay(2026-08-20, `experiments/kkkim/20260820_registry_replay/`)가 드러낸 구멍 **A2**를 메우는 게이트의 입력.

- **게이트**: `agents/critic/scripts/check_seal_timeline.py`
- **자기검증**: `agents/critic/tests/test_seal_timeline.py` (비공허 mutation 테스트 — 정상 통과 + 회고적 검출 둘 다 확인)
- **판정**: "sealed-forward"라 주장한 예측이 결과보다 **먼저** 커밋됐는지 git 시각으로 검증. 늦었으면(회고적) FAIL.

## 왜
7종 CI 중 git 이력을 보는 게이트가 없어 A2("봉인의 진위는 시각이 가른다")를 못 잡았다. 결정론 게이트로 기계화 가능한 3건 중 분석-무결성 직결 최우선 구멍.

## CI 배선 전 필수 (공허 게이트 방지)
빈 매니페스트는 seal 0건이라 무조건 통과한다 = **공허 게이트**(우리가 금지한 anti-pattern). 따라서:
1. 먼저 실제 봉인 주장을 매니페스트에 채운다(SHA 또는 path). 근거 = `LAW_HELDOUT_SCOREBOARD.md`.
2. 채운 뒤에 `critic-validators.yml`에 8번째 스텝으로 배선(Critic=braveji 판단).
빈 채로 CI에 넣으면 "통과만 하는 게이트"라 이 게이트의 존재 이유와 모순된다.
13 changes: 13 additions & 0 deletions agents/critic/seal_manifests/TEMPLATE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"_doc": "회고적 사전등록 탐지용 seal manifest. check_seal_timeline.py 입력.",
"_rule": "각 seal: prediction 커밋 시각 < result 커밋 시각 이어야 sealed-forward. 위반 시 게이트 FAIL.",
"_how": "prediction/result_commit = git SHA, 또는 prediction/result_path = 파일(마지막 커밋 시각).",
"_populate": "실제 봉인 주장(예: 교차암 폐·위·두경부 sealed-forward)을 여기 채운다. 근거 타임라인 = experiments/crosscancer/LAW_HELDOUT_SCOREBOARD.md('폐 예측 07-12 05:06 < 결과 15:35' 등).",
"seals": [
{
"claim": "(예시) 폐 histology sealed-forward",
"prediction_commit": "<예측 커밋 SHA>",
"result_commit": "<결과 커밋 SHA>"
}
]
}
18 changes: 18 additions & 0 deletions agents/critic/seal_manifests/VALIDATION_2026-08-20.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# A2 게이트 실데이터 검증 (2026-08-20)

Registry-Replay가 MISSED로 지목한 A2(회고적 사전등록)를 게이트로 만든 뒤, **실제 교차암 커밋에 돌려** 검증했다. 근거 타임라인 = `experiments/crosscancer/LAW_HELDOUT_SCOREBOARD.md` §인식론 구분.

## 결과 — 게이트가 sealed-forward와 retrospective를 git 시각만으로 정확히 구분

| 코호트 | 예측(봉인) 커밋 | 결과 커밋 | 게이트 판정 | 스코어보드 라벨 |
|---|---|---|---|---|
| 폐 | 77c0633 (07-12 05:06) | 994b187 (07-12 15:35) | ✅ SEALED-FORWARD | sealed ✓일치 |
| 위 | b5b0088 (07-12 05:18) | 2760fb8 (07-12 16:22) | ✅ SEALED-FORWARD | sealed ✓일치 |
| 두경부 | b5b0088 (07-12 05:18) | 28eb0af (07-13 20:36) | ✅ SEALED-FORWARD | sealed ✓일치 |
| **대장(데모)** | 77c0633 (07-12 05:06) | **afedc6a (07-12 04:45)** | ❌ **RETROSPECTIVE** | **retrospective ✓일치** |

**핵심**: 게이트가 사람이 손으로 붙인 라벨(스코어보드의 정직한 sealed/retrospective 구분)을 **git 커밋 시각만으로 독립 재현**했다. 폐·위·두경부 봉인 주장은 실증됐고, 대장은 정확히 회고적으로 걸린다(스코어보드가 이미 정직하게 회고적이라 라벨한 것과 일치).

- 적용(enforcement) 매니페스트 = `crosscancer_seals.json`(폐·위·두경부 3건 = sealed-forward 주장 → 전부 PASS).
- 대장은 sealed-forward로 **주장하지 않으므로** enforcement에 넣지 않는다(정직 라벨 그대로). 위 표의 대장은 게이트가 회고적을 잡는지 보이는 **데모**.
- 규율: 게이트 튜닝 없음, 커밋 시각은 불변(결정론). incident(A2)→gate→실데이터 검증 루프 완결.
21 changes: 21 additions & 0 deletions agents/critic/seal_manifests/crosscancer_seals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"_doc": "교차암 sealed-forward 봉인 검증 (A2 게이트 실데이터). 근거 LAW_HELDOUT_SCOREBOARD.md.",
"_rule": "예측(사전등록 봉인) 커밋이 결과(mil_cost_results.json) 커밋보다 먼저여야 sealed-forward.",
"seals": [
{
"claim": "폐 held-out sealed-forward",
"prediction_commit": "77c063306209771a6655ec18667dc1e6091ad3e7",
"result_commit": "994b18734d89eb0ab867bd59ddd42c39c00d4bb2"
},
{
"claim": "위 held-out sealed-forward",
"prediction_commit": "b5b0088cd5257db75f384cd3f9e99b7fb7d3fcb6",
"result_commit": "2760fb8af9175dbba2d0ce04d8dd0ea041767558"
},
{
"claim": "두경부 held-out sealed-forward",
"prediction_commit": "b5b0088cd5257db75f384cd3f9e99b7fb7d3fcb6",
"result_commit": "28eb0af3ba043308eb5b0f453eb56afea87f6b7f"
}
]
}
65 changes: 65 additions & 0 deletions agents/critic/tests/test_seal_timeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""check_seal_timeline 게이트 자기검증 (비공허 mutation 테스트).

게이트가 "통과만 하는" 공허한 게이트가 아님을 증명한다:
- 정상(예측이 결과보다 먼저 커밋) → PASS(exit 0)
- mutant(예측이 결과보다 늦게 = 회고적) → FAIL(exit 1)
둘 다 기대대로여야 이 테스트가 통과한다. (critic-validators.yml의 게이트 mutation 규율)

임시 git 저장소를 만들어 커밋 시각을 GIT_COMMITTER_DATE로 통제한다 — 결정론·자기완결.
"""
import json, os, subprocess, sys, tempfile
from pathlib import Path

GATE = Path(__file__).resolve().parents[1] / "scripts" / "check_seal_timeline.py"


def run(cmd, cwd, env=None):
return subprocess.run(cmd, cwd=cwd, env=env, capture_output=True, text=True)


def git_commit(repo, fname, when_epoch, env0):
(Path(repo) / fname).write_text(fname + "\n")
run(["git", "add", fname], repo, env0)
env = dict(env0)
env["GIT_AUTHOR_DATE"] = f"{when_epoch} +0000"
env["GIT_COMMITTER_DATE"] = f"{when_epoch} +0000"
run(["git", "commit", "-m", f"add {fname}"], repo, env)
sha = run(["git", "rev-parse", "HEAD"], repo, env0).stdout.strip()
return sha


def main() -> int:
with tempfile.TemporaryDirectory() as repo:
env0 = dict(os.environ)
env0["GIT_AUTHOR_NAME"] = env0["GIT_COMMITTER_NAME"] = "test"
env0["GIT_AUTHOR_EMAIL"] = env0["GIT_COMMITTER_EMAIL"] = "test@test"
run(["git", "init", "-q"], repo, env0)
sha_pred = git_commit(repo, "prediction.txt", 1_700_000_000, env0) # 먼저
sha_res = git_commit(repo, "result.txt", 1_700_009_999, env0) # 나중

ok_manifest = Path(repo) / "ok.json"
ok_manifest.write_text(json.dumps({"seals": [
{"claim": "정상 sealed-forward", "prediction_commit": sha_pred, "result_commit": sha_res}]}))
mut_manifest = Path(repo) / "mut.json"
mut_manifest.write_text(json.dumps({"seals": [
{"claim": "회고적 mutant", "prediction_commit": sha_res, "result_commit": sha_pred}]}))

r_ok = run([sys.executable, str(GATE), str(ok_manifest)], repo, env0)
r_mut = run([sys.executable, str(GATE), str(mut_manifest)], repo, env0)

pass_ok = r_ok.returncode == 0
fail_mut = r_mut.returncode == 1
print("정상 케이스 exit", r_ok.returncode, "(기대 0):", "OK" if pass_ok else "실패")
print("mutant 케이스 exit", r_mut.returncode, "(기대 1):", "OK" if fail_mut else "실패")
if pass_ok and fail_mut:
print("✅ 게이트 비공허 확인 — 정상 통과 + 회고적 검출")
return 0
print("❌ 게이트가 공허하거나 오작동")
print(" ok stdout:", r_ok.stdout.strip()[:200])
print(" mut stdout:", r_mut.stdout.strip()[:200])
return 1


if __name__ == "__main__":
sys.exit(main())
Loading