Files
llm-wiki/docs/superpowers/specs/2026-06-06-spec-a-deterministic-backbone-gate-design.md

147 lines
9.8 KiB
Markdown

---
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`는 *공유 기계장치*만(정책 아님).