diff --git a/agents/critic/scripts/check_result_coverage.py b/agents/critic/scripts/check_result_coverage.py new file mode 100644 index 0000000..ed8188a --- /dev/null +++ b/agents/critic/scripts/check_result_coverage.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +"""결과 산출물 커버리지 게이트 — 원고에 반영되지 않은 실험 결과를 찾는다. + +배경: CPTAC 외부검증 결과 5종이 분석 완료 상태로 원고에 한 번도 반영되지 않은 +채 두 달이 지났다(PITFALLS_REGISTRY A9·C11). 기존 게이트는 '원고에 실린 것'의 +타당성만 검증하고 '실려야 하는데 안 실린 것'은 보지 않는다. 이 스크립트가 그 +구멍을 메운다. + +판정 방식 + 1. experiments/ 아래 결과 산출물을 열거한다. + 2. 각 산출물이 원고에서 참조되는지 두 경로로 확인한다. + (a) 경로 참조 — 주석이나 본문에 파일명·디렉터리명이 등장 + (b) 수치 참조 — 산출물의 대표 수치가 원고 본문에 등장 + 3. 어느 쪽에도 안 걸리면 results/EXCLUDED.md 에 제외 사유가 있는지 본다. + 4. 남은 것 = 미분류. 미분류가 0이어야 제출 가능하다. + +사용 + python3 agents/critic/scripts/check_result_coverage.py + python3 agents/critic/scripts/check_result_coverage.py --manuscript manuscript/DRAFT_ML4H_v2_full.md + python3 agents/critic/scripts/check_result_coverage.py --json # 기계 판독용 + +종료 코드: 미분류 0이면 0, 있으면 1. +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +# 결과 산출물로 볼 파일 이름·패턴 +ARTIFACT_NAMES = {"metrics.json", "ext_eval_summary.json"} +ARTIFACT_GLOBS = ["**/*_summary.json", "**/*_results.json", "**/*_eval*.json"] + +# 결과가 아니라 운영·설정에 해당해 커버리지 대상에서 뺀다 +SKIP_PARTS = { + "_cache", "__pycache__", ".ipynb_checkpoints", "logs", "queue", + "config", "configs", "manifest", "manifests", +} +SKIP_NAME_RE = re.compile( + r"(config|manifest|queue|shard|DONE|watchdog|split_policy|registry\.jsonl)", re.I +) + +# EXCLUDED.md 에서 '제외 확정'으로 인정할 태그. 이 태그가 붙은 줄에 경로가 +# 적혀 있어야만 제외로 센다. +EXCLUDED_TAG = "[제외확정]" + +# 대표 수치로 쓸 키 (성능 지표) +METRIC_KEYS = ( + "auc", "auroc", "ext_auc", "ext_auprc", "auprc", + "balanced_accuracy", "ext_balanced_accuracy", "macro_f1", "f1", +) + + +def find_repo_root(start: Path) -> Path: + cur = start.resolve() + for cand in [cur, *cur.parents]: + if (cand / ".git").exists() and (cand / "experiments").is_dir(): + return cand + return cur + + +def collect_artifacts(root: Path) -> list[Path]: + exp = root / "experiments" + if not exp.is_dir(): + return [] + found: set[Path] = set() + for p in exp.rglob("*.json"): + if any(part in SKIP_PARTS for part in p.parts): + continue + if SKIP_NAME_RE.search(p.name): + continue + if p.name in ARTIFACT_NAMES: + found.add(p) + continue + for pat in ARTIFACT_GLOBS: + if p.match(pat): + found.add(p) + break + return sorted(found) + + +def extract_metrics(path: Path) -> dict[str, float]: + """산출물에서 대표 수치를 뽑는다. 중첩 dict 까지 훑는다.""" + out: dict[str, float] = {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + except Exception: + return out + + def walk(obj, prefix=""): + if isinstance(obj, dict): + for k, v in obj.items(): + key = f"{prefix}{k}" + if isinstance(v, (int, float)) and not isinstance(v, bool): + if any(m in k.lower() for m in METRIC_KEYS): + out[key] = float(v) + else: + walk(v, prefix=f"{key}.") + elif isinstance(obj, list): + for item in obj[:20]: + walk(item, prefix=prefix) + + walk(data) + return out + + +def number_variants(val: float) -> list[str]: + """원고에 적힐 법한 표기 변형. + + 소수 넷째·셋째 자리만 쓴다. 둘째 자리(0.53 등)는 우연 일치가 너무 많아 + 커버리지 근거로 못 쓴다. 실제로 초판에서 2자리 매칭 때문에 미반영 + 산출물이 전부 '반영됨'으로 통과했다. + """ + variants = [] + for nd in (4, 3): + s = f"{val:.{nd}f}" + if s not in variants: + variants.append(s) + return variants + + +def load_manuscript(root: Path, explicit: list[str] | None) -> tuple[str, list[Path]]: + if explicit: + paths = [root / p for p in explicit] + else: + mdir = root / "manuscript" + paths = sorted(mdir.glob("DRAFT_*full*.md")) if mdir.is_dir() else [] + text_parts, used = [], [] + for p in paths: + if p.is_file(): + text_parts.append(p.read_text(encoding="utf-8", errors="replace")) + used.append(p) + return "\n".join(text_parts), used + + +def load_excluded(root: Path) -> tuple[str, Path | None]: + for cand in (root / "results" / "EXCLUDED.md", root / "EXCLUDED.md"): + if cand.is_file(): + return cand.read_text(encoding="utf-8", errors="replace"), cand + return "", None + + +def classify(art: Path, root: Path, manu: str, excluded: str) -> dict: + rel = art.relative_to(root).as_posix() + parent = art.parent.name + + # 경로 토큰은 '구체적인' 것만 쓴다. 조부모 디렉터리는 팀원 핸들(sjpark 등)이라 + # 원고 본문에 그대로 등장해 전부 매칭시켜 버린다(초판 실패 원인). + def specific(tok: str) -> bool: + return bool(tok) and len(tok) >= 8 and ("_" in tok or "/" in tok) + + path_hit = None + for token in (rel, parent): + if specific(token) and token in manu: + path_hit = token + break + + metrics = extract_metrics(art) + hits, misses = [], [] + for key, val in metrics.items(): + if val == 0 or abs(val) > 1e6: + continue + matched = next((v for v in number_variants(val) if v in manu), None) + if matched: + hits.append(f"{key}={val:g} (원고 '{matched}')") + else: + misses.append(f"{key}={val:g}") + + # 제외 인정은 `[제외확정]` 태그가 붙은 줄에서만. 태그를 요구하지 않으면 + # EXCLUDED.md 에 '반영 예정'으로 적어 둔 항목까지 제외로 읽혀, 이 게이트가 + # 스스로를 무력화한다. + exc_hit = None + if excluded: + for line in excluded.splitlines(): + if EXCLUDED_TAG not in line: + continue + for token in (rel, parent): + if specific(token) and token in line: + exc_hit = token + break + if exc_hit: + break + + if path_hit and not misses: + status = "반영됨" + elif hits and misses: + status = "부분반영" + elif hits or path_hit: + status = "반영됨" + elif exc_hit: + status = "의도적 제외" + else: + status = "미분류" + + return { + "path": rel, + "status": status, + "path_hit": path_hit, + "metric_hits": hits, + "metric_misses": misses, + "excluded_hit": exc_hit, + } + + +def main() -> int: + ap = argparse.ArgumentParser(description="결과 산출물 커버리지 게이트") + ap.add_argument("--root", default=None, help="저장소 루트 (기본: 자동 탐지)") + ap.add_argument("--manuscript", action="append", default=None, + help="검사할 원고 파일 (반복 지정 가능, 기본: manuscript/DRAFT_*full*.md)") + ap.add_argument("--json", action="store_true", help="JSON 으로 출력") + args = ap.parse_args() + + root = Path(args.root).resolve() if args.root else find_repo_root(Path(__file__).parent) + manu, manu_files = load_manuscript(root, args.manuscript) + if not manu_files: + print("[오류] 원고 파일을 찾지 못했습니다. --manuscript 로 지정하십시오.", file=sys.stderr) + return 2 + excluded, exc_path = load_excluded(root) + + artifacts = collect_artifacts(root) + rows = [classify(a, root, manu, excluded) for a in artifacts] + + counts = {"반영됨": 0, "부분반영": 0, "의도적 제외": 0, "미분류": 0} + for r in rows: + counts[r["status"]] += 1 + + if args.json: + print(json.dumps({"root": str(root), "counts": counts, "rows": rows}, + ensure_ascii=False, indent=2)) + return 0 if counts["미분류"] == 0 else 1 + + print("=" * 72) + print("결과 산출물 커버리지 리포트") + print("=" * 72) + print(f"저장소 : {root}") + print(f"원고 : {', '.join(p.relative_to(root).as_posix() for p in manu_files)}") + print(f"제외목록 : {exc_path.relative_to(root).as_posix() if exc_path else '(없음 — results/EXCLUDED.md 미생성)'}") + print(f"산출물 : {len(artifacts)}건") + print() + print(f" 반영됨 {counts['반영됨']:3d}") + print(f" 부분반영 {counts['부분반영']:3d} <- 일부 수치만 원고에 있음, 확인 요망") + print(f" 의도적 제외 {counts['의도적 제외']:3d}") + print(f" 미분류 {counts['미분류']:3d} <- 제출 전 0 이어야 함") + print() + + unclassified = [r for r in rows if r["status"] == "미분류"] + if unclassified: + print("-" * 72) + print("미분류 (원고에 흔적 없음, 제외 사유도 없음)") + print("-" * 72) + for r in unclassified: + print(f"\n {r['path']}") + if r["metric_misses"]: + print(f" 미반영 수치: {', '.join(r['metric_misses'][:6])}") + else: + print(" (성능 지표 키 없음)") + + partial = [r for r in rows if r["status"] == "부분반영"] + if partial: + print() + print("-" * 72) + print("부분반영 (같은 파일 안에서 쓰인 수치와 안 쓰인 수치가 갈림)") + print("-" * 72) + for r in partial: + print(f"\n {r['path']}") + print(f" 원고에 있음 : {', '.join(r['metric_hits'][:4])}") + print(f" 원고에 없음 : {', '.join(r['metric_misses'][:6])}") + + if unclassified or partial: + print() + print(" 조치: 원고에 반영하거나, results/EXCLUDED.md 에") + print(" 제외 사유·근거 등급(E1/E2/E3)·재검토 조건을 적으십시오.") + else: + print("미분류·부분반영 0건 — 커버리지 게이트 통과.") + + print() + return 0 if counts["미분류"] == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/PITFALLS_REGISTRY.md b/docs/PITFALLS_REGISTRY.md index 419b617..438ccc1 100644 --- a/docs/PITFALLS_REGISTRY.md +++ b/docs/PITFALLS_REGISTRY.md @@ -36,6 +36,14 @@ | C5 | 07-24 | C | **C2 재발**: "진행사항/할일" 물음에 git·JIRA만 보고 **SESSION_LOG·HANDOFF를 안 읽어** 이미 끝난 ①(cost CI 환자단위)을 "지금 할 일"로 제안, 이미 승인된 ②(게이트 기준 재정의)를 "결정 필요"로 오보. 사용자가 "session도 확인하랬는데" 지적 | 내가 쓴 C2 장치(중복 확인)를 **내가 안 지킴** → 진짜 장치는 "**'진행사항/할일/앞으로 뭐' 물음엔 git·JIRA보다 HANDOFF 최상단 + SESSION_LOG 최근일을 *먼저* 읽는다**". `/bg`·상태 보고 절차에 이 순서를 명문화 | | C6 | 07-26 | C | 결정·수치가 번복됐는데 **원 댓글은 옛 내용 그대로** 남아 남을 헷갈림(뒤집힌 B결정 11440·옛 다중FM값 11363·끝난 병합봉인 11152·대체된 split v1 10469) | **결정·수치를 번복하면 그 자리에서 원 JIRA 댓글에 배너**(폐기/정정/종료/대체 + 대체 코멘트·정본 링크)를 단다. 삭제 아님(기록 보존). PUT `/rest/api/3/issue/{k}/comment/{id}` | | C7 | 08-04 | C | '내 할 일/보고'를 **키 범위(BIOP02-104~118)로 훑어** 신규 배정 카드 BIOP02-124(게재 blocker) 놓침. 사용자 3회 지적(assignee만도 부족·댓글 멘션·신규까지) | 정본 = **JQL 전수 조회**: `project=BIOP02 AND statusCategory!=Done AND (assignee=me OR comment~me(멘션) OR text~me)`. 키 범위·기억으로 훑지 않는다(C2/C5와 동류 — '없음' 주장 전 실물 쿼리) | +| A9 | 09-02 | A | **초록만 보고(E2) 스쿱 판정** → CPTAC 외부검증 결과 5종을 원고에서 제외. 전문 확보하니 상대는 환자 층화 MCCV뿐(기관 미통제)이고 AUROC를 보고조차 안 해 **같은 실험이 아니었음** — 전제가 무효인데 두 달간 재검토 없음 | **근거 등급 E1/E2/E3 표기 의무화**. E2로는 결과 제외·논문 재설계·투고처 변경 **불가**(잠정 판정만). E2 판정에는 전문 확보 티켓+만료일 동반. 제외 결과는 `results/EXCLUDED.md`에 사유·등급·재검토 조건 기록 | +| T8 | 09-02 | T | 결과 JSON의 `criterion`·`purpose` 문자열이 `(5 seed, ddof=1)`인데 실제 `shuffle_seeds`는 **20개** — 라벨과 데이터 불일치로 판정 근거 오독 위험(BIOP02-123) | 게이트에 **라벨↔배열 길이 정합 검사** 추가. 정정 시 수치 불변을 파싱 대조로 증명(PR #153: 3파일 6줄만 변경, 수치 219개 전부 동일 확인) | +| C8 | 09-02 | C | **정본은 완료인데 헤더 배너가 미완으로 낡아** 5주 정지(BIOP02-70: §9 서명 07-11·07-13 완료 ↔ L3 배너 "cross-sign pending"). 같은 유형이 동시 관측 — BIOP02-151(draft가 폐기된 §10 참조), BIOP01-83(원장은 미점검, 실제는 08-29 점검 완료) | C6의 역방향임(C6=번복 후 원 댓글 방치, C8=완료 후 배너 방치). **stale 스캐너**를 게이트에 추가: 배너·헤더 상태 문구 ↔ 본문 서명표 정합, draft 참조 섹션 번호의 현행성. 상태를 두 곳에 적었으면 한 곳은 반드시 생성물로 만들 것 | +| C9 | 09-02 | C | **결정이 실행 슬롯 없이 증발**: `PROGRESS_DECISIONS.md` L115 "CPTAC은 별도 §4로 격리"가 `SECTION_ASSIGNMENT_paperC.md`에 슬롯으로 안 만들어져, 8월 풀드래프트 집필 때 넣을 자리가 없었음. 결정은 실험 로그에만 남고 원고 골격에 도달 못함 | 결정 레코드에 **반영처·반영 오너·기한**을 필수 필드로. 세 칸이 비면 결정으로 인정하지 않음. 산출물에 슬롯을 만드는 것까지가 결정의 완료 조건 | +| C10 | 09-02 | C | **담당자는 잔여 없음, 잔여 작업자는 미배정** → 3개월 반 orphan(BIOP02-70). jamie가 07-13에 "내 범위 완료, 추가 액션 없음"을 정확히 보고했으나 재배정이 없어 티켓이 주인 없이 뜸. 실제 잔여는 braveji(critic_status caution→pass 3건)·sjpark 몫 | **본인 범위 완료 보고 시 그 자리에서 재배정하거나 티켓 분할.** "내 몫 끝, 나머지는 남 소관"으로 종료 금지. `/mine` 관문에 "assignee에게 실제 잔여가 있는가" 추가 | +| C11 | 09-02 | C | **검수가 '실린 것'만 보고 '빠진 것'을 안 봄** — Critic 7-point는 원고에 실린 결과의 타당성만 검증해, 산출물 5종(CPTAC)이 통째로 누락된 상태가 어떤 게이트에도 안 걸림 | **산출물 커버리지 리포트를 제출 게이트로**: 실험 산출물 중 원고 미반영분을 자동 목록화 → `의도적 제외(EXCLUDED.md 근거)` / `미분류`로 분류, **미분류 0이어야 제출**. Critic 체크리스트에 "실려야 하는데 안 실린 결과" 항목 추가 | +| G6 | 09-02 | G | **서버 반납 반출에서 101.7G 유실.** ① brca 71G를 "이건규 Drive에 중복"으로 판단해 **일부러 제외**했으나 `/home`(컨테이너 로컬)과 `/workspace`(공유)는 별개라 중복이 아니었음 ② crosscancer·yale_raw 는 rsync 가 code 12·255 로 끊겼는데 스크립트가 **exit code 를 안 보고 DONE 을 찍음**. 확보율 brca 4.7%·crosscancer 41.5%·bp01data 65.5% | ①은 A9 동형(미확인 전제로 되돌리기 어려운 결정) — **중복 주장은 경로를 열어 확인한 뒤에만**. ②는 전역 하드룰 3번(응답코드≠성공) — **전송 스크립트는 rsync 종료코드를 검사하고 실패 시 DONE 금지**, 완료 판정은 수신측 파일 수·크기 대조로. 회수 요청 = BIOP02-146 #12115 | +| C12 | 09-02 | C | **손실 대조표를 만들어 놓고 원장에 안 올려 아무도 안 봄.** 09-01 반출 세션이 서버 인벤토리 58,464파일과 크기까지 대조한 `LOSS_REPORT.md` 를 만들었으나 TODO·HANDOFF 어디에도 안 적어, 09-02 `/mine` 이 백업 폴더를 실측하기 전까지 유실 사실 자체가 묻혀 있었음 | C11(검수가 누락을 안 봄)의 쌍둥이 — **산출물을 만드는 것과 그것이 읽히게 하는 것은 다른 일**이다. 손실·갭 보고서는 만든 그 턴에 원장 최상단에 한 줄로 올린다. 시한부 항목(볼륨 생존 중에만 회수 가능)은 🔴🔴 로 표시 | ## 상세 (표에 안 담기는 맥락) @@ -56,5 +64,25 @@ ### C2·C3 — 병렬 세션 환경의 기본 실수 여러 창이 동시에 도는 환경에서 내 세션의 기억은 **최신이 아니다**. 착수 전 실측(파일 존재·커밋 로그·전 브랜치 grep)이 없으면 이미 끝난 일을 다시 하거나, 남이 잘하고 있는 걸 고치려 든다. **교훈: "내 기억"이 아니라 "현재 파일"이 근거다.** + +### A9·C11 — 맞는 것을 싣지 않은 사고 (2026-09-02, CPTAC) +`experiments/sjpark/*/ext_eval_summary.json` 5건(ER 0.894·PR 0.778·HER2 0.530·PAM50 5class 0.722·4class 0.818, 전부 `test_split: cptac_external`)이 실재하는데 `manuscript/` 14개 파일과 git 전 이력에서 "CPTAC" 문자열이 **0건**이었다. 빠진 게 아니라 처음부터 안 들어갔다. +직접 원인은 2026-07-10 스쿱 판정이다(`research/paperA-positioning/2026-07-10_novelty-scoop-analysis.md` L12·L49·L185: "TCGA→CPTAC 외부검증은 이미 표준·출판된 설계 … 논문 본체로 삼으면 안 됨, 한 줄 인용 후 폐기"). 그런데 그 판정의 근거는 **초록 한 편**이었고, `paper-info.yaml`에 "표-단위 정확 AUC는 Springer paywall로 미확보"라고 스스로 적어 두었다. **한계를 알고 적었는데도 결정의 무게가 제한되지 않았다.** +2026-09-02 Europe PMC(PMC13269319, CC-BY)에서 전문을 확보해 확인하니 전제가 무효였다. 상대는 patient-stratified MCCV 10 random splits만 쓰고 기관을 통제하지 않았으며(`tissue source site`·`submitter`·`batch`·`scanner` 전부 0회), **AUROC를 아예 보고하지 않는다**(macro-F1·PR-AUC만). 같은 설계로 같은 결과를 낸 것이 아니었다. +→ **교훈 둘.** ① 근거 등급과 결정의 무게를 맞춰야 한다. 초록으로 내릴 수 있는 결정과 전문으로 내릴 수 있는 결정이 다르다(A5·A6과 동류 — "적혀 있으면 열어서 확인한다"의 문헌판). ② 현행 검수는 **실린 것의 타당성만** 본다. 누락은 어떤 게이트에도 안 걸린다. 커버리지 리포트가 없으면 같은 일이 또 난다. +상세 제안 = `docs/PROCESS_PROPOSAL_2026-09-02_evidence-grade-and-coverage.md`. + +### C8 — 상태를 두 곳에 적으면 한 곳은 반드시 낡는다 +BIOP02-70은 §9 서명이 2026-07-11(kkkim)·07-13(braveji)에 이미 끝나 있었는데, 같은 파일 L3 배너와 L142 표가 "Critic cross-sign pending"으로 남아 있었다. 07-13에 그 배너를 읽은 사람이 "진짜 블로커는 §9 sign-off"라고 보고했고, 08-18에 실측으로 정정될 때까지 **5주간 이미 끝난 일을 미완으로 알고 멈춰 있었다.** +같은 세션에서 동형 3건이 더 나왔다. BIOP02-151(draft가 2026-08-20에 폐기된 §10을 참조해 PAM50을 "미해결"로 표기), BIOP01-83(박세진 08-29 점검 완료를 원장이 "미점검"으로 유지 → 대외 회신에 사실오류 발생), BIOP02-123(20 seed 데이터에 "5 seed" 라벨). +→ **교훈: 요약·배너·원장은 정본의 복사본이고, 복사본은 반드시 낡는다.** 사람 손으로 동기화하지 말고 생성물로 만들거나 정합 검사를 게이트에 건다. C6(번복 후 원 댓글 방치)의 역방향이라 둘을 같이 봐야 한다. + + +### G6·C12 — 반출은 절반만 성공했고, 그 사실이 묻혀 있었다 (2026-09-02) +GPU 서버 반납 직전 맥으로 188G·14,723파일을 받았는데, 같은 폴더의 `LOSS_REPORT.md` 가 **확보율 brca 4.7% · crosscancer 41.5% · bp01data 65.5%** 를 적고 있었다. 손실 합계 146G, 그중 GPU 없이 재생성 불가능한 임베딩이 101.7G다. +원인이 둘이고 성격이 다르다. **brca 72.8G 는 의도적 제외였다** — `pull_rest.log` 02:16:51 에 "multifm_archive(brca 71G)=이건규 Drive 중복이라 제외"라고 적혀 있다. 그런데 그 경로는 `/home/kkkim/data/` 이고 상대가 백업한 것은 `/workspace` 라, CLAUDE.md 가 명시한 컨테이너 격리 때문에 중복이 아니었다. **crosscancer 28.9G 는 전송 실패였다** — rsync 가 code 12 로 끊겼는데 스크립트가 그 다음 줄에 `DONE` 을 찍고 `ALL DONE` 으로 마감했다. +→ **교훈: 반출은 보낸 쪽 로그가 아니라 받은 쪽 파일 수·크기로 판정한다.** 그리고 "다른 곳에 있으니 안 받아도 된다"는 판단은 그 다른 곳을 열어 확인한 뒤에만 한다(A9·A6 동류). +더 아픈 것은 세 번째다. **대조표는 지시대로 만들어졌다.** 만들고도 원장에 안 올려 하루 넘게 아무도 몰랐고, 그 사이 컨테이너가 정지됐다. 다행히 `docker stop` 이라 볼륨이 남아 회수 요청이 가능했지만(BIOP02-146 #12115), 볼륨까지 지워졌으면 끝이었다. **갭 보고서는 만든 그 턴에 원장 최상단으로 올린다.** + --- 갱신 규칙: 새 실수가 생기면 **그 세션에** 표 한 줄을 추가한다(나중에 몰아 쓰면 맥락이 날아간다). diff --git a/docs/PROCESS_PROPOSAL_2026-09-02_evidence-grade-and-coverage.md b/docs/PROCESS_PROPOSAL_2026-09-02_evidence-grade-and-coverage.md new file mode 100644 index 0000000..5677220 --- /dev/null +++ b/docs/PROCESS_PROPOSAL_2026-09-02_evidence-grade-and-coverage.md @@ -0,0 +1,131 @@ +# 제안 — 근거 등급과 산출물 커버리지 게이트 (2026-09-02) + +> 배경 사건: CPTAC 외부검증 결과 5종이 **분석은 끝났는데 원고에 한 번도 들어가지 않은 채** 두 달이 지남. 원인을 추적하니 단발 실수가 아니라 세 가지 구조적 결함이었고, 같은 유형이 이 프로젝트에서 반복 관측됨. +> 대상: BIOP01·BIOP02 공통. 상세 사례는 `docs/PITFALLS_REGISTRY.md` A9·T8·C8·C9·C10 참조. +> 작성 = kkkim. 검토 요청 = braveji(Critic 총괄)·이건규(주저자). + +--- + +## 1. 무슨 일이 있었나 + +`experiments/sjpark/*/ext_eval_summary.json` 5건이 실재함. ER 0.894(n=387), PR 0.778(n=375), HER2 0.530(n=294), PAM50 5-class 0.722(n=395), PAM50 4-class 0.818(n=382). 전부 `test_split: cptac_external`. + +그런데 `manuscript/` 14개 파일과 git 전 이력에서 "CPTAC" 문자열이 **0건**임. 빠진 것이 아니라 처음부터 들어간 적이 없음. + +원인은 셋임. + +**(1) 확인되지 않은 근거로 되돌리기 어려운 결정을 내림.** 2026-07-10 스쿱 판정이 Fernandez-Romero 2026의 **초록 한 편**만 보고 내려졌고, 그 판정이 "논문 본체로 삼으면 안 됨, 한 줄 인용 후 폐기"라는 실행으로 이어짐. `paper-info.yaml`에 "표-단위 정확 AUC는 Springer paywall로 미확보"라고 스스로 적어 두었으나, 그 한계가 결정의 무게를 제한하지 못함. 2026-09-02 전문 확보 후 확인하니 상대는 **환자 층화 MCCV**만 쓰고 기관을 통제하지 않았으며 AUROC를 보고하지도 않음. 같은 실험이 아니었음. + +**(2) 결정이 실행 경로로 옮겨지지 않고 증발함.** `experiments/crosscancer/PROGRESS_DECISIONS.md` L115에 "유방 CPTAC 외부전이 열화는 별도 §4로 격리"라고 적혔으나, `manuscript/SECTION_ASSIGNMENT_paperC.md`에 §4 슬롯이 만들어지지 않음. 결정은 실험 로그에만 남고 원고 골격에는 도달하지 못함. + +**(3) 검수가 "실린 것"만 보고 "빠진 것"을 보지 않음.** 현행 Critic 7-point는 원고에 실린 결과의 타당성을 검증함. 산출물 중 원고에 반영되지 않은 것을 점검하는 항목이 없음. 그래서 결과 5종이 통째로 누락된 상태가 어떤 게이트에도 걸리지 않음. + +--- + +## 2. 같은 유형의 반복 관측 + +이 세션 한 번의 점검에서만 아래가 나옴. 전부 "정본은 맞는데 요약·표기가 낡아 사람이 잘못 읽음"에 해당함. + +| 건 | 정본 | 낡은 표기 | 대가 | +|---|---|---|---| +| BIOP02-70 | §9 서명 2026-07-11·07-13 완료 | 헤더 배너 L3 "Critic cross-sign pending" | **5주 정지** | +| BIOP02-151 | §10.1(2026-08-20)로 PAM50 정본 확정 | draft가 폐기된 §10을 참조해 "미해결" | 결정된 사안이 미결로 재상정 | +| BIOP02-123 | `shuffle_seeds` 20개 | `criterion` 문자열 "(5 seed, ddof=1)" | 판정 근거 오독 위험 | +| BIOP01-83 | 박세진 2026-08-29 점검 완료 | 원장·HANDOFF는 "미점검" | 대외 회신에 사실오류 발생 | +| BIOP01-25 | draft_v2 magnitude 정본 | FINDINGS가 signed를 magnitude로 오라벨 | 열흘 방치 (기존 등재) | + +담당 공백도 같이 관측됨. BIOP02-70은 assignee(jamie)가 2026-07-13에 "내 범위 완료, 추가 액션 없음"을 정확히 보고했으나 재배정이 이뤄지지 않아, **담당자에게는 할 일이 없고 잔여 작업자(braveji·sjpark)는 배정되지 않은 상태**로 3개월 반이 경과함. + +--- + +## 3. 제안 — 아홉 가지 + +### 역할 분담 + +**(1) 문헌 판정에 Owner≠Reviewer를 적용할 것.** +현행 Owner≠Reviewer는 결과와 원고에만 적용됨. 그러나 스쿱 판정은 논문 구조를 통째로 바꾸는 힘을 가지며, 실제로 Paper A를 flagship에 흡수시킴. **논문 구조·범위를 바꾸는 문헌 판정은 단독 결정에서 제외**하고 판정자와 검토자를 나눌 것. + +**(2) 결정 레코드에 반영처·반영 오너·기한을 필수 필드로 둘 것.** +현행 결정 기록에는 무엇을 정했는지만 있고 누가 어디에 반영하는지가 없음. 아래 세 칸이 비면 결정으로 인정하지 않을 것. + +``` +결정 : CPTAC 외부전이는 별도 §4로 격리 +반영처 : manuscript/SECTION_ASSIGNMENT_paperC.md 에 §4 행 신설 +반영자 : kkkim 기한: 2026-07-20 +``` + +**(3) 본인 범위 완료를 보고할 때 그 자리에서 재배정하거나 티켓을 분할할 것.** +"내 몫은 끝, 나머지는 남 소관"으로 종료하지 않을 것. 보고자가 잔여 블로커의 실제 수행자로 assignee를 옮기거나 티켓을 나눔. BIOP02-70부터 적용할 것. + +### 업무 진행 + +**(4) ★ 근거 등급을 필수로 표기할 것.** +외부 문헌에 기반한 모든 판정에 등급을 붙임. + +| 등급 | 뜻 | 허용 결정 범위 | +|---|---|---| +| **E1** | 전문 확보·정독 | 제한 없음 | +| **E2** | 초록·요약만 | **잠정 판정만.** 결과 제외·논문 재설계·투고처 변경 불가 | +| **E3** | 2차 인용·검색 스니펫 | 메모만, 판정 근거로 사용 불가 | + +이번 사건을 막았을 유일한 장치임. 2026-07-10 판정은 E2였고, E2로는 "결과를 원고에서 뺀다"에 도달할 수 없었을 것임. + +**(5) E2 판정에 만료일과 전문 확보 태스크를 함께 걸 것.** +E2로 잠정 판정을 내리면 같은 자리에서 전문 확보 티켓을 만들고 재검토 기한을 적음. 이번에도 Europe PMC(PMC13269319)에서 CC-BY 전문이 즉시 확보됨. 두 달간 재시도가 없었을 뿐임. + +**(6) 원고에서 제외하는 결과는 폐기 사유를 남길 것.** +`results/EXCLUDED.md`를 신설하고 아래를 기록함. + +``` +CPTAC 외부검증 5종 (ER/PR/HER2/PAM50 5·4class) +제외 사유 : Fernandez-Romero 2026과 동일 설계로 판단, 스쿱 +근거 등급 : E2 (초록만, 전문 미확보) +재검토 조건 : 전문 확보 시 / 분할 설계 상이가 확인될 시 +``` + +현행은 결과가 조용히 사라지며, 원고에 없다는 사실 자체가 관측되지 않음. 사유를 남기면 전제가 바뀔 때 복원 가능함. + +### 검수 과정 + +**(7) ★ 산출물 커버리지 리포트를 제출 게이트에 넣을 것.** +실험 산출물 중 원고에 반영되지 않은 것을 자동 목록화하고 각각을 둘로 분류함. + +- **의도적 제외** — `EXCLUDED.md`에 사유와 근거 등급이 있음 +- **미분류** — 이 값이 **0이어야 제출** + +이번 건에 돌렸다면 `ext_eval_summary.json` 5건이 원고 언급 0회로 즉시 검출됐을 것임. 가장 값싸고 직접적인 대책임. + +**(8) stale 스캐너를 게이트에 추가할 것.** +`agents/critic/scripts/`에 이미 `check_number_drift.py`·`manuscript_parity_ko_en.py`·`verify_citations.py`가 있음. 같은 결로 네 가지를 더함. + +- 헤더·배너의 상태 문구가 본문 서명표와 일치하는가 (BIOP02-70) +- draft가 참조하는 정책 섹션 번호가 현행인가 (BIOP02-151의 폐기된 §10) +- 결과 JSON의 라벨 문자열이 실제 배열 길이와 맞는가 (BIOP02-123) +- registry `critic_status`와 원고 수록 여부가 정합하는가 + +전부 기계로 검출 가능하며, 사람이 놓쳐 5주씩 소요된 항목임. + +**(9) Critic 체크리스트에 누락 점검 항목을 추가할 것.** +현행 7-point에 아래 한 줄을 더함. + +> **실려야 하는데 실리지 않은 결과가 있는가.** 산출물 인벤토리와 대조하고, 제외된 것은 사유와 근거 등급을 확인한다. + +--- + +## 4. 도입 순서 + +마감(ML4H 2026-09-10) 부담을 고려해 셋으로 나눔. + +| 시점 | 항목 | 비용 | +|---|---|---| +| **즉시** | (4) 근거 등급 · (7) 커버리지 리포트 · (3) 완료 보고 시 재배정 | 규칙 한 줄 + 스크립트 1개 | +| **제출 후** | (6) EXCLUDED.md · (8) stale 스캐너 · (9) Critic 항목 | 중간 | +| **다음 시즌** | (1) 문헌 판정 Owner≠Reviewer · (2) 반영 오너 필수 · (5) E2 만료일 | 규율 개정 | + +**(7)은 이번 제출 전에 최소 한 번 돌릴 것을 권함.** CPTAC 외에 누락된 산출물이 더 있는지 아직 모름. + +--- + +## 5. 한 줄 요약 + +이번 사고는 틀린 것을 실은 사건이 아니라 **맞는 것을 싣지 않은 사건**이며, 현행 검수 체계는 후자를 점검하지 않음. 커버리지 리포트 하나가 가장 값싸고 확실한 대책이라고 판단함. diff --git a/results/EXCLUDED.md b/results/EXCLUDED.md new file mode 100644 index 0000000..098f784 --- /dev/null +++ b/results/EXCLUDED.md @@ -0,0 +1,60 @@ +# 원고 미반영 산출물 대장 + +> 실험 산출물 중 **원고에 싣지 않기로 한 것**과 그 사유를 남기는 파일임. 결과가 조용히 사라지는 것을 막는 것이 목적임. +> 배경 = `docs/PITFALLS_REGISTRY.md` A9·C11. CPTAC 외부검증 5종이 분석 완료 상태로 원고에 한 번도 반영되지 않은 채 두 달이 지났고, 어떤 게이트에도 걸리지 않았음. +> 검사기 = `agents/critic/scripts/check_result_coverage.py`. **`[제외확정]` 태그가 붙은 줄에 경로가 적힌 것만** 제외로 인정함. 태그 없이 적으면 게이트가 계속 잡음. + +## 기재 규칙 + +- 한 항목에 **산출물 경로 · 제외 사유 · 근거 등급 · 재검토 조건 · 판정자**를 적음. +- 근거 등급: **E1** 전문·원자료 확인 / **E2** 요약·초록만 / **E3** 2차 인용. **E2 이하로는 제외를 확정하지 않음**(A9 재발 방지). +- 판정이 안 끝난 것은 §2에 두고 `[제외확정]`을 붙이지 않음. 게이트에서 계속 미분류로 뜨는 것이 정상임. + +--- + +## 1. 제외 확정 + +| 산출물 | 사유 | 등급 | 재검토 조건 | +|---|---|---|---| +| `[제외확정]` `experiments/template/metrics.json` | 신규 실험용 빈 템플릿이며 실측 결과가 아님 | E1 | 없음 | +| `[제외확정]` `experiments/sjpark/immune_signature_dummy_v1/metrics.json` | 배선 점검용 더미 실행임. 파일명이 `dummy`이고 성능 지표 키가 없음 | E1 | 없음 | +| `[제외확정]` `experiments/crosscancer/LUNG_NSCLC/pilot/tiling_summary.json` | 타일링 전처리 집계이며 모델 성능이 아님. Methods 서술로 충분함 | E1 | 타일링 파라미터를 결과로 주장할 경우 | +| `[제외확정]` `experiments/crosscancer/LUNG_NSCLC/pilot/embedding_summary.json` | 임베딩 추출 집계이며 모델 성능이 아님. 위와 같음 | E1 | 위와 같음 | + +--- + +## 2. 판정 대기 (제외 아님, 게이트에 계속 뜸) + +### 2-1. 원고에 실어야 한다고 보는 것 — kkkim 의견 + +| 산출물 | 왜 실어야 하나 | 판정 필요 | +|---|---|---| +| `sjpark/{er,pr,her2}_status_clam_uni_v2/ext_eval_summary.json`, `sjpark/pam50_clam_mb_uni_v1{,_4class}/ext_eval_summary.json` | CPTAC 외부검증 5종. 원고 전체에 `ext_` 수치가 **하나도 없음**. 판별력은 보존되는데(ER 0.9013→0.894, PAM50 4-class 0.8053→0.8181) 라우팅·보정 층은 붕괴한다는 해리가 cost-of-substitution 프레임의 직접 근거임 | braveji (critic_status caution→pass), BIOP02-70 | +| `sjpark/{er,pr,her2}_status_clam_uni_v2_labelshuffle/metrics.json`, `sjpark/pam50_clam_mb_uni_v1_labelshuffle/metrics.json` | 라벨셔플 음성대조 4종. 라벨을 섞으면 우연 수준으로 내려감(ER ext 0.3501, PR ext 0.3664, HER2 0.4829, PAM50 ext 0.5393). 우리 양성 결과가 배선 산물이 아님을 보이는 방어 근거인데 원고에 없음 | 박세진 (결과 오너) | + +### 2-2. 구버전으로 대체됐는지 확인이 필요한 것 + +| 산출물 | 잠정 판단 | 판정 필요 | +|---|---|---| +| `sjpark/er_status_uni_v1/metrics.json` (auc 0.8209) | Sprint 1 MLP 베이스라인이며 Sprint 3 CLAM(`er_status_clam_uni_v2`, auc 0.9013)으로 대체된 것으로 보임 | 박세진 | +| `sjpark/pr_status_uni_v1/metrics.json` (auc 0.7125) | 위와 같음 | 박세진 | +| `sjpark/pam50_uni_v1/metrics.json`, `pam50_uni_v2/metrics.json` (auc 0.7113) | 위와 같음 | 박세진 | +| `sjpark/her2_status_uni_v1/metrics.json` (auc 0.5509) | 위와 같음. 다만 HER2는 v1·v2 모두 우연 수준이라 방법 비의존성 근거로 쓸 여지가 있음 | 박세진 | + +베이스라인을 비교 대상으로 본문에 남길지, Methods 한 줄로 줄일지가 갈림. **대체됐다는 이유만으로 지우면 "왜 CLAM을 썼나"의 근거가 사라짐.** + +### 2-3. 아직 이른 것 + +| 산출물 | 상태 | +|---|---| +| `experiments/kkkim/20260830_encoder_ablation/metrics.json` (auc 0.8918) | BIOP02-149. PR #150이 2026-08-31 병합됨. `critic_status` v2 재검토 대기 중이라 원고 반영은 그 뒤 | +| `braveji/BIOP02-56_counterfactual_recompute/{pam50_4class,pr}/counterfactual_summary.json` | Critic #3 counterfactual 재계산. 원고 반영 범위를 braveji 판정에 맞춰야 함 | +| `experiments/kkkim/20260804_operating_point/label_quality_results.json` | 성능 지표 키가 없어 자동 판정 불가. 내용 확인 후 분류 | + +--- + +## 3. 갱신 규칙 + +- 결과를 원고에서 빼기로 하면 **그 자리에서** 여기에 적음. 나중에 몰아 쓰면 사유가 날아감. +- 제외 확정으로 올릴 때는 근거 등급을 함께 적고, **E2 이하면 전문 확보 티켓을 먼저 만듦**. +- 판정이 끝난 항목은 §2에서 §1로 옮기거나 원고에 반영하고 이 표에서 지움.