--- title: "Spec A — 결정론 backbone을 진짜 게이트로 (하이브리드 PreToolUse/PostToolUse)" source_type: llm-generated status: draft confidence: medium tags: [harness, claude-code, hooks, design, automation] last_reviewed: 2026-06-06 --- # Spec A — 결정론 backbone을 진짜 게이트로 > 상위 감사: [[2026-06-06-harness-audit-report]] §3 G1·G5·G6. 본 spec은 그 첫 슬라이스. > 두 설계 결정 확정: **(1) 하이브리드 게이트**, **(2) Lean & safe 범위/SSOT**. ## 1. 문제 (감사에서) - **G1**: `wiki_structure_lint.py`의 `--hook` 경로가 findings가 있어도 **항상 `sys.exit(0)`** (line 531) — 582행짜리 결정론 린터가 *경고 프린터*일 뿐 게이트가 아니다. 게다가 PostToolUse라 사후. 유일한 실제 차단(`wiki_claim_gate.py`)은 **5개 path-prefix만** 커버 → `wiki/projects`·`invest-*`·파생 산출물·`raw/errors`는 쓰기-시점 강제 0. - **G5**: SSOT 분열 — `structure_lint`는 `templates/`에서 도출(좋음), 그러나 `claim_gate`는 섹션명·컬럼을 inline 하드코딩, `SKILL.md`는 source_type 재나열 → 택소노미 사본 3개, drift 위험. - **G6**: 유일한 exit-2 차단 훅 `claim_gate.py`에 유닛 테스트 **0개**(비차단 린터는 잘 테스트됨 — 비대칭). ## 2. 비목표 (이 spec 범위 밖) - judge 에이전트 return-schema / N-vote quorum → **Spec B**. - funnel stats 계약 → **Spec C**. - 누락 카테고리의 *새 의미 규칙*(invest source+timestamp, 파생물 canonical-source+status, wiki/projects claim-backed) → 후속(Spec B/C 또는 A.2). **단, 섹션/frontmatter 존재 검사는 C1이 template-derived이므로 게이트 배선만으로 전 카테고리 자동 확장됨.** - Antigravity 훅 포팅 → 기존 follow-up(`docs/superpowers/notes/2026-06-04-phase2-antigravity-hook-coverage.md`). - branch-spec/project-spec의 Workflow 변환 → **Spec D**(보류). ## 3. 핵심 설계 결정 (확정) ### DD1 — 하이브리드 게이트 지점·의미 PostToolUse exit-2는 쓰기를 되돌리지 못한다(파일은 이미 디스크, 모델에 "고쳐라"만 전달). 따라서: - **항상-틀린 검사(C2 깨진 링크)** → **PreToolUse**로 옮겨 `projected_content`에 검사, exit 2로 **쓰기 자체 차단**(ghost가 디스크에 안 닿음). - **완성성 검사(C1 섹션/frontmatter, C3 선택조건)** → 기존 `is_completeness_checkable` 철학 유지: '완성 선언' 문서에만 **PostToolUse exit-2 fix-up**. 기존 코드 철학(C2-항상 / C1·C3-완성시)과 정확히 일치한다. ### DD2 — Lean & safe 범위 + SSOT - claim_gate의 *기존* 5-prefix 의미 규칙은 그대로. 새 의미 규칙 없음. - SSOT: claim_gate의 하드코딩 prefix→요구 맵 + 공유 이벤트 파싱을 **새 `wiki_rules.py` 모듈**로 추출(두 훅이 import). 템플릿-테이블 파싱(Deep SSOT)은 채택 안 함 — fragile. ## 4. 아키텍처 ``` .claude/hooks/ wiki_rules.py ← NEW. 공유 SSOT 모듈 (stdlib only) wiki_claim_gate.py ← MOD. wiki_rules import (projected_content/파싱/PREFIX_REQUIREMENTS 이관) wiki_structure_lint.py ← MOD. --pre 모드 신설 + --hook fix-up 티어링 test_wiki_structure_lint.py ← 기존 test_wiki_claim_gate.py ← NEW ``` ### 4.1 `wiki_rules.py` (NEW 공유 모듈) - **이관(claim_gate→여기):** `tool_name()`, `tool_input()`, `target_path()`, `write_content()`, `projected_content()`, `command_string()`, `rel_to_root()`, `has_table()`. - **추출(하드코딩→dict, SSOT):** ```python PREFIX_REQUIREMENTS = { "raw/official-docs/": {"tables": [("## Claims Extracted", [...6 cols])], "sections": ["## Usage Boundaries"]}, "raw/company-tech-blogs/": {...동일...}, "raw/branch-notes/": {"tables": [("## Decision Evidence Map", [...5 cols])], "section_regex": [r"^## .*\bClaims To Verify\b"], "semantic": ["officially_supported_needs_strength"]}, "wiki/concepts/": {"tables": [("## Claim-backed Knowledge", [...4 cols])]}, "docs/superpowers/specs/*-report.md": {"semantic": ["complete_needs_traceability"]}, } ``` (claim_gate의 `check_markdown_write` 로직이 이 dict를 순회하도록 리팩터 — 동작 동일, 출처가 dict로.) - **상수:** `CRITICAL_CODES`, `FIXUP_CODES`, `WARN_CODES` (structure_lint이 import). ```python CRITICAL_CODES = {"BROKEN_LINK", "BROKEN_MD_LINK"} # PreToolUse block FIXUP_CODES = {"MISSING_SECTION","MISSING_FRONTMATTER", "EMPTY_SELECTION_CRITERION","DANGLING_ANCHOR", "PROJECT_NO_DIAGRAM","PROJECT_NO_BRANCH_TABLE", "UNMAPPED_SOURCE_TYPE"} # PostToolUse exit-2 (완성시) # 그 외(NO_FRONTMATTER 등) → WARN exit 0 ``` ### 4.2 `wiki_structure_lint.py --pre` (NEW 모드) - stdin JSON 이벤트 → `wiki_rules.target_path()` + `projected_content()`로 *쓰기 후 예상 본문* 계산. - `raw/` 또는 `wiki/`의 `.md`가 아니면 exit 0. - `build_vault_index(root)` + `check_c2(projected_doc, ...)` 실행. - **`code in CRITICAL_CODES`인 finding이 있으면 stderr로 사유 출력 + `sys.exit(2)`** (쓰기 차단). - 백틱 placeholder(`` `[[future]]` ``)는 `_code_spans`가 이미 면제 → forward-ref 정상. - 차단 메시지에 탈출구 명시: "미존재 타깃은 백틱 코드(`` `[[slug]]` ``)로 표기하거나 타깃을 먼저 생성." - CRITICAL 없으면 exit 0(DANGLING_ANCHOR 등은 PostToolUse가 처리). - **성능:** PreToolUse는 매 쓰기마다 발동 → `build_vault_index`(rglob) 비용. 30s timeout 내 여유지만, 캐시 불가(이벤트마다 새 프로세스). projected 본문에서 *추출된 위키링크가 0개면 인덱스 빌드 스킵*하는 early-exit 추가. ### 4.3 `wiki_structure_lint.py --hook` (MOD, fix-up 티어링) 현재: C2 항상 + (완성시) C1/C3 → 전부 stderr 출력 → 항상 exit 0. 변경: - C2 **broken-link**는 이미 PreToolUse에서 차단됨 → `--hook`에서는 중복 차단 안 함(여전히 출력은 하되 exit 코드엔 미반영). DANGLING_ANCHOR(타깃 존재, 앵커만 부재)는 여기 fix-up 대상. - `is_completeness_checkable(doc)` 이고 `findings` 중 `code in FIXUP_CODES`가 있으면 → stderr 사유 + **`sys.exit(2)`** (fix-up 루프; 파일은 디스크에 있으나 모델이 고침). - 그 외 → 기존처럼 WARN 출력 + `sys.exit(0)`. - **무삭제:** `findings[:10]` 잘림 시 ` … 외 {len-10}건 (suppressed)` 1줄 추가(`--hook`·`--pre` 양쪽). ### 4.4 `settings.json` 배선 ```jsonc "PreToolUse": [ { "matcher": "*", "hooks": [ /* wiki_claim_gate.py (기존) */ ] }, { "matcher": "Write|Edit|MultiEdit", "hooks": [ { "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/wiki_structure_lint.py --pre", "timeout": 30 } ] } ], "PostToolUse": [ { "matcher": "Write|Edit|MultiEdit", "hooks": [ /* wiki_structure_lint.py --hook (기존, 동작 변경) */ ] } ] ``` SubagentStart/SubagentStop(claim_gate)는 불변. ## 5. 영향 분석 / 위험 | 위험 | 완화 | |---|---| | PreToolUse C2-block이 클러스터 작성 중 형제 forward-ref를 막음 | 백틱 placeholder(코드가 이미 면제). 차단 메시지에 탈출구 명시. C1/C3는 PreToolUse 차단 안 함(완성시 PostToolUse fix-up). | | 매 쓰기 `build_vault_index` 비용 | projected 본문에 위키링크 0개면 인덱스 빌드 스킵. 30s timeout. | | PostToolUse exit-2가 무한 fix-up 루프 | claim_gate가 쓰는 `stop_hook_active` 가드 패턴 동일 적용 — 재진입 시 통과. | | `wiki_rules.py` import가 Codex에서 깨짐 | 같은 디렉터리 stdlib import → Codex 동일 스크립트 실행이라 동작. `--check`(sync_automation)와 무관(훅은 SSOT 1벌). | | claim_gate 리팩터가 기존 동작 변경 | dict-주도로 바꾸되 *동작 동치*. 신규 `test_wiki_claim_gate.py`가 5-prefix 케이스 회귀 고정. | ## 6. 수용 기준 (검증 가능) 1. `--pre`: 미존재 타깃 `[[ghost]]`를 본문에 포함한 Write 이벤트(stdin JSON) → exit 2 + stderr에 사유. **백틱** `` `[[ghost]]` ``은 exit 0. 2. `--pre`: 위키링크 0개 본문 → `build_vault_index` 미호출(스킵) + exit 0. 3. `--hook`: `status: verified` 문서가 필수 섹션 누락 → exit 2. `status: draft` 동일 문서 → exit 0(WARN만). 4. `--hook`/`--pre`: findings 11건 → 10건 출력 + `… 외 1건 (suppressed)`. 5. `claim_gate`: 리팩터 후 기존 5-prefix 차단 케이스 5종 + 통과 케이스가 `test_wiki_claim_gate.py`에서 green. 6. `python3 .claude/hooks/test_wiki_structure_lint.py` + `test_wiki_claim_gate.py` 전부 통과. 7. `wiki_structure_lint.py --all` 회귀: 본 변경 전후 FAIL 집합 동일(게이트 배선은 `--all`에 영향 없음). 8. 실제 wiki 문서 1개 정상 작성(완성 frontmatter + 유효 링크) → Pre/Post 훅 모두 통과(exit 0), 정상 저장. ## 7. 구현 순서 (writing-plans에서 단계화) 1. `wiki_rules.py` 생성 — claim_gate에서 공유 헬퍼 이관 + `PREFIX_REQUIREMENTS` dict + severity 상수. 2. `wiki_claim_gate.py` — `wiki_rules` import, `check_markdown_write`를 dict-주도로 리팩터(동작 동치). 3. `test_wiki_claim_gate.py` — 5-prefix 차단/통과 회귀 고정(2 전에 작성 = TDD). 4. `wiki_structure_lint.py` — `--pre` 모드 + `--hook` fix-up 티어링 + 무삭제 1줄. 5. `test_wiki_structure_lint.py` — `--pre`/fix-up 케이스 추가. 6. `settings.json` — PreToolUse에 `--pre` 배선. 7. 수동 스모크: §6 수용 기준 1-8 실행. ## 8. 메모 - 본 spec은 *기존 메커니즘 강화*(저위험)이며 새 아키텍처를 도입하지 않는다. Workflow/quorum 같은 고위험 변경은 Spec B/D로 분리. - `claim_gate`와 `structure_lint`의 책임 분리 유지: claim_gate=claim 의미, structure_lint=구조/링크. `wiki_rules.py`는 *공유 기계장치*만(정책 아님).