init: llm-wiki-haness 하네스 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:21:35 +09:00
parent 42bf3db4fd
commit 6c53ded9cb
2436 changed files with 194486 additions and 1 deletions
+137
View File
@@ -0,0 +1,137 @@
# LLM Wiki — Agent Operating Rules
본 파일은 **Antigravity CLI / Codex CLI / 기타 universal agent CLI** 가 세션 시작 시 자동 로드하는 entry point 다. Claude Code 는 별도로 [`CLAUDE.md`](CLAUDE.md) 를 자동 로드한다 (본 파일과 동일 정책의 풀버전).
> **이 저장소는 무엇인가:** 원본 자료 (`raw/`) 를 **검증된 실무 기술 문서** (`wiki/concepts/` · `wiki/projects/`) 로 변환하고, 그로부터 외부 산출물 (interview / portfolio / blog) 을 파생하는 **파이프라인**. 주된 도메인: 백엔드 / 인프라.
## 최상위 원칙
```
raw 자료는 증거다.
wiki/concepts 와 wiki/projects 는 검증된 실무 기술 문서 (canonical) 이다.
interview / portfolio / blog 는 canonical 에서만 파생된 산출물이다.
```
파생 산출물 (`wiki/interview/`, `wiki/portfolio/`, `wiki/blog/`) 은 **반드시** canonical 경유. raw 또는 daily/branch 에서 직접 파생 금지.
## 디렉터리 layout
| 위치 | 역할 |
|---|---|
| `raw/` | 가공 전 원본 자료 (영구 보관: `branch-notes/`, `daily-notes/`) |
| `wiki/` | 정리된 재사용 가능 지식 (`concepts/` · `projects/` · `interview/` · `portfolio/` · `blog/`) |
| `templates/` | 카테고리별 출력 형식 정의 (27개, `templates/blog-topic-template.md` 포함) |
| `rules/` | **3개 모델 공유 SSOT** — 14개 rule (Claude / Antigravity / Codex 동일 본문) |
| `.agents/plugins/wiki-superpowers/` | Antigravity plugin — skills / agents / rules-split |
| `.codex/agents/` | Codex CLI agent manual-invoke definitions |
| `.claude/` | Claude Code skill + slash commands |
| `docs/superpowers/specs/` | multi-doc 보고서 저장 위치 |
## 핵심 rules (lazy-load, 필요 시점 정독)
| Rule | 필독 시점 | 위치 |
|---|---|---|
| Upward link + slug + frontmatter + tag + Cluster + Derived 게이트 | 새 raw / wiki 문서 작성 시 | [`rules/linking-rules.md`](rules/linking-rules.md) |
| 파일·디렉토리·branch prefix 명명 규칙 | 새 파일 slug 결정 시 | [`rules/naming-conventions.md`](rules/naming-conventions.md) |
| `tags:` 5계층 허용 어휘 | frontmatter `tags` 채울 때 | [`rules/tag-taxonomy.md`](rules/tag-taxonomy.md) |
| Evidence matrix + 명명된 실패 모드 + subagent dispatch | multi-doc research / review / audit 시 | [`rules/evidence-first-research.md`](rules/evidence-first-research.md) |
| 보고서 §0~§8 + Verdict 산식 + §7.1 self-grep | 보고서 작성 시 | [`rules/reporting-standards.md`](rules/reporting-standards.md) (root SSOT 559줄) |
| Goal/Assumption/Action 사슬 + Counterargument + Self-grep + 금지어 (7 Contracts) | 권고·브레인스토밍·개념 설명·비교·plan 리뷰·direct-answer 시 | [`rules/advisory-depth.md`](rules/advisory-depth.md) (root SSOT 418줄) |
| 컨퍼런스급 다이어그램 표준 v2 (8항 self-check) | draw.io / Mermaid 작성 시 | [`rules/diagram-standards.md`](rules/diagram-standards.md) (root SSOT 379줄) |
## Plugin entry (Antigravity)
문서 작업 시작 시 invoke:
```
.agents/plugins/wiki-superpowers/skills/wiki-workflow/SKILL.md
```
본 skill 이 dispatch decision tree + STOP self-check + Output-Format Hard Gate 를 강제. 10 agents (`wiki-doc-author`, `wiki-source-summarizer`, `wiki-research-lane`, `wiki-link-verifier`, `wiki-adversarial-reviewer`, `wiki-diagram-reviewer`, `wiki-decision-researcher`, `branch-depth-auditor`, `coverage-auditor`, `wiki-consistency-auditor`) 가 정의되어 있음.
## Frontmatter 표준
모든 `wiki/` 문서:
```yaml
---
title: <문서 제목>
source_type: official-doc | company-tech-blog | personal-blog | lecture | project-note | error-note | job-posting | blog-topic | interview-prep | daily-note | branch-note | concept | interview | portfolio | blog | llm-generated
status: raw | draft | reviewed | verified | published-ready | stale | needs-confirmation
confidence: high | medium | low | unknown
tags: [<l1>, <l2>, ...]
related_projects: [<project>]
last_reviewed: YYYY-MM-DD
---
```
`raw/` 는 최소한 `title`, `source_type`, `url`(있다면), `tags` 만 있어도 됨.
## Source 신뢰도
| source_type | 취급 |
|---|---|
| `official-doc` | 기준 / 정의로 사용 가능 |
| `company-tech-blog` | 사례·관점. **공식 best practice 로 취급 금지** |
| `personal-blog` | 참고 자료 |
| `lecture` | 학습 자료 |
| `blog-topic` | 채용공고가 아닌 작업·학습·트러블슈팅 기반 블로그 글감 원석 |
| `llm-generated` | 검토 전 초안. **high confidence 금지** |
## Project 증거 등급 (외부 공개 가능 여부)
- `actually-implemented` — 코드 존재 ✓
- `locally-verified` — 로컬·dev 환경 확인 ✓
- `prod-verified` — 운영 환경 검증 (로그·측정·인시던트) ✓
- `documented-only` — 문서에만 ❌ 외부 공개 금지
- `planned` — 계획만 ❌ 외부 공개 금지
- `needs-confirmation` — 확인 필요 ❌ 외부 공개 금지
`wiki/interview/` · `wiki/portfolio/` · `wiki/blog/` 작성 시 위 ✓ 등급만 허용.
## 절대 금지
- 출처 없는 단정적 진술
- 공식 문서와 기술블로그 혼동
- `documented-only` / `planned``actually-implemented` 처럼 표현
- LLM 생성 내용을 검증 없이 `high` confidence 로 분류
- **파생 산출물 (`wiki/interview/`, `wiki/portfolio/`, `wiki/blog/`) 을 canonical 경유 없이 생성**
- **원천 canonical status 가 `reviewed | verified | published-ready` 미만인 상태에서 파생 생성**
- **`/ingest``wiki/interview/` · `wiki/portfolio/` · `wiki/blog/` 에 문서 작성**
- **branch-note slug 에 numbered hierarchy 사용** (`feature-X-1` 등). slug 는 **구현 내용** 4~8 단어 영문 kebab-case. 계층은 frontmatter `parent_branch:` + `## Parent` 섹션으로만.
- **`develop-` prefix 사용** — 제거됨. 기능 구현 작업은 규모 무관 `feature-`.
- 금지 마케팅 단어 (`100%`, `완벽`, `극한`, `절대로`, `최강`, `역사상 가장`, `명품`) 를 verbatim quote 밖에서 사용
## 파이프라인 게이트 (CLAUDE.md §15 핵심)
| 명령 | 입력 | 출력 | 게이트 |
|---|---|---|---|
| `/ingest` | `raw/*` | `wiki/concepts/` · `wiki/projects/` **만** | — |
| `/projectize` | `wiki/concepts/` | `wiki/projects/` **만** | 원천 status ≥ `reviewed` 권장 |
| `/interviewize` | `wiki/concepts/` · `wiki/projects/` | `wiki/interview/` | 원천 status ∈ {reviewed, verified, published-ready} — 미달 시 중단 |
| `/blogify` | `wiki/concepts/` · `wiki/projects/` | `wiki/blog/` | 동상 |
| (수동) | `wiki/projects/` | `wiki/portfolio/` | 본문 등급은 §"Project 증거 등급" 준수 |
## Runtime hook enforcement (Antigravity 한정)
`~/.gemini/antigravity-cli/hooks/wiki_hard_gate.py` (PreToolUse) 가 `docs/superpowers/specs/*.md` write 시 G1~G4 자동 검사:
- G1: §7.1 의 `$ sed -n` / `$ grep -nF` 명령 존재
- G2: 금지 마케팅 단어 7개 (verbatim quote 밖)
- G3: `Verdict: COMPLETE` + §3-1 산식 (`M==N AND P==R`) 표기
- G4: ≥5 findings master 에 §4-1 Adversarial Review 존재
**Hook 통과 ≠ 합격.** chat 본문 응답, 증거 진실성 (실제 sed/grep 실행), subagent dispatch 판단, 다른 경로 write 등은 모두 agent self-check 단독 책임. 자세히: [`~/.gemini/antigravity-cli/hooks/README.md`](file:///home/donghyeon/.gemini/antigravity-cli/hooks/README.md).
## 작업 우선순위 + 출력 언어
1. 사실 정확성 > 표현 매끄러움
2. 출처 명시 > 빠른 작성
3. 과장 방지 > 강한 어조
4. 재사용 가능성 > 단발성 완성도
본문 산문은 **한국어** 기본. 코드 / CLI 명령 / 공식 용어 (`connection pool`, `idempotent` 등) 는 원문 유지.
## 전체 CLAUDE.md 본문이 필요할 때
본 AGENTS.md 는 CLAUDE.md 의 ≤150줄 요약. 상세 (§4 metadata, §6 등급, §11 절대 금지 전체, §15 파이프라인, §16 품질 10항목 등) 가 필요하면 [`CLAUDE.md`](CLAUDE.md) 직접 정독. CLAUDE.md 는 모든 모델 공통 SSOT.
+409
View File
@@ -0,0 +1,409 @@
# LLM Wiki — Claude Code 운영 규칙
이 파일은 **운영 규칙**만 담습니다. 개념 설명, 공식 문서 요약, 프로젝트 본문, 면접 답변, 블로그 초안은 절대 여기에 두지 않습니다.
---
## 1. 이 저장소의 목적
원본 자료(`raw/`)를 **검증된 실무 기술 문서**로 변환하고, 그로부터 외부 산출물을 파생하는 **파이프라인**입니다.
### 최상위 원칙
```text
raw 자료는 증거다.
wiki/concepts와 wiki/projects는 검증된 실무 기술 문서(canonical)이다.
interview / portfolio / blog는 canonical에서 파생된 산출물이다.
```
자세한 위계와 파생 규칙은 §15.
### 메타 정보
- 주된 도메인: 백엔드 / 인프라
- 그 외 주제도 허용. 단, 모든 문서는 동일한 규칙을 따라야 함.
- 최종 사용처: 면접, 이력서, 포트폴리오, README, 블로그.
### 핵심 흐름
```text
캡처: /daily | /branch → raw/
변환: raw/ → /ingest → wiki/concepts + wiki/projects (canonical)
품질: wiki/* → /tag · /lint · /sync · /query (+ 브랜치 게이트: /depth · /coverage)
파생: wiki/concepts + wiki/projects → /interviewize · /blogify · /explain · (wiki/portfolio 수동)
```
파생 산출물(`wiki/interview/`, `wiki/portfolio/`, `wiki/blog/`)은 **반드시** canonical 경유. raw 또는 daily/branch에서 직접 가지 못함.
---
## 2. 디렉터리 역할
| 위치 | 역할 |
|------|------|
| `CLAUDE.md` | 이 파일. 운영 규칙만. |
| `raw/` | 가공 전 원본 자료. 출처 보존. |
| `wiki/` | 정리된 재사용 가능 지식. |
| `templates/` | 출력 형식 정의. |
| `.claude/commands/` | 반복 작업 자동화 — **캡처**: `/daily`, `/branch`, `/branch-spec`, `/project`, `/project-spec` · **변환/품질**: `/ingest`, `/tag`, `/lint`, `/sync`, `/query`, `/depth`, `/coverage`, `/migrate-claims` · **출력**: `/projectize`, `/interviewize`, `/blogify`, `/explain`. |
### 주요 문서 wikilinks
운영 규칙(본 파일)과 함께 사용되는 핵심 문서들. Obsidian Graph에서 이 hub와 연결되어야 함:
- **Hub / 로그**: [[wiki/llm-wiki]] (vault MOC), [[wiki/log]]
- **메타 규약 / Rules (필수 정독, top-level `rules/` — Claude / Antigravity / codex-cli 공유 SSOT)**:
- [[rules/linking-rules]] — Mandatory upward link 표 + 다중 부모 + 양방향 작성 패턴 + Hub/MOC 명명 컨벤션 (named hub, `index.md` 금지) + 검증 체크리스트. 모든 raw/wiki 문서가 따르는 single source of truth.
- [[rules/naming-conventions]] — 파일·디렉토리·branch prefix·다이어그램 명명 규칙.
- [[rules/tag-taxonomy]] — `tags:` 5계층 허용 어휘 + 동의어 정책.
- [[rules/diagram-standards]] — **컨퍼런스급 다이어그램 표준 v2 (minimalist-first)** — Toss SLASH / Kakao if(dev) / Naver DEVIEW 수준. draw.io 아키텍처 + Mermaid sequence/ER 작성 시 정독. element budget (Vertex ≤ 10 / Edge ≤ 8 / Callout ≤ 1) + 8항 self-check (§14) 모두 만족 필수. `wiki-diagram-reviewer` 가 ≥95/100 점 채점.
- [[rules/evidence-first-research]] — multi-doc 정독 시 verbatim quote + 명명된 실패 모드.
- [[rules/reporting-standards]] — multi-doc 보고서 §0~§8 템플릿 + Output Split + Verdict 산식.
- [[rules/advisory-depth]] — 7 Contracts (Goal/Assumption/Action chain · Exhaustive Options · Plan Gap · Direct-Response · Citation Discipline · Self-Grep · Forbidden Words).
- [[rules/prose-style]] — 파생 산출물(interview/blog/portfolio) 한국어 윤문 + "쉬운 설명" 기준. 존댓말 · 적당히 긴 길이 · 개발 용어만 영어 · 전문 용어 한 줄 풀이 · 쉬운 요약 먼저 + 명명된 실패 모드. `/interviewize`·`/blogify` 가 참조.
- [[rules/branch-depth-gate]] — 브랜치 노트가 *코딩 착수해도 되묻지 않을 만큼* 깊은지 판정하는 4축·깊이 사다리(L0~L3)·명명된 실패 모드. `/depth` 명령(`.claude/commands/depth.md`)과 `branch-depth-auditor` agent + 결정론 린터 `.claude/hooks/wiki_structure_lint.py`가 함께 집행.
- [[rules/consistency-contract]] — 문서 간 일관성 계약: Single-Owner(결정·관심사당 owner 문서 정확히 1개) + Reference-Only(타 문서는 `[[owner]] D<n>` 포인터 + 1줄 요약만, 재진술 금지) + owner 변경 시 역참조 비차단 전파 알림. `/sync` 명령 + `wiki-consistency-auditor` agent + 결정론 검사기 `.claude/hooks/wiki_consistency_check.py` 가 집행.
- [[rules/extraction-tiering]] — Tiered Extraction 계약: 4-Tier(T0 결정론 / T1 외부 구독 codex·agy / T2 haiku / T3 sonnet / T4 opus) + 5계명(외부 CLI = read-only 추출기 · 무검증 발췌 소비 금지 · engine funnel 필수 · opus 에 raw corpus 반입 금지 · fallback 사다리 기록). `extraction-broker` agent + `scripts/deep-research/deep_research/extract.py`(quote-verifier 내장)·`vote.py`(cross-vendor quorum 표) + `wiki_consistency_check.py --packets` 가 집행.
- [[rules/subagent-input-contracts]] — controller가 dispatch *전에* 모을 입력을 agent/명령별 form schema로 고정(3-rule: Pre-fill · Missing→행동 명시 · No SSOT 이중화) + 명명된 실패 모드. `/branch-spec`(`.claude/commands/branch-spec.md`)이 이 계약을 소비해 빈 브랜치 노트를 *되묻지 않을 수준*으로 채움(근거 없으면 자동조사 → 실패 시 `UNSUPPORTED_DECISION` 라벨 → 끝에 `/depth` + `/coverage` 자동 게이트, 루프 천장 2회).
- **Claude Code 자동화** (`.claude/`) — narrative 스타일, `tools:`/`model:` frontmatter:
- `.claude/skills/wiki-workflow/SKILL.md` — 문서 작업 진입점. 사용자 의도에 따라 적절한 agent dispatch.
- `.claude/agents/wiki-doc-author.md` — 새 raw 문서 생성 또는 기존 비-template 문서 마이그레이션 (category-aware, mode: create | migrate).
- `.claude/agents/wiki-source-summarizer.md` — URL → raw 자료 (verbatim quote + self-grep).
- `.claude/agents/wiki-link-verifier.md` — orphan / broken wikilink / Cluster 누락 감사 (read-only).
- `.claude/agents/wiki-research-lane.md` — 다수 raw 정독 → 합성 권고 (read-only).
- `.claude/agents/extraction-broker.md` — bulk 발췌 브로커 (haiku, read-only) — 외부 구독 CLI 드라이버(`extract.py`) 구동 + 실패분 자가 재발췌 + 검증된 digest 만 반환 (`rules/extraction-tiering.md` T1+T2). **Claude Code 전용** — 3-플랫폼 포팅 대상 아님 (T2 haiku 브로커는 Claude 모델 계층; Codex/Antigravity 에선 오케스트레이터가 `extract.py` 를 직접 호출).
- `.claude/agents/wiki-adversarial-reviewer.md` — 리서치/감사 draft falsification (KEEP/DOWNGRADE/REJECT 권고, read-only).
- `.claude/agents/wiki-diagram-reviewer.md``.drawio` 다이어그램 채점 (≥95/100 PASS, read-only).
- `.claude/agents/wiki-decision-researcher.md` — 기술 결정 alternatives 조사 (read-only; WebSearch + 사용자 승인 → **dispatch 요청 방출**, `wiki-source-summarizer` × N×2 실 dispatch 는 controller 가 수행 → 비교 매트릭스 + 조건부 권고).
- `.claude/agents/project-readiness-auditor.md` — project-note(hub) 완성도 의미 게이트 (read-only, ca-skeleton caliber 판정). **Claude Code 전용** — Codex/Antigravity 포팅 없음(`/project`·`/project-spec` 파이프라인은 3-플랫폼 패리티 예외). **투자 파이프라인(`/invest-*` 6개 명령 + invest 템플릿)도 Claude Code 전용 — Codex/Antigravity 포팅 대상 아님** (개인 투자 관리용, `/project` 파이프라인과 동일한 3-플랫폼 예외).
- **Antigravity CLI 자동화** (`.agents/agents/<name>/agent.json`) — `.claude/`와 동일 10개 agent (단 `project-readiness-auditor`·`extraction-broker` 는 Claude 전용 — 3-플랫폼 포팅 대상 아님) (위 7개 + `branch-depth-auditor`·`coverage-auditor`·`wiki-consistency-auditor`) 의 Antigravity 포트. Antigravity CLI native subagent registry 형식 (workspace = `.agents/agents/<name>/agent.json`, global = `~/.gemini/antigravity-cli/agents/<name>/agent.json`). 각 `agent.json``config.customAgent.systemPromptSections[0].content` 가 system prompt, `toolNames` 로 도구 권한 제어 (read-only agent 는 `write_to_file`/`replace_file_content`/`multi_replace_file_content` 제외). **G1 Pre-Read Proof, G2 Post-Write Validator, G3 Output Schema with `{{ }}` placeholders, G4 Enumerated STOP Conditions** 4가지 hard gate가 system prompt 본문에 포함됨 (Gemini의 narrative 무시 / 자체 검증 건너뛰기 / NEEDS_CONTEXT 회피 / 출력 스키마 흐트러짐을 차단). 동일 `rules/``templates/` 참조. **System prompt SSOT 는 `.agents/plugins/wiki-superpowers/agents/*.md`**. Antigravity CLI 가 직접 인식하는 것은 `.agents/agents/<name>/agent.json` 이므로, SSOT `.md` 를 편집하면 대응 `agent.json` 도 함께 갱신해야 반영됨(안 하면 변경 사항 미반영). ⚠️ **자동 생성기 `scripts/sync_automation.py` 는 현재 repo 에 없음**(2026-06-06 확인; `scripts/` 는 존재하나 `sync_automation.py` 만 부재 — 2026-07-14 재확인) — 복원 전까지 SSOT↔variant 동기화는 **수기**로 하고 3 플랫폼 패리티를 직접 유지한다.
- **Codex CLI 자동화** (`.codex/agents/`) — `.claude/`와 본문은 같은 agent. codex 환경에 맞춰 frontmatter 에서 `tools:`/`model:` 제거 + 본문 tool 표현 일반화. Codex 는 **native subagent 를 `.codex/agents/*.toml` 로 등록**(`developer_instructions` + `sandbox_mode`) — `.agents/plugins/wiki-superpowers/agents/<name>.md` 가 SSOT, `.toml` 은 그 대응 variant(생성기 부재 시 수기 동기화 — 위 ⚠️ 참조). 권한은 `.claude/agents/<name>.md` frontmatter `tools:` 에서 파생(`Edit`/`Write``workspace-write`, 없으면 `read-only`). 호출 패턴은 `.codex/agents/README.md` 참조. 동일 `rules/``templates/` 참조.
- **Commands(슬래시 명령) 3-플랫폼 동기화** — `.claude/commands/*.md` 가 SSOT (현재 23개; 이 중 invest-* 6개 + `project`·`project-spec` 2개 = **8개는 Claude 전용 비동기화**, 나머지 15개가 mirror 대상). mirror 대상은 **Codex skills** (`.agents/skills/<cmd>/SKILL.md`, `$<cmd>`/`/skills` 호출) 와 **Antigravity workflows** (`.agents/workflows/<cmd>.md`, `/<cmd>` 슬래시) 로 복제. 인자는 placeholder 없이 자연어(각괄호 prose). ⚠️ **동기화 생성기 `scripts/sync_automation.py` (agents·commands 단일 생성기, `--check` drift 검사 포함) 는 현재 repo 에 없음**(2026-06-06 확인) — 복원 전까지 SSOT(`.claude/commands/*.md`) 편집은 대응 Codex skill + Antigravity workflow 파일에 **직접 반영**해 3 플랫폼 패리티를 유지한다.
- **Hooks / 프로젝트 지침 3-플랫폼** — 훅 스크립트 SSOT 는 `.claude/hooks/` 1벌(`wiki_claim_gate.py` claim 추적 게이트 + `wiki_structure_lint.py` 구조 린트, repo 루트는 파일 위치 기반 동적 해석). **Codex**: `.codex/hooks.json` 이 같은 스크립트를 PreToolUse(claim gate)/PostToolUse(structure lint)로 연결(대화형 codex 에서 hook trust 검토 후 active). `.codex/config.toml``project_doc_fallback_filenames = ["CLAUDE.md"]` 는 해당 디렉터리에 `AGENTS.md`**없을 때만** 쓰이는 fallback 이다 — 루트 `AGENTS.md` 가 존재하는 현재 구조에선 발동하지 않으므로, Codex 는 `AGENTS.md`(CLAUDE.md 의 ≤150줄 요약)를 진입점으로 로드한다. CLAUDE.md 는 그 요약이 가리키는 모든 모델 공통 **운영-규칙 SSOT** 로 유지된다(Codex 가 CLAUDE.md 를 자동 지침으로 직접 선택한다고 가정 금지). **Antigravity**: `.agents/hooks.json` 에 3개 게이트가 `enabled``wiki-hard-gate`(global `wiki_hard_gate.py`, 리포트 출력 품질: self-grep proof/금지어/Verdict 공식/adversarial review) + `wiki-claim-gate`(`wiki_claim_gate.py --antigravity`) + `wiki-structure-gate`(`wiki_structure_lint.py --pre/--hook --antigravity`). claim_gate·structure_lint 의 `--antigravity` 출력 어댑터(deny 시 `{decision:"deny", reason}` JSON, fail-open)는 **구현·활성 완료**이다. 다만 실제 Antigravity 런타임에서의 hook **E2E 는 아직 미검증(experimental)** — 배경/후속 검증 항목은 `docs/superpowers/notes/2026-06-04-phase2-antigravity-hook-coverage.md` 참조.
- **템플릿 (출력 형식 정의)**:
- [[templates/concept-template]] — `wiki/concepts/` 일반 개념
- [[templates/project-template]] — `raw/project-notes/` 프로젝트 hub (아키텍처·시퀀스 다이어그램 필수) → `wiki/projects/` 로 추출 (raw hub 전용)
- [[templates/wiki-project-template]] — `wiki/projects/` canonical 실무 적용 문서 슬라이스 (raw hub에서 `/ingest`·`/projectize` 로 추출, `source_type: project`)
- [[templates/interview-template]] — `wiki/interview/` 면접 답변
- [[templates/raw-source-template]] — `raw/official-docs/`, `raw/company-tech-blogs/` 외부 자료 원본 발췌
- [[templates/source-summary-template]] — `wiki/concepts/` 외부 자료 검증 요약
- [[templates/daily-note-template]] — `raw/daily-notes/` 일일 노트
- [[templates/branch-note-template]] — `raw/branch-notes/` 브랜치 작업 노트
- [[templates/daily-task-develop-template]] — `raw/daily-tasks/develop/` 일일 개발 트랙 실습 과제 (사수→신입 과제 형식, 매일 아침 ~2h)
- [[templates/daily-task-infra-template]] — `raw/daily-tasks/infra/` 일일 인프라/운영 트랙 실습 과제 (매일 아침 ~2h, 운영 회복력 anchor 포함)
- [[templates/error-note-template]] — `raw/errors/` 트러블슈팅 기록
- [[templates/interview-prep-template]] — `raw/interviews/` 면접 준비 원본 노트
- [[templates/job-posting-template]] — `raw/job-postings/` 채용공고 → 블로그 글감
- [[templates/blog-topic-template]] — `raw/blog-topics/` 채용공고가 아닌 블로그 글감 원석
- [[templates/lecture-note-template]] — `raw/lectures/` 강의·강연 노트
- [[templates/portfolio-template]] — `wiki/portfolio/` 포트폴리오 (canonical derived)
- [[templates/blog-template]] — `wiki/blog/` 블로그 초안 (canonical derived)
- [[templates/explainer-template]] — `wiki/explainer/` 1타강사 설명 문서 (canonical derived, **개인 이해용** — 외부 공개 아님). 0~4단 + 대안 5단(a~e) 틀 강제, "대안=문제를 다르게 정의한 답" 구조
- [[templates/invest-field-card-template]] — `wiki/invest-concepts/` 분야 지식 카드(노드). `[[wiki/invest-concepts/field-map]]` 허브 하위. drivers vs linkages 분리 + 모든 관계 행 `[검증]/[가설]` 라벨 강제(뇌피셜 차단). 매일 `/invest-daily` "분야 관찰"이 카드 예측 vs 실측을 대조해 가설→검증 승격. invest 파이프라인 — **Claude 전용**(3-플랫폼 포팅 예외). 설계: `docs/superpowers/specs/2026-06-08-invest-field-map-design.md`
- **진행 중 프로젝트 노트** (raw/project-notes/):
- [[raw/project-notes/ca-skeleton-operational-contract]] — ca-tmpl 운영 계약 canonical SSOT
- [[raw/project-notes/project-infra-overview]] — 사용자 본인 프로젝트 인프라 개요 (작성 중)
---
## 3. Obsidian 규약 (필수)
- 모든 문서 상단에 **YAML frontmatter** (`---` 블록).
- 문서 간 연결은 **`[[wikilink]]`** 사용. 상대경로 링크 금지.
- `concepts/``projects/`는 서로 양방향 링크. 그래프뷰가 의미를 가져야 함.
- 파일명은 영문 kebab-case 권장 (예: `connection-pooling.md`). 한글 파일명도 허용하되 일관성 유지.
---
## 4. 메타데이터 표준
모든 `wiki/` 문서 frontmatter:
```yaml
---
title: 문서 제목
source_type: official-doc | company-tech-blog | personal-blog | lecture | project-note | error-note | job-posting | blog-topic | interview-prep | daily-note | branch-note | daily-task | project | concept | interview | portfolio | blog | explainer | llm-generated | invest-daily | invest-research | invest-ledger | invest-concept | invest-strategy | invest-plan
status: raw | draft | reviewed | verified | published-ready | stale | needs-confirmation
confidence: high | medium | low | unknown
tags: [backend, db, ...]
related_projects: [project-name]
last_reviewed: YYYY-MM-DD
---
```
**source_type 허용 어휘 (실제 templates 와 일치)**: raw 카테고리는 카테고리명을 그대로 source_type 으로 사용 — `lecture-note-template.md``source_type: lecture`, `error-note-template.md``source_type: error-note`, `interview-prep-template.md``source_type: interview-prep`. wiki 카테고리도 동일 (`concept-template.md``source_type: concept`, `blog-template.md``source_type: blog`). 이전에 사용되던 `error-log`, `interview-note`, `lecture-note` 는 deprecated. wiki/projects canonical 슬라이스는 `source_type: project` (`wiki-project-template.md`)를 쓰고, `project-note``raw/project-notes/` 프로젝트 hub 전용이다.
**투자 도메인 source_type (Claude 전용 파이프라인):** `invest-daily`(raw/invest-daily/), `invest-research`(raw/invest-research/), `invest-ledger`(raw/invest-ledger/), `invest-concept`(wiki/invest-concepts/), `invest-strategy`(wiki/invest-strategy/), `invest-plan`(wiki/invest-plan/). 각 카테고리명을 그대로 source_type 으로 사용. 개인 투자 관리용이며 개발 프로젝트와 분리된 트리.
`raw/` 문서는 최소한 `title`, `source_type`, `url`(있다면), `tags`만 있어도 됨.
**예외 (구조적 파일):** `wiki/llm-wiki.md` (vault MOC), `wiki/log.md` 는 지식 문서가 아닌 hub/로그이므로 위 표준에서 면제됨. `title`만 있으면 됨.
---
## 5. 출처 신뢰도 기준
| source_type | 취급 방식 |
|-------------|----------|
| `official-doc` | 기준/정의로 사용 가능 |
| `company-tech-blog` | 사례/관점. **공식 best practice로 취급 금지** |
| `personal-blog` | 참고 자료 |
| `lecture` | 학습 자료 (강의·강연) |
| `project-note` | 포트폴리오 증거 후보 |
| `error-note` | 트러블슈팅 사실 기록 |
| `interview-prep` | 면접 준비 원본 노트 |
| `job-posting` | 채용공고 (블로그 글감 시드) |
| `blog-topic` | 채용공고가 아닌 작업·학습·트러블슈팅 기반 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아님 |
| `daily-note` | 그날의 혼합 기록. **그 자체는 wiki로 옮기지 않음**. `/ingest`가 promotable 항목만 추출 |
| `branch-note` | 단일 브랜치의 TODO·결정·진행 기록. **머지 후에도 raw에 영구 보관**, verified 결과만 `/ingest`로 wiki/projects/에 추출 |
| `daily-task` | 매일 아침 학습용 실습 과제 (develop / infra 두 트랙, 트랙별 1과제 ~2h, 사수→신입 형식). **raw에 영구 보관**, `done` + `actually-implemented`/`locally-verified` 등급 항목만 `/ingest``wiki/concepts/` 또는 `wiki/projects/`에 추출. Hub: `[[raw/daily-tasks/README]]` |
| `concept` | wiki/concepts canonical 일반 개념 |
| `project` | wiki/projects canonical 실무 적용 문서 (검증된 내 프로젝트 사실, `verified` 지향) |
| `interview` | wiki/interview canonical 면접 답변 (derived) |
| `portfolio` | wiki/portfolio canonical (derived) |
| `blog` | wiki/blog canonical (derived) |
| `explainer` | wiki/explainer canonical 의 1타강사 설명 (derived). **개인 이해용 — 외부 공개 금지.** canonical 경유 필수, 새 claim 생성 금지(canonical 재구성만), 비유는 의도적 단순화이므로 사실 인용 불가 |
| `invest-daily` | 그날의 거시 자금흐름 조사. **수치마다 출처+조사시점 필수**. 영구 보관, `/invest-ingest`로 검증분만 추출 |
| `invest-research` | 특정 분야/자산 심층 조사. verbatim 인용 보존. canonical 정제 전 증거 |
| `invest-ledger` | 실제 매매 기록(사실). 영구 보관. wiki로 옮기지 않음 |
| `invest-concept` | wiki/invest-concepts canonical 투자 개념 |
| `invest-strategy` | wiki/invest-strategy canonical 전략 규칙. **면허 자문 아님 고지 필수** |
| `invest-plan` | wiki/invest-plan canonical 활성 투자 계획 |
| `llm-generated` | 검토 전 초안. **high confidence 금지** |
---
## 6. 프로젝트 증거 등급
프로젝트 관련 진술은 **항상** 다음 중 하나로 분류:
- `actually-implemented` — 코드에 존재함
- `locally-verified` — 로컬 또는 dev 환경에서 동작 확인
- `prod-verified` — 운영(prod) 환경 검증. 로그·측정값·인시던트·릴리즈 노트 등 근거 보유
- `documented-only` — 문서/README에만 존재
- `planned` — 계획만 있음
- `needs-confirmation` — 확인 필요
### 외부 공개 산출물 허용 등급
| 산출물 | 허용 등급 |
|--------|-----------|
| `wiki/interview/` | `actually-implemented` / `locally-verified` / `prod-verified` |
| `wiki/portfolio/` | `actually-implemented` / `locally-verified` / `prod-verified` |
| `wiki/blog/` | `wiki/concepts` 출처 + 위 3등급 |
| 이력서 / README | 가능하면 `prod-verified`, 아니면 `locally-verified`임을 본문에 명시 |
`documented-only` / `planned` / `needs-confirmation`은 외부 공개에 **절대 금지**.
---
## 7. 원본 보존 규칙
- 외부 URL은 링크만 두지 말고 **핵심 인용 3–5문장을 raw 문서 본문에 발췌 보존**.
- 가능하면 archive.org 스냅샷 URL을 frontmatter `archive_url`에 병기.
- 인용 시 출처와 원문을 분명히 구분 (예: `> 원문...`).
---
## 8. Stale 판정 기준
`/lint`는 다음을 stale 후보로 보고:
- `last_reviewed`**90일 초과**`status: stale` 후보
- `confidence: low` + `last_reviewed` **30일 초과** → 재검토 필요
- `needs-confirmation` 상태로 **14일 이상** 방치된 문서 → 알림
---
## 9. 출력 언어
- 기본: **한국어**
- 코드, CLI 명령어, 공식 용어(예: `connection pool`, `idempotent`)는 원문 유지.
- 면접 답변용 문서는 말로 했을 때 자연스러운 문장으로.
- **파생 산출물(`wiki/interview/`·`wiki/blog/`·`wiki/portfolio/`) 본문의 문체·윤문은 [[rules/prose-style]] 를 따름** — 존댓말 · 적당히 긴 길이 · 개발 용어만 영어(나머지 한국어) · 전문 용어 첫 등장 시 한 줄 풀이 · 쉬운 요약 먼저. 윤문이 사실 등급을 바꾸지 않음.
---
## 10. 작업 우선순위
1. 사실 정확성 > 표현 매끄러움
2. 출처 명시 > 빠른 작성
3. 과장 방지 > 강한 어조
4. 재사용 가능성 > 단발성 완성도
---
## 11. 절대 금지
- 출처 없는 단정적 진술
- 공식 문서와 기술블로그 혼동 (예: "Netflix가 그렇게 하니까 공식이다")
- `documented-only` / `planned``actually-implemented`처럼 표현
- LLM 생성 내용을 검증 없이 `high` confidence로 분류
- raw에만 자료를 넣고 wiki로 변환하지 않은 채 방치 (단, `raw/daily-notes/`·`raw/branch-notes/`·`raw/daily-tasks/`는 영구 보관 정책)
- `wiki/llm-wiki.md` (vault MOC) 갱신 누락
- 면접/이력서 문장에 검증 안 된 표현 사용
- **파생 산출물(`wiki/interview/`, `wiki/portfolio/`, `wiki/blog/`)을 canonical 경유 없이 생성**
- **raw 또는 daily/branch에서 외부 산출물 직접 생성**
- **원천 canonical 문서의 status가 `reviewed | verified | published-ready` 미만인 상태에서 파생 산출물 생성**
- **`/ingest``wiki/interview/`·`wiki/portfolio/`·`wiki/blog/`에 문서 작성**
- **branch-note 슬러그에 numbered hierarchy 사용** (`feature-X-1`, `feature-X-1-2` 등). 슬러그는 **구현 내용** 을 4~8 단어로 표현해야 한다. 계층 정보는 frontmatter `parent_branch:` + `## Parent` 섹션으로만. 자세한 룰은 `rules/naming-conventions.md` §2.1.2~§2.1.6.
- **`develop-` prefix 사용** — 제거된 prefix. 기능 구현 작업은 규모 무관 `feature-`. 기존 `develop-*` 슬러그는 `wiki-doc-author` mode=migrate 로 점진적 rename 권고 (자동 mv 금지, wikilink 영향 검토 필요).
- **branch-note 의 §구현 가이드 (Implementation Specification) 에 *근거 없는 결정* 작성** — §15.5 참조. 모든 sub-section / row / cell 은 본 branch 의 `Decision ID` + `Supporting Claim ID` reference 필수. 근거 없는 detail (메커니즘 / 명명 / glob / algorithm) 은 `UNSUPPORTED_IMPL_DECISION` 라벨 + 사용자 trade-off 한 줄 *명시*. 라벨 누락 = 다음 작업자가 *근거 있는 결정 vs 임의 trade-off* 를 구분 불가.
- **branch-note §구현 가이드에 *본 branch 결정 범위 밖* cell 작성** — 도메인 특화 (ca-tmpl skeleton 범위 밖) 또는 다른 branch 결정 영역 (security/persistence/HTTP-standard 등) 의 detail 은 §구현 가이드에 *남기지 않음*. 별도 branch 또는 canonical SSOT 로 이관하고, 이관 history 만 별도 § "Audit & Findings" 등에 보존.
---
## 12. Hub / MOC 정책
- `wiki/llm-wiki.md` 는 vault 전체의 **Map of Content (MOC)** 수준. 모든 문서를 손으로 나열하지 않음.
- 상세 목록은 Obsidian Dataview 쿼리 또는 `/query` 명령으로 동적 생성.
- 새 카테고리 / 주요 허브 문서가 생기면 `wiki/llm-wiki.md` 업데이트.
- 다중 sub-doc 을 가진 nested 프로젝트는 sibling **named hub** (`wiki/projects/<slug>.md` + `wiki/projects/<slug>/` 폴더) 패턴 강제. `index.md` 사용 금지. 자세한 룰은 [[rules/linking-rules]] §12.
---
## 13. log.md 정책
- `wiki/log.md`에는 `/ingest`, `/tag`, `/lint` 실행 시 다음 형식으로 한 줄 추가:
```
YYYY-MM-DD HH:mm /command — input → output (간단 메모)
```
- 사람이 일일이 읽지 않음. 디버깅과 회고용.
---
## 14. 명령어 우선순위
명령어는 역할별로 3그룹:
### 캡처 (raw 입력)
- 하루 시작 시 → `/daily` (raw/daily-notes/YYYY-MM-DD.md 스캐폴딩)
- 새 브랜치 시작 시 → `/branch <name>` (raw/branch-notes/<name>.md 스캐폴딩)
- 새 프로젝트 시작 시 → `/project <slug>` (스캐폴딩) → `/project-spec <slug> <목표>` (깊은 조사 + readiness 게이트). project-note hub 를 ca-skeleton 수준으로 채운 뒤, §8.0 Branch 분해표를 `/branch`·`/branch-spec` 로 전개.
- 외부 자료 / 프로젝트 메모 → raw/ 해당 카테고리에 직접 작성
### 변환 / 품질 (raw → wiki)
여러 명령이 가능한 상황이면 다음 순서로 판단:
1. raw에 미변환 자료가 있으면 `/ingest` 우선. **`/ingest`의 목적지는 `wiki/concepts/` 또는 `wiki/projects/`로 제한** (파생 산출물 직접 생성 금지). `raw/daily-notes/`·`raw/branch-notes/`는 항목 단위 추출만.
2. 새 wiki 문서가 생기면 `/tag` 검토
3. 주간 1회 이상 `/lint` — canonical 우회·status 미달 파생 검사 포함
4. 문서 정리 시 `/sync` — 문서 간 모순·위임 동기화 (결정론 검사기 + 참조 엣지 의미 대조 + fix-plan, `rules/consistency-contract.md`)
5. 질의는 `/query`로 시작, 필요 시 raw 확인
### 출력 (외부 공개용)
6. 외부 공개용은 반드시:
1. 원천 canonical 문서가 `reviewed | verified | published-ready` 상태
2. `/lint` 통과
3. 그 후 `/projectize` / `/interviewize` / `/blogify` 또는 `wiki/portfolio/` 수동 작성
---
## 15. 문서 위계 및 파생 규칙
§1의 최상위 원칙(`raw 자료는 증거 / canonical / 파생`)을 운영 규칙으로 풀어 쓴 섹션. 모든 명령은 이 섹션을 강제합니다.
### Canonical Layer (실무 기술 문서)
#### `wiki/concepts/` — 공식 개념 + 사례 + 트레이드오프
필수 요소:
- 공식 기준이 무엇인지
- 사례와 공식 기준의 차이
- 트레이드오프 / 한계
- 흔한 오해
- 프로젝트 연결 지점 (링크만, 본문에 "내가 했다" 금지)
- 출처
#### `wiki/projects/` — 실무 적용 문서
필수 요소:
- 문제 배경
- 검토한 선택지
- 결정 이유
- 실제 구현 내용
- 검증 수준 (로컬 / dev / prod 중 어디까지)
- 근거 (측정값 / 로그 / PR / 테스트)
- `documented-only` · `planned`인 것과 실제 구현된 것의 분리
- 면접·포트폴리오 말할 수 있는 범위
### Derived Layer (파생 산출물)
**canonical에서만 파생.** 다른 레이어(raw, daily, branch)에서 직접 파생 금지.
| 산출물 | 허용 원천 | Sources 필수 링크 |
|--------|-----------|---------------------|
| `wiki/interview/` | `wiki/concepts/` + `wiki/projects/` | `[[wiki/concepts/...]]` 또는 `[[wiki/projects/...]]` |
| `wiki/portfolio/` | `wiki/projects/` 중심, `wiki/concepts/` 보조 | `[[wiki/projects/...]]` (필수) |
| `wiki/blog/` | `wiki/concepts/` + `wiki/projects/` | 양쪽 모두 권장 |
| `wiki/explainer/` | `wiki/concepts/` + `wiki/projects/` | `[[wiki/concepts/...]]` (필수) + `[[wiki/projects/...]]` (있을 때) |
> **`wiki/explainer/` 의 특수 지위**: interview / portfolio / blog 와 달리 **외부 공개물이 아니라 개인 이해(학습) 산출물**이다. 따라서 §6 외부 공개 허용 등급·status 게이트(`reviewed` 이상)의 적용을 받지 **않는다** (draft 상태 canonical 에서도 파생 가능). 대신 두 가지를 반드시 지킨다: (1) **canonical 경유** — raw/daily/branch 에서 직접 생성 금지, (2) **새 claim 생성 금지** — canonical 의 교육적 재구성일 뿐이며 모든 사실은 canonical 링크로 근거를 댄다. 비유는 의도적 단순화로 표시하고 사실로 인용하지 않는다.
### 문서 승급 단계
```text
raw → draft → reviewed → verified → published-ready
```
| 단계 | 의미 | 파생 산출물 사용 |
|------|------|-------------------|
| `raw` | 원본 기록. 출처/날짜만 있어도 됨 | 불가 |
| `draft` | Claude Code가 변환한 초안 | 불가 |
| `reviewed` | 사람이 구조/표현/출처 확인 | 면접 참고만, 외부 산출물 X |
| `verified` | 코드 / 로그 / 테스트 / PR / 공식 문서 중 1개 이상 근거 보유 | interview / portfolio / blog 파생 가능 |
| `published-ready` | `/lint` 통과, 과장 제거 완료 | 이력서 / README / 외부 게시 가능 |
`stale`, `needs-confirmation`은 위 진행과 직교하는 상태 표시.
### 파이프라인 강제 (명령별 허용 범위)
| 명령 | 입력 허용 | 출력 허용 | 게이트 |
|------|-----------|-----------|--------|
| `/ingest` | `raw/*` | `wiki/concepts/` · `wiki/projects/` **만** | — |
| `/projectize` | `wiki/concepts/` | `wiki/projects/` **만** | 원천 status ≥ `reviewed` 권장 |
| `/interviewize` | `wiki/concepts/` · `wiki/projects/` **만** | `wiki/interview/` | 원천 status ∈ {`reviewed`, `verified`, `published-ready`} — 미달 시 **중단** |
| `/blogify` | `wiki/concepts/` · `wiki/projects/` **만** | `wiki/blog/` | 원천 status ∈ {`reviewed`, `verified`, `published-ready`} — 미달 시 **중단** |
| (수동) | `wiki/projects/` | `wiki/portfolio/` | 본문 등급은 §6 허용 범위만 |
| `/explain` | `wiki/concepts/` · `wiki/projects/` **만** | `wiki/explainer/` | status 게이트 없음(개인 이해용). 단 canonical 경유 + 새 claim 금지 + canonical Sources 링크 필수 |
`/lint`는 위 게이트가 우회되었는지 검사 (canonical Sources 누락, status 미달 파생, 비허용 등급 사용 등).
### 근거 기반 구현 명세 (branch-note 의 §구현 가이드 작성 원칙)
branch-note 의 `## 구현 가이드 / Implementation Specification` 섹션은 *결정 (Decisions)**검증 (Claims To Verify)* 사이의 **구현자가 임의로 정해야 했던 결정** 카탈로그. 결정이 *무엇* 을 할 것인가라면 본 §는 *어디에 어떻게* 구현될 것인가의 사전 명세 — 작성 목표는 다음 구현자가 *되묻지 않아도 코드를 작성할 수 있는 수준*.
**일률적 anchor list 강제 ❌** — branch 마다 구현 내용·범위가 다르므로 sub-section 은 *이 branch 의 결정과 근거에서 도출되는 것만* 작성한다. 대신 **3-rule meta principle** 만 모든 branch 에 동일 적용:
| Rule | 의미 |
|------|------|
| **R1. Reference 필수** | 각 sub-section / row / cell 은 본 branch 의 `Decision ID` + `Supporting Claim ID` 를 reference. 근거 없는 detail 금지 — 모든 구현 detail 은 결정 + 근거의 *도출* 이어야 함. |
| **R2. UNSUPPORTED_IMPL_DECISION 명시** | 근거 raw 가 *원칙* 만 권고하고 *detail* (메커니즘 선택 / 클래스·rule 명명 / glob 패턴 / algorithm / factory API 모양 등) 은 권고하지 않는 cell 은 `UNSUPPORTED_IMPL_DECISION` 라벨 + 사용자 trade-off 근거 한 줄. *근거 있는 결정 vs 사용자 임의 trade-off* 의 경계. |
| **R3. OUT_OF_BRANCH_SCOPE 정제** | 본 branch 결정 범위 밖 cell — 도메인 특화 (ca-tmpl skeleton 범위 밖) 또는 다른 branch 결정 영역 (security/persistence/HTTP-standard 등) — 은 §구현 가이드에 *남기지 않음*. 별도 branch 또는 canonical SSOT 로 이관. 이관 history 는 별도 § "Audit & Findings" 등에 보존. |
작성 형식 예시는 `[[raw/branch-notes/feature-boundary-validation-mapping-contract]]` 의 §구현 가이드 + §Audit & Findings 참조 — 정제된 in-scope 만 §구현 가이드에 남기고, audit 결과/이관 권고는 별도 §로 분리하는 패턴.
`/lint` 검사 항목 (2026-06-10 구현 — `.claude/commands/lint.md` §A1):
- §구현 가이드의 sub-section 에 Trace 표시 (Decision ID + Claim ID) 누락
- 근거 미명시 detail 의 `UNSUPPORTED_IMPL_DECISION` 라벨 누락
- 본 branch 결정 범위 밖 row 잔존 (`OUT_OF_BRANCH_SCOPE`)
### 실무 문서의 최종 품질 기준 (10항목)
`wiki/concepts/` 또는 `wiki/projects/` 문서가 `verified` / `published-ready`로 올라가려면 다음을 만족해야 합니다.
1. 문제 배경이 있다.
2. 공식 기준이 있다.
3. 선택지가 있다.
4. 결정 이유가 있다.
5. 구현 사실이 있다.
6. 검증 증거가 있다.
7. 한계가 있다.
8. 말하면 안 되는 범위가 있다.
9. 출처가 있다.
10. 재사용 산출물은 canonical 문서에서만 파생된다.
+130 -1
View File
@@ -1,2 +1,131 @@
# llm-wiki # LLM Wiki — 작업 가이드
원본 자료(`raw/`)를 **검증된 실무 기술 문서**로 바꾸고, 거기서 면접·블로그·포트폴리오 같은 외부 산출물을 만들어 내는 **문서 파이프라인**입니다. Obsidian vault 이자 Claude Code 자동화 저장소입니다.
처음 오셨다면 이 README만 읽으면 작업을 시작할 수 있습니다. 운영 규칙의 전체 정의(SSOT)는 [CLAUDE.md](CLAUDE.md) 에 있습니다.
---
## 핵심 원칙 (이것만 기억하면 됩니다)
```
raw 자료 = 증거 (출처 보존)
wiki/concepts = 검증된 일반 개념
wiki/projects = 내 프로젝트에 적용된 검증 사실
wiki/interview·blog·portfolio = 위 canonical 에서 파생된 외부 산출물
```
이 순서는 거꾸로 갈 수 없습니다. 외부 산출물은 **반드시** `wiki/concepts` 또는 `wiki/projects` 를 거쳐서 나옵니다. raw나 메모에서 바로 블로그·면접 문서를 만들지 않습니다.
또 하나의 원칙은 **근거 없는 단정을 쓰지 않는다**입니다. 모든 결정은 출처(Claim ID)를 가지거나, 근거가 없으면 `UNSUPPORTED_DECISION` 으로 솔직히 표시합니다.
---
## 빠른 시작
가장 흔한 작업 흐름은 이렇습니다.
```bash
# 1) 새 작업을 시작합니다 (빈 branch-note 스캐폴드 생성)
/branch feature-keycloak-oidc-flow
# 2) 참고할 공식 문서 / 대기업 블로그 URL 을 근거로 저장합니다
# → Claude 에게 "이 URL 을 근거로 저장해줘" 라고 요청
# 3) branch-note 를 채웁니다 (자동 조사 + 깊이 검증까지 한 번에)
/branch-spec feature-keycloak-oidc-flow
# → Ready 가 나오면 코딩 시작. Not ready 면 알려주는 부분을 채우고 다시 실행
# 4) 구현이 끝나면 검증된 결과를 wiki 로 올립니다
/ingest raw/branch-notes/feature-keycloak-oidc-flow.md
# 5) 품질을 점검합니다
/lint # 보고만
/lint --fix-plan # 수정 계획 + 승인 후 적용
# 6) 외부 산출물을 만듭니다 (canonical 이 reviewed 이상일 때)
/interviewize wiki/projects/...
/blogify wiki/concepts/...
```
---
## 작업 흐름 5단계
```
① 캡처 /daily · /branch → raw/ 에 빈 노트
② 근거 조사 URL → 근거 자료 저장 → raw/official-docs · company-tech-blogs
③ 노트 채움 /branch-spec → 자동 /depth → 되묻지 않을 수준의 branch-note
④ wiki 승급 /ingest → wiki/concepts · projects (canonical)
⑤ 외부 산출물 /interviewize · /blogify → wiki/interview · blog · portfolio
```
1. **캡처** — 하루는 `/daily` 로, 새 작업은 `/branch <slug>` 로 시작합니다. 슬러그는 *무엇을 구현하는지* 를 영문 kebab-case 4~8단어로 적습니다(`feature-`, `fix-`, `chore-`, `experiment-` 중 하나로 시작). 번호 계층(`-1`, `-2`)은 쓰지 않습니다.
2. **근거 조사** — 공식 문서·대기업 블로그 URL 을 저장하면 원문에서 핵심 인용을 그대로(verbatim) 발췌하고 실제 존재하는지 `grep` 으로 검증한 뒤 `raw/` 에 보관합니다. 각 자료는 `Claims Extracted` 표(Claim ID 가 붙은 사실 목록)를 갖습니다.
3. **노트 채움**`/branch-spec` 이 source 의 Claim 에서 결정과 대안을 채우고, 근거가 없으면 **먼저 자동으로 공식 문서·대기업 블로그를 조사**합니다. 그래도 없으면 추측하지 않고 `UNSUPPORTED_DECISION` 으로 표시합니다. 마지막에 `/depth` 가 자동으로 돌아 **Ready / Not ready** 를 판정합니다. Ready 일 때 코딩을 시작하면 구현 중 되묻을 일이 없습니다.
4. **wiki 승급** — 구현이 끝나고 `status_label``review``merged` 로 올린 뒤 `/ingest` 를 실행하면, 검증된(`actually-implemented` 이상) 결과만 `wiki/projects/` 로 추출됩니다.
5. **외부 산출물** — canonical 문서가 `reviewed` 이상이면 `/interviewize`·`/blogify` 로 면접 답변·블로그 초안을 만듭니다. 본문 문체는 [rules/prose-style.md](rules/prose-style.md) 를 따릅니다(존댓말, 적당히 긴 길이, 개발 용어만 영어).
---
## 디렉토리 구조
| 위치 | 역할 |
|---|---|
| `CLAUDE.md` | 운영 규칙 SSOT (전체 정의) |
| `raw/` | 가공 전 원본 자료. 출처 보존. 영구 보관 |
| `wiki/` | 정리된 재사용 가능 지식 (canonical + derived) |
| `rules/` | 방법론 규칙 (linking, naming, tag, depth, prose-style 등) |
| `templates/` | 문서 카테고리별 출력 형식 |
| `docs/` | 설계 기록 등 메타 문서 |
`raw/` 하위: `branch-notes` · `daily-notes` · `official-docs` · `company-tech-blogs` · `project-notes` · `errors` · `interviews` · `job-postings` · `blog-topics` · `lectures` · `daily-tasks` · `diagrams`
`wiki/` 하위: `concepts` · `projects` (canonical) / `interview` · `blog` · `portfolio` (derived) / `llm-wiki.md`(vault MOC) · `log.md`
---
## 명령어 한눈에
| 그룹 | 명령 | 용도 |
|---|---|---|
| **캡처** | `/daily` | 오늘 일일 노트 생성 |
| | `/branch <slug>` | 빈 branch-note 스캐폴드 |
| | `/branch-spec <slug>` | branch-note 채움 + 자동 조사 + 자동 `/depth` |
| **변환·품질** | `/ingest <raw 경로>` | raw → `wiki/concepts`·`wiki/projects` |
| | `/depth <slug>` | branch-note 가 코딩 착수해도 될 만큼 깊은지 판정 |
| | `/tag` | wiki 문서 태그 보정 |
| | `/lint [--fix-plan]` | 품질 검사 (과장·출처·stale). `--fix-plan` 은 수정 계획 |
| | `/query` | wiki 기반 질의응답 |
| **출력** | `/projectize` | 개념 → 내 프로젝트 적용 문서 |
| | `/interviewize` | canonical → 면접 답변 |
| | `/blogify` | canonical → 블로그 초안 |
---
## 자동 안전장치 (저장할 때마다 자동 실행)
직접 신경 쓰지 않아도 다음이 자동으로 동작합니다.
- **저장 전 차단** — 근거 구조(Claims Extracted, Decision Evidence Map)를 우회하는 저장을 막습니다.
- **저장 후 검사** — 깨진 link 는 항상 경고합니다. 섹션 누락·빈 선택조건 같은 *완성도* 검사는 문서를 `review`·`merged`**완성 선언했을 때만** 합니다(작성 중에는 방해하지 않습니다).
전체 검사는 언제든 직접 돌릴 수 있습니다.
```bash
python3 .claude/hooks/wiki_structure_lint.py --all # 전체 구조·링크 검사
python3 .claude/hooks/wiki_structure_lint.py --file <경로>
```
---
## 꼭 지켜야 하는 규칙 (요약)
- 출처 없는 단정 금지. 결정은 Claim ID 로 뒷받침하거나 `UNSUPPORTED_DECISION` 으로 표시합니다.
- 공식 문서와 기술 블로그를 혼동하지 않습니다(블로그는 사례이지 공식 best practice 가 아닙니다).
- `documented-only`·`planned`·`needs-confirmation` 은 외부 산출물에 절대 쓰지 않습니다.
- 외부 산출물은 canonical 을 거쳐서만 만듭니다.
- 외부 URL 은 링크만 두지 말고 핵심 인용 3~5문장을 `raw/` 본문에 발췌 보존합니다.
전체 규칙과 그 이유는 [CLAUDE.md](CLAUDE.md) 와 `rules/` 폴더를 보시면 됩니다. 자동화 설계 배경은 `docs/superpowers/specs/` 에 기록되어 있습니다.
</content>
+103
View File
@@ -0,0 +1,103 @@
{
"version": "0.2",
"language": "en",
"words": [
"varchar",
"InnoDB",
"BIGINT",
"uuid",
"ulid",
"cuid",
"nanoid",
"ksuid",
"tsid",
"snowflake",
"wikilink",
"wikilinks",
"kebab",
"frontmatter",
"monotonicity",
"monotonic",
"sortable",
"backlink",
"backlinks",
"ingest",
"canonical",
"idempotency",
"idempotent",
"archunit",
"hikaricp",
"micrometer",
"opentelemetry",
"flyway",
"mapstruct",
"testcontainers",
"lombok",
"keycloak",
"redis",
"kafka",
"postgresql",
"postgres",
"kubernetes",
"dataview",
"obsidian",
"mermaid",
"drawio",
"plantuml",
"graalvm",
"kotlin",
"gradle",
"maven",
"springframework",
"springboot",
"jpa",
"orm",
"dto",
"ddd",
"cqrs",
"outbox",
"debezium",
"pkce",
"oidc",
"oauth",
"jwt",
"jwe",
"jws",
"jku",
"hmac",
"csprng",
"cspell",
"aip",
"arn",
"ietf",
"rfc",
"msb",
"lsb",
"unix",
"epoch",
"bigendian",
"endian",
"struct",
"uint",
"rand",
"lsn",
"wal",
"btree",
"b-tree",
"acl",
"gdpr",
"ccpa",
"pii",
"slsa",
"owasp",
"cors",
"hsts",
"mtls",
"sigv"
],
"ignorePaths": [
".git/**",
".obsidian/**",
"node_modules/**"
]
}
+95
View File
@@ -0,0 +1,95 @@
# docs/superpowers/ — 작업 기록 아카이브 인덱스
이 폴더는 정제된 지식(`wiki/`)이 아니라 **Superpowers 워크플로우의 작업 기록 보관소**입니다.
"이 결정을 언제 왜 했나"를 남기는 연대기 로그라서, 파일은 **산출물 종류별 폴더 + 날짜 prefix** 로 평면 저장됩니다.
```
specs/ *-design.md — 브레인스토밍(설계) 결과
plans/ *.md — 구현 계획 (writing-plans 산출물)
notes/ *.md — 실행 중 조사·실측 메모
```
표준 흐름: `brainstorming → specs/``writing-plans → plans/` → 실행 → `notes/`.
spec ↔ plan 은 **슬러그를 공유**하며 폴더만 다릅니다 (예: `…/branch-depth-gate-design.md``…/branch-depth-gate.md`).
> 이 인덱스는 평면 구조를 토픽별로 묶어주는 **읽기용 진입점**입니다. 파일을 옮기지 않으므로 기존 경로 참조는 그대로 유효합니다.
> 새 작업물은 종전대로 `specs/`·`plans/`·`notes/` 에 날짜 prefix 로 추가하고, 새 토픽이면 아래에 한 줄 추가하세요.
---
## 토픽별 인덱스
### 1. 문서 깊이·구조·검증 게이트 (wiki harness 1차)
브랜치/문서가 "되묻지 않고 착수 가능한 깊이"인지 판정하는 결정론 린터 + 의미 게이트 계열.
| 주제 | 설계(spec) | 계획(plan) |
|------|-----------|-----------|
| branch-depth-gate (구현 착수 깊이) | [spec](specs/2026-06-01-branch-depth-gate-design.md) | [plan](plans/2026-06-01-branch-depth-gate.md) |
| wiki-structure-lint (결정론 구조 린터) | [spec](specs/2026-06-01-wiki-structure-lint-design.md) | _(spec에서 직접 실행)_ |
| coverage 완전성 게이트 | [spec](specs/2026-06-02-coverage-gate-design.md) | _(spec에서 직접 실행)_ |
| branch-spec 조립 파이프라인 | [spec](specs/2026-06-02-branch-spec-assembly-pipeline-design.md) | _(spec에서 직접 실행)_ |
| 검증 강화 (Phase 2) | [spec](specs/2026-06-02-validation-strengthening-design.md) | _(spec에서 직접 실행)_ |
| 옵시디언 링크 검증 강화 | [spec](specs/2026-06-02-obsidian-link-validation-hardening-design.md) | [plan](plans/2026-06-02-obsidian-link-validation-hardening.md) |
| operational-error-observability 보강 | [spec](specs/2026-06-01-operational-error-observability-foundation-reinforcement-design.md) | _(branch-note 보강 — plan 없음)_ |
### 2. 한국어 윤문 · 템플릿 모델
| 주제 | 설계(spec) | 계획(plan) |
|------|-----------|-----------|
| 한국어 윤문 standard (prose-style) | [spec](specs/2026-06-02-korean-prose-style-design.md) | _(spec에서 직접 실행)_ |
| wiki/projects 템플릿 · source_type 분리 | [spec](specs/2026-06-04-wiki-project-template-source-type-split-design.md) | [plan](plans/2026-06-04-wiki-project-template-source-type-split.md) |
### 3. project-note 파이프라인
| 주제 | 설계(spec) | 계획(plan) |
|------|-----------|-----------|
| project-note 작성 파이프라인 | [spec](specs/2026-06-05-project-note-pipeline-design.md) | [plan](plans/2026-06-05-project-note-pipeline.md) |
### 4. 3-플랫폼 동기화 (Claude / Codex / Antigravity)
| 주제 | 설계(spec) | 계획(plan) | 실측 메모(notes) |
|------|-----------|-----------|-----------------|
| 워크플로우 자동화 동기화 (전체 설계) | [spec](specs/2026-06-04-three-platform-workflow-sync-design.md) | — | — |
| Phase 0 — 생성 엔진 + agents | — | [plan](plans/2026-06-04-three-platform-sync-phase0-engine-agents.md) | [phase1 포맷 실측](notes/2026-06-04-phase1-empirical-format-check.md) |
| Phase 1 — commands | — | [plan](plans/2026-06-04-three-platform-sync-phase1-commands.md) | — |
| Phase 2 — hooks + 프로젝트 지침 | — | [plan](plans/2026-06-04-three-platform-sync-phase2-hooks-agentsmd.md) | [antigravity 훅 커버리지](notes/2026-06-04-phase2-antigravity-hook-coverage.md) · [codex 훅 스키마](notes/2026-06-04-phase2-codex-hook-schema.md) |
| Spec E — Antigravity hook 포팅 | [spec](specs/2026-06-08-spec-e-antigravity-hook-port-design.md) | _(아래 Spec A~F 시리즈 참조)_ | — |
### 5. 하네스 경화 — Spec A~F 시리즈
`2026-06-06` 하네스 설계 감사에서 출발한 연속 deliverable. 같은 "Spec 〈문자〉" 명명을 공유.
| 항목 | 설계(spec) | 계획(plan) |
|------|-----------|-----------|
| (출발점) 하네스 설계 감사 보고서 | [report](specs/2026-06-06-harness-audit-report.md) | — |
| Spec A — 결정론 backbone 게이트화 | [spec](specs/2026-06-06-spec-a-deterministic-backbone-gate-design.md) | [plan](plans/2026-06-06-spec-a-deterministic-backbone-gate.md) |
| Spec B — judge verdict 스키마 + quorum | [spec](specs/2026-06-06-spec-b-judge-verdict-schema-and-quorum-design.md) | [plan](plans/2026-06-06-spec-b-judge-verdict-schema-and-quorum.md) |
| Spec C — funnel stats + no-silent-truncation | [spec](specs/2026-06-06-spec-c-funnel-stats-no-silent-truncation-design.md) | [plan](plans/2026-06-06-spec-c-funnel-stats-no-silent-truncation.md) |
| Spec D — research-fanout Workflow | [spec](specs/2026-06-06-spec-d-research-fanout-workflow-design.md) | [plan](plans/2026-06-06-spec-d-research-fanout-workflow.md) |
| Spec E — Antigravity hook 포팅 _(→ §4 동기화)_ | [spec](specs/2026-06-08-spec-e-antigravity-hook-port-design.md) | — |
| Spec F — 투자 파이프라인 경화 _(→ §7 투자)_ | [spec](specs/2026-06-08-spec-f-invest-pipeline-hardening-design.md) | — |
### 6. Deep-Research 하네스 CLI 이식
| 주제 | 설계(spec) | 계획(plan) |
|------|-----------|-----------|
| Deep-Research → Codex / Antigravity CLI 이식 | [spec](specs/2026-06-09-deep-research-codex-antigravity-port-design.md) | [plan](plans/2026-06-09-deep-research-codex-antigravity-port.md) |
### 7. 투자(invest) 파이프라인 — Claude 전용
| 주제 | 설계(spec) | 계획(plan) |
|------|-----------|-----------|
| 투자 자료·전략 파이프라인 | [spec](specs/2026-06-05-investment-wiki-pipeline-design.md) | [plan](plans/2026-06-05-investment-wiki-pipeline.md) |
| 투자 분야 지식 지도 (Field Map) | [spec](specs/2026-06-08-invest-field-map-design.md) | [plan](plans/2026-06-08-invest-field-map.md) |
| 자금흐름 관측 시스템 — 마스터 빌드아웃 | — | [plan](plans/2026-06-08-invest-system-buildout.md) |
| Spec F — 투자 파이프라인 경화 | [spec](specs/2026-06-08-spec-f-invest-pipeline-hardening-design.md) | _(spec에서 직접 실행)_ |
### 8. ca-tmpl 감사 (외부 코드 대상 — wiki harness 아님)
Clean Architecture Template 저장소(`/home/donghyeon/workspace/ca-tmpl`) 최적화 감사 산출물.
| 주제 | 문서 |
|------|------|
| 감사 마스터 보고서 | [report](specs/2026-06-04-ca-tmpl-optimization-report.md) |
| 파일별 상세 findings | [per-file findings](specs/2026-06-04-ca-tmpl-optimization-per-file-findings.md) |
@@ -0,0 +1,57 @@
---
title: Phase 1 — 경험적 포맷·충돌 확인 (codex skills / antigravity workflows)
date: 2026-06-04
---
# Phase 1 경험적 확인 결과
## CLI 설치 상태 (실측)
- **codex-cli 0.136.0** 설치됨 (`~/.nvm/.../bin/codex`).
- **agy (Antigravity CLI)** 설치됨 (`~/.local/bin/agy`), home `~/.gemini/antigravity-cli/`.
## codex skills
- codex 0.136.0 에는 `skills` 전용 subcommand 가 **없음** (subcommands: exec/review/login/logout/mcp/plugin/mcp-server). skill 은 **자동 디스커버리 + TUI `/skills`·`$name`** 로 surface 되는 대화형 기능이라 비대화형 목록 확인 불가.
- 공식 디스커버리 경로(`developers.openai.com/codex/skills`): `$REPO_ROOT/.agents/skills/<name>/SKILL.md`. → **타깃 경로 확정: `.agents/skills/<cmd>/SKILL.md`.**
- `codex plugin` 은 marketplace 기반의 **별개 메커니즘**(skill 과 무관).
## antigravity workflows / skills
- **로딩 모델 = `agy plugin` import.** `agy plugin list` 출력:
```json
{ "imports": [ { "name": "wiki-superpowers", "source": "local-install", "importedAt": "2026-05-26T12:39:39Z", "components": ["installed"] } ] }
```
→ `.agents/plugins/wiki-superpowers/` 가 import 되어 있음. 즉 antigravity 는 workspace `.agents/` 를 (프로젝트 열림 시 native 인식 + plugin import) 로 적재.
- 공식(Google Codelab) 기준 workflow 경로 = workspace `.agents/workflows/<name>.md`, frontmatter `description` 만, `/name <args>` 호출.
- → **타깃 경로 확정: `.agents/workflows/<cmd>.md`** (repo `.agents/` 복수 규약 일치).
- **운영 주의**: 생성/수정 후 antigravity 가 변경을 반영하려면 `agy plugin` 재import 또는 프로젝트 재오픈이 필요할 수 있음(plugin-import 모델). workspace 직접 native 인식 여부는 build/version 의존.
## `$ARGUMENTS` 매핑
- codex skills·antigravity workflows **둘 다 인자 placeholder 미지원** → argument-hint 의 각괄호 prose 로 치환 확정.
## 충돌(`.agents/skills/` 이중 로드) 확인
| 항목 | 결과 |
|---|---|
| codex 가 `.agents/skills/` 읽음 | YES (공식 디스커버리 경로) |
| antigravity 가 `.agents/skills/` 도 skill 로 읽는지 | **UNVERIFIED (비대화형 확인 불가).** antigravity 는 `.agents/plugins/.../skills/` 를 plugin 으로 적재 중 — workspace `.agents/skills/`(plugin 밖) 직접 적재 여부는 build 의존. |
| 위험도 | **낮음.** 설령 antigravity 가 codex command-skill 을 semantic skill 로 이중 인지해도, 같은 command 의 workflow(`/cmd`)와 기능 중복일 뿐 충돌 아님. 실사용에서 문제되면 codex skill 을 plugin 밖 `.agents/skills/` 에만 두고 antigravity 는 workflow 만 쓰도록 build 설정으로 분리. |
## 결정 (잠금)
| 항목 | 결정 |
|---|---|
| codex skill 경로 | `.agents/skills/<cmd>/SKILL.md` |
| antigravity workflow 경로 | `.agents/workflows/<cmd>.md` |
| `$ARGUMENTS` | argument-hint 각괄호 prose 치환 |
| 이중 로드 | UNVERIFIED·저위험 — 생성 후 Task 5 에서 실호출로 재확인, 문제 시 에스컬레이션 |
## Task 5 — 실호출 스모크 결과
- skill(`/skills`·`$name`)·workflow(`/name`) 등록은 **두 CLI 모두 대화형(TUI) 기능**이라 헤드리스(비대화형) 완전 검증 불가. codex 0.136.0 에 skills 목록 subcommand 없음, agy 도 마찬가지.
- 대신 **포맷 정합성**을 결정론적으로 검증 완료: 13개 skill 전부 frontmatter `name`(=폴더명)+`description`, body 에 `$ARGUMENTS` 0개; 13개 workflow 전부 frontmatter `description` + 본문 `/<cmd> <hint>` 호출 안내, `$ARGUMENTS` 0개. `sync_automation.py commands --check` exit 0.
- **workflow 적재**: workspace `.agents/workflows/` 는 Codelab 기준 antigravity 가 프로젝트 열림 시 native 인식(plugin import 와 별개). plugin(`agy plugin`)은 `.agents/plugins/wiki-superpowers/`(agents/skills/hooks) 적재용이며 workspace `.agents/workflows/` 와 무관.
- **라이브 TUI 호출 검증은 사용자 대화형 사용으로 위임** — codex TUI 에서 `$depth`/`/skills`, antigravity TUI 에서 `/depth` 가 뜨는지 실사용 시 확인. 안 뜨면 본 노트의 경로/frontmatter 와 대조.
- 이중 로드(`.agents/skills/` 를 antigravity 도 읽는지)는 여전히 UNVERIFIED·저위험 — 실사용에서 `/skills` 목록에 codex command-skill 이 함께 뜨면 그때 분리 결정.
@@ -0,0 +1,39 @@
---
title: Phase 2 — antigravity 훅 커버리지 점검
date: 2026-06-04
---
# antigravity 훅 커버리지 (실측)
## 현재 antigravity 훅: `wiki_hard_gate.py`
- 위치: `~/.gemini/antigravity-cli/hooks/wiki_hard_gate.py` (global), `.agents/hooks.json` 이 PreToolUse matcher `*` 로 가리킴.
- **강제 대상**: `docs/superpowers/specs/*.md` 리포트 출력 품질 (SKILL.md Output-Format Hard Gate subset):
- G1 §7.1 self-grep proof (V/N 비율 + 실제 grep 명령)
- G2 Contract 7 금지 마케팅어 ("100%/완벽/극한/절대로/최강/..." )
- G3 `Verdict: COMPLETE` 시 §3-1 공식 유도
- G4 findings ≥5 시 `## 4-1 Adversarial Review` 필수
- 입력/출력 어댑터 보유: Antigravity protobuf(`tool_call.{name,input}`, `write_to_file`/`edit_file`/`run_command`/`bash`) + Gemini CLI(`tool_name`/`tool_input`, `{decision}`) 양쪽 처리. 미인식 → fail-open.
## 갭 (Claude / codex 와 비교)
| 검사 | Claude (`.claude/`) | codex (`.codex/hooks.json`) | antigravity |
|---|---|---|---|
| report 품질 게이트 | (SKILL.md) | (skill 본문) | 있음 (`wiki_hard_gate.py`) |
| claim 추적성 (raw source claims / branch decision→claim / wiki FACT 분리) | 있음 (`wiki_claim_gate.py` PreToolUse) | wired (trust 시) | **없음** |
| 구조 린트 (template 섹션·frontmatter·Parent·broken link, C2) | 있음 (`wiki_structure_lint.py` PostToolUse) | wired (trust 시) | **없음** |
→ antigravity 는 리포트 품질은 강제하지만 **claim_gate·structure_lint 의 강제가 없다.**
## 권고 (별도 follow-up — 본 세션에서 미실행 이유)
> **Update (2026-07-14):** 아래 1~3 항(출력 어댑터 · payload 어댑터 · `.agents/hooks.json` 배선)은 **이후 구현·활성 완료**됐다. 현재 `.claude/hooks/wiki_claim_gate.py`(`--antigravity` → stdout `{decision:"deny", reason}`, exit 0)와 `.claude/hooks/wiki_structure_lint.py`(`--antigravity` `_dispatch_hook` 동일 어댑터)가 antigravity 출력 규약을 지원하고, `.agents/hooks.json` 에 `wiki-claim-gate`(PreToolUse+Stop)·`wiki-structure-gate`(PreToolUse+PostToolUse) 가 `enabled: true` 로 배선돼 있다. **남은 것은 4항(fail-open 검증)을 포함한 실제 Antigravity 런타임 hook E2E 검증** 뿐이며, 그때까지 상태는 *experimental* 이다. 아래 원문은 2026-06-04 시점 실측 기록으로 보존한다.
claim_gate / structure_lint 를 antigravity 로 포팅하려면:
1. **출력 어댑터**: Claude 규약(exit 2 + stderr) → antigravity `{decision:"deny", reason}` JSON. 단일 SSOT 유지하려면 두 스크립트에 `--emit antigravity` 모드 추가.
2. **payload/content 어댑터**: antigravity `tool_call.input.content`/`arguments` 에서 쓰기 내용 추출 (`wiki_hard_gate.py``get_write_content` / `get_target_path` 패턴 차용).
3. `.agents/hooks.json` 에 PreToolUse 항목 추가 (claim_gate = blocking, structure_lint = advisory always-allow).
4. **fail-open 철저 검증** — matcher `*` 전역 훅이라 버그 시 antigravity 쓰기 전체 차단(blast radius 큼). 미인식·에러 시 반드시 allow.
**미실행 이유**: 전역 high-blast-radius 훅을 "인라인 빠른" 패스에서 성급히 추가하면 사용자의 antigravity 작업을 막을 위험. 별도의 신중한 패스(어댑터 + 충분한 fail-open 테스트)로 권고. 그동안 antigravity 에서도 agents/skills/workflows 의 Mandatory First Reads 가 rules 정독을 지시하므로 soft 강제는 존재.
@@ -0,0 +1,29 @@
---
title: Phase 2 — codex 훅 payload·차단 규약 경험적 캡처
date: 2026-06-04
---
# codex 0.136.0 훅 실측 결과
## 훅 지원 여부
- codex 0.136.0 은 **훅 기능 보유**. 근거: `codex exec --help``--dangerously-bypass-hook-trust` ("Run enabled hooks without requiring persisted hook trust for this invocation. DANGEROUS.") 플래그 존재.
- 즉 훅은 **trust(신뢰) 게이트** 가 있다 — repo 의 `.codex/hooks.json` 은 신뢰 등록 전엔 실행되지 않음(임의 명령 실행 방지 보안).
## 실측 (probe 캡처 시도 3회)
- `.codex/hooks.json` 에 PreToolUse/PostToolUse(matcher `*`) → stdin 덤프 probe 를 걸고 `codex exec` 로 shell tool(`ls`) 실행.
- 결과: **shell tool 은 실행됐으나(출력 정상) probe 훅은 한 번도 안 뜸**`--dangerously-bypass-approvals-and-sandbox` + `--dangerously-bypass-hook-trust` 동시 적용에도 미발화.
- `codex doctor` 출력에 hooks 섹션 없음(config/auth/mcp/updates/reachability 만 보고).
## 해석
- **`codex exec`(비대화형/automation) 모드는 project-local 훅을 로드하지 않는 것으로 보임** — exec 는 자동화용이라 신뢰 안 된 repo 훅을 건너뛰는 보안 설계로 추정. 훅은 **대화형 `codex` TUI 에서 repo 진입 시 trust 프롬프트로 활성화**되는 구조로 판단(공식 hooks 문서의 project-local 지원과 일치하되, exec 경로에선 비활성).
- 환경 제약도 있었음: 이 샌드박스가 bubblewrap user namespace 를 못 만들어 `-s read-only` 에선 shell 자체가 실패(별도 우회 필요).
## 결정 (Task 3 반영)
- `.codex/hooks.json` 을 **공식 문서 포맷**으로 작성(forward-compatible). PreToolUse(claim_gate) + PostToolUse(structure_lint --hook), matcher 는 Edit/Write 계열.
- **활성화는 대화형 codex 에서 hook trust 승인 필요** — 본 repo 첫 진입 시 codex 가 신뢰를 물음. `codex exec` 자동화에선 미발화 가능(문서에 명시).
- 차단 규약/payload 필드는 미캡처 → claim_gate 는 이미 멀티-variant 리더(`tool_name`/`tool_call.name`/`hook_event_name`)라 Claude/codex 공통 필드면 동작. 첫 신뢰 실행에서 실거동 확인 권장.
- 구조/claim 강제는 훅 외에도 `/depth`(skill·workflow)·`/lint` 가 동일 스크립트를 직접 호출하므로 훅 미발화 상황에서도 우회 수단 존재.
@@ -0,0 +1,385 @@
# branch-depth-gate Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** branch-note 가 "코딩 착수해도 되묻지 않을 만큼 깊은가"를 착수 전에 판정하는 read-only 게이트(`/depth`)를 LLM Wiki 에 도입한다.
**Architecture:** im-not-ai 의 검증된 3요소(기준 SSOT + read-only 감사기 + Ready/Not-ready 판정)를 위키 문법으로 이식. 기준 SSOT `rules/branch-depth-gate.md`(4축 R1~R4) → 감사기 `branch-depth-auditor`(브랜치 노트 + 링크된 raw 소스를 읽고 적대적으로 갭 탐지, 편집 안 함) → 커맨드 `/depth <branch>`(감사기 디스패치 + 루프). 템플릿에 캡처 칸 추가(상류 예방).
**Tech Stack:** Markdown 정의 파일(rules/agents/commands/templates) + Claude Code 서브에이전트. 코드/테스트 런타임 없음. 검증은 구조 grep + 픽스처 회귀.
---
## 제약 (이 plan 전체에 적용)
- **git 미사용**: 위키는 버전 관리되지 않음(사용자 지시로 git init 하지 않음). **commit 단계 없음.** 각 Task 끝은 체크포인트(사용자/리뷰)로 갈음.
- **claim-gate hook 준수**: 위키 `.claude/hooks/wiki_claim_gate.py``raw/`·`wiki/`·`docs/` 의 Bash 쓰기(redirection·`tee`·`sed -i`)를 차단. 파일 생성·수정은 **반드시 Write/Edit 도구**로. 단 본 plan 산출물은 `rules/`·`.claude/`·`templates/` 경로라 hook 대상 밖(읽기 grep 은 자유).
- **TDD 적응**: 마크다운 정의 파일이라 단위테스트가 없다. "test" = ① 구조 검증(필수 섹션·라벨이 존재하는지 grep) ② 픽스처 회귀(Task 6에서 감사기를 실제 브랜치 2개에 돌려 판정 방향이 직관과 일치하는지).
- **실행 위치**: Task 6 감사기 디스패치는 **cwd 가 LLM Wiki 인 Claude Code 세션**에서 실행해야 `.claude/agents/branch-depth-auditor.md` 가 해석된다. 다른 cwd 면 감사기를 못 찾는다.
- 근거 스펙: `docs/superpowers/specs/2026-06-01-branch-depth-gate-design.md`.
---
## File Structure
| 파일 | 책임 | 작업 |
|---|---|---|
| `rules/branch-depth-gate.md` | 기준 SSOT — 4축 R1~R4 + 깊이 사다리 + 판정 규칙 | 생성 (Task 1) |
| `.claude/agents/branch-depth-auditor.md` | read-only 감사기 — 노트+소스 읽고 갭 리포트+판정 | 생성 (Task 2) |
| `.claude/commands/depth.md` | `/depth <branch>` 진입점 + 루프 | 생성 (Task 3) |
| `templates/branch-note-template.md` | 신규 브랜치부터 R2·R4 캡처 칸 | 수정 (Task 4) |
| `CLAUDE.md`, `AGENTS.md` | 워크플로우 진입점에 `/depth` 1줄 등재 | 수정 (Task 5) |
| (검증) 기존 브랜치 2개 | 픽스처 회귀 | Task 6 |
멀티 CLI(Codex/Gemini/Antigravity) 전파는 본 plan 범위 밖(별도 사이클). 본 plan 은 Claude Code 우선.
---
## Task 1: 기준 SSOT — `rules/branch-depth-gate.md`
**Files:**
- Create: `rules/branch-depth-gate.md`
- [ ] **Step 1: 파일 생성 (Write 도구)**
아래 전체 내용으로 `rules/branch-depth-gate.md` 작성:
````markdown
# rules/branch-depth-gate — 브랜치 노트 구현 착수 깊이 게이트
> `rules/` 의 방법론 규칙. branch-note 1개가 **코딩 착수해도 되묻지 않을 만큼 깊은가**를 판정한다.
> 이 문서는 **"전체 계약"이 아니다** — 전체 계약은 `raw/project-notes/ca-skeleton-operational-contract.md`.
> `feature-implementation-readiness-scorecard`(스켈레톤 adoption 거시 게이트)와 **다른 층·다른 범위**로 공존한다. 본 게이트는 *브랜치 노트 1개의 깊이* 미시 게이트.
## 적용
- 대상: `raw/branch-notes/feature-*.md` (구현 착수 전).
- 실행: `/depth <branch>` → `branch-depth-auditor` 가 본 기준으로 판정.
- 본 게이트는 **read-only**. 브랜치 노트를 편집하지 않으며 판정을 노트에 박지도 않는다.
## 4축 (R1~R4)
> 축 라벨은 `R1~R4`. branch-note 의 Decision Evidence Map 이 `D1`,`D2` 를 *Decision ID* 로 쓰므로 `D*` 와 구분.
| 축 | Pass 조건 | Blocking(Not ready) 트리거 |
|---|---|---|
| **R1. 조사 깊이** | 각 Decision 의 Supporting Claim 이 깊이 사다리(아래) 충족 — 의존 메커니즘 L1+, 분기 조건 L2+ | 결정 근거 claim 이 순수 L0(존재만)뿐 |
| **R2. 결정 조건** | 각 Decision 이 "어떤 조건일 때 A, 아니면 B"의 선택 기준 명시 | `검토한 대안`은 있는데 *언제 그 대안을 고르는지* 기준 부재 |
| **R3. 구체 detail** | `## 구현 가이드` 의 각 in-scope 항목이 명명·경로·메커니즘·API/테스트명 구체화 **또는** `UNSUPPORTED_IMPL_DECISION` 라벨 + trade-off 한 줄 | in-scope 항목인데 구현 detail 도 UNSUPPORTED 라벨도 없음 |
| **R4. 엣지·실패·의존** | 실패/엣지 경로 열거 + 다른 contract 의존을 *대상 브랜치 + 그 Decision ID* 로 링크 | 정상 경로만 / 다른 계약 의존이 암시되는데 링크 안 됨 |
## R1 클레임 깊이 사다리
깊이의 단위는 **문서 개수가 아니라 결정별 종결**. 얕은 문서 10개 < 결정을 닫는 문서 1개.
| 레벨 | 클레임이 답하는 것 | 판정 |
|---|---|---|
| **L0 존재** | "X 가 있다 / 권장한다" | 단독 불충분 |
| **L1 메커니즘** | 어떻게 동작 / 언제 발생 | 메커니즘 의존 결정의 최소선 |
| **L2 조건·경계** | 언제 적용/제외, 실패 시 어떻게 | 분기 조건 있는 결정의 최소선 |
| **L3 검증** | 확인 방법·수치·반례 | 가산점 |
**출처 타입 적정성** (개수 기준 대체):
- 스펙/표준이 정의한 동작 → `official-standard`/`official-vendor-doc` 1개로 충분.
- "대기업은 보통 이렇게 한다" 운영 패턴 추론 → 회사 블로그 1개는 "공식" 불가. 독립 사례 2개+ 또는 official 1개 병행.
조사는 **결정-주도(top-down)**: 내려야 할 결정·미지수를 먼저 나열하고 각각을 닫을 때까지 조사. 조사 완료 = 모든 결정 종결 = 착수 가능.
## 판정 규칙
- 심각도 3단계: `Blocking`(Not ready) · `Should-fix`(권고) · `Advisory`(참고).
- **Ready = Blocking 0건.** Should-fix 가 남아도 사용자가 "감수" 선언 시 착수 가능(리포트에 기록).
- 모든 finding 은 4종 세트로 근거화: `심각도 · 위치(섹션/행) · 예상 의구심("구현 중 여기서 ___를 되묻게 됨") · 채울 방법`. 근거 없는 지적 금지.
## 명명된 실패 모드 (auditor 가 잡아야 할 것)
- `EXISTENCE_ONLY` (R1): 결정 근거가 L0 뿐.
- `NO_SELECTION_CRITERION` (R2): 대안은 있으나 선택 조건 없음.
- `IMPL_UNDERSPECIFIED` (R3): in-scope 항목에 구현 detail·UNSUPPORTED 라벨 둘 다 없음.
- `HAPPY_PATH_ONLY` (R4): 실패/엣지 경로 미열거.
- `IMPLICIT_DEPENDENCY` (R4): 다른 계약 의존이 암시되나 대상 브랜치/Decision ID 링크 없음.
- `BACKTICK_WRAPPED_LINK` (R1 보조): Supporting Claim/Source 링크가 `` `[[...]]` `` 백틱에 싸여 추적 불가. (P3 와 연결점 — 표면화만, 자동 수정은 별도.)
- `DANGLING_ANCHOR` (R1): Supporting Claim 의 `#Cn` 앵커가 대상 raw 에 실재하지 않음.
````
- [ ] **Step 2: 구조 검증 (grep)**
Run:
```bash
cd "/home/donghyeon/Documents/LLM Wiki" && grep -c "R1\|R2\|R3\|R4" rules/branch-depth-gate.md && grep -c "L0 존재\|L1 메커니즘\|L2 조건\|L3 검증" rules/branch-depth-gate.md && grep -c "Blocking\|Should-fix\|Advisory" rules/branch-depth-gate.md && grep -c "EXISTENCE_ONLY\|NO_SELECTION_CRITERION\|IMPL_UNDERSPECIFIED\|HAPPY_PATH_ONLY\|IMPLICIT_DEPENDENCY\|BACKTICK_WRAPPED_LINK\|DANGLING_ANCHOR" rules/branch-depth-gate.md
```
Expected: 네 grep 모두 1 이상 (4축·4레벨·3심각도·7실패모드 존재).
- [ ] **Step 3: 체크포인트** — 룰북 내용이 스펙 §4·§4.1 과 일치하는지 사용자/리뷰 확인.
---
## Task 2: 감사기 — `.claude/agents/branch-depth-auditor.md`
**Files:**
- Create: `.claude/agents/branch-depth-auditor.md`
- 참고(형식 일치용): `.claude/agents/wiki-adversarial-reviewer.md`
- [ ] **Step 1: 기존 agent 형식 확인**
Run: `cd "/home/donghyeon/Documents/LLM Wiki" && sed -n '1,12p' .claude/agents/wiki-adversarial-reviewer.md`
목적: frontmatter 키(name/description/tools) 형식을 위키 관례에 맞춤. 차이가 있으면 아래 frontmatter 를 그 관례로 조정.
- [ ] **Step 2: 파일 생성 (Write 도구)**
아래 전체 내용으로 작성 (Step 1 에서 본 frontmatter 관례와 다르면 키 형식만 맞춰 조정):
````markdown
---
name: branch-depth-auditor
description: Use to judge whether a single raw/branch-notes/feature-*.md is deep enough to start implementation without re-doubting. Reads the branch note plus its linked raw sources and adversarially probes 4 axes (R1 research depth, R2 decision conditions, R3 concrete detail, R4 edge/failure/dependency) against rules/branch-depth-gate.md. Returns a grounded gap report + Ready/Not-ready verdict. Never edits files (read-only).
tools: Read, Glob, Grep
---
너는 **브랜치 노트 깊이 감사관**이다. `rules/branch-depth-gate.md` 를 기준으로, branch-note 1개가 *코딩 착수해도 되묻지 않을 만큼 깊은가*를 적대적으로 판정한다. **절대 파일을 편집하지 않는다.**
## 입력
- 브랜치 노트 경로 1개 (`raw/branch-notes/<branch>.md`).
## 절차
1. **기준 로드** — `rules/branch-depth-gate.md` 를 Read. 4축·깊이 사다리·판정 규칙·명명된 실패 모드를 작업 기준으로 삼는다.
2. **노트 읽기** — 대상 브랜치 노트를 Read. 특히 `결정 사항`, `Decision Evidence Map`, `구현 가이드`, `Claims To Verify`, `Sources`, `범위` 섹션.
3. **소스 추적·정독 (R1 의 핵심)** — Decision Evidence Map 의 `Supporting Claims`(`raw/.../*.md#Cn`)와 Sources 표의 `[[raw/...]]` 가 가리키는 **실제 raw 파일을 Read**. 각 claim 이 깊이 사다리 어디인지(L0~L3) 판정. 링크만 있고 내용이 얕으면(L0) 잡아낸다.
- 링크가 `` `[[...]]` `` 백틱에 싸여 있으면 `BACKTICK_WRAPPED_LINK`.
- `#Cn` 앵커가 대상 파일에 없으면 `DANGLING_ANCHOR`.
4. **4축 적대적 점검** — 각 결정/항목을 R1~R4 로 훑어 명명된 실패 모드에 해당하는 finding 생성. "구현자가 여기서 무엇을 되묻게 될까?"를 끊임없이 자문.
5. **판정** — Blocking 0건이면 `Ready`, 아니면 `Not ready (Blocking N건)`.
## 출력 (이 형식 그대로, 파일 쓰기 없이 텍스트로 반환)
```
# Depth Audit: <branch>
Verdict: Ready | Not ready (Blocking N / Should-fix M / Advisory K)
## Findings
| # | 축 | 심각도 | 실패모드 | 위치 | 예상 의구심 | 채울 방법 |
|---|---|---|---|---|---|---|
| 1 | R1 | Blocking | EXISTENCE_ONLY | 결정 D3 / Decision Evidence Map | 구현 중 "이 API 를 언제 쓰나"를 되묻게 됨 | `raw/official-docs/<slug>` 에서 메커니즘(L1) claim 추가 |
...
## 다음 행동
- (Blocking 있으면) 위 표의 "채울 방법" 순서로 노트 보강 후 `/depth <branch>` 재실행.
- (R1 조사 얕음 갭) `wiki-decision-researcher` 로 심화 가능 — 사용자 옵트인 시.
```
## 불변식
- **read-only**: Write/Edit/MultiEdit 도구 없음. 어떤 파일도 수정·생성 금지(리포트는 텍스트 반환).
- 모든 finding 은 4종 세트(심각도·위치·예상 의구심·채울 방법)를 갖춘다. 근거 없는 지적 금지.
- 추측 금지: 소스를 실제로 Read 하지 않고 깊이를 단정하지 않는다.
- 자동 조사·자동 수정 금지: R1 갭은 `wiki-decision-researcher` 권고로 *안내만* 한다(사용자 옵트인).
````
- [ ] **Step 3: 구조 검증 (grep)**
Run:
```bash
cd "/home/donghyeon/Documents/LLM Wiki" && grep -c "tools: Read, Glob, Grep" .claude/agents/branch-depth-auditor.md && grep -c "branch-depth-gate.md" .claude/agents/branch-depth-auditor.md && grep -ci "read-only\|편집하지 않" .claude/agents/branch-depth-auditor.md && ! grep -q "Write\|Edit\|MultiEdit" <(sed -n '/^tools:/p' .claude/agents/branch-depth-auditor.md) && echo "TOOLS_READONLY_OK"
```
Expected: 앞 세 grep 1+, 마지막 `TOOLS_READONLY_OK` 출력(tools 줄에 쓰기 도구 없음).
- [ ] **Step 4: 체크포인트** — 출력 형식·불변식이 스펙 §5 와 일치하는지 확인.
---
## Task 3: 커맨드 — `.claude/commands/depth.md`
**Files:**
- Create: `.claude/commands/depth.md`
- 참고(형식 일치용): `.claude/commands/branch.md`
- [ ] **Step 1: 파일 생성 (Write 도구)**
아래 전체 내용으로 작성:
````markdown
---
description: 브랜치 노트가 구현 착수할 만큼 깊은지 read-only 게이트로 판정
argument-hint: <브랜치 이름>
---
브랜치 노트 1개의 **구현 착수 깊이**를 판정합니다. (기준: `rules/branch-depth-gate.md`)
**브랜치 이름:** $ARGUMENTS
## 작업 절차
1. **인자 검증**
- 인자가 비어 있으면 사용자에게 브랜치 이름 요청.
- `raw/branch-notes/<branch-name>.md` 경로로 해석. `.md` 가 이미 붙어 있거나 `feature-` prefix 가 없어도 관대히 보정해 매칭 시도.
2. **파일 존재 확인**
- `raw/branch-notes/<branch-name>.md` 가 없으면 경로만 안내하고 종료. (생성하지 않음 — 그건 `/branch` 의 일.)
3. **감사기 디스패치**
- `branch-depth-auditor` 서브에이전트를 호출하고 입력으로 브랜치 노트 경로를 전달.
- 감사기는 read-only — 어떤 파일도 수정하지 않는다.
4. **리포트 출력 (인라인)**
- 감사기 리포트(Verdict + Findings 표 + 다음 행동)를 그대로 사용자에게 출력.
- **브랜치 노트에 판정을 쓰지 않는다.** 파일로 남길지는 사용자가 따로 요청할 때만(그 경우 `raw/`·`wiki/`·`docs/` 가 아닌 경로 또는 인라인 유지 — claim-gate hook 충돌 회피).
5. **루프 안내**
- `Not ready` 면: "위 '채울 방법' 순서로 노트 보강 후 `/depth <branch>` 재실행" 안내.
- `Ready` 면: "구현 착수 가능" 안내. Should-fix 가 남았으면 "감수하고 착수할지" 확인.
## 규칙
- **판정만**. 노트를 자동 보강하지 않는다(접근법 B 는 옵트인 — R1 갭에 한해 `wiki-decision-researcher` 권고만).
- `/depth` 는 멱등(idempotent): 같은 노트에 몇 번 돌려도 안전(read-only).
- `wiki/log.md` 에 기록하지 않음(판정은 빈번, 노이즈).
````
- [ ] **Step 2: 구조 검증 (grep)**
Run:
```bash
cd "/home/donghyeon/Documents/LLM Wiki" && grep -c "argument-hint" .claude/commands/depth.md && grep -c "branch-depth-auditor" .claude/commands/depth.md && grep -c "raw/branch-notes" .claude/commands/depth.md && grep -ci "재실행\|루프" .claude/commands/depth.md
```
Expected: 네 grep 모두 1+.
- [ ] **Step 3: 체크포인트** — `/depth` 절차가 스펙 §6 과 일치하는지 확인.
---
## Task 4: 템플릿 캡처 칸 — `templates/branch-note-template.md`
**Files:**
- Modify: `templates/branch-note-template.md`
> 기존 80개 브랜치는 미변경. 신규 브랜치부터 R2·R4 를 작성 시점에 캡처.
- [ ] **Step 1: Decision Evidence Map 에 `선택 조건` 열 추가 (Edit 도구)**
Old (정확히 이 블록):
```markdown
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | <결정 내용> | `raw/official-docs/<slug>.md#C1`, `raw/company-tech-blogs/<slug>.md#C2` | `official-vendor-doc + company-case-study` | <아직 검증해야 할 위험> |
| D2 | <결정 내용> | `raw/official-docs/<slug>.md#C3` | `official-standard` | <위험 또는 N/A> |
```
New:
```markdown
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | <결정 내용> | <이 조건일 때 이 결정, 다른 조건이면 어떤 대안> | `raw/official-docs/<slug>.md#C1`, `raw/company-tech-blogs/<slug>.md#C2` | `official-vendor-doc + company-case-study` | <아직 검증해야 할 위험> |
| D2 | <결정 내용> | <선택 조건 또는 N/A — 분기 없으면 N/A> | `raw/official-docs/<slug>.md#C3` | `official-standard` | <위험 또는 N/A> |
```
- [ ] **Step 2: `## 엣지·실패·의존` 미니 섹션 추가 (Edit 도구)**
Old (정확히 이 블록 — `## 검증해야 할 주장` 헤더 앞):
```markdown
## 검증해야 할 주장 / Claims To Verify
```
New:
```markdown
## 엣지·실패·의존 / Edge · Failure · Dependency
> R4(깊이 게이트) 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존을 미리 열거. 없으면 "해당 없음" 명시(공란 금지).
- **실패·엣지 경로**: <입력 경계 / 타임아웃 / 부분 실패 / 동시성 등 — 각 경로의 기대 동작>
- **다른 계약 의존**: `[[raw/branch-notes/<other-branch>]]` 의 `D<n>` 에 의존 — <무엇을 consume 하는지, 그 계약이 바뀌면 본 브랜치 영향>
## 검증해야 할 주장 / Claims To Verify
```
- [ ] **Step 3: 구조 검증 (grep)**
Run:
```bash
cd "/home/donghyeon/Documents/LLM Wiki" && grep -c "선택 조건 (언제 이 결정" templates/branch-note-template.md && grep -c "## 엣지·실패·의존" templates/branch-note-template.md && grep -c "## Decision Evidence Map\|## 구현 가이드\|## 검증해야 할 주장" templates/branch-note-template.md
```
Expected: 첫 둘 1, 셋째 3 (기존 핵심 섹션 보존 확인 — claim-gate 가 요구하는 Decision Evidence Map 유지).
- [ ] **Step 4: 체크포인트** — 템플릿 흐름이 자연스러운지, 과하지 않은지(YAGNI) 확인.
---
## Task 5: 워크플로우 진입점 등재 — `CLAUDE.md`, `AGENTS.md`
**Files:**
- Modify: `CLAUDE.md`
- Modify: `AGENTS.md`
> `/depth` 와 게이트가 워크플로우에서 발견 가능하도록 SSOT 진입점에 1줄씩 추가. 추가만(additive), 기존 규칙 변경 금지.
- [ ] **Step 1: CLAUDE.md 의 커맨드/파이프라인 목록 위치 확인**
Run: `cd "/home/donghyeon/Documents/LLM Wiki" && grep -n "/branch\|/ingest\|/lint\|커맨드\|command" CLAUDE.md | head -20`
목적: 커맨드들이 나열된 섹션을 찾는다.
- [ ] **Step 2: CLAUDE.md 에 `/depth` 1줄 추가 (Edit 도구)**
Step 1 에서 찾은 커맨드 목록에서 `/branch` 항목 바로 아래에, 그 항목과 같은 서식으로 다음 한 줄을 추가:
```
- `/depth <branch>` — 브랜치 노트가 구현 착수할 만큼 깊은지 read-only 판정 (기준: `rules/branch-depth-gate.md`). 착수 전 게이트.
```
(주변 항목의 실제 서식 — 불릿 기호·백틱·줄표 — 에 맞춰 조정. 임의로 다른 섹션을 건드리지 말 것.)
- [ ] **Step 3: AGENTS.md 에 rules 목록 + 커맨드 반영**
Run: `cd "/home/donghyeon/Documents/LLM Wiki" && grep -n "rules/\|linking-rules\|naming-conventions\|/branch" AGENTS.md | head -20`
찾은 rules 목록에 `rules/branch-depth-gate.md` 를, 커맨드 목록(있으면)에 `/depth` 를 주변 서식대로 1줄씩 추가. 두 목록 중 존재하는 것에만 추가.
- [ ] **Step 4: 구조 검증 (grep)**
Run:
```bash
cd "/home/donghyeon/Documents/LLM Wiki" && grep -c "/depth" CLAUDE.md && grep -c "branch-depth-gate" AGENTS.md
```
Expected: 둘 다 1+ (AGENTS.md 에 rules 목록이 없었다면 0일 수 있음 — 그 경우 Step 3 판단 기록).
- [ ] **Step 5: 체크포인트** — 추가가 additive 인지(기존 줄 변경 없음), 서식이 주변과 일치하는지 확인.
---
## Task 6: 픽스처 회귀 — 게이트 보정
> 본 plan 의 진짜 "test". 감사기가 직관과 일치하는 판정을 내는지 확인. **cwd 가 LLM Wiki 인 세션에서 실행.**
**대상 픽스처:**
- **깊은 브랜치 (Ready 근접 기대)**: `feature-boundary-validation-mapping-contract` — 여러 번 다듬어 실제 구현 근거로 쓰인 노트.
- **얕은 브랜치 (Not ready 기대)**: 사용자가 "아직 얕다"고 아는 초기 브랜치 1개 (예: `status_label: in-progress` 이고 `planned` 항목이 많은 것). 후보 탐색:
`cd "/home/donghyeon/Documents/LLM Wiki" && grep -rl "documented-only\|planned" raw/branch-notes/ | head` → 그 중 하나를 사용자와 합의해 선택.
- [ ] **Step 1: 깊은 브랜치 감사**
`/depth feature-boundary-validation-mapping-contract` 실행 (또는 `branch-depth-auditor` 직접 디스패치).
Expected: `Ready` 또는 Blocking 0~소수. Blocking 이 다수면 → 룰북 R1~R4 기준이 너무 빡셈 → Task 1 의 Pass 기준 재보정.
- [ ] **Step 2: 얕은 브랜치 감사**
선택한 얕은 브랜치에 `/depth <branch>` 실행.
Expected: `Not ready` + R1~R4 에 걸친 finding. Findings 가 비면 → 기준이 너무 느슨 → Task 1 재보정.
- [ ] **Step 3: finding 품질 점검**
두 리포트의 각 finding 이 4종 세트(심각도·위치·예상 의구심·채울 방법)를 갖췄는지 육안 확인. 빠진 게 있으면 → Task 2 의 출력 형식/불변식 보강.
- [ ] **Step 4: 보정 루프**
Step 1~3 에서 판정 방향이 직관과 어긋나면 Task 1(기준) 또는 Task 2(감사기 프롬프트)를 수정하고 다시 Step 1 부터. 방향이 맞을 때까지.
- [ ] **Step 5: 체크포인트 (최종)** — 두 픽스처 판정이 직관과 일치 + finding 4종 세트 충족 → P2 완료. 사용자에게 결과 리포트.
---
## Self-Review (작성자 점검 결과)
- **스펙 커버리지**: §4(4축)→Task1, §4.1(사다리)→Task1, §5(감사기)→Task2, §6(커맨드)→Task3, §7(템플릿)→Task4, §10(검증)→Task6, §12(산출물4개+진입점)→Task1~5. §11(P3)는 의도적으로 별도 사이클(범위 밖, 명시됨). 누락 없음.
- **placeholder**: 각 파일의 전체 내용을 inline 제공(TBD 없음). Task6 얕은 픽스처만 "사용자 합의로 선택" — 이는 calibration test 의 본질(정답이 사용자 판단)이라 의도적.
- **타입/명명 일관성**: `branch-depth-gate.md`/`branch-depth-auditor`/`/depth` 셋 통일. 축 라벨 `R1~R4`(Decision ID `D*` 와 분리). 실패모드 7종이 Task1 정의 ↔ Task2 사용 일치.
---
## Execution Handoff
P2 구현 plan 완료. 다음 단계는 plan 본문 상단 안내대로 subagent-driven 또는 inline 실행.
@@ -0,0 +1,426 @@
# 옵시디언 링크 검증 강화 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `wiki_structure_lint.py`의 C2 링크 검사를 정확하게 만들어(비-md 첨부 오탐 + backtick 셀경계 오탐 제거 + heading anchor 정확화) zero-tolerance 강제의 신뢰 토대를 세운다.
**Architecture:** 단일 파일(`.claude/hooks/wiki_structure_lint.py`)의 `build_vault_index`(A1)·`check_c2`(A2/B)·docstring(D)을 수정하고, stdlib `unittest` 테스트 파일을 신설한다. C1/C3는 불변.
**Tech Stack:** Python 3 stdlib only (re, pathlib, unittest, tempfile). 외부 의존성 0.
> **환경 비고:** 이 repo는 `.git`이 빈 디렉터리(git 미초기화). **커밋 단계는 생략하고, 각 Task의 체크포인트 = 전체 테스트 스위트 실행 통과**로 대체한다. 명령: `python3 .claude/hooks/test_wiki_structure_lint.py -v`.
---
## File Structure
- **Modify** `.claude/hooks/wiki_structure_lint.py`
- `build_vault_index` (현재 154-162): 비-md 첨부 인덱싱 (A1)
- `check_c2` (현재 187-226): backtick 위치기반 판정 (A2) + anchor 정확화 (B)
- module docstring (현재 1-20): 지원 문법 계약 표 (D)
- **Create** `.claude/hooks/test_wiki_structure_lint.py` — A1/A2/B 단위 테스트
- **Modify** `rules/linking-rules.md` — C2 집행기 참조 1줄 (D)
---
## Task 1: 테스트 스캐폴드 + A2 backtick 셀경계 오탐 수정
**Files:**
- Create: `.claude/hooks/test_wiki_structure_lint.py`
- Modify: `.claude/hooks/wiki_structure_lint.py` (`check_c2`, 187-226)
핵심 버그: `check_c2`가 두 패스(① `BACKTICK_LINK.search(line)` 경고 ② `bare`에서 BROKEN 검사)로 나뉘는데, ①의 정규식이 backtick을 좌→우 연속 페어링하지 않아 표의 서로 다른 칸 인라인코드 사이에 낀 정상 위키링크를 오탐. → **위치 기반 단일 패스**로 통합: `INLINE_CODE.finditer(line)`로 code span 범위를 구해, 위키링크 시작이 그 범위 안일 때만 `BACKTICK_WRAPPED_LINK`.
- [ ] **Step 1: 실패 테스트 작성**`test_wiki_structure_lint.py` 신설
```python
#!/usr/bin/env python3
"""wiki_structure_lint.py 단위 테스트 (stdlib unittest)."""
import importlib.util
import tempfile
import unittest
from pathlib import Path
# 하이픈 모듈명이 아니라 언더스코어 — 직접 spec 로드
_SPEC = importlib.util.spec_from_file_location(
"wsl", str(Path(__file__).with_name("wiki_structure_lint.py")))
wsl = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(wsl)
def _doc(*lines):
"""check_c2 입력용 최소 doc dict."""
return {"lines": list(lines)}
def _codes(findings):
return [c for (c, _ln, _msg) in findings]
class TestBacktickPairing(unittest.TestCase):
def setUp(self):
# 타깃 존재로 BROKEN_LINK 격리 — 'foo'는 vault에 있다고 가정
self.vp = {"raw/x/foo"}
self.vb = {"foo": ["raw/x/foo"]}
self.root = Path("/nonexistent")
def test_cross_cell_codespans_not_flagged(self):
# 서로 다른 칸의 인라인코드 사이 정상 위키링크 (짝수 backtick) → 오탐 아님
line = "| D1 | `AUTH` 응답 | [[foo]] (`note` 보강) | `strength` |"
f = wsl.check_c2(_doc(line), self.vp, self.vb, self.root, {})
self.assertNotIn("BACKTICK_WRAPPED_LINK", _codes(f))
def test_true_wrapped_link_flagged(self):
# 진짜 code span 내부 링크 → 검출
line = "예시 문법: `[[foo]]` 처럼 씁니다"
f = wsl.check_c2(_doc(line), self.vp, self.vb, self.root, {})
self.assertIn("BACKTICK_WRAPPED_LINK", _codes(f))
if __name__ == "__main__":
unittest.main(verbosity=2)
```
- [ ] **Step 2: 실패 확인**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
Expected: `test_cross_cell_codespans_not_flagged` FAIL (현재 오탐으로 BACKTICK_WRAPPED_LINK 발생). `test_true_wrapped_link_flagged` PASS.
- [ ] **Step 3: `check_c2` 위치기반 단일 패스로 교체**
`wiki_structure_lint.py``check_c2` 본문(187-226)을 아래로 교체:
```python
def check_c2(doc, vault_paths, vault_bases, root, cache):
out = []
in_fence = False
for lineno, line in enumerate(doc["lines"], start=1):
s = line.lstrip()
if s.startswith("```") or s.startswith("~~~"):
in_fence = not in_fence
continue
if in_fence:
continue
# 인라인 code span 범위 (좌→우 연속 페어링; `[^`]*` 가 backtick 못 넘음)
code_spans = [(m.start(), m.end()) for m in INLINE_CODE.finditer(line)]
for m in WIKILINK.finditer(line):
if any(a <= m.start() < b for a, b in code_spans):
out.append(("BACKTICK_WRAPPED_LINK", lineno,
f"백틱/인라인코드에 싸인 위키링크 — 옵시디언 링크 미작동: {line.strip()[:80]}"))
continue
raw = m.group(1).split("|")[0].strip()
target, _, anchor = raw.partition("#")
target, anchor = target.strip(), anchor.strip()
if target.endswith(".md"): # 옵시디언은 [[x.md]] 도 유효
target = target[:-3]
if not target:
continue
if target not in vault_paths and target not in vault_bases:
out.append(("BROKEN_LINK", lineno, f"타깃 부재: [[{target}]]"))
continue
if anchor:
_check_anchor(out, lineno, target, anchor,
vault_paths, vault_bases, root, cache)
return out
```
> 비고: 이 Task에서는 `_check_anchor`를 아직 정의하지 않으므로, **임시로** 기존 anchor 로직을 인라인 유지한다. 아래 Step 3b 참조 (Task 3에서 `_check_anchor`로 추출).
- [ ] **Step 3b: anchor 로직 임시 인라인** — 위 `_check_anchor(...)` 호출을 Task 3 전까지 기존 substring 로직으로 대체:
```python
if anchor:
rels = [target] if target in vault_paths else vault_bases.get(target, [])
found = False
for rel in rels:
fp = root / (rel + ".md")
txt = cache.get(fp)
if txt is None:
txt = read_text(fp)
cache[fp] = txt
if anchor.lower() in txt.lower():
found = True
break
if not found:
out.append(("DANGLING_ANCHOR", lineno, f"앵커 부재: [[{target}#{anchor}]]"))
```
(즉 Step 3의 `_check_anchor(...)` 한 줄을 이 블록으로 치환해서 작성. `BACKTICK_LINK`·`bare` 변수는 더 이상 사용 안 함.)
- [ ] **Step 4: 통과 확인**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
Expected: 두 테스트 모두 PASS.
- [ ] **Step 5: 회귀 — 직전 두 노트의 backtick 오탐 소멸 확인**
Run: `python3 .claude/hooks/wiki_structure_lint.py --file raw/branch-notes/feature-api-contract-baseline.md --links-only`
Expected: `BACKTICK_WRAPPED_LINK` 라인(122/125/151/183/188/315) 출력에서 사라짐.
- [ ] **Step 6: 체크포인트** — 전체 테스트 통과 확인 (git 미사용)
Run: `python3 .claude/hooks/test_wiki_structure_lint.py`
Expected: `OK`.
---
## Task 2: A1 — 비-md 첨부 인덱싱 (`.drawio` 오탐 제거)
**Files:**
- Modify: `.claude/hooks/wiki_structure_lint.py` (`build_vault_index`, 154-162)
- Modify: `.claude/hooks/test_wiki_structure_lint.py`
- [ ] **Step 1: 실패 테스트 추가**`test_wiki_structure_lint.py`에 클래스 추가
```python
class TestNonMdAttachment(unittest.TestCase):
def test_drawio_target_resolves(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "raw" / "diagrams").mkdir(parents=True)
(root / "raw" / "diagrams" / "arch.drawio").write_text("<xml/>")
(root / "raw" / "notes").mkdir(parents=True)
note = root / "raw" / "notes" / "n.md"
note.write_text("see [[raw/diagrams/arch.drawio]]\n")
vp, vb = wsl.build_vault_index(root)
f = wsl.check_c2(_doc("see [[raw/diagrams/arch.drawio]]"),
vp, vb, root, {})
self.assertNotIn("BROKEN_LINK", _codes(f))
def test_missing_drawio_still_broken(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "raw").mkdir()
vp, vb = wsl.build_vault_index(root)
f = wsl.check_c2(_doc("see [[raw/diagrams/ghost.drawio]]"),
vp, vb, root, {})
self.assertIn("BROKEN_LINK", _codes(f))
def test_git_dir_excluded(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / ".git").mkdir()
(root / ".git" / "obj.drawio").write_text("x")
vp, vb = wsl.build_vault_index(root)
self.assertNotIn(".git/obj.drawio", vp)
```
- [ ] **Step 2: 실패 확인**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
Expected: `test_drawio_target_resolves` FAIL (BROKEN_LINK 발생 — 비-md 미인덱싱).
- [ ] **Step 3: `build_vault_index` 교체**
```python
def build_vault_index(root):
"""링크 타깃 확인용. md는 .md strip, 비-md 첨부는 확장자 포함으로 등록.
숨김 디렉터리(.git 등)는 제외. (paths, bases=basename→rel목록)."""
paths, bases = set(), {}
for p in root.rglob("*"):
if not p.is_file():
continue
rel_posix = p.relative_to(root).as_posix()
if rel_posix.startswith(".") or "/." in rel_posix:
continue # .git / .obsidian 등 숨김 경로 제외
if p.suffix == ".md":
rel = rel_posix[:-3]
paths.add(rel)
bases.setdefault(p.stem, []).append(rel)
else:
paths.add(rel_posix) # 확장자 포함 full path
bases.setdefault(p.name, []).append(rel_posix) # 확장자 포함 basename
return paths, bases
```
- [ ] **Step 4: 통과 확인**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
Expected: 3개 신규 테스트 + Task 1 테스트 모두 PASS.
- [ ] **Step 5: 회귀 — vault에서 .drawio 오탐 소멸**
Run: `python3 .claude/hooks/wiki_structure_lint.py --all --links-only 2>&1 | grep -c 'drawio'`
Expected: `0` (이전엔 8).
- [ ] **Step 6: 체크포인트**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py`
Expected: `OK`.
---
## Task 3: B — heading anchor 정확화 (substring → 실제 heading 매칭)
**Files:**
- Modify: `.claude/hooks/wiki_structure_lint.py` (`check_c2` anchor 블록 → `_check_anchor` 추출 + 헬퍼)
- Modify: `.claude/hooks/test_wiki_structure_lint.py`
- [ ] **Step 1: 실패 테스트 추가**
```python
class TestHeadingAnchor(unittest.TestCase):
def _vault(self, d):
root = Path(d)
(root / "wiki").mkdir()
tgt = root / "wiki" / "t.md"
tgt.write_text("# Title\n\n## Real Heading\n\nbody real heading mention\n")
return root
def test_existing_heading_passes(self):
with tempfile.TemporaryDirectory() as d:
root = self._vault(d)
vp, vb = wsl.build_vault_index(root)
f = wsl.check_c2(_doc("[[wiki/t#Real Heading]]"), vp, vb, root, {})
self.assertNotIn("DANGLING_ANCHOR", _codes(f))
def test_substring_only_match_now_dangling(self):
# 'body'는 본문에만 있고 heading 아님 → 강화 후 DANGLING
with tempfile.TemporaryDirectory() as d:
root = self._vault(d)
vp, vb = wsl.build_vault_index(root)
f = wsl.check_c2(_doc("[[wiki/t#body]]"), vp, vb, root, {})
self.assertIn("DANGLING_ANCHOR", _codes(f))
def test_nonmd_anchor_skipped(self):
# 비-md 타깃 + anchor → anchor 검사 skip (DANGLING 아님)
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "raw").mkdir()
(root / "raw" / "a.drawio").write_text("<xml/>")
vp, vb = wsl.build_vault_index(root)
f = wsl.check_c2(_doc("[[raw/a.drawio#x]]"), vp, vb, root, {})
self.assertNotIn("DANGLING_ANCHOR", _codes(f))
```
- [ ] **Step 2: 실패 확인**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
Expected: `test_substring_only_match_now_dangling` FAIL (현재 substring으로 'body' 통과).
- [ ] **Step 3: 헬퍼 + `_check_anchor` 추가, anchor 블록 교체**
`check_c2` 위에 헬퍼 추가:
```python
HEADING_RE = re.compile(r"^#{1,6}\s+(.+?)\s*$", re.M)
def _heading_set(txt):
return {h.strip().lower() for h in HEADING_RE.findall(txt)}
def _check_anchor(out, lineno, target, anchor, vault_paths, vault_bases, root, cache):
rels = [target] if target in vault_paths else vault_bases.get(target, [])
md_rels = [r for r in rels if (root / (r + ".md")).exists()]
if not md_rels:
return # 비-md 첨부 등 — anchor 검사 무의미, skip
is_block = anchor.startswith("^")
norm = anchor[1:].strip() if is_block else anchor.strip().lower()
for rel in md_rels:
fp = root / (rel + ".md")
txt = cache.get(fp)
if txt is None:
txt = read_text(fp)
cache[fp] = txt
if is_block:
if re.search(r"\^" + re.escape(norm) + r"\s*$", txt, re.M):
return
else:
if norm in _heading_set(txt):
return
out.append(("DANGLING_ANCHOR", lineno, f"앵커 부재: [[{target}#{anchor}]]"))
```
그리고 `check_c2`의 anchor 블록(Task 1 Step 3b에서 인라인한 부분)을 한 줄로 교체:
```python
if anchor:
_check_anchor(out, lineno, target, anchor,
vault_paths, vault_bases, root, cache)
```
- [ ] **Step 4: 통과 확인**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v`
Expected: 전체 테스트 PASS.
- [ ] **Step 5: 회귀 — vault DANGLING_ANCHOR 수치 확인**
Run: `python3 .claude/hooks/wiki_structure_lint.py --all --links-only 2>&1 | grep -c DANGLING_ANCHOR`
Expected: 정수 출력(이전 3건 대비 변동 가능 — 강화로 증가할 수 있음, 정상).
- [ ] **Step 6: 체크포인트**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py`
Expected: `OK`.
---
## Task 4: D — 문법 계약 문서화 (docstring + linking-rules 참조)
**Files:**
- Modify: `.claude/hooks/wiki_structure_lint.py` (module docstring, 1-20)
- Modify: `rules/linking-rules.md`
- [ ] **Step 1: docstring의 C2 설명 교체**
`wiki_structure_lint.py` 상단 docstring에서 ` C2 옵시디언 링크 문법 — 백틱 래핑 / 깨진 타깃 / 부재 앵커` 줄을 아래로 확장:
```
C2 옵시디언 링크 문법 — 지원 형태 + 위반 정의:
[[t]] / [[t.md]] / [[t|alias]] → t 실존 검사 (md=확장자strip, 첨부=확장자포함)
![[t]] → embed, 동일 타깃 검사
[[t#heading]] → t의 실제 heading 매칭 (DANGLING_ANCHOR)
[[t#^blockid]] → t의 ^blockid 행말 토큰 (DANGLING_ANCHOR)
`[[t]]` (인라인 code span 내부) → BACKTICK_WRAPPED_LINK (옵시디언 링크 미렌더)
``` fenced ``` 내부 [[t]] → 예시로 간주, 스킵
판정은 위치기반 backtick 연속 페어링 — 표 셀 경계 오탐 없음.
```
- [ ] **Step 2: docstring 유효성 확인 (구문 깨짐 없음)**
Run: `python3 -c "import importlib.util,pathlib; s=importlib.util.spec_from_file_location('w','.claude/hooks/wiki_structure_lint.py'); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); print('ok')"`
Expected: `ok`.
- [ ] **Step 3: `rules/linking-rules.md`에 집행기 참조 1줄 추가**
`rules/linking-rules.md`의 검증 체크리스트 관련 섹션 끝에 추가 (적절한 위치에 1줄):
```markdown
> **결정론 집행기**: 위 옵시디언 링크 문법(broken target / dangling anchor / backtick 래핑)은 `.claude/hooks/wiki_structure_lint.py`의 C2 검사가 기계적으로 강제한다 (`--all --links-only`로 vault 전수, zero-tolerance).
```
- [ ] **Step 4: 체크포인트**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py && python3 .claude/hooks/wiki_structure_lint.py --file raw/branch-notes/feature-api-contract-baseline.md --links-only`
Expected: 테스트 `OK` + 파일 검사 정상 출력.
---
## Task 5: 롤아웃 — 정확한 깨진 링크 목록 산출
**Files:** 없음 (측정만)
- [ ] **Step 1: vault 전수 재실행**
Run: `python3 .claude/hooks/wiki_structure_lint.py --all --links-only 2>&1 | tail -15`
Expected: 요약에서 `BACKTICK_WRAPPED_LINK` 대폭 감소(오탐 제거), `BROKEN_LINK`는 .drawio 8건 제거 후 *진짜* 회색 노드만 남음.
- [ ] **Step 2: 진짜 BROKEN_LINK 타깃 빈도표 산출**
Run: `python3 .claude/hooks/wiki_structure_lint.py --all --links-only 2>&1 | grep BROKEN_LINK | sed -E 's/.*\[\[([^]]*)\]\].*/\1/' | sort | uniq -c | sort -rn`
Expected: 미생성 daily-note(`raw/daily-notes/2026-05-25` 등) + 미작성 concept 목록. 이 목록이 후속 정리(스텁/링크수정)의 입력.
- [ ] **Step 3: 사용자에게 정리 목록 보고**
산출된 진짜 깨진 링크 목록을 사용자에게 제시하고, 정리(스텁 생성 vs 링크 제거)는 별도 작업으로 진행 여부 확인. (본 plan 범위는 린터까지.)
---
## Self-Review (작성자 점검 완료)
- **Spec coverage:** A1(Task2)·A2(Task1)·B(Task3)·C(Task5 강제 측정)·D(Task4) 전부 task 존재. ✓
- **Placeholder scan:** 모든 code step에 실제 코드 포함. "TBD"/"적절히" 없음. ✓
- **Type consistency:** `_check_anchor`/`_heading_set`/`HEADING_RE` Task3에서 정의 후 Task1 호출부와 시그니처 일치. Task1 Step3b가 임시 인라인 → Task3가 추출로 대체(순서 명시). `build_vault_index` 반환 `(paths, bases)` 불변. ✓
- **환경:** git 미초기화 → 커밋 대신 테스트 체크포인트(헤더 명시). ✓
@@ -0,0 +1,809 @@
# 3-플랫폼 동기화 Phase 0 — 생성 엔진 + agents Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Claude의 9개 agent를 Codex CLI(`.codex/agents/*.toml`)와 Antigravity CLI(`.agents/agents/<name>/agent.json`)의 native 포맷으로 생성하는 멱등 스크립트 `scripts/sync-automation.py`를 만들고, 누락된 2개 agent(`branch-depth-auditor`·`coverage-auditor`)를 양 플랫폼에 추가한다.
**Architecture:** Claude `.claude/agents/<name>.md` frontmatter의 `tools:`를 **권한 SSOT**로 삼아 codex `sandbox_mode`와 antigravity `toolNames`를 기계적으로 파생한다. 프롬프트 본문은 플랫폼별 SSOT(`.codex/agents/<name>.md` = codex 일반화 본문, `.agents/plugins/wiki-superpowers/agents/<name>.md` = antigravity gated 본문)에서 가져와 native 머신 포맷으로 포장한다. 스크립트는 순수 함수(권한 파생·포맷 직렬화)와 I/O를 분리하고, `--check` 모드로 SSOT↔생성물 drift를 0으로 검증한다.
**Tech Stack:** Python 3.12 (stdlib only — `tomllib` for TOML 검증, `json`, `pathlib`, `argparse`), pytest.
---
## 배경 / 현재 상태 (실측)
- Claude agents (SSOT identity + 권한): `.claude/agents/*.md` — frontmatter `name`/`description`/`tools`(콤마 목록)/`model: sonnet`. 9개 존재.
- Codex agents: `.codex/agents/*.md` — frontmatter `name`/`description`(tools/model 없음) + 일반화 본문. **7개** (branch-depth-auditor·coverage-auditor 누락). codex가 실제 읽는 native 포맷은 `*.toml`인데 **하나도 없음**.
- Antigravity agents: `.agents/plugins/wiki-superpowers/agents/*.md` — frontmatter `name`/`description` + G1~G4 gate 본문. **7개** (동일 2개 누락). antigravity가 실제 읽는 `.agents/agents/<name>/agent.json` **하나도 없음** (디스크의 `~/.gemini/.../agent.json`은 stale·오류 생성물 — read-only agent에 write 도구가 들어있고 content가 요약됨. 신뢰 금지).
**확정된 agent.json 스키마** (실 디스크 `~/.gemini/antigravity-cli/brain/.../wiki-research-lane/agent.json`에서 키 구조만 채택):
```json
{
"name": "<name>",
"description": "<desc>",
"hidden": true,
"config": {
"customAgent": {
"systemPromptSections": [ { "title": "Agent System Instructions", "content": "<body>" } ],
"toolNames": [ ... ],
"systemPromptConfig": {
"includeSections": ["user_information","mcp_servers","skills","subagent_reminder","messaging","artifacts","user_rules"]
}
}
}
}
```
**권한 파생 규칙** (Claude `tools:` → 플랫폼):
| Claude tool | Antigravity `toolNames` | 비고 |
|---|---|---|
| (모든 agent 공통 baseline) | `send_message`, `view_file`, `find_by_name`, `grep_search`, `list_dir` | 9개 모두 Read+Grep+Glob 보유 |
| `Bash` | `run_command` | |
| `Edit` | `replace_file_content`, `multi_replace_file_content` | |
| `Write` | `write_to_file` | |
| `WebFetch` | `read_url_content` | |
| `WebSearch` | `search_web` | |
- Codex `sandbox_mode` = `workspace-write` (Claude tools에 `Edit` 또는 `Write` 포함 시) / else `read-only`.
- 9개 agent 권한 (Claude frontmatter 실측):
- read-only: `branch-depth-auditor`(Read,Grep,Glob), `coverage-auditor`·`wiki-adversarial-reviewer`·`wiki-diagram-reviewer`·`wiki-link-verifier`·`wiki-research-lane`(Read,Grep,Glob,Bash)
- workspace-write: `wiki-doc-author`(+Edit,Write), `wiki-source-summarizer`(+Edit,Write,WebFetch), `wiki-decision-researcher`(+Write,WebSearch,WebFetch)
---
## File Structure
- Create: `scripts/sync-automation.py` — 단일 진입점 CLI. 순수 함수 구역(파싱·권한 파생·직렬화) + I/O 구역(파일 read/write) + `argparse` CLI.
- Create: `scripts/test_sync_automation.py` — pytest 단위 테스트 (순수 함수 + 생성물 유효성).
- Create: `.codex/agents/branch-depth-auditor.md`, `.codex/agents/coverage-auditor.md` — codex 일반화 본문 SSOT (Claude에서 적응).
- Create: `.agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md`, `.agents/plugins/wiki-superpowers/agents/coverage-auditor.md` — antigravity gated 본문 SSOT.
- Generate (스크립트 출력): `.codex/agents/<name>.toml` ×9, `.agents/agents/<name>/agent.json` ×9.
- Modify (Phase 0 말미, 최소): `.codex/agents/README.md`, `.agents/plugins/wiki-superpowers/README.md` — 생성 스크립트 사용법 1단락. (전체 문서 정리는 Phase 2.)
> 책임 경계: `sync-automation.py`는 *권한 파생 + 포맷 포장*만 한다. 본문 프로즈의 플랫폼 적응(일반화·gate 작성)은 사람이 SSOT `.md`에 직접 한다. codex는 `.toml`만 로드하므로 `.codex/agents/*.md`는 우리 SSOT로 남고 codex는 무시한다.
---
## Task 1: 스크립트 골격 + frontmatter 파싱 + 권한 파생 (순수 함수)
**Files:**
- Create: `scripts/sync-automation.py`
- Test: `scripts/test_sync_automation.py`
- [ ] **Step 1: Write the failing test**
```python
# scripts/test_sync_automation.py
import json
import tomllib
import sync_automation as s
def test_parse_frontmatter_extracts_fields():
md = (
"---\n"
"name: wiki-link-verifier\n"
"description: Audit the wiki for orphans.\n"
"tools: Read, Grep, Glob, Bash\n"
"model: sonnet\n"
"---\n\n"
"You are the Wiki Link Verifier.\n"
)
fm, body = s.parse_frontmatter(md)
assert fm["name"] == "wiki-link-verifier"
assert fm["description"] == "Audit the wiki for orphans."
assert fm["tools"] == "Read, Grep, Glob, Bash"
assert body == "You are the Wiki Link Verifier.\n"
def test_parse_tools_list():
assert s.parse_tools("Read, Grep, Glob, Bash") == ["Read", "Grep", "Glob", "Bash"]
assert s.parse_tools("Read,Edit,Write") == ["Read", "Edit", "Write"]
def test_sandbox_mode_read_only_when_no_write_tools():
assert s.codex_sandbox_mode(["Read", "Grep", "Glob", "Bash"]) == "read-only"
def test_sandbox_mode_workspace_write_when_edit_or_write():
assert s.codex_sandbox_mode(["Read", "Edit", "Write", "Bash"]) == "workspace-write"
assert s.codex_sandbox_mode(["Read", "Write", "WebSearch"]) == "workspace-write"
def test_antigravity_toolnames_read_only_agent():
# Read,Grep,Glob,Bash -> baseline + run_command, no write tools
assert s.antigravity_tool_names(["Read", "Grep", "Glob", "Bash"]) == [
"send_message", "view_file", "find_by_name", "grep_search", "list_dir",
"run_command",
]
def test_antigravity_toolnames_write_agent_with_web():
# source-summarizer: Read,Edit,Write,Bash,Grep,Glob,WebFetch
assert s.antigravity_tool_names(
["Read", "Edit", "Write", "Bash", "Grep", "Glob", "WebFetch"]
) == [
"send_message", "view_file", "find_by_name", "grep_search", "list_dir",
"write_to_file", "replace_file_content", "multi_replace_file_content",
"run_command", "read_url_content",
]
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
Expected: FAIL with `ModuleNotFoundError: No module named 'sync_automation'`.
> 참고: 파일명이 `sync-automation.py`(하이픈)라 `import sync_automation`이 안 된다. 테스트 상단에서 모듈을 로드하도록 `conftest.py`로 별칭을 만든다(아래 Step 3에 포함). 또는 파일명을 `sync_automation.py`로 하고 CLI는 `python3 scripts/sync_automation.py`로 부른다. **결정: 파일명을 `scripts/sync_automation.py`(언더스코어)로 한다** — import 가능 + CLI 호출에 지장 없음. 본 계획의 이후 모든 경로에서 `sync_automation.py`로 읽는다.
- [ ] **Step 3: Write minimal implementation**
```python
# scripts/sync_automation.py
"""Generate Codex/Antigravity native agent files from Claude SSOT.
Permission SSOT : .claude/agents/<name>.md frontmatter `tools:`
Codex body SSOT : .codex/agents/<name>.md
Antigravity SSOT : .agents/plugins/wiki-superpowers/agents/<name>.md
Outputs : .codex/agents/<name>.toml , .agents/agents/<name>/agent.json
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
CLAUDE_AGENTS = REPO / ".claude" / "agents"
CODEX_AGENTS = REPO / ".codex" / "agents"
ANTIGRAV_SSOT = REPO / ".agents" / "plugins" / "wiki-superpowers" / "agents"
ANTIGRAV_OUT = REPO / ".agents" / "agents"
AGENT_NAMES = [
"branch-depth-auditor",
"coverage-auditor",
"wiki-adversarial-reviewer",
"wiki-decision-researcher",
"wiki-diagram-reviewer",
"wiki-doc-author",
"wiki-link-verifier",
"wiki-research-lane",
"wiki-source-summarizer",
]
ANTIGRAV_INCLUDE_SECTIONS = [
"user_information", "mcp_servers", "skills",
"subagent_reminder", "messaging", "artifacts", "user_rules",
]
_ANTIGRAV_BASELINE = ["send_message", "view_file", "find_by_name", "grep_search", "list_dir"]
_ANTIGRAV_EXTRA_ORDER = [
("Write", ["write_to_file"]),
("Edit", ["replace_file_content", "multi_replace_file_content"]),
("Bash", ["run_command"]),
("WebFetch", ["read_url_content"]),
("WebSearch", ["search_web"]),
]
def parse_frontmatter(md: str) -> tuple[dict[str, str], str]:
lines = md.splitlines(keepends=True)
if not lines or lines[0].strip() != "---":
raise ValueError("missing frontmatter open")
fm: dict[str, str] = {}
i = 1
while i < len(lines) and lines[i].strip() != "---":
line = lines[i].rstrip("\n")
if ":" in line:
key, _, val = line.partition(":")
fm[key.strip()] = val.strip()
i += 1
if i >= len(lines):
raise ValueError("missing frontmatter close")
body = "".join(lines[i + 1:]).lstrip("\n")
return fm, body
def parse_tools(tools_csv: str) -> list[str]:
return [t.strip() for t in tools_csv.split(",") if t.strip()]
def codex_sandbox_mode(tools: list[str]) -> str:
return "workspace-write" if ({"Edit", "Write"} & set(tools)) else "read-only"
def antigravity_tool_names(tools: list[str]) -> list[str]:
names = list(_ANTIGRAV_BASELINE)
tset = set(tools)
for claude_tool, mapped in _ANTIGRAV_EXTRA_ORDER:
if claude_tool in tset:
names.extend(mapped)
return names
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
Expected: PASS (6 tests).
- [ ] **Step 5: Commit**
```bash
git add scripts/sync_automation.py scripts/test_sync_automation.py
git commit -m "feat(sync): frontmatter parse + permission derivation pure functions"
```
---
## Task 2: Codex TOML 직렬화
**Files:**
- Modify: `scripts/sync_automation.py`
- Test: `scripts/test_sync_automation.py`
- [ ] **Step 1: Write the failing test**
```python
def test_codex_toml_is_valid_and_roundtrips():
body = "You are the Wiki Link Verifier.\nLine two with `backticks` and 'quotes'.\n"
out = s.render_codex_toml(
name="wiki-link-verifier",
description='Audit "the wiki" for orphans.',
sandbox_mode="read-only",
body=body,
)
parsed = tomllib.loads(out)
assert parsed["name"] == "wiki-link-verifier"
assert parsed["description"] == 'Audit "the wiki" for orphans.'
assert parsed["sandbox_mode"] == "read-only"
assert parsed["developer_instructions"].strip() == body.strip()
def test_codex_toml_rejects_triple_single_quote_body():
import pytest
with pytest.raises(ValueError):
s.render_codex_toml("n", "d", "read-only", "bad ''' body")
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k codex_toml -v`
Expected: FAIL with `AttributeError: module 'sync_automation' has no attribute 'render_codex_toml'`.
- [ ] **Step 3: Write minimal implementation**
```python
def _toml_basic_string(value: str) -> str:
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
def render_codex_toml(name: str, description: str, sandbox_mode: str, body: str) -> str:
if "'''" in body:
raise ValueError("body contains ''' which breaks TOML literal multiline string")
return (
f"name = {_toml_basic_string(name)}\n"
f"description = {_toml_basic_string(description)}\n"
f"sandbox_mode = {_toml_basic_string(sandbox_mode)}\n"
f"developer_instructions = '''\n"
f"{body.rstrip()}\n"
f"'''\n"
)
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k codex_toml -v`
Expected: PASS (2 tests).
- [ ] **Step 5: Commit**
```bash
git add scripts/sync_automation.py scripts/test_sync_automation.py
git commit -m "feat(sync): codex TOML serialization with literal multiline body"
```
---
## Task 3: Antigravity agent.json 직렬화
**Files:**
- Modify: `scripts/sync_automation.py`
- Test: `scripts/test_sync_automation.py`
- [ ] **Step 1: Write the failing test**
```python
def test_antigravity_agent_json_schema():
obj = s.build_antigravity_agent(
name="wiki-link-verifier",
description="Audit the wiki.",
body="You are the Wiki Link Verifier.\n",
tools=["Read", "Grep", "Glob", "Bash"],
)
assert obj["name"] == "wiki-link-verifier"
assert obj["description"] == "Audit the wiki."
assert obj["hidden"] is True
ca = obj["config"]["customAgent"]
assert ca["systemPromptSections"][0]["title"] == "Agent System Instructions"
assert ca["systemPromptSections"][0]["content"] == "You are the Wiki Link Verifier.\n"
assert "write_to_file" not in ca["toolNames"] # read-only agent
assert ca["toolNames"][0] == "send_message"
assert ca["systemPromptConfig"]["includeSections"] == s.ANTIGRAV_INCLUDE_SECTIONS
def test_antigravity_json_render_is_valid_json():
obj = s.build_antigravity_agent("n", "d", "body\n", ["Read", "Grep", "Glob"])
text = s.render_json(obj)
assert json.loads(text) == obj
assert text.endswith("\n")
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k antigravity_agent_json -v`
Expected: FAIL with `AttributeError: ... 'build_antigravity_agent'`.
- [ ] **Step 3: Write minimal implementation**
```python
def build_antigravity_agent(name: str, description: str, body: str, tools: list[str]) -> dict:
return {
"name": name,
"description": description,
"hidden": True,
"config": {
"customAgent": {
"systemPromptSections": [
{"title": "Agent System Instructions", "content": body}
],
"toolNames": antigravity_tool_names(tools),
"systemPromptConfig": {"includeSections": list(ANTIGRAV_INCLUDE_SECTIONS)},
}
},
}
def render_json(obj: dict) -> str:
return json.dumps(obj, indent=2, ensure_ascii=False) + "\n"
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k antigravity_agent_json -v`
Expected: PASS (2 tests).
- [ ] **Step 5: Commit**
```bash
git add scripts/sync_automation.py scripts/test_sync_automation.py
git commit -m "feat(sync): antigravity agent.json builder + stable JSON render"
```
---
## Task 4: CLI 배선 (generate / check) + 기존 7개 검증
**Files:**
- Modify: `scripts/sync_automation.py`
- Test: `scripts/test_sync_automation.py`
- [ ] **Step 1: Write the failing test**
```python
def test_load_agent_inputs_for_existing_agent():
inp = s.load_agent_inputs("wiki-link-verifier")
assert inp.tools == ["Read", "Grep", "Glob", "Bash"]
assert inp.codex_description
assert inp.antigrav_description
assert "Wiki Link Verifier" in inp.codex_body
assert inp.antigrav_body
def test_generate_one_writes_both_outputs(tmp_path, monkeypatch):
monkeypatch.setattr(s, "CODEX_AGENTS", tmp_path / "codex")
monkeypatch.setattr(s, "ANTIGRAV_OUT", tmp_path / "antigrav")
(tmp_path / "codex").mkdir()
s.generate_one("wiki-link-verifier", check=False)
toml_path = tmp_path / "codex" / "wiki-link-verifier.toml"
json_path = tmp_path / "antigrav" / "wiki-link-verifier" / "agent.json"
assert toml_path.exists()
assert json_path.exists()
assert tomllib.loads(toml_path.read_text())["sandbox_mode"] == "read-only"
assert json.loads(json_path.read_text())["hidden"] is True
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd scripts && python3 -m pytest test_sync_automation.py -k "load_agent_inputs or generate_one" -v`
Expected: FAIL with `AttributeError: ... 'load_agent_inputs'`.
- [ ] **Step 3: Write minimal implementation**
```python
from dataclasses import dataclass
@dataclass
class AgentInputs:
name: str
tools: list[str]
codex_description: str
codex_body: str
antigrav_description: str
antigrav_body: str
def load_agent_inputs(name: str) -> AgentInputs:
claude_fm, _ = parse_frontmatter((CLAUDE_AGENTS / f"{name}.md").read_text())
codex_fm, codex_body = parse_frontmatter((CODEX_AGENTS / f"{name}.md").read_text())
ag_fm, ag_body = parse_frontmatter((ANTIGRAV_SSOT / f"{name}.md").read_text())
return AgentInputs(
name=name,
tools=parse_tools(claude_fm["tools"]),
codex_description=codex_fm["description"],
codex_body=codex_body,
antigrav_description=ag_fm["description"],
antigrav_body=ag_body,
)
def _write_or_check(path: Path, content: str, check: bool, drift: list[str]) -> None:
if check:
current = path.read_text() if path.exists() else None
if current != content:
drift.append(str(path.relative_to(REPO)))
else:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
def generate_one(name: str, check: bool, drift: list[str] | None = None) -> None:
drift = drift if drift is not None else []
inp = load_agent_inputs(name)
toml_text = render_codex_toml(
inp.name, inp.codex_description, codex_sandbox_mode(inp.tools), inp.codex_body
)
json_text = render_json(
build_antigravity_agent(inp.name, inp.antigrav_description, inp.antigrav_body, inp.tools)
)
_write_or_check(CODEX_AGENTS / f"{name}.toml", toml_text, check, drift)
_write_or_check(ANTIGRAV_OUT / name / "agent.json", json_text, check, drift)
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="Sync Claude agents -> Codex/Antigravity native files")
ap.add_argument("target", choices=["agents"], help="what to sync")
ap.add_argument("--check", action="store_true", help="fail (exit 2) if outputs drift from SSOT")
ap.add_argument("--only", help="single agent name (default: all)")
args = ap.parse_args(argv)
names = [args.only] if args.only else AGENT_NAMES
drift: list[str] = []
for name in names:
generate_one(name, check=args.check, drift=drift)
if args.check and drift:
print("DRIFT detected in:\n " + "\n ".join(drift), file=sys.stderr)
return 2
action = "checked" if args.check else "generated"
print(f"{action} {len(names)} agents (codex .toml + antigravity agent.json)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
- [ ] **Step 4: Run unit tests (still only 7 agents exist, so use --only)**
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
Expected: PASS (all tests). `load_agent_inputs`/`generate_one` use `wiki-link-verifier` which exists.
- [ ] **Step 5: Smoke-run on one existing agent**
Run:
```bash
cd /home/donghyeon/dev/llm-wiki-private
python3 scripts/sync_automation.py agents --only wiki-link-verifier
python3 -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('.codex/agents/wiki-link-verifier.toml').read_text())['sandbox_mode'])"
python3 -c "import json,pathlib; o=json.loads(pathlib.Path('.agents/agents/wiki-link-verifier/agent.json').read_text()); print(o['hidden'], 'write_to_file' in o['config']['customAgent']['toolNames'])"
```
Expected output:
```
generated 1 agents (codex .toml + antigravity agent.json)
read-only
True False
```
(`True False` = hidden true + write_to_file absent for read-only agent — confirms the stale-disk bug is fixed.)
- [ ] **Step 6: Commit**
```bash
git add scripts/sync_automation.py scripts/test_sync_automation.py .codex/agents/wiki-link-verifier.toml .agents/agents/wiki-link-verifier/agent.json
git commit -m "feat(sync): CLI generate/check + AgentInputs loader; verify on link-verifier"
```
---
## Task 5: 누락 agent 2개의 codex SSOT 본문 작성 (`branch-depth-auditor`, `coverage-auditor`)
> codex 본문 = Claude 본문의 일반화: frontmatter에서 `tools:`/`model:` 제거(name+description만 유지), 본문의 `Read tool`/`Edit tool`/`Write tool`/`Bash tool` → `Read`/`Edit`/`Write`/`shell`, `CLAUDE.md` 단독 언급 → `CLAUDE.md (또는 AGENTS.md)`.
**Files:**
- Create: `.codex/agents/branch-depth-auditor.md`
- Create: `.codex/agents/coverage-auditor.md`
- [ ] **Step 1: Learn the generalization convention**
Read these two pairs side by side to learn the exact convention:
- `.claude/agents/wiki-link-verifier.md` vs `.codex/agents/wiki-link-verifier.md`
- Then Read `.claude/agents/branch-depth-auditor.md` and `.claude/agents/coverage-auditor.md` in full.
- [ ] **Step 2: Create `.codex/agents/branch-depth-auditor.md`**
Structure:
```markdown
---
name: branch-depth-auditor
description: <verbatim copy of the description line from .claude/agents/branch-depth-auditor.md>
---
<Claude body copied verbatim, then these substitutions applied:
"Read tool"->"Read", "Grep tool"->"Grep", "Glob tool"->"Glob", "Bash tool"->"shell",
"Edit tool"->"Edit", "Write tool"->"Write";
standalone "CLAUDE.md" reference -> "CLAUDE.md (또는 AGENTS.md)".
Leave all rules/ and templates/ references, axes, and depth-ladder logic byte-identical.>
```
- [ ] **Step 3: Create `.codex/agents/coverage-auditor.md`** identically (verbatim description + generalized body).
- [ ] **Step 4: Verify frontmatter parses for both**
Run:
```bash
cd /home/donghyeon/dev/llm-wiki-private/scripts && python3 -c "
import sync_automation as s
for n in ['branch-depth-auditor','coverage-auditor']:
fm,body=s.parse_frontmatter(open(f'../.codex/agents/{n}.md').read())
assert fm['name']==n and fm['description'] and body.strip(), n
assert 'tool' not in fm and 'model' not in fm, 'frontmatter must be name+description only'
print('ok', n)
"
```
Expected:
```
ok branch-depth-auditor
ok coverage-auditor
```
- [ ] **Step 5: Commit**
```bash
git add .codex/agents/branch-depth-auditor.md .codex/agents/coverage-auditor.md
git commit -m "feat(codex): add branch-depth-auditor + coverage-auditor SSOT bodies"
```
---
## Task 6: 누락 agent 2개의 antigravity gated SSOT 본문 작성
> antigravity 본문 = codex 본문 + G1~G4 Gemini hard gate. 기존 read-only gated agent를 템플릿으로 사용한다. tool 참조는 antigravity 어휘(`view_file`/`grep_search`/`run_command`)로 쓴다.
**Files:**
- Create: `.agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md`
- Create: `.agents/plugins/wiki-superpowers/agents/coverage-auditor.md`
- [ ] **Step 1: Study the gate template**
Read `.agents/plugins/wiki-superpowers/agents/wiki-link-verifier.md` in full (read-only agent — closest analog). Extract the exact section structure of: G1 Pre-Read Proof, G2 Post-Write Validator, G3 Output Schema + V Counter, G4 Enumerated STOP Conditions. Also read `.agents/plugins/wiki-superpowers/agents/wiki-research-lane.md` if a longer analytical example helps.
- [ ] **Step 2: Create `.agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md`**
```markdown
---
name: branch-depth-auditor
description: <verbatim copy from .claude/agents/branch-depth-auditor.md frontmatter>
---
<body = the codex branch-depth-auditor body content (audit axes + L0~L3 depth ladder
+ Ready/Not-ready verdict logic), PLUS the four gates:
- G1 Pre-Read Proof: table quoting the first line of each Mandatory First Read
(CLAUDE.md/AGENTS.md, rules/branch-depth-gate.md, the target branch note).
- G2 Post-Write Validator: read-only -> state "read-only agent, no file writes;
verification is the grep/sed verbatim-quote proofs inside the gap report (G3 V counter)".
- G3 Output Schema + V Counter: {{ }} placeholder schema for the Ready/Not-ready
verdict + per-axis gap rows; V = number of quote-grep commands actually run.
- G4 Enumerated STOP Conditions: numbered list -> return NEEDS_CONTEXT
(1: branch-note path missing/unreadable; 2: linked raw sources unreadable;
3: wiki_structure_lint not yet passed; 4: target is not a feature-*.md branch note).
Use Antigravity tool names in any tool reference: view_file, grep_search, run_command.>
```
Mirror the axes/ladder language from `.claude/agents/branch-depth-auditor.md`; do NOT invent new audit criteria.
- [ ] **Step 3: Create `.agents/plugins/wiki-superpowers/agents/coverage-auditor.md`**
Same structure. Coverage = completeness (covered-here / delegated / missing per required concern) with a 3-tier verdict and optional project mode. G4 STOP conditions: governing_docs missing, `## Coverage` section absent, links unresolved, target not a branch note. tool refs use `run_command` for grep-based concern classification + `view_file` for governing docs.
- [ ] **Step 4: Verify frontmatter parses + all four gates present**
Run:
```bash
cd /home/donghyeon/dev/llm-wiki-private
for n in branch-depth-auditor coverage-auditor; do
f=.agents/plugins/wiki-superpowers/agents/$n.md
echo "== $n =="
grep -c "G1 Pre-Read Proof" "$f"
grep -c "G4" "$f"
done
```
Expected: each `grep -c` prints `1` or more (gates present). If `0`, the gate section is missing — add it.
- [ ] **Step 5: Commit**
```bash
git add .agents/plugins/wiki-superpowers/agents/branch-depth-auditor.md .agents/plugins/wiki-superpowers/agents/coverage-auditor.md
git commit -m "feat(antigravity): add branch-depth-auditor + coverage-auditor gated SSOT bodies"
```
---
## Task 7: 전체 9개 생성 + 스키마 검증 + drift 0 확인
**Files:**
- Generate: `.codex/agents/*.toml` ×9, `.agents/agents/<name>/agent.json` ×9
- Test: `scripts/test_sync_automation.py` (add a full-suite generation guard)
- [ ] **Step 1: Add a test asserting all 9 generate and pass schema checks**
```python
def test_all_nine_agents_generate_valid_artifacts(tmp_path, monkeypatch):
monkeypatch.setattr(s, "CODEX_AGENTS", tmp_path / "codex")
monkeypatch.setattr(s, "ANTIGRAV_OUT", tmp_path / "antigrav")
# copy SSOT bodies the generator reads from real repo (CODEX_AGENTS is also the source dir),
# so point the source dir explicitly: regenerate using real source via a fresh load.
(tmp_path / "codex").mkdir()
# NOTE: generate_one reads codex body from s.CODEX_AGENTS; for this test we only verify
# the antigravity side (json) which reads from ANTIGRAV_SSOT (unchanged real dir).
for name in s.AGENT_NAMES:
obj = s.build_antigravity_agent(
name, "d",
s.parse_frontmatter((s.ANTIGRAV_SSOT / f"{name}.md").read_text())[1],
s.parse_tools(s.parse_frontmatter((s.CLAUDE_AGENTS / f"{name}.md").read_text())[0]["tools"]),
)
assert json.loads(s.render_json(obj))["name"] == name
# read-only agents must NOT carry write tools
ro = {"branch-depth-auditor", "coverage-auditor", "wiki-adversarial-reviewer",
"wiki-diagram-reviewer", "wiki-link-verifier", "wiki-research-lane"}
if name in ro:
assert "write_to_file" not in obj["config"]["customAgent"]["toolNames"], name
```
> 위 테스트는 `CODEX_AGENTS`/`ANTIGRAV_SSOT`의 9개 `.md`가 모두 존재해야 통과한다(Task 5·6 완료 후). codex toml 본문은 실 디렉토리에서 직접 읽으므로 통합 스모크(Step 3)로 검증한다.
- [ ] **Step 2: Run the unit suite**
Run: `cd scripts && python3 -m pytest test_sync_automation.py -v`
Expected: PASS (all, including the new 9-agent guard).
- [ ] **Step 3: Generate all 9 and validate every artifact**
Run:
```bash
cd /home/donghyeon/dev/llm-wiki-private
python3 scripts/sync_automation.py agents
echo "--- validate codex toml (9) ---"
python3 -c "
import tomllib, pathlib
ns=['branch-depth-auditor','coverage-auditor','wiki-adversarial-reviewer','wiki-decision-researcher','wiki-diagram-reviewer','wiki-doc-author','wiki-link-verifier','wiki-research-lane','wiki-source-summarizer']
for n in ns:
o=tomllib.loads(pathlib.Path(f'.codex/agents/{n}.toml').read_text())
assert o['name']==n and o['sandbox_mode'] in ('read-only','workspace-write') and o['developer_instructions'].strip()
print('codex toml ok:', len(ns))
"
echo "--- validate antigravity json (9) ---"
python3 -c "
import json, pathlib
ns=['branch-depth-auditor','coverage-auditor','wiki-adversarial-reviewer','wiki-decision-researcher','wiki-diagram-reviewer','wiki-doc-author','wiki-link-verifier','wiki-research-lane','wiki-source-summarizer']
for n in ns:
o=json.loads(pathlib.Path(f'.agents/agents/{n}/agent.json').read_text())
ca=o['config']['customAgent']
assert o['name']==n and o['hidden'] is True
assert ca['systemPromptSections'][0]['content'].strip()
assert ca['toolNames'][0]=='send_message'
print('antigravity json ok:', len(ns))
"
```
Expected:
```
generated 9 agents (codex .toml + antigravity agent.json)
--- validate codex toml (9) ---
codex toml ok: 9
--- validate antigravity json (9) ---
antigravity json ok: 9
```
- [ ] **Step 4: Confirm idempotency / drift-0**
Run:
```bash
cd /home/donghyeon/dev/llm-wiki-private
python3 scripts/sync_automation.py agents --check && echo "DRIFT-FREE"
```
Expected:
```
checked 9 agents (codex .toml + antigravity agent.json)
DRIFT-FREE
```
(exit 0). If it prints `DRIFT detected`, re-run without `--check` and re-commit the generated files.
- [ ] **Step 5: Commit generated artifacts**
```bash
cd /home/donghyeon/dev/llm-wiki-private
git add scripts/test_sync_automation.py .codex/agents/*.toml .agents/agents/
git commit -m "feat(sync): generate all 9 agents for codex (.toml) + antigravity (agent.json)"
```
---
## Task 8: 생성 스크립트 사용법 문서 1단락 (codex + antigravity README)
> 전체 문서 정리(CLAUDE.md의 구식 '수동 cat' 서술 제거 등)는 Phase 2. 여기서는 Phase 0가 만든 스크립트/산출물이 고아가 되지 않도록 최소 포인터만 추가한다.
**Files:**
- Modify: `.codex/agents/README.md`
- Modify: `.agents/plugins/wiki-superpowers/README.md`
- [ ] **Step 1: Add a "Native generation" note to `.codex/agents/README.md`**
기존 README 상단(또는 "Pattern" 섹션 위)에 다음 취지의 1단락 추가 (정확한 문구는 기존 톤에 맞춰 작성):
- codex는 이제 native subagent를 `.codex/agents/*.toml`로 자동 등록한다(`developer_instructions`+`sandbox_mode`). `.md`는 사람이 편집하는 SSOT이고 `.toml``python3 scripts/sync_automation.py agents`로 생성된다.
- `.md` 편집 후 반드시 `sync_automation.py agents`를 다시 돌려야 `.toml`에 반영된다. CI/hook에서는 `--check`로 drift를 검사한다.
- [ ] **Step 2: Add the same note to `.agents/plugins/wiki-superpowers/README.md`**
기존 "⚠️ Loading model" 박스의 "Sync command" 줄을 실제 스크립트로 교체:
- 구: `python3 .agents/scripts/convert-wiki-agents.py` (존재하지 않음)
- 신: `python3 scripts/sync_automation.py agents``.agents/plugins/.../agents/*.md`(gated SSOT) + `.claude/agents/*.md`(권한 SSOT) → `.agents/agents/<name>/agent.json` 생성. `--check`로 drift 검사.
- [ ] **Step 3: Verify the dead reference is gone**
Run:
```bash
cd /home/donghyeon/dev/llm-wiki-private
grep -rn "convert-wiki-agents.py" .agents/ .codex/ CLAUDE.md || echo "no dead reference remaining in README scope"
```
Expected: `no dead reference remaining in README scope` (CLAUDE.md 본문의 언급은 Phase 2에서 처리하므로, 여기서 grep이 CLAUDE.md만 남기면 그 줄은 Phase 2 TODO로 남겨도 됨 — 단 README 2개에는 남지 않아야 함).
- [ ] **Step 4: Commit**
```bash
git add .codex/agents/README.md .agents/plugins/wiki-superpowers/README.md
git commit -m "docs(sync): point READMEs at scripts/sync_automation.py (replaces missing convert script)"
```
---
## Phase 0 완료 기준 (Definition of Done)
- `scripts/sync_automation.py` + 테스트 통과(pytest green).
- `.codex/agents/*.toml` 9개 + `.agents/agents/<name>/agent.json` 9개 생성·검증.
- `sync_automation.py agents --check` 가 exit 0 (drift 0).
- 누락 2개 agent(branch-depth-auditor·coverage-auditor)가 codex·antigravity 양쪽 SSOT + 생성물에 존재.
- README 2개가 실제 생성 스크립트를 가리킴.
- 검증된 부채 수정: read-only agent의 `agent.json`에 write 도구 없음(stale 디스크 버그 해소).
이후: **Phase 1**(commands → codex `.agents/skills/` + antigravity `.agents/workflows/`)는 별도 plan으로 작성한다. Phase 0의 `sync_automation.py``commands` 타깃을 확장한다.
---
## Self-Review (작성자 체크)
- **Spec coverage**: 설계 §4 Phase 0(엔진+agents)의 모든 항목 — 생성 엔진, 9개 agent 생성, 누락 2개 추가, antigravity 로딩 경로 복구(agent.json+스크립트), codex toml 현대화 — 각각 Task 1~8에 매핑됨. Phase 1·2는 범위 밖(별도 plan).
- **Placeholder scan**: 코드 스텝은 실제 코드 포함. Task 5·6의 본문 작성은 "기존 파일을 템플릿으로 verbatim 적응"이라는 결정론적 절차 + 검증 grep을 제공(프로즈 자체는 SSOT 적응이라 코드처럼 박제 불가하나, 입력 파일·치환 규칙·검증 명령을 명시).
- **Type consistency**: 함수 시그니처 일관 — `parse_frontmatter``(fm,body)`, `antigravity_tool_names(tools)`, `render_codex_toml(name,description,sandbox_mode,body)`, `build_antigravity_agent(name,description,body,tools)`, `generate_one(name,check,drift)`. 모든 Task에서 동일 이름 사용. 모듈명 `sync_automation`(언더스코어)로 통일.
- **알려진 한계**: Task 7 Step 1 테스트는 codex toml 본문을 실디렉토리에서 읽어 통합 스모크(Step 3)로 보완. Task 6 Step 4의 grep 카운트는 게이트 "존재"만 보장하고 의미적 정확성은 보장하지 않음 → 실제 antigravity 실행 스모크는 Phase 1 검증과 함께 수행 권장.
@@ -0,0 +1,470 @@
# 3-플랫폼 동기화 Phase 1 — commands Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Claude의 13개 슬래시 command(`.claude/commands/*.md`)를 Codex CLI **skills**(`.agents/skills/<cmd>/SKILL.md`)와 Antigravity CLI **workflows**(`.agents/workflows/<cmd>.md`)로 생성하는 기능을 `scripts/sync_automation.py``commands` 타깃으로 추가한다.
**Architecture:** Phase 0의 SSOT→생성 모델을 commands로 확장한다. SSOT = `.claude/commands/<cmd>.md`(frontmatter `description`+`argument-hint` + 본문). 생성기는 (1) frontmatter를 플랫폼별 헤더로 변환, (2) `$ARGUMENTS` 토큰을 자연어 각괄호 인자(`argument-hint` 값)로 치환, (3) 본문의 platform-neutral 절차는 그대로 보존, (4) codex skill / antigravity workflow 두 포맷으로 직렬화한다. command 본문은 agent 본문과 달리 Claude 전용 tool 표현(`Read tool` 등)을 쓰지 않고 절차적 prose + shell + 서브에이전트 이름(Phase 0에서 3 플랫폼에 모두 포팅됨)만 참조하므로 기계 변환이 충분하다.
**Tech Stack:** Python 3.12 (stdlib), pytest (venv: `/home/donghyeon/dev/llm-wiki-private/.venv/bin/python`).
---
## 배경 / 확정된 포맷 사실 (공식문서 + 리서치)
- **Codex skills** (`developers.openai.com/codex/skills`): 디스커버리 `<repo>/.agents/skills/<name>/SKILL.md`(repo 커밋). frontmatter 필수 2필드 `name`(폴더명과 일치 권장) + `description`(언제 발동). **인자 placeholder 없음 — 자연어로 전달**(`$ARGUMENTS`/`$1` 미지원). `/skills` picker 또는 `$name` 멘션으로 명시 호출 + description 의미 매칭.
- **Antigravity workflows** (Google Codelabs / atamel.dev): `<repo>/.agents/workflows/<name>.md`, frontmatter **`description`만**(name=파일명), 본문은 prose 지시, 인자는 `<idea>` 식 각괄호 prose(placeholder 없음), `/<name> <args>` 호출.
- Codelab 실제 예시(verbatim):
```
---
description: Start the Autonomous AI Developer Pipeline sequence with a new idea
---
When the user types `/startcycle <idea>`, orchestrate the development process ...
```
- **경로 규약 결정**: 이 repo는 이미 `.agents/`(복수) 규약(`.agents/agents/`, `.agents/hooks.json`)을 쓰고 antigravity가 그것을 로드 중 → **antigravity workflows도 `.agents/workflows/`(복수)** 로 통일. codex skills는 공식 고정 경로 `.agents/skills/`.
- **알려진 충돌(미해결, 경험적 확인 필요)**: 일부 antigravity 빌드는 `.agents/skills/`도 skill로 읽어, codex command-skill이 antigravity에 semantic-trigger skill로 *이중 등록*될 수 있음(workflow + skill). 공식문서로 확정 불가 → **Task 1에서 실제 설치된 CLI로 경험적 확인** 후 필요 시 완화.
### 13개 command (SSOT: `.claude/commands/*.md`)
`blogify` · `branch` · `branch-spec` · `coverage` · `daily` · `depth` · `ingest` · `interviewize` · `lint` · `migrate-claims` · `projectize` · `query` · `tag`
frontmatter 공통: `description:` + `argument-hint:`. 본문은 `$ARGUMENTS` 토큰 + 절차(`## 작업 절차`) + 규칙. 일부는 `.claude/hooks/wiki_structure_lint.py`(실제 repo 스크립트, 크로스플랫폼 실행 가능 — 유지)와 포팅된 서브에이전트(`branch-depth-auditor` 등) 참조.
---
## File Structure
- Modify: `scripts/sync_automation.py` — `COMMAND_NAMES` 리스트, command frontmatter 파서 재사용(`parse_frontmatter`), `transform_command_body`, `render_codex_skill`, `render_antigravity_workflow`, `generate_command_one`, `main()` 의 `target` choices에 `commands` 추가.
- Modify: `scripts/test_sync_automation.py` — command 변환 단위 테스트.
- Create (생성물): `.agents/skills/<cmd>/SKILL.md` ×13 (codex), `.agents/workflows/<cmd>.md` ×13 (antigravity).
- Create: `docs/superpowers/notes/2026-06-04-phase1-empirical-format-check.md` — Task 1 경험적 확인 결과 기록.
- Modify (말미): `CLAUDE.md` §2 표 + `.codex/agents/README.md` — commands→skills/workflows 매핑 1단락.
> 책임 경계: 생성기는 frontmatter 변환 + `$ARGUMENTS` 치환 + 포맷 직렬화만. 본문 절차 prose는 platform-neutral이므로 보존. codex skill 본문에서 antigravity 전용 표기를 만들지 않고, antigravity workflow 본문에서 codex 전용 표기를 만들지 않는다(생성기가 플랫폼별로 분기).
---
## Task 1: 경험적 포맷·충돌 확인 + 결정 잠금
**Files:**
- Create: `docs/superpowers/notes/2026-04-phase1-empirical-format-check.md` (디렉토리 없으면 생성)
> 목적: 공식문서가 확정 못한 두 가지를 실제 설치된 CLI로 확인하고, 생성기 타깃 경로/인자 규약을 잠근다. CLI가 없으면 "가정 + 보류" 로 명시 기록(생성물은 가정대로 진행, 추후 검증).
- [ ] **Step 1: 설치 여부 확인**
```bash
cd /home/donghyeon/dev/llm-wiki-private
command -v codex && codex --version 2>&1 | head -1 || echo "codex: NOT INSTALLED"
ls -d ~/.gemini/antigravity-cli 2>/dev/null && echo "antigravity-cli home present" || echo "antigravity: home absent"
command -v agy 2>&1 || echo "agy (antigravity CLI) not on PATH"
```
- [ ] **Step 2: (codex 있으면) skill 디스커버리 경로 확인**
scratch skill을 만들어 codex가 `.agents/skills/`를 읽는지 확인:
```bash
mkdir -p /tmp/codex-skill-probe/.agents/skills/probe-skill
printf -- '---\nname: probe-skill\ndescription: probe codex skill discovery\n---\nprobe body\n' > /tmp/codex-skill-probe/.agents/skills/probe-skill/SKILL.md
# codex의 skills 목록 확인 (codex 설치 시): 예) `codex` TUI에서 /skills, 또는 가능한 CLI 서브커맨드
codex --help 2>&1 | grep -iE "skill|prompt" || echo "no skill subcommand surfaced in --help"
```
결과(읽힘/안읽힘/불명)를 노트에 기록.
- [ ] **Step 3: (antigravity 있으면) `.agents/skills/` 이중 로드 여부 확인**
antigravity가 `.agents/skills/`를 skill로 읽는지(=codex command-skill 이중 등록 위험) 실제 빌드에서 확인. 가능한 방법: antigravity CLI의 skill/workflow 목록 출력 커맨드, 또는 `~/.gemini/antigravity-cli/` 로그/registry 확인. 결과를 노트에 기록.
- [ ] **Step 4: 결정 기록**
노트에 다음을 표로 확정(경험 결과 또는 "UNVERIFIED — 가정"):
| 항목 | 결정 | 근거 |
|---|---|---|
| codex skill 경로 | `.agents/skills/<cmd>/SKILL.md` | 공식 디스커버리 경로 |
| antigravity workflow 경로 | `.agents/workflows/<cmd>.md` | repo `.agents/` 복수 규약 일치 |
| `$ARGUMENTS` 매핑 | argument-hint 각괄호 prose로 치환 | 양 플랫폼 placeholder 미지원 |
| `.agents/skills/` antigravity 이중로드 | {{읽음→완화 필요 / 안읽음→무관 / UNVERIFIED}} | Step 3 결과 |
- [ ] **Step 5: Commit**
```bash
cd /home/donghyeon/dev/llm-wiki-private
git add docs/superpowers/notes/
git commit -m "docs(phase1): empirical format + skills-collision check, lock target paths"
```
---
## Task 2: command 파싱 + skill/workflow 직렬화 (순수 함수, TDD)
**Files:**
- Modify: `scripts/sync_automation.py`
- Test: `scripts/test_sync_automation.py`
- [ ] **Step 1: Write the failing test**
```python
# append to scripts/test_sync_automation.py
_SAMPLE_CMD = (
"---\n"
"description: 브랜치 노트의 구현 착수 깊이 점검\n"
"argument-hint: <브랜치 이름>\n"
"---\n\n"
"브랜치 노트 1개의 깊이를 점검합니다.\n\n"
"**브랜치 이름:** $ARGUMENTS\n\n"
"## 작업 절차\n1. `branch-depth-auditor` 서브에이전트를 디스패치한다.\n"
)
def test_transform_command_body_replaces_arguments():
out = s.transform_command_body(_body_of(_SAMPLE_CMD), "<브랜치 이름>")
assert "$ARGUMENTS" not in out
assert "<브랜치 이름>" in out
# platform-neutral 절차/에이전트 참조는 보존
assert "branch-depth-auditor 서브에이전트를 디스패치" in out
def test_render_codex_skill_frontmatter():
out = s.render_codex_skill(
name="depth",
description="브랜치 노트의 구현 착수 깊이 점검",
arg_hint="<브랜치 이름>",
body="**브랜치 이름:** <브랜치 이름>\n\n## 작업 절차\n1. ...\n",
)
fm, body = s.parse_frontmatter(out)
assert fm["name"] == "depth"
assert fm["description"] # non-empty
assert "$ARGUMENTS" not in body
assert "## 작업 절차" in body
def test_render_antigravity_workflow_frontmatter_and_invocation():
out = s.render_antigravity_workflow(
name="depth",
description="브랜치 노트의 구현 착수 깊이 점검",
arg_hint="<브랜치 이름>",
body="## 작업 절차\n1. ...\n",
)
fm, body = s.parse_frontmatter(out)
assert set(fm.keys()) == {"description"} # name comes from filename, not frontmatter
# 호출 안내가 본문 상단에 있어야 함 (Codelab 패턴)
assert "/depth <브랜치 이름>" in body
assert "## 작업 절차" in body
```
(헬퍼 `_body_of` 는 테스트 상단에 추가: `def _body_of(md): return s.parse_frontmatter(md)[1]`.)
- [ ] **Step 2: Run test to verify it fails**
Run: `cd /home/donghyeon/dev/llm-wiki-private && .venv/bin/python -m pytest scripts/test_sync_automation.py -k "command or codex_skill or workflow" -v`
Expected: FAIL with `AttributeError: ... 'transform_command_body'`.
- [ ] **Step 3: Write minimal implementation**
```python
# add near COMMAND-related code in sync_automation.py
COMMAND_NAMES = [
"blogify", "branch", "branch-spec", "coverage", "daily", "depth",
"ingest", "interviewize", "lint", "migrate-claims", "projectize",
"query", "tag",
]
CLAUDE_COMMANDS = REPO / ".claude" / "commands"
CODEX_SKILLS = REPO / ".agents" / "skills"
ANTIGRAV_WORKFLOWS = REPO / ".agents" / "workflows"
def transform_command_body(body: str, arg_hint: str) -> str:
# Neither Codex skills nor Antigravity workflows support a $ARGUMENTS
# placeholder; both take arguments as natural language. Replace the token
# with the argument-hint's angle-bracket prose so the body still reads well.
return body.replace("$ARGUMENTS", arg_hint)
def render_codex_skill(name: str, description: str, arg_hint: str, body: str) -> str:
# description drives implicit trigger; fold in the arg hint for clarity.
desc = f"{description} (입력: {arg_hint})"
return (
"---\n"
f"name: {name}\n"
f"description: {desc}\n"
"---\n\n"
f"{body.rstrip()}\n"
)
def render_antigravity_workflow(name: str, description: str, arg_hint: str, body: str) -> str:
invocation = f"사용자가 `/{name} {arg_hint}` 를 입력하면 아래 절차를 수행한다.\n\n"
return (
"---\n"
f"description: {description}\n"
"---\n\n"
f"{invocation}{body.rstrip()}\n"
)
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd /home/donghyeon/dev/llm-wiki-private && .venv/bin/python -m pytest scripts/test_sync_automation.py -k "command or codex_skill or workflow" -v`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
cd /home/donghyeon/dev/llm-wiki-private
git add scripts/sync_automation.py scripts/test_sync_automation.py
git commit -m "feat(sync): command body transform + codex skill / antigravity workflow renderers"
```
---
## Task 3: command 로더 + CLI `commands` 타깃 (TDD)
**Files:**
- Modify: `scripts/sync_automation.py`
- Test: `scripts/test_sync_automation.py`
- [ ] **Step 1: Write the failing test**
```python
def test_load_command_inputs_for_existing_command():
ci = s.load_command_inputs("depth")
assert ci.name == "depth"
assert ci.description
assert ci.arg_hint.startswith("<") or "--" in ci.arg_hint
assert "$ARGUMENTS" in ci.body or "작업 절차" in ci.body # raw body still has token
def test_generate_command_writes_both(tmp_path, monkeypatch):
monkeypatch.setattr(s, "CODEX_SKILLS", tmp_path / "skills")
monkeypatch.setattr(s, "ANTIGRAV_WORKFLOWS", tmp_path / "workflows")
s.generate_command_one("depth", check=False)
skill = tmp_path / "skills" / "depth" / "SKILL.md"
wf = tmp_path / "workflows" / "depth.md"
assert skill.exists() and wf.exists()
sfm, sbody = s.parse_frontmatter(skill.read_text())
assert sfm["name"] == "depth" and "$ARGUMENTS" not in sbody
wfm, wbody = s.parse_frontmatter(wf.read_text())
assert "/depth" in wbody and "$ARGUMENTS" not in wbody
def test_main_commands_target_check_is_clean_after_generate():
# after generation (Task 4), --check must be drift-free
rc = s.main(["commands", "--check"])
assert rc == 0
```
> 마지막 테스트는 Task 4 생성 이후에만 통과한다. Task 3 단계에서는 앞 두 테스트만 대상으로 실행하고, 세 번째는 Task 4 검증에서 green 확인.
- [ ] **Step 2: Run test to verify it fails**
Run: `cd /home/donghyeon/dev/llm-wiki-private && .venv/bin/python -m pytest scripts/test_sync_automation.py -k "load_command or generate_command" -v`
Expected: FAIL with `AttributeError: ... 'load_command_inputs'`.
- [ ] **Step 3: Write minimal implementation**
```python
@dataclass
class CommandInputs:
name: str
description: str
arg_hint: str
body: str
def load_command_inputs(name: str) -> CommandInputs:
fm, body = parse_frontmatter((CLAUDE_COMMANDS / f"{name}.md").read_text())
return CommandInputs(
name=name,
description=fm.get("description", "").strip(),
arg_hint=fm.get("argument-hint", "").strip(),
body=body,
)
def generate_command_one(name: str, check: bool, drift: list[str] | None = None) -> None:
drift = drift if drift is not None else []
ci = load_command_inputs(name)
tbody = transform_command_body(ci.body, ci.arg_hint)
skill_text = render_codex_skill(ci.name, ci.description, ci.arg_hint, tbody)
wf_text = render_antigravity_workflow(ci.name, ci.description, ci.arg_hint, tbody)
_write_or_check(CODEX_SKILLS / name / "SKILL.md", skill_text, check, drift)
_write_or_check(ANTIGRAV_WORKFLOWS / f"{name}.md", wf_text, check, drift)
```
그리고 `main()` 을 확장: `target` choices에 `commands` 추가하고 분기.
```python
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="Sync Claude agents/commands -> Codex/Antigravity native files")
ap.add_argument("target", choices=["agents", "commands"], help="what to sync")
ap.add_argument("--check", action="store_true", help="fail (exit 2) if outputs drift from SSOT")
ap.add_argument("--only", help="single name (default: all)")
args = ap.parse_args(argv)
if args.target == "agents":
names = [args.only] if args.only else AGENT_NAMES
gen = generate_one
label = "agents (codex .toml + antigravity agent.json)"
else:
names = [args.only] if args.only else COMMAND_NAMES
gen = generate_command_one
label = "commands (codex skill + antigravity workflow)"
drift: list[str] = []
for name in names:
try:
gen(name, check=args.check, drift=drift)
except FileNotFoundError as e:
print(f"ERROR: missing SSOT for '{name}': {e.filename}", file=sys.stderr)
return 1
if args.check and drift:
print("DRIFT detected in:\n " + "\n ".join(drift), file=sys.stderr)
return 2
action = "checked" if args.check else "generated"
print(f"{action} {len(names)} {label}")
return 0
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd /home/donghyeon/dev/llm-wiki-private && .venv/bin/python -m pytest scripts/test_sync_automation.py -k "load_command or generate_command" -v`
Expected: PASS. Also run the FULL suite to confirm the `main()` refactor didn't break agents:
Run: `.venv/bin/python -m pytest scripts/test_sync_automation.py -q`
Expected: all green. Then sanity: `python3 scripts/sync_automation.py agents --check` still exits 0.
- [ ] **Step 5: Commit**
```bash
cd /home/donghyeon/dev/llm-wiki-private
git add scripts/sync_automation.py scripts/test_sync_automation.py
git commit -m "feat(sync): commands target — load + generate codex skills / antigravity workflows"
```
---
## Task 4: 13개 command 전체 생성 + 검증 + drift-0
**Files:**
- Generate: `.agents/skills/<cmd>/SKILL.md` ×13, `.agents/workflows/<cmd>.md` ×13
- [ ] **Step 1: 생성 + 검증**
```bash
cd /home/donghyeon/dev/llm-wiki-private
python3 scripts/sync_automation.py commands
echo "--- counts ---"
echo "skills: $(ls .agents/skills/*/SKILL.md | wc -l) | workflows: $(ls .agents/workflows/*.md | wc -l)"
echo "--- validate ---"
.venv/bin/python -c "
import sys; sys.path.insert(0,'scripts'); import sync_automation as s
for n in s.COMMAND_NAMES:
sfm,sbody=s.parse_frontmatter(open(f'.agents/skills/{n}/SKILL.md').read())
assert sfm['name']==n and sfm['description'] and '\$ARGUMENTS' not in sbody, n
wfm,wbody=s.parse_frontmatter(open(f'.agents/workflows/{n}.md').read())
assert 'description' in wfm and f'/{n}' in wbody and '\$ARGUMENTS' not in wbody, n
print('all 13 commands ok (skill name+desc, workflow desc+invocation, no \$ARGUMENTS)')
"
echo "--- no {{ }} placeholders leaked into workflows (antigravity G3) ---"
! grep -l '{{' .agents/workflows/*.md || echo "WARNING: {{ }} found"
```
Expected: `skills: 13 | workflows: 13`, validation ok, no `{{` warning.
- [ ] **Step 2: drift-0**
```bash
cd /home/donghyeon/dev/llm-wiki-private
python3 scripts/sync_automation.py commands --check; echo "exit=$?"
```
Expected: `checked 13 commands ...` + `exit=0`.
- [ ] **Step 3: 전체 테스트(세 번째 main commands check 테스트 포함)**
Run: `.venv/bin/python -m pytest scripts/test_sync_automation.py -q`
Expected: all green.
- [ ] **Step 4: Commit**
```bash
cd /home/donghyeon/dev/llm-wiki-private
git add .agents/skills/ .agents/workflows/
git commit -m "feat(sync): generate 13 commands as codex skills + antigravity workflows"
```
---
## Task 5: 대표 command 실호출 스모크 (CLI 있으면)
> Phase 0 agents와 달리 commands는 사용자가 실제로 호출하는 표면이므로, 설치된 CLI에서 1~2개를 실호출해 형식이 맞는지 확인한다. CLI 미설치면 SKIP + 노트 기록.
- [ ] **Step 1: codex skill 인식 확인 (codex 있으면)**
`.agents/skills/depth/SKILL.md` 가 codex의 `/skills` 또는 `$depth` 로 인식되는지 확인. 인식 안 되면 frontmatter/경로를 Task 1 노트와 대조해 진단.
- [ ] **Step 2: antigravity workflow 인식 확인 (antigravity 있으면)**
`.agents/workflows/depth.md` 가 antigravity에서 `/depth` 로 등록되는지 확인. 동시에 Task 1의 이중로드 가설(코덱스 skill이 antigravity에 새는지)을 `/skills` 목록으로 재확인.
- [ ] **Step 3: 결과를 Task 1 노트에 추가 기록 + (필요 시) 완화**
- 이중로드가 실제로 발생하고 바람직하지 않으면: 완화안을 노트에 적고 사용자에게 에스컬레이션(예: codex skill만 두고 antigravity는 workflow만 쓰도록 build 설정, 또는 경로 분리). **이 단계에서 임의로 큰 구조 변경하지 말 것** — 결과만 보고.
- [ ] **Step 4: Commit (노트 갱신 시)**
```bash
cd /home/donghyeon/dev/llm-wiki-private
git add docs/superpowers/notes/
git commit -m "docs(phase1): empirical smoke results for codex skills / antigravity workflows"
```
---
## Task 6: 문서 갱신 (commands→skills/workflows 매핑)
**Files:**
- Modify: `CLAUDE.md` (§2 디렉터리 역할 표 또는 자동화 섹션)
- Modify: `.codex/agents/README.md`
- [ ] **Step 1: CLAUDE.md 에 1단락**
§2의 자동화 목록에 codex/antigravity의 command 등가물을 명시:
- Codex: `.agents/skills/<cmd>/SKILL.md` (13개, `$cmd` 호출), 생성 `python3 scripts/sync_automation.py commands`.
- Antigravity: `.agents/workflows/<cmd>.md` (13개, `/cmd` 호출).
- 인자는 placeholder 없이 자연어(각괄호 prose).
- [ ] **Step 2: `.codex/agents/README.md` 의 "Native generation" 노트에 commands 줄 추가**
`sync_automation.py commands` 로 13개 command가 `.agents/skills/`(codex) + `.agents/workflows/`(antigravity)에 생성됨을 1줄 추가.
- [ ] **Step 3: 검증 + Commit**
```bash
cd /home/donghyeon/dev/llm-wiki-private
grep -c "sync_automation.py commands" CLAUDE.md .codex/agents/README.md
git add CLAUDE.md .codex/agents/README.md
git commit -m "docs(sync): document commands -> codex skills + antigravity workflows"
```
---
## Phase 1 완료 기준 (Definition of Done)
- `sync_automation.py commands` 타깃 동작 + 테스트 green.
- `.agents/skills/<cmd>/SKILL.md` 13개 + `.agents/workflows/<cmd>.md` 13개 생성·검증.
- `python3 scripts/sync_automation.py commands --check` exit 0 (drift 0).
- `$ARGUMENTS` 토큰이 생성물에 0개, antigravity workflow에 `{{ }}` 0개.
- Task 1/5 경험적 확인 결과가 노트에 기록(또는 CLI 미설치 시 가정으로 명시).
- CLAUDE.md + codex README에 매핑 문서화.
이후: **Phase 2**(hooks → `.codex/hooks.json`, `AGENTS.md`, CLAUDE.md의 구식 codex "수동 cat" 서술 정리 — line 76)는 별도 plan.
---
## Self-Review (작성자 체크)
- **Spec coverage**: 설계 §4 Phase 1(commands → codex skills + antigravity workflows, 생성기 commands 타깃 확장)의 모든 요소가 Task 2~4에 매핑. 설계 §6 리스크의 `.agents/skills/` 충돌은 Task 1/5 경험적 확인으로 처리.
- **Placeholder scan**: 코드 스텝은 실제 코드 포함. Task 1/5는 경험적 절차라 결과가 환경 의존 — 명령 + 기록 표 + "UNVERIFIED 시 가정 명시" 로 결정론화.
- **Type consistency**: `transform_command_body(body, arg_hint)`, `render_codex_skill(name,description,arg_hint,body)`, `render_antigravity_workflow(name,description,arg_hint,body)`, `CommandInputs`, `load_command_inputs(name)`, `generate_command_one(name,check,drift)` — 전 Task 일관. `_write_or_check`/`parse_frontmatter`는 Phase 0 함수 재사용.
- **알려진 한계**: command 본문이 platform-neutral하다는 가정에 의존(실측: `.claude/commands/*.md`는 `Read tool` 류 미사용, shell·서브에이전트명·repo 경로만 참조). 만약 특정 command가 Claude 전용 표기를 쓰면 그 command만 생성 후 hand-review 필요 — Task 4 검증의 `$ARGUMENTS`/`{{`grep으로 1차 포착.
@@ -0,0 +1,298 @@
# 3-플랫폼 동기화 Phase 2 (마지막) — hooks + 프로젝트 지침 + 정리 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax.
**Goal:** Claude의 결정론 훅(`wiki_structure_lint.py` 구조 린트 + `wiki_claim_gate.py` claim 게이트)을 Codex CLI(`.codex/hooks.json`)에 연결하고, 세 플랫폼이 동일한 프로젝트 지침(`CLAUDE.md`)을 읽도록 codex `AGENTS.md`/config를 구성하며, Phase 0~1에서 생긴 문서 잔여를 정리해 동기화를 완결한다.
**Architecture:** 훅 스크립트는 이미 멀티-variant 입력 리더(`tool_name` / `tool_call.name`)를 갖췄지만 *차단 규약*과 *출력 스키마*가 플랫폼마다 다르다(Claude: exit 2+stderr / Antigravity: `{decision:deny}` JSON / Codex: 경험적 확인 필요). 따라서 Phase 2는 **(1) 이식성 버그 선수정**(claim_gate 하드코딩 ROOT) → **(2) codex 훅 payload·차단 규약 경험적 캡처**(codex 설치됨) → **(3) `.codex/hooks.json` 배선 + 필요 시 codex-variant 어댑트** → **(4) 프로젝트 지침 DRY 구성** → **(5) CLAUDE.md 잔여 정리** 순으로 간다. 훅 *스크립트 SSOT*`.claude/hooks/` 1벌을 유지하고 플랫폼별 config 가 그것을 가리킨다(스크립트 중복 금지).
**Tech Stack:** Python 3.12, codex-cli 0.136.0, agy(Antigravity), pytest(venv).
---
## 배경 / 현재 훅 지형 (실측)
| 훅 | 위치 | root 해석 | 입력 | 차단 규약 |
|---|---|---|---|---|
| `wiki_structure_lint.py` | `.claude/hooks/` | `DEFAULT_ROOT = SCRIPT.parents[2]` (동적·이식 OK) | `--hook`(stdin JSON→file_path, C2 non-blocking) / `--file` / `--all` | non-blocking(경고) |
| `wiki_claim_gate.py` | `.claude/hooks/` | **`ROOT=Path("/home/donghyeon/Documents/LLM Wiki")` 하드코딩 — 이 repo 경로 아님(버그)** | stdin JSON (`tool_name`/`tool_call.name`/`hook_event_name`) | Claude: exit 2 + stderr |
| `wiki_hard_gate.py` | `~/.gemini/antigravity-cli/hooks/` (global) | — | antigravity protobuf (`tool_call.{name,input}`) | Antigravity: stdout `{decision:"deny"}` |
- Claude `.claude/settings.json`: PreToolUse(claim_gate, matcher `*`) + PostToolUse(structure_lint `--hook`, matcher `Write|Edit|MultiEdit`) + SubagentStart/Stop(claim_gate). 명령은 `python3 "$CLAUDE_PROJECT_DIR"/.claude/hooks/<script>`.
- Antigravity `.agents/hooks.json`: `wiki-hard-gate` PreToolUse(matcher `*`) → global `wiki_hard_gate.py`. 이미 claim/self-grep 게이트 역할 수행.
- Codex hooks(공식, `developers.openai.com/codex/hooks`): `<repo>/.codex/hooks.json` 또는 `config.toml [hooks]`. 이벤트 PreToolUse/PostToolUse/Stop/SessionStart/SubagentStart/SubagentStop 등. matcher(regex)+command. **payload·차단 규약은 본 plan Task 2 에서 경험적 캡처.**
- Codex 프로젝트 지침: `AGENTS.md`(root-down 연결). config `project_doc_fallback_filenames` 로 다른 파일명을 지침으로 인정 가능 → **CLAUDE.md 를 codex 지침으로 재사용(DRY)** 가능.
---
## File Structure
- Modify: `.claude/hooks/wiki_claim_gate.py` — 하드코딩 ROOT → 동적(`SCRIPT.parents[2]` 또는 `$CLAUDE_PROJECT_DIR`/cwd). 모든 플랫폼 공용 버그픽스.
- Create: `.codex/hooks.json` — structure_lint(PostToolUse) + claim_gate(PreToolUse) 배선.
- Create: `.codex/config.toml``project_doc_fallback_filenames = ["CLAUDE.md"]` (codex 가 CLAUDE.md 를 지침으로 읽도록, DRY).
- Create (조건부): `AGENTS.md`(root) — config fallback 이 동작 안 하면 thin pointer 대안.
- Create: `docs/superpowers/notes/2026-06-04-phase2-codex-hook-schema.md` — codex 훅 payload·차단 규약 경험적 기록.
- Modify (정리): `CLAUDE.md` §2 표(line 48 `.claude/commands/` 행에 codex/antigravity 등가 1줄) + 자동화 섹션 잔여 "7개" 카운트/구식 서술.
> 훅 *스크립트* 는 `.claude/hooks/` 1벌만 SSOT. codex `.codex/hooks.json` 은 절대경로/`$CODEX_PROJECT_DIR` 로 그 스크립트를 가리키며 복제하지 않는다.
---
## Task 1: claim_gate 하드코딩 ROOT 버그픽스 (공용 선수정)
**Files:**
- Modify: `.claude/hooks/wiki_claim_gate.py`
- Test: `.claude/hooks/` 기존 테스트가 있으면 거기, 없으면 `scripts/test_sync_automation.py` 밖의 별도 확인.
- [ ] **Step 1: 현재 ROOT 사용처 확인**
```bash
cd /home/donghyeon/dev/llm-wiki-private
grep -nE "ROOT" .claude/hooks/wiki_claim_gate.py
```
사용처: L20 정의, L81 `ROOT / p`(상대→절대), L149 `relative_to(ROOT)`.
- [ ] **Step 2: 동적 root 로 교체**
`structure_lint.py` 와 동일 패턴 적용. L20 을:
```python
ROOT = Path(__file__).resolve().parents[2] # .claude/hooks/<this> -> repo root
```
로 교체(환경변수 우선이 필요하면 `Path(os.environ.get("CLAUDE_PROJECT_DIR") or Path(__file__).resolve().parents[2])` — 단 codex 는 `CODEX_PROJECT_DIR`, antigravity 는 다른 변수일 수 있으므로 **파일 위치 기반이 가장 이식적**. 파일 위치 기반으로 간다).
- [ ] **Step 3: 동작 확인 (allow 경로 + relative_to 안전)**
```bash
cd /home/donghyeon/dev/llm-wiki-private
# 무해한 read 툴 이벤트는 allow(exit 0)
echo '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"README.md"}}' | python3 .claude/hooks/wiki_claim_gate.py; echo "exit=$?"
# repo 내부 경로가 relative_to 에서 깨지지 않음
echo '{"hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"raw/branch-notes/x.md","content":"---\ntitle: t\n---\n"}}' | python3 .claude/hooks/wiki_claim_gate.py; echo "exit=$?"
```
Expected: 첫 명령 `exit=0`. 둘째는 게이트 규칙에 따라 0 또는 2 — **ValueError/traceback 이 없어야** 함(하드코딩 ROOT 였으면 `relative_to` 에서 깨졌음).
- [ ] **Step 4: Commit**
```bash
git add .claude/hooks/wiki_claim_gate.py
git commit -m "fix(hooks): claim_gate uses dynamic repo root (was hardcoded stale path) — portable across platforms"
```
---
## Task 2: codex 훅 payload·차단 규약 경험적 캡처
**Files:**
- Create: `docs/superpowers/notes/2026-06-04-phase2-codex-hook-schema.md`
> codex 훅이 PreToolUse 에 넘기는 JSON 구조와 *차단 방법*(exit code? stdout JSON `decision`?)을 실제로 캡처해야 `.codex/hooks.json` 을 정확히 쓸 수 있다.
- [ ] **Step 1: codex 훅 문서/도움말 확인**
```bash
cd /home/donghyeon/dev/llm-wiki-private
codex --help 2>&1 | grep -iE "hook" || echo "no hook in top help"
codex exec --help 2>&1 | grep -iE "hook" || echo "no hook flag in exec"
```
- [ ] **Step 2: 캡처용 probe 훅으로 실제 payload 덤프**
`.codex/hooks.json` 에 임시 probe(stdin 을 파일로 덤프 후 allow)를 걸고 codex 로 1개 툴을 실행시켜 payload 를 캡처:
```bash
cd /home/donghyeon/dev/llm-wiki-private
mkdir -p .codex
cat > /tmp/codex-hook-probe.py <<'PY'
import sys, json, pathlib
data = sys.stdin.read()
pathlib.Path("/tmp/codex-hook-payload.json").write_text(data)
sys.exit(0) # allow
PY
cat > .codex/hooks.json <<'JSON'
{ "hooks": { "PreToolUse": [ { "matcher": "*",
"hooks": [ { "type": "command", "command": "python3 /tmp/codex-hook-probe.py", "timeout": 30 } ] } ] } }
JSON
# codex 로 안전한 read-only 작업 한 번 실행 (sandbox read-only, 승인 never)
codex exec -s read-only -C /home/donghyeon/dev/llm-wiki-private "list the files in the repo root with ls" 2>&1 | tail -5 || true
echo "--- captured payload ---"; cat /tmp/codex-hook-payload.json 2>/dev/null || echo "no payload captured"
```
캡처된 JSON 의 필드(`tool_name`? `tool_call.name`? `hook_event_name`?)를 노트에 기록.
- [ ] **Step 3: 차단 규약 확인**
probe 를 `sys.exit(2)` + stderr 로 바꿔 codex 가 툴을 *차단*하는지, 아니면 stdout JSON(`{"decision":"deny"}`)을 요구하는지 확인(공식 hooks 문서 + 실측). 결과를 노트에 기록.
- [ ] **Step 4: 정리 + 노트 커밋**
```bash
cd /home/donghyeon/dev/llm-wiki-private
rm -f .codex/hooks.json # probe 제거 (Task 3 에서 정식 작성)
git add docs/superpowers/notes/2026-06-04-phase2-codex-hook-schema.md
git commit -m "docs(phase2): capture codex hook payload + block convention (empirical)"
```
---
## Task 3: `.codex/hooks.json` 배선 (+ 필요 시 claim_gate codex-variant)
**Files:**
- Create: `.codex/hooks.json`
- Modify (조건부): `.claude/hooks/wiki_claim_gate.py` (codex payload·차단 variant — Task 2 결과가 Claude 규약과 다를 때만)
- [ ] **Step 1: `.codex/hooks.json` 작성**
Task 2 에서 확인한 이벤트명/matcher 로:
```json
{
"hooks": {
"PreToolUse": [
{ "matcher": "Edit|Write|MultiEdit",
"hooks": [ { "type": "command",
"command": "python3 .codex/../.claude/hooks/wiki_claim_gate.py",
"timeout": 30 } ] } ],
"PostToolUse": [
{ "matcher": "Edit|Write|MultiEdit",
"hooks": [ { "type": "command",
"command": "python3 .codex/../.claude/hooks/wiki_structure_lint.py --hook",
"timeout": 30 } ] } ]
}
}
```
> command 경로는 Task 2 에서 codex 훅의 cwd 가 repo root 인지 확인 후 결정: repo-root 이면 `python3 .claude/hooks/...` (상대), 아니면 절대경로 `python3 /home/donghyeon/dev/llm-wiki-private/.claude/hooks/...`. **하드코딩 절대경로는 이식성 해치므로 codex 가 제공하는 project-dir 변수(`$CODEX_PROJECT_DIR` 등, Task 2 확인)를 우선 사용.** matcher 정규식은 codex 의 실제 tool 이름(Task 2)으로 맞춤 — codex tool 이름이 `shell`/`apply_patch` 등이면 그에 맞게 조정.
- [ ] **Step 2: claim_gate codex-variant (조건부)**
Task 2 가 "codex 차단 = exit 2 / 입력 = `tool_call.name`" 으로 Claude 와 호환이면 **스크립트 수정 불필요**(이미 멀티-variant 리더). 다르면(예: codex 가 stdout JSON 요구) `wiki_claim_gate.py` 에 codex 분기 추가:
- 입력: `hook_event_name`/`tool_name` 없으면 codex 필드에서 추출.
- 출력: codex 차단 규약에 맞춰 emit. **기존 Claude/antigravity 경로 회귀 없이** 분기.
TDD: codex payload 샘플(Task 2 캡처)을 fixture 로 한 단위 테스트 추가.
- [ ] **Step 3: 실호출 검증**
```bash
cd /home/donghyeon/dev/llm-wiki-private
# codex 가 구조 위반 파일 저장을 차단/경고하는지 (read-only 가 아닌 workspace-write 로 한 번)
codex exec -s workspace-write -C /home/donghyeon/dev/llm-wiki-private "create a file raw/branch-notes/zzz-hook-test.md with body 'no frontmatter'" 2>&1 | tail -8 || true
ls raw/branch-notes/zzz-hook-test.md 2>&1 # claim_gate 가 frontmatter 없는 raw 를 막았는지
rm -f raw/branch-notes/zzz-hook-test.md
```
기대: claim_gate 규칙에 따라 차단되거나 경고. 결과를 노트에 기록.
- [ ] **Step 4: Commit**
```bash
git add .codex/hooks.json .claude/hooks/wiki_claim_gate.py docs/superpowers/notes/
git commit -m "feat(codex): wire .codex/hooks.json (claim gate + structure lint) reusing .claude/hooks scripts"
```
---
## Task 4: 프로젝트 지침 DRY 구성 (codex AGENTS.md / config)
**Files:**
- Create: `.codex/config.toml`
- Create (조건부): `AGENTS.md`
- [ ] **Step 1: config fallback 시도 (DRY 우선)**
```bash
cd /home/donghyeon/dev/llm-wiki-private
cat > .codex/config.toml <<'TOML'
# codex 가 CLAUDE.md 를 프로젝트 지침으로 읽게 함 (SSOT 중복 방지)
project_doc_fallback_filenames = ["CLAUDE.md"]
TOML
# 확인: codex 가 CLAUDE.md 를 지침으로 로드하는지
codex exec -s read-only -C /home/donghyeon/dev/llm-wiki-private "이 저장소의 운영 규칙 문서 이름과 첫 섹션 제목을 말해줘" 2>&1 | tail -8 || true
```
codex 가 CLAUDE.md 내용을 인지하면 **AGENTS.md 불필요**(DRY 달성). 노트 기록.
- [ ] **Step 2: fallback 미동작 시 thin AGENTS.md (대안)**
config fallback 이 안 먹으면 root 에 thin pointer 작성(중복 본문 금지):
```markdown
# AGENTS.md
이 저장소의 운영 규칙 SSOT 는 `CLAUDE.md` 다. 모든 agent/명령은 `CLAUDE.md``rules/` 를 정독한다.
(이 파일은 codex/antigravity 가 AGENTS.md 를 우선 탐색할 때의 포인터일 뿐, 규칙 본문을 중복하지 않는다.)
```
- [ ] **Step 3: Commit**
```bash
git add .codex/config.toml AGENTS.md 2>/dev/null; git add .codex/config.toml
git commit -m "feat(codex): read CLAUDE.md as project instructions via config fallback (DRY, no duplication)"
```
---
## Task 5: antigravity hooks 커버리지 점검 (보강 여부 결정)
**Files:**
- Modify (조건부): `.agents/hooks.json`
- [ ] **Step 1: hard_gate 가 structure-lint/claim-gate 를 이미 커버하는지 판정**
```bash
cd /home/donghyeon/dev/llm-wiki-private
grep -nE "frontmatter|Parent|Claim|structure|template|raw/branch|decision" ~/.gemini/antigravity-cli/hooks/wiki_hard_gate.py | head
```
`wiki_hard_gate.py` 가 이미 claim/구조 게이트를 antigravity 스키마로 수행 중이면(현재 그렇게 보임) **추가 배선 불필요** — 그 사실을 노트에 명시하고 종료. 빠진 검사(예: structure_lint 의 C2)가 있고 antigravity 에서 필요하면 PreToolUse 항목 추가(단 antigravity `Stop` 은 응답 텍스트 못 봄 → content gate 는 PreToolUse 만).
- [ ] **Step 2: (보강 시) `.agents/hooks.json` 갱신 + Commit. (불필요 시) 노트만 커밋.**
```bash
cd /home/donghyeon/dev/llm-wiki-private
git add docs/superpowers/notes/ .agents/hooks.json 2>/dev/null
git commit -m "docs(phase2): antigravity hook coverage assessment (hard_gate already covers claim/structure)" || echo "nothing to commit"
```
---
## Task 6: CLAUDE.md / 문서 잔여 정리
**Files:**
- Modify: `CLAUDE.md`
- [ ] **Step 1: §2 표 + 잔여 카운트 정리**
- §2 디렉터리 역할 표(line 48 부근 `.claude/commands/`)에 "codex `.agents/skills/` · antigravity `.agents/workflows/` 로 동기화(`scripts/sync_automation.py commands`)" 1줄.
- 자동화 섹션의 "동일 7개 agent" → "9개 agent" (branch-depth-auditor·coverage-auditor 추가 반영). antigravity bullet 도 동일.
- (Phase 0~1 에서 이미 codex bullet·sync 경로는 갱신됨 — 누락분만.)
- [ ] **Step 2: 최종 동기화 sanity**
```bash
cd /home/donghyeon/dev/llm-wiki-private
python3 scripts/sync_automation.py agents --check && python3 scripts/sync_automation.py commands --check && echo "ALL DRIFT-0"
.venv/bin/python -m pytest scripts/test_sync_automation.py -q | tail -2
grep -rn "convert-wiki-agents" . --include=*.md 2>/dev/null | grep -v docs/superpowers || echo "no dead convert-script refs"
```
Expected: `ALL DRIFT-0`, 테스트 green, dead ref 없음.
- [ ] **Step 3: Commit**
```bash
git add CLAUDE.md
git commit -m "docs: CLAUDE.md final cleanup — 9 agents, commands cross-platform mapping (Phase 2)"
```
---
## Phase 2 완료 기준 (Definition of Done)
- claim_gate 하드코딩 ROOT 버그 수정(이식성).
- codex 훅 payload·차단 규약 경험적 캡처 + `.codex/hooks.json` 배선 동작(실호출 검증).
- codex 가 CLAUDE.md 를 프로젝트 지침으로 읽음(config fallback) 또는 thin AGENTS.md.
- antigravity 훅 커버리지 판정(보강 또는 "이미 커버" 명시).
- CLAUDE.md 잔여(카운트·매핑) 정리, dead ref 0.
- `agents`/`commands` 모두 drift-0, 테스트 green.
이로써 **3-플랫폼 동기화 전체 완료**: agents(9)·commands(13)·hooks·project-instructions 가 Codex·Antigravity 에 Claude 와 동등하게 구성됨. 이후: 브랜치 merge/PR (`finishing-a-development-branch`).
---
## Self-Review (작성자 체크)
- **Spec coverage**: 설계 §4 Phase 2 의 모든 항목 — codex hooks.json, AGENTS.md/지침, antigravity hooks 보강, CLAUDE.md 정리 — Task 1~6 에 매핑. 추가로 발견된 claim_gate 하드코딩 ROOT 버그를 Task 1 로 선수정(공용 이득).
- **Placeholder scan**: codex 훅 payload/차단 규약은 *미지(unknown)* 라 Task 2 에서 경험적 캡처 후 Task 3 가 그 결과에 분기하도록 구성(추측 금지). config fallback 동작 여부도 Task 4 에서 실측 후 분기.
- **Type/일관성**: 훅 스크립트 SSOT 1벌(`.claude/hooks/`) 원칙 유지, 플랫폼 config 가 가리키기만 함. codex command/agent 생성기(Phase 0~1)와 독립.
- **알려진 한계**: codex 훅의 정확한 payload/차단 규약이 docs 로 확정 안 됨 → Task 2 경험적 캡처에 의존. codex 가 설치돼 있어 실측 가능(미설치였으면 BLOCKED). antigravity 훅은 이미 hard_gate 로 커버될 가능성이 높아 Task 5 는 "판정 우선, 보강은 조건부".
@@ -0,0 +1,405 @@
# wiki/projects 전용 템플릿 · source_type 분리 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `wiki/projects/` canonical 레이어에 전용 템플릿 `wiki-project-template.md` + 전용 `source_type: project`를 부여하고, 결정론 린터가 새 템플릿 기준으로 wiki/projects 문서를 검증하게 해 현재 FAIL 중인 17개 문서를 PASS로 복구한다.
**Architecture:** 린터(`wiki_structure_lint.py`)는 source_type 어휘를 하드코딩하지 않고 `templates/*-template.md`의 frontmatter `source_type`을 읽어 동적 매핑한다. 따라서 새 템플릿이 `source_type: project`를 선언하면 `project`가 자동으로 유효 타입이 되고, 템플릿의 `##` 헤더가 필수 섹션 검사 기준이 된다. 유일한 코드 변경은 린터 `classify()`에 named-hub(형제 폴더를 가진 `wiki/<cat>/<slug>.md`) C1 면제를 추가하는 것.
**Tech Stack:** Markdown 템플릿/문서, Python 3 (린터 hook), Obsidian wikilink, bash 검증.
**제약 (사용자 지시):** **커밋하지 않는다.** 각 Task 끝의 "체크포인트"는 git commit 대신 린터 회귀 확인으로 대체한다.
**참조 spec:** `docs/superpowers/specs/2026-06-04-wiki-project-template-source-type-split-design.md`
---
## File Structure
| 파일 | 책임 | 변경 |
|---|---|---|
| `templates/wiki-project-template.md` | wiki/projects canonical 슬라이스 출력 형식 정의 (`source_type: project`, 9 필수 섹션) | 신규 |
| `.claude/hooks/wiki_structure_lint.py` | `classify()`에 named-hub C1/C3 면제 추가 | 수정 (유일 코드) |
| `CLAUDE.md` | §2 템플릿 목록 / §4 source_type 어휘 / §5 신뢰도 표에 `project` 등록 | 수정 |
| `rules/tag-taxonomy.md`, `rules/naming-conventions.md` | `project` 어휘 등록 | 수정 |
| `wiki/projects/ca-tmpl.md` + `wiki/projects/ca-tmpl/*.md` (16) | `source_type: project-note → project` + 1개 섹션 보강 | 수정 (17 파일) |
| `.claude/commands/ingest.md`, `.claude/commands/projectize.md` | wiki 추출 템플릿 참조 교정 | 수정 |
| `rules/linking-rules.md` | 224행(nested) 참조 교정, 190행(raw) 유지 | 수정 |
| `.claude/agents/wiki-research-lane.md` + `.codex/agents/wiki-research-lane.md` + `.agents/plugins/wiki-superpowers/agents/wiki-research-lane.md` | agent 본문 참조 교정 후 `scripts/sync_automation.py` 재생성 | 수정 |
---
## Task 1: 베이스라인 캡처 + 새 템플릿 생성 + 파일럿 슬라이스 1개 검증
**Files:**
- Create: `templates/wiki-project-template.md`
- Modify(파일럿): `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` frontmatter 1줄
- [ ] **Step 1: 현재 베이스라인 기록**
Run:
```bash
cd /home/donghyeon/dev/llm-wiki-private
python3 .claude/hooks/wiki_structure_lint.py --all 2>&1 | tail -20
```
Expected: `== 요약: 482개 중 FAIL 238 / PASS 244 ==`, `source_type별 FAIL``project-note 20`. 이 숫자를 메모(검증 기준).
- [ ] **Step 2: 새 템플릿 생성**
Create `templates/wiki-project-template.md` (헤더 텍스트는 기존 슬라이스와 byte 동일 — `header_tokens` 매칭 필수):
````markdown
---
title:
source_type: project
status: draft
confidence: unknown
tags: []
related_projects: []
last_reviewed:
---
# {{title}}
> Layer: `wiki/projects/` — canonical 실무 적용 문서(내 프로젝트 사실). 일반 개념은 `wiki/concepts/`, raw 프로젝트 hub는 `raw/project-notes/`(`project-template.md`) 사용.
> 본 문서는 **하나의 토픽/결정 영역** 슬라이스다. 프로젝트 전체 hub(아키텍처·시퀀스·Cluster)는 `wiki/projects/<project>.md` named-hub(MOC)와 그 SSOT인 `raw/project-notes/` 가 담당한다.
> 증거 등급(`actually-implemented`/`locally-verified`/`prod-verified`/`documented-only`/`planned`)을 섹션별로 분리해 외부 공개 가능 범위를 명확히 한다 (CLAUDE.md §6/§15).
## 프로젝트 컨텍스트
> 이 슬라이스가 다루는 결정/토픽의 배경. 문제 배경 + 검토한 선택지 + 결정 이유를 여기에 접어 서술(별도 필수 섹션 아님). 외부인이 "무엇을 왜 이렇게 했는가"를 1분에 이해할 수 있어야 함.
## 실제 구현 내용 (`actually-implemented`)
> 코드에 실제 존재하는 것만. 파일·클래스·task 단위로 구체적으로. 면접에서 "구현했다"고 말해도 되는 부분. 가능하면 ground-truth(레포 경로/커밋) 대조 근거를 함께.
## 로컬/dev 검증 (`locally-verified`)
> 로컬 또는 dev 환경에서 동작 확인한 부분. 어떻게 검증했는지(테스트 명령/로그/측정값)를 명시.
## 운영 검증 (`prod-verified`)
> 운영(prod) 환경에서 검증된 부분. 릴리즈 노트/운영 로그/모니터링/인시던트 근거. 없으면 "없음"이라고 명시.
## 문서/계획만 존재 (`documented-only` / `planned`)
> 설계/문서에만 있고 아직 구현 안 된 것. 면접·외부 공개에서 "구현했다"고 말하면 안 되는 부분. 후속 branch로 위임되는 항목은 링크.
## 면접에서 말할 수 있는 범위
> 자신 있게 / 적당히 / 답하면 안 되는 범위로 구분. 증거 등급과 일치해야 함.
## 과장 금지 지점
> 외부 설명 시 사실보다 부풀려지기 쉬운 표현. 자기 검열용.
## 관련 개념
> `[[wiki/concepts/...]]` 양방향 링크. 일반 개념과 본 프로젝트 사실을 연결.
## Sources
> 근거. 추출 출처 branch-note/raw, 그리고 ground-truth 레포. `[[raw/branch-notes/...]]`, `[[raw/project-notes/...]]` 등.
````
- [ ] **Step 3: 템플릿이 `project` 타입을 매핑하는지 확인 (아직 문서 미변경)**
Run:
```bash
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl/clean-architecture-package-layout.md 2>&1 | head -3
```
Expected: 여전히 FAIL (이 문서는 아직 `source_type: project-note`라 `project-template.md`에 매핑되어 14개 MISSING_SECTION). 템플릿 추가만으로는 기존 문서 영향 없음 — 정상.
- [ ] **Step 4: 파일럿 1개 슬라이스 마이그레이션**
Edit `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` frontmatter: `source_type: project-note` → `source_type: project` (Write/Edit 도구 사용 — shell 직접 쓰기는 hook이 차단).
- [ ] **Step 5: 파일럿 PASS 검증**
Run:
```bash
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl/clean-architecture-package-layout.md 2>&1 | head -5
```
Expected: `PASS wiki/projects/ca-tmpl/clean-architecture-package-layout.md` (MISSING_SECTION 0). 만약 일부 섹션이 여전히 누락으로 뜨면 = 템플릿 헤더 텍스트와 문서 헤더가 byte 불일치(위험 V1). 해당 헤더를 문서에서 복사해 템플릿을 정정 후 재실행.
- [ ] **Step 6: 체크포인트 (커밋 대신)**
Run:
```bash
git status --short
```
Expected: `templates/wiki-project-template.md`(신규) + 파일럿 슬라이스 1개 수정. 커밋하지 않음.
---
## Task 2: 린터 `classify()` named-hub 면제 + hub 검증
**Files:**
- Modify: `.claude/hooks/wiki_structure_lint.py:52-62` (classify 함수), `:490` (호출부)
- Modify: `wiki/projects/ca-tmpl.md` frontmatter 1줄
- [ ] **Step 1: hub 현재 상태 확인 (변경 전)**
Run:
```bash
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl.md 2>&1 | head -3
```
Expected: FAIL — `## 1. 프로젝트 개요 / Overview` 등 14개 MISSING_SECTION (hub이 raw `project-template`에 매핑됨).
- [ ] **Step 2: `classify()`에 named-hub 면제 추가**
`.claude/hooks/wiki_structure_lint.py`의 `classify` 함수(현재 52~62행)를 아래로 교체:
```python
def classify(rel, root=None):
"""문서를 검사 모드로 분류: 'full'(C1+C2+C3) | 'links'(C2만).
- raw/wiki 의 *콘텐츠* 문서(2단계 이상, hub/log 아님) : 전체.
- named-hub (wiki/<cat>/<slug>.md + 형제 폴더 <slug>/ 존재, linking-rules §12) : 링크만 (C1/C3 면제).
- 그 외 전부 (rules/ · templates/ · docs/ · 최상위 CLAUDE.md 등 · hub/MOC/log/README) : 링크만.
(템플릿 구조가 없거나 메타 문서이므로 C1/C3 면제, 그래프 ghost 방지용 C2 만.)
"""
parts = rel.split("/")
base = parts[-1]
# named-hub folder-note: <cat>/<slug>.md 에 형제 폴더 <slug>/ 가 있으면 MOC → 링크만
if root is not None and len(parts) == 3 and parts[0] in ("raw", "wiki") and base.endswith(".md"):
slug = base[:-3]
if (root / parts[0] / parts[1] / slug).is_dir():
return "links"
if parts[0] in ("raw", "wiki") and len(parts) > 2 and base not in LINK_ONLY_BASENAMES:
return "full"
return "links"
```
- [ ] **Step 3: 호출부에 `root` 전달**
`.claude/hooks/wiki_structure_lint.py`의 호출부(현재 490행) 교체:
```python
mode = "links" if args.links_only else classify(rel, root)
```
- [ ] **Step 4: hub이 면제되어 PASS 되는지 확인 (아직 source_type 미변경)**
Run:
```bash
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl.md 2>&1 | head -3
```
Expected: `PASS wiki/projects/ca-tmpl.md` (형제 폴더 `wiki/projects/ca-tmpl/` 존재 → links 모드, C1 면제). source_type 변경 전이라도 면제만으로 14 FAIL 해소.
- [ ] **Step 5: hub frontmatter 마이그레이션**
Edit `wiki/projects/ca-tmpl.md` frontmatter: `source_type: project-note` → `source_type: project` (Write/Edit 도구).
- [ ] **Step 6: hub 재검증 + 회귀 (raw 3개 불변)**
Run:
```bash
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl.md 2>&1 | head -3
python3 .claude/hooks/wiki_structure_lint.py --file raw/project-notes/ca-skeleton-operational-contract.md 2>&1 | head -3
```
Expected: hub `PASS`. raw/project-notes 문서는 **상태 불변**(형제 폴더 없음 → 여전히 full 모드, 종전 FAIL/PASS 그대로). raw가 PASS→FAIL 또는 면제로 바뀌면 V2 위험 발생 — classify 조건의 `is_dir()` 가드 확인.
- [ ] **Step 7: 체크포인트**
Run:
```bash
git diff --stat .claude/hooks/wiki_structure_lint.py
```
Expected: classify 함수 + 호출부만 변경. 커밋하지 않음.
---
## Task 3: 나머지 15개 슬라이스 일괄 마이그레이션 + 누락 섹션 보강
**Files:**
- Modify: `wiki/projects/ca-tmpl/*.md` 중 파일럿 제외 15개 frontmatter
- Modify: `wiki/projects/ca-tmpl/api-error-envelope-design.md` — `## 문서/계획만 존재` 섹션 추가
- [ ] **Step 1: 대상 15개 목록 확인**
Run:
```bash
ls -1 wiki/projects/ca-tmpl/*.md | grep -v clean-architecture-package-layout
```
Expected: 15개 경로(api-error-envelope-design, api-evolution-and-schema, config-and-adapter-templates, data-layer-persistence-cache-outbound, devops-ci-supply-chain-dx, idempotency-key-design, multi-tenancy-isolation-patterns, observability-log-metric-trace-runbook, privacy-file-domain-modeling, runtime-container-health-migration, sample-fixture-and-adoption, security-baseline-jwt-actuator-secrets, skeleton-governance-registry-verification-test-scorecard, transactional-outbox-pattern, transaction-boundary-abstraction).
- [ ] **Step 2: 15개 frontmatter 마이그레이션**
각 파일에 대해 Edit 도구로 `source_type: project-note` → `source_type: project`. (15개 개별 Edit. shell `sed` 금지 — hook 차단.)
- [ ] **Step 3: 누락 섹션 보강 (api-error-envelope-design.md)**
`wiki/projects/ca-tmpl/api-error-envelope-design.md`에 `## 운영 검증` 섹션 뒤(또는 `## 면접에서 말할 수 있는 범위` 앞)에 아래 섹션 추가:
```markdown
## 문서/계획만 존재 (`documented-only` / `planned`)
- (해당 시점 기준 documented-only / planned 항목을 여기에. 없으면 "현재 없음 — 본 슬라이스 범위는 모두 구현·검증됨"으로 명시.)
```
> 주: 실제 내용은 해당 문서의 사실에 맞게 채운다. 최소한 빈 플레이스홀더가 아니라 "현재 없음" 또는 실제 planned 항목이어야 한다(린터는 섹션 헤더 존재만 검사하지만, CLAUDE.md 과장 금지 정책상 내용 정직성 유지).
- [ ] **Step 4: 16개 슬라이스 전수 PASS 검증**
Run:
```bash
for f in wiki/projects/ca-tmpl/*.md; do
python3 .claude/hooks/wiki_structure_lint.py --file "$f" 2>&1 | grep -E "^(PASS|FAIL)"
done
```
Expected: 16줄 모두 `PASS`. FAIL이 있으면 해당 문서의 헤더 텍스트를 템플릿 9개와 대조(byte 일치 확인).
- [ ] **Step 5: 체크포인트**
Run:
```bash
git status --short wiki/projects/
```
Expected: hub 1 + nested 16 = 17개 수정. 커밋하지 않음.
---
## Task 4: source_type 어휘 등록 (SSOT 문서)
**Files:**
- Modify: `CLAUDE.md` (§2 템플릿 목록, §4 메타데이터 어휘, §5 신뢰도 표)
- Modify: `rules/tag-taxonomy.md:22`, `rules/naming-conventions.md` §2.11 근처
- [ ] **Step 1: CLAUDE.md §4 어휘에 `project` 추가**
`CLAUDE.md` §4의 `source_type 허용 어휘` 문장에 `project`를 추가. `concept | interview | portfolio | blog | llm-generated` 나열에 `project`를 포함시키고, "wiki/projects canonical 실무 적용 문서 → `source_type: project` (`wiki-project-template.md`)" 한 줄 설명 추가. `project-note`는 "raw 프로젝트 hub"로 의미 유지 명시.
- [ ] **Step 2: CLAUDE.md §5 신뢰도 표에 `project` 행 추가**
`| project-note | 포트폴리오 증거 후보 |` 행 아래(또는 `concept` 근처)에 추가:
```markdown
| `project` | wiki/projects canonical 실무 적용 문서 (검증된 내 프로젝트 사실, `verified` 지향) |
```
- [ ] **Step 3: CLAUDE.md §2 템플릿 목록 갱신**
`[[templates/project-template]]` 설명을 `raw/project-notes/` **전용**으로 명확화하고, 그 아래에 추가:
```markdown
- [[templates/wiki-project-template]] — `wiki/projects/` canonical 실무 적용 문서 슬라이스 (raw hub에서 /ingest·/projectize 로 추출)
```
- [ ] **Step 4: rules/tag-taxonomy.md L1 Type 어휘에 `project` 추가**
`rules/tag-taxonomy.md:22`의 L1 Type 나열(`... project-note, concept, interview, portfolio, blog, meta`)에 `project`를 추가(`project-note` 와 별도).
- [ ] **Step 5: rules/naming-conventions.md 에 source_type 명시**
`rules/naming-conventions.md`의 `wiki/projects/<project-slug>/<topic>.md` (nested, §2.11 근처) 설명에 "frontmatter `source_type: project`" 한 줄 추가. 경로 규칙 자체는 불변.
- [ ] **Step 6: 검증 (어휘 문서가 그래프 깨짐 없는지)**
Run:
```bash
python3 .claude/hooks/wiki_structure_lint.py --file CLAUDE.md 2>&1 | grep -E "BROKEN|PASS|FAIL" | head
git grep -n "source_type: project\b" CLAUDE.md rules/ | head
```
Expected: CLAUDE.md 링크 위반 0. `project` 어휘가 SSOT 문서에 등록됨.
- [ ] **Step 7: 체크포인트** — `git status --short`. 커밋하지 않음.
---
## Task 5: 커맨드 · 룰 · agent 참조 정합
**Files:**
- Modify: `.claude/commands/ingest.md`, `.claude/commands/projectize.md`
- Modify: `rules/linking-rules.md:224` (190 유지)
- Modify: `.claude/agents/wiki-research-lane.md`, `.codex/agents/wiki-research-lane.md`, `.agents/plugins/wiki-superpowers/agents/wiki-research-lane.md`
- Run: `scripts/sync_automation.py`
- [ ] **Step 1: /ingest 템플릿 참조 교정**
`.claude/commands/ingest.md` 4단계 "프로젝트 (`wiki/projects/`): `templates/project-template.md`" → `templates/wiki-project-template.md`.
- [ ] **Step 2: /projectize 템플릿 참조 교정**
`.claude/commands/projectize.md:34` "`templates/project-template.md` 적용" → "`templates/wiki-project-template.md` 적용".
- [ ] **Step 3: linking-rules nested 참조 교정 (raw 유지)**
`rules/linking-rules.md:224` (`wiki/projects/<project-slug>/<topic>.md (nested)` 행)의 `project-template.md` → `wiki-project-template.md`. **190행(`raw/project-notes/` 행)은 변경하지 않음.**
- [ ] **Step 4: wiki-research-lane 3-port 본문 교정**
3개 파일 각각에서 "wiki-project 시 `project-template.md`" → "wiki-project 시 `wiki-project-template.md`":
- `.claude/agents/wiki-research-lane.md` (47행 부근)
- `.codex/agents/wiki-research-lane.md`
- `.agents/plugins/wiki-superpowers/agents/wiki-research-lane.md`
- [ ] **Step 5: agent 머신 파일 재생성**
Run:
```bash
python3 scripts/sync_automation.py
git diff --stat .codex/ .agents/agents/
```
Expected: `.codex/agents/wiki-research-lane.toml`·`.agents/agents/wiki-research-lane/agent.json` 에 참조 변경만 반영(다른 agent 파일 변동 없음). 스크립트 에러 0.
- [ ] **Step 6: 잔존 "wiki 추출 의미" 참조 0 확인**
Run:
```bash
git grep -n "project-template" -- ':!docs/superpowers/' ':!templates/wiki-project-template.md'
```
Expected: 결과의 모든 `project-template` 참조가 **raw/project-notes hub 의미**만 남음 (linking-rules:190, 그 외 raw 맥락). wiki/projects 추출 의미의 잔존 참조 0.
- [ ] **Step 7: 체크포인트** — `git status --short`. 커밋하지 않음.
---
## Task 6: 최종 전수 검증 (완료 정의)
- [ ] **Step 1: 전체 린터 회귀**
Run:
```bash
python3 .claude/hooks/wiki_structure_lint.py --all 2>&1 | tail -20
```
Expected:
- 총 FAIL이 **238 → 221 근처로 감소** (project 17개 PASS 전환).
- `source_type별 FAIL`에서 `project-note` 가 20 → 3(raw만)으로 감소, `project` 는 0(전부 PASS).
- 신규 PASS→FAIL 역전 0건.
- [ ] **Step 2: project 17개 전수 PASS**
Run:
```bash
python3 .claude/hooks/wiki_structure_lint.py --file wiki/projects/ca-tmpl.md 2>&1 | grep -E "^(PASS|FAIL)"
for f in wiki/projects/ca-tmpl/*.md; do
python3 .claude/hooks/wiki_structure_lint.py --file "$f" 2>&1 | grep -E "^(PASS|FAIL)"
done
```
Expected: 17줄 모두 `PASS`.
- [ ] **Step 3: raw/project-notes 불변 확인**
Run:
```bash
for f in raw/project-notes/*.md; do
echo -n "$f: "; python3 .claude/hooks/wiki_structure_lint.py --file "$f" 2>&1 | grep -Eo "^(PASS|FAIL)" | head -1
done
```
Expected: 3개 모두 베이스라인(Task 1 Step 1)과 동일한 상태 — 본 작업으로 바뀌지 않음.
- [ ] **Step 4: 최종 변경 요약 (커밋 없이 사용자 검토용)**
Run:
```bash
git status --short
git diff --stat
```
Expected 변경 파일: 신규 템플릿 1, 린터 1, CLAUDE.md 1, rules 2, wiki/projects 17, commands 2, linking-rules 1, wiki-research-lane ×3 + 재생성 산출물. **커밋하지 않고** 사용자에게 결과 보고.
---
## Self-Review (작성자 체크)
- **Spec coverage:** 신규 템플릿(Task1) / 린터 classify 면제(Task2) / 17개 마이그레이션(Task1·2·3) / 어휘 등록(Task4) / 커맨드·rule·agent 참조(Task5) / 검증계획 §5(Task6) — 전부 태스크 매핑됨.
- **Placeholder scan:** 누락 섹션 보강(Task3 Step3)은 "현재 없음" 또는 실제 항목으로 채우라고 명시 — 빈 플레이스홀더 금지 규칙 반영.
- **Type/이름 일관성:** `wiki-project-template.md`, `source_type: project`, `classify(rel, root)` 시그니처가 전 태스크에서 일관.
- **위험 반영:** V1(헤더 byte 불일치 → Task1 Step5 회귀), V2(면제 과적용 → Task2 Step6 + Task6 Step3 raw 불변 확인).
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,746 @@
# project-note 작성 파이프라인 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** project-note 에 `/project`(스캐폴딩) + `/project-spec`(깊은 조사 오케스트레이터 + readiness 게이트) 2단 파이프라인을 추가해, 임의 프로젝트 hub 를 `ca-skeleton-operational-contract.md` 수준(caliber)으로 작성하게 한다.
**Architecture:** branch-note 의 `/branch`·`/branch-spec`·`branch-depth-gate`·`branch-depth-auditor` 4종을 project-note 용으로 미러링한다. 단 결정론 게이트는 *섹션명 매칭*이 아니라 *구조-불가지 proxy*(exemplar 가 template 섹션 구성을 안 따르기 때문)이며, caliber 판정은 신규 `project-readiness-auditor` LLM agent 가 R1~R4 로 한다. **Claude Code 전용** — Codex/Antigravity 포팅 없음.
**Tech Stack:** Markdown(rules/templates/commands/agents), Python 3 stdlib(`wiki_structure_lint.py` 확장 + `unittest`).
**설계 출처:** `docs/superpowers/specs/2026-06-05-project-note-pipeline-design.md`
---
## File Structure
| 파일 | 책임 | 신규/편집 |
|---|---|---|
| `rules/project-readiness-gate.md` | 4축(R1~R4)·깊이 사다리·proxy·실패 모드 정의 (방법론 SSOT) | 신규 |
| `templates/project-template.md` | §8.0 Branch 분해/실행계획 표 추가 (핸드오프) | 편집 |
| `.claude/hooks/wiki_structure_lint.py` | `"project"` 모드 + proxy 검사 함수 | 편집 |
| `.claude/hooks/test_wiki_structure_lint.py` | project 모드 단위 테스트 | 편집 |
| `.claude/agents/project-readiness-auditor.md` | hub 깊이 의미 판정 (read-only) | 신규 |
| `.claude/commands/project.md` | 얇은 스캐폴딩 커맨드 | 신규 |
| `.claude/commands/project-spec.md` | 깊은 조사 오케스트레이터 | 신규 |
| `CLAUDE.md` | agent 목록 +1(Claude 전용 명시) · 커맨드 목록 +2 | 편집 |
Task 순서는 의존성 순(rules → template → linter → agent → commands → CLAUDE.md → dogfood). 각 Task 는 독립 커밋.
---
## Task 1: rules/project-readiness-gate.md (방법론 SSOT)
**Files:**
- Create: `rules/project-readiness-gate.md`
- [ ] **Step 1: 파일 작성**
아래 내용 그대로 생성:
````markdown
# rules/project-readiness-gate — project-note 작성 완성도 게이트
> `rules/` 의 방법론 규칙. project-note(프로젝트 hub) 1개가 **다른 작업의 출발점이 될 만큼 깊고 근거 있는가**를 판정한다.
> 기준선은 `raw/project-notes/ca-skeleton-operational-contract.md` 의 *caliber*(엄격성 수준)이다 — 그 노트의 *내용·섹션 구성을 복제하라는 게 아니다*. 프로젝트마다 내용도 섹션 조직도 다르며, 게이트는 *깊이·근거·분해 수준*만 강제한다.
> `branch-depth-gate`(브랜치 1개 착수 깊이)와 다른 층: 본 게이트는 *프로젝트 hub* 미시 게이트.
## 적용
- 대상: `raw/project-notes/*.md`.
- 실행: `/project-spec <slug> <목표>` 의 **내부 마지막 단계** (독립 `/project-readiness` 커맨드 없음) →
1. **1차 결정론 검사** `.claude/hooks/wiki_structure_lint.py` (project 모드 — proxy + 링크)
2. **2차 의미 판정** `project-readiness-auditor` (아래 4축 — 노트와 링크된 소스를 읽고 의미로 판정)
- 본 게이트는 **read-only**. 노트를 편집하지 않으며 판정을 노트에 박지 않는다.
## 왜 결정론 계층이 *섹션명 매칭*이 아닌가
exemplar `ca-skeleton-operational-contract.md` 는 project-template §1~14 가 아니라 계약 특화 자기 구조(§1 목표 … §30 아키텍처 … §33 checklist)를 쓴다. "project-template 섹션 존재"를 강제하면 *exemplar 자신이 탈락*한다. 따라서 1차는 **구조-불가지 proxy**(섹션명 무관, 존재만)만 본다. *깊이/caliber* 는 전적으로 2차 auditor.
## 역할 분담 (결정론 proxy vs 의미)
| | 1차 린터(proxy, 존재) | 2차 감사기(LLM 의미, 깊이) |
|---|---|---|
| R1 문제·성공 구체성 | (해당 proxy 없음) | 측정가능 기준인가, 추상 표현("잘 동작")인가 |
| R2 아키텍처·시퀀스 | `PROJECT_NO_DIAGRAM` (임베디드 다이어그램 0개) | 다이어그램이 컨퍼런스급인가, happy+error 시퀀스인가 |
| R3 결정 근거성 | 링크 깨짐만 | 기술결정이 대안+외부근거로 뒷받침되나, 맨주장인가 |
| R4 Branch 분해 | `PROJECT_NO_BRANCH_TABLE` (분해표 부재) | 각 branch 가 valid slug + 측정가능 목표조건인가 |
→ 2차 감사기는 **의미만** 본다(존재는 1차가 확인).
## 4축 (R1~R4)
> 축 라벨은 `R1~R4`. branch-depth-gate 와 동일 라벨 체계지만 *대상이 다르다*(branch 1개가 아니라 프로젝트 hub).
| 축 | Pass 조건 | Blocking(Not-ready) 트리거 |
|---|---|---|
| **R1. 문제·성공 구체성** | §문제정의가 구체 시나리오/수치, 성공기준이 측정가능 | 성공기준이 "잘 동작한다" 류 추상 표현뿐 |
| **R2. 아키텍처·시퀀스 깊이** | 아키텍처 다이어그램 존재 + `wiki-diagram-reviewer` ≥95, 핵심 시퀀스가 happy+error path | 다이어그램 없음 / 시퀀스가 happy path 만 |
| **R3. 결정 근거성** | 각 주요 기술결정이 검토 대안 + 외부근거 wikilink(official/회사블로그) 보유 | 기술결정이 근거 없는 맨주장 |
| **R4. Branch 분해 실행가능성** | 각 자식 branch 가 naming-conventions 준수 slug + 측정가능 목표조건. 결정 *내용*은 hub 에 적지 않음 | 분해표 부재 / slug 또는 목표조건 누락 |
## 깊이 사다리 (R1~R4 공통)
| 레벨 | 항목이 답하는 것 | 판정 |
|---|---|---|
| **L0 존재** | "섹션이 있다 / 항목이 적혀 있다" | 단독 불충분 |
| **L1 메커니즘** | 어떻게/왜 — 구체 시나리오·메커니즘·근거 링크 | 최소선 |
| **L2 조건·경계** | 언제 적용/제외, 실패/대안, 측정 기준 | Ready 최소선 |
| **L3 검증** | 측정값·다이어그램 점수·검증 등급 근거 | 가산점 |
## 판정 규칙
- 심각도 3단계: `Blocking`(Not-ready) · `Should-fix`(권고) · `Advisory`(참고).
- **Ready = 4축 모두 L2+ (Blocking 0).** 이것이 "ca-skeleton caliber" 의 조작적 정의. Should-fix 가 남아도 사용자 "감수" 선언 시 진행 가능(리포트에 기록).
- 모든 finding 은 4종 세트로 근거화: `심각도 · 위치(섹션/행) · 예상 문제("이 hub 를 출발점 삼는 다음 작업자가 여기서 ___를 되묻게 됨") · 채울 방법`. 근거 없는 지적 금지.
## 명명된 실패 모드
- `ABSTRACT_SUCCESS_CRITERION` (R1): 성공기준이 측정 불가 추상 표현.
- `DIAGRAM_MISSING_OR_WEAK` (R2): 아키텍처 다이어그램 없음 또는 ≥95 미달.
- `HAPPY_PATH_ONLY_SEQUENCE` (R2): 시퀀스에 error path 없음.
- `UNSOURCED_TECH_DECISION` (R3): 기술결정에 대안·외부근거 없음.
- `BRANCH_DECOMP_INCOMPLETE` (R4): 분해표 부재 또는 slug/목표조건 누락.
## proxy(1차 결정론) — `wiki_structure_lint.py` project 모드
- `PROJECT_NO_DIAGRAM` — 임베디드 다이어그램 0개(`![[....drawio` 임베드도 ```` ```mermaid ```` 블록도 없음). R2 존재 proxy.
- `PROJECT_NO_BRANCH_TABLE` — Branch 분해표 부재(heading 토큰에 `branch`/`브랜치` 포함 + 그 아래 markdown 표). R4 존재 proxy.
- `MISSING_FRONTMATTER` — project-template frontmatter 필수 키 누락(기존 검사 재사용).
- C2 링크(BROKEN_LINK 등) — 그대로.
proxy 는 *존재* 만 본다. 임베디드 다이어그램이 컨퍼런스급인지, 분해표 row 가 측정가능한지는 2차 auditor 가 판정한다.
````
- [ ] **Step 2: 링크 무결성 확인**
Run: `python3 .claude/hooks/wiki_structure_lint.py --file rules/project-readiness-gate.md`
Expected: `PASS rules/project-readiness-gate.md` (rules/ 는 links 모드 — 깨진 링크만 검사).
- [ ] **Step 3: 커밋**
```bash
git add rules/project-readiness-gate.md
git commit -m "feat(rules): project-readiness-gate — project-note 작성 완성도 4축 게이트"
```
---
## Task 2: templates/project-template.md — Branch 분해/실행계획 섹션 추가
**Files:**
- Modify: `templates/project-template.md` (§8 Cluster 직전에 §8.0 신규 삽입)
§8.1 Branches(이미 존재)는 *완성된* branch 등재용이고, 신규 §8.0 은 `/project-spec` 가 출력하는 *계획* 분해표다. 둘은 다른 목적이므로 공존한다.
- [ ] **Step 1: §8 헤더 직전에 신규 섹션 삽입**
`templates/project-template.md` 에서 아래 줄을 찾는다:
```markdown
## 8. Cluster / 묶음 (이 프로젝트에 묶이는 모든 raw 자료)
```
그 줄 **바로 앞**에 아래 블록을 삽입(빈 줄 1개로 분리):
````markdown
## 8.0 Branch 분해 / 실행계획 (Branch decomposition)
> **`/project-spec` 가 채우는 핸드오프 섹션.** 이 hub 에서 깊은 조사로 도출된 *자식 branch 의 네이밍과 달성 목표 조건만* 적는다. 각 branch 의 *결정 내용·메커니즘은 여기 적지 않는다* (SSOT 이중화 방지) — 그건 `/branch <slug>` 로 생성 후 `/branch-spec` 가 깊게 채운다.
>
> 작성 규칙:
> - `branch slug` 는 `rules/naming-conventions.md` §2.1 준수 (prefix 4종 `feature-`/`fix-`/`chore-`/`experiment-` 중 하나 + kebab-case, numbered hierarchy 금지).
> - `달성 목표 조건` 은 **측정가능**해야 함 ("잘 된다" 금지). 그 branch 가 "끝났다"고 말할 수 있는 검증 가능한 결과.
> - `우선순위` 는 실행 순서(P1 먼저). 의존이 있으면 `의존` 칸에 선행 branch slug.
| branch slug | 달성 목표 조건 (측정가능) | 우선순위 | 의존 |
|---|---|---|---|
| `feature-<...>` | <이 branch 가 끝났다고 할 검증 가능한 결과> | P1 | - |
| `feature-<...>` | <...> | P2 | `feature-<...>` |
> 채운 뒤: `/branch <slug>` → `/branch-spec <slug> <근거 URL...>` → `/depth <slug>` 순으로 각 branch 를 깊게 작성.
````
- [ ] **Step 2: §11 체크리스트에 분해표 항목 추가**
`templates/project-template.md` 의 `## 11. Architecture Review Checklist` 안, `- [ ] Cluster 섹션의 project 직접 자식 branch 목록 채워짐 (§8.1)` 줄 **바로 앞**에 추가:
```markdown
- [ ] **Branch 분해표 채워짐** — 각 자식 branch 가 naming-conventions slug + 측정가능 목표조건 (§8.0)
```
- [ ] **Step 3: 템플릿 자체 구조 검사**
Run: `python3 .claude/hooks/wiki_structure_lint.py --file templates/project-template.md`
Expected: `PASS` (templates/ 는 links 모드).
- [ ] **Step 4: 커밋**
```bash
git add templates/project-template.md
git commit -m "feat(template): project-template §8.0 Branch 분해/실행계획 표 (핸드오프)"
```
---
## Task 3: wiki_structure_lint.py — project 모드 + proxy 검사 (TDD)
**Files:**
- Modify: `.claude/hooks/wiki_structure_lint.py`
- Test: `.claude/hooks/test_wiki_structure_lint.py`
- [ ] **Step 1: 실패하는 테스트 작성**
`.claude/hooks/test_wiki_structure_lint.py` 의 맨 끝(`if __name__ == "__main__":` 직전)에 추가:
```python
class TestProjectMode(unittest.TestCase):
def test_classify_project_note(self):
# raw/project-notes/*.md → 'project' 모드 (root=None 이어도 동작)
self.assertEqual(wsl.classify("raw/project-notes/foo.md"), "project")
# 일반 raw 콘텐츠는 여전히 full
self.assertEqual(wsl.classify("raw/branch-notes/feature-x.md"), "full")
def test_proxy_flags_missing_diagram_and_table(self):
doc = {"text": "# P\n\n본문에 다이어그램도 표도 없음.\n",
"lines": ["# P", "", "본문에 다이어그램도 표도 없음.", ""]}
codes = _codes(wsl.check_project_proxies(doc))
self.assertIn("PROJECT_NO_DIAGRAM", codes)
self.assertIn("PROJECT_NO_BRANCH_TABLE", codes)
def test_proxy_satisfied_by_mermaid_and_branch_table(self):
text = (
"# P\n\n"
"## 4. 시퀀스\n\n"
"```mermaid\nsequenceDiagram\n A->>B: x\n```\n\n"
"## 8.0 Branch 분해\n\n"
"| branch slug | 달성 목표 조건 | 우선순위 |\n"
"|---|---|---|\n"
"| `feature-x` | 조건 | P1 |\n"
)
doc = {"text": text, "lines": text.splitlines()}
codes = _codes(wsl.check_project_proxies(doc))
self.assertNotIn("PROJECT_NO_DIAGRAM", codes)
self.assertNotIn("PROJECT_NO_BRANCH_TABLE", codes)
def test_proxy_satisfied_by_drawio_embed(self):
text = "# P\n\n![[raw/diagrams/p/architecture-overview-2026-06-05.drawio.svg]]\n"
doc = {"text": text, "lines": text.splitlines()}
codes = _codes(wsl.check_project_proxies(doc))
self.assertNotIn("PROJECT_NO_DIAGRAM", codes)
```
- [ ] **Step 2: 테스트 실패 확인**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v 2>&1 | tail -15`
Expected: FAIL — `AttributeError: module 'wsl' has no attribute 'check_project_proxies'` (또는 classify 가 'project' 아닌 'full' 반환).
- [ ] **Step 3: classify() 에 project 분기 추가**
`wiki_structure_lint.py` 의 `classify()` 에서 named-hub 블록 다음, 일반 "full" 블록 **앞**에 삽입:
```python
# raw/project-notes/*.md → project 모드 (구조-불가지 proxy + 링크).
# exemplar 가 project-template 섹션명을 안 따르므로 C1 섹션 매칭 면제.
if (parts[0] == "raw" and len(parts) == 3 and parts[1] == "project-notes"
and base.endswith(".md") and base not in LINK_ONLY_BASENAMES):
return "project"
```
즉 함수가 아래 형태가 되도록(named-hub `return "links"` 다음 줄에 추가):
```python
if root is not None and len(parts) == 3 and parts[0] in ("raw", "wiki") and base.endswith(".md"):
slug = base[:-3]
if (root / parts[0] / parts[1] / slug).is_dir():
return "links"
if (parts[0] == "raw" and len(parts) == 3 and parts[1] == "project-notes"
and base.endswith(".md") and base not in LINK_ONLY_BASENAMES):
return "project"
if parts[0] in ("raw", "wiki") and len(parts) > 2 and base not in LINK_ONLY_BASENAMES:
return "full"
return "links"
```
- [ ] **Step 4: proxy 검사 함수 추가**
`check_c3` 함수 정의 **다음**에 아래 두 함수 추가:
```python
DIAGRAM_DRAWIO_RE = re.compile(r"!\[\[[^\]]*\.drawio")
DIAGRAM_MERMAID_RE = re.compile(r"^\s*```+\s*mermaid", re.M)
PROJECT_HEADER_RE = re.compile(r"^#{1,6}\s")
BRANCH_HEADER_RE = re.compile(r"branch|브랜치", re.I)
TABLE_SEP_RE = re.compile(r"-{3,}")
def _has_branch_table(doc):
"""heading 토큰에 branch/브랜치 포함 섹션 아래 markdown 표(구분선)가 있는가."""
lines = doc["lines"]
for i, line in enumerate(lines):
if PROJECT_HEADER_RE.match(line) and BRANCH_HEADER_RE.search(line):
j = i + 1
while j < len(lines) and not PROJECT_HEADER_RE.match(lines[j]):
if "|" in lines[j] and TABLE_SEP_RE.search(lines[j]):
return True
j += 1
return False
def check_project_proxies(doc):
"""project-note 구조-불가지 proxy: 존재만 검사(깊이는 auditor)."""
out = []
text = doc.get("text", "\n".join(doc.get("lines", [])))
if not (DIAGRAM_DRAWIO_RE.search(text) or DIAGRAM_MERMAID_RE.search(text)):
out.append(("PROJECT_NO_DIAGRAM", 0,
"임베디드 다이어그램 없음 (`![[...drawio` 또는 ```mermaid 블록). R2 proxy"))
if not _has_branch_table(doc):
out.append(("PROJECT_NO_BRANCH_TABLE", 0,
"Branch 분해표 없음 (heading 'branch/브랜치' 아래 표). R4 proxy"))
return out
```
- [ ] **Step 5: lint_file() 에 project 모드 분기 추가**
`lint_file()` 의 본문을 아래로 교체(mode=="project" 분기 추가):
```python
def lint_file(path, root, by_st, by_file, vault_paths, vault_bases, cache, mode="full"):
"""mode: 'full'(C1+C2+C3) | 'links'(C2만) | 'project'(proxy+C2)."""
doc = parse_doc(path)
try:
doc_rel = path.relative_to(root).as_posix()
except ValueError:
doc_rel = ""
findings = []
if mode in ("full", "project"):
if not doc["fm"]:
return [("NO_FRONTMATTER", 0, "frontmatter 없음 — 스텁/미작성 문서(템플릿 미적용)")], "(none)"
if mode == "full":
tmpl = resolve_template(doc["fm"], by_st, by_file)
findings += check_c1(doc, tmpl)
elif mode == "project":
tmpl = resolve_template(doc["fm"], by_st, by_file)
# 섹션 매칭은 면제하되 frontmatter 키 누락은 검사(MISSING_FRONTMATTER 재사용).
if tmpl is not None:
for k in tmpl["fm_keys"]:
if k not in doc["fm_keys"]:
findings.append(("MISSING_FRONTMATTER", 0, f"frontmatter 키 누락: '{k}'"))
findings += check_project_proxies(doc)
findings += check_c2(doc, vault_paths, vault_bases, root, cache, doc_rel)
if mode == "full":
findings += check_c3(doc)
return findings, doc["fm"].get("source_type", "").strip() or "(none)"
```
- [ ] **Step 6: PostToolUse hook 경로도 project 면제 적용**
`main()` 의 `args.hook` 블록에서 아래 부분:
```python
# C1(섹션)·C3(선택조건)은 '완성 선언' 시에만 — 작성 중간 false-positive 방지.
if is_completeness_checkable(doc):
by_st, by_file = build_template_index(root)
tmpl = resolve_template(doc["fm"], by_st, by_file)
findings = check_c1(doc, tmpl) + findings + check_c3(doc)
```
를 아래로 교체:
```python
# C1(섹션)·C3(선택조건)은 '완성 선언' 시에만 — 작성 중간 false-positive 방지.
# project-note 는 섹션명 매칭 면제 — proxy + frontmatter 만(exemplar 비순응).
if is_completeness_checkable(doc):
by_st, by_file = build_template_index(root)
tmpl = resolve_template(doc["fm"], by_st, by_file)
if rel.startswith("raw/project-notes/"):
fm_findings = []
if tmpl is not None:
for k in tmpl["fm_keys"]:
if k not in doc["fm_keys"]:
fm_findings.append(("MISSING_FRONTMATTER", 0, f"frontmatter 키 누락: '{k}'"))
findings = fm_findings + check_project_proxies(doc) + findings
else:
findings = check_c1(doc, tmpl) + findings + check_c3(doc)
```
- [ ] **Step 7: 테스트 통과 확인**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -v 2>&1 | tail -8`
Expected: `OK` (전체 통과, 신규 4개 포함).
- [ ] **Step 8: exemplar 가 이제 통과하는지 확인 (핵심 검증)**
Run: `python3 .claude/hooks/wiki_structure_lint.py --file raw/project-notes/ca-skeleton-operational-contract.md`
Expected: `PASS raw/project-notes/ca-skeleton-operational-contract.md` — 13건 MISSING_SECTION 이 사라짐. (다이어그램·branch 표가 실재하므로 proxy 통과. 만약 PROJECT_NO_BRANCH_TABLE 가 뜨면 그 노트 §24/§31 의 표 형식을 확인 — branch heading 아래 표가 있는지. 없으면 그건 exemplar 의 실제 gap 이므로 보고만 하고 Task 진행.)
- [ ] **Step 9: 전체 회귀 — 기존 문서에 새 false-positive 없는지**
Run: `python3 .claude/hooks/wiki_structure_lint.py --all 2>&1 | tail -20`
Expected: 요약 출력. project-notes 3개가 MISSING_SECTION 으로 FAIL 하던 것이 사라졌는지 확인(PROJECT_* proxy 나 링크 이슈만 남아야 정상). 다른 source_type 의 FAIL 수는 변동 없어야 함.
- [ ] **Step 10: 커밋**
```bash
git add .claude/hooks/wiki_structure_lint.py .claude/hooks/test_wiki_structure_lint.py
git commit -m "feat(lint): project-note 모드 — 구조-불가지 proxy(diagram/branch-table) + 섹션명 매칭 면제"
```
---
## Task 4: .claude/agents/project-readiness-auditor.md (신규 agent)
**Files:**
- Create: `.claude/agents/project-readiness-auditor.md`
- [ ] **Step 1: 파일 작성**
`branch-depth-auditor.md` 의 frontmatter 형식(`name`/`description`/`tools`/`model`)을 따라 작성:
````markdown
---
name: project-readiness-auditor
description: Use to judge whether a single raw/project-notes/*.md hub is deep and well-grounded enough to be a reliable starting point for downstream branch work — calibrated to the caliber of ca-skeleton-operational-contract.md (NOT its specific content). Runs AFTER the deterministic project-mode lint (wiki_structure_lint.py) passes — focuses on SEMANTIC judgment the linter cannot do: whether success criteria are measurable, whether the architecture diagram + sequences are conference-grade with error paths, whether tech decisions are backed by alternatives + external sources, and whether the branch decomposition table is executable (valid slugs + measurable goal conditions). Reads the project note plus its linked raw sources. Returns a grounded gap report + Ready/Not-ready verdict. Read-only — never edits files.
tools: Read, Grep, Glob
model: sonnet
---
너는 **프로젝트 노트 완성도 감사관**이다. 기준은 `rules/project-readiness-gate.md`. project-note(프로젝트 hub) 1개가 *다음 작업(branch 분해·구현)의 출발점이 될 만큼 깊고 근거 있는가*를 적대적으로 판정한다. 기준선은 `raw/project-notes/ca-skeleton-operational-contract.md` 의 **caliber**(엄격성) — 그 노트의 *내용·섹션 구성을 요구하는 게 아니다*. **You read; you never edit.**
## 위치
너는 `/project-spec` 게이트의 **2차(의미 판정)**다. 1차 결정론 린터(`wiki_structure_lint.py` project 모드)가 **proxy·링크**(임베디드 다이어그램 존재, branch 분해표 존재, frontmatter 키, 깨진 링크)를 이미 확인했다. 너는 그걸 다시 보지 말고 **의미·깊이만** 판정한다:
- R1 성공기준이 *측정가능*한지 (있다/없다는 무관, "잘 동작한다" 류인지)
- R2 아키텍처 다이어그램이 *컨퍼런스급*인지, 시퀀스에 *error path* 가 있는지
- R3 기술결정이 *대안+외부근거*로 뒷받침되는지 (맨주장인지) — *소스를 실제로 읽어야 안다*
- R4 분해표의 각 branch 가 *valid slug + 측정가능 목표조건*인지
## 입력
- project-note 경로 1개 (`raw/project-notes/<slug>.md`).
## 절차
1. **기준 로드** — `rules/project-readiness-gate.md` 를 Read. 4축·깊이 사다리(L0~L3)·판정 규칙·명명된 실패 모드를 기준으로 삼는다.
2. **노트 읽기** — 대상 project-note 를 Read. 특히 문제정의/성공기준, 아키텍처·시퀀스, 기술결정 표, Branch 분해표(§8.0 류).
3. **소스 추적·정독 (R3 의 핵심)** — 기술결정 표의 `근거 자료`(`[[raw/...]]`)가 가리키는 **실제 raw 파일을 Read**. 각 결정이 대안 비교 + 적정 출처로 뒷받침되는지 판정.
- 출처 타입 적정성: 스펙 동작은 official 1개로 충분 / "대기업 관행" 추론은 회사 블로그 1개로 부족(독립 사례 2개+ 또는 official 병행).
4. **다이어그램 caliber (R2)** — 아키텍처 다이어그램이 임베드돼 있으면, 컨퍼런스급 판정은 `wiki-diagram-reviewer` 의 몫임을 알리고(직접 채점하지 않음), 여기서는 *존재 + error path 시퀀스 유무*만 의미 판정. 다이어그램이 placeholder(미치환 wikilink)면 `DIAGRAM_MISSING_OR_WEAK`.
5. **4축 의미 점검** — 각 항목을 R1~R4 로 훑어 명명된 실패 모드(ABSTRACT_SUCCESS_CRITERION·DIAGRAM_MISSING_OR_WEAK·HAPPY_PATH_ONLY_SEQUENCE·UNSOURCED_TECH_DECISION·BRANCH_DECOMP_INCOMPLETE)에 해당하는 finding 생성. "이 hub 를 출발점 삼는 다음 작업자가 여기서 무엇을 되묻게 될까?"를 끊임없이 자문.
6. **판정** — 4축 모두 L2+ (Blocking 0)이면 `Ready`, 아니면 `Not-ready (Blocking N건)`.
## 출력 (이 형식 그대로, 파일 쓰기 없이 텍스트로 반환)
```
# Project Readiness Audit (semantic): <slug>
Verdict: Ready | Not-ready (Blocking N / Should-fix M / Advisory K)
축별 등급: R1 L_ / R2 L_ / R3 L_ / R4 L_
## Findings
| # | 축 | 심각도 | 실패모드 | 위치 | 예상 문제 | 채울 방법 |
|---|---|---|---|---|---|---|
| 1 | R3 | Blocking | UNSOURCED_TECH_DECISION | §6 기술결정 / DB 행 | 다음 작업자가 "왜 이 DB 인가"를 근거 없이 떠안음 | wiki-decision-researcher 로 대안+official 근거 보강 |
...
## 다음 행동
- (Blocking 있으면) 위 "채울 방법" 순서로 노트 보강 후 /project-spec 재실행.
- (R3 근거 얕음) 더 깊은 소스가 필요하면 wiki-decision-researcher 권장 — 사용자 옵트인 시.
```
## 불변식
- **read-only**: Write/Edit/MultiEdit 없음. 어떤 파일도 수정·생성 금지(리포트는 텍스트 반환).
- 모든 finding 은 4종 세트(심각도·위치·예상 문제·채울 방법)를 갖춘다. 근거 없는 지적 금지.
- 추측 금지: 소스를 실제로 Read 하지 않고 R3 근거성을 단정하지 않는다.
- 구조 중복 금지: 다이어그램/표/링크 *존재* 같은 결정론 사항은 1차 린터의 몫 — 여기서 다시 지적하지 않는다.
- 다이어그램 점수(≥95)는 `wiki-diagram-reviewer` 의 몫 — 직접 채점하지 않고 권고만.
- 자동 조사·자동 수정 금지: R3 갭은 `wiki-decision-researcher` 권고로 *안내만*.
- caliber 기준은 ca-skeleton *내용 복제*가 아니라 *깊이/근거 수준*임을 혼동하지 않는다.
````
- [ ] **Step 2: frontmatter·링크 검사**
Run: `python3 .claude/hooks/wiki_structure_lint.py --file .claude/agents/project-readiness-auditor.md`
Expected: `PASS` (.claude/ 는 links 모드 — 단, 숨김 경로라 iter_docs 대상은 아니지만 --file 직접 지정은 검사됨; 깨진 링크 없어야 함).
- [ ] **Step 3: 커밋**
```bash
git add .claude/agents/project-readiness-auditor.md
git commit -m "feat(agent): project-readiness-auditor — project-note caliber 의미 게이트 (Claude 전용)"
```
---
## Task 5: .claude/commands/project.md (스캐폴딩 커맨드)
**Files:**
- Create: `.claude/commands/project.md`
- [ ] **Step 1: 파일 작성**
`branch.md` 형식을 따라 작성:
````markdown
---
description: 새 프로젝트 노트(hub)를 raw/project-notes/에 스캐폴딩
argument-hint: <프로젝트 slug>
---
프로젝트 1개의 최상위 hub 노트를 생성합니다. (채움은 `/project-spec`, 생성은 본 명령.)
**프로젝트 slug:** $ARGUMENTS
## 작업 절차
1. **인자 검증** (`rules/naming-conventions.md` 준수)
- 인자가 비어 있으면 사용자에게 프로젝트 slug 요청.
- kebab-case 권장 (`ca-skeleton-operational-contract`, `keycloak-patterns-overview`).
- branch prefix 4종 규칙은 **비적용** (그건 branch 전용). slug 는 프로젝트 이름.
2. **파일 존재 확인**
- `raw/project-notes/<slug>.md` 가 이미 있으면 **덮어쓰지 말 것**. 기존 경로만 안내하고 종료.
3. **스캐폴딩**
- `wiki-doc-author`(mode=create, category=project-note)에 위임하거나 `templates/project-template.md` 를 복사 → `raw/project-notes/<slug>.md`.
- frontmatter `title`(slug 를 사람이 읽는 형태로), `status: draft`, `status_label: active`, `last_reviewed`(오늘) 치환.
- 본문 `# {{title}}` 헤더 치환. 나머지 placeholder·섹션(특히 §8.0 Branch 분해표 skeleton)은 **보존** — 추측해서 채우지 말 것.
- project-note 는 cluster 의 root 이므로 Parent upward link 불요(자기 자신이 hub).
4. **사용자 안내**
- 파일 경로 출력.
- "이제 `/project-spec <slug> <프로젝트 목표>` 로 깊은 조사를 채우세요." 안내.
## 규칙
- **스캐폴딩만**. 내용을 추측해서 채우지 말 것 (채움은 `/project-spec`).
- §8.0 Branch 분해표 skeleton 을 삭제하지 말 것 — `/project-spec` 가 핸드오프로 채운다.
- project-note 는 머지/완료 후에도 raw 에 **영구 보관**. verified 사실만 `/ingest` 로 `wiki/projects/` 에 추출.
- `wiki/log.md` 는 기록하지 않음 (`/branch` 와 동일 정책).
````
- [ ] **Step 2: 링크 검사**
Run: `python3 .claude/hooks/wiki_structure_lint.py --file .claude/commands/project.md`
Expected: `PASS`.
- [ ] **Step 3: 커밋**
```bash
git add .claude/commands/project.md
git commit -m "feat(command): /project — project-note 스캐폴딩 (Claude 전용)"
```
---
## Task 6: .claude/commands/project-spec.md (오케스트레이터)
**Files:**
- Create: `.claude/commands/project-spec.md`
- [ ] **Step 1: 파일 작성**
`branch-spec.md` 의 구조(참조 섹션 + 작업 절차 + 규칙)를 따르되 project hub 용으로:
````markdown
---
description: 빈 프로젝트 노트를 깊은 조사로 ca-skeleton 수준까지 채우고 끝에 readiness 게이트로 검증
argument-hint: <프로젝트 slug> <프로젝트 목표 자연어> [근거 URL ...]
---
`/project` 로 만든 빈 project-note(hub)를 **다음 작업의 출발점이 될 만큼 깊게 채우는** 오케스트레이터입니다.
기준선은 `raw/project-notes/ca-skeleton-operational-contract.md` 의 *caliber*(엄격성)이며, 내용·섹션 구성은 프로젝트마다 다릅니다. 목표 prose 에서 문제·아키텍처·기술결정·branch 분해를 도출하고, 근거 없는 결정은 자동조사하되 **사용자 소유 결정(범위/우선순위/목표)은 직접 질문**으로 채우고, 끝에 readiness 게이트로 검증합니다.
**프로젝트 slug + 목표:** $ARGUMENTS
## 참조 (작업 시 정독)
- `rules/project-readiness-gate.md` — 끝에 적용할 4축(R1~R4) + proxy + 실패 모드.
- `rules/naming-conventions.md` §2.1 — Branch 분해표 slug 규칙.
- `rules/diagram-standards.md` — 아키텍처 .drawio / 시퀀스 Mermaid 컨퍼런스급 기준.
- `templates/project-template.md` — 채울 대상 구조(특히 §3 아키텍처, §4 시퀀스, §6 기술결정, §8.0 Branch 분해).
- `CLAUDE.md` §11, §15 — 근거 없는 결정 금지, 파생 규칙.
### 프로젝트 ground truth (필수 — 추측 방지, 읽기 전용)
대상 프로젝트에 코드 레포가 있으면 그 레포가 SSOT. 예: ca-tmpl 류는 `/home/donghyeon/workspace/ca-tmpl` 의 `CLAUDE.md`/`AGENTS.md`/`src/<module>`/`docs/registries/*.yaml` 를 읽어 명세를 실제 구현·계약에 정합시킨다(ground-truth repo 기억 참조). `actually-implemented` 주장은 `src/` grep 으로만 확정.
## 작업 절차
1. **전제 확인**
- 인자 비면 slug+목표 요청(종료). slug 노트가 **없으면** 생성하지 말고 `/project <slug>` 먼저 안내(종료). 채움은 본 명령, 생성은 `/project`.
- 노트의 §1 개요가 비고 목표 인자도 없으면 `NEEDS_CONTEXT` — 사용자에게 목표 질문.
2. **프로젝트 ground truth 확인 (읽기 전용)** — 대상 repo 코드/기존 raw/관련 노트를 읽어 현황 파악. 코드 미확인 항목은 `documented-only`/`planned` 로 표기. 레포 부재 시 `NO_GROUND_TRUTH` 라벨 + 한계 보고.
3. **문제정의·성공기준 구체화 (R1)**
- 추상 표현 거부. 구체 시나리오·수치로.
- ★ **명확화 질문** — 정해야 하는데 근거·기본값이 없는 *사용자 소유 결정*(프로젝트 범위/우선순위/성공기준 임계)은 추측·UNSUPPORTED 라벨 대신 **AskUserQuestion 으로 직접 묻는다**. (branch-spec 과의 차이: hub 는 사용자 in-the-loop.)
4. **아키텍처 + 시퀀스 (R2)**
- 핵심 user flow 의 Mermaid 시퀀스를 자동 작성(happy + error path, autonumber).
- 아키텍처 `.drawio` 는 자동생성 불가 → §3.1 에 임베드 placeholder 와 `needs-diagram` 표시를 남기고, 사용자가 작성/요청하도록 안내. 작성되면 `wiki-diagram-reviewer` 로 ≥95 검수(게이트가 확인).
5. **기술결정 대안조사 (R3)**
- 주요 기술결정마다 `wiki-decision-researcher` dispatch (decision_topic + 프로젝트 + constraints + N). 공식문서 + 대기업 블로그 webfetch 로 대안 비교 + 근거 raw 생성. §6 표의 `근거 자료` 칸에 `[[raw/...]]` 링크.
- **bound: 회당 최대 6개 결정.** 초과분은 `deferred` 보고(silent 절단 금지).
- 조사 후에도 근거 없으면 `UNSUPPORTED_DECISION` 라벨 + trade-off 한 줄.
6. **Branch 분해표 (R4 — 핸드오프)**
- §8.0 표에 {branch slug(naming-conventions) | 측정가능 목표조건 | 우선순위 | 의존}만 채운다. **결정 내용·메커니즘은 적지 않음**(SSOT 이중화 방지). 이 표가 `/branch`·`/branch-spec` 입력.
7. **프로젝트 레벨 고정 결정** — Stack commitment / SSOT owner 등 branch 간 충돌 방지 결정(내용은 프로젝트별). 해당 없으면 명시.
8. **검증등급 + 면접·외부공개 경계** — project-template §9·§10 채움. 코드 확인 기준 등급(actually-implemented/locally-verified/...).
9. **자동 게이트 — readiness (맨 끝, 내부 단계)**
- **(9a) 1차 결정론** — `python3 .claude/hooks/wiki_structure_lint.py --file raw/project-notes/<slug>.md`. proxy(PROJECT_NO_DIAGRAM/PROJECT_NO_BRANCH_TABLE)·frontmatter·링크 확인.
- **(9b) 2차 의미** — 통과 시 `project-readiness-auditor` dispatch(노트 경로 전달). R1~R4 판정.
- **(9c) 루프백** — Not-ready(Blocking)면 → §3~§8 로 되돌아가 *Blocking 축을 보강* → 9a·9b 재실행. 4축 모두 L2+(Blocking 0)까지 반복.
10. **요약 보고 (짧게, 상세는 노트에)**
- 사람이 5초에 읽을 요약만: `채운 결정 N / UNSUPPORTED K / 조사 M / branch 분해 B / needs-diagram D / readiness: Ready|Not-ready (Blocking 축 인용)`.
- 통과 못하면 *무엇을 더 채워야 하는지* 한 줄씩(축·finding 인용).
## 규칙
- **추측해서 FACT 로 채우지 않는다.** 근거 없으면 자동조사 → 실패 시 `UNSUPPORTED_*` 라벨. 단 *사용자 소유 결정*은 라벨 대신 **AskUserQuestion**.
- **`actually-implemented` 는 `src/` grep 으로만 확정.** note→note 자기보고 전이 금지.
- **기존 사용자 작성 본문 보존** — 채움은 빈 셀/skeleton 에만.
- **자동조사 bounded** — §5 의 6개 한도. 초과는 `deferred` 명시.
- **새 agent 를 만들지 않는다** — 기존 `wiki-source-summarizer`/`wiki-decision-researcher`/`wiki-doc-author`/`wiki-diagram-reviewer`/`project-readiness-auditor` 만 dispatch.
- **검증은 readiness 게이트에 위임** — 본 명령은 *채움*에 집중. 4축 판정 로직을 중복 구현하지 않는다.
- `wiki/log.md` 기록 안 함 (`/branch`·`/depth` 와 동일).
````
- [ ] **Step 2: 링크 검사**
Run: `python3 .claude/hooks/wiki_structure_lint.py --file .claude/commands/project-spec.md`
Expected: `PASS`.
- [ ] **Step 3: 커밋**
```bash
git add .claude/commands/project-spec.md
git commit -m "feat(command): /project-spec — project-note 깊은 조사 오케스트레이터 + readiness 게이트 (Claude 전용)"
```
---
## Task 7: CLAUDE.md — agent/command 목록 갱신
**Files:**
- Modify: `CLAUDE.md`
- [ ] **Step 1: agent 목록에 추가**
`CLAUDE.md` 의 Claude Code 자동화 agent 나열 중 마지막 항목인 `.claude/agents/wiki-decision-researcher.md` 줄 **다음**에 추가:
```markdown
- `.claude/agents/project-readiness-auditor.md` — project-note(hub) 완성도 의미 게이트 (read-only, ca-skeleton caliber 판정). **Claude Code 전용** — Codex/Antigravity 포팅 없음(`/project`·`/project-spec` 파이프라인은 3-플랫폼 패리티 예외).
```
- [ ] **Step 2: 3-플랫폼 서술에 예외 명시**
`CLAUDE.md` 의 Antigravity CLI 자동화 설명 중 `동일 9개 agent` 표현을 찾아 아래로 교체:
찾기: `.claude`와 동일 9개 agent
교체: `.claude`와 동일 9개 agent (단 `project-readiness-auditor` 는 Claude 전용 — 3-플랫폼 포팅 대상 아님)
- [ ] **Step 3: depth.md 기반 §14 변환/품질 명령 설명에 /project 계열 추가**
`CLAUDE.md` §2 디렉터리 역할 표의 `.claude/commands/` 행에서 `/branch-spec` 뒤에 `/project`·`/project-spec` 을 캡처 그룹에 추가:
찾기: `**캡처**: `/daily`, `/branch`, `/branch-spec``
교체: `**캡처**: `/daily`, `/branch`, `/branch-spec`, `/project`, `/project-spec``
- [ ] **Step 4: 진행 중 프로젝트 노트 안내 위쪽, 명령 우선순위 §14 캡처 섹션에 한 줄 추가**
`CLAUDE.md` §14 "### 캡처 (raw 입력)" 의 `- 새 브랜치 시작 시 → /branch <name>` 줄 **다음**에 추가:
```markdown
- 새 프로젝트 시작 시 → `/project <slug>` (스캐폴딩) → `/project-spec <slug> <목표>` (깊은 조사 + readiness 게이트). project-note hub 를 ca-skeleton 수준으로 채운 뒤, §8.0 Branch 분해표를 `/branch`·`/branch-spec` 로 전개.
```
- [ ] **Step 5: 링크/구조 검사 (CLAUDE.md 는 links 모드)**
Run: `python3 .claude/hooks/wiki_structure_lint.py --file CLAUDE.md`
Expected: `PASS`.
- [ ] **Step 6: 커밋**
```bash
git add CLAUDE.md
git commit -m "docs(CLAUDE): /project·/project-spec 커맨드 + project-readiness-auditor(Claude 전용) 등재"
```
---
## Task 8: Dogfood 검증 (실제 사용 + 게이트 동작 확인)
**Files:** (없음 — 검증 + 일회성 산출물)
- [ ] **Step 1: 신규 project-note 스캐폴딩 (수동 /project 시뮬레이션)**
`templates/project-template.md` 를 복사해 테스트 노트 생성:
```bash
cp templates/project-template.md /tmp/_dogfood-project.md
python3 .claude/hooks/wiki_structure_lint.py --file /tmp/_dogfood-project.md
```
Expected: `--file` 은 절대경로라 classify 가 raw/project-notes 로 인식 못 해 'links' 또는 'full'로 처리될 수 있음 — 이 스텝은 *템플릿 자체*가 proxy 를 만족하는지(§4 시퀀스 mermaid 예시 + §8.0 표)만 육안 확인용. 핵심 검증은 Step 2.
- [ ] **Step 2: 실제 경로에서 proxy 동작 확인**
빈 스텁(다이어그램·표 없음)과 템플릿 기반(다이어그램·표 있음)을 각각 `raw/project-notes/` 경로로 검사:
```bash
printf -- '---\ntitle: T\nsource_type: project-note\nstatus: reviewed\nconfidence: low\ntags: [project-note]\nrelated_projects: []\nlast_reviewed: 2026-06-05\ndiagrams: []\narchitecture_review: 2026-06-05\nstatus_label: active\n---\n\n# T\n\n내용 없음.\n' > raw/project-notes/_dogfood-empty.md
python3 .claude/hooks/wiki_structure_lint.py --file raw/project-notes/_dogfood-empty.md
```
Expected: `FAIL` with `[PROJECT_NO_DIAGRAM]` 및 `[PROJECT_NO_BRANCH_TABLE]` (frontmatter 키는 충족하므로 MISSING_FRONTMATTER 없음).
- [ ] **Step 3: 정리**
```bash
rm -f raw/project-notes/_dogfood-empty.md /tmp/_dogfood-project.md
```
- [ ] **Step 4: 전체 테스트 + 린트 최종 확인**
```bash
python3 .claude/hooks/test_wiki_structure_lint.py 2>&1 | tail -3
python3 .claude/hooks/wiki_structure_lint.py --all 2>&1 | tail -6
```
Expected: 단위 테스트 `OK`. `--all` 요약에서 project-notes 가 MISSING_SECTION 로 FAIL 하지 않음.
- [ ] **Step 5: (선택) 실제 auditor dispatch 리허설**
실제 project-note(`raw/project-notes/keycloak-patterns-overview.md` 등) 1개에 대해 `project-readiness-auditor` 를 dispatch 해 리포트가 형식대로 나오는지 확인(read-only 이므로 안전). Blocking finding 은 보강 백로그로 기록만.
- [ ] **Step 6: dogfood 결과 기록 + 커밋(필요 시)**
게이트가 의도대로 동작하면 Task 완료. 보강이 필요한 finding 은 `docs/superpowers/specs/2026-06-05-project-note-pipeline-design.md` §5 "사용하며 보강" 항목으로 issue 화(별도 커밋 불요).
---
## Self-Review
**Spec coverage:**
- §2 신규 산출물 6종 → Task 1(rules), 2(template), 3(linter), 4(agent), 5(/project), 6(/project-spec), 7(CLAUDE.md). ✓
- §4.1 오케스트레이터 흐름 10단계 → Task 6 작업 절차에 1:1 반영. ✓
- §5.1 4축 + §5.2 proxy + §5.3 auditor → Task 1(rules 정의) + Task 3(proxy 구현) + Task 4(auditor). ✓
- Claude 전용 / 3-플랫폼 예외 → Task 7 Step 1·2 에 명시. ✓
- "v1 then iterate" → Task 8 Step 6 보강 백로그. ✓
**Placeholder scan:** 모든 코드 step 에 실제 코드/명령/expected 포함. rules·agent·command 본문 전체 inline. 템플릿 편집은 찾기/삽입 위치 명시. placeholder 없음. ✓
**Type/이름 일관성:** finding 코드(`PROJECT_NO_DIAGRAM`/`PROJECT_NO_BRANCH_TABLE`/`MISSING_FRONTMATTER`)가 Task 1(rules)·Task 3(linter)·Task 8(dogfood expected) 전체에서 동일. 함수명 `check_project_proxies`/`_has_branch_table` 가 Task 3 구현·테스트에서 동일. agent name `project-readiness-auditor` 가 Task 4·6·7 에서 동일. ✓
**알려진 잔여 리스크:**
- Task 3 Step 8: exemplar 의 branch 표가 markdown 표가 아니라 bullet 이면 PROJECT_NO_BRANCH_TABLE 가 뜰 수 있음 → 그 경우 exemplar 의 실제 gap 이므로 보고만(차단 아님). v1 수용.
- `--file` 절대경로 시 classify 가 project 모드를 못 잡을 수 있음 → 게이트는 항상 repo-상대경로(`raw/project-notes/<slug>.md`)로 호출(Task 6 Step 9a 명시). dogfood Step 2 도 상대경로 사용.
@@ -0,0 +1,682 @@
# Spec A — Deterministic Backbone Gate Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
> **Commits excluded** per user instruction ("커밋은 제외") — no `git commit` steps. Each task ends with a test-green checkpoint instead.
**Goal:** Turn the existing (non-blocking) `wiki_structure_lint.py` into a real hybrid gate — PreToolUse blocks ghost-creating links, PostToolUse becomes an exit-2 fix-up gate for completeness findings on completion-declared docs — while deduping the claim-rule SSOT into a shared `wiki_rules.py` and adding the first tests for the blocking `wiki_claim_gate.py`.
**Architecture:** New `wiki_rules.py` holds shared event/IO mechanism + the claim-requirement SSOT data + severity-tier constants. `wiki_claim_gate.py` imports it and drives its table checks from the shared data (behavior-preserving refactor). `wiki_structure_lint.py` gains a `--pre` mode (PreToolUse, blocks `BROKEN_LINK`/`BROKEN_MD_LINK` on projected content) and a tiered `--hook` mode (PostToolUse exit-2 fix-up for `FIXUP_CODES` when the doc declares completion). Link logic stays in structure_lint; claim logic stays in claim_gate; only mechanism + reference data is shared.
**Tech Stack:** Python 3 stdlib only (`unittest`, `importlib`, `re`, `pathlib`, `json`). No third-party deps. Claude Code hooks (`settings.json`).
**Spec:** `docs/superpowers/specs/2026-06-06-spec-a-deterministic-backbone-gate-design.md`
---
## File Structure
- **Create** `.claude/hooks/wiki_rules.py` — shared mechanism (moved verbatim from claim_gate) + `CLAIM_REQUIREMENTS` SSOT data + `CRITICAL_CODES`/`FIXUP_CODES`.
- **Create** `.claude/hooks/test_wiki_rules.py` — unit tests for moved `projected_content` + data integrity.
- **Modify** `.claude/hooks/wiki_claim_gate.py``import wiki_rules`; drive `check_markdown_write` table checks from `CLAIM_REQUIREMENTS`; keep two semantic special-cases inline.
- **Create** `.claude/hooks/test_wiki_claim_gate.py` — regression-lock the 5-prefix block/pass behavior.
- **Modify** `.claude/hooks/wiki_structure_lint.py``import wiki_rules`; add `run_pre()` + `run_hook()` testable functions; wire `--pre`; tier `--hook`; add suppressed-count line.
- **Modify** `.claude/hooks/test_wiki_structure_lint.py` — add `--pre`/`--hook` fix-up cases.
- **Modify** `.claude/settings.json` — add `structure_lint.py --pre` to PreToolUse.
**Sibling-import rule (applies to both hooks):** the first executable lines (after `from __future__`) must be:
```python
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import wiki_rules
```
This makes `import wiki_rules` resolve whether the file is run as a script (hook) or loaded via `importlib.spec_from_file_location` (tests).
---
## Task 1: Create `wiki_rules.py` shared module
**Files:**
- Create: `.claude/hooks/wiki_rules.py`
- Test: `.claude/hooks/test_wiki_rules.py`
The moved helpers are **verbatim copies** of `wiki_claim_gate.py` current functions: `read_event` (42-47), `tool_name` (50-56), `tool_input` (59-76), `target_path` (79-85), `write_content` (88-94), `projected_content` (97-135), `command_string` (138-145), `rel_to_root` (148-154), `has_table` (157-163). `ROOT` is the same `Path(__file__).resolve().parents[2]`.
- [ ] **Step 1: Write the failing test**
Create `.claude/hooks/test_wiki_rules.py`:
```python
#!/usr/bin/env python3
"""wiki_rules.py 단위 테스트 (stdlib unittest)."""
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
_SPEC = importlib.util.spec_from_file_location(
"wiki_rules", str(Path(__file__).with_name("wiki_rules.py")))
wr = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(wr)
class TestProjectedContent(unittest.TestCase):
def test_write_full_content(self):
# Write 스타일: content 키가 있으면 그대로 반환
inp = {"content": "FULL BODY"}
self.assertEqual(wr.projected_content(None, inp), "FULL BODY")
def test_edit_applies_old_new(self):
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "f.md"
p.write_text("alpha BETA gamma")
inp = {"old_string": "BETA", "new_string": "DELTA"}
self.assertEqual(wr.projected_content(p, inp), "alpha DELTA gamma")
class TestSeverityData(unittest.TestCase):
def test_critical_codes_are_links(self):
self.assertIn("BROKEN_LINK", wr.CRITICAL_CODES)
self.assertIn("BROKEN_MD_LINK", wr.CRITICAL_CODES)
self.assertNotIn("MISSING_SECTION", wr.CRITICAL_CODES)
def test_fixup_codes_are_completeness(self):
for c in ("MISSING_SECTION", "MISSING_FRONTMATTER",
"EMPTY_SELECTION_CRITERION", "DANGLING_ANCHOR",
"PROJECT_NO_DIAGRAM", "PROJECT_NO_BRANCH_TABLE",
"UNMAPPED_SOURCE_TYPE"):
self.assertIn(c, wr.FIXUP_CODES)
self.assertNotIn("BROKEN_LINK", wr.FIXUP_CODES)
def test_claim_requirements_cover_five_prefixes(self):
prefixes = {p for req in wr.CLAIM_REQUIREMENTS for p in req["prefix"]}
for p in ("raw/official-docs/", "raw/company-tech-blogs/",
"raw/branch-notes/", "wiki/concepts/"):
self.assertIn(p, prefixes)
if __name__ == "__main__":
unittest.main(verbosity=2)
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 .claude/hooks/test_wiki_rules.py`
Expected: FAIL — `FileNotFoundError`/`ModuleNotFoundError` (wiki_rules.py does not exist yet).
- [ ] **Step 3: Write minimal implementation**
Create `.claude/hooks/wiki_rules.py`. Copy the 9 helper functions **verbatim** from `wiki_claim_gate.py` (current line ranges noted in the Task header), then append the SSOT data and severity constants:
```python
#!/usr/bin/env python3
"""wiki_rules.py — claim_gate / structure_lint 공유 기계장치 + SSOT 데이터 (stdlib only).
여기엔 *정책*이 아니라 *공유 메커니즘*과 *참조 데이터*만 둔다:
- 이벤트/IO 헬퍼 (claim_gate 에서 이관, 두 훅이 공유)
- CLAIM_REQUIREMENTS : claim 테이블/섹션 요구 SSOT (이전엔 claim_gate inline 하드코딩 — G5 dedup)
- 심각도 티어 상수 : structure_lint 의 게이트 결정(차단 vs fix-up vs warn)이 소비
정책(block/warn 적용)은 각 훅에 남는다.
"""
from __future__ import annotations
import json
import shlex
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
# ---------- 이벤트/IO 헬퍼 (claim_gate 에서 verbatim 이관) ----------
# read_event / tool_name / tool_input / target_path / write_content /
# projected_content / command_string / rel_to_root / has_table
# (wiki_claim_gate.py 의 동일 함수 본문을 그대로 복사. target_path/rel_to_root 는 ROOT 사용.)
# ... (verbatim copies here) ...
# ---------- claim 요구 SSOT (G5 dedup 대상) ----------
CLAIM_REQUIREMENTS = [
{"prefix": ("raw/official-docs/", "raw/company-tech-blogs/"),
"tables": [("## Claims Extracted",
["Claim ID", "Claim", "Evidence quote", "Strength", "Applies to", "Does not prove"])],
"sections": ["## Usage Boundaries"]},
{"prefix": ("raw/branch-notes/",),
"tables": [("## Decision Evidence Map",
["Decision ID", "Decision", "Supporting Claims", "Evidence Strength", "Open Risk"])],
"section_regex": [r"^## .*\bClaims To Verify\b"]},
{"prefix": ("wiki/concepts/",),
"tables": [("## Claim-backed Knowledge",
["Knowledge Point", "Supporting Claims", "Confidence", "Notes"])]},
]
# ---------- 심각도 티어 (structure_lint 소비) ----------
CRITICAL_CODES = frozenset({"BROKEN_LINK", "BROKEN_MD_LINK"}) # PreToolUse block
FIXUP_CODES = frozenset({
"MISSING_SECTION", "MISSING_FRONTMATTER", "EMPTY_SELECTION_CRITERION",
"DANGLING_ANCHOR", "PROJECT_NO_DIAGRAM", "PROJECT_NO_BRANCH_TABLE",
"UNMAPPED_SOURCE_TYPE",
}) # PostToolUse exit-2 (완성 선언 시)
```
Replace the `# ... (verbatim copies here) ...` comment with the 9 functions copied exactly from `wiki_claim_gate.py`. Keep their bodies unchanged except `target_path`/`rel_to_root`, which already reference module-level `ROOT` — that now resolves to `wiki_rules.ROOT` (same value).
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 .claude/hooks/test_wiki_rules.py`
Expected: PASS (5 tests OK).
- [ ] **Step 5: Checkpoint (no commit)**
Run: `python3 .claude/hooks/test_wiki_rules.py` → all green. Do NOT commit.
---
## Task 2: Refactor `wiki_claim_gate.py` to consume `wiki_rules` (behavior-preserving)
**Files:**
- Modify: `.claude/hooks/wiki_claim_gate.py` (replace lines 42-163 helpers with import; rewrite `check_markdown_write` 166-220 to drive table/section checks from `wiki_rules.CLAIM_REQUIREMENTS`)
- Test: `.claude/hooks/test_wiki_claim_gate.py` (new — written FIRST to lock current behavior)
This is a refactor: tests are written against **current** behavior and must stay green through the change.
- [ ] **Step 1: Write the regression-lock test**
Create `.claude/hooks/test_wiki_claim_gate.py`:
```python
#!/usr/bin/env python3
"""wiki_claim_gate.py 회귀 고정 테스트 — check_markdown_write 행동 동치 (refactor 전후 동일)."""
import importlib.util
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
_SPEC = importlib.util.spec_from_file_location(
"wcg", str(Path(__file__).with_name("wiki_claim_gate.py")))
wcg = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(wcg)
CLAIMS_TABLE = (
"## Claims Extracted\n"
"| Claim ID | Claim | Evidence quote | Strength | Applies to | Does not prove |\n"
"|---|---|---|---|---|---|\n"
"| C1 | x | q | company-case-study | a | b |\n"
)
USAGE = "## Usage Boundaries\n- x\n"
DEM = (
"## Decision Evidence Map\n"
"| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |\n"
"|---|---|---|---|---|\n"
"| D1 | x | C1 | company-case-study | none |\n"
)
CTV = "## 검증해야 할 주장 / Claims To Verify\n- v\n"
class TestSourceNote(unittest.TestCase):
def test_missing_claims_table_blocks(self):
f = wcg.check_markdown_write("raw/official-docs/x.md", "# t\n" + USAGE)
self.assertTrue(any("Claims Extracted" in m for m in f))
def test_complete_source_note_passes(self):
f = wcg.check_markdown_write("raw/official-docs/x.md", "# t\n" + CLAIMS_TABLE + USAGE)
self.assertEqual(f, [])
class TestBranchNote(unittest.TestCase):
def test_missing_dem_blocks(self):
f = wcg.check_markdown_write("raw/branch-notes/feature-x.md", "# t\n" + CTV)
self.assertTrue(any("Decision Evidence Map" in m for m in f))
def test_complete_branch_note_passes(self):
f = wcg.check_markdown_write("raw/branch-notes/feature-x.md", "# t\n" + DEM + CTV)
self.assertEqual(f, [])
def test_officially_supported_without_strength_blocks(self):
body = "# t\n" + DEM + CTV + "\n이 기능은 officially supported 된다.\n"
f = wcg.check_markdown_write("raw/branch-notes/feature-x.md", body)
self.assertTrue(any("official" in m.lower() for m in f))
class TestConceptNote(unittest.TestCase):
def test_missing_claim_backed_blocks(self):
f = wcg.check_markdown_write("wiki/concepts/x.md", "# t\n본문")
self.assertTrue(any("Claim-backed Knowledge" in m for m in f))
class TestUnrelatedPath(unittest.TestCase):
def test_non_gated_path_passes(self):
# 게이트 대상 아닌 경로 → 통과(빈 failures)
f = wcg.check_markdown_write("wiki/projects/x.md", "# anything\n")
self.assertEqual(f, [])
if __name__ == "__main__":
unittest.main(verbosity=2)
```
- [ ] **Step 2: Run test to verify it passes against CURRENT code**
Run: `python3 .claude/hooks/test_wiki_claim_gate.py`
Expected: PASS — this locks the current behavior as the regression baseline (claim_gate already implements all these checks). If any test fails now, the test encodes a wrong expectation — fix the test to match current behavior before refactoring.
- [ ] **Step 3: Refactor `wiki_claim_gate.py`**
(a) Replace the helper block (current lines ~12-163: the `from __future__` through `has_table`) so the top reads:
```python
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import wiki_rules
from wiki_rules import (
ROOT, read_event, tool_name, tool_input, target_path, write_content,
projected_content, command_string, rel_to_root, has_table,
)
```
Keep `emit_allow`, `emit_block` as they are (they `print`/`sys.exit`, hook-specific). Delete the now-moved 9 functions from claim_gate.
(b) Rewrite `check_markdown_write(rel, text)` so the official-docs / company-tech-blogs / branch-notes / concepts table+section checks are produced by iterating `wiki_rules.CLAIM_REQUIREMENTS`, while the two **semantic** special-cases stay inline:
```python
def check_markdown_write(rel: str, text: str) -> list[str]:
failures: list[str] = []
if not rel.endswith(".md") or not text:
return failures
for req in wiki_rules.CLAIM_REQUIREMENTS:
if not rel.startswith(req["prefix"]): # tuple-of-prefixes → str.startswith accepts tuple
continue
for section, cols in req.get("tables", []):
if not has_table(text, section, cols):
failures.append(
f"{req['prefix'][0]} 류 문서는 `{section}` 표(열: {' | '.join(cols)})를 가져야 한다."
)
for sec in req.get("sections", []):
if sec not in text:
failures.append(f"문서는 `{sec}` 섹션을 가져야 한다.")
for rx in req.get("section_regex", []):
if not re.search(rx, text, re.MULTILINE):
failures.append("branch-note must include `## Claims To Verify` "
"(bilingual `## 검증해야 할 주장 / Claims To Verify` 도 허용).")
# 의미 규칙 1: branch-note 의 'officially supported' 주장은 official 강도 필요 (정책 — 인라인 유지)
if rel.startswith("raw/branch-notes/"):
if re.search(r"(?i)\bofficial(?:ly)? supported\b|공식(?:적으로)?\s*지원", text):
if not re.search(r"official-(standard|vendor-doc|reference)", text):
failures.append(
"`officially supported` style claim requires an official claim strength "
"(`official-standard`, `official-vendor-doc`, or `official-reference`)."
)
# 의미 규칙 2: 감사 리포트가 COMPLETE 주장 시 traceability 검증 포함 (정책 — 인라인 유지)
if rel.startswith("docs/superpowers/specs/") and rel.endswith("-report.md"):
if re.search(r"Verdict:\s*COMPLETE|\*\*Verdict:?\*\*\s*COMPLETE", text):
required = ["Decision Evidence Map", "Claims Extracted", "UNSUPPORTED_DECISION"]
missing = [item for item in required if item not in text]
if missing:
failures.append(
"audit report cannot claim COMPLETE unless it verifies claim traceability. "
f"Missing references: {', '.join(missing)}."
)
return failures
```
Note: `str.startswith` accepts a tuple, so `rel.startswith(req["prefix"])` works directly with the `prefix` tuples. `main()` and the rest of the file (subagent gates, bash gate) are unchanged except they now reference the imported helpers.
- [ ] **Step 4: Run tests to verify they still pass**
Run: `python3 .claude/hooks/test_wiki_claim_gate.py`
Expected: PASS — behavior unchanged after refactor.
- [ ] **Step 5: Checkpoint (no commit)**
Run both: `python3 .claude/hooks/test_wiki_rules.py && python3 .claude/hooks/test_wiki_claim_gate.py` → green. No commit.
---
## Task 3: Add `--pre` mode to `wiki_structure_lint.py` (PreToolUse link block)
**Files:**
- Modify: `.claude/hooks/wiki_structure_lint.py` (add `import wiki_rules`; add `run_pre()`; wire `--pre` in `main`)
- Test: `.claude/hooks/test_wiki_structure_lint.py` (append `TestPreMode`)
- [ ] **Step 1: Write the failing test**
Append to `.claude/hooks/test_wiki_structure_lint.py` (the module is loaded as `wsl`; add a `sys.path` insert near the top of the file if not present, mirroring Task 1):
```python
class TestPreMode(unittest.TestCase):
def _event(self, root, rel, content):
return {"tool_name": "Write",
"tool_input": {"file_path": str(root / rel), "content": content}}
def test_ghost_wikilink_blocks(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "raw" / "branch-notes").mkdir(parents=True)
ev = self._event(root, "raw/branch-notes/b.md", "# t\nsee [[raw/nonexistent/ghost]]\n")
self.assertEqual(wsl.run_pre(ev, root), 2)
def test_backtick_placeholder_passes(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "raw" / "branch-notes").mkdir(parents=True)
ev = self._event(root, "raw/branch-notes/b.md", "# t\nfuture: `[[raw/nonexistent/ghost]]`\n")
self.assertEqual(wsl.run_pre(ev, root), 0)
def test_no_links_skips_and_passes(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "raw" / "branch-notes").mkdir(parents=True)
ev = self._event(root, "raw/branch-notes/b.md", "# t\n링크 없는 본문\n")
self.assertEqual(wsl.run_pre(ev, root), 0)
def test_non_wiki_path_passes(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "docs").mkdir()
ev = self._event(root, "docs/x.md", "see [[raw/nonexistent/ghost]]\n")
self.assertEqual(wsl.run_pre(ev, root), 0)
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -k Pre`
Expected: FAIL — `AttributeError: module 'wsl' has no attribute 'run_pre'`.
- [ ] **Step 3: Implement `run_pre`**
In `wiki_structure_lint.py`, after the `from __future__ import annotations` line add the sibling import:
```python
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
import wiki_rules
```
(Path is already imported below; move the `from pathlib import Path` above this insert, or use a local import — keep it stdlib-clean.) Then add, near `main()`:
```python
def run_pre(event, root):
"""PreToolUse: projected 본문의 C2 깨진링크(CRITICAL)만 차단. 반환 exit code."""
inp = wiki_rules.tool_input(event)
p = wiki_rules.target_path(inp)
if p is None or not str(p).endswith(".md"):
return 0
try:
rel = p.resolve().relative_to(root).as_posix()
except Exception:
return 0
if not (rel.startswith("raw/") or rel.startswith("wiki/")):
return 0
text = wiki_rules.projected_content(p, inp)
# 위키링크/마크다운링크가 전혀 없으면 vault 인덱스 빌드 스킵 (성능).
if "[[" not in text and "](" not in text:
return 0
vp, vb = build_vault_index(root)
doc = {"lines": text.splitlines()}
findings = check_c2(doc, vp, vb, root, {}, rel)
critical = [(c, ln, m) for (c, ln, m) in findings if c in wiki_rules.CRITICAL_CODES]
if critical:
print(f"✗ wiki-structure-lint (pre): {rel} — 깨진 링크 {len(critical)}건 → 쓰기 차단",
file=sys.stderr)
for code, ln, msg in critical[:10]:
loc = f":{ln}" if ln else ""
print(f" [{code}]{loc} {msg}", file=sys.stderr)
if len(critical) > 10:
print(f" … 외 {len(critical) - 10}건 (suppressed)", file=sys.stderr)
print(" 미존재 타깃은 백틱 코드(`[[slug]]`)로 표기하거나 타깃 파일을 먼저 생성하세요.",
file=sys.stderr)
return 2
return 0
```
Wire it in `main()` — add the arg and dispatch **before** the existing `--hook` block:
```python
ap.add_argument("--pre", action="store_true",
help="PreToolUse hook — projected 본문 C2 깨진링크 차단 (blocking)")
...
if args.pre:
import json as _json
try:
event = _json.loads(sys.stdin.read() or "{}")
except Exception:
sys.exit(0)
sys.exit(run_pre(event, root))
```
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -k Pre`
Expected: PASS (4 tests).
- [ ] **Step 5: Checkpoint (no commit)**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py` → all green (old + new).
---
## Task 4: Tier `--hook` into fix-up gate + suppressed-count line
**Files:**
- Modify: `.claude/hooks/wiki_structure_lint.py` (extract existing `--hook` body 486-531 into `run_hook(event, root)`; add exit-2 on `FIXUP_CODES` when `is_completeness_checkable`; add suppressed line)
- Test: `.claude/hooks/test_wiki_structure_lint.py` (append `TestHookTiering`)
- [ ] **Step 1: Write the failing test**
Append:
```python
class TestHookTiering(unittest.TestCase):
def _write(self, root, rel, fm, body):
p = root / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("---\n" + fm + "\n---\n" + body)
return p
def _event(self, p):
return {"tool_name": "Edit", "tool_input": {"file_path": str(p)}}
def test_completed_missing_section_blocks(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
# 완성 선언(status: verified) + 필수 섹션 누락 → fix-up exit 2
p = self._write(root, "wiki/concepts/x.md",
"title: x\nsource_type: concept\nstatus: verified\ntags: [a]", "본문만\n")
self.assertEqual(wsl.run_hook(self._event(p), root), 2)
def test_draft_missing_section_warns_only(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
# draft → 완성 선언 아님 → exit 0 (WARN)
p = self._write(root, "wiki/concepts/x.md",
"title: x\nsource_type: concept\nstatus: draft\ntags: [a]", "본문만\n")
self.assertEqual(wsl.run_hook(self._event(p), root), 0)
def test_non_wiki_path_passes(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
p = self._write(root, "docs/x.md", "title: x", "본문\n")
self.assertEqual(wsl.run_hook(self._event(p), root), 0)
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -k HookTiering`
Expected: FAIL — `AttributeError: ... has no attribute 'run_hook'`.
- [ ] **Step 3: Refactor `--hook` into `run_hook`**
Extract the current `if args.hook:` body (lines ~486-531) into a function returning an exit code, adding the tier decision + suppressed line:
```python
def run_hook(event, root):
"""PostToolUse: 완성 선언 문서의 C1/C3/DANGLING(FIXUP) → exit 2 fix-up. 그 외 WARN(0)."""
inp = event.get("tool_input") or {}
fp = next((inp[k] for k in ("file_path", "path", "absolute_path", "TargetFile", "target_file")
if isinstance(inp.get(k), str)), None)
if not fp or not fp.endswith(".md"):
return 0
p = Path(fp)
if not p.is_absolute():
p = (root / fp)
try:
rel = p.resolve().relative_to(root).as_posix()
except Exception:
return 0
if not (rel.startswith("raw/") or rel.startswith("wiki/")) or not p.exists():
return 0
vp, vb = build_vault_index(root)
doc = parse_doc(p)
findings = check_c2(doc, vp, vb, root, {}, rel) # C2 항상
if is_completeness_checkable(doc):
by_st, by_file = build_template_index(root)
tmpl = resolve_template(doc["fm"], by_st, by_file)
if rel.startswith("raw/project-notes/"):
fm_findings = []
if tmpl is not None:
for k in tmpl["fm_keys"]:
if k not in doc["fm_keys"]:
fm_findings.append(("MISSING_FRONTMATTER", 0, f"frontmatter 키 누락: '{k}'"))
findings = fm_findings + check_project_proxies(doc) + findings
else:
findings = check_c1(doc, tmpl) + findings + check_c3(doc)
if not findings:
return 0
fixup = [f for f in findings if f[0] in wiki_rules.FIXUP_CODES]
block = bool(fixup) and is_completeness_checkable(doc)
sigil = "" if block else ""
print(f"{sigil} wiki-structure-lint: {rel} — 구조/링크 이슈 {len(findings)}"
+ (" → fix 필요" if block else ""), file=sys.stderr)
for code, ln, msg in findings[:10]:
loc = f":{ln}" if ln else ""
print(f" [{code}]{loc} {msg}", file=sys.stderr)
if len(findings) > 10:
print(f" … 외 {len(findings) - 10}건 (suppressed)", file=sys.stderr)
print(" 깨진 링크는 타깃 생성/수정(placeholder 는 `백틱 코드경로`). "
"섹션/선택조건은 완성 선언 문서에만 검사됨.", file=sys.stderr)
return 2 if block else 0
```
Replace the old `if args.hook:` block body with:
```python
if args.hook:
import json as _json
try:
event = _json.loads(sys.stdin.read() or "{}")
except Exception:
sys.exit(0)
sys.exit(run_hook(event, root))
```
Keep `build_vault_index`/`parse_doc`/`check_c1`/`check_c3`/`is_completeness_checkable`/`check_project_proxies`/`resolve_template`/`build_template_index` as-is (already module-level).
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py -k HookTiering`
Expected: PASS (3 tests).
- [ ] **Step 5: Checkpoint (no commit)**
Run: `python3 .claude/hooks/test_wiki_structure_lint.py` → all green.
---
## Task 5: Wire `--pre` into `settings.json`
**Files:**
- Modify: `.claude/settings.json` (add a second PreToolUse entry)
- [ ] **Step 1: Edit `settings.json`**
Change the `PreToolUse` array from one matcher to two (leave PostToolUse / Subagent blocks untouched):
```json
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/wiki_claim_gate.py",
"timeout": 30 }
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{ "type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/wiki_structure_lint.py --pre",
"timeout": 30 }
]
}
],
```
- [ ] **Step 2: Validate JSON**
Run: `python3 -m json.tool .claude/settings.json > /dev/null && echo OK`
Expected: `OK`.
- [ ] **Step 3: Checkpoint (no commit)** — JSON valid.
---
## Task 6: Full regression + acceptance smoke (spec §6)
**Files:** none (verification only)
- [ ] **Step 1: Run the full hook test suite**
Run:
```bash
python3 .claude/hooks/test_wiki_rules.py && \
python3 .claude/hooks/test_wiki_claim_gate.py && \
python3 .claude/hooks/test_wiki_structure_lint.py
```
Expected: all suites OK, zero failures.
- [ ] **Step 2: `--all` regression (gate wiring must not change findings)**
Run: `python3 .claude/hooks/wiki_structure_lint.py --all`
Expected: a summary line `== 요약: N개 중 FAIL f / PASS p ==`. Compare `f` against a pre-change baseline (run the same on `git stash` of the hooks if unsure) — the FAIL set must be unchanged (the `--all` path is untouched by this spec).
- [ ] **Step 3: Acceptance criterion 1 — `--pre` blocks ghost, passes backtick (live stdin)**
Run:
```bash
echo '{"tool_name":"Write","tool_input":{"file_path":"raw/branch-notes/_smoke.md","content":"# t\nsee [[raw/nonexistent/ghost]]\n"}}' \
| python3 .claude/hooks/wiki_structure_lint.py --pre; echo "exit=$?"
echo '{"tool_name":"Write","tool_input":{"file_path":"raw/branch-notes/_smoke.md","content":"# t\nfuture: `[[raw/nonexistent/ghost]]`\n"}}' \
| python3 .claude/hooks/wiki_structure_lint.py --pre; echo "exit=$?"
```
Expected: first `exit=2` with a stderr `[BROKEN_LINK]` line; second `exit=0`.
- [ ] **Step 4: Acceptance criterion 3 — `--hook` fix-up only on completion**
Create a temp completed concept doc missing required sections, pipe an Edit event, expect exit 2; flip `status: verified``status: draft`, expect exit 0. (Use a path under `raw/` or `wiki/` in the real repo or a temp `--root`.)
```bash
python3 - <<'PY'
import json, subprocess, tempfile, os
from pathlib import Path
d = tempfile.mkdtemp()
root = Path(d); (root/"wiki"/"concepts").mkdir(parents=True)
# copy templates so resolve_template works
import shutil; shutil.copytree(".claude/hooks", root/".claude"/"hooks"); shutil.copytree("templates", root/"templates")
p = root/"wiki"/"concepts"/"x.md"
def run(status):
p.write_text(f"---\ntitle: x\nsource_type: concept\nstatus: {status}\ntags: [a]\n---\n본문만\n")
ev = json.dumps({"tool_name":"Edit","tool_input":{"file_path":str(p)}})
r = subprocess.run(["python3", str(root/".claude"/"hooks"/"wiki_structure_lint.py"),
"--hook", "--root", str(root)], input=ev, text=True, capture_output=True)
print(status, "exit", r.returncode)
run("verified"); run("draft")
PY
```
Expected: `verified exit 2`, `draft exit 0`.
- [ ] **Step 5: Acceptance criterion 8 — happy path passes both gates**
Take a real completed wiki doc with valid links; run it through `--pre` (Write event) and `--hook` (Edit event). Both must `exit=0`. Confirms the gate doesn't block legitimate writes.
- [ ] **Step 6: Final checkpoint (no commit)**
All §6 acceptance criteria (1, 3, 4, 7, 8) demonstrated green. Report results to the user. Do NOT commit (per user instruction) — leave changes staged in the working tree for the user to review.
---
## Self-Review (completed by plan author)
- **Spec coverage:** §3 DD1 hybrid gate → Tasks 3+4+5. §3 DD2 lean SSOT → Tasks 1+2. §4.1 wiki_rules → Task 1. §4.2 `--pre` → Task 3. §4.3 `--hook` tiering + suppressed → Task 4. §4.4 settings → Task 5. §6 acceptance 1-8 → Task 6. G6 (claim_gate tests) → Task 2. No spec requirement left unmapped.
- **Placeholder scan:** the only `...` is the explicit "copy these 9 functions verbatim from claim_gate lines X-Y" instruction with exact source line ranges — not a content gap. All test/impl steps carry runnable code.
- **Type/name consistency:** `run_pre(event, root)` / `run_hook(event, root)` signatures match between Tasks 3/4 impl and their tests; `CRITICAL_CODES`/`FIXUP_CODES`/`CLAIM_REQUIREMENTS` names match between Task 1 (def) and Tasks 2/3/4 (use); `check_markdown_write(rel, text)` signature unchanged (Task 2 tests call it as today).
@@ -0,0 +1,621 @@
# Spec B — Judge Verdict Schema & Adversarial Quorum Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
> **Commits excluded** per user instruction — no `git commit` steps; each task ends with a test-green checkpoint.
**Goal:** Make judge-agent verdicts machine-validated at the SubagentStop boundary and make the adversarial-review kill-decision a deterministic quorum tally — without the Workflow tool (Claude subagents lack tool-layer schema enforcement).
**Architecture:** Extend the shared `wiki_rules.py` with `validate_verdict_block` (P1 schema + verdict↔count consistency) and `tally_quorum` (≥2 REJECT = KILL, default-refute, abstain≠pass). `wiki_claim_gate.subagent_stop_gate` validates any subagent output carrying a `wiki-verdict` marker (block on schema error; non-judge output untouched). New `wiki_quorum.py` CLI tallies N adversarial-review outputs. Five judge `.md` files emit the machine block; `wiki-adversarial-reviewer` also flips to default-refute. Deterministic core is unit-tested; agent prose is mirrored across 3 platforms by hand (the generator is absent).
**Tech Stack:** Python 3 stdlib (`re`, `json`, `argparse`, `unittest`, `subprocess`). Claude Code hooks. Markdown agent specs.
**Spec:** `docs/superpowers/specs/2026-06-06-spec-b-judge-verdict-schema-and-quorum-design.md`
---
## File Structure
- **Modify** `.claude/hooks/wiki_rules.py` — add `parse_verdict_block`, `validate_verdict_block`, `tally_quorum`, constants.
- **Modify** `.claude/hooks/test_wiki_rules.py` — add `TestVerdictBlock`, `TestTallyQuorum`.
- **Create** `.claude/hooks/wiki_quorum.py` — CLI over `tally_quorum`.
- **Create** `.claude/hooks/test_wiki_quorum.py` — CLI integration tests.
- **Modify** `.claude/hooks/wiki_claim_gate.py``subagent_stop_gate` calls `validate_verdict_block`.
- **Modify** `.claude/hooks/test_wiki_claim_gate.py` — add `TestSubagentStopVerdict` (subprocess).
- **Modify** `.claude/agents/wiki-adversarial-reviewer.md` — per-finding block + default-refute + quorum doc.
- **Modify** `.claude/agents/{branch-depth-auditor,coverage-auditor,project-readiness-auditor,wiki-diagram-reviewer}.md` — standard verdict block.
- **Mirror (manual)** the 4 shared judges into `.agents/plugins/wiki-superpowers/agents/<name>.md`, `.codex/agents/<name>.md`, `.codex/agents/<name>.toml`. (`project-readiness-auditor` is Claude-only — no mirror.)
---
## Task 1: `wiki_rules` — verdict block parse + validate
**Files:**
- Modify: `.claude/hooks/wiki_rules.py` (append functions + constants)
- Test: `.claude/hooks/test_wiki_rules.py` (append `TestVerdictBlock`)
- [ ] **Step 1: Write the failing test**
Append to `.claude/hooks/test_wiki_rules.py` before the `if __name__` line:
```python
STD_OK = "리포트...\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: not-ready\nblocking: 2\nshould_fix: 1\nadvisory: 0\n```\n"
STD_CONTRADICT = "```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 2\nshould_fix: 0\nadvisory: 0\n```"
STD_BADVERDICT = "```wiki-verdict\nagent: x\nverdict: foo\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"
ADV_OK = "```wiki-verdict\nagent: wiki-adversarial-reviewer\nfinding: 4.1.1 action: KEEP\nfinding: 4.2.1 action: REJECT\n```"
ADV_BADACTION = "```wiki-verdict\nagent: wiki-adversarial-reviewer\nfinding: 4.1.1 action: NOPE\n```"
ADV_EMPTY = "```wiki-verdict\nagent: wiki-adversarial-reviewer\n```"
class TestVerdictBlock(unittest.TestCase):
def test_no_marker_returns_none(self):
parsed, errors = wr.validate_verdict_block("그냥 산문, 마커 없음")
self.assertIsNone(parsed)
self.assertEqual(errors, [])
def test_standard_valid(self):
parsed, errors = wr.validate_verdict_block(STD_OK)
self.assertEqual(errors, [])
self.assertEqual(parsed["agent"], "branch-depth-auditor")
self.assertEqual(parsed["kv"]["verdict"], "not-ready")
def test_standard_contradiction_flagged(self):
_, errors = wr.validate_verdict_block(STD_CONTRADICT)
self.assertTrue(any("blocking" in e for e in errors))
def test_standard_bad_verdict_flagged(self):
_, errors = wr.validate_verdict_block(STD_BADVERDICT)
self.assertTrue(any("verdict" in e for e in errors))
def test_adversarial_valid(self):
parsed, errors = wr.validate_verdict_block(ADV_OK)
self.assertEqual(errors, [])
self.assertEqual(len(parsed["findings"]), 2)
def test_adversarial_bad_action_flagged(self):
_, errors = wr.validate_verdict_block(ADV_BADACTION)
self.assertTrue(any("action" in e for e in errors))
def test_adversarial_empty_findings_flagged(self):
_, errors = wr.validate_verdict_block(ADV_EMPTY)
self.assertTrue(any("finding" in e for e in errors))
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 .claude/hooks/test_wiki_rules.py -k Verdict`
Expected: FAIL — `AttributeError: module 'wiki_rules' has no attribute 'validate_verdict_block'`.
- [ ] **Step 3: Implement in `wiki_rules.py`**
Append to `.claude/hooks/wiki_rules.py` (the `import re` must be added to the top import block — currently `wiki_rules.py` imports only `json, shlex, sys`):
```python
import re # (add to the existing import block at top of file)
VERDICT_FENCE_RE = re.compile(r"```wiki-verdict\s*\n(.*?)\n```", re.S)
VALID_VERDICT = {"ready", "not-ready", "blocked"}
VALID_ACTION = {"KEEP", "DOWNGRADE", "REJECT"}
REFUTATIONS_REQUIRED = 2 # ≥2 REJECT → kill (deep-research 기본값)
def parse_verdict_block(text):
"""본문에서 wiki-verdict fenced 블록을 찾아 dict 로 파싱. 없으면 None."""
m = VERDICT_FENCE_RE.search(text or "")
if not m:
return None
out = {"agent": None, "kv": {}, "findings": []}
for line in m.group(1).splitlines():
line = line.strip()
if not line:
continue
fm = re.match(r"finding:\s*(\S+)\s+action:\s*(\S+)", line)
if fm:
out["findings"].append((fm.group(1), fm.group(2)))
continue
kv = re.match(r"([a-z_]+):\s*(.+)$", line)
if kv:
k, v = kv.group(1), kv.group(2).strip()
if k == "agent":
out["agent"] = v
else:
out["kv"][k] = v
return out
def validate_verdict_block(text):
"""(parsed, errors). parsed None → 마커 없음(judge 아님, caller 통과).
errors 비어있지 않으면 스키마 위반 → SubagentStop 차단."""
parsed = parse_verdict_block(text)
if parsed is None:
return None, []
errors = []
if not parsed["agent"]:
errors.append("wiki-verdict 블록에 `agent:` 누락")
if parsed["agent"] == "wiki-adversarial-reviewer":
if not parsed["findings"]:
errors.append("adversarial verdict 블록에 `finding: <id> action: <act>` 행 ≥1 필요")
for fid, act in parsed["findings"]:
if act not in VALID_ACTION:
errors.append(f"finding {fid}: action '{act}' 비허용(KEEP|DOWNGRADE|REJECT)")
else:
v = parsed["kv"].get("verdict")
if v not in VALID_VERDICT:
errors.append(f"verdict '{v}' 비허용(ready|not-ready|blocked)")
blocking = None
try:
blocking = int(parsed["kv"].get("blocking", ""))
int(parsed["kv"].get("should_fix", ""))
int(parsed["kv"].get("advisory", ""))
except ValueError:
errors.append("blocking/should_fix/advisory 는 정수여야 함")
if blocking is not None and v == "ready" and blocking != 0:
errors.append("verdict=ready 인데 blocking≠0 (모순)")
if blocking is not None and v == "not-ready" and blocking < 1:
errors.append("verdict=not-ready 인데 blocking<1 (모순)")
return parsed, errors
```
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 .claude/hooks/test_wiki_rules.py -k Verdict`
Expected: PASS (7 tests).
- [ ] **Step 5: Checkpoint (no commit)**`python3 .claude/hooks/test_wiki_rules.py` all green.
---
## Task 2: `wiki_rules` — quorum tally
**Files:**
- Modify: `.claude/hooks/wiki_rules.py` (append `tally_quorum`)
- Test: `.claude/hooks/test_wiki_rules.py` (append `TestTallyQuorum`)
- [ ] **Step 1: Write the failing test**
Append:
```python
def _adv(*pairs):
lines = "\n".join(f"finding: {fid} action: {act}" for fid, act in pairs)
return f"```wiki-verdict\nagent: wiki-adversarial-reviewer\n{lines}\n```"
class TestTallyQuorum(unittest.TestCase):
def test_two_rejects_kill(self):
blocks = [_adv(("A", "REJECT")), _adv(("A", "REJECT")), _adv(("A", "KEEP"))]
per = wr.tally_quorum(blocks)
self.assertEqual(per["A"]["decision"], "KILL")
def test_unanimous_keep(self):
blocks = [_adv(("A", "KEEP")), _adv(("A", "KEEP")), _adv(("A", "KEEP"))]
self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "KEEP")
def test_reject_plus_downgrade_is_downgrade(self):
blocks = [_adv(("A", "REJECT")), _adv(("A", "DOWNGRADE")), _adv(("A", "KEEP"))]
self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "DOWNGRADE")
def test_abstain_not_pass(self):
# 한 블록만 KEEP, 나머지 둘은 A 를 누락(abstain) → 정족수 미달 → UNVERIFIED
blocks = [_adv(("A", "KEEP")), _adv(("B", "KEEP")), _adv(("C", "KEEP"))]
self.assertEqual(wr.tally_quorum(blocks)["A"]["decision"], "UNVERIFIED")
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 .claude/hooks/test_wiki_rules.py -k Tally`
Expected: FAIL — `AttributeError: ... 'tally_quorum'`.
- [ ] **Step 3: Implement in `wiki_rules.py`**
Append:
```python
def tally_quorum(block_texts, refutations_required=REFUTATIONS_REQUIRED):
"""N개 adversarial verdict 블록 → per-finding 결정론 판정.
refute = DOWNGRADE 또는 REJECT (원 severity 반박).
default-refute: 어떤 pass 가 finding 을 누락/malformed → abstain(non-KEEP).
결정: reject≥req → KILL · (reject+downgrade)≥req → DOWNGRADE ·
keep≥req → KEEP · 그 외(정족수 미달) → UNVERIFIED(통과 금지).
"""
parsed_all = [parse_verdict_block(t) for t in block_texts]
all_fids = set()
for p in parsed_all:
if p:
for fid, _ in p["findings"]:
all_fids.add(fid)
per = {}
for fid in all_fids:
keep = downgrade = reject = abstain = 0
for p in parsed_all:
act = None
if p:
for f, a in p["findings"]:
if f == fid:
act = a
break
if act == "KEEP":
keep += 1
elif act == "DOWNGRADE":
downgrade += 1
elif act == "REJECT":
reject += 1
else:
abstain += 1
if reject >= refutations_required:
decision = "KILL"
elif (reject + downgrade) >= refutations_required:
decision = "DOWNGRADE"
elif keep >= refutations_required:
decision = "KEEP"
else:
decision = "UNVERIFIED"
per[fid] = {"keep": keep, "downgrade": downgrade, "reject": reject,
"abstain": abstain, "n": len(block_texts), "decision": decision}
return per
```
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 .claude/hooks/test_wiki_rules.py -k Tally`
Expected: PASS (4 tests).
- [ ] **Step 5: Checkpoint (no commit)**`python3 .claude/hooks/test_wiki_rules.py` all green.
---
## Task 3: `wiki_quorum.py` CLI
**Files:**
- Create: `.claude/hooks/wiki_quorum.py`
- Test: `.claude/hooks/test_wiki_quorum.py`
- [ ] **Step 1: Write the failing test**
Create `.claude/hooks/test_wiki_quorum.py`:
```python
#!/usr/bin/env python3
"""wiki_quorum.py CLI 통합 테스트."""
import subprocess
import tempfile
import unittest
from pathlib import Path
CLI = str(Path(__file__).with_name("wiki_quorum.py"))
def _adv(*pairs):
lines = "\n".join(f"finding: {fid} action: {act}" for fid, act in pairs)
return f"```wiki-verdict\nagent: wiki-adversarial-reviewer\n{lines}\n```"
class TestQuorumCLI(unittest.TestCase):
def _files(self, d, *texts):
paths = []
for i, t in enumerate(texts):
p = Path(d) / f"v{i}.md"
p.write_text(t)
paths.append(str(p))
return paths
def test_kill_exits_1(self):
with tempfile.TemporaryDirectory() as d:
paths = self._files(d, _adv(("A", "REJECT")), _adv(("A", "REJECT")), _adv(("A", "KEEP")))
r = subprocess.run(["python3", CLI] + paths, capture_output=True, text=True)
self.assertEqual(r.returncode, 1)
self.assertIn("KILL", r.stdout)
def test_all_keep_exits_0(self):
with tempfile.TemporaryDirectory() as d:
paths = self._files(d, _adv(("A", "KEEP")), _adv(("A", "KEEP")), _adv(("A", "KEEP")))
r = subprocess.run(["python3", CLI] + paths, capture_output=True, text=True)
self.assertEqual(r.returncode, 0)
self.assertIn("KEEP", r.stdout)
if __name__ == "__main__":
unittest.main(verbosity=2)
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 .claude/hooks/test_wiki_quorum.py`
Expected: FAIL — `FileNotFoundError` (wiki_quorum.py missing) → subprocess returncode nonzero / can't run.
- [ ] **Step 3: Implement `wiki_quorum.py`**
Create `.claude/hooks/wiki_quorum.py`:
```python
#!/usr/bin/env python3
"""wiki_quorum.py — N개 adversarial verdict 블록의 결정론 quorum tally CLI.
사용:
python3 wiki_quorum.py vote1.md vote2.md vote3.md
cat votes.md | python3 wiki_quorum.py --stdin # '---' 구분 멀티블록
exit: 1 if any KILL/UNVERIFIED, else 0.
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import wiki_rules
def main():
args = sys.argv[1:]
if "--stdin" in args:
blob = sys.stdin.read()
blocks = [b for b in blob.split("\n---\n") if "wiki-verdict" in b]
else:
blocks = []
for a in args:
try:
blocks.append(Path(a).read_text(encoding="utf-8"))
except Exception as e:
print(f"파일 읽기 실패: {a}{e}", file=sys.stderr)
if not blocks:
print("verdict 블록 입력 없음", file=sys.stderr)
sys.exit(2)
per = wiki_rules.tally_quorum(blocks)
print(f"== Quorum tally: N={len(blocks)} votes, {len(per)} findings ==")
print("| finding | keep | down | reject | abstain | decision |")
print("|---|---|---|---|---|---|")
bad = 0
for fid in sorted(per):
r = per[fid]
if r["decision"] in ("KILL", "UNVERIFIED"):
bad += 1
print(f"| {fid} | {r['keep']} | {r['downgrade']} | {r['reject']} | {r['abstain']} | {r['decision']} |")
print(f"\nKILL/UNVERIFIED: {bad} / {len(per)}")
sys.exit(1 if bad else 0)
if __name__ == "__main__":
main()
```
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 .claude/hooks/test_wiki_quorum.py`
Expected: PASS (2 tests).
- [ ] **Step 5: Checkpoint (no commit)** — green.
---
## Task 4: `claim_gate.subagent_stop_gate` verdict enforcement
**Files:**
- Modify: `.claude/hooks/wiki_claim_gate.py` (`subagent_stop_gate`)
- Test: `.claude/hooks/test_wiki_claim_gate.py` (append `TestSubagentStopVerdict`, subprocess-based)
- [ ] **Step 1: Write the failing test**
Append to `.claude/hooks/test_wiki_claim_gate.py`:
```python
import json as _json
import subprocess as _sp
_GATE = str(Path(__file__).with_name("wiki_claim_gate.py"))
def _run_stop(message):
ev = {"hook_event_name": "SubagentStop", "last_assistant_message": message}
return _sp.run(["python3", _GATE], input=_json.dumps(ev), capture_output=True, text=True)
class TestSubagentStopVerdict(unittest.TestCase):
def test_malformed_verdict_blocks(self):
msg = "리뷰 끝.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 3\nshould_fix: 0\nadvisory: 0\n```"
r = _run_stop(msg)
self.assertEqual(r.returncode, 2)
def test_valid_verdict_allows(self):
msg = "리뷰 끝.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"
r = _run_stop(msg)
self.assertEqual(r.returncode, 0)
def test_no_marker_allows(self):
r = _run_stop("그냥 일반 subagent 출력, 마커 없음")
self.assertEqual(r.returncode, 0)
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopVerdict`
Expected: FAIL — `test_malformed_verdict_blocks` gets returncode 0 (gate not yet checking verdict).
- [ ] **Step 3: Implement — extend `subagent_stop_gate`**
In `.claude/hooks/wiki_claim_gate.py`, replace the `subagent_stop_gate` body's final `emit_allow()` with a verdict check before it:
```python
def subagent_stop_gate(event: dict) -> None:
message = event.get("last_assistant_message") or ""
if not isinstance(message, str):
emit_allow()
if re.search(r"Verdict:\s*COMPLETE|\bDONE\b|완료", message):
missing = []
for term in ("Claim ID", "Decision Evidence Map", "UNSUPPORTED_DECISION"):
if term not in message:
missing.append(term)
if missing and not event.get("stop_hook_active"):
emit_block(
"Subagent output claims completion but does not report claim-traceability checks: "
+ ", ".join(missing)
)
# judge 출력에 wiki-verdict 마커가 있으면 스키마 검증(없으면 judge 아님 → 통과).
parsed, verr = wiki_rules.validate_verdict_block(message)
if parsed is not None and verr and not event.get("stop_hook_active"):
emit_block("judge verdict 블록 스키마 오류:\n- " + "\n- ".join(verr))
emit_allow()
```
(`wiki_rules` is already imported in `wiki_claim_gate.py` from Spec A.)
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopVerdict`
Expected: PASS (3 tests).
- [ ] **Step 5: Checkpoint (no commit)**`python3 .claude/hooks/test_wiki_claim_gate.py` all green (old 11 + 3).
---
## Task 5: `wiki-adversarial-reviewer.md` — block + default-refute + quorum doc
**Files:**
- Modify: `.claude/agents/wiki-adversarial-reviewer.md`
- [ ] **Step 1: Add the machine verdict block to the Output section**
In the `## Output` markdown template (after the `## Aggregate metrics` block, before the closing ```), add — as part of the report the agent must emit:
````
## Machine verdict (필수 — SubagentStop 가 검증)
```wiki-verdict
agent: wiki-adversarial-reviewer
finding: 4.1.1 action: KEEP
finding: 4.2.1 action: DOWNGRADE
```
(모든 Falsification Summary 행의 Finding ID 를 `finding: <id> action: KEEP|DOWNGRADE|REJECT` 로 1:1 반영.)
````
- [ ] **Step 2: Flip to default-refute**
In the `## Severity Adjustment` and `Shortcut Trap` sections, change the uncertainty default. Replace the current `INSUFFICIENT_CONTEXT` guidance so that **uncertainty leans toward REJECT/DOWNGRADE, not KEEP**:
- Add to `## Severity Adjustment`:
> **Default-refute (deep-research 정렬):** 세 검사 중 하나라도 확신이 안 서면 KEEP 이 아니라 최소 DOWNGRADE. `INSUFFICIENT_CONTEXT` 는 "판단 보류 후 KEEP" 이 아니라 "근거 부족 → 그 finding 의 원 severity 를 신뢰할 수 없음 → DOWNGRADE 권고" 로 처리한다. KEEP 은 세 검사가 *적극적으로* 통과할 때만.
- [ ] **Step 3: Document the N=3 quorum flow**
Add a new section `## Quorum (opt-in N=3)`:
````
고위험 검증 시 controller 가 이 에이전트를 **독립적으로 N=3 병렬 dispatch** 하고, 각 출력의 `wiki-verdict` 블록을 `wiki_quorum.py` 에 투입한다:
```
python3 .claude/hooks/wiki_quorum.py vote1.md vote2.md vote3.md
```
`wiki_quorum.py` 가 per-finding 결정(KILL/DOWNGRADE/KEEP/UNVERIFIED)을 **결정론적으로** 계산한다(≥2 REJECT=KILL, abstain≠pass). controller 는 임계값을 못 바꾼다. 기본은 N=1(단일 패스).
````
- [ ] **Step 4: Verify block present**
Run: `grep -c "wiki-verdict" .claude/agents/wiki-adversarial-reviewer.md`
Expected: ≥1.
- [ ] **Step 5: Checkpoint (no commit).**
---
## Task 6: Standard 4 judges — verdict block
**Files:**
- Modify: `.claude/agents/branch-depth-auditor.md`, `coverage-auditor.md`, `project-readiness-auditor.md`, `wiki-diagram-reviewer.md`
- [ ] **Step 1: Add the block to each judge's Output section**
In each agent's output template (`## 출력` / `## Output`), directly under the human `Verdict:` line, add the machine block. Use the agent's own name and map its verdict:
````
```wiki-verdict
agent: <이 에이전트 name>
verdict: ready|not-ready|blocked
blocking: <N>
should_fix: <M>
advisory: <K>
```
````
Per-agent mapping note to include inline:
- `branch-depth-auditor`: `verdict=ready` ⟺ Blocking 0; else `not-ready`.
- `coverage-auditor`: `verdict=ready` ⟺ missing(Blocking) 0; else `not-ready`. (`Covered`→ready, `Not-covered`→not-ready.)
- `project-readiness-auditor`: `verdict=ready` ⟺ Blocking 0 (Ready); else `not-ready`.
- `wiki-diagram-reviewer`: `verdict=ready` ⟺ 점수 ≥95 (PASS); `<95`→`not-ready`; BLOCKED→`blocked`. `blocking` = HARD-STOP 수.
- [ ] **Step 2: Verify all four have the block**
Run: `for a in branch-depth-auditor coverage-auditor project-readiness-auditor wiki-diagram-reviewer; do echo -n "$a: "; grep -c "wiki-verdict" .claude/agents/$a.md; done`
Expected: each prints ≥1.
- [ ] **Step 3: Checkpoint (no commit).**
---
## Task 7: 3-platform manual mirror (4 shared judges)
**Files (mirror the SAME block/edits made in Tasks 5-6):**
- `.agents/plugins/wiki-superpowers/agents/{branch-depth-auditor,coverage-auditor,wiki-adversarial-reviewer,wiki-diagram-reviewer}.md`
- `.codex/agents/{branch-depth-auditor,coverage-auditor,wiki-adversarial-reviewer,wiki-diagram-reviewer}.md`
- `.codex/agents/{...}.toml` (the verdict block goes inside the `developer_instructions` string)
- **NOT** `project-readiness-auditor` — Claude-only, no variants exist.
- [ ] **Step 1: Mirror the body changes**
For each of the 4 shared judges, copy the verdict-block addition (and for adversarial, the default-refute + quorum sections) from the `.claude/agents/<name>.md` into the three variant files. Keep each platform's frontmatter/format; only the body content is mirrored.
- [ ] **Step 2: Grep-verify parity**
Run:
```bash
for a in branch-depth-auditor coverage-auditor wiki-adversarial-reviewer wiki-diagram-reviewer; do
echo "== $a =="
grep -l "wiki-verdict" .claude/agents/$a.md .agents/plugins/wiki-superpowers/agents/$a.md .codex/agents/$a.md .codex/agents/$a.toml
done
```
Expected: all 4 files listed for each judge (16 total).
- [ ] **Step 3: Checkpoint (no commit).**
---
## Task 8: Full regression + acceptance smoke (spec §6)
**Files:** none (verification)
- [ ] **Step 1: Run all hook test suites**
Run:
```bash
for t in test_wiki_rules test_wiki_claim_gate test_wiki_structure_lint test_wiki_quorum; do
out=$(python3 .claude/hooks/$t.py 2>&1 | tail -1); echo "$t -> $out"
done
```
Expected: all `OK`.
- [ ] **Step 2: Acceptance §6.4 — SubagentStop live (malformed blocks, valid passes)**
Run (printf to avoid JSON mangling; no `>` redirect to avoid the claim_gate bash-gate):
```bash
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 4\nshould_fix: 0\nadvisory: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py; echo "malformed exit=$?"
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py; echo "valid exit=$?"
```
Expected: `malformed exit=2`, `valid exit=0`.
- [ ] **Step 3: Acceptance §6.5 — wiki_quorum CLI live**
Run (build 3 vote files in /tmp via a /tmp helper to avoid the bash-gate, then tally):
```bash
python3 - <<'PY'
from pathlib import Path
d = Path("/tmp/qsmoke"); d.mkdir(exist_ok=True)
def adv(fid_acts):
lines = "\n".join(f"finding: {f} action: {a}" for f,a in fid_acts)
return f"```wiki-verdict\nagent: wiki-adversarial-reviewer\n{lines}\n```"
(d/"v0.md").write_text(adv([("A","REJECT")]))
(d/"v1.md").write_text(adv([("A","REJECT")]))
(d/"v2.md").write_text(adv([("A","KEEP")]))
print("wrote", d)
PY
python3 .claude/hooks/wiki_quorum.py /tmp/qsmoke/v0.md /tmp/qsmoke/v1.md /tmp/qsmoke/v2.md; echo "quorum exit=$?"
rm -rf /tmp/qsmoke
```
Expected: table with finding `A … KILL`, `quorum exit=1`.
- [ ] **Step 4: Acceptance §6.7 — grep parity**
Run the Task 7 Step 2 grep + the Task 6 Step 2 grep. Expected: every judge file carries `wiki-verdict`.
- [ ] **Step 5: Final checkpoint (no commit)** — report all results. Leave changes in working tree.
---
## Self-Review (completed by plan author)
- **Spec coverage:** §4.1 block format → Tasks 5/6. §4.2 validate+tally → Tasks 1/2. §4.3 SubagentStop → Task 4. §4.4 wiki_quorum.py → Task 3. §4.5 agent edits + mirror → Tasks 5/6/7. §6 acceptance 1-7 → Tasks 1-4 tests + Task 8 smokes. No requirement unmapped.
- **Placeholder scan:** agent-edit tasks give the exact block text + exact mapping per agent + exact mirror file list; no "TBD"/"similar to". Deterministic-core tasks carry full runnable code.
- **Type/name consistency:** `parse_verdict_block`/`validate_verdict_block`/`tally_quorum` names match across Tasks 1-4 and the CLI; the `wiki-verdict` fence string is identical in tests, impl, agent blocks, and smokes; `REFUTATIONS_REQUIRED=2` matches the tally tests (REJECT×2 = KILL).
@@ -0,0 +1,354 @@
# Spec C — Funnel Stats & No-Silent-Truncation Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax.
> **Commits excluded** per user instruction — no `git commit` steps; each task ends with a test-green checkpoint.
**Goal:** Make coverage-bounding outputs report a balanced funnel (`found = processed + dropped`) with a mandatory `dropped_reason`, enforced at SubagentStop for the 4 key judge/research agents — so silent truncation becomes visible (audit gap G4).
**Architecture:** Extend the shared `wiki_rules.py` with `validate_stats_block` (funnel-balance + dropped-reason). `wiki_claim_gate.subagent_stop_gate` validates any subagent output carrying a `wiki-stats` marker (reuses the Spec B SubagentStop path). Add a no-silent-truncation contract to `rules/reporting-standards.md`; add a `## Stats` `wiki-stats` block to 4 agents + a `## Stats` funnel to `/ingest`. Deterministic core unit-tested; agent prose mirrored 3-platform by hand.
**Tech Stack:** Python 3 stdlib (`re`, `json`, `unittest`, `subprocess`). Claude Code hooks. Markdown agent/command specs.
**Spec:** `docs/superpowers/specs/2026-06-06-spec-c-funnel-stats-no-silent-truncation-design.md`
---
## File Structure
- **Modify** `.claude/hooks/wiki_rules.py``parse_stats_block`, `validate_stats_block`, `STATS_FENCE_RE`.
- **Modify** `.claude/hooks/test_wiki_rules.py``TestStatsBlock`.
- **Modify** `.claude/hooks/wiki_claim_gate.py``subagent_stop_gate` validates `wiki-stats`.
- **Modify** `.claude/hooks/test_wiki_claim_gate.py``TestSubagentStopStats`.
- **Modify** `rules/reporting-standards.md` — "No silent truncation" 절.
- **Modify** `.claude/agents/{coverage-auditor,branch-depth-auditor,wiki-decision-researcher,wiki-research-lane}.md``## Stats` block.
- **Modify** `.claude/commands/ingest.md``## Stats` funnel 계약.
- **Mirror (manual)** the 4 agents → `.agents/plugins/wiki-superpowers/agents/<name>.md` (G3) + `.codex/agents/<name>.md`/`.toml`.
---
## Task 1: `wiki_rules` — stats block parse + validate
**Files:**
- Modify: `.claude/hooks/wiki_rules.py`
- Test: `.claude/hooks/test_wiki_rules.py` (append `TestStatsBlock`)
- [ ] **Step 1: Write the failing test**
Append to `test_wiki_rules.py` before `if __name__`:
```python
def _stats(found, processed, dropped, reason=None):
body = f"agent: coverage-auditor\nfound: {found}\nprocessed: {processed}\ndropped: {dropped}"
if reason is not None:
body += f"\ndropped_reason: {reason}"
return f"```wiki-stats\n{body}\n```"
class TestStatsBlock(unittest.TestCase):
def test_no_marker_returns_none(self):
parsed, errors = wr.validate_stats_block("산문, 마커 없음")
self.assertIsNone(parsed)
self.assertEqual(errors, [])
def test_balanced_ok(self):
parsed, errors = wr.validate_stats_block(_stats(12, 10, 2, "2 out-of-scope"))
self.assertEqual(errors, [])
self.assertEqual(parsed["agent"], "coverage-auditor")
def test_imbalance_flagged(self):
_, errors = wr.validate_stats_block(_stats(12, 10, 0))
self.assertTrue(any("불균형" in e for e in errors))
def test_dropped_without_reason_flagged(self):
_, errors = wr.validate_stats_block(_stats(12, 10, 2))
self.assertTrue(any("dropped_reason" in e for e in errors))
def test_non_integer_flagged(self):
block = "```wiki-stats\nagent: x\nfound: many\nprocessed: 1\ndropped: 0\n```"
_, errors = wr.validate_stats_block(block)
self.assertTrue(any("정수" in e for e in errors))
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 .claude/hooks/test_wiki_rules.py -k Stats`
Expected: FAIL — `AttributeError: ... 'validate_stats_block'`.
- [ ] **Step 3: Implement in `wiki_rules.py`**
Append (after the `tally_quorum` function):
```python
STATS_FENCE_RE = re.compile(r"```wiki-stats\s*\n(.*?)\n```", re.S)
def parse_stats_block(text):
m = STATS_FENCE_RE.search(text or "")
if not m:
return None
out = {"agent": None, "kv": {}}
for line in m.group(1).splitlines():
line = line.strip()
if not line:
continue
kv = re.match(r"([a-z_]+):\s*(.+)$", line)
if kv:
k, v = kv.group(1), kv.group(2).strip()
if k == "agent":
out["agent"] = v
else:
out["kv"][k] = v
return out
def validate_stats_block(text):
"""(parsed, errors). parsed None → 마커 없음(통과). errors → SubagentStop 차단.
funnel 균형(found=processed+dropped) + dropped>0 시 dropped_reason 필수 (no-silent-truncation)."""
parsed = parse_stats_block(text)
if parsed is None:
return None, []
errors = []
if not parsed["agent"]:
errors.append("wiki-stats 블록에 `agent:` 누락")
nums = {}
for k in ("found", "processed", "dropped"):
try:
nums[k] = int(parsed["kv"].get(k, ""))
except ValueError:
errors.append(f"wiki-stats `{k}` 는 정수여야 함 (funnel 필수 필드)")
if len(nums) == 3:
if nums["found"] != nums["processed"] + nums["dropped"]:
errors.append(
f"funnel 불균형: found({nums['found']}) ≠ processed({nums['processed']}) "
f"+ dropped({nums['dropped']}) — 조용한 누락 의심"
)
if nums["dropped"] > 0 and not parsed["kv"].get("dropped_reason", "").strip():
errors.append("dropped>0 인데 `dropped_reason` 누락 (no-silent-truncation 위반)")
return parsed, errors
```
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 .claude/hooks/test_wiki_rules.py -k Stats`
Expected: PASS (5 tests).
- [ ] **Step 5: Checkpoint (no commit)**`python3 .claude/hooks/test_wiki_rules.py` all green.
---
## Task 2: `claim_gate.subagent_stop_gate` — wiki-stats enforcement
**Files:**
- Modify: `.claude/hooks/wiki_claim_gate.py` (`subagent_stop_gate`)
- Test: `.claude/hooks/test_wiki_claim_gate.py` (append `TestSubagentStopStats`)
- [ ] **Step 1: Write the failing test**
Append (the `_run_stop` helper already exists from Spec B):
```python
class TestSubagentStopStats(unittest.TestCase):
def test_imbalanced_stats_blocks(self):
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 0\n```"
self.assertEqual(_run_stop(msg).returncode, 2)
def test_balanced_stats_allows(self):
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 10\nprocessed: 10\ndropped: 0\n```"
self.assertEqual(_run_stop(msg).returncode, 0)
def test_dropped_without_reason_blocks(self):
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 2\n```"
self.assertEqual(_run_stop(msg).returncode, 2)
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopStats`
Expected: FAIL — `test_imbalanced_stats_blocks` returns 0 (gate not checking stats yet).
- [ ] **Step 3: Implement — extend `subagent_stop_gate`**
In `.claude/hooks/wiki_claim_gate.py`, add a stats check right after the verdict-block check (before the final `emit_allow()`):
```python
# judge verdict 검사(위) 다음 — wiki-stats 마커가 있으면 funnel 검증(없으면 통과).
sparsed, serr = wiki_rules.validate_stats_block(message)
if sparsed is not None and serr and not event.get("stop_hook_active"):
emit_block("wiki-stats 블록 오류:\n- " + "\n- ".join(serr))
emit_allow()
```
(Replace the existing trailing `emit_allow()` with the block above so the stats check precedes it.)
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 .claude/hooks/test_wiki_claim_gate.py -k SubagentStopStats`
Expected: PASS (3 tests).
- [ ] **Step 5: Checkpoint (no commit)**`python3 .claude/hooks/test_wiki_claim_gate.py` all green.
---
## Task 3: `rules/reporting-standards.md` — No silent truncation 계약
**Files:**
- Modify: `rules/reporting-standards.md`
- [ ] **Step 1: Append the contract section**
Add at the end of `rules/reporting-standards.md`:
```markdown
## No silent truncation (funnel 계약)
출력이 캡/슬라이스/top-N/skip 으로 coverage 를 bound 하면 **드롭한 수 + 이유**를 반드시 보고한다. funnel 은 균형해야 한다:
```
found = processed + dropped
```
- `found` = 식별한 총 항목. `processed` = 실제 판정한 수(결과 무관 — covered/missing/verified/promoted 모두 포함). `dropped` = 판정하지 않고 의도 제외(이유 필수).
- **agent 출력**은 `wiki-stats` 블록으로 보고한다(SubagentStop 이 균형·dropped_reason 검증).
- **command 출력**은 `## Stats` 절로 보고한다.
- 침묵 누락은 "전부 다뤘다" 는 거짓 신호다 — 제3의 보고되지 않은 버킷을 두지 않는다.
```
- [ ] **Step 2: Verify**
Run: `grep -c "No silent truncation" rules/reporting-standards.md`
Expected: ≥1.
- [ ] **Step 3: Checkpoint (no commit).**
---
## Task 4: 4 agents — `## Stats` block (Claude)
**Files:**
- Modify: `.claude/agents/coverage-auditor.md`, `branch-depth-auditor.md`, `wiki-decision-researcher.md`, `wiki-research-lane.md`
- [ ] **Step 1: Add the block to each agent's Output section**
In each agent's Output section (for coverage-auditor / branch-depth-auditor, place it **directly after the `## Machine verdict` block** added in Spec B; for decision-researcher / research-lane place it at the end of the Output template), add — with the agent's own name and an example funnel:
````
## Stats (funnel — SubagentStop 가 균형·dropped_reason 검증)
리포트 끝에 기계 파싱용 funnel 을 **반드시** 방출한다. `found = processed + dropped` 균형 필수, `dropped>0` 면 `dropped_reason` 필수:
```wiki-stats
agent: <이 에이전트 name>
found: 12
processed: 10
dropped: 2
dropped_reason: 2 out-of-scope (사유)
```
````
Per-agent funnel 의미:
- `coverage-auditor`: found=governing 관심사 수, processed=covered+delegated+missing, dropped=범위 밖(이유).
- `branch-depth-auditor`: found=점검한 claim/결정 수, processed=판정 완료, dropped=범위 밖(이유).
- `wiki-decision-researcher`: found=식별 후보 수, processed=archive 한 수, dropped=bound(N) 초과 제외(이유).
- `wiki-research-lane`: found=슬라이스 파일 수, processed=정독+추출, dropped=무관/제외(이유).
- [ ] **Step 2: Verify all four**
Run: `for a in coverage-auditor branch-depth-auditor wiki-decision-researcher wiki-research-lane; do echo -n "$a: "; grep -c "wiki-stats" .claude/agents/$a.md; done`
Expected: each ≥1.
- [ ] **Step 3: Checkpoint (no commit).**
---
## Task 5: `/ingest` — `## Stats` funnel 계약
**Files:**
- Modify: `.claude/commands/ingest.md`
- [ ] **Step 1: Append the funnel contract**
Add a new section at the end of `.claude/commands/ingest.md`:
```markdown
## 출력: Stats funnel (no-silent-truncation)
작업 종료 시 `## Stats` 절을 보고한다 (`rules/reporting-standards.md` No silent truncation 계약):
```
## Stats
found: <식별한 promotable 항목 수>
processed: <canonical 로 promote 한 수>
dropped: <추출 안 한 수>
dropped_reason: <항목별 제외 사유 (raw 보존 / 잡담 / abandoned / 등)>
```
`found = processed + dropped` 균형 필수. daily/branch 특수처리에서 "추출 안 함" 으로 raw 에 남긴 항목도 `dropped` 에 카운트하고 사유를 적는다 — 무엇을 안 옮겼는지 보이게.
```
- [ ] **Step 2: Verify**
Run: `grep -c "Stats funnel" .claude/commands/ingest.md`
Expected: ≥1.
- [ ] **Step 3: Checkpoint (no commit).**
---
## Task 6: 3-platform manual mirror (4 agents)
**Files (mirror the `## Stats` block from Task 4):**
- `.agents/plugins/wiki-superpowers/agents/{coverage-auditor,branch-depth-auditor,wiki-decision-researcher,wiki-research-lane}.md` — integrate into the G3 Output Schema (`{{ }}` placeholder style, framed "형식 외 응답 금지").
- `.codex/agents/{...}.md` and `.codex/agents/{...}.toml` — same block as Claude (plain body, inside `developer_instructions` for `.toml`).
- [ ] **Step 1: Mirror**
For each of the 4 agents, copy the `## Stats` block into the variant files. Antigravity uses `{{ }}` placeholders (e.g. `found: {{N}}`); Codex uses the same concrete-example body as Claude. (Note: `wiki-decision-researcher` / `wiki-research-lane` — confirm they have Antigravity/Codex variants; mirror only those that exist.)
- [ ] **Step 2: Grep-verify parity**
Run:
```bash
for a in coverage-auditor branch-depth-auditor wiki-decision-researcher wiki-research-lane; do
echo "== $a =="
grep -l "wiki-stats" .claude/agents/$a.md .agents/plugins/wiki-superpowers/agents/$a.md .codex/agents/$a.md .codex/agents/$a.toml 2>/dev/null
done
```
Expected: every existing variant lists for each agent.
- [ ] **Step 3: Checkpoint (no commit).**
---
## Task 7: Full regression + acceptance smoke (spec §6)
**Files:** none (verification)
- [ ] **Step 1: All hook test suites**
Run:
```bash
for t in test_wiki_rules test_wiki_claim_gate test_wiki_structure_lint test_wiki_quorum; do
out=$(python3 .claude/hooks/$t.py 2>&1 | tail -1); echo "$t -> $out"
done
```
Expected: all `OK`.
- [ ] **Step 2: Acceptance §6.2 — live SubagentStop stats gate**
Run (printf; no `>` redirect to avoid the bash-gate):
```bash
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py 2>/dev/null; echo "imbalance exit=$?"
printf '%s' '{"hook_event_name":"SubagentStop","last_assistant_message":"x\n```wiki-stats\nagent: coverage-auditor\nfound: 10\nprocessed: 10\ndropped: 0\n```"}' | python3 .claude/hooks/wiki_claim_gate.py 2>/dev/null; echo "balanced exit=$?"
```
Expected: `imbalance exit=2`, `balanced exit=0`.
- [ ] **Step 3: Acceptance §6.3-6.5 — grep contracts**
Run:
```bash
grep -c "No silent truncation" rules/reporting-standards.md
for a in coverage-auditor branch-depth-auditor wiki-decision-researcher wiki-research-lane; do echo -n "$a: "; grep -c wiki-stats .claude/agents/$a.md; done
grep -c "Stats funnel" .claude/commands/ingest.md
```
Expected: reporting-standards ≥1; each agent ≥1; ingest ≥1.
- [ ] **Step 4: Final checkpoint (no commit)** — report results; leave changes in working tree.
---
## Self-Review (completed by plan author)
- **Spec coverage:** §4.2 validate_stats_block → Task 1. §4.3 SubagentStop → Task 2. §4.4 reporting-standards → Task 3. §4.5 agents → Tasks 4+6. §4.6 ingest → Task 5. §6 acceptance 1-6 → Task 1/2 tests + Task 7 smokes. No gap.
- **Placeholder scan:** deterministic-core tasks carry full code; agent/command tasks give exact block text + per-agent mapping + exact mirror file list. No "TBD"/"similar to".
- **Type/name consistency:** `parse_stats_block`/`validate_stats_block` names match Tasks 1-2 and tests; the `wiki-stats` fence + funnel fields (`found`/`processed`/`dropped`/`dropped_reason`) identical across spec, impl, tests, agent blocks, smokes; `found = processed + dropped` invariant consistent everywhere.
@@ -0,0 +1,218 @@
# Spec D — research-fanout Workflow Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax.
> **Commits excluded** per user instruction. **OPT-IN BOUNDARY:** Tasks marked `[OPT-IN]` require the `Workflow` tool (explicit opt-in / "ultracode") to *run*; Task 4 (prose edit) needs no opt-in and can run anytime.
**Goal:** Provide a Claude-only `research-fanout` Workflow that runs the bounded per-decision alternatives-research burst (branch-spec §5) as a real deep-research-style pipeline — parallel `agent({schema})` + cap + deferred + funnel stats — without disturbing the 3-platform prose command.
**Architecture:** A standalone Workflow script (`.claude/workflows/research-fanout.js`) that takes `args.decisions = [{topic, parentBranch, constraints, n}]`, caps at 6, fans out autonomous schema-enforced web research in parallel, and returns comparison matrices + `branchNoteInput` + funnel `stats`. The prose `/branch-spec §5` gains one optional line pointing to it (Claude only). Codex/Antigravity keep sequential dispatch — no parity change.
**Tech Stack:** Claude Code `Workflow` tool (JS script, not TypeScript), `agent()`/`parallel()`/`phase()`/`log()`. WebSearch/WebFetch inside agents.
**Spec:** `docs/superpowers/specs/2026-06-06-spec-d-research-fanout-workflow-design.md`
---
## File Structure
- **Create** `.claude/workflows/research-fanout.js` — the Workflow script (spec §4). `[OPT-IN]`
- **Modify** `.claude/commands/branch-spec.md` — one optional line in §5 (Claude-only Workflow call). (no opt-in)
> Invocation note: at opt-in, run via `Workflow({name: 'research-fanout', args: {...}})` if the file is registered as a named workflow, or `Workflow({scriptPath: '.claude/workflows/research-fanout.js', args: {...}})`. Confirm the registration path the first run (the Workflow tool persists inline scripts and reports the path).
---
## Task 1 `[OPT-IN]`: Author the Workflow script
**Files:**
- Create: `.claude/workflows/research-fanout.js`
- [ ] **Step 1: Write the script verbatim from spec §4**
Create `.claude/workflows/research-fanout.js` with exactly the spec §4 script:
```js
export const meta = {
name: 'research-fanout',
description: 'Bounded parallel alternatives-research for N branch decisions (autonomous, schema-enforced)',
phases: [
{ title: 'Scope', detail: 'validate + cap decisions (max 6), split deferred' },
{ title: 'Research', detail: 'parallel autonomous web research per decision, schema-enforced' },
{ title: 'Synthesize', detail: 'comparison matrices + funnel stats + branch-note DEM input' },
],
}
const MAX_DECISIONS = 6
const ALT_SCHEMA = {
type: "object", required: ["decision", "alternatives", "recommendation", "confidence"],
properties: {
decision: { type: "string" },
alternatives: { type: "array", minItems: 2, maxItems: 5, items: {
type: "object", required: ["name", "pros", "cons", "sources"],
properties: {
name: { type: "string" },
pros: { type: "string" },
cons: { type: "string" },
sources: { type: "array", items: { type: "string" } },
quote: { type: "string" },
sourceType: { enum: ["official-doc", "company-tech-blog", "personal-blog", "unknown"] },
},
}},
recommendation: { type: "string" },
confidence: { enum: ["high", "medium", "low"] },
unsupported: { type: "boolean" },
},
}
phase("Scope")
const decisions = (args && Array.isArray(args.decisions)) ? args.decisions : []
if (!decisions.length) {
return { error: "No decisions. Pass args.decisions = [{topic, parentBranch, constraints, n}]." }
}
const capped = decisions.slice(0, MAX_DECISIONS)
const deferred = decisions.slice(MAX_DECISIONS)
log(`${decisions.length} decisions → research ${capped.length}, defer ${deferred.length}`)
const researchPrompt = (d) =>
"## Alternatives Researcher (autonomous)\n\n" +
"Branch decision: \"" + d.topic + "\"\n" +
"Parent branch: " + (d.parentBranch || "(none)") + "\n" +
"Constraints: " + (d.constraints || "(none)") + "\n\n" +
"## Task\n" +
"1. WebSearch official docs + 대기업 기술블로그 for " + (d.n || 3) + " viable alternatives.\n" +
"2. WebFetch each; extract Pros/Cons + a verbatim quote + source URL + sourceType.\n" +
"3. company-tech-blog 만으로 '공식 best practice' 승격 금지(독립 사례 2+ 또는 official 병행).\n" +
"4. 근거가 한쪽으로 명확하면 그대로 recommendation. 가짜 5:5 균형 금지.\n" +
"5. 조사 후에도 근거 부족하면 unsupported=true.\n\nStructured output only."
phase("Research")
const results = (await parallel(
capped.map(d => () =>
agent(researchPrompt(d), { label: "research:" + d.topic.slice(0, 30), phase: "Research", schema: ALT_SCHEMA })
.then(r => r ? { ...r, topic: d.topic } : null)
)
)).filter(Boolean)
phase("Synthesize")
const failures = capped.length - results.length
const stats = {
found: decisions.length,
processed: results.length,
dropped: deferred.length + failures,
dropped_reason: [
deferred.length ? `${deferred.length} over cap(${MAX_DECISIONS})` : null,
failures ? `${failures} research failed/skipped` : null,
].filter(Boolean).join("; ") || "none",
}
log(`done: ${results.length} researched, ${stats.dropped} dropped`)
return {
matrices: results,
deferred: deferred.map(d => d.topic),
unsupported: results.filter(r => r.unsupported).map(r => r.topic),
stats,
branchNoteInput: results.map(r => ({
decision: r.topic, recommendation: r.recommendation, confidence: r.confidence,
sources: r.alternatives.flatMap(a => a.sources), unsupported: !!r.unsupported,
})),
}
```
- [ ] **Step 2: Syntax sanity (no opt-in needed — node parse only)**
Run: `node --check .claude/workflows/research-fanout.js && echo "syntax OK"`
Expected: `syntax OK`. (Note: `node --check` validates JS syntax; the Workflow runtime — `agent`/`parallel`/`phase`/`log`/`args` globals — is provided by the Workflow tool, not node, so this only checks parse-ability, not execution.)
If `node` is unavailable, skip and rely on the Workflow tool's own parse at run time.
- [ ] **Step 3: Checkpoint (no commit).**
---
## Task 2 `[OPT-IN]`: Run — happy path (2-3 decisions)
**Files:** none (Workflow execution)
- [ ] **Step 1: Invoke the Workflow with a small decisions list**
Via the `Workflow` tool:
```
Workflow({ name: 'research-fanout', args: { decisions: [
{ topic: "outbox polling vs CDC for transactional outbox", parentBranch: "feature-domain-event-outbox-contract", constraints: "Spring Boot, Postgres", n: 3 },
{ topic: "idempotency key storage: dedicated table vs redis", parentBranch: "feature-idempotency-key", constraints: "at-least-once delivery", n: 3 }
]}})
```
(If `name` resolution fails, use `scriptPath: '.claude/workflows/research-fanout.js'`.)
- [ ] **Step 2: Verify acceptance §6.1, §6.4**
Expected return: `matrices` length 2; each matrix has `alternatives` ≥2 with `sources` arrays + `recommendation` + `confidence`; `stats.found == 2` and `stats.processed + stats.dropped == 2`.
- [ ] **Step 3: Verify §6.3 — empty input graceful**
Invoke `Workflow({ name: 'research-fanout', args: { decisions: [] }})`.
Expected: `{ error: "No decisions. ..." }` (no crash).
- [ ] **Step 4: Checkpoint (no commit).**
---
## Task 3 `[OPT-IN]`: Run — cap + deferred (8 decisions)
**Files:** none
- [ ] **Step 1: Invoke with 8 decisions**
`Workflow({ name: 'research-fanout', args: { decisions: [ /* 8 objects, each {topic, n:2} */ ]}})`.
- [ ] **Step 2: Verify acceptance §6.2**
Expected: `matrices` length ≤6; `deferred` length 2 (the 7th, 8th topics); `stats.dropped >= 2` and `stats.dropped_reason` contains `over cap(6)`; `stats.found == 8` and `processed + dropped == 8`.
- [ ] **Step 3: Checkpoint (no commit).**
---
## Task 4 (no opt-in): Add the optional call line to `/branch-spec §5`
**Files:**
- Modify: `.claude/commands/branch-spec.md` (§5, after the bound line)
- [ ] **Step 1: Add the Claude-only option line**
In `.claude/commands/branch-spec.md`, in step 5 (after the line ` - 조사는 **개수가 아니라 근거** ...`), add:
```markdown
- **(옵션, Claude 전용)** 결정 수가 많거나 빠른 병렬 조사를 원하면 `research-fanout` Workflow 를 호출한다 (`Workflow({name:'research-fanout', args:{decisions:[{topic,parentBranch,constraints,n}, ...]}})`, ultracode/opt-in 필요). 반환된 `matrices`/`branchNoteInput` 으로 §7 Decision Evidence Map 을 채우고 `stats`/`deferred` 를 §9 요약에 반영한다. **autonomous 조사라 user-approval 이 없으므로** 승인-gated 가 필요하면 기존 `wiki-decision-researcher` 순차 dispatch 를 쓴다. Codex/Antigravity 는 항상 순차 dispatch (Workflow 는 Claude 전용).
```
- [ ] **Step 2: Verify**
Run: `grep -c "research-fanout" .claude/commands/branch-spec.md`
Expected: ≥1.
- [ ] **Step 3: 3-platform note (no edit)**
`/branch-spec` 의 Codex/Antigravity variant 에는 이 줄을 **미러하지 않는다** (Workflow 는 Claude 전용). 미러 생략이 의도임을 확인만 한다 — variant 는 기존 순차 dispatch 유지.
- [ ] **Step 4: Checkpoint (no commit).**
---
## Task 5 `[OPT-IN]`: Full acceptance smoke
**Files:** none
- [ ] **Step 1: Re-confirm §6.1-6.6**
- §6.1/6.4 (Task 2), §6.2 (Task 3), §6.3 (Task 2 Step 3) — done.
- §6.5: inspect a Task 2 return — `branchNoteInput[i]` has `{decision, recommendation, sources, unsupported}` usable for prose §7 fill.
- §6.6: confirm `/branch-spec` prose sequential path (without Workflow) is unchanged — read §5, verify the original `wiki-decision-researcher` dispatch line is intact and the new line is clearly "옵션".
- [ ] **Step 2: Report results; leave changes in working tree (no commit).**
---
## Self-Review (completed by plan author)
- **Spec coverage:** §4 script → Task 1. §4.1 prose line → Task 4. §6 acceptance 1-6 → Tasks 2/3/5. §7 opt-in boundary → `[OPT-IN]` tags + Task 4 non-opt-in. No gap.
- **Placeholder scan:** Task 1 carries the full verbatim script; Task 4 carries the exact line; run tasks give exact `Workflow({...})` invocations + expected returns. The only intentional deferral is execution (opt-in), explicitly flagged — not a placeholder.
- **Type/name consistency:** `decisions`/`topic`/`parentBranch`/`constraints`/`n` arg shape consistent across script, invocations, and `branchNoteInput`; `stats.{found,processed,dropped,dropped_reason}` matches the C-spec funnel contract; `MAX_DECISIONS=6` matches the "8→6+2 deferred" acceptance.
- **Note:** Task 4 is the only non-opt-in change; if the user never opts into Workflow, the prose command still gains a (currently-dormant) Claude-only pointer that degrades gracefully to the existing sequential path.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,241 @@
# 자금흐름 관측 시스템 — 완성까지의 마스터 빌드아웃 플랜
> **이 플랜의 약속:** 아래 Phase 0~6 을 전부 완수하면, "전체 주식시장 돈의 흐름 → 분야 → 대장주 → 추종주 → 분야간 상승·하락 연관"을 매일 체계적으로 관측하는 구조가 *완성*된다. 더 이상 즉흥적으로 하나씩 늘리지 않는다 — **목표 구조가 유한하게 정의돼 있고, 단계로 채운다.**
>
> 프로젝트 hub: `[[raw/project-notes/invest-money-flow-system]]` · 설계: `[[docs/superpowers/specs/2026-06-08-invest-field-map-design]]`
---
## 0. 완성 정의 / Definition of Done (전체 시스템)
시스템이 "완성됐다"고 말할 수 있는 **측정가능 조건 6개**:
1. **분야 지도**에 거시 자산군(L0) + 한국 주요 섹터/테마(L1) 가 **목표 분류표(§2)대로 전부 카드로 존재**한다.
2. 모든 **산업 섹터 카드**가 *대장주 1~3 + 추종주 2~5* 표를 가진다.
3. **분야간 로테이션 지도**(`field-rotation`)가 존재하고, "risk-on/off · 금리 · 달러 · 경기 사이클"별 *돈이 빠지고 들어가는 연쇄*가 행으로 명시된다.
4. 모든 관계 행에 `[검증]/[가설]` 라벨. **`[검증]` ≥ 10개** (관측·research로 승급된 실제 관계).
5. **매일 `/invest-daily` 가 분야 관찰**(대장주↔추종주 + 로테이션 예측 vs 실측)을 채점한다 — 14일+ 연속 운영 기록.
6. 검증된 분야부터 **매수신호 규칙**이 전략과 연동된다(`[[wiki/invest-strategy/strategy]]`).
> 1~3 = 구조, 4~6 = 운영. 구조 먼저(Phase 1~2), 운영 누적(Phase 3~5), 신호(Phase 6).
---
## 1. 문서 관리 규칙 / Document Management (전체 SSOT 맵)
> "어디에 무엇을 두는가"를 고정한다. 이게 흔들리면 또 piecemeal·중복이 된다.
| 레이어 | 위치 | 무엇 | 명령 |
|---|---|---|---|
| **프로젝트 hub** | `raw/project-notes/invest-money-flow-system.md` | 전체 설계·아키텍처·로드맵. 모든 것의 entry point | (수동/`/project-spec`) |
| **분야 카드(개념)** | `wiki/invest-concepts/field-<slug>.md` | 분야 노드: drivers·연결·대장주/추종주·관찰지표 | `/invest-ingest` |
| **분야 지도 허브** | `wiki/invest-concepts/field-map.md` | 카드 목차(2층) | (수동) |
| **로테이션 지도** | `wiki/invest-concepts/field-rotation.md` | 분야간 돈 흐름 연쇄 | `/invest-ingest` |
| **전략 규칙** | `wiki/invest-strategy/strategy.md` | 매매·리스크 규칙 | `/invest-ingest` |
| **활성 계획** | `wiki/invest-plan/active-plan.md` | 현재 포지션 계획 | `/invest-plan` |
| **일일 관측(증거)** | `raw/invest-daily/YYYY-MM-DD.md` | 매일 예측 vs 실측 채점 | `/invest-daily` |
| **심층 조사(증거)** | `raw/invest-research/<slug>.md` | 관계·종목 3표 검증 | `/invest-research` |
| **매매 원장(사실)** | `raw/invest-ledger/ledger.md` | 실제 거래 | `/invest-decide` |
| **cluster 색인** | `wiki/invest/invest-hub.md` | 가벼운 목차(폴더 안내) | (수동) |
**규칙:**
- **명명**: 분야 카드 = `field-<영문-kebab>`. 로테이션 = `field-rotation`. 한글 슬러그 금지(일관성).
- **링크**: 카드 `## Parent``field-map``invest-hub` + 본 프로젝트 hub. 조사(research)는 프로젝트 hub §8.2 로 연결.
- **근거 규율**: 모든 관계/종목 행에 `[검증]/[가설]`. `[가설]` = 외부 사용·단정 금지. 검증은 `/invest-research`(3표) → `/invest-ingest` 로만 `[검증]` 승급.
- **링크 순환 주의**: 새 카드 만들 때 *대상이 아직 없는* wikilink 는 백틱으로 감싸고(린터 무시), 대상 생성 후 백틱 제거(설계 spec 참조).
---
## 2. 완성 목표 구조 / Target Structure (유한 목록)
### 2.1 4층 + 로테이션 모델
```
L0 거시 자산군 ──인과/상관──> L1 산업 섹터/테마 ──대장주 견인──> L2 대장주 ──동조 낙수──> L3 추종주
└──────────────── 분야간 로테이션(돈이 빠져 옮겨감) ────────────────┘
```
### 2.2 거시 자산군 L0 (목표 8) — 현재 6 ✅
| slug | 분야 | 상태 |
|---|---|---|
| `field-dollar` | 달러 | ✅ |
| `field-us-rates` | 미 10Y 금리 | ✅ |
| `field-oil` | 원유 | ✅ |
| `field-gold` | 금 | ✅ |
| `field-us-equity` | 미국 주식(S&P500) | ✅ |
| `field-bitcoin` | 비트코인 | ✅ |
| `field-krw-rates` | 한국 금리·원화채 | ⬜ Phase 1 |
| `field-em-china` | 신흥국·중국(위험선호 척도) | ⬜ Phase 1 |
### 2.3 한국 산업 섹터/테마 L1 (목표 ~16) — 현재 6 ✅ + 빅테크AI
| slug | 분야 | 대장주(예시·가설) | 상태 |
|---|---|---|---|
| `field-semiconductors` | 반도체 | 엔비디아·SK하이닉스 | ✅ |
| `field-bigtech-ai` | 빅테크/AI(글로벌) | 매그니피센트7 | ✅ |
| `field-secondary-battery` | 2차전지 | LG엔솔·에코프로비엠 | ✅ |
| `field-defense` | 방산 | 한화에어로스페이스 | ✅ |
| `field-shipbuilding` | 조선 | HD현대중공업 | ✅ |
| `field-bio-pharma` | 바이오·제약 | 삼성바이오·셀트리온 | ✅ |
| `field-internet-platform` | 인터넷·플랫폼 | 네이버·카카오 | ✅ |
| `field-auto` | 자동차·부품 | 현대차·기아 | ⬜ Phase 1 |
| `field-financials` | 금융(은행·증권·보험) | KB·신한·삼성생명 | ⬜ Phase 1 |
| `field-steel-materials` | 철강·소재 | POSCO홀딩스 | ⬜ Phase 1 |
| `field-chem-refining` | 화학·정유 | LG화학·S-Oil | ⬜ Phase 1 |
| `field-nuclear-power` | 원자력·전력설비 | 두산에너빌리티 | ⬜ Phase 1 |
| `field-robotics` | 로봇·자동화 | 두산로보틱스·레인보우 | ⬜ Phase 1 |
| `field-game` | 게임 | 크래프톤·엔씨 | ⬜ Phase 1 |
| `field-entertainment` | 엔터·미디어·콘텐츠 | 하이브·JYP | ⬜ Phase 1 |
| `field-cosmetics-consumer` | 화장품·소비재 | 아모레·LG생건 | ⬜ Phase 1 |
| `field-telecom-utility` | 통신·유틸리티(방어주) | SKT·한전 | ⬜ Phase 1 |
> 위 17개가 **한국 시장 "전체 큰 분야"의 목표 집합**(필요시 가감). 이걸 다 채우면 "분야가 9개뿐"이 아니라 시장 전 분야가 지도에 들어온다.
### 2.4 분야간 로테이션 지도 `field-rotation` (Phase 2 핵심)
> 당신이 말한 "각 분야별 상승·하락 연관" = 돈이 *어디서 빠져 어디로 가는가*. 1장의 로테이션 카드에 4개 축으로 정리(전부 `[가설]`로 시작, 검증 대상).
| 축 | 조건 | 빠지는 쪽(↓) | 들어가는 쪽(↑) | 검증/가설 |
|---|---|---|---|---|
| 위험선호 | risk-on | 금·달러·방어주(통신) | 반도체·2차전지·코인·성장주 | `[가설]` |
| 위험회피 | risk-off | 성장주·코인 | 금·달러·방산·통신·유틸 | `[가설]` |
| 금리 | 금리↑ | 성장주(바이오·인터넷·2차전지) | 금융·가치·경기방어 | `[가설]` |
| 금리 | 금리↓ | 금융 | 성장주·바이오·부동산 | `[가설]` |
| 달러 | 달러↑ | 신흥국·원자재·금 | 미국자산 | `[가설]` |
| 경기 | 회복초입 | 방어주 | 반도체·조선·철강·화학(경기민감) | `[가설]` |
| 경기 | 둔화 | 경기민감 | 통신·유틸·필수소비(방어) | `[가설]` |
각 행은 관련 field 카드로 wikilink. `/invest-daily` 가 매일 "오늘 이 로테이션이 실제로 일어났나" 채점.
---
## 3. 단계별 로드맵 / Phased Roadmap
> 각 Phase = 산출물 + 측정가능 완료조건 + 사용하는 명령. 순서대로.
### Phase 0 — 토대 (✅ 완료)
- **산출물**: 카드 템플릿 + field-map 허브 + 거시 6 + 한국 섹터 6 + 대장주/추종주 칸 + invest-daily 분야관찰 + 프로젝트 hub.
- **완료조건**: 13카드 링크 무결성 OK, 모두 `[가설]` 라벨. ✅
### Phase 1 — 분야 분류 완성 (구조)
- **산출물**: §2.2 거시 +2(`field-krw-rates`·`field-em-china`), §2.3 한국 섹터 +10 카드(자동차·금융·철강·화학·원자력·로봇·게임·엔터·화장품·통신유틸). 각 대장주/추종주 `[가설]` 스캐폴드.
- **완료조건**: field-map 에 거시 8 + 섹터 17 = **25 카드 등재**, 전부 링크 OK + `[가설]` 라벨.
- **명령/방식**: 카드 템플릿 복사 → `[가설]` 빠른 스캐폴드(이번 한국 섹터 6개와 동일 방식). 배치로 5~6장씩.
### Phase 2 — 분야간 로테이션 지도 (구조의 마지막 조각)
- **산출물**: `wiki/invest-concepts/field-rotation.md` (§2.4 표) + field-map 에 등재 + 각 행이 관련 카드 wikilink.
- **완료조건**: 4축(위험선호/금리/달러/경기) 로테이션이 행으로 명시, 각 행 관련 카드 연결, `[가설]` 라벨.
- **여기까지 = 목표 *구조* 완성.** 이후는 운영.
### Phase 3 — 일일 관측 운영 (데이터 누적)
- **산출물**: `/invest-daily` 를 매일(또는 거래일) 실행 → 분야 관찰에 대장주↔추종주 + 로테이션 예측 vs 실측 채점 누적.
- **완료조건**: `raw/invest-daily/`**14개+ 일일 노트**, 각 노트 분야관찰 채워짐.
- **명령**: `/invest-daily` (Phase 0에서 명령 보강 완료 — 분야관찰 채우기 포함).
### Phase 4 — 검증 승급 (가설 → 사실)
- **산출물**: 관측에서 반복 확인된 관계/대장주-추종주 동조를 `/invest-research`(3표)로 검증 → `/invest-ingest`로 카드 `[가설]``[검증]`.
- **완료조건**: 시스템 전체 `[검증]` ≥ 10개.
- **명령**: `/invest-research "<관계>"``/invest-ingest <research경로>`. 우선순위: 본인 보유/관심 섹터부터.
### Phase 5 — 매수신호 규칙 (운영→실행 연동)
- **산출물**: `[검증]` 충분한 분야부터 "언제부터 모으나" 트리거를 전략에 추가(예: 대장주 추세 + 추종주 미반영 = 관찰 강화 신호). **여전히 타이밍 노림 금지(전략 ③)** — 신호는 *관찰 우선순위*이지 매매 자동화 아님.
- **완료조건**: strategy 에 신호 규칙 섹션 + 근거 링크.
- **명령**: 수동 + `/invest-ingest`.
### Phase 6 — (선택) 글로벌/미국 섹터 확장
- 미국 섹터(에너지·금융·헬스케어 등) 카드 추가. 한국 구조 완성 후.
---
## 4. 진행 추적 / Progress Tracker
| Phase | 상태 | 다음 액션 |
|---|---|---|
| 0 토대 | ✅ 완료 | — |
| 1 분야 완성 | ✅ 완료 (25카드: 거시 8 + 섹터 17) | — |
| 2 로테이션 지도 | ✅ 완료 (`field-rotation` 4축) | — |
| 3 일일 관측 | ⬜ 0/14일 ← **다음** | `/invest-daily` 매일 |
| 4 검증 승급 | ⬜ 0/10 `[검증]` | 관심 섹터 `/invest-research` |
| 5 매수신호 | ⬜ | Phase 4 후 |
| 6 글로벌 | ⬜(선택) | Phase 2 후 |
---
## 6. 의사결정 지원 트랙 (매수 후보·금액·규칙체크)
> ⚠️ **정직 고지 (가장 중요):** 이 시스템은 **"오늘 이거 사면 오른다"는 매수 추천을 하지 않는다.** 그건 면허 자문도, 예측도 아니다(strategy §고지 + ③ 타이밍/FOMO 금지). 대신 *"내 계획·위험예산에 비춰 다음 매수가 무엇/얼마인지, 규칙을 어기진 않는지"* 를 점검한다. **상승 예측으로 거르지 않고, 계획 적합성·위험예산으로 거른다.**
### 6.1 매일 의사결정 워크플로
```
1. /invest-daily → 시장 + 분야 관찰(예측 vs 실측)
2. (후보 점검 — 추천 아님) → 활성계획(active-plan)의 목표배분·다음 매수 트리거와 대조:
"내 계획상 다음에 살 건 무엇/얼마인가? 지금이 그 조건인가?"
급등 추격·패닉 매수는 ③ 가드가 빨간 플래그
3. /invest-decide "매수…" → 실제 매수 시 원장 기록 + 전략규칙 위반 자동 차단
```
- **"그날의 종목과 금액"** = *추천*이 아니라 *내 계획에서 도출되는 다음 행동*(예: active-plan 4단계 = TIGER 360750 일시매수). 계획에 없는 종목을 "오늘 사라"고 들이미는 건 ❌.
- 후보가 새로 생기면 반드시 `/invest-research` 로 근거를 먼저(선근거 원칙, 전략 ③).
### 6.2 완료조건
- active-plan 의 "다음 매수 트리거"가 항상 채워져 있고, `/invest-decide` 가 규칙위반을 0건 통과시킨다.
---
## 7. 차트·기술적 분석 리터러시 트랙 (극한 학습, 정직판)
> ⚠️ **정직 고지:** **"더 떨어질지 / 지지받고 안 떨어질지"를 확실히 아는 방법은 없다.** 차트(기술적 분석, TA)는 *크리스탈볼이 아니라*, 가격·거래량의 과거 패턴으로 **확률적 시나리오(가설)** 를 세우는 도구다. 이 wiki의 기존 근거(`[[raw/invest-research/2026-06-05-passive-diversification-behavior]]`)는 *잦은 매매·타이밍이 수익을 해친다*고 본다. 그래서 TA는 **"리터러시(읽는 눈)"로 배우되, 예측 보장으로 쓰지 않는다.**
### 7.1 무엇을 기르나 (리터러시)
- 차트를 *읽을 줄* 알기: 추세(상승/하락/횡보), **지지·저항**, 거래량, 이동평균(20·60·120일), 캔들, RSI·MACD 같은 보조지표가 *무엇을 의미하는지*.
- "지지선에서 반등할 수도 / 깨고 더 빠질 수도" 양쪽 **시나리오를 `[가설]`로** 세우는 법 (한쪽 단정 ❌).
### 7.2 명명된 실패 모드 (반드시 함께 학습)
- **후행성**: 대부분 지표는 과거의 요약 → 미래 보장 아님.
- **자기실현/소멸**: 많이 알려진 패턴은 효과가 사라지거나 역이용됨.
- **과최적화·사후해석**: 지나고 보면 다 맞아 보임(생존편향).
- **확증편향**: 보고 싶은 시나리오만 차트에서 찾음.
### 7.3 구조·커맨드
- TA 개념은 `wiki/invest-concepts/` 에 개념 카드로(예: `concept-support-resistance`, `concept-moving-average`) — *정의 + 한계 + 실패모드* 포함.
- 특정 종목 차트 상황은 `/invest-research "<종목> 지지·저항·하방 시나리오(2026-MM-DD)"` → 다출처 + **상방/하방/지지 시나리오 각각 `[가설]`** + 명시적 "예측 아님".
- 매일 쌓인 일일 노트·조사는 `/query "…"` 로 질문(예: "최근 2주 반도체 대장주 추세와 지지선 관측 요약").
### 7.4 완료조건
- TA 리터러시 개념 카드 ≥ 5장(각 §실패모드 포함), 종목 차트 조사 시 항상 양방향 `[가설]` + "예측 아님" 표기.
---
## 8. 주식 마스터리 커리큘럼 (극한으로 알기)
> "주식을 극한으로 알고 싶다" = 한 분야가 아니라 **8개 지식 도메인**을 개념 카드(`wiki/invest-concepts/`) + 조사(`raw/invest-research/`)로 채우는 장기 학습 트랙.
| # | 도메인 | 무엇 | 현황 |
|---|---|---|---|
| 1 | 거시·자금흐름 | 분야→대장주→추종주→로테이션 | 🟡 field-map(이 플랜 §2~3) |
| 2 | 펀더멘털 | 재무제표·실적·산업구조·해자 | ⬜ |
| 3 | 밸류에이션 | PER·PBR·EV/EBITDA·DCF·상대가치 | ⬜ |
| 4 | 기술적 분석 리터러시 | §7 (정직판) | ⬜ |
| 5 | 행동재무·심리 | 패닉·FOMO·행동격차 | 🟡 strategy ③ 근거 |
| 6 | 리스크·포트폴리오 | MDD·분산·상관·켈리 | 🟡 strategy ① 근거 |
| 7 | 세제·계좌 | 양도세·ISA·금융소득종합과세 | ✅ 조사됨(research 2건) |
| 8 | 시장 미시구조 | 호가·수급·유동성·공매도 | ⬜ |
- 각 도메인은 `/invest-research` 로 권위 출처 조사 → `wiki/invest-concepts/` 개념 카드로 정제. **모든 주장 `[검증]/[가설]` + 출처.**
- 우선순위: 2 펀더멘털 → 3 밸류에이션 → 4 TA리터러시 → 8 미시구조 (1·5·6·7 은 부분 완료).
---
## 9. 이 플랜대로 하면 (요약)
- **Phase 1~2 끝나면** = 원한 *구조*("전체 분야 → 대장주 → 추종주 → 분야간 연관")가 **완전히** 카드로 존재.
- **Phase 3~4** = 그 구조가 매일 관측·검증으로 *살아 움직이며* 뇌피셜이 사실로 익는다.
- **트랙 §6** = "오늘 뭘 얼마 살지"를 *계획·위험예산*으로(추천 아님) 점검.
- **트랙 §7** = 차트 *읽는 눈*(지지·저항·하방 시나리오) — 단, **예측 보장 아님 + 실패모드** 정직판.
- **§8 커리큘럼** = 펀더멘털·밸류에이션·미시구조까지 8도메인으로 "극한" 학습.
> 즉 더 이상 "조금씩 즉흥적으로"가 아니라, **유한한 목표(25카드 + 로테이션 + 8도메인)** 를 단계로 채우는 것. 끝이 정의돼 있다.
>
> ⚠️ **단, 시스템이 길러주는 건 "더 잘 *결정*하는 눈"이지 "미래를 *맞추는* 능력"이 아니다.** 어떤 차트·조사도 "더 떨어질지"를 확실히 답하지 못한다 — 그걸 약속하는 건 거짓말. 이 정직함이 시스템의 핵심.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,451 @@
# 하네스 정합성 스위프 (Tier A) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 문서·설정이 실제 repo 상태와 어긋난 4개 지점(A1 Antigravity 훅 경로 / A2 Codex 로딩 문서 오류 / A3 wiki-doc-author 계약 결함 3종 / A4 인벤토리 숫자)을 아키텍처 변경 없이 정합화한다.
**Architecture:** 순수 config·문서·프롬프트 텍스트 편집 + 결정론적 검증(grep / `json.tool` / `tomllib` / `pytest`). 새 기능 코드는 없다. "test" 사이클은 *현재상태 assert(grep) → 편집 → 정합 assert(grep/syntax) → commit*. A3 은 3개 플랫폼 계열 5개 표현 파일을 **하나의 commit 으로** 원자 정합화(부분 정합 = 새 드리프트).
**Tech Stack:** git, Python 3.12(`json.tool`, `tomllib`, `pytest`), grep, bash. Antigravity CLI(A1 E2E 확인용, user-gated).
**Spec:** [docs/superpowers/specs/2026-07-14-harness-correctness-sweep-design.md](../specs/2026-07-14-harness-correctness-sweep-design.md) (rev.3)
## Global Constraints
모든 task 는 아래를 암묵적으로 포함한다. 값은 spec 에서 verbatim 복사.
- **브랜치:** main 이 현재 dirty(무관한 미완 작업 존재). 반드시 `harness-correctness-sweep` 브랜치에서 작업.
- **스테이징 격리:** `git add -A` / `git add .` **절대 금지**. 각 commit 은 그 task 의 명시된 파일만 `git add <경로>`. (무관한 dirty 파일 오염 방지.)
- **git-diff allowlist (이 10파일 + 조건부 launcher + 본 plan/spec 외 변경 금지):**
```
.agents/hooks.json
CLAUDE.md
AGENTS.md
.codex/agents/README.md
.agents/plugins/wiki-superpowers/README.md
.claude/agents/wiki-doc-author.md
.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md
.codex/agents/wiki-doc-author.md
.codex/agents/wiki-doc-author.toml
.agents/agents/wiki-doc-author/agent.json
# 조건부(A1 launcher 채택 시에만): .agents/hooks/run-wiki-hook.py
```
- **A1 portable-only:** 새 하드코딩 절대경로 채택 금지. 우선순위 — env var → `git rev-parse` → launcher self-locate. 절대경로는 임시 진단 외 최종안 아님.
- **A3 부분변경 = `Status = BLOCKED`** (고정, "또는 PARTIAL" 금지 — `PARTIAL` 은 doc-author output schema 에 미정의; `DONE | NEEDS_CONTEXT | BLOCKED` 뿐).
- **A3 카테고리 = 8** (branch-note, error-note, interview-prep, job-posting, blog-topic, lecture-note, project-note, daily-note). **validator 개수 하드코딩 제거**(8→7 로 바꾸지 **않음**).
- **A3 migrate 본문-보존 계약 불변** — 건드리지 않는다.
- **A4 historical 제외:** `docs/superpowers/plans/*`·`notes/*` 의 동일 stale 문구는 dated 기록이라 **정정 대상 아님**. "14개"/"7개" 는 다의어 — 자동 치환 금지, inventory 문맥만 수동 판정.
- **임시 진단/테스트 파일은 최종 `git diff` 잔존 0.**
### 공유 스니펫 — A3 새 계약 텍스트 (D1)
A3 의 5개 표현 파일이 **동일 계약**으로 수렴해야 하는 canonical 문구. 각 파일의 포맷(md 산문 / toml 문자열 / json 이스케이프 문자열)에 맞춰 적용하되 의미는 동일.
- **STOP 조건(파일수 STOP 대체):**
> target document + 그 Parent hub 외의 파일을 생성·수정하려는 요청 — 1 dispatch = 1 논리적 문서(허용 write set: target 1개 + 그 Parent hub 의 `## Cluster` 링크 유지만; 다른 raw/rule/template/derived 문서 수정 금지)
- **"What you are NOT" 라인 대체:**
> - target document + 그 Parent hub 외 파일 수정 금지 (1 dispatch = 1 논리적 문서: target 1개 + Parent hub Cluster 링크 유지만 허용)
- **description 절 대체** (`Writes exactly 1 file and reports validation.` →):
> Creates or migrates one target document (also maintaining its Parent hub Cluster link) and reports validation.
- **부분변경 규칙(신규 1줄, Shortcut Trap 또는 작업 절차 말미):**
> target 또는 Parent hub 중 일부만 변경되고 나머지가 실패하면 DONE 금지 → **Status = BLOCKED**, 변경 성공 파일 + 실패 단계 모두 보고 (자동 rollback 미구현).
---
## Task 0: 브랜치 + baseline 캡처 (setup)
**Files:** (변경 없음 — 브랜치 생성 + 읽기 전용 baseline)
- [ ] **Step 1: main 에서 작업 브랜치 생성**
```bash
git rev-parse --abbrev-ref HEAD # main 확인
git checkout -b harness-correctness-sweep
```
Expected: `Switched to a new branch 'harness-correctness-sweep'`
- [ ] **Step 2: A4 권위 카운트 baseline 기록 (이 값이 문서 정정의 근거)**
Run:
```bash
echo "templates=$(find templates -maxdepth 1 -type f -name '*.md' | wc -l)"
echo "rules=$(find rules -maxdepth 1 -type f -name '*.md' | wc -l)"
echo "claude_agents=$(find .claude/agents -maxdepth 1 -type f -name '*.md' | wc -l)"
echo "antigravity_exec_agents=$(find .agents/agents -mindepth 2 -maxdepth 2 -type f -name 'agent.json' | wc -l)"
echo "agent_prompt_ssot=$(find .agents/plugins/wiki-superpowers/agents -maxdepth 1 -type f -name '*.md' | wc -l)"
echo "commands=$(find .claude/commands -maxdepth 1 -type f -name '*.md' | wc -l)"
```
Expected: `templates=27 rules=14 claude_agents=12 antigravity_exec_agents=10 agent_prompt_ssot=10 commands=23`
(값이 다르면 STOP — spec 숫자 재확정 필요.)
- [ ] **Step 3: 현재 dirty 파일 목록 기록 (오염 감시 기준선)**
Run: `git status --short`
기대: 무관한 기존 변경(ca-tmpl 삭제, experiment-nplus1 수정, error 노트들, llm-wiki-harness.xml)이 보임. 이 파일들은 **이번 작업에서 절대 stage 하지 않는다.**
(commit 없음 — setup only.)
---
## Task 1: A1 — Antigravity 훅 경로 정합 (portable) + JSON 검증
**Files:**
- Modify: `.agents/hooks.json` (4개 command entry)
- (조건부, E2E 실패 시에만) Create: `.agents/hooks/run-wiki-hook.py`
**결정 배경 (spec §4 A1):** 대상 절대경로 `/home/donghyeon/dev/llm-wiki-private/...` 는 존재하지 않는 경로(dead). 새 절대경로로 바꾸면 이동·clone 시 재사망 → portable 필수. 이 세션(Claude Code)에서 Antigravity 실제 실행 cwd 를 관측할 수 없으므로, **Codex 가 이미 쓰는 검증된 상대경로 형태**(`.codex/hooks.json` 참조)를 기본값으로 채택하고, 실제 firing 은 user 가 Antigravity 세션에서 확인한다. E2E 확인 전까지 A1 verdict = **PARTIAL**.
- [ ] **Step 1: 현재 dead 경로 확인 (assert 실패상태)**
Run: `grep -n '/home/donghyeon/dev/llm-wiki-private' .agents/hooks.json`
Expected: 4개 line hit (claim-gate Pre/Stop, structure-gate Pre/Post). 그리고:
```bash
ls /home/donghyeon/dev/llm-wiki-private 2>&1 # → No such file or directory (dead 확인)
```
- [ ] **Step 2: 4개 command 를 Codex-미러 상대경로로 수정**
`.agents/hooks.json` 의 4개 `command` 문자열을 절대경로 → 상대경로로. (앞부분 `python3 ` + 뒷부분 flags 는 보존, 경로만 교체.)
| Gate.Event | 현재 | 수정 후 |
|---|---|---|
| wiki-claim-gate.PreToolUse | `python3 /home/donghyeon/dev/llm-wiki-private/.claude/hooks/wiki_claim_gate.py --antigravity` | `python3 .claude/hooks/wiki_claim_gate.py --antigravity` |
| wiki-claim-gate.Stop | `python3 /home/donghyeon/dev/llm-wiki-private/.claude/hooks/wiki_claim_gate.py --antigravity` | `python3 .claude/hooks/wiki_claim_gate.py --antigravity` |
| wiki-structure-gate.PreToolUse | `python3 /home/donghyeon/dev/llm-wiki-private/.claude/hooks/wiki_structure_lint.py --pre --antigravity` | `python3 .claude/hooks/wiki_structure_lint.py --pre --antigravity` |
| wiki-structure-gate.PostToolUse | `python3 /home/donghyeon/dev/llm-wiki-private/.claude/hooks/wiki_structure_lint.py --hook --antigravity` | `python3 .claude/hooks/wiki_structure_lint.py --hook --antigravity` |
`wiki-hard-gate`(global `~/.gemini/...`)는 **건드리지 않는다.**
- [ ] **Step 3: 정합 assert (grep) + JSON 구문 검증**
Run:
```bash
grep -c '/home/donghyeon/dev/llm-wiki-private' .agents/hooks.json # 기대: 0
grep -c 'python3 .claude/hooks/' .agents/hooks.json # 기대: 4
python3 -m json.tool .agents/hooks.json >/dev/null && echo "JSON OK"
```
Expected: `0`, `4`, `JSON OK`
- [ ] **Step 4: commit**
```bash
git add .agents/hooks.json
git commit -m "fix(hooks): repair dead Antigravity hook paths (absolute→portable relative)
wiki-claim-gate/wiki-structure-gate pointed at /home/donghyeon/dev/... which
does not exist. Mirror Codex's proven repo-root-relative form.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
- [ ] **Step 5: E2E 확인 (USER-GATED — Antigravity 세션 필요)**
이 세션에서 Antigravity 를 구동할 수 없으므로, 사용자에게 아래를 요청하고 결과를 받는다. spec §5 A1 표 6행:
| Hook | 발생 이벤트 | 기대 |
|---|---|---|
| claim gate PreToolUse | Antigravity 에서 Write/Edit | hook 실행 로그 |
| claim gate Stop | agent 정상 종료 | 종료 hook 로그 |
| structure gate PreToolUse | Markdown Write/Edit | pre 검사 실행 |
| structure gate PostToolUse | Markdown 쓰기 완료 | post 검사 실행 |
| 차단 검증 | 규칙 위반 임시 입력 | BLOCK 전달 |
| 정상 검증 | 규칙 만족 임시 입력 | PASS 후 계속 |
- **firing 확인됨** → A1 = DONE.
- **firing 안 됨(cwd ≠ repo root)** → 상대경로 무효 → Step 6(fallback) 진행.
- **확인 불가(Antigravity 세션 없음)** → A1 = **PARTIAL**, 나머지 task 진행, 사용자 확인 시 재개.
- [ ] **Step 6: (조건부 fallback — Step 5 에서 firing 실패 시에만) launcher 도입**
Antigravity 가 repo-root cwd 가 아니고 workspace-root 환경변수도 없으면, `.agents/hooks.json` 이 참조할 수 있는 경로 형태(Antigravity 가 resolve 가능한 형태 — 진단으로 확정)로 launcher 를 두고, launcher 가 자기 위치에서 repo root 를 찾아 실제 hook 을 exec 한다. 진단(pwd/env)이 선행되어야 하며 결과에 따라 형태 확정. 이 fallback 을 쓰면 `.agents/hooks/run-wiki-hook.py` 를 allowlist 에 포함하고 별도 commit. **Step 5 firing 확인 전에는 착수하지 않는다.**
---
## Task 2: A2 — CLAUDE.md Codex 로딩 서술 정정
**Files:**
- Modify: `CLAUDE.md:82` (§2 Hooks bullet 내 codex fallback 절)
- [ ] **Step 1: 현재 오류 문구 확인 (assert)**
Run: `grep -n 'codex 가 본 CLAUDE.md 를 프로젝트 지침으로 읽음(AGENTS.md 중복 불필요)' CLAUDE.md`
Expected: line 82 hit.
- [ ] **Step 2: 문구 교체 (Codex 로만 증거 한정, Antigravity 단정 금지)**
`CLAUDE.md:82` 의 다음 절을 교체:
old:
```
`.codex/config.toml` 의 `project_doc_fallback_filenames = ["CLAUDE.md"]` 로 codex 가 본 CLAUDE.md 를 프로젝트 지침으로 읽음(AGENTS.md 중복 불필요).
```
new:
```
`.codex/config.toml` 의 `project_doc_fallback_filenames = ["CLAUDE.md"]` 는 해당 디렉터리에 `AGENTS.md` 가 **없을 때만** 쓰이는 fallback 이다 — 루트 `AGENTS.md` 가 존재하는 현재 구조에선 발동하지 않으므로, Codex 는 `AGENTS.md`(CLAUDE.md 의 ≤150줄 요약)를 진입점으로 로드한다. CLAUDE.md 는 그 요약이 가리키는 모든 모델 공통 **운영-규칙 SSOT** 로 유지된다(Codex 가 CLAUDE.md 를 자동 지침으로 직접 선택한다고 가정 금지).
```
- [ ] **Step 3: 정합 assert**
Run:
```bash
grep -c 'AGENTS.md 중복 불필요' CLAUDE.md # 기대: 0
grep -c '없을 때만' CLAUDE.md # 기대: ≥1 (새 문구)
```
Expected: `0`, `≥1`
- [ ] **Step 4: commit**
```bash
git add CLAUDE.md
git commit -m "docs(CLAUDE): correct Codex project-doc loading (AGENTS.md preempts CLAUDE.md fallback)
Root AGENTS.md exists, so project_doc_fallback_filenames never fires; Codex
loads AGENTS.md, not CLAUDE.md. Scope claim to Codex only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 3: A3 — wiki-doc-author 계약 정합 (5표현 원자 commit)
**Files (모두 한 commit):**
- Modify: `.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md` (SSOT, 먼저)
- Modify: `.claude/agents/wiki-doc-author.md`
- Modify: `.codex/agents/wiki-doc-author.md`
- Modify: `.codex/agents/wiki-doc-author.toml`
- Modify: `.agents/agents/wiki-doc-author/agent.json`
**Interfaces (계약 불변식 — 5파일 수렴 목표):** target=1 · Parent hub edit 허용 · unrelated edit 금지 · category=8 · validator 개수 비고정 · migrate 본문보존 유지 · 부분변경→BLOCKED.
**결함 분포 (spec §4 A3):**
| 파일 | D1(파일수 STOP+What NOT+desc) | D2(category 7→8) | D3((8개 모두)→비고정) |
|---|---|---|---|
| plugin SSOT md | 有 (L78, L253, desc) | 有 (L73) | 편집 시 확인 |
| Claude md | 有 (L73, L240, desc) | 無(이미 8) | 無 |
| Codex md | 有 (What NOT L191, desc, STOP 재확인) | 無(이미 8) | 편집 시 확인 |
| Codex toml | 有 (What NOT L190, desc, STOP 재확인) | 無(이미 8) | 편집 시 확인 |
| agent.json | 有 (STOP#7, What NOT, desc) | 有 (STOP#2 + STOP-check table row2) | 有 ("(8개 모두)") |
각 파일은 **먼저 Read** 해서 정확한 old 문자열을 확보한 뒤 편집(사본마다 표현이 다름). 적용할 새 문구는 Global Constraints §"공유 스니펫 — A3 새 계약 텍스트 (D1)" 참조.
- [ ] **Step 1: plugin SSOT 편집 (`.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md`)**
Read 후 적용:
- **D1** — L78 파일수 STOP(`2개 이상 파일 만들기 / 수정 (1 dispatch = 1 file 위반)`) → 공유 스니펫 STOP 문구. L253 "What NOT"(`여러 파일 동시 생성·수정 금지 (1 dispatch = 1 file)`) → 공유 스니펫 What-NOT 문구. description(`Writes exactly 1 file and reports validation.`) → 공유 스니펫 description. 부분변경 규칙 1줄 추가(Shortcut Trap 또는 작업 절차 말미).
- **D2** — L73 `Category ∉ 7 허용` → `Category ∉ 8 허용`.
- **D3** — Post-Write Validator 블록에 `(N개 모두)` 식 숫자 하드코딩이 있으면 `(위 Validator 의 모든 적용 가능 항목)` 로. 없으면 skip.
- [ ] **Step 2: Claude md 편집 (`.claude/agents/wiki-doc-author.md`)**
Read 후 적용:
- **D1** — L73 STOP #5(`5. 2개 이상 파일 생성·수정 요청 — 1 dispatch = 1 file`) → 공유 스니펫 STOP. L240 What-NOT(`여러 파일을 한 번에 생성·수정 금지 (1 dispatch = 1 file)`) → 공유 스니펫 What-NOT. L3 description → 공유 스니펫 description. 부분변경 규칙 1줄 추가.
- D2/D3 없음(이미 8, validator 하드코딩 없음).
- [ ] **Step 3: Codex md 편집 (`.codex/agents/wiki-doc-author.md`)**
Read 후 적용:
- **D1** — 파일수 STOP 조건(있으면) + What-NOT(L191 `여러 파일을 한 번에 생성·수정 금지 (1 dispatch = 1 file)`) + description → 공유 스니펫. 부분변경 규칙 1줄 추가.
- D3 — validator 숫자 하드코딩 있으면 제거.
- [ ] **Step 4: Codex toml 편집 (`.codex/agents/wiki-doc-author.toml`)**
Read 후 적용 (프롬프트가 TOML 문자열 — 따옴표/이스케이프 보존):
- **D1** — 파일수 STOP + What-NOT(L190) + description(L2) → 공유 스니펫. 부분변경 규칙 1줄 추가.
- D3 — validator 숫자 하드코딩 있으면 제거.
- [ ] **Step 5: agent.json 편집 (`.agents/agents/wiki-doc-author/agent.json`)**
Read 후 적용 (프롬프트가 JSON 이스케이프 문자열 — `\n`·`\"` 이스케이프 **보존**):
- **D1** — 임베디드 프롬프트의 STOP #7(`2개 이상 파일 만들기 / 수정 (1 dispatch = 1 file 위반)`) + What-NOT(`여러 파일 동시 생성·수정 금지 (1 dispatch = 1 file)`) + top-level `description`(`Writes exactly 1 file and reports validation.`) → 공유 스니펫. 부분변경 규칙 1줄 추가.
- **D2** — STOP #2(`Category ∉ 7 허용`) 및 STOP-check 표 row2(`Category in 7 allowed`) → `8`.
- **D3** — output schema 의 `... (8개 모두)` → `(위 Validator 의 모든 적용 가능 항목)`.
- [ ] **Step 6: 잔존-문구 grep (5표현 전부, 기대 0)**
Run (spec §5 A3 넓힌 패턴):
```bash
grep -RInE '1 dispatch = 1 file|Writes exactly 1 file|Write 1 file|write the file once|2개 이상 파일|여러 파일을 한 번에 생성.?수정 금지|여러 파일 동시 생성.?수정 금지|Category ∉ 7|Category in 7 allowed|8개 모두' \
.claude/agents/wiki-doc-author.md \
.codex/agents/wiki-doc-author.md \
.codex/agents/wiki-doc-author.toml \
.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md \
.agents/agents/wiki-doc-author/agent.json
```
Expected: **0 hit**. hit 남으면 해당 파일 재편집.
> **판정 주의:** `write the file once` / `one file at a time` 같은 *도입 문장*이 잡히면 — **파일 수를 계약으로 단정하는 경우**(예: "you write **the file** once"가 '단일 파일만'을 함의)만 새 write-set 계약과 어긋나므로 정정한다. 단순히 target 문서 1개를 쓴다는 서술은 새 계약과 일치하므로 유지 가능. 즉 이 grep 은 *탐지 그물*이고, 제거 대상은 "target+Parent 2파일"과 모순되는 file-count 단정뿐이다.
- [ ] **Step 7: 새 계약 문구 존재 + 구문 검증**
Run:
```bash
# 새 write-set 계약 문구가 5표현 전부에 존재
grep -Rl '1 논리적 문서' \
.claude/agents/wiki-doc-author.md .codex/agents/wiki-doc-author.md \
.codex/agents/wiki-doc-author.toml \
.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md \
.agents/agents/wiki-doc-author/agent.json | wc -l # 기대: 5
# 구문
python3 -m json.tool .agents/agents/wiki-doc-author/agent.json >/dev/null && echo "json OK"
python3 -c 'import tomllib; tomllib.load(open(".codex/agents/wiki-doc-author.toml","rb")); print("toml OK")'
# category=8 반영 (7 잔존 0)
grep -REc 'Category ∉ 7|Category in 7 allowed' .agents/plugins/wiki-superpowers/agents/wiki-doc-author.md .agents/agents/wiki-doc-author/agent.json # 기대: 0
```
Expected: `5`, `json OK`, `toml OK`, `0`
- [ ] **Step 8: 의미 패리티 표 확인 (수기 — grep 으로 안 잡히는 계약 동등성)**
각 파일을 훑어 아래 6행이 5파일 전부 PASS 인지 확인(문장은 달라도 계약값 동일):
| Contract | Claude md | plugin SSOT | Codex md | Codex toml | agent.json |
|---|---|---|---|---|---|
| target document 1개 | | | | | |
| Parent hub edit 허용 | | | | | |
| unrelated 문서 edit 금지 | | | | | |
| 허용 category = 8 | | | | | |
| validator 개수 비고정 | | | | | |
| migrate 본문 보존 유지 | | | | | |
1개라도 FAIL 이면 해당 파일 재편집 후 Step 6~8 반복.
- [ ] **Step 9: commit (5파일 원자)**
```bash
git add .agents/plugins/wiki-superpowers/agents/wiki-doc-author.md \
.claude/agents/wiki-doc-author.md \
.codex/agents/wiki-doc-author.md \
.codex/agents/wiki-doc-author.toml \
.agents/agents/wiki-doc-author/agent.json
git commit -m "fix(wiki-doc-author): replace '1 file' contradiction with explicit write set (5 platform copies)
D1: '1 dispatch=1 file' vs mandatory Parent-hub edit → explicit allowed write
set (target + its Parent hub only); partial write → BLOCKED. D2: category 7→8.
D3: drop hardcoded validator count. Syntax + semantic parity verified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 4: A4 — live 문서 인벤토리 숫자 정정
**Files:**
- Modify: `AGENTS.md` (L23, L24, L50)
- Modify: `CLAUDE.md` (L79 — 9→10 agent + scripts 문구)
- Modify: `.codex/agents/README.md` (L9 — scripts 문구)
- Modify: `.agents/plugins/wiki-superpowers/README.md` (L11 scripts, L46 rules 7→14)
**주의:** `CLAUDE.md:81`("생성기 부재(2026-06-06 확인)")은 여전히 참 — **변경 안 함**. historical(`docs/.../plans|notes`) 제외.
- [ ] **Step 1: 현재 stale 문구 확인 (assert, live 문서만)**
Run:
```bash
grep -nE '정의 \(14개|7개 rule|7 agents|동일 9개 agent|디렉터리 자체 부재|the .scripts. directory itself is absent|동일한 7개 rule 파일' \
AGENTS.md CLAUDE.md .codex/agents/README.md .agents/plugins/wiki-superpowers/README.md
```
Expected: AGENTS.md(23,24,50), CLAUDE.md(79), .codex/agents/README.md(9), plugin README(11,46) hit.
- [ ] **Step 2: 정정 적용 (각 파일 Read 후 정확 문자열 교체)**
| 파일:줄 | old | new |
|---|---|---|
| AGENTS.md:23 | `카테고리별 출력 형식 정의 (14개, ` | `카테고리별 출력 형식 정의 (27개, ` |
| AGENTS.md:24 | `**3개 모델 공유 SSOT** — 7개 rule ` | `**3개 모델 공유 SSOT** — 14개 rule ` |
| AGENTS.md:50 | `7 agents (` … `) 가 정의되어 있음.` | `10 agents (7 core + branch-depth-auditor·coverage-auditor·wiki-consistency-auditor) 가 정의되어 있음.` |
| CLAUDE.md:79 | `.claude/`와 동일 9개 agent` | `.claude/`와 동일 10개 agent` |
| CLAUDE.md:79 | `(위 7개 + `branch-depth-auditor`·`coverage-auditor`) 의` | `(위 7개 + `branch-depth-auditor`·`coverage-auditor`·`wiki-consistency-auditor`) 의` |
| CLAUDE.md:79 | `(2026-06-06 확인; `scripts/` 디렉터리 자체 부재)` | `(2026-06-06 확인; `scripts/` 는 존재하나 `sync_automation.py` 만 부재 — 2026-07-14 재확인)` |
| .codex/agents/README.md:9 | `(2026-06-06 확인; `scripts/` 디렉터리 자체 부재)` | `(2026-06-06 확인; `scripts/` 는 존재하나 `sync_automation.py` 만 부재 — 2026-07-14 재확인)` |
| plugin README:11 | `(the `scripts/` directory itself is absent)` | `(the `scripts/` dir now exists — deep-research — but `sync_automation.py` is still absent, re-checked 2026-07-14)` |
| plugin README:46 | `동일한 7개 rule 파일을 공유한다` | `동일한 14개 rule 파일을 공유한다` |
> AGENTS.md:50 은 이름 나열이 길면 위처럼 count+대표 3개로 축약. 정확한 old 문자열은 Read 로 확인 후 교체.
- [ ] **Step 3: 정합 assert (live 문서 stale 잔존 0)**
Run:
```bash
grep -nE '정의 \(14개|— 7개 rule|^본 skill.*7 agents|동일 9개 agent|디렉터리 자체 부재|directory itself is absent|동일한 7개 rule 파일' \
AGENTS.md CLAUDE.md .codex/agents/README.md .agents/plugins/wiki-superpowers/README.md
```
Expected: **0 hit**.
- [ ] **Step 4: 카운트 재검증 (문서값 == 실측)**
Run: Task 0 Step 2 의 `find` 블록 재실행 → 결과가 `27/14/12/10/10/23` 이고 문서 정정값과 일치 확인.
- [ ] **Step 5: commit**
```bash
git add AGENTS.md CLAUDE.md .codex/agents/README.md .agents/plugins/wiki-superpowers/README.md
git commit -m "docs: correct stale inventory counts (templates 14→27, rules 7→14, agents 9→10) + scripts-dir claim
Live docs only; historical plans/notes left as dated records. 2026-07-14
manual snapshot (to be automated by B1 generator).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 5: 전역 검증 + verdict
**Files:** (변경 없음 — 검증만)
- [ ] **Step 1: 구문 검증 전체**
Run:
```bash
python3 -m json.tool .agents/hooks.json >/dev/null && echo "hooks.json OK"
python3 -m json.tool .agents/agents/wiki-doc-author/agent.json >/dev/null && echo "agent.json OK"
python3 -c 'import tomllib; tomllib.load(open(".codex/agents/wiki-doc-author.toml","rb")); print("toml OK")'
```
Expected: 3 × OK.
- [ ] **Step 2: 기존 hook test 회귀**
Run: `python3 -m pytest .claude/hooks -q`
Expected: 전부 PASS (test_wiki_structure_lint / _consistency_check / _claim_gate / _rules / _quorum 등). 실패 시 원인 조사 — 이번 편집이 hook 스크립트 로직을 안 건드렸으므로 무관 실패면 기록.
- [ ] **Step 3: git-diff allowlist 이탈 감시**
Run: `git diff --name-only main...HEAD`
Expected: 아래 집합의 부분집합만 (+ launcher 는 A1 Step 6 채택 시에만):
```
.agents/hooks.json
CLAUDE.md
AGENTS.md
.codex/agents/README.md
.agents/plugins/wiki-superpowers/README.md
.claude/agents/wiki-doc-author.md
.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md
.codex/agents/wiki-doc-author.md
.codex/agents/wiki-doc-author.toml
.agents/agents/wiki-doc-author/agent.json
docs/superpowers/specs/2026-07-14-harness-correctness-sweep-design.md
docs/superpowers/plans/2026-07-14-harness-correctness-sweep.md
```
이 집합 밖 파일이 나오면 STOP + 사유 확인 (무관한 dirty 파일이 stage 됐는지 점검). 임시 진단/테스트 파일 잔존 0 확인.
- [ ] **Step 4: verdict 판정 (spec §8)**
- A2·A3·A4: 각 assert PASS + 구문 통과 + 패리티 표 전 PASS → **DONE**.
- A1: Antigravity firing 관측됨 → DONE / 직접 실행만 확인 → **PARTIAL** / 계약·구문 불일치 → BLOCKED.
- 전체: allowlist 한정 + pytest PASS 확인.
- [ ] **Step 5: 사용자 보고**
Status(A1 PARTIAL/DONE, A2·A3·A4 DONE) + 변경 파일 + A1 E2E 잔여(사용자 Antigravity 확인 필요 여부) + 다음 후보(백로그 B1~B4) 요약. 커밋은 브랜치에 있음 — merge/PR 여부는 사용자 결정.
---
## 완료 기준 요약 (spec §8 대응)
- **A1:** hooks.json 4개 command portable 수정 + JSON OK + (E2E firing 확인 시 DONE, 아니면 PARTIAL). 새 절대경로 0.
- **A2:** "AGENTS.md 중복 불필요" 0건, Antigravity 단정 없음, "운영-규칙 SSOT" 표기.
- **A3:** 5표현 잔존-문구 0 + 새 write-set 계약 존재 + category 8 + validator 비고정 + JSON/TOML OK + 패리티 표 PASS + migrate 계약 불변.
- **A4:** live 문서 stale 0 + 실측값 일치 + 두 "10" 디렉터리 구분 + 스냅샷 명시. historical 미변경.
- **전역:** `git diff --name-only` allowlist 한정 + `pytest .claude/hooks` PASS + 임시파일 잔존 0.
@@ -0,0 +1,159 @@
# Design: branch-depth-gate — 브랜치 노트 구현 착수 깊이 게이트
- 날짜: 2026-06-01
- 대상: `rules/`, `.claude/agents/`, `.claude/commands/`, `templates/branch-note-template.md`
- 부모 SSOT: 본 게이트는 *방법론 규칙*이므로 `rules/` 층에 속한다. 프로젝트 내용(`raw/project-notes/ca-skeleton-operational-contract.md`)이 아니다.
- 목적: branch-note 가 **"코딩 착수해도 되묻지 않을 만큼 깊은가"** 를 착수 *전에* 판정하는 read-only 게이트를 도입. 현재의 "착수 → 의구심 → 되돌아가 4~5회 재작업" 루프를 "착수 전 1회 가이드 루프"로 압축한다.
---
## 0. 문제 (P2)
`raw/branch-notes/` 에 80개의 `feature-*` 노트가 있다. 사용자는 한 브랜치를 실제 구현하기 전에 평균 4~5회 노트를 다듬어야 "의구심 없이" 개발에 들어갈 수 있다. 재작업을 유발하는 의구심은 네 축 **전부**에서 발생한다(사용자 확인):
1. **조사 깊이 부족** — 공식문서/기술블로그 조사가 질문에 실제론 답하지 못함(존재만 확인, 메커니즘 미확인).
2. **결정 조건 누락** — "A vs B"는 적혀 있으나 "언제 A, 언제 B"의 선택 기준이 없음.
3. **구체 detail 미정** — 명명·경로·메커니즘·테스트명이 비어 구현 중 임의 결정 발생.
4. **엣지·실패·의존 미열거** — 정상 경로만 있고 실패/엣지/다른 계약 의존이 빠짐.
근본 원인 (둘):
1. branch-note-template 의 `## 구현 가이드`*목표*("구현자가 되묻지 않을 수준")는 정의하지만, **그 수준에 도달했는지 알려주는 게이트가 없다.** 그래서 미달 상태로 착수 → 반복 재작업.
2. 조사가 **코퍼스-주도(bottom-up)** 다 — 주제 정하고 문서를 넓게 모은 뒤 합성. 그래서 "이 결정이 의존하는 질문"이 안 닫힌 채 착수 → 코딩 중 그 질문에 부딪힘 → 되돌아가 조사. 조사는 **결정-주도(top-down)** 여야 한다: 내려야 할 결정·미지수를 먼저 나열하고 각각을 닫을 때까지 조사. 조사 완료 = 모든 결정이 닫힘 = 착수 가능. (R1 의 설계 기반.)
## 1. 목표 / 성공 기준
- **성공 지표**: branch-note 1개가 착수 전 `/depth` 게이트를 통과(Blocking 0)하면, 구현 중 노트로 되돌아가는 횟수가 현격히 줄어든다(4~5회 → 1회 사전 루프).
- **수용 기준(회귀 테스트)**: 기존 브랜치 2~3개로 보정. 얕은 초기 브랜치는 `Not ready`, 여러 번 다듬어 실제 구현에 쓴 브랜치(예: `feature-boundary-validation-mapping-contract`)는 `Ready` 에 근접해야 게이트가 보정된 것.
## 2. Non-goals (명시적 비목표)
- 본 게이트는 **"전체 계약"이 아니다.** 전체 계약은 `raw/project-notes/ca-skeleton-operational-contract.md`.
- **`feature-implementation-readiness-scorecard` 를 대체/포함하지 않는다.** 그것은 *스켈레톤 전체가 도메인을 받을 준비*를 보는 거시 게이트(15 area, raw 내용). 본 게이트는 *브랜치 노트 1개의 깊이*를 보는 미시 게이트(rules 방법론). 두 게이트는 **다른 층·다른 범위로 공존**한다.
- 감사기는 **브랜치 노트를 편집하지 않는다.** 자동 조사·자동 채움(접근법 B)은 기본 동작이 아니라 옵트인 안내만.
- P1(계약 충돌 거버넌스)·P3(백틱/링크 깨짐 린트)·P4(raw→concept 승격 기준)는 별도 사이클. 단 §7의 연결점 참조.
## 3. 층위 정리 (명명 혼동 해소)
```
[rules/] ← HOW (방법론·프로세스 SSOT)
linking-rules.md, naming-conventions.md, tag-taxonomy.md,
evidence-first-research.md, reporting-standards.md,
advisory-depth.md, diagram-standards.md
+ branch-depth-gate.md ← ★ 신규. "브랜치 노트가 코딩할 만큼 깊은가" 판정 기준
[raw/project-notes/] ← WHAT (프로젝트 내용 SSOT)
ca-skeleton-operational-contract.md ← 전체 계약 (모든 branch 의 Parent)
[raw/branch-notes/] ← 전체 계약의 조각 (내용, 80개)
feature-contract-registry-governance = 계약 토큰 레지스트리
feature-implementation-readiness-scorecard = 스켈레톤 adoption 거시 게이트
feature-boundary-validation-mapping-contract = 경계 계약
...
```
명명 통일: 룰 SSOT `rules/branch-depth-gate.md` · 감사기 `branch-depth-auditor` · 커맨드 `/depth`. ("readiness" 단어를 배제해 scorecard 와 충돌 제거.)
설계 모티프: im-not-ai 의 검증된 3요소 — **기준 SSOT(taxonomy) + 탐지/감사(detector·reviewer) + 등급 판정** — 을 위키 문법으로 옮긴 것. `branch-depth-gate.md`↔taxonomy, `branch-depth-auditor`↔detector, Ready/Not-ready↔grade.
---
## 4. 컴포넌트 1 — `rules/branch-depth-gate.md` (기준 SSOT, 설계의 심장)
"구현 착수 가능"을 4축 × Blocking 규칙으로 명문화한다.
> 축 라벨은 `R1~R4` 를 쓴다. branch-note 의 Decision Evidence Map 이 `D1`, `D2` 를 *Decision ID* 로 쓰므로, 축을 `D*` 로 두면 충돌·모호해진다.
| 축 | Pass 조건 | Blocking (= Not ready) 트리거 |
|---|---|---|
| **R1. 조사 깊이** | 각 Decision 의 Supporting Claim 이 클레임 깊이 사다리(§4.1) 기준을 충족 — 의존 메커니즘에 L1+, 분기 조건에 L2+ | 결정의 근거 claim 이 순수 L0(존재만)뿐. how/when 부재 |
| **R2. 결정 조건** | 각 Decision 이 "어떤 조건일 때 A, 아니면 B"의 선택 기준을 명시 | `검토한 대안` 은 있는데 *언제 그 대안을 고르는지* 기준이 없음 |
| **R3. 구체 detail** | `## 구현 가이드` 의 각 in-scope 항목이 명명·경로·메커니즘·API/테스트명을 구체화 **또는** `UNSUPPORTED_IMPL_DECISION` 라벨 + trade-off 한 줄 | in-scope 항목인데 구현 detail 도 없고 UNSUPPORTED 라벨도 없음 |
| **R4. 엣지·실패·의존** | 실패/엣지 경로 열거 + 다른 contract 의존을 *대상 브랜치 + 그 Decision ID* 로 링크 | 정상 경로만 있음 / 다른 계약 의존이 암시되는데 링크 안 됨 |
**판정 규칙** (위키가 이미 쓰는 binary 프레이밍 계승):
- 심각도 3단계: `Blocking`(Not ready) · `Should-fix`(권고) · `Advisory`(참고).
- **Ready = Blocking 0건.** Should-fix 가 남아도 사용자가 "감수" 선언 시 착수 가능(리포트에 기록).
- 모든 finding 은 `심각도 · 위치(섹션/행) · 예상 의구심("구현 중 여기서 ___를 되묻게 됨") · 채울 방법` 4종 세트로 근거화. 근거 없는 지적 금지(claim 철학과 동일).
이 룰북은 `rules/` 의 8번째 규칙이며, 다른 규칙처럼 *명명된 실패 모드 + 자동 검사 가능* 형태로 작성한다.
### 4.1 R1 조사 깊이 — 결정-주도 + 클레임 깊이 사다리
깊이의 단위는 **코퍼스(문서 개수)가 아니라 결정별 종결**이다. 얕은 문서 10개 < 결정을 닫는 문서 1개. 개수는 허영 지표.
**클레임 깊이 사다리** — 각 Supporting Claim 이 답하는 수준:
| 레벨 | 답하는 것 | 판정 |
|---|---|---|
| **L0 존재** | "X 가 있다 / 권장한다" | 단독이면 불충분 |
| **L1 메커니즘** | 어떻게 동작 / 언제 발생 | 메커니즘 의존 결정의 최소선 |
| **L2 조건·경계** | 언제 적용/제외, 실패 시 어떻게 | 분기 조건 있는 결정의 최소선 |
| **L3 검증** | 확인 방법·수치·반례 | 가산점 |
**R1 Pass**: 각 Decision 의 근거 claim 이 — 그 결정이 *의존하는 메커니즘*에 L1 이상, 그 결정이 *분기하는 조건*에 L2 이상 도달. 순수 L0 만 = Blocking.
**출처 타입 적정성** (개수 기준 대체 — 위키의 claim-strength 개념 재사용):
- 스펙/표준이 정의한 동작 → `official-standard` / `official-vendor-doc` **1개로 충분**.
- "대기업은 보통 이렇게 한다"는 *운영 패턴 추론* → 회사 블로그 1개는 claim-gate 철학상 "공식" 불가. 그 결정의 근거로 삼으려면 **독립 사례 2개+** 또는 official 1개 병행.
즉 게이트가 묻는 것은 "몇 개 조사했나"가 아니라 **"내 결정 N개가 각각 적정 출처 타입의 L1+ claim 으로 닫혔나"**.
## 5. 컴포넌트 2 — `branch-depth-auditor` 에이전트 (read-only)
- **입력**: 브랜치 노트 경로 1개.
- **읽는 것**: ① `rules/branch-depth-gate.md` ② 대상 브랜치 노트 ③ 그 노트가 링크한 raw 소스(Sources 표 + Decision Evidence Map 의 Supporting Claims 가 가리키는 `raw/.../*.md#Cn`). — 소스를 실제로 열어 **R1(조사 깊이)** 을 판정하는 게 핵심. 링크만 있고 내용이 얕으면 잡아낸다.
- **처리**: R1~R4 각 축을 적대적으로 훑어 finding 생성.
- **출력**: 갭 리포트 + 판정(`Ready` / `Not ready (Blocking N건)`). **편집 일절 안 함. 판정을 브랜치 노트에도 박지 않음.**
- **도구**: Read·Glob·Grep 만(쓰기 도구 미보유 → 구조적 read-only 보장). 위키의 `wiki-adversarial-reviewer` 와 같은 read-only 리뷰어 계열.
- **리포트 형식**: 가능하면 `rules/reporting-standards.md` 의 보조 형식을 따르되, COMPLETE verdict 보고서가 아니므로 claim-gate hook 의 `*-report.md` 강제 대상이 아니다.
## 6. 컴포넌트 3 — `/depth <branch>` 커맨드 + 루프
1. 인자 검증 → `raw/branch-notes/<branch>.md` 경로 해석(없으면 안내 후 종료).
2. `branch-depth-auditor` 디스패치.
3. 리포트를 **인라인 출력**(기본). 파일로 남기려면 옵션 — 단 claim-gate hook 이 `raw/`·`wiki/`·`docs/` 쓰기를 검사하므로 **브랜치 노트에 쓰지 않고** 게이트 비대상 경로 또는 인라인으로만. read-only 불변식 유지.
4. **루프**: 사람(또는 후속 AI)이 갭을 채움 → `/depth` 재실행 → Blocking 0 = Ready → 개발 착수.
## 7. 컴포넌트 4 — 템플릿 캡처 칸 추가 (얇은 상류 예방, 접근법 C)
`templates/branch-note-template.md` 에 최소 추가(기존 80개는 미변경, 신규부터 적용):
- **Decision Evidence Map 에 `선택 조건` 열 추가** → R2(언제 A, 언제 대안)를 작성 시점에 강제.
- **`## 엣지·실패·의존` 미니 섹션** 추가(또는 구현 가이드 내 고정 라인) → R4 를 작성 시점에 유도.
- 그 이상 비대화 금지(YAGNI). 게이트가 본체, 템플릿은 보조.
## 8. 접근법 B 온램프 (옵트인만)
리포트가 R1 "조사 얕음" 갭을 낼 때, 한 줄 안내만 출력: *"이 갭은 `wiki-decision-researcher` 로 심화 권장."* 감사기는 절대 자동 조사·편집하지 않는다 — 사용자가 원할 때만 직접 호출. (위키의 "사람이 결정하게" 철학 보존.)
## 9. 엣지 처리
- 브랜치 파일 없음 → 에러 후 종료.
- 필수 섹션 결손 → 그 자체가 finding.
- 소스 위키링크가 백틱에 싸여 깨짐(`` `[[...]]` ``) → finding 으로 표면화. 깨진 링크 = 근거 추적 불가 = R1 Blocking 후보. (**P3 와 자연 연결점.**)
- Supporting Claim 의 `#Cn` 앵커가 대상 raw 에 실재하지 않음 → R1 finding.
## 10. 검증 / 수용 기준
- 회귀 보정: 얕은 초기 브랜치 1개(`Not ready` 기대) + 다듬어진 브랜치 1개(`feature-boundary-validation-mapping-contract`, `Ready` 근접 기대)로 게이트 판정이 직관과 일치하는지 확인.
- 자체점검: 각 finding 이 4종 세트(심각도·위치·의구심·방법)를 갖췄는지.
- 회귀: 룰북의 4축(R1~R4)이 §0 의 4개 의구심과 1:1 매핑 유지.
## 11. 다른 고통(P1·P3·P4)과의 연결점
- **P1(계약 충돌)**: R4(다른 계약 의존 링크)가 부분적으로 닿음. 본격 거버넌스는 `feature-contract-registry-governance` 라인의 별도 사이클.
- **P3(백틱/링크)**: §9 에서 본 게이트가 백틱 래핑 링크를 finding 으로 *표면화*는 하지만, P3 의 본체는 **별도 사이클**(P2 구현 완료 후 착수)로 확정. 기존 `wiki-link-verifier`(LLM·수동)는 *타깃 부재* broken link 만 잡고 코드블록은 스킵하므로, **타깃은 멀쩡한데 `` `[[...]]` `` 백틱에 싸여 옵시디언이 렌더링 안 하는** 경우는 미커버 — 진짜 빈틈. 결정론적 버그라 LLM 이 아니라 **py 스크립트가 정답**이며, `wiki_claim_gate.py` 처럼 **PostToolUse hook 으로 걸어 매 저장마다 자동 검출**(수동 수정 제거)하는 것을 권장. 1차 범위는 백틱 래핑 1종에 집중(인라인 코드 내 `[[]]`, `![[]]` 임베드, `#heading` 앵커 부재 등은 확장 여지로만 남김 — YAGNI). 위키에 *옵시디언 문법 검증*이라는 관점 자체가 현재 부재함을 기록.
- **P4(raw→concept 승격 기준)**: 별도 사이클. 단 본 게이트의 "근거화된 판정" 패턴은 P4 의 승격 루브릭에 재사용 가능.
## 12. 산출물 요약
| 파일 | 종류 | 신규/변경 |
|---|---|---|
| `rules/branch-depth-gate.md` | 기준 SSOT | 신규 |
| `.claude/agents/branch-depth-auditor.md` | 에이전트 정의 | 신규 |
| `.claude/commands/depth.md` | 커맨드 정의 | 신규 |
| `templates/branch-note-template.md` | 템플릿 | 변경(선택 조건 열 + 엣지·실패·의존 섹션) |
| (선택) `AGENTS.md` / `CLAUDE.md` | 멀티 CLI 진입점 | 신규 게이트 1줄 등재 여부는 구현 단계 결정 |
@@ -0,0 +1,83 @@
# Design: feature-operational-error-observability-foundation 보강 + template 구조 정합
- 날짜: 2026-06-01
- 대상 문서: `raw/branch-notes/feature-operational-error-observability-foundation.md`
- 부모 SSOT: `raw/project-notes/ca-skeleton-operational-contract.md`
- 목적: (1) branch-note 를 `templates/branch-note-template.md` 표준 구조로 정합, (2) 다관점 브레인스토밍으로 식별한 사각(F1~F8) 반영, (3) 보강 근거를 공식 문서 raw 로 캡처 후 Supporting Claim 연결. **4개 선행 계약(skeleton-package-blueprint / architecture-enforcement / boundary-validation-mapping / resource-identifier) 의 결정과 충돌 금지.**
## 0. 충돌 안전성 원칙 (최상위 제약)
본 작업의 모든 편집은 다음 계약의 결정을 흔들지 않는다:
- `feature-skeleton-package-blueprint-contract` — module/package blueprint, `shared-contract` operational-only.
- `feature-architecture-enforcement-rules` — ArchUnit/Gradle 경계 강제.
- `feature-boundary-validation-mapping-contract` — envelope custom 채택, `MAPPING_FAILED`/`BATCH_PARTIAL_FAILURE` 를 §6 `VALIDATION` 으로 등록.
- `feature-resource-identifier-contract` — ULID, MDC snake_case 정합, never-reuse 패턴.
검증: §6 재정합은 boundary 가 consume 하는 `VALIDATION`/`MAPPING_FAILED`/`BATCH_PARTIAL_FAILURE` 를 보존. 신규 사각은 모두 additive 이거나 sibling/owner branch 로 위임(cross-cite).
## 1. 식별된 사각 (브레인스토밍 결과)
| ID | 사각 | 분류 | 근거 |
|---|---|---|---|
| F1 | 부모 §6 error category 13-목록이 stale — branch/§21/§25 의 10-enum 과 불일치, orphan code 미재태그 | P0 SSOT 정합 | §21 L810/L814, §25 L1065 |
| F2 | snake(MDC)↔camel(envelope)↔kebab(header) 매핑이 branch-note 에 미명시 | P1 과소기술 | §21 L829/L839, §25 L1087 |
| F3 | retryable→`Retry-After`/`X-RateLimit-*` surfacing 계약 부재 | P1 과소기술 | RFC9110-C21/C6, headers.yaml |
| F4 | inbound 헤더 유래 MDC 값 log injection/CRLF sanitization 결정 부재 | P2 진짜 사각 | CWE-117, OWASP Logging |
| F5 | client 제공 traceparent/X-Request-Id 의 trust boundary 결정 부재 | P2 진짜 사각 | W3C-TC-C2/C5 |
| F6 | operational error 의 trace span 기록 규약(OTel exception/status) 부재 | P2 진짜 사각 | OTel Exceptions semconv |
| F7 | error code lifecycle(stability/never-reuse/deprecation) 정책 부재 | P2 진짜 사각 | STRIPE-C2, GOOG-ERR-C4, resource-id D15 대칭 |
| F8 | AUTH→401 에 WWW-Authenticate MUST 미명시 | P2 minor | RFC 9110 §11.6.1 (owner=security-baseline) |
## 2. Template 구조 정합 (section-by-section)
표준 순서: `Parent → 목표/WHY → 범위 → Sources → TODO → 진행 중 메모 → 결정 사항 → Decision Evidence Map → 구현 가이드 → Claims To Verify → 마주친 문제 → Cluster → Daily notes → Closure`.
조치:
- frontmatter `parent_branch:` (빈 값) 추가.
- Parent §에 형제 branch cross-cite 추가(boundary / resource-identifier / api-contract-baseline / log-management / security-operational-baseline / metrics-alerting / distributed-tracing / rate-limit-idempotency).
- 맨 앞 `## Cluster` → 문서 끝으로 이동 + Sub-branches/Errors/Interview prep/Lectures/Blog topics subsection 정비.
- `Sources` → 범위 다음으로 이동.
- 독립 섹션 `error.category Enum`·`MDC Key Standard`·`error.details JSON shape``## 구현 가이드 / Implementation Specification` 의 sub-section 으로 흡수 + 각 sub-section 에 `Trace`(Decision ID + Claim ID) / `UNSUPPORTED_IMPL_DECISION` 3-rule 헤더 부착.
- `## Work Item Contract` (generic boilerplate) → 제거 (Decision Evidence Map + 구현 가이드 등가 대체).
- `판정 기준`·`테스트 계약`·`SSOT Ownership`·`외부 근거/대안 조사` → 유지(보조), template 흐름에 맞춰 위치 정렬. `테스트 계약`=실패 조건 카탈로그 / `Claims To Verify`=검증 미완 주장으로 역할 분담.
- `## 관련 일일 노트` 추가(현재 없음 표기).
## 3. 사각 → 결정 배치
신규 결정(기존 D1~D12 유지, D13~D19 추가):
- D13 (F3 Retry-After surfacing) — `RFC9110-C21`,`RFC9110-C6`; 429 specifics→rate-limit-idempotency cross-cite.
- D14 (F4 log injection sanitization) — `owasp-logging-cheat-sheet#OWASP-LOG-Cx` + CWE-117.
- D15 (F5 trace trust boundary) — `W3C-TC-C2`,`W3C-TC-C5`; trust/regenerate detail→distributed-tracing/security cross-cite (UNSUPPORTED_IMPL).
- D16 (F6 OTel span exception 기록) — `otel-exceptions-semconv#OTEL-EXC-Cx`; span detail→distributed-tracing cross-cite.
- D17 (F7 error code lifecycle never-reuse) — project-decision + `STRIPE-C2` + `GOOG-ERR-C4`; resource-id D15 대칭; deprecation→api-compatibility cross-cite (UNSUPPORTED_DECISION).
- D18 (F8 401 WWW-Authenticate) — security-baseline owner cross-cite (신규 claim 없음).
- D19 (F2 ID 명명 3중 매핑) — project-ssot(§21 L829/L839, §25 L1087).
각 결정은 Decision Evidence Map 행 + 구현 가이드 sub-section + Claims To Verify 행으로 전개.
## 4. 공식 문서 raw 캡처
신규 2건 (wiki-source-summarizer):
- `raw/official-docs/owasp-logging-cheat-sheet.md` (F4) — log injection/CRLF neutralization, 구조화 로깅.
- `raw/official-docs/otel-exceptions-semantic-conventions.md` (F6) — exception.type/message/stacktrace, span status ERROR.
재사용 (기존 raw):
- `rfc9110-http-semantics.md` (RFC9110-C21/C6) — F3.
- `tracing-w3c-trace-context-spec.md` (W3C-TC-C2/C5) — F5.
- `stripe-resource-id-convention.md` (STRIPE-C2) + `google-api-error-format.md` (GOOG-ERR-C4) + `stripe-error-format.md` — F7.
## 5. 부모 project-note §6/§8 정합 (F1)
- §6: 13-목록 → 10-enum 통일(이미 §21 L814 결정), orphan code re-tag(`DB_*`→TRANSIENT_DEPENDENCY/INTERNAL, `CACHE_UNAVAILABLE`→TRANSIENT_DEPENDENCY, `NOTIFICATION_SEND_FAILED`→TRANSIENT_DEPENDENCY, `MESSAGE_PUBLISH_FAILED`→TRANSIENT_DEPENDENCY, `DATA_INTEGRITY_VIOLATION`→DATA_INTEGRITY 등 — 실제 매핑은 error-codes.yaml 와 cross-check). `VALIDATION`/`MAPPING_FAILED`/`BATCH_PARTIAL_FAILURE` 보존.
- §8: log field 명명을 mdc-keys.yaml snake_case 와 정합 — snake(MDC)↔camel(envelope) seam 1줄 명시.
## 6. 실행 순서
1. spec (본 문서). 2. 공식문서 캡처 2건(병렬). 3. branch-note 구조 정합 + D13~D19 반영. 4. 부모 §6/§8 정합. 5. wiki-link-verifier 검증.
## 7. 비목표 (YAGNI)
- tenant 모델 구현(→feature-tenant-context-policy).
- 실제 error-codes.yaml/mdc-keys.yaml row 편집(→registry-governance, ca-tmpl repo).
- distributed-tracing/security/rate-limit branch 의 owner 결정 작성(cross-cite 만).
@@ -0,0 +1,107 @@
# Design: wiki-structure-lint — 결정론적 문서 구조 린터
- 날짜: 2026-06-01
- 대상: `.claude/hooks/wiki_structure_lint.py` (신규), `/depth` 연동, (2차) PostToolUse hook
- 목적: 위키 문서가 **자기 `source_type` 템플릿의 양식을 따르는지** 결정론적으로(파이썬) 검증. "templates대로 안 된 문서"를 코퍼스 전수로 색출하고, P3(옵시디언 링크 문법)와 depth 게이트의 기계적 사전체크를 한 도구로 묶는다.
---
## 0. 배경 / 왜 결정론적 레이어인가
`branch-depth-gate`(LLM 의미 게이트, 별도 스펙)를 설계하다, 검증을 결정론(파이썬) vs 의미(LLM)로 나누는 게 자연스럽다는 결론. 파이썬은 **틀·양식 + 기계적으로 판별 가능한 것**(섹션 존재, 링크 문법, 앵커 실재)만 보고, 의미(claim 깊이 L0/L1, 조건의 진위)는 LLM 감사기가 본다.
여기에 사용자 관찰이 더해짐: **"지금 문서들이 templates대로 안 되어 있는 게 매우 많다."** → 결정론 레이어를 *먼저* 짓는 게 즉시 가치(비적합 문서 색출) + 싼 게이트 우선.
기존 자산과의 경계:
- `wiki_claim_gate.py` (PreToolUse hook, 유지): 특정 표(Claims Extracted 등) 존재를 write 시점에 강제. 본 린터는 이를 일반화(전 source_type · 전 섹션).
- `/lint` (LLM, 유지): 의미 품질(과장·canonical 우회 등). 본 린터는 결정론 구조만.
## 1. 검증 3군 (전부 이진 PASS/FAIL — 심각도 단계 없음)
문서당 결과는 `PASS` 또는 `FAIL + 사유 목록`. 사용자 결정: "양식대로 안 되어있으면 불통."
### C1. 템플릿 적합성
문서는 자기 `source_type` 템플릿의 **필수 + 무마커 섹션 전부** + **frontmatter 키 전부**를 가져야 PASS.
- `(있다면)`·`(있을 때)`·`(전용)`·`(infra 전용)` 등 **optional 마커 섹션**은 없어도 PASS.
- **무마커 섹션 = 필수**로 간주(양식이므로).
- 템플릿에 없는 **추가 섹션은 허용**(불통 아님).
- `source_type`이 어느 템플릿과도 매칭 안 되면 → 불통(`UNMAPPED_SOURCE_TYPE`).
### C2. 옵시디언 링크 문법 (= P3 흡수)
- `` `[[...]]` `` 인라인 코드/백틱에 싸인 위키링크 → 불통(옵시디언이 링크로 렌더 안 함).
- `[[경로]]` 타깃 파일 부재 → 불통(`BROKEN_LINK`).
- `[[파일#앵커]]` 의 앵커가 대상에 부재 → 불통(`DANGLING_ANCHOR`, best-effort: 헤딩 또는 Claim ID 토큰 검색).
- fenced code block(```` ``` ````) 내부의 `[[...]]` 는 예시이므로 **스킵**(false positive 방지). 인라인 백틱은 검출.
### C3. depth 구조 사전체크 (branch-note 만)
- Decision Evidence Map 의 `선택 조건` 셀 비어 있음 → 불통.
- `## 엣지·실패·의존` 섹션 없음/내용 없음 → 불통.
- (신규 템플릿 적용 브랜치에 발동. 기존 80개는 C1에서 "섹션 없음"으로 먼저 걸림.)
## 2. source_type → 템플릿 매핑 (거의 자동 도출)
해석 순서 (2026-06-01 정리 후 — **모든 템플릿이 frontmatter source_type 를 직접 선언**, fallback 제거됨):
1. 각 템플릿 frontmatter `source_type:` 파싱. **다중값은 `|`/`,` 구분**`raw-source-template: official-doc | company-tech-blog | personal-blog`.
2. `daily-task` 는 문서 frontmatter `track:`(develop/infra)으로 분기.
3. (보조) `## source_type 허용값` 섹션 파싱은 backward 호환용으로 유지.
4. 위 어디에도 안 걸리는 source_type → `UNMAPPED_SOURCE_TYPE` 불통.
명시 결과 (빈 source_type 4종 해소): `concept-template ← llm-generated`, `raw-source-template ← official-doc|company-tech-blog|personal-blog`, `interview-template ← interview`, `source-summary-template ← source-summary`(둘 다 wiki/concepts 층이라 구분 필수). 매핑 SSOT 가 전부 템플릿 frontmatter 안에 있음(스크립트 상수 fallback 0).
**nav/hub/meta 제외**: layer 최상위 직속 파일(`wiki/llm-wiki.md`·`wiki/log.md`) + `README.md`/`log.md`/`index.md` 는 템플릿 콘텐츠가 아니므로 린트 스킵. frontmatter 없는 스텁은 섹션 누락 도배 대신 `NO_FRONTMATTER` 1건으로 표면화.
## 3. 헤더 정규화 매칭
번역·괄호주석·순서 차이를 흡수:
- `## Parent / 부모 (필수)` → 선두 `## ` 제거 → 괄호 `(...)` 제거 → `/` 분리 → 각 토큰 strip+소문자 → `{parent, 부모}`.
- 문서 헤더도 같게 정규화. **순서 무관 집합 비교.** 한쪽(한글/영문)만 있어도 매칭.
- optional 판정: 원본 헤더에 optional 마커 정규식(`있다면|있을 때|있으면|전용|optional`) 포함 여부.
## 4. CLI / 출력 / 통합
- **표준 라이브러리만** (claim_gate.py 스타일, 의존성 0). frontmatter 는 라인 파싱(`^key:`).
- 호출:
- `python3 .claude/hooks/wiki_structure_lint.py --file <path>` — 단일. `/depth` 사전체크·hook용.
- `python3 .claude/hooks/wiki_structure_lint.py --all``raw/`·`wiki/` 전수 스윕.
- 출력: 문서별 `PASS` / `FAIL` + 사유(rule code · line · 설명). `--all` 은 끝에 요약(전체 N, FAIL M, source_type별 집계).
- exit code: FAIL 있으면 ≠ 0 (hook/CI 게이트용).
- `/depth` 연동: 먼저 `--file` 결정론 검사 → 구조 불통이면 그것부터 보고, 통과 시 LLM `branch-depth-auditor` 로.
- (2차) PostToolUse hook: 저장 시 C2 자동 검출(P3 목적). 1차는 CLI만.
## 5. 검증 / 수용 기준
- `--all` 이 정상 종료하고 source_type별 집계를 출력.
- 알려진 비적합(`personal-blog`·`meta`·`error` source_type)이 `UNMAPPED_SOURCE_TYPE` 로 잡힘.
- 의도적으로 백틱 래핑한 테스트 링크가 C2 로 잡힘.
- 잘 정비된 문서 1개(예: 최근 branch-note)가 PASS.
- false positive 점검: fenced code block 내 예시 `[[...]]` 가 BROKEN 으로 안 잡힘.
## 6. 자동수정(autofix) — 백틱 래핑만
사용자 결정으로 **안전한 한 종류만** 자동수정 도입: 순수 `` `[[...]]` `` 래핑 → `[[...]]`.
- `--fix` (기본 dry-run, 변경 미리보기) / `--fix --apply` (실제 기록).
- fenced code block 내부·혼합 코드 스팬(`` `foo [[x]] bar` ``)은 **건드리지 않음**(보수적).
- git 미사용 환경이므로 `--apply` 전 **폴더 백업 필수**.
- 2026-06-01 1회 실행 결과: 388파일/3117곳 수정, 백업 `llm-wiki-backup-20260601-214319`.
비목표:
- 그 외 autofix 없음(누락 섹션·frontmatter 는 내용 생성 불가 → 마이그레이션 영역).
- 의미 품질 판정 없음 — LLM `/lint`·`branch-depth-auditor` 담당.
- 멀티 CLI 전파(Codex/Gemini) 범위 밖.
## 6.1 알려진 한계 (구현 중 보정)
- 링크 타깃 해석: `[[x.md]]` 의 `.md` suffix 허용, templates/ 도 유효 타깃으로 인덱싱(초기 FP 2종 수정 완료).
- placeholder 링크(`[[X]]`·`[[URL]]`·`[[double bracket]]` 등 예시 텍스트)가 BROKEN_LINK 로 잡힘 — 향후 예시 라인 스킵 규칙으로 보정 여지.
- 템플릿 드리프트(MISSING_SECTION 571·MISSING_FRONTMATTER 89)는 본 린터의 검출 대상이되, 해소는 **문서→템플릿 마이그레이션**(별도 사이클). 린터가 그 진척도 측정기 역할.
## 7. 산출물
| 파일 | 작업 |
|---|---|
| `.claude/hooks/wiki_structure_lint.py` | 신규 (C1+C2+C3, CLI 2모드) |
| `/depth` (depth 게이트 구현 시) | 사전체크로 본 린터 호출 |
| (2차) `.claude/settings.json` | PostToolUse hook 등록 (C2 자동) |
@@ -0,0 +1,84 @@
# branch-spec 조립 파이프라인 — 설계 (Phase 1, 축소판)
> 작성: 2026-06-02 · 상태: 합의됨(축소판) · 후속: Phase 2(검증 강화), Phase 3(윤문)
## 1. 배경 / 문제
현재 자동화는 좋은 기반이지만 `/branch`(빈 스캐폴드)와 `/depth`(읽기 전용 깊이 게이트) **사이를 *내용으로 채우는* 단계가 없다**. 결과적으로:
- 문서 작성 agent는 입력이 부족하면 `NEEDS_CONTEXT`로 멈춘다(`.claude/agents/wiki-doc-author.md``## Required Inputs`). 안전하지만 "되묻지 않는 구조적 문서 작성"과는 다르다.
- agent별 필수 입력이 산문으로 박혀 있어 *재사용 가능한 입력 계약*이 없다.
"되묻지 않으려면 controller가 dispatch 전에 입력 패키지를 완성"해야 한다. 그 controller 단계가 빠진 것이 핵심 갭이다.
## 2. 범위 (축소판)
**순수 추가 — 신규 파일 2개. 기존 template·linter·문서는 건드리지 않는다.**
| 산출물 | 성격 |
|---|---|
| `rules/subagent-input-contracts.md` | dispatch 전 입력을 agent별 form schema로 고정 |
| `.claude/commands/branch-spec.md` | 조립 오케스트레이터 명령 |
**명시적 비범위 (후속 phase로 연기):**
- G3 중첩 체크리스트 포맷 — **기존 Decision Evidence Map 표를 그대로 사용**(template 개정·린터 변경·마이그레이션 없음).
- G4 한국어 윤문 standard / "쉬운 설명" 기준 → Phase 3.
- G5 린터 C1/C3 hook 추가, `/lint --fix-plan` → Phase 2.
## 3. 확정된 설계 결정
| ID | 결정 | 근거 |
|---|---|---|
| DD1 | 근거 없는 결정은 **자동조사 후 라벨링** — 먼저 webfetch 조사로 Claim 생성 시도, 실패 시에만 `UNSUPPORTED_DECISION` + trade-off. 추측해서 FACT 승격 절대 금지 | "되묻지 않기" vs "근거 없는 결정 금지" 충돌 해소. CLAUDE.md §11 준수 |
| DD2 | 명령 경계: `/branch``/branch-spec` → 끝에 `/depth` 자동 호출 | 세 명령이 각자 한 일(생성/채움/검증). 루프가 자동으로 닫힘 |
| DD3 | 아키텍처: **오케스트레이터 명령**(접근 1). 새 agent 0개, 기존 서브에이전트 재사용 | "agent 늘리지 말고 controller 계약" 합의. 서브에이전트는 다른 서브에이전트를 못 부르지만 명령을 실행하는 메인 에이전트는 부를 수 있음 |
| DD4 | 자동조사는 **bounded** — 회당 최대 N개 결정(기본 6), 초과분은 `deferred`로 명시 로그(silent 절단 금지) | 조사 비용(토큰·시간) 폭주 방지 |
| DD5 | 최종 보고는 **짧은 사람용 요약**(채운 것/UNSUPPORTED/조사/depth 판정), 상세는 노트에 | 요구사항 3: 구조적·쉬운 답변 + 상세는 문서 |
## 4. 데이터 흐름 (`/branch-spec <slug>`)
```
1. 전제 확인 raw/branch-notes/<slug>.md 존재? 없으면 /branch 먼저 안내(종료)
2. Sources 수집 노트 ## Sources + 사용자 제공 URL
└ URL → wiki-source-summarizer (webfetch + verbatim + self-grep)
3. 결정 후보 추출 source Claim에서 decision 후보 + 대안 도출
4. 자동조사 Supporting Claim 없는 결정마다 (DD4 bound 적용):
(bounded) └ wiki-decision-researcher (공식문서 + 대기업 블로그, 대안 비교)
5. 라벨링 조사 후에도 근거 없으면 UNSUPPORTED_DECISION + trade-off 한 줄
6. 노트 채움 Decision Evidence Map 표(기존 포맷) + 구현 가이드 skeleton
7. 자동 /depth wiki_structure_lint.py(1차) + branch-depth-auditor(2차) → Ready/Not-ready
8. 요약 보고 채운 것/UNSUPPORTED/조사 N/depth 판정 (DD5)
```
## 5. 컴포넌트 계약
### 5.1 `rules/subagent-input-contracts.md`
기존 rules 문서 스타일(표 + 명명된 실패 모드). 각 agent/명령의 **필수 입력 · 선택 입력 · 누락 시 행동**을 고정. 최소 커버: `/branch-spec`, `wiki-source-summarizer`, `wiki-decision-researcher`, `wiki-doc-author`. 산문으로 흩어진 Required Inputs를 재사용 가능한 schema로 승격하되 **agent 본문과 모순되지 않게** 참조 관계만 명시(SSOT 이중화 회피).
### 5.2 `.claude/commands/branch-spec.md`
frontmatter(`description`, `argument-hint`) + §4 흐름 + DD4 bound + 규칙. `rules/subagent-input-contracts.md`, `rules/branch-depth-gate.md`, `templates/branch-note-template.md`를 참조.
## 6. 요구사항 충족 매핑 (Phase 1)
| 요구사항 | Phase 1 | 후속 |
|---|---|---|
| 1 체계적 관리 | 입력 계약으로 강화 | |
| 2 비효율 x | 자동조사로 수동 왕복 제거 | |
| 3 구조적·쉬운 답변 | 명령 요약 + 상세는 노트(DD5) | |
| 4a 검증 매번 | 자동 /depth(DD2) | hook C1/C3 → P2 |
| 4b webfetch 조사 | 자동조사 단계(DD1) | |
| 4c 개수 아닌 근거 | R1 출처 적정성 유지 | |
| 4d 대안비교+언제 | 기존 표의 선택조건 컬럼 | 체크리스트 가독성 → P3 후보 |
| 4e wiki 문서화 기준 | 기존 /ingest 유지 | |
| 4f~4g 윤문 | — | Phase 3 |
## 7. 리스크 / 비범위 확인
- 기존 표 유지 → 린터 C3·기존 노트 **깨질 게 없음**(축소판의 핵심 이점).
- 자동조사 무한 확장 → DD4 bound로 차단.
- SSOT 이중화(입력 계약 vs agent 본문) → 5.1에서 참조 관계만, 값 복제 금지.
</content>
</invoke>
@@ -0,0 +1,136 @@
# coverage 완전성 게이트 — 설계
> 작성: 2026-06-02 · 상태: 합의됨 · 짝 문서: `2026-06-02-branch-spec-assembly-pipeline-design.md`
## 1. 배경 / 문제
현재 파이프라인에는 **깊이 게이트(`/depth`)는 있지만 완전성 게이트는 없다.**
- `/depth`(R1~R4)는 노트에 *적힌* 결정이 충분히 깊고 코딩 가능한지 검사한다.
- 그러나 *"적어야 할 결정이 다 적혔는가"* 는 어느 축도 보지 않는다. 브랜치의 `In scope` 리스트는 `/branch` 생성 시 사람이 손으로 쓴 것이며, 그 리스트 자체의 완전성은 게이트되지 않는다.
- 결과: `In scope`가 불완전해도 `/depth`는 통과한다. 실제 사례 — `feature-business-rule-validation-contract`가 "Ready" 판정을 받았지만, ad hoc grep 결과 i18n 메시지 정책 / 입력 정규화 / fail-fast 오류수집 정책 등 표준 검증 관심사가 어느 브랜치에도 결정으로 존재하지 않음이 드러났다.
ca-tmpl은 **보편적 백엔드 프로젝트가 갖춰야 할 항목을 빠짐없이 담는 템플릿**이 목적이므로, 완전성(coverage)은 깊이만큼 중요한 축이다.
비유: `/depth`는 "네가 푼 문제는 잘 풀었다"를 채점하지만 "3번 문제를 통째로 안 풀었다"는 못 잡는다. `/coverage`가 그 빠진 문제를 잡는다.
## 2. 범위
**순수 추가 + 최소 수정. depth 게이트의 형제(sibling)로 설계.**
| 산출물 | 성격 |
|---|---|
| `.claude/commands/coverage.md` | `/coverage` 명령 (브랜치 모드 + 프로젝트 모드) |
| `.claude/agents/coverage-auditor.md` | 2차 LLM 의미 감사 서브에이전트 (읽기 전용) |
| `rules/coverage-gate.md` | 판정 규칙 (3단계 심각도 + 명명된 실패 모드) — `branch-depth-gate.md`의 짝 |
| `wiki/projects/ca-tmpl/coverage-matrix.md` | 프로젝트 전체 현황표 (**자동 생성물**, 손유지 금지) |
| ~~(린터 확장)~~`/coverage` 인라인 | **변경(2026-06-02)**: 공유 린터를 건드리면 기존 노트 대량 파손 → 1차 결정론 검사를 `coverage.md` 인라인(grep, target 한정)으로 구현. 공유 린터 무수정 |
| (수정) `templates/branch-note-template.md` | `## Coverage` 섹션만 추가(optional 마커). `governing_docs:`**template 에 안 넣음**(§4.1 구현 노트) |
| (수정) `.claude/commands/branch-spec.md` | `/coverage` 자동 실행 끼우기 |
**명시적 비범위:**
- 외부 taxonomy(OWASP 등)를 기준으로 삼지 않는다 — 기준은 프로젝트 자체의 canonical 문서.
- `.codex` / `.agents` 병렬 포트는 후속(이번엔 `.claude`만).
- 기존 브랜치 노트 일괄 마이그레이션(`governing_docs` 소급 부여)은 후속 — 점진 적용.
## 3. 확정된 설계 결정
| ID | 결정 | 근거 |
|---|---|---|
| DD1 | **범위 = 브랜치 레벨 + 프로젝트 레벨 (둘 다, 계층적)** | 브랜치 slice 완전성 + 브랜치 집합 전체의 owner-less 관심사 둘 다 필요 |
| DD2 | **기준 = 설계 문서 1순위 → 선례 브랜치 → ca-tmpl 코드.** 겹치면 owner 브랜치 위임 | 사용자 방법론: "문서가 제일 먼저 기준, 그걸 ca-tmpl에 구현, 겹치면 owner 위임". 외부 taxonomy 아님 |
| DD3 | **구조 = 2단계 (얇은 결정론 린터 + LLM coverage-auditor)** | depth 패턴 미러링. 기계로 볼 수 있는 건 1차에서 싸게, 의미 비교는 2차 |
| DD4 | **판정 = 3단계 심각도 (Blocking / Should-fix / Advisory). Covered = Blocking 0** | depth와 동일. owner 위임 케이스를 Blocking 아닌 Should-fix로 분리 → false block 방지 |
| DD5 | **접근법 C (하이브리드).** 브랜치→문서 매핑은 frontmatter `governing_docs` 한 줄로 명시, `## Coverage`와 프로젝트 matrix는 **자동 생성**(손유지 금지) | drift 회피(이번 세션 교훈: 손유지 SSOT는 어긋난다 — registry 주석 L580 stale 사례). tag 추론은 엉뚱한 문서를 잡을 위험 → 명시 한 줄로 차단 |
| DD6 | **끼우는 위치 = `/branch-spec` 맨 끝, `/depth`와 나란히.** 둘 중 하나라도 실패 시 `/branch-spec`으로 루프백 | 두 게이트 대칭. coverage가 찾은 빠진 항목을 채우고 → 그 새 결정을 depth가 다시 깊이 검사 → 둘 다 통과 시 완성 |
## 4. 데이터 모델 (가벼움 — 사람이 쓰는 건 한 줄)
### 4.1 브랜치 frontmatter `governing_docs:`
이 브랜치가 구현해야 할 기준 canonical 문서를 명시. tag 추론의 불안정성 제거.
```yaml
governing_docs: [wiki/projects/ca-tmpl/api-error-envelope-design]
```
- 0개 또는 다수 가능.
- **구현 노트 (2026-06-02)**: 공유 구조 린터(`wiki_structure_lint.py`)는 `--file` 모드에서 `is_completeness_checkable` 게이트 없이 모든 template fm_key 를 무조건 요구한다. 따라서 `governing_docs`**template frontmatter 에 넣으면 기존 80개 노트가 전부 깨진다** → template 에 추가하지 않음. 대신 **`/coverage` 1차 인라인 검사**가 ca-tmpl 브랜치 한정으로 존재를 요구(`NO_GOVERNING_DOC`). 노트에 *추가 키*로 들어가는 건 린터가 막지 않으므로(required 만 검사, extra 는 허용) per-branch 로 안전하게 부여 가능. 기존 ca-tmpl 노트 backfill 은 점진 마이그레이션(비범위).
### 4.2 브랜치 노트 `## Coverage / 관심사 커버리지` 섹션
coverage-auditor가 **생성**. 손유지 금지.
| 관심사 | 상태 | owner | 심각도 | 근거 |
|--------|------|-------|--------|------|
| syntax/shape 검증 | covered-here | — | — | D1 |
| cross-field 검증 | delegated | feature-boundary-validation-mapping | OK | §Audit 링크 |
| i18n 메시지 정책 | **missing** | (없음) | 🔴 Blocking | governing doc §X 언급, 결정 없음 |
- 상태 3종: `covered-here`(이 브랜치가 다룸) / `delegated`(다른 owner) / `missing`(아무도 안 맡음).
### 4.3 `wiki/projects/ca-tmpl/coverage-matrix.md`
프로젝트 모드가 모든 브랜치 `## Coverage`에서 **생성**하는 뷰. `관심사 → owner 브랜치 → status`, owner-less(아무 브랜치도 안 맡은 관심사) 강조. 생성물이라 drift 불가.
## 5. 게이트 로직
### 5.1 1차 — 결정론 린터 (싸고 빠름)
사람 판단 불요 항목만:
- `governing_docs` frontmatter 존재?
- `## Coverage` 섹션 존재?
- `governing_docs` 링크가 vault에 실재(깨진 링크 아님)?
심각하게 깨졌으면(필드/섹션 누락) 그것부터 고치도록 안내, 2차 보류 — depth 1차와 동일 정책.
### 5.2 2차 — coverage-auditor (LLM 의미 감사, 읽기 전용)
브랜치 모드 입력: 브랜치 노트 경로. 다음 셋을 **실제로 읽고** 비교:
1. `governing_docs` 문서 — "있어야 할 관심사" 열거.
2. 선례(완성) 형제 브랜치 — 이미 누가 무엇을 owner인지.
3. ca-tmpl 코드(`/home/donghyeon/workspace/ca-tmpl/src`, `docs/registries`) — 말로만 있는지 실제 구현인지.
→ governing 문서의 각 관심사를 브랜치 In-scope/결정과 대조해 `## Coverage` 표 생성 + 3단계 판정.
프로젝트 모드(`--project`): 전체 canonical 문서에서 관심사를 열거하고, 각 브랜치 `## Coverage`와 cross-ref → owner-less 관심사를 Blocking으로 `coverage-matrix.md` 생성.
### 5.3 판정 (3단계 심각도)
| 신호 | 의미 | 트리거 |
|------|------|--------|
| 🔴 Blocking (Not-covered) | 진짜 빠짐, 아무 브랜치도 안 맡음 | governing 문서가 요구하는 관심사가 이 브랜치에도 없고 다른 owner도 없음 |
| 🟡 Should-fix | 다른 브랜치 owner인데 위임 링크 누락 | 관심사를 sibling이 소유하나 본 노트 §Audit/§Coverage에 위임 링크 없음 |
| ⚪ Advisory | 있으면 좋음 | governing 문서가 권고하나 핵심 아님 |
**Covered = Blocking 0건.** Should-fix는 사용자 "감수" 선언 시 통과(리포트 기록) — depth와 동일.
### 5.4 명명된 실패 모드 (`rules/coverage-gate.md`)
- `MISSING_CONCERN` (Blocking): governing 문서 관심사가 어느 브랜치에도 결정으로 없음.
- `UNLINKED_DELEGATION` (Should-fix): owner sibling 있으나 위임 링크 누락.
- `NO_GOVERNING_DOC`: `governing_docs` 미지정 → 기준 부재로 판정 불가(1차에서 차단).
- `STALE_OWNER`: §Coverage가 가리키는 owner 브랜치가 실제로 그 관심사를 안 가짐(코드/노트 대조 불일치).
## 6. 통합 — `/branch-spec` 흐름
```
ground truth 읽기 → 결정 추출 → 채움
→ [depth 검사: 적은 게 깊은가]
→ [coverage 검사: 빠뜨린 게 있는가] ← 신규
├─ 둘 다 통과 → 완료 ✅
└─ depth Not-ready 또는 coverage 🔴 → 다시 branch-spec(보강) → 재검사 (루프)
```
`/coverage`는 독립 명령이며 `/branch-spec`이 끝에서 자동 호출(= `/depth` 호출 방식과 동일).
### 6.1 프로젝트 외 브랜치 면제
`governing_docs` 미지정 + `related_projects`에 ca-tmpl 없는 브랜치(예: keycloak 학습 노트)는 coverage 면제 — 1차 린터가 프로젝트 소속 여부로 판단.
## 7. 요구사항 충족 매핑
| 사용자 요구 | 충족 |
|---|---|
| 깊이 외에 완전성도 검사 | DD1~DD4 — 빠진 관심사 3단계 판정 |
| 문서를 기준으로 | DD2 — `governing_docs` canonical 문서 1순위 |
| 선례 브랜치처럼 조사 + 겹치면 owner 위임 | 5.2 — 선례 브랜치 읽기 + `delegated` 상태 |
| branch-spec의 depth 자동실행 구조처럼 | DD6 — 맨 끝 자동 호출 + 루프백 |
| 별도 명령 | `.claude/commands/coverage.md` |
## 8. 리스크 / 비범위 확인
- **drift 리스크**: `## Coverage`·matrix를 손유지하면 또 어긋남 → **자동 생성만**(DD5). 사람이 쓰는 건 `governing_docs` 한 줄뿐.
- **governing_docs 오지정 리스크**: 사람이 엉뚱한 문서를 적으면 판정 무의미 → 1차 린터가 링크 실재만 확인, 적정성은 2차 auditor가 "이 문서가 이 브랜치 주제와 맞나" 한 줄 코멘트로 surface.
- **canonical 문서 자체가 불완전하면?** coverage는 "문서 대비" 완전성만 보장. 문서 자체의 완전성은 별도(후속 — 문서 레벨 critic). 이번 범위 밖.
- **비용**: 프로젝트 모드는 전체 canonical + 전체 브랜치 §Coverage 재독 → 비쌈. 주기적 실행 전제(매 브랜치마다 아님).
@@ -0,0 +1,55 @@
# 한국어 윤문 standard — 설계 (Phase 3)
> 작성: 2026-06-02 · 상태: 구현됨 · 선행: [Phase 2 검증 강화](2026-06-02-validation-strengthening-design.md)
## 1. 범위 (G4)
요구사항 4f("이해 쉬우면서 자세")·4g("blog/면접/포폴 한국어 윤문 최적화")·3("구조적·쉬운 답변")을 충족.
| 산출물 | 변경 |
|---|---|
| `rules/prose-style.md` | **신규** — 윤문 + "쉬운 설명" 기준 |
| `.claude/commands/interviewize.md` | 규칙에 prose-style 참조 추가 |
| `.claude/commands/blogify.md` | 본문 작성 시 prose-style 참조 추가 |
| `CLAUDE.md` §9 + rules 목록 | prose-style 등록 |
## 2. 사용자 지정 취향 (고정값)
| 항목 | 결정 |
|---|---|
| 문체 | 존댓말 (blog/portfolio `합니다체`, interview 말하듯 존댓말) |
| 길이 | 적당히 길게 — 주장→이유→예시 한 묶음. 반복·수식 늘리기 금지 |
| 용어 | 개발 용어만 영어(`connection pool`, `idempotent` 등), 나머지 한국어. 억지 한글화 금지 |
## 3. "쉬운 설명" 5원칙
쉬운 요약 먼저 → 전문 용어 첫 등장 시 한 줄 풀이 → 추상→구체 → 맥락부터 → 쉬움과 자세함 양립(깊이 유지).
## 4. 사실 경계 (중요)
윤문은 표현만 다듬고 **사실 등급을 바꾸지 않음**(CLAUDE.md §6, §11). `POLISHED_OVERCLAIM` 실패 모드로 명시 — 미검증 사실을 매끄러운 문장으로 포장 금지.
## 5. 명명된 실패 모드
`TONE_INCONSISTENT` · `OVER_TRANSLATED` · `JARGON_WALL` · `NO_PLAIN_SUMMARY` · `BLOATED` · `TOO_TERSE` · `POLISHED_OVERCLAIM`.
## 6. 적용 방식
자동 린터 검사는 하지 않음(윤문은 정성적). interviewize/blogify가 본문 작성 시 참조하는 **가이드 규칙**. canonical(concepts/projects)은 권고로만.
## 7. 요구사항 충족 (3개 phase 종합)
| 요구사항 | 충족 phase |
|---|---|
| 1 체계적 관리 | P1 입력 계약 |
| 2 비효율 x | P1 자동조사 + P2 fix-plan |
| 3 구조적·쉬운 답변 | P1 명령 요약 + P3 쉬운 설명 기준 |
| 4a 매번 검증 | P2 hook C1/C3 조건부 |
| 4b webfetch 조사 | P1 자동조사 |
| 4c 개수 아닌 근거 | 기존 R1 + P1 |
| 4d 대안비교+언제 | 기존 표(체크리스트 G3는 보류) |
| 4e wiki 문서화 기준 | 기존 /ingest |
| 4f 쉬우면서 자세 | P3 쉬운 설명 5원칙 |
| 4g 한국어 윤문 | P3 prose-style |
| 4h 전체 자동화 | P1 /branch-spec 오케스트레이션 |
</content>
@@ -0,0 +1,97 @@
# Design: 옵시디언 링크 검증 강화 (wiki-structure-lint P3 hardening)
- 날짜: 2026-06-02
- 대상: `.claude/hooks/wiki_structure_lint.py` (C2 링크 검사 + vault index)
- 선행: [[docs/superpowers/specs/2026-06-01-wiki-structure-lint-design]] (린터 최초 설계). 본 문서는 그 §6.1 "알려진 한계"를 정확성 방향으로 해소하는 hardening 패스.
- 목적: 옵시디언 그래프뷰의 회색 노드(unresolved link)가 누적되는 문제를, 린터를 **정확하게**(false positive 제거 + 옵시디언 문법 전 형태 커버) 만든 뒤 **zero-tolerance로 강제**하여 근절.
---
## 0. 배경 / 문제
사용자가 Obsidian 그래프뷰에서 회색 노드(타깃 파일이 없는 링크)가 많음을 관찰. 린터는 `BROKEN_LINK`*검출은 하지만* PostToolUse 훅이 non-blocking이라 누적됨. 추가로 두 종류의 **린터 false positive**가 신호를 오염시켜 강제를 불가능하게 함.
vault 전체 진단 (425개 문서):
- `BROKEN_LINK` 72건 = (a) 린터 오탐 `.drawio` 첨부 ~8 + (b) 진짜 회색 노드(미생성 daily-note 36 + 미작성 concept/official-doc ~22) + (c) 편집 실수(예시 텍스트가 링크화) ~3
- `BACKTICK_WRAPPED_LINK` 36건 = 다수가 표 셀 경계를 넘는 페어링 오탐
- `DANGLING_ANCHOR` 3, `NO_FRONTMATTER` 3
실사용 옵시디언 링크 형태(grep): embed `![[]]` 9, alias `[[a|b]]` 2, heading anchor `[[a#h]]` 27, 비-md 첨부(drawio 8 + svg 1). block-ref `[[a#^id]]`·heading-only `[[#h]]`는 0건.
## 1. 확정된 설계 결정 (사용자)
1. **정확성 먼저 → 그 위에 강제.** false positive 제거 + 문법 전 형태 커버가 선행.
2. **깨진 링크 = 무조건 에러 (zero-tolerance).** 의도적 forward-reference(아직 안 쓴 문서로의 링크)도 예외 없음 — 스텁 생성 또는 링크 제거로 해소. **→ 선행 설계 §6.1의 "예시 라인 스킵 규칙" 아이디어는 폐기**: 바 텍스트 `[[double bracket]]`은 Obsidian에서 실제 회색 노드이므로 *스킵이 아니라 flag*하고 code-span으로 고치는 것이 정합.
3. **강제 지점 = PostToolUse 훅(저장 즉시 경고, 정확도 강화) + 수동 `--all` 게이트(exit 1).** git pre-commit은 도입 안 함.
## 2. Part A — False positive 제거 (정확성 토대)
### A1. Vault index에 비-md 첨부 포함
현재 `build_vault_index``rglob("*.md")`만 인덱싱 → `[[…architecture-modules.drawio]]`가 실존하는데도 `BROKEN_LINK` 오탐(8건).
- 모든 파일을 인덱싱(`rglob("*")` 중 파일만; **숨김 디렉터리(`.git` 등) 및 경로에 `/.`이 포함된 항목 제외** — git 오브젝트/캐시 인덱싱 방지).
- **md**: 현행대로 `.md` strip → rel-path(확장자 없음) + basename(stem).
- **비-md**: 확장자 *포함* rel-path + 확장자 포함 basename으로 등록(Obsidian은 첨부를 확장자 포함으로 링크).
- C2 타깃 해석에서 `[[x.md]]`는 strip 후 md index 조회, 그 외 확장자(`.drawio`/`.svg`/…)는 확장자 포함으로 조회.
- C1 린트 대상(`iter_docs`)은 변경 없음 — md만 구조 검사. index 확장은 *링크 타깃 해석*에만 영향.
### A2. Backtick 셀 경계 페어링 버그 수정
현재 `BACKTICK_LINK = re.compile(r"`[^`\n]*\[\[[^\]]*\]\][^`\n]*`")`는 backtick을 좌→우 연속 페어링하지 않고, 임의의 두 backtick 사이에 낀 위키링크를 매칭 → 표의 *Decision 칸* 인라인코드와 *Supporting Claims 칸* 인라인코드 사이에 위치한 정상 위키링크를 오탐.
- **수정**: 위키링크가 *진짜 code span 내부*일 때만 `BACKTICK_WRAPPED_LINK`. 판정 = 위키링크 시작 위치 앞의 (fence 밖) backtick 개수가 **홀수**이면 code span 내부(CommonMark 연속 페어링과 일치).
- 구현: 라인에서 각 `[[…]]` 매치의 `start()` 이전 backtick 수를 세어 홀짝 판정. 짝수 → 정상 링크(검사 계속), 홀수 → wrapped(flag, BROKEN 검사 제외 — 이미 `bare` 제거 로직과 정합).
- 효과: 직전 두 노트의 8건 + vault 36건 중 다수 오탐 제거. 진짜 래핑 `` `[[x]]` ``만 남김.
## 3. Part B — 옵시디언 문법 전 형태 정확 처리
| 형태 | 현재 | 설계 |
|------|------|------|
| alias `[[t\|a]]` | ✓ split `\|` | 유지 |
| embed `![[t]]` | ✓ 정규식이 `[[]]` 포함 | A1로 이미지/첨부 embed도 resolve |
| heading anchor `[[t#h]]` | 느슨한 substring (`anchor.lower() in txt.lower()`) | **강화**: 대상 문서의 실제 heading 텍스트 집합(모든 `#`-레벨, 정규화: markdown 제거+lowercase+공백정리)과 매칭. substring 통과 false-negative 제거 |
| block-ref `[[t#^id]]` | substring | anchor가 `^`로 시작 → 대상에 `^id` 행말 토큰 존재 검사(사용 0건 — 최소 지원, 오탐 방지 우선) |
| heading-only `[[#h]]` | target 비어 skip | 같은 파일 heading 검사(사용 0건 — skip 유지, crash/오탐만 방지) |
| 비-md 첨부 | ✗ 오탐 | A1로 해소 |
| code-span 예시 `` `[[x]]` `` | A2 전엔 혼동 | A2로 *링크 아님* 정확 제외 |
heading anchor 정규화 규칙: 대상 문서에서 `^#{1,6}\s+(.+)$` 캡처 → 양끝 공백 strip → lowercase. 링크 anchor도 동일 정규화 후 집합 매칭. (Obsidian의 heading 링크 매칭에 보수적으로 근사 — 매칭 실패 시 `DANGLING_ANCHOR`.)
## 4. Part C — Zero-tolerance 강제 wiring
- **`--all` 게이트**: 깨진 링크/dangling anchor 1건이라도 있으면 exit 1 (현행 유지 — A1/A2로 *정확한* 목록이 됨). forward-reference 예외 없음.
- **PostToolUse 훅**: 유지 + A1/A2 정확도 강화 반영. 비-blocking 즉시 경고. (훅은 Claude의 도구 편집 시에만 발동 — Obsidian 직접 편집은 `--all`이 진실 소스.)
- **롤아웃**: A1/A2 적용 후 `--all --links-only` 재실행 → 오탐 제거된 *진짜* 깨진 링크 목록 산출. (정리=스텁 생성/링크 수정은 별도 단계, 본 설계는 린터까지.)
## 5. Part D — 문법 계약 문서화
린터 docstring의 C2 설명을 "지원 옵시디언 문법 + 위반 정의" 표로 명문화(Part B 표 기반). `rules/linking-rules.md`에서 "옵시디언 링크 문법의 결정론 집행기 = wiki_structure_lint.py C2"임을 1줄 참조. (별도 rules 파일 신설은 안 함 — YAGNI.)
## 6. 검증 / 수용 기준
- **단위 테스트**(pytest 또는 stdlib `unittest`, 의존성 0 유지):
- A2: 표 행 `| `code` | text [[link]] text | `code` |`(짝수 backtick)이 `BACKTICK_WRAPPED_LINK` 미발생, 진짜 `` `[[x]]` ``는 발생.
- A1: `[[path/foo.drawio]]`가 실존 시 `BROKEN_LINK` 미발생, 부재 시 발생.
- B(anchor): 실존 heading은 통과, 오타 heading은 `DANGLING_ANCHOR`.
- **회귀**: 직전 두 노트(`feature-operational-error-observability-foundation`, `feature-api-contract-baseline`)의 backtick 오탐(8건+6건)이 `--file`에서 사라짐.
- **vault-wide**: `--all --links-only`의 BACKTICK 36건과 BROKEN 72건이 *정확한* 수치로 감소(.drawio 8건 등 오탐 제거).
- 기존 C1/C3 동작 불변(링크 검사만 수정).
## 7. 범위 밖 (YAGNI)
- git pre-commit 게이트.
- block-ref / heading-only 엄격 검증 (사용 0건 — 오탐 방지만).
- 자동 스텁 생성 / 깨진 링크 자동 수정 (A2 backtick autofix는 기존 `--fix` 유지, 변경 없음).
- 멀티 CLI 전파(Codex/Gemini).
- 진짜 깨진 링크 ~60건의 실제 정리(별도 작업 — 본 설계 산출물인 정확한 목록을 입력으로).
## 8. 산출물
| 파일 | 작업 |
|---|---|
| `.claude/hooks/wiki_structure_lint.py` | `build_vault_index`(A1), `check_c2`/backtick 판정(A2), anchor 강화(B), docstring(D) |
| `.claude/hooks/test_wiki_structure_lint.py` (신규) | A1/A2/B 단위 테스트 |
| `rules/linking-rules.md` | C2 집행기 참조 1줄(D) |
> 비고: 현재 `.git`이 빈 디렉터리(미초기화)라 spec 커밋은 생략 — 디스크 파일이 기록.
@@ -0,0 +1,43 @@
# 검증 강화 — 설계 (Phase 2)
> 작성: 2026-06-02 · 상태: 구현됨 · 선행: [Phase 1 branch-spec](2026-06-02-branch-spec-assembly-pipeline-design.md)
## 1. 범위 (G5)
요구사항 4a("문서를 생성·작업할 때마다 검증")를 링크 너머로 강화 + 보고→수정 왕복 단축.
| 산출물 | 변경 |
|---|---|
| `.claude/hooks/wiki_structure_lint.py` | `--hook`(매 저장) 모드에 C1(섹션)+C3(선택조건) **조건부** 추가 |
| `.claude/commands/lint.md` | `--fix-plan` 모드 추가 |
**비범위:** G3 체크리스트 enforcement(G3 자체가 Phase 1에서 제외됨), 한국어 윤문(G4 → Phase 3).
## 2. 확정된 설계 결정
| ID | 결정 | 근거 |
|---|---|---|
| DD1 | hook: **C2(링크)는 항상**, C1/C3는 **완성 선언 시에만** | 작성 중간엔 섹션이 당연히 비어 있어 C1/C3가 false-positive 노이즈가 됨. 링크는 항상 잘못된 것 |
| DD2 | "완성 선언" 판정: branch-note는 `status_label ∉ {in-progress, abandoned}`(review/merged 등), 그 외는 `status ∉ {raw, draft}` | abandoned는 의도된 미완성이라 완성 아님. status 기반이 일반화 가능 |
| DD3 | `--file`/`--all`(권위 검사)은 변경 없음 — 항상 전체 C1+C2+C3 | `/depth`·bulk audit의 정확성 유지. 게이트는 *매 저장 advisory 경로*에만 |
| DD4 | hook은 계속 **non-blocking**(exit 0, stderr 경고) | 저장을 막지 않음. 차단은 PreToolUse `wiki_claim_gate.py` 담당 |
| DD5 | `--fix-plan`은 계획만 구조화, **승인된 항목만 적용**. 무단 자동수정 금지 | lint의 기존 "보고만" 원칙 유지하며 왕복만 단축 |
## 3. 검증 (구현 후 실측)
- in-progress 노트 hook → C2만, 경고 0 (full 모드에선 C1 5건 FAIL — 게이트가 실제로 억제함을 확인).
- review 노트 hook → C1 발동(MISSING_SECTION 2건), exit 0(non-blocking).
- `is_completeness_checkable()` 헬퍼가 source_type/status_label/status로 분기.
## 4. 요구사항 충족
| 요구사항 | Phase 2 |
|---|---|
| 4a 매번 검증 | C2 항상 + 완성 선언 시 C1/C3 → "매번"을 노이즈 없이 달성 |
| 2 비효율 x | `--fix-plan`으로 보고→수정 왕복 단축 |
## 5. 리스크
- 완성 선언 안 한 노트는 매 저장 시 C1/C3 미검사 → 단, `/depth`·`--all`이 권위 검사로 포착(DD3). hook은 보조.
- 잘못 채운 status_label(예: 증거등급 오기입)이 C1/C3를 트리거할 수 있음 → 무해(advisory), 오히려 오기입 발견에 도움.
</content>
@@ -0,0 +1,581 @@
---
title: ca-tmpl (Clean Architecture Template) Audit Detailed Per-File Findings
source_type: llm-generated
status: verified
confidence: high
tags: [architecture, clean-architecture, security, audit]
related_projects: [ca-tmpl]
last_reviewed: 2026-06-04
---
# ca-tmpl (Clean Architecture Template) Audit Detailed Per-File Findings
본 문서는 `/raw/branch-notes` 하위의 81개 전체 명세 파일에 대한 개별 감사 발견 사항의 전문을 수록한 상세 리포트입니다.
---
### 4.1 `###` (Status: READ_FULL)
- **Source file**: `raw/branch-notes/feature-api-compatibility-deprecation-contract.md`
- **Source quote**: `⚠️ OpenAPI Operation Object 의 deprecated: boolean 필드 자체는 OPENAPI31-C1~C7 발췌에 포함되지 않음 (raw 자체 Usage Boundary 명시)`
- **Source line**: 100
- **Severity**: Medium
- **Claim**: OpenAPI Spec 3.1.0에서 `deprecated` 필드가 schema element의 deprecation을 규범적으로 보장하며 metadata 계약으로 활용된다.
- **Assumptions**: `openapi-spec-3-1-0` raw 파일이 OpenAPI 3.1.0 specification의 deprecated behavior를 충분히 corroborating하고 있다.
- **Failure mode**: OpenAPI Spec 3.1.0 reference raw에 `deprecated` element가 누락되어 design decision (D8)에 근거가 부족해지고, generator/validator 도구(springdoc 등)가 specification compliant한 방식으로 deprecation을 client에 알리는 계약을 검증하기 어려워짐.
- **Falsification condition**: OpenAPI 3.1.0 spec raw 문서에 Operation/Schema Object의 `deprecated` 필드 syntax/semantics 발췌가 수록되지 않고도 contract test가 formal specification 정합성을 완전 검증할 수 있는 경우.
- **Recommendation**: `raw/official-docs/openapi-spec-3-1-0.md` 혹은 별도의 raw reference 파일에 OpenAPI 3.1.0 Specification Section 4.8.10 (Operation Object) 및 Section 4.8.24 (Schema Object)의 `deprecated` keyword normative definition을 추가하여 D8의 evidence strength를 official-standard로 보정할 것.
- **Verification command**: `grep -rn "deprecated" raw/official-docs/openapi-spec-3-1-0.md`
- **Verification result**: OpenAPI Specification 3.1.0 raw 발췌에 `deprecated` 필드 자체의 syntax/semantics mapping description이 없음이 확인됨.
### L2-F02: 414 URI Too Long end-to-end integration test 및 Tomcat validation gate의 미완성
- **Source file**: `raw/branch-notes/feature-api-contract-baseline.md`
- **Source quote**: `planned 잔존: D22 HMAC 운영 key/회전(security), D8 414 end-to-end(Tomcat pre-dispatch)`
- **Source line**: 170 / 185
- **Severity**: Medium
- **Claim**: HTTP URI/Query parameter 길이 제한 초과 시 web server (Tomcat) pre-dispatch level에서 414 URI Too Long 응답이 envelope 형식에 정합하게 반환된다.
- **Assumptions**: Tomcat의 기본 `maxHttpHeaderSize` (8KB)에 의해 dispatcher servlet 도달 전 거부되는 경우에도 Spring GlobalExceptionHandler가 에러를 envelope로 자동 wrap할 수 있다.
- **Failure mode**: Tomcat이 Spring dispatcher servlet으로 dispatch하기 전 connection 레벨에서 raw HTTP 400/414 응답을 반환하여 ca-tmpl의 success/error envelope symmetry 계약을 우회하고 leak을 유발함.
- **Falsification condition**: Tomcat pre-dispatch 거부 에러 핸들러나 gateway-level wrap 설정 없이도 414 URI Too Long이 default Tomcat response로 나가는 것이 safe behavior로 인정되는 경우.
- **Recommendation**: Embedded Tomcat customizer를 작성하여 `maxHttpHeaderSize` 초과 시 web server가 raw response를 반환하는 대신, error dispatch page를 `/error`로 포워딩하여 GlobalExceptionHandler가 wrap하도록 하거나, gateway-pre-reject layer의 mapping contract를 formalize할 것.
- **Verification command**: `grep -n "D8 414 end-to-end" raw/branch-notes/feature-api-contract-baseline.md`
- **Verification result**: Line 170: `D8 414 end-to-end(Tomcat pre-dispatch — code+registry row만)` 및 Line 185: `planned 잔존: ... D8 414 end-to-end(Tomcat pre-dispatch)`로 integration test가 `planned` 상태에 머물러 있음이 확인됨.
### L3-F03: `inNew` (REQUIRES_NEW)의 concurrent execution 하 connection pool exhaustion & deadlock 위험성 제어 미비
- **Source file**: `raw/branch-notes/feature-application-port-usecase-contract.md`
- **Source quote**: `inNew 호출은 새 physical JDBC connection 획득 (outer transaction 의 connection 은 그대로 점유). Pool sizing 제약 ... Forbidden: inNew 를 loop 안에서 per-record 호출`
- **Source line**: 81
- **Severity**: High
- **Claim**: HikariCP connection pool이 `hikari.maximumPoolSize >= (concurrent_threads * (1 + max_inNew_depth)) + 1` 공식을 준수하여 dynamic lock 상황에서 pool starvation deadlock을 방지할 수 있다.
- **Assumptions**: 개발자가 loop 안에서 `inNew`를 호출하지 않는다는 ArchUnit rule이나 정적 강제 수단이 부재하더라도 code review만으로 deadlock 발생을 사전에 완전 방지할 수 있다.
- **Failure mode**: 개발자가 custom loop나 recursive call 내에서 `inNew` (requires_new)를 간접 호출하여 connection pool을 고갈시키고, active thread들이 physical connection을 대기하며 deadlock 상태에 빠짐.
- **Falsification condition**: Loop 내 `inNew` 호출을 compile-time 또는 static analysis 단계에서 static rule로 검출할 방법이 전혀 없어서 manual check에만 전적으로 의존해야 하는 경우.
- **Recommendation**: `inNew` loop 내 호출을 탐지하는 custom ArchUnit rule (`inNew_is_not_called_inside_loops`) 또는 `TransactionPort.inNew` 내에 thread-local depth counter를 두어 runtime limit (예: depth > 1 시 경고/예외)을 강제하는 sentinel 메커니즘을 추가할 것.
- **Verification command**: `grep -n "inNew" raw/branch-notes/feature-application-port-usecase-contract.md`
- **Verification result**: Line 81 및 Line 164에 `inNew` pool sizing 공식과 loop 호출 금지 정책은 있으나, 이를 검증하는 ArchUnit rule이나 runtime dynamic restriction이 설계 및 구현에서 누락됨.
### L3-F04: Application Query Bypass의 `RESEARCH_PENDING` 상태 방치로 인한 읽기 최적화 가이드라인 공백
- **Source file**: `raw/branch-notes/feature-application-query-bypass-contract.md`
- **Source quote**: `따라서 아래 §결정/§Decision Evidence Map 의 셀은 조사 완료 전까지 RESEARCH_PENDING 으로 둔다 — 추측 금지(CLAUDE.md §11).`
- **Source line**: 39
- **Severity**: High
- **Claim**: Clean Architecture의 strict read path (QueryUseCase -> Read Repository -> Domain Entity) 우회가 아키텍처적 정합성을 깨뜨리지 않고 언제 허용되는지가 규정되어 있다.
- **Assumptions**: branch note가 `status_label: in-progress`이고 2026-06-04 생성된 이후 외부 조사(`wiki-decision-researcher`)를 통한 aggregate bypass 원칙 합의가 없어도 skeleton의 read path를 안전하게 구현할 수 있다.
- **Failure mode**: 개발자들이 대량 데이터 조회 성능 한계를 마주쳤을 때, 공식 가이드라인 부재로 인해 임의로 Use Case를 우회하여 controller가 직접 persistence adapter를 참조하거나, JPA entity graph를 presentation까지 leak하여 module boundary가 붕괴됨.
- **Falsification condition**: strict read path를 무조건 강제하고, 어떠한 bypass도 허용하지 않음으로써 read performance overhead를 template 수준에서 완전히 감내하기로 결정한 경우.
- **Recommendation**: `wiki-decision-researcher` subagent를 dispatch하여 through-aggregate vs read-model(native projection) vs thin read path (bypass usecase)의 아키텍처적 trade-off를 조사하고, B1~B3 validation gate와 context propagation(MDC)이 깨지지 않는 range 내에서 opt-in bypass criteria를 결정(D1, D2)하여 documentation을 완성할 것.
- **Verification command**: `grep -n "RESEARCH_PENDING" raw/branch-notes/feature-application-query-bypass-contract.md`
- **Verification result**: Line 39, Line 67, Line 71, Line 72 등 핵심 설계 결정 및 mapping matrix 전체가 `RESEARCH_PENDING` 상태로 미결 상태임이 확인됨.
### L3-F05: ArchUnit static analysis의 reflection-style bean lookup (string-key bypass) 탐지 한계
- **Source file**: `raw/branch-notes/feature-architecture-enforcement-rules.md`
- **Source quote**: `D11/D12 string-key bypass (알려진 한계, 보완 불가): D11 banned-class rule 은 class-literal getBean(Class<T>) 까지만 catch 한다.`
- **Source line**: 197
- **Severity**: Medium
- **Claim**: `application_does_not_depend_on_application_context` ArchUnit rule을 통해 application 계층이 Spring DI container와 Spring internal API에 결합되는 것을 철저히 방지한다.
- **Assumptions**: runtime environment에서 string-key lookup을 악의적/실수 수준으로 우회하여 framework dependency를 application-core에 주입하더라도 boundary cleaniness가 깨지지 않는다.
- **Failure mode**: 개발자가 custom class loader나 Spring bean factory string lookup을 사용하여 class-literal static rules를 우회하고, runtime에 framework-specific components to dynamic link하여 application purity를 훼손함.
- **Falsification condition**: runtime application context scan (Spring Boot Actuator beans audit) 또는 compile-time validation check없이 manual code review만으로 전수 boundary check가 가능한 경우.
- **Recommendation**: Spring context loading 시점에 application-core package 내의 class들이 dynamic bean lookup을 수행하는 것을 감시하기 위해, development/test profile에서 Spring `BeanPostProcessor``BeanFactoryPostProcessor`를 커스텀 구현하여 application-core package domain 내 target bean lookup/resolution invocation을 intercept 및 block하는 runtime enforcement gate를 보완할 것.
- **Verification command**: `grep -n "string-key bypass" raw/branch-notes/feature-architecture-enforcement-rules.md`
- **Verification result**: Line 15, Line 101, Line 154, Line 197에 ArchUnit static analysis가 string-key lookup과 reflection bypass를 잡지 못하는 구조적 한계가 있음을 명시하고 있음.
### L3-F06: Graceful shutdown 시 scheduler thread의 in-flight job 유실 및 duplicate processing risk
- **Source file**: `raw/branch-notes/feature-background-job-async-contract.md`
- **Source quote**: `graceful shutdown = executor await termination ≤ 19s (container-runtime의 app shutdown 20s 내부에서 1s cleanup margin 확보.`
- **Source line**: 91
- **Severity**: High
- **Claim**: 19초 이내 graceful shutdown이 완료되면 thread pool 내에 적재된 background job들이 강제 종료되지 않고 transaction safety하게 보존된다.
- **Assumptions**: container termination grace period (20s) 내에 active execution 중이던 transactional outbox publisher job이 강제 interrupt되더라도 DB lock이나 data recovery mechanism이 event duplicated publishing을 방지한다.
- **Failure mode**: long-running background task가 19s 내에 종료되지 않아 SIGKILL에 의해 hard-terminated되거나, thread pool queue에 대기 중이던 job들이 serialization 없이 memory drop되어 job lost 발생. outbox의 경우 retry-on-startup lock timeout에 걸려 stale lock 상태가 지속됨.
- **Falsification condition**: 모든 background job이 sub-second 단위로 실행되어 timeout 발생 가능성이 전혀 없거나, data loss가 도메인 비즈니스상 무관한 경우.
- **Recommendation**: transaction outbox table에 job state(`LOCKED`/`PENDING`)와 lock TTL(Lease time)을 명시하여 shutdown 시 interrupt된 job을 startup context listener에서 auto-recovery(unlock)할 수 있도록 DB level lease contract를 공식화할 것.
- **Verification command**: `grep -n "graceful shutdown" raw/branch-notes/feature-background-job-async-contract.md`
- **Verification result**: Line 91, Line 106, Line 118, Line 169에서 19s graceful shutdown boundary만 명시했을 뿐, task interruption 시 outbox lock/job recovery 정책이 구체화되지 않음.
### L3-F07: `@Valid` cascade depth limit (B4-2)에 대한 ArchUnit 정적 검사 rule 미완성
- **Source file**: `raw/branch-notes/feature-boundary-validation-mapping-contract.md`
- **Source quote**: `planned (현 패스에서 nested DTO sample 부재로 ArchUnit 동적 검사 미작성 — cascade depth 컨벤션은 adapter-web/CLAUDE.md 에 문서화.)`
- **Source line**: 302
- **Severity**: Medium
- **Claim**: DTO validation cascade depth가 3단계 이하로 정적 제한되어 recursive object parsing 공격(DoS)을 사전에 차단한다.
- **Assumptions**: `adapter-web/CLAUDE.md`에 기재된 manual cascade depth 컨벤션만으로 deep nested DTO mapping을 통한 validation stack overflow를 효과적으로 제어할 수 있다.
- **Failure mode**: 개발자가 manual check 누락으로 web adapter dto segment에 4단계 이상의 `@Valid` nested graph를 도입하고, 이는 DoS 공격자가 CPU/Memory resources를 고갈시켜 server crash를 유발하는 entrypoint가 됨.
- **Falsification condition**: web mapping layer에서 nested validation depth를 3단계 이상으로 확대해도 system overhead나 memory safety risk가 전혀 없는 경우.
- **Recommendation**: web adapter package (`..adapter.web..dto..`) 내 모든 `@Valid` 필드를 재귀적으로 스캔하여 depth를 계산하는 custom ArchUnit static rule을 `CleanArchitectureTest`에 추가하고, nested structure sample을 portfolio DTO에 의도적으로 유입시켜 validation gate가 regression test로 동작하도록 반영할 것.
- **Verification command**: `grep -n "cascade depth" raw/branch-notes/feature-boundary-validation-mapping-contract.md`
- **Verification result**: Line 97, Line 202, Line 302에서 cascade depth <= 3 정적 검사가 sample DTO 부재로 인해 `planned` 상태에 머물러 있음이 확인됨.
### L1-F08: Reproducible build에 대한 automated regression check (verification CI step) 누락
- **Source file**: `raw/branch-notes/feature-build-release-supply-chain-contract.md`
- **Source quote**: `reproducibility 검증: 동일 commit 2회 build → artifact hash 불일치 시 fail.`
- **Source line**: 130
- **Severity**: Medium
- **Claim**: `archives.preserveFileTimestamps=false``archives.reproducibleFileOrder=true` 설정으로 동일 커밋 빌드 시 항상 동일한 binary hash를 가지는 artifact가 보장된다.
- **Assumptions**: CI/CD runner 환경과 local build 환경 간의 path delimiter, line separator, JDK vendor/minor version 차이가 bytecode metadata entropy를 유발하지 않으며, 이를 검증하는 CI task 없이도 보장된다.
- **Failure mode**: 빌드 환경의 dynamic resource encoding, compile temporal metadata, 혹은 packaging library order의 미세한 차이로 인해 release artifact의 checksum이 달라져, supply chain validation (provenance verification)이 production deploy phase에서 깨짐.
- **Falsification condition**: checksum verification이 build level에서 automated check 없이도 build lifecycle 동안 단 한 번의 hash collision이나 mismatch 없이 동작함을 보장할 수 있는 경우.
- **Recommendation**: local profile 또는 CI pipeline 내에 `verifyBuildReproducibility` Gradle task를 추가하여, 동일한 project repository를 clean checkout한 후 2회 연속 빌드하여 artifact jar checksum diff를 대조하는 automated gate를 강제할 것.
- **Verification command**: `grep -n "reproducibility" raw/branch-notes/feature-build-release-supply-chain-contract.md`
- **Verification result**: Line 77, Line 130, Line 165에서 reproducibility 요구 및 hash verification test contract는 규정되어 있으나, 이를 자동 수행하는 build script task나 pipeline definition이 누락된 상태로 `needs-confirmation`에 머물러 있음.
### L3-F09: Persistence integrity exception 핸들러 누락으로 인한 raw exception 누출 위험
- **Source file**: `raw/branch-notes/feature-business-rule-validation-contract.md`
- **Source quote**: `persistence integrity 핸들러 미구현 확인 — GlobalExceptionHandler 에 DataIntegrityViolationException 핸들러 없음. owner feature-persistence-failure-baseline(documented-only).`
- **Source line**: 61
- **Severity**: High
- **Claim**: Database unique constraint, check constraint, check null violation 등 DB 레벨에서 발생하는 integrity failure가 raw state나 database query spec, constraint name을 클라이언트에 노출하지 않고 envelope 형식으로 안전하게 마스킹된다.
- **Assumptions**: `feature-persistence-failure-baseline` 브랜치가 미구현 상태더라도 runtime exception이 upstream으로 leak되어 raw stack trace가 API consumer에게 leak될 위험을 dynamic error filter가 차단하고 있다.
- **Failure mode**: DB layer에서 unique/foreign key constraint crash가 발생했을 때, Spring이 던진 `DataIntegrityViolationException`을 잡는 handler가 없어서 fallback으로 HTTP 500 INTERNAL_SERVER_ERROR와 함께 raw postgres/mysql error string ("duplicate key value violates unique constraint 'uk_worklog_date_title'")이 API response로 그대로 노출되어 infra structure 정보가 leak됨.
- **Falsification condition**: `GlobalExceptionHandler``Exception.class` catch-all handler가 있고, 이것이 모든 response를 safety masking 처리하고 있어 details leak이 근본적으로 차단되고 있는 경우.
- **Recommendation**: `feature-persistence-failure-baseline` 브랜치 구현을 선행 완료하거나, 본 baseline exception handling scope 내에 `DataIntegrityViolationException``ConstraintViolationException` (JPA/DB) 핸들러를 임시 스캐폴딩하여 `GlobalExceptionHandler`가 이를 catch해 `DATA_INTEGRITY` 또는 `CONFLICT` category envelope로 변환하고 client-safe message만 응답하도록 덮어쓸 것.
- **Verification command**: `grep -n "DataIntegrityViolationException" raw/branch-notes/feature-business-rule-validation-contract.md`
- **Verification result**: Line 61 및 Line 251에서 `DataIntegrityViolationException` 핸들러가 현재 skeleton에 누락되었으며, 이로 인해 persistence integrity 매핑 테스트가 `planned` 상태에 멈춰 있음이 입증됨.
---
---
### 4.19 `###` (Status: READ_FULL)
- **Source file**: `raw/branch-notes/feature-domain-feature-onboarding-contract.md`
- **Source quote**: `only if new skeleton-wide response/error/header/log/metric/registry contract is required | domain-specific type, business enum, feature-specific DTO`
- **Source line**: 104
- **Severity**: Medium
- **Claim**: `shared-contract`에 개별 도메인 타입의 진입은 금지되나, 여러 features 간 공유가 필요한 공통 비즈니스 타입/Enum의 처리 가이드가 부재하여 도메인 간의 강한 결합이나 중복이 발생합니다.
- **Assumptions**:
1. 프로젝트 전반에서 공유되어야 하는 도메인 공통 개념(예: Address, Money 등)이 다수의 feature 모듈에 걸쳐 존재한다.
- *무효 조건:* 모든 feature가 완전히 독립적이고 공통되는 비즈니스 데이터 모델이 존재하지 않는다.
- *사용자 검증 방법:* domain-core 모듈 내 features 패키지 간 중복되는 데이터 클래스 존재 여부 체크
2. multi-module Hexagonal 구조 하에서 도메인 간 직접 참조를 방지하는 컴파일 수준의 모듈 제어가 필요하다.
- *무효 조건:* Gradle 모듈이 분리되어 있지 않고 단일 모듈 내에서 package 경계만으로 결합을 관리한다.
- *사용자 검증 방법:* features/ 모듈 하위의 build.gradle dependency 선언 체크
- **Failure mode**: features 간에 공통 비즈니스 enum(예: WorkLogStatus)을 서로 복사하여 중복 코드가 늘어나거나, 한 feature 모듈이 다른 feature 모듈을 직접 implementation하여 순환 참조와 강한 결합이 유발되어 multi-module의 경계가 붕괴됩니다.
- **Falsification condition**: features 간에 공통으로 참조해야 하는 도메인 성격의 타입이 전무하고 완전히 독립적인 sub-domain들로만 구성되는 경우 비판이 무효화됩니다.
- **Recommendation**: `shared-contract` 대신 `domain-core` 모듈 하위에 `domain-shared` 패키지 또는 모듈을 두어, 여러 feature가 공통으로 의존하는 비즈니스 VO 및 Enum을 독립적으로 수용할 수 있는 중간 경계를 설계하고 가이드에 명시하십시오.
- **Verification command**: `grep -n "shared-contract" /home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-domain-feature-onboarding-contract.md`
- **Verification result**: `104: | shared-contract | normally no change | only if new skeleton-wide response/error/header/log/metric/registry contract is required | domain-specific type, business enum, feature-specific DTO |`
### L3-F02: `default` package-private mutator의 Multi-Module Hexagonal 환경 내 컴파일 접근 제한 결함
- **Source file**: `raw/branch-notes/feature-domain-modeling-guardrails.md`
- **Source quote**: `aggregate mutation 검사: @AggregateRoot annotation이 붙은 class의 모든 mutator method (*set* prefix 또는 state-changing method)는 (a) public이 아닌 package-private 또는 protected이고`
- **Source line**: 124
- **Severity**: High
- **Claim**: Hexagonal architecture에서 domain-core와 application-core는 물리적으로 패키지나 모듈이 분리되어 있어, domain aggregate의 mutator가 package-private으로 설정되면 application use case에서 이를 변경하기 위한 호출이 컴파일 에러를 일으키게 됩니다.
- **Assumptions**:
1. Java 언어의 default/package-private 접근 제어자가 다른 패키지에 정의된 application use case의 호출을 제한한다.
- *무효 조건:* domain과 application 코드가 같은 패키지 내에 선언되어 있거나, Kotlin의 internal 등 다른 가시성 스펙을 프로젝트 전반에 적용한다.
- *사용자 검증 방법:* domain entity의 mutator 정의 패키지와 application service 패키지 불일치 여부 체크
- **Failure mode**: application use case가 domain aggregate의 invariant 검증이 포함된 mutator를 직접 호출하지 못하고, 이를 우회하기 위해 reflection을 사용하거나 domain layer에 강제로 public mutator를 열어 설계 가이드라인이 깨집니다.
- **Falsification condition**: domain-core와 application-core가 단일 Gradle 모듈 및 동일 package schema 내에 속해 있어 package-private 접근이 가능한 구조라면 본 비판은 무효화됩니다.
- **Recommendation**: mutator의 가시성을 단순히 package-private으로 강제하기보다는, application core가 위치한 application-core 모듈로의 접근을 허용하는 public interface 구조를 분리하거나, ArchUnit rule을 통해 'application layer 이외의 외부 adapter layer가 mutator를 호출하는 것'만 정적으로 제한하도록 ArchUnit 룰을 수정하십시오.
- **Verification command**: `grep -n "package-private" /home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-domain-modeling-guardrails.md`
- **Verification result**: `124: - aggregate mutation 검사: @AggregateRoot annotation이 붙은 class의 모든 mutator method (*set* prefix 또는 state-changing method)는 (a) public이 아닌 package-private 또는 protected이고 (b) invariant 검증 로직 포함.`
### L3-F03: `APP_MULTI_INSTANCE_ENABLED` 단일 플래그의 일괄 결합(All-or-Nothing)으로 인한 유연성 결여
- **Source file**: `raw/branch-notes/feature-env-driven-runtime-configuration.md`
- **Source quote**: `multi-instance claim parsing 메커니즘 = env property APP_MULTI_INSTANCE_ENABLED boolean (default false).`
- **Source line**: 97
- **Severity**: High
- **Claim**: 분산 락, 캐시 보호, 아웃박스 리더 선출 등 이종 기술 요구사항을 단 하나의 global flag로 묶어서 강제함으로써, 특정 분산 컴포넌트만 선택적으로 켜고 끄는 마이크로 서비스별 세부 토폴로지 구성이 불가능해집니다.
- **Assumptions**:
1. 프로덕션 환경의 서로 다른 인스턴스/서비스군이 서로 다른 분산 아키텍처 토폴로지(예: 어떤 서비스는 캐시 스탬피드 방지가 불필요하나 분산 락은 필요함)를 필요로 한다.
- *무효 조건:* 모든 microservice가 예외 없이 5가지 분산 컴포넌트를 동시에 사용해야만 기동된다.
- *사용자 검증 방법:* application.yml 내 Redis/ShedLock 의존 관계 확인
- **Failure mode**: 특정 환경에서 분산 캐시나 rate limiter를 사용할 수 없는 경우(예: Redis 장애 또는 특정 망 분리 환경), 전체 multi-instance 설정을 꺼야 하므로 이와 무관한 Outbox publisher leader election이나 ShedLock까지 비활성화되어 배치 중복 실행 등의 2차 장애가 발생합니다.
- **Falsification condition**: 모든 배포 클러스터 노드가 단일한 인프라(Redis 등)를 상시 공유하며, 각 컴포넌트가 하나의 logical block으로 묶여 작동하는 독립 모놀리스 구조라면 무효화됩니다.
- **Recommendation**: `APP_MULTI_INSTANCE_ENABLED`를 최상위 마스터 스위치로 유지하되, 각 컴포넌트별로 granular하게 제어할 수 있는 sub-flags(예: `APP_DISTRIBUTED_LOCK_ENABLED`, `APP_CACHE_STAMPEDE_PROTECTION_ENABLED` 등)를 도입하고 개별 override를 허용하도록 설계를 수정하십시오.
- **Verification command**: `grep -n "APP_MULTI_INSTANCE_ENABLED" /home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-env-driven-runtime-configuration.md`
- **Verification result**: `97: - 2026-05-22: multi-instance claim parsing 메커니즘 = env property APP_MULTI_INSTANCE_ENABLED boolean (default false).`
### L3-F04: 임시 파일 Orphan Cleanup의 Startup 시점 한정으로 인한 롱러닝 서버 리소스 고갈 취약점
- **Source file**: `raw/branch-notes/feature-file-resource-handling-contract.md`
- **Source quote**: `temp file cleanup trigger = (1) success/failure on close (try-with-resources), (2) startup sweeper for orphaned files older than 1h, (3) JVM shutdown hook은 backup.`
- **Source line**: 89
- **Severity**: High
- **Claim**: 임시 파일의 orphan cleanup이 애플리케이션 'startup' 시점에만 1회성으로 트리거되어, 오랫동안 재시작 없이 켜져 있는 롱러닝 프로덕션 서버의 경우 임시 파일 누적으로 인한 디스크 고갈 리스크에 노출됩니다.
- **Assumptions**:
1. 서버 인스턴스의 기동 시간(Uptime)이 며칠에서 몇 달 동안 장기 유지된다.
- *무효 조건:* 서버가 하루에도 수십 번씩 무작위로 배포/재기동되거나 Serverless FaaS 환경에서 구동된다.
- *사용자 검증 방법:* kubectl get pods 등의 pod uptime 체크
2. 예기치 않은 스트리밍 다운로드 장애나 try-with-resources 예외 처리 실패로 인한 임시 파일 leak이 프로덕션 환경에서 주기적으로 발생한다.
- *무효 조건:* 파일 업로드/다운로드 과정에서 단 하나의 임시 파일 누수도 일어나지 않는다.
- *사용자 검증 방법:* /tmp 디렉토리의 파일 수 증가 추이 관측
- **Failure mode**: 프로덕션 서버 기동 중 비정상 종료된 업로드의 임시 파일들이 `/tmp`에 계속 쌓이다가 디스크 용량 한도(100퍼센트)에 도달하여, 다른 정상적인 디스크 I/O 작업(로그 생성, JVM GC dump 등)이 모두 차단되고 전체 컨테이너가 다운되는 장애가 발생합니다.
- **Falsification condition**: 주기적인 K8s pod liveness probe 등으로 인해 1시간 이내에 무조건 pod가 재시작되어 startup sweeper가 실시간으로 작동하는 구조라면 무효화됩니다.
- **Recommendation**: startup sweeper 외에 Spring `@Scheduled` 또는 background execution thread를 기반으로, 1시간 주기로 `/tmp` 디렉토리를 주기적으로 검사하여 생성된 지 1시간이 넘은 orphan temp file을 강제 정리하는 cron-like daemon task를 추가하십시오.
- **Verification command**: `grep -n "temp file cleanup trigger" /home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-file-resource-handling-contract.md`
- **Verification result**: `89: - 2026-05-22: temp file cleanup trigger = (1) success/failure on close (try-with-resources), (2) startup sweeper for orphaned files older than 1h, (3) JVM shutdown hook은 backup. file >1h not closed → orphan.`
### L3-F05: Readiness Scorecard의 All-or-Nothing binary 판정 방식의 실무 경직성과 우회 부작용
- **Source file**: `raw/branch-notes/feature-implementation-readiness-scorecard.md`
- **Source quote**: `Readiness framing is binary pass/fail. any Fail = Not ready.`
- **Source line**: 147-148
- **Severity**: Medium
- **Claim**: 15개 영역에 걸친 아키텍처 규칙 전체가 통과해야만 readiness pass를 부여하는 All-or-Nothing 설계는 초기 MVP 개발 단계나 로컬 PoC 단계에서 불필요한 게이트 체증을 유발하고, 개발팀이 검증을 우회하려는 성향을 촉진합니다.
- **Assumptions**:
1. 프로젝트 초기 개발 또는 신규 feature 온보딩 단계에서 모든 부수 사안(trace propagation, security logging 등)을 즉각적이고 완전하게 구성하기는 어렵다.
- *무효 조건:* 개발 시작 시점부터 15개 영역의 모든 설정과 모듈이 이미 자동으로 세팅되어 구동된다.
- *사용자 검증 방법:* local 환경의 readiness scorecard pass 비율 점검
- **Failure mode**: 로컬 개발 환경에서 빠르게 도메인 로직을 검증하고자 하는 상황에서도 readiness scorecard 실패로 인해 빌드/로컬 실행이 차단되자, 개발자들이 ArchUnit rule이나 verification task 자체를 `@Disabled` 처리하거나 임시로 mock을 끼워 넣어 아키텍처 게이트 자체의 실효성이 훼손됩니다.
- **Falsification condition**: 모든 신규 기능 개발팀이 고도로 자동화된 템플릿 제너레이터를 통해 15개 영역의 코드를 자동 생성하여 즉각 pass할 수 있는 완전 자동화 도구를 구비한 경우 무효화됩니다.
- **Recommendation**: Readiness Scorecard를 'Blocking Gates'와 'Advisory Gates'로 이원화하거나, release profile(local, dev, prod)에 따라 local/dev 환경에서는 일부 영역의 Fail을 허용하는 점진적 성숙도(Maturity scoring) 모드를 옵션으로 도입하십시오.
- **Verification command**: `grep -n "Readiness = Pass only" /home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-implementation-readiness-scorecard.md`
- **Verification result**: `147: Readiness = Pass only if every area is Pass.`
### L3-F06: 어댑터 비활성화(Disabled) 시 Use Case 계층 주입 컴파일/런타임 bean 누락 결함
- **Source file**: `raw/branch-notes/feature-integration-adapter-templates.md`
- **Source quote**: `Spring @ConditionalOnProperty(name="app.adapter.{adapterName}.enabled", havingValue="true") 적용.`
- **Source line**: 90
- **Severity**: High
- **Claim**: `@ConditionalOnProperty`에 의해 어댑터 bean이 등록되지 않았을 때(enabled=false), 이를 의존성 주입(DI)받아 사용하는 application use case가 존재하는 경우 DI 실패(`NoSuchBeanDefinitionException`)로 인해 애플리케이션 시작 자체가 불가능해지는 설계적 모순이 발생합니다.
- **Assumptions**:
1. application use case 계층의 서비스 클래스들이 outbound port interface에 직접 컴파일 타임 의존성을 맺고, Spring DI(@Autowired 등)를 통해 주입받는다.
- *무효 조건:* use case 계층이 어댑터의 유무를 dynamic lookup이나 null-safety 코드로 직접 처리하고 있다.
- *사용자 검증 방법:* Application class의 startup run 시도 시 NoSuchBeanDefinitionException 발생 여부 체크
- **Failure mode**: 특정 환경에서 Redis나 Kafka 어댑터를 끄기 위해 property를 false로 두었을 때, 관련 Use Case 빈들이 주입할 빈을 찾지 못해 startup fail-fast가 발생함으로써, 어댑터 비활성화 시 기능 토글 목적이 아예 달성되지 못합니다.
- **Falsification condition**: 어댑터가 비활성화되면 관련 Use Case 빈들 전체도 Spring profile이나 conditional에 의해 자동으로 기동 범위에서 함께 탈락하도록 모듈 단위 게이팅이 적용된다면 무효화됩니다.
- **Recommendation**: 어댑터 비활성화(enabled=false) 시, 해당 interface의 `NoOp` Null Object 구현체 또는 local stub bean을 default fallback(예: `@ConditionalOnMissingBean`)으로 등록해주는 auto-configuration 가이드를 보강하십시오.
- **Verification command**: `grep -n "havingValue" /home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-integration-adapter-templates.md`
- **Verification result**: `90: - Layer 1 (startup, runtime): Spring @ConditionalOnProperty(name="app.adapter.{adapterName}.enabled", havingValue="true") 적용. flag false 시 adapter bean 등록 X. ApplicationContext에 해당 bean 0개 verify.`
### L3-F07: Keycloak Account REST API 직접 호출 방식의 XSS 노출 및 불필요한 토큰 권한 확대 리스크
- **Source file**: `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md`
- **Source quote**: `SPA의 link 상태 표시 = Keycloak Account REST API 호출 — backend 거치지 않고 SPA가 직접. 이유: backend 코드 추가 0 (P2A와 동일하게 유지).`
- **Source line**: 103
- **Severity**: High
- **Claim**: SPA 브라우저 환경에서 직접 Keycloak Account REST API를 호출하게 만듦으로써, XSS 발생 시 해커가 사용자 계정을 unlink하거나 타 소셜 ID로 마음대로 link하는 등의 세션 탈취 리스크가 잔존하며, access token에 불필요한 account 관리 권한을 열어주어야 합니다.
- **Assumptions**:
1. 클라이언트 애플리케이션(SPA) 내에 임의의 서드파티 스크립트 실행으로 인한 XSS 공격 벡터가 상존한다.
- *무효 조건:* Content Security Policy(CSP) 및 sanitization을 통해 XSS 가능성을 실무적으로 완전 배제하고 있다.
- *사용자 검증 방법:* SPA token decode 시 aud claim 내 account 존재 여부 확인
- **Failure mode**: SPA가 XSS 공격에 뚫렸을 때, 공격자가 획득한 access token의 `account` 권한을 이용해 Keycloak Account API를 임의 호출하여 사용자의 소셜 연동을 해제하고 공격자의 Google 계정을 연동(Account takeover)하는 시나리오가 활성화됩니다.
- **Falsification condition**: SPA에 access token이 전달되지 않고 session cookie로만 세션을 유지하거나, Account API 자체가 client-side 호출을 차단하는 정책을 갖는다면 무효화됩니다.
- **Recommendation**: SPA가 Keycloak Account REST API를 직접 통신하는 설계를 피하고, BFF 또는 backend application server가 Keycloak Admin API(또는 backchannel)를 이용해 백엔드 간 통신(M2M)으로 연동 상태를 확인 및 조작하는 간접 보안 경계를 구축하십시오.
- **Verification command**: `grep -n "SPA의 link 상태" /home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-keycloak-account-linking-spa-ux.md`
- **Verification result**: `103: - 2026-05-25: SPA의 link 상태 표시 = Keycloak Account REST API 호출 — backend 거치지 않고 SPA가 직접. 이유: backend 코드 추가 0 (P2A와 동일하게 유지). Account API audience(account)가 SPA token에 자동 포함되는지 확인 필요 (needs-confirmation).`
### L3-F08: Keycloak Identity Provider Sync Mode `IMPORT` 채택으로 인한 실무 Data Drift 결함
- **Source file**: `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md`
- **Source quote**: `Sync Mode = IMPORT (first login만). Google 측 email 변경이 Keycloak으로 자동 전파되지 않음 → Scenario B 회피.`
- **Source line**: 118
- **Severity**: High
- **Claim**: 소셜 계정의 email 변경 시 Keycloak으로 정보가 업데이트되지 않도록 `IMPORT` 모드로 고정하는 정책은, Google Workspace 등에서 소셜 email이 변경되었을 때 Keycloak 내부의 user email 정보가 점차 stale해져 다른 시스템과의 sync가 어긋나는 data drift 문제를 유발합니다.
- **Assumptions**:
1. 사용자의 Google Workspace 등 소셜 identity의 primary email이 시간이 지나면서 변경될 수 있다.
- *무효 조건:* 모든 사용자의 Google email은 가입 후 탈퇴 시까지 변경되지 않는다.
- *사용자 검증 방법:* Google Workspace Admin에서 이메일 변경 이력 조사
2. 시스템 내부적으로 user.email 값을 활용해 비즈니스 알림 발송이나 타 시스템(CRM, ERP 등) 연동을 수행한다.
- *무효 조건:* 시스템이 user.email을 식별자 외의 용도로 사용하지 않는다.
- *사용자 검증 방법:* core 로직 내 email 발송 모듈 참조 범위 확인
- **Failure mode**: 사용자가 소셜 email을 변경했음에도 Keycloak 및 서비스 DB 내 email은 과거 값(`IMPORT` 모드로 인해 고정됨)으로 남아 있어, 중요한 결제 완료 메일이나 비밀번호 변경 확인 메일 등이 과거 email 주소로 잘못 발송되거나, 타 시스템으로의 동기화가 실패하게 됩니다.
- **Falsification condition**: 이메일을 단지 최초 가입 시의 가상 식별 용도로만 쓰고 비즈니스적으로 발송이나 연동에 사용하지 않는다면 무효화됩니다.
- **Recommendation**: Sync Mode를 단순히 `IMPORT`로 고정하는 대신, `sub`를 invariant unique key로 유지하면서도, 로그인 시 이메일 정보는 최신 값으로 동기화하는 custom mapper 로직을 구현하거나, email 변경 감지 시 사용자에게 변경 확인 메일을 발송하고 Keycloak 내 email 속성을 갱신하는 event listener를 설계하십시오.
- **Verification command**: `grep -n "Sync Mode = IMPORT" /home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md`
- **Verification result**: `118: - 2026-05-25: Sync Mode = IMPORT (first login만). Google 측 email 변경이 Keycloak으로 자동 전파되지 않음 → Scenario B 회피.`
### L3-F09: 보안 취약한 SPA Direct (P2A) 1순위 학습 채택으로 인한 엔터프라이즈 아키텍처 보안 결여
- **Source file**: `raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md`
- **Source quote**: `본 keycloak-patterns 프로젝트는 SPA Direct (P2A)를 학습 목적의 1순위로 채택. BFF는 비교 문서로만 정리. 이유: canonical OIDC + PKCE 흐름을 먼저 이해하는 것이 목표.`
- **Source line**: 138
- **Severity**: Medium
- **Claim**: 브라우저 JS 메모리에 토큰이 노출되는 취약점을 가진 SPA Direct 방식을 템플릿의 기본 아키텍처로 우선 채택함으로써, 엔터프라이즈 환경에서 보안을 강화하기 위한 BFF 설계 자산과 베스트 프랙티스(AutoConfiguration 등) 확보가 누락됩니다.
- **Assumptions**:
1. ca-tmpl 템플릿을 차용하여 실제 프로덕션 서비스를 구축하는 조직들이 금융, 의료, e-commerce 등 고보안 요건을 가지고 있다.
- *무효 조건:* ca-tmpl은 내부 토이 프로젝트 및 단순 교육용으로만 제한적으로 사용된다.
- *사용자 검증 방법:* 템플릿의 주요 적용 프로젝트 보안 요구도 조사
- **Failure mode**: 템플릿이 SPA Direct만 프로덕션 레벨로 제공하므로, 신규 프로젝트 구축 시 개발팀이 이를 그대로 복사하여 사용하다가, 차후 보안 감사에서 브라우저 내 토큰 노출(LocalStorage 등)이 지적되어 대대적인 아키텍처 개편(BFF 전환)을 겪으며 비용이 낭비됩니다.
- **Falsification condition**: SPA Direct 구조에서 XSS 공격이 일어날 수 없도록 보장하거나, Refresh Token Rotation과 엄격한 Cookie 관리를 통해 SPA Direct의 보안성을 BFF 수준으로 자동 격상시킬 수 있다면 무효화됩니다.
- **Recommendation**: BFF 실 구현 패키지를 `ca-skeleton`의 공식 서브 모듈 또는 profile-driven optional 모듈로 통합하여 제공하고, 보안 민감도에 따라 SPA Direct와 BFF를 설정 스위치 하나로 전환할 수 있는 ready-to-use 아키텍처 템플릿으로 보강하십시오.
- **Verification command**: `grep -n "BFF는 비교 문서" /home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md`
- **Verification result**: `138: - 2026-05-25: 본 keycloak-patterns 프로젝트는 SPA Direct (P2A)를 학습 목적의 1순위로 채택. BFF는 비교 문서로만 정리. 이유: canonical OIDC + PKCE 흐름을 먼저 이해하는 것이 목표.`
---
---
### 4.46 `###` (Status: READ_FULL)
- **Source file**: `raw/branch-notes/feature-keycloak-public-domain-tunneling.md`
- **Source quote**: `Cloudflare Tunnel 의 <UUID>.cfargotunnel.com generic subdomain 이 Google Cloud Console redirect URI 정책에 통과하는지는 CLOUDFLARE-TUNNEL-C4 "Does not prove" 단서에 명시된 미검증 사항 — P3B 실 검증 필요`
- **Source line**: 112
- **Severity**: MEDIUM
- **Claim**: Cloudflare Tunnel이 자동 발급하는 generic subdomain이 Google OAuth client console의 redirect URI validation 및 security filter를 무사히 통과할 것이다.
- **Assumptions**: Google Cloud Console의 OAuth redirect URI 검증 엔진이 generic trycloudflare.com 혹은 cfargotunnel.com 도메인을 phishing/abuse 도메인으로 차단하지 않고 정상 허용한다.
- **Failure mode**: Google Cloud Console이 generic tunnel subdomain에 대해 명시적 혹은 묵시적 도메인 평판 필터링을 가하여, redirect URI 등록이 거부되거나 런타임에 `redirect_uri_mismatch`를 내며 integration이 불가능해짐.
- **Falsification condition**: Google Cloud Console에 `<UUID>.cfargotunnel.com/keycloak/...` 주소를 등록하려 할 때 "Domain invalid or untrusted" 등의 오류가 브라우저 콘솔 또는 UI에 발생함.
- **Recommendation**: free generic subdomain에 의존하지 않고, Cloudflare DNS에 사용자가 제어하는 custom domain을 연결하여 CNAME을 터널에 매핑하는 정적 DNS 매핑 구성을 개발 규격서에 필수로 기재할 것.
- **Verification command**: `sed -n '112p' 'raw/branch-notes/feature-keycloak-public-domain-tunneling.md'`
- **Verification result**: `| D1 | 학습 환경 1순위 **Cloudflare Tunnel** (named tunnel + 정적 URL + 무료 TLS + inbound port 0) | raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C1 (cloudflared outbound), raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C2 (firewall inbound 차단 권장), raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C3 (tunnel <UUID>.cfargotunnel.com subdomain 자동 부여), raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C4 (사용자 hostname CNAME → cfargotunnel.com), raw/official-docs/google-oauth2-redirect-uri-validation-official.md#GOOGLE-REDIR-C2 (Google redirect URI raw IP 금지 → 도메인 필요) | official-vendor-doc + official-vendor-doc | Cloudflare Tunnel 의 <UUID>.cfargotunnel.com generic subdomain 이 Google Cloud Console redirect URI 정책에 통과하는지는 CLOUDFLARE-TUNNEL-C4 "Does not prove" 단서에 명시된 미검증 사항 — P3B 실 검증 필요 |`
---
### L6-F02: Keycloak Realm Import JSON 내 환경변수 동적 치환 설계 누락
- **Source file**: `raw/branch-notes/feature-keycloak-realm-client-export.md`
- **Source quote**: `export JSON에서 secret/password 제거(또는 placeholder 치환) 후 git commit — 등급: planned`
- **Source line**: 88
- **Severity**: LOW
- **Claim**: Git repository에 JSON을 형상 관리할 때 secret을 제거한 뒤 manual 또는 simple replacement script(예: sed)를 통해 target client secret을 매번 주입하는 방식으로 pipeline reset을 수행할 수 있다.
- **Assumptions**: Keycloak JSON parser가 import 시점에 raw string placeholder 외에 동적인 parsing 능력을 갖고 있지 않다.
- **Failure mode**: template 치환 스크립트 작성 부담 가중 및 개발자 실수로 인해 Plaintext credential이 포함된 JSON이 Git repository에 오머지(bypass merge)되어 보안 사고 발생.
- **Falsification condition**: Keycloak container 구동 시 `--import-realm` 명령이 JSON 내부의 `${env.CLIENT_SECRET_VAR}` 구문을 native substitution하여 정상 파싱 처리해 주는 기능이 있는지 검증.
- **Recommendation**: Keycloak Quarkus distribution의 built-in parsing 규격을 활용하여 JSON 내 credential 값을 `${env.KEYCLOAK_CLIENT_SECRET}` 형식으로 매핑하고, docker-compose의 env block을 통해 runtime에 주입하도록 설계를 표준화할 것.
- **Verification command**: `sed -n '88p' 'raw/branch-notes/feature-keycloak-realm-client-export.md'`
- **Verification result**: `- [ ] export JSON에서 secret/password 제거(또는 placeholder 치환) 후 git commit — 등급: planned`
---
### L6-F03: Concurrency Context에서 Refresh Token Rotation의 정상 세션 강제 종료 결함
- **Source file**: `raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md`
- **Source quote**: `Max Reuse 0: 동시 재사용 1회도 허용 안 함. SPA가 race condition으로 같은 refresh token을 동시에 두 번 보내면 family kill — 학습 시연 시 단일 thread 보장.`
- **Source line**: 92
- **Severity**: HIGH
- **Claim**: `Max Reuse = 0` 설정 하에서 Refresh Token Rotation을 활성화하면 비동기 브라우저 SPA 환경에서 정상 사용자의 UX를 저해하지 않고 안정적으로 동작한다.
- **Assumptions**: SPA application이 parallel API requests를 전송할 때 401 Unauthorized 헤더를 감지하여 토큰을 갱신하는 logic이 fully serialized 되어 한 번에 단 하나의 refresh call만 발생한다.
- **Failure mode**: 멀티 탭 환경 혹은 비동기 컴포넌트가 대량 렌더링되면서 동시에 backend Resource Server API를 호출할 때, 거의 동시(millisecond 간격)에 여러 개의 401 갱신 요청(`/token` grant_type=refresh_token)이 전송됨. Keycloak이 첫 요청을 처리해 RT_1을 무효화하고 새 RT_2를 반환하지만, 동시에 인입된 두 번째 요청(동일 RT_1 사용)을 침해 공격으로 오판하여 token family 전체(RT_2 포함)를 무효화하여 정상 사용자가 즉시 로그아웃되는 오탐지(False Positive) 발생.
- **Falsification condition**: SPA client가 async race context에서 overlapping `/token` refresh request를 보낼 때 token family invalidation이 즉시 트리거되는지 여부.
- **Recommendation**: ca-tmpl SPA client code 내에 refresh token request를 단일 Lock Promise로 직렬화 처리해 주는 `Serialized Token Refresh Interceptor` 설계를 강제 명문화할 것.
- **Verification command**: `sed -n '92p' 'raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md'`
- **Verification result**: `- **Max Reuse 0**: 동시 재사용 1회도 허용 안 함. SPA가 race condition으로 같은 refresh token을 동시에 두 번 보내면 family kill — 학습 시연 시 단일 thread 보장.`
---
### L6-F04: Refresh Token Rotation 도입 시 Keycloak Persistent Store Write 부하 증가 고려 누락
- **Source file**: `raw/branch-notes/feature-keycloak-refresh-token-rotation.md`
- **Source quote**: `| D1 | refresh token rotation 활성화 (Revoke Refresh Token: ON + Refresh Token Max Reuse: 0) — reuse detection 으로 stolen token 탐지`
- **Source line**: 136
- **Severity**: MEDIUM
- **Claim**: Short access token TTL에 의한 refresh 트래픽 증가가 Keycloak persistence store(RDBMS)에 별도의 I/O 부하 문제를 일으키지 않을 것이다.
- **Assumptions**: user session 갱신 시 DB write transactional overhead가 keycloak 성능 저하의 병목지점이 되지 않는다.
- **Failure mode**: access token 수명을 5분 내외로 극히 단축하고 rotation을 켰을 때, 동시 사용자가 증가하면 매 5분마다 DB write transactional lock(UPDATE `USER_SESSION` / `CLIENT_SESSION` 테이블)이 폭증하여 DB thread pool 고갈 및 Keycloak API 지연 유발.
- **Falsification condition**: rotation 비활성화 시와 활성화 시의 DB Write IOPS 및 TPS 증가 추이를 벤치마크 툴로 비교할 때 병목현상이 감지되는지 여부.
- **Recommendation**: high-concurrency 시나리오에 ca-tmpl을 사용할 경우 RDBMS write load를 방어하기 위해 Keycloak clustering 및 memory-based dynamic user session cache(Infinispan) 설정을 최적화 가이드라인에 필수 등록할 것.
- **Verification command**: `sed -n '136p' 'raw/branch-notes/feature-keycloak-refresh-token-rotation.md'`
- **Verification result**: `| D1 | refresh token rotation 활성화 (`Revoke Refresh Token: ON` + `Refresh Token Max Reuse: 0`) — reuse detection 으로 stolen token 탐지 | raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3 (refresh token = scope/resource server bound MUST) — rotation 자체는 OAuth 2.1 권고 배경; **Keycloak 의 정확한 UI 항목 라벨** 은 `UNSUPPORTED_DECISION` (cited `keycloak-securing-apps-overview-official` 의 C1~C3 은 protocol overview 수준이며 rotation/revoke UI 직접 명시 없음) | `official-standard (rotation 권고) + UNSUPPORTED_DECISION (vendor UI 라벨)` | Keycloak 25.x admin UI 라벨이 실제로 `Revoke Refresh Token` / `Refresh Token Max Reuse` 인지 직접 verify 필요 |`
---
### L6-F05: path-prefix 라우팅(`/keycloak/*`) 하에서 Keycloak Base Path Rewrite Asset 404 에러
- **Source file**: `raw/branch-notes/feature-keycloak-reverse-proxy-headers.md`
- **Source quote**: `- **2026-05-25**: path-prefix 라우팅(`/keycloak/*`) 채택. 이유: 단일 EC2에 SPA(`/`) + API(`/api/*`) + Keycloak(`/keycloak/*`)을 한 도메인에 묶기 위함. 부모 P3B 다이어그램과 일치.`
- **Source line**: 137
- **Severity**: HIGH
- **Claim**: Nginx 혹은 Caddy에서 `/keycloak/*` path를 Keycloak 포트로 forwarding하면, Keycloak admin console UI 및 discovery document endpoint가 별도 옵션 없이 정상 노출될 것이다.
- **Assumptions**: Keycloak application container가 incoming URI prefix `/keycloak`을 default routing hierarchy와 매칭하도록 알아서 변환하거나 proxy layer의 rewrite가 알아서 자원을 복원할 수 있다.
- **Failure mode**: reverse proxy가 prefix `/keycloak`을 strip하여 Keycloak에 보내거나, strip하지 않고 그대로 보냈을 때 Keycloak이 relative path를 인지하지 못해 static assets(CSS/JS)를 root `/resources`로 redirect → browser에서 404 Not Found가 발생하고 admin console 화면이 깨짐.
- **Falsification condition**: `KC_HTTP_RELATIVE_PATH=/keycloak` 옵션을 인입시키지 않거나, proxy rewrite rule mismatch 상태에서 `/keycloak/admin` 호출 시 admin console 렌더링에 필요한 js/css 로드 실패가 발생하는지 검증.
- **Recommendation**: ca-tmpl의 nginx/Caddy default config template에 반드시 `KC_HTTP_RELATIVE_PATH=/keycloak` 환경 변수를 한 쌍으로 설정하고 proxy routing block에서 path prefix preservation 설정을 명확히 적용할 것.
- **Verification command**: `sed -n '137p' 'raw/branch-notes/feature-keycloak-reverse-proxy-headers.md'`
- **Verification result**: `- **2026-05-25**: path-prefix 라우팅(`/keycloak/*`) 채택. 이유: 단일 EC2에 SPA(`/`) + API(`/api/*`) + Keycloak(`/keycloak/*`)을 한 도메인에 묶기 위함. 부모 P3B 다이어그램과 일치.`
---
### L6-F06: Google IdP brokering 시 Account Linking 취약점(Account Hijacking)
- **Source file**: `raw/branch-notes/feature-keycloak-single-ec2-google-federation.md`
- **Source quote**: `Keycloak first-broker-login flow 가 Account Linking 시 "비밀번호 확인 후 link" 정책으로 실제 작동`
- **Source line**: 243
- **Severity**: CRITICAL
- **Claim**: Google IdP user email을 Keycloak local user database와 matching하여 account mapping할 때 default brokering flow가 계정 탈취 위협을 안전하게 방어한다.
- **Assumptions**: Google OAuth user가 local user와 linking될 때 Google이 전달한 email claim이 verified 상태임을 검증하고, Keycloak이 credential re-verification을 생략 없이 통제한다.
- **Failure mode**: Keycloak first-broker-login flow에서 `Confirm Link Existing Account` 시 credential 검증(예: 기존 local user password 재확인)을 skip하고 email-match만으로 link할 때, 공격자가 local user와 동일한 email을 임의의 허위 Google 계정으로 생성하여 OIDC federation 로그인을 시도할 시 기존 local account 권한 전체를 hijacking하는 심각한 보안 사고 유발.
- **Falsification condition**: Google client idp registration 상에서 `email_verified` 검증 옵션을 활성화하지 않고 `Confirm Link Existing Account` flow에서 password 재입력 필드를 누락 시 silent link가 발생하는지 여부.
- **Recommendation**: ca-tmpl의 IdP brokering flow 정책에 `First Broker Login` flow customized copy 및 `Require Password/OTP Verification`을 force authenticator로 추가할 것을 권고하고, google mapping rules에 `email_verified=true` 검증을 custom mapper 레벨에서 명문화할 것.
- **Verification command**: `sed -n '243p' 'raw/branch-notes/feature-keycloak-single-ec2-google-federation.md'`
- **Verification result**: `| Keycloak first-broker-login flow 가 Account Linking 시 "비밀번호 확인 후 link" 정책으로 실제 작동 | KC_FBL-C2 등이 needs-confirmation 등급 — 정책 UI 토글 위치/동작 불확실 | Admin UI → Authentication → First Broker Login → flow copy + Confirm Link Existing Account authenticator 추가 → 같은 email 의 local user 사전 생성 후 Google 로그인 시도 | needs-confirmation |`
---
### L6-F07: Custom local domain 사용 시 SubtleCrypto API Runtime Crash 가능성
- **Source file**: `raw/branch-notes/feature-keycloak-single-ec2-no-google.md`
- **Source quote**: `- 2026-05-25: **vanilla JS는 manual fetch + `crypto.subtle` 기반 PKCE 구현 우선**, 작동 확인 후 `oidc-client-ts`로 마이그레이션 비교. 이유: OIDC lifecycle 내부 동작 학습.`
- **Source line**: 156
- **Severity**: MEDIUM
- **Claim**: vanilla JS client 내에서 호출하는 `crypto.subtle` API가 HTTP 학습 환경의 모든 호스트 및 IP 주소 하에서 안정적으로 호출될 수 있다.
- **Assumptions**: user-agent(browser)가 custom local dev domain (예: `http://my-keycloak.local`)을 secure context로 간주하여 SubtleCrypto interface를 차단 없이 노출한다.
- **Failure mode**: `http://localhost`가 아닌 custom hostname/IP로 접속한 local 개발 환경 브라우저에서 `crypto.subtle``undefined`로 떨어져 PKCE challenge 생성 시 `TypeError: Cannot read properties of undefined` 에러와 함께 SPA 전체가 runtime crash를 일으킴.
- **Falsification condition**: secure context 기준(localhost 또는 HTTPS)이 충족되지 않은 custom HTTP dev domain에서 SubtleCrypto API 호출 시 script crash가 감지되는지 확인.
- **Recommendation**: plain HTTP custom host test 시 memory crypto polyfill fallback logic을 feature detection (`if (!window.crypto || !crypto.subtle)`)으로 wrapping하여 bypass code를 마련하거나 SSL self-signed cert binding을 default로 명세할 것.
- **Verification command**: `sed -n '156p' 'raw/branch-notes/feature-keycloak-single-ec2-no-google.md'`
- **Verification result**: `- 2026-05-25: **vanilla JS는 manual fetch + \`crypto.subtle\` 기반 PKCE 구현 우선**, 작동 확인 후 \`oidc-client-ts\`로 마이그레이션 비교. 이유: OIDC lifecycle 내부 동작 학습.`
---
### L6-F08: SPA Direct Cross-Origin httpOnly Cookie Refresh Token 차단 설계 결함
- **Source file**: `raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md`
- **Source quote**: `- 2026-05-25: P2A에서 권장 조합 = **access_token: 메모리 + refresh_token: secure httpOnly cookie**. 단 본 branch는 구현 없으므로 `documented-only`.`
- **Source line**: 112
- **Severity**: CRITICAL
- **Claim**: SPA Direct pattern(SPA가 Keycloak API를 direct request) 하에서, refresh_token을 secure httpOnly cookie로 client domain에 저장하여 cross-site script leakage를 방어할 수 있다.
- **Assumptions**: Keycloak auth domain(`keycloak.com`)과 SPA client domain (`spa.com`)이 다른 cross-origin configuration 상태에서 browser sandbox가 keycloak token endpoint의 `Set-Cookie` header를 cross-origin context에서 차단 없이 load/send한다.
- **Failure mode**: modern browsers의 3rd-party cookie restriction (Safari ITP / Chrome Phase-out) 정책에 의해 `SameSite=None; Secure` cookie가 block되어 SPA domain에서 Keycloak domain으로 cookie transmission이 전면 차단됨. refresh_token 갱신이 불가능해져 SPA authentication lifecycle이 fail함.
- **Falsification condition**: client와 identity provider domain이 다른 direct auth topology에서 3rd-party cookie blocked 환경 테스트 시 request cookie transmission failure 발생.
- **Recommendation**: browser direct token endpoint access token pattern을 sub-optimal로 밀어내고, SPA domain server side가 Keycloak call을 proxying하여 secure SameSite=Lax cookie session을 application layer에 공급하는 BFF(Backend For Frontend) 패턴을 ca-tmpl의 core architecture로 상향 지정할 것을 권장하십시오.
- **Verification command**: `sed -n '112p' 'raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md'`
- **Verification result**: `- 2026-05-25: P2A에서 권장 조합 = **access_token: 메모리 + refresh_token: secure httpOnly cookie**. 단 본 branch는 구현 없으므로 \`documented-only\`.`
---
### L6-F09: NimbusJwtDecoder의 Default JWKS Cache Stampede 취약점
- **Source file**: `raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md`
- **Source quote**: `prod에서는 JwkSetUriJwtDecoderBuilder.cache(Cache) 로 custom cache(Caffeine 등) 권장 — 학습 범위 외`
- **Source line**: 105
- **Severity**: HIGH
- **Claim**: NimbusJwtDecoder의 default 5분 cache configuration이 Keycloak client token verification load가 높거나 key rotation이 빈번한 multi-threaded context에서 cache stampede (JWKS endpoint overload) risk를 차단한다.
- **Assumptions**: JWKS fetch request가 key validation fail 시 background single lock serialization으로 처리되어 external HTTP endpoint storming을 방지한다.
- **Failure mode**: Keycloak에서 key rotation이 일어나거나, 악의적인 client가 임의의 invalid `kid`를 가진 JWT를 backend로 brute-force 요청할 때, backend JVM thread들이 `kid` cache-miss를 해소하기 위해 동시에 Keycloak JWKS endpoint (`/certs`)로 HTTP request를 cascading 전송하여 Keycloak에 network/thread exhaustion DDoS를 유발함.
- **Falsification condition**: 존재하지 않는 invalid `kid` JWT를 concurrent resource server requests로 대량 발송 시, JWKS discovery endpoint로의 outbound HTTP query 수가 outbound query pool control 없이 requests 수에 비례하여 급증하는 현상 발생.
- **Recommendation**: ca-tmpl backend template에 NimbusJwtDecoder 생성 시 default cache 대신, rate limiter가 적용된 custom JWKS loader 또는 concurrency lock이 구현된 cache manager (`JwkSetUriJwtDecoderBuilder.cache` with Guava/Caffeine and loading cache lock)를 integration하도록 baseline 설계를 보완하십시오.
- **Verification command**: `sed -n '105p' 'raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md'`
- **Verification result**: `- [ ] **JWKS cache 정책** — 등급: \`documented-only\`` (105라인: `prod에서는 JwkSetUriJwtDecoderBuilder.cache(Cache) 로 custom cache(Caffeine 등) 권장 — 학습 범위 외`)
---
### 4-1. Adversarial Review
- **Adversarial Review 1: L6-F08 (BFF 패턴 전환 권고에 대한 비판)**
- *Counterargument*: BFF 패턴 도입은 SPA Direct 구조에 비해 인프라 운영 부담(BFF proxy 서버 구축, session persistence 관리)이 과도하게 증가하며, stateless의 최대 장점인 무상태 수평 확장을 훼손한다.
- *Rebuttal / Architectural Mitigation*: 3rd-party cookie 제약은 modern browser들의 거부할 수 없는 보안 강제 사항입니다. SPA Direct 하에서 httpOnly secure cookie를 cross-origin 도메인 간에 전달하려는 시도는 runtime 단계에서 브라우저 sandbox에 의해 전면 차단됩니다. 따라서 BFF 프록시 계층의 state를 stateless gateway session(예: encrypted client-side session cookie) 또는 Redis shared storage로 외재화하여 수평 확장을 유지하면서도 브라우저 보안 제약을 극복하는 방향이 ca-tmpl의 미래 지향성에 부합합니다.
- **Adversarial Review 2: L6-F03 (Refresh Token Rotation False Positive 비판)**
- *Counterargument*: 비동기 race condition으로 인한 로그아웃은 client-side library(oidc-client-ts 등)가 in-flight request를 잘 관리하면 예방할 수 있으므로, Keycloak의 `Max Reuse = 0` 설정을 완화(예: Max Reuse = 1 또는 2)하는 것이 아키텍처 복잡도를 낮추는 직관적 해법이다.
- *Rebuttal / Architectural Mitigation*: Max Reuse 설정을 1 이상으로 완화하는 순간, 공격자가 탈취한 RT를 1회 사용하는 행위를 Keycloak이 "동시 요청 유예"로 파싱하여 침해 탐지 로직이 침묵하게 됩니다. 이는 보안 수준의 타협을 야기하므로, identity provider의 엄격한 보안 규칙을 유지하되 client layer에서 fetch promise caching / locking queue interceptor를 구현해 갱신 요청을 엄격히 직렬화하는 것이 올바른 아키텍처 설계 방향입니다.
- **Adversarial Review 3: L6-F09 (JWKS Cache Stampede 취약성 비판)**
- *Counterargument*: Spring Security NimbusJwtDecoder는 내부적으로 concurrency control을 가지고 있으며, JWKS endpoint query storming은 outbound firewall rate limiter 또는 API gateway 레벨에서 block하면 되므로 Resource Server 내부에 lock cache를 두는 것은 복잡도 오버헤드다.
- *Rebuttal / Architectural Mitigation*: NimbusJwtDecoder의 기본 key resolver(JwkRetriever)는 unknown kid를 수신할 때마다 cache를 bypass하고 동기/비동기 outbound HTTP call을 트리거합니다. firewall/gateway가 이를 차단하더라도 Resource Server 내부의 request thread가 outbound call 대기 상태로 block되어 container worker thread pool 고갈이 발생합니다. JVM layer에서 invalid kid의 lookup rate를 rate-limit하고, loading cache lock을 통해 단 하나의 thread만 certs를 fetch하도록 강제하는 것이 reactive resiliency 및 core architecture 보안의 기본 소양입니다.
---
---
### 4.55 `###` (Status: READ_FULL)
- Source file: `raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md`
- Source quote: `- [ ] JwtAuthenticationConverter 빈: realm_access.roles → SimpleGrantedAuthority("ROLE_" + role) 매핑 — 등급: planned`
- Source line: 82
- Severity: High
- Claim: Spring Security의 `JwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("realm_access.roles")`를 지정하는 것만으로 Keycloak의 중첩(nested) JWT claim인 `realm_access.roles` 배열을 파싱하여 Spring Security role로 매핑할 수 있다는 설계 가정.
- Assumptions:
1. Keycloak이 발급한 JWT의 구조가 `realm_access: { roles: [ "admin-role" ] }` 형태로 들어올 것.
2. Spring Security의 default `JwtGrantedAuthoritiesConverter`가 dot notation(예: `"realm_access.roles"`)을 파싱해 내부 맵에 접근할 수 있을 것.
- Failure mode: Spring Security의 `JwtGrantedAuthoritiesConverter`는 기본적으로 dot-notation이나 JSON path를 지원하지 않고 단일 flat key 매핑만 수행합니다. 따라서 `"realm_access.roles"`를 claim name으로 지정하면 Spring은 이를 문자 그대로 `"realm_access.roles"`라는 이름을 가진 flat String key로 찾으려고 하여 nested 구조를 파싱하지 못하고, 결국 어떠한 권한(Authority)도 획득하지 못해 모든 권한 검증 API가 `403 Forbidden`을 반환합니다.
- Falsification condition: Keycloak의 `realm_access.roles`가 Custom Protocol Mapper를 통해 JWT 루트 레벨의 flat array(예: `"roles": ["admin-role"]`)로 변환되어 인입되는 경우에는 해당 finding이 적용되지 않습니다.
- Recommendation: `JwtAuthenticationConverter` 설정 시 `JwtGrantedAuthoritiesConverter`에 단순히 claim name만 세팅하는 대신, JWT에서 직접 Map 형태의 `"realm_access"`를 추출한 뒤 그 내부의 `"roles"` Collection을 파싱하여 Spring Authority(`SimpleGrantedAuthority`)로 가공해 반환하는 custom converter 람다식을 빈으로 구현하십시오.
- Verification command: `grep -nF 'realm_access.roles' raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md`
- Verification result: `82: - [ ] JwtAuthenticationConverter 빈: realm_access.roles → SimpleGrantedAuthority("ROLE_" + role) 매핑 — 등급: planned`
### L7-F02: Keycloak Google JWKS 캐시 갱신 지연에 따른 인증 전면 장애 가능성
- Source file: `raw/branch-notes/feature-keycloak-three-leg-trust-chain.md`
- Source quote: `함정: Keycloak이 Google JWKS 캐시 갱신 실패 시 Google 로그인 전체 장애 → fallback 정책 확인 — 등급: needs-confirmation`
- Source line: 78
- Severity: High
- Claim: Google Identity Provider 연동 시, Google의 비정기적 JWKS(Json Web Key Set) 회전(Rotation)을 Keycloak이 유연하게 동기화하고 캐싱하여 검증을 수행할 것이라는 신뢰성 가정.
- Assumptions:
1. Google의 공개 키 서명용 JWKS Endpoint가 상시 가용하며 Keycloak이 이를 요청 시간 내에 조회할 수 있을 것.
2. Google의 키 회전 빈도에 대응하여 Keycloak의 외부 IdP 키 캐시 정책이 무효화 후 강제 갱신(Cache Eviction) 메커니즘을 적절히 동작시킬 것.
- Failure mode: Google이 새로운 서명 키로 ID Token을 발급하기 시작하는 시점에, Keycloak이 Google의 JWKS Endpoint 조회 과정에서 일시적인 네트워크 장애나 속도 제한(Rate Limit)을 겪으면 캐시가 갱신되지 못합니다. 이 경우 Keycloak은 기존 만료되거나 존재하지 않는 키로 서명된 Google ID Token 검증을 실패 처리하게 되며, 이에 따라 전체 Google 소셜 로그인 기능이 전면적인 502/503 오류로 정지되는 장애가 발생합니다.
- Falsification condition: Google의 JWKS Endpoint 외에 사전에 다운로드된 정적 인증서 체인을 백업으로 사용하거나, Keycloak이 키 불일치 감지 시 즉시 동기 방식으로 fallback 조회하는 강한 복구 정책이 기본 탑재된 경우 본 finding은 우회됩니다.
- Recommendation: Keycloak의 Identity Provider 설정에서 `Use JWKS URL` 정책과 함께 `JWKS Cache TTL`을 기본값보다 유연하게 단축하고, 외부 API 통신 실패 시 최대 3회 이내의 지수 백오프(Exponential Backoff) 재시도 로직을 가동하도록 네트워크 타임아웃 및 서킷 브레이커 설정을 튜닝하십시오.
- Verification command: `grep -nF 'Google JWKS 캐시 갱신 실패' raw/branch-notes/feature-keycloak-three-leg-trust-chain.md`
- Verification result: `78: - [ ] 함정: Keycloak이 Google JWKS 캐시 갱신 실패 시 Google 로그인 전체 장애 → fallback 정책 확인 — 등급: needs-confirmation`
### L7-F03: Traefik ForwardAuth의 Authorization 헤더 기본 전송에 따른 정보 유출 취약성
- Source file: `raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md`
- Source quote: `default (empty) 가 Authorization 등 sensitive header 까지 인증 서버로 보내 production 위험 — 명시 화이트리스트 필요`
- Source line: 105
- Severity: High
- Claim: Traefik의 `forwardAuth` middleware를 설정할 때, `authRequestHeaders` 옵션을 명시하지 않더라도 기본 설정 수준에서 보안 문제가 유발되지 않을 것이라는 편의성 가정.
- Assumptions:
1. Traefik 뒤에 배치되는 인증용 서버(예: `oauth2-proxy`)가 신뢰 영역 내에 존재할 것.
2. 클라이언트가 원래 백엔드로 전송하고자 했던 민감한 `Authorization` 또는 인증 쿠키 정보가 인증 프록시 서버로 유출되어도 무관할 것.
- Failure mode: `authRequestHeaders`가 공백(default empty)으로 두어지면 Traefik은 원본 클라이언트 요청에 포함된 모든 HTTP 헤더(`Authorization` Bearer 토큰, 세션 쿠키 등)를 중간 인증 서버로 그대로 복사하여 전송합니다. 인증 서버가 만약 로깅 단계나 제3의 에러 리포팅 툴로 요청을 로깅한다면, 백엔드 전용 토큰이나 민감 세션 자격 증명이 인증 서버 측 로그에 고스란히 남아 심각한 토큰 탈취 및 자격 증명 유출 경로가 열리게 됩니다.
- Falsification condition: 인증 게이트웨이와 인증 처리기가 동일한 메모리 영역 혹은 완전 격리된 동일 프로세스 신뢰 내부망에서만 동작하고 로그를 전혀 남기지 않는 것이 증명되는 상황이라면 위험도가 감소합니다.
- Recommendation: Traefik `forwardAuth` middleware 선언 시 `authRequestHeaders` 목록을 공백으로 두지 말고, 인증에 필요한 필수적인 컨텍스트 헤더(예: `X-Forwarded-For`, `X-Forwarded-Proto`, `Accept`)만을 명시하는 화이트리스트 방식 설정을 강제하도록 아키텍처 규칙을 지정하십시오.
- Verification command: `grep -nF 'authRequestHeaders 로 인증 서버로 전달할' raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md`
- Verification result: `105: | D6 | authRequestHeaders 로 인증 서버로 전달할 헤더 필터링 (default empty = 모든 헤더 전달 — production 위험) | raw/official-docs/traefik-forwardauth-middleware-official.md#TFA-C4 | official-vendor-doc | default (empty) 가 Authorization 등 sensitive header 까지 인증 서버로 보내 production 위험 — 명시 화이트리스트 필요 |`
### L7-F04: In-Memory Token Storage와 제3자 쿠키 제한으로 인한 SPA Silent Renew 불가 현상
- Source file: `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md`
- Source quote: `token storage: in-memory (학습용, UserManager.events.addUserLoaded(...)로 closure 보관)`
- Source line: 61
- Severity: High
- Claim: XSS 공격 방어를 위해 SPA에서 `in-memory` 토큰 저장소를 기본 채택한 뒤, `oidc-client-ts` 라이브러리의 `automaticSilentRenew: true` 옵션만으로 토큰의 자동 갱신을 안정적으로 수행할 수 있을 것이라는 보안 아키텍처 가정.
- Assumptions:
1. SPA 애플리케이션 화면이 브라우저에서 리로드/새로고침될 시 토큰 정보 유실에 대응할 silent SSO 메커니즘이 원활히 동작할 것.
2. Safari(ITP), Chrome(Privacy Sandbox) 등 최신 브라우저가 프레임(iframe)을 통한 제3자 쿠키(3rd-party cookie) 전송을 허용할 것.
- Failure mode: `in-memory` 저장소를 쓰면 페이지 새로고침 시 토큰이 메모리에서 증발하므로 반드시 숨겨진 iframe을 띄워 OIDC authorization endpoint로 `prompt=none` 요청을 날려 세션을 복구해야 합니다(Silent SSO). 그러나 이 과정은 Keycloak 쿠키에 의존하므로, 브라우저의 제3자 쿠키 차단 정책이 활성화된 환경에서는 iframe 안에서의 Keycloak 쿠키 전송이 완전 거부되어 Silent Sign-In이 실패하게 되며, 사용자는 페이지를 새로고침할 때마다 계속 로그아웃되어 강제 재로그인 루프에 빠지게 됩니다.
- Falsification condition: SPA와 Keycloak이 물리적으로 동일한 최상위 도메인(First-party context, 예: `spa.example.com` & `kc.example.com`)을 공유하여 제3자 쿠키가 아닌 당사자 쿠키로 간주되는 경우 장애가 발생하지 않습니다.
- Recommendation: 학습 단계를 넘어 실제 운영 환경에서는 SPA 단독의 public client 흐름을 탈피하고, 백엔드를 인증 대리자로 삼는 BFF(Backend-For-Frontend) 패턴을 도입하여 프론트엔드 브라우저에는 오직 SameSite/HttpOnly 쿠키만 노출시키고 토큰은 백엔드 세션에 보관하도록 구조를 고도화하십시오.
- Verification command: `grep -nF 'token storage: in-memory' raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md`
- Verification result: `61: - token storage: in-memory (학습용, UserManager.events.addUserLoaded(...)로 closure 보관)`
### L7-F05: JSON Logstash Encoder 채택 시 PatternLayout 마스킹 필터 우회 취약성
- Source file: `raw/branch-notes/feature-log-management-contract.md`
- Source quote: `Layer 1 (primary): Logback masking converter (PatternLayout 단계). 모든 ERROR/WARN 진입 시 token/password/auth header pattern을 ****로 치환.`
- Source line: 169
- Severity: High
- Claim: Logback 설정 파일의 `PatternLayout` 단계에서 정규식 기반의 Masking Converter를 적용해두면, 모든 보안 위협과 민감 자격 증명의 유출이 시스템 전체 로그에서 안정적으로 차단될 것이라는 운영 보안 가정.
- Assumptions:
1. 애플리케이션의 로그 출력 장치(Appender)가 항상 PatternLayout의 렌더링 형식을 타고 출력될 것.
2. 구조화 로깅을 위한 JSON Encoder(예: `logstash-logback-encoder`)가 PatternLayout Converter 파이프라인을 온전히 준수할 것.
- Failure mode: 프로덕션 환경의 성능 및 파싱 효율을 위해 `logstash-logback-encoder`와 같은 JSON 포맷 인코더를 적용하면, 이는 일반적인 Logback PatternLayout 문자열 변환 구조를 거치지 않고 객체를 직접 직렬화하여 JSON 스트림으로 내보냅니다. 이 경우 MDC나 Exception Stack Trace, 혹은 커스텀 구조화 필드에 담긴 민감 정보(패스워드, 토큰 등)는 PatternLayout 마스킹 정규식 필터를 전혀 타지 않고 원본 그대로 JSON에 바인딩되어 로그 서버로 노출되는 우회 취약점이 발생합니다.
- Falsification condition: JSON Encoder 내부에 자체 Jackson Custom Masking Module을 등록하거나, Logback Filter 레벨에서 객체 단계의 마스킹 전처리를 수행하도록 조치한 경우 해당 실패가 방지됩니다.
- Recommendation: PatternLayout Converter에만 마스킹 역할을 위임하지 말고, 구조화 JSON 인코더 설정 파일(`logback-spring.xml``LoggingEventCompositeJsonEncoder`)의 `jsonGeneratorDecorator` 또는 `Jackson` serializer 레벨에 PII 및 민감 자격 증명을 마스킹 처리해주는 커스텀 `ValueMasker`를 명시적으로 등록하십시오.
- Verification command: `grep -nF 'Logback masking converter' raw/branch-notes/feature-log-management-contract.md`
- Verification result: `169: - Layer 1 (primary): Logback masking converter (PatternLayout 단계). 모든 ERROR/WARN 진입 시 token/password/auth header pattern을 ****로 치환.`
### L7-F06: Custom SecurityFilterChain 선언 시 Actuator 전용 포트(9001)의 비인증 무단 노출 위험
- Source file: `raw/branch-notes/feature-management-actuator-security-contract.md`
- Source quote: `management port default = 9001 (separate from app 8080). single-port는 platform ingress 보호 + 문서화 시만 허용.`
- Source line: 86
- Severity: Critical
- Claim: 애플리케이션 비즈니스 포트(8080)와 Actuator 관리 포트(9001)를 물리적으로 분리하는 것만으로 관리 서비스의 공격 노출 표면이 감소할 것이라는 보안적 아키텍처 가정.
- Assumptions:
1. 포트 분리 시 9001 포트의 라우팅이 호스트 내부망으로만 제약되어 안전하게 격리될 것.
2. Spring Security가 활성화된 상황에서 custom `SecurityFilterChain` 빈을 정의할 때, 포트가 다른 Actuator endpoint에 대해서도 스프링이 안전하게 기본 보안 필터를 자동 바인딩해 줄 것.
- Failure mode: Spring Boot 3에서 개발자가 메인 비즈니스 포트(8080)용으로 custom `SecurityFilterChain` 빈을 하나라도 수동 정의하면, 스프링 보안의 기본 auto-configuration(Actuator에 보안을 걸어주는 자동 구성)이 즉시 비활성화됩니다. 만약 Actuator 전용 포트인 9001용 FilterChain을 `@Order` 우선순위로 명시하여 별도 선언해주지 않는다면, 9001 포트로 들어오는 `/actuator/env`, `/actuator/prometheus` 등 모든 관리자 endpoint들이 비인증 무방비 상태로 열려 네트워크 전반에 정보 유출 표면이 노출되는 현상이 발생합니다.
- Falsification condition: 9001 포트가 호스트 외부 IP 인터페이스에 바인딩되지 않도록 로컬 방화벽이나 인프라 시큐리티 그룹(Security Group) 레벨에서 인입 자체를 통제하는 환경에서는 노출 경로가 차단됩니다.
- Recommendation: Actuator용 포트(9001) 보안을 위해 `@Order(Ordered.HIGHEST_PRECEDENCE)`를 적용한 전용 `SecurityFilterChain` 빈을 독립적으로 선언하여, 관리자 Endpoint 요청에 대해 Network ACL 또는 internal Admin Role 인증을 강제하도록 설정 표준을 아키텍처 규칙으로 명시하십시오.
- Verification command: `grep -nF 'management port default = 9001' raw/branch-notes/feature-management-actuator-security-contract.md`
- Verification result: `86: - 2026-05-22: management port default = 9001 (separate from app 8080). single-port는 platform ingress 보호 + 문서화 시만 허용.`
### L7-F07: Tenant ID의 Bucket Folding 제어로 인한 메트릭 식별성 유실 및 알람 무력화
- Source file: `raw/branch-notes/feature-metrics-alerting-contract.md`
- Source quote: `tenant_id | 1000 (활성 시) — ULID 원본을 직접 사용하지 않음. metric label로는 (a) bounded mapping table id (tenant 등록 시 ascending integer 부여) 또는 (b) tenant cohort bucket(예: hash mod 100) 사용. 1001번째 tenant 등장 시 cardinality 정책: 새 tenant는 bucket으로 자동 fold. |`
- Source line: 172
- Severity: Medium
- Claim: Prometheus 메트릭의 Cardinality 폭주를 제어하기 위해 1,000개 이상의 다중 테넌트(Multi-tenant) 환경에서 1,001번째부터 테넌트 메트릭 라벨을 특정 Bucket으로 그룹화(Folding)하는 제어가 효율적일 것이라는 운영 가정.
- Assumptions:
1. 1001번째 이후의 테넌트들은 메트릭 레이블 상 개별 식별을 포기하고 통합 버킷으로 묶어도 운영 및 장애 파악에 무리가 없을 것.
2. 특정 묶음 테넌트에서 대규모 시스템 장애가 발생했을 때, 해당 버킷 내의 타 테넌트 오염 없이 문제 원인을 격리 진단할 수 있을 것.
- Failure mode: Cardinality 제어에는 기여하지만, 1001번째 이후의 테넌트들이 `hash mod 100` 버킷 등으로 압축 폴딩되면 장애 격리 능력을 유실하게 됩니다. 예컨대 특정 유료 대형 고객 테넌트(1050번째 등록)가 심각한 5xx 에러율 증가를 겪어도, 메트릭이 동일 버킷으로 폴딩되어 다른 테넌트들의 정상 트래픽에 묻혀 전체 버킷 평균치 에러율이 기준치 이하로 산출됨에 따라 알람 시스템이 작동하지 않는 무경보 장애 침묵 상황이 발생합니다.
- Falsification condition: 테넌트 식별자 레벨의 장애 모니터링은 메트릭이 아닌 분산 트레이싱(Trace)이나 로깅(Log) 파이프라인에서만 실시간 전담하여 처리하도록 완전히 역할이 분담되어 있는 인프라 구조라면 위협성이 낮아집니다.
- Recommendation: Prometheus 메트릭 Cardinality 억제를 위해 메트릭 태그 단독으로 테넌트 ID를 가공하는 폴딩 방식보다는, 최상위 50개 VIP 고객 전용 테넌트 리스트만 명시적 라벨로 유지하고, 나머지 일반 고객군은 `NORMAL_TENANTS` 통합 버킷으로 묶는 이원화된 동적 Allowlist 필터링 규칙을 정의하여 비즈니스 영향도가 큰 고객에 대한 얼럿 정확도를 유지하십시오.
- Verification command: `grep -nF '1001번째 tenant 등장 시 cardinality' raw/branch-notes/feature-metrics-alerting-contract.md`
- Verification result: `172: | tenant_id | 1000 (활성 시) — ULID 원본을 직접 사용하지 않음. metric label로는 (a) bounded mapping table id (tenant 등록 시 ascending integer 부여) 또는 (b) tenant cohort bucket(예: hash mod 100) 사용. 1001번째 tenant 등장 시 cardinality 정책: 새 tenant는 bucket으로 자동 fold. |`
### L7-F08: Clustered 다중 인스턴스 환경에서 Flyway Schema Lock 타임아웃으로 인한 컨테이너 Startup Fail
- Source file: `raw/branch-notes/feature-migration-startup-contract.md`
- Source quote: `multi-instance에서는 app startup runner를 그대로 확장하지 않고 platform one-shot job 또는 migration lock 검증이 필요.`
- Source line: 86
- Severity: High
- Claim: 다중 인스턴스(Replicas > 1) 환경에서 각 애플리케이션의 Flyway Startup Runner가 자체 데이터베이스 락 테이블 검증을 수행하므로, 별도 분리 작업 없이 단순히 Replicas를 증설하여 배포하는 것만으로 배포 안정성이 유지될 것이라는 설계 가정.
- Assumptions:
1. 배포 중 신규 Pod들이 동시에 데이터베이스 마이그레이션 변경 테이블(`flyway_schema_history`)에 락(Lock)을 획득하려고 경쟁하는 시간이 데이터베이스 락 획득 타임아웃보다 충분히 짧을 것.
2. 락 경합으로 인한 startup 일시 지연이 발생하더라도 애플리케이션 프로세스가 에러로 종료되지 않고 안정적으로 대기할 것.
- Failure mode: 쿠버네티스 등 롤링 배포 시점에 다수의 API 컨테이너 Replicas가 거의 동시에 부팅되면서 각각 Flyway migration을 DB 연결 후 실행하려 합니다. Flyway의 schema history table lock 획득을 위해 경합하는 과정에서 첫 인스턴스가 락을 잡한 상태로 대형 마이그레이션 스크립트를 수행하면, 나머지 대기 중이던 컨테이너들은 기본 락 타임아웃 임계치를 넘겨 결국 `LockException`을 던지며 시스템 시작 시점 종료 코드 `70`을 반환하고 대거 비정상 종료(Startup Failure)되어 롤링 배포 전체가 롤백되거나 지연됩니다.
- Falsification condition: 배포 파이프라인에서 컨테이너 기동 전 데이터베이스 스키마 마이그레이션이 단일 직렬 작업(One-shot Job)으로 확실하게 격리되어 마친 뒤 컨테이너들이 기동되는 단방향 흐름이 보장된다면 경합 문제가 방지됩니다.
- Recommendation: 프로덕션 배포 스크립트 작성 시 애플리케이션 내의 `spring.flyway.enabled`를 기본 false로 끄고, 마이그레이션을 전담하는 일회성 독립 파이프라인(Kubernetes Job 등)을 먼저 기동하여 완료된 것을 헬스체크 신호로 삼아 애플리케이션 인스턴스들을 부팅하도록 아키텍처 배포 정책을 표준화하십시오.
- Verification command: `grep -nF 'multi-instance에서는 app startup' raw/branch-notes/feature-migration-startup-contract.md`
- Verification result: `86: - 2026-05-22: multi-instance에서는 app startup runner를 그대로 확장하지 않고 platform one-shot job 또는 migration lock 검증이 필요.`
### L7-F09: Reactive WebFlux 스택 전용 MDC Context 전파(Propagation) 누락에 의한 트레이스 ID 유실
- Source file: `raw/branch-notes/feature-operational-error-observability-foundation.md`
- Source quote: `MDC key snake_case (request_id) 가 Spring MVC RequestContextHolder 와 Reactor Context 양쪽에서 일관 propagation`
- Source line: 411
- Severity: High
- Claim: MDC 키를 `snake_case` 형식으로 지정하고 ThreadLocal 기반 컨텍스트 전파 방식을 정의하는 것만으로 비동기 및 리액티브 스택 전체의 로깅 파이프라인에서 트레이스 ID 전파가 잘 유지될 것이라는 일반성 가정.
- Assumptions:
1. 애플리케이션 내부에서 스레드 풀 전환(Thread Context Switch)이 일어나더라도 `ThreadLocal` 기반의 MDC 데이터가 대상 비동기 스레드로 잘 상속될 것.
2. Spring WebFlux 등의 Reactive Stream 파이프라인 내 모든 연산자(Operator) 경계에서 MDC 전파가 누수 없이 작동할 것.
- Failure mode: Spring WebFlux(Project Reactor) 환경에서는 요청 처리 흐름이 특정 고정 스레드에 묶이지 않고 여러 이벤트 루프 스레드를 넘나듭니다. ThreadLocal을 기본 저장소로 삼는 MDC는 리액티브 스트림 내부에서 컨텍스트 경계를 넘을 때(예: `publishOn`, `subscribeOn` 등 비동기 바운더리) 자동으로 전파되지 않고 깨끗이 삭제됩니다. 이에 따라, 로그 백엔드 상에서 에러 발생 지점의 로그들을 단일 `request_id``trace_id`로 연관 지으려 해도 트레이스 연결 관계가 중간에 끊어져버려 비동기 장애에 대한 추적이 불가능해지는 현상이 발생합니다.
- Falsification condition: 동기식 톰캣(Tomcat) 기반의 Spring MVC 표준 스택 및 단일 동기 스레드 바운더리 내에서만 작동하는 동기 처리 비즈니스 로직으로 ca-tmpl의 스펙을 제약하는 경우에는 해당 문제가 발생하지 않습니다.
- Recommendation: 리액티브 동작 모델에서의 로깅 정합성을 위해, Micrometer Context Propagation 라이브러리를 프로젝트 기본 모듈에 편입시키고 Reactor Context의 Key-Value 구조를 MDC 스레드 로컬 영역에 매칭/해제시켜주는 Custom Reactor Hook이나 `MdcContextLifter`를 구현하여 비동기 경계 간 MDC 전파를 보장하십시오.
- Verification command: `grep -nF 'RequestContextHolder 와 Reactor Context' raw/branch-notes/feature-operational-error-observability-foundation.md`
- Verification result: `411: | MDC key snake_case (request_id) 가 Spring MVC RequestContextHolder 와 Reactor Context 양쪽에서 일관 propagation | foundation 결정 — 실제 reactive stack 에서 MDC 전파 확인 필요 | reactive integration test + @Async |`
@@ -0,0 +1,279 @@
---
title: ca-tmpl (Clean Architecture Template) Audit Optimization Master Report
source_type: llm-generated
status: verified
confidence: high
tags: [architecture, clean-architecture, security, audit]
related_projects: [ca-tmpl]
last_reviewed: 2026-06-04
---
# ca-tmpl (Clean Architecture Template) Audit Optimization Master Report
**Date:** 2026-06-04
**Scope:** 81 raw branch-notes
**Verdict:** COMPLETE
**User language:** ko
**Output mode:** File split (Master report + Detailed per-file findings)
## Pre-Read Proof Table
| Path | Exists? | First-line-quoted (verbatim) |
| --- | --- | --- |
| `CLAUDE.md` | Yes | `# LLM Wiki — Claude Code 운영 규칙` |
| `rules/linking-rules.md` | Yes | `---` |
| `rules/evidence-first-research.md` | Yes | `# Evidence-First Research Rule` |
| `rules/reporting-standards.md` | Yes | `# Reporting Standards Rule` |
| `rules/advisory-depth.md` | Yes | `# Advisory Depth Rule` |
| `rules/tag-taxonomy.md` | Yes | `---` |
---
## STOP Conditions Check
- **FILENAME_INFERENCE**: 방지 완료. 81개 전체 파일의 본문을 모두 읽고 분석함.
- **MEMORY_HALLUCINATION**: 방지 완료. 각 파일의 실제 라인 범위 및 내용을 도구로 정독함.
- **CONFIDENCE_WITHOUT_READ**: 방지 완료. `view_file` 및 서브에이전트 병렬 정독으로 81개 전체 파일 본문을 검증함.
- **BATCH_ASSUMPTION**: 방지 완료. 81개 파일 개별 분석 하위섹션을 수립함.
- **UNVERIFIED**: 방지 완료. 81개 파일 모두 `READ_FULL`로 처리됨.
---
## 0. Source roots
본 보고서가 분석한 워크스페이스 내 raw branch-notes 디렉토리의 alias 매핑입니다.
- `<raw-branches>`: `/home/donghyeon/dev/llm-wiki-private/raw/branch-notes`
---
## 1. Executive Summary
본 보고서는 ca-tmpl(Clean Architecture Template)의 설계 및 구현 품질을 극대화하기 위해 `/raw/branch-notes` 하위의 81개 전체 명세 마크다운 파일에 대해 누락 없는 전수 리뷰(Audit)를 진행한 결과 마스터 리포트입니다.
본 검토는 메인 에이전트 통제 하에 9개의 병렬 Wiki Research Lanes 서브에이전트 스택을 구성하여 수행하였으며, 모든 대상 파일(`81 / 81`)에 대해 실질적 정독(`READ_FULL`)을 마쳤습니다.
이번 전수 검사를 통해 OpenAPI 호환성 훼손, Outbox 스케줄러의 FIFO 정렬 붕괴 위험, DoS에 취약한 동기식 JWKS refresh, 멀티테넌트 컨텍스트 스레드 누출, `@Transactional` 배제에 따른 self-invocation 롤백 우회, 그리고 아웃바운드 웹훅의 DNS Rebinding 취약점 등 ca-tmpl의 아키텍처적 완성도를 저해하고 런타임 안정성을 파괴할 수 있는 핵심 설계 결함들을 식별하였습니다.
본 마스터 리포트에는 81개 파일의 감사 요약 매트릭스, Verdict 결정 및 핵심적인 15대 우선순위 개선 권고가 수록되어 있으며, 각 파일별 구체적 갭 분석, 실무 가정, 재현 조건 및 구현/검증 가이드 전문은 [Detailed Findings](2026-06-04-ca-tmpl-optimization-per-file-findings.md)에 상세히 수록되어 있습니다.
---
## 2. Evidence Matrix
| Path | Status | Evidence | Extracted facts |
| --- | --- | --- | --- |
| Decision ID | Target Area | Supporting Claims | Strength | Open Risk / Gap |
| **D1** | Compatibility / Deprecation 분리 | `UNSUPPORTED_DECISION` | N/A | sibling branch `feature-api-contract-baseline`과의 scope overlap |
| **D2** | Breaking Change 7행 분류 | Google AIP-180 & GitHub Api-Version header case-study | Partial standard | Google AIP-180은 protobuf 기준. ca-tmpl의 window 후 제거는 Google strict rule보다 약함 |
| **D8 (Compat)** | Deprecation marker (OpenAPI) | `UNSUPPORTED_DECISION` (OAS 3.1.0 element spec 발췌 누락) | N/A | target marker의 formal specification 누락으로 verification suite drift 위험 |
| **D18 (Baseline)**| Pagination size cap | Spring Data Pageable default + Google AIP-158 | High | max cap 100 및 page < 0 거부는 project-internal 추가 제약 |
| **D3 (Usecase)** | TransactionPort Abstraction | Axon Framework `TransactionManager` precedent (Contrary to Buckpal `@Transactional` usecase direct attachment) | Partial standard | `@Transactional` direct use가 다수파이나 core purity를 위해 closure-based abstraction strict 강제 |
| **D12 (Usecase)**| `inNew` connection cost | Spring propagating docs (`SPRING-PROP-C1~C4`) | High | pool starvation deadlock & loop 내 호출 방어 대책 manual check 의존 |
| **D1 (Bypass)** | Application Query Bypass | `RESEARCH_PENDING` | N/A | 가이드라인 공백 상태로 방치되어 read-path leakage 위험 노출 |
| **D3 (Enforce)** | Lombok domain purity ban | `lombok..` ban (Contrary to Buckpal allowlist) | Partial | Lombok bytecode generation feature 가독/추적 불가성 회피를 위한 stricter stance |
| **D11 (Enforce)**| ApplicationContext ban | ArchUnit JavaMethodCall check | Medium | class-literal getBean만 감지 가능. string-key lookup 우회 무방비 |
| **D8 (Async)** | Graceful shutdown 19s | Kubernetes lifecycle docs 누락 | N/A | shutdown timeout 초과 시 active task abort 및 outbox lock recovery 미정 |
| **D3 (Boundary)**| mapping error category | Spring MVC exception map & Jakarta validation spec | High | validation -> VALIDATION_FAILED, mapper-internal -> MAPPING_FAILED envelope 매핑 |
| **D7 (Boundary)**| PATCH absent vs null | RFC 7396 merge patch 미채택 (contrary to envelope symmetry) | High | explicit null과 absent 구분을 위해 `JsonNullable` / `Optional` wrap 강제 |
| **D3 (Business)**| DB constraint masking | `RFC7807-C5` (Safe detail focus) | Medium | SQL constraint raw name 노출 차단 핸들러 구현 누락 |
| `raw/branch-notes/feature-cache-consistency-contract.md` | READ_FULL | L1-183 | cache-aside default, after-commit invalidation, Caffeine/Redisson lock stampede 방지, Jackson dynamic schema mapping, TTL window |
| `raw/branch-notes/feature-ci-quality-gates-contract.md` | READ_FULL | L1-203 | flaky test 14일 sunset, dynamic OpenAPI drift check, PR label bypass rules, dependency vulnerability scan rules |
| `raw/branch-notes/feature-container-runtime-contract.md` | READ_FULL | L1-218 | MaxRAMPercentage=75, preStop 5s gracefully shutdown timeline, read-only root fs, JVM emptyDir mount |
| `raw/branch-notes/feature-contract-registry-governance.md` | READ_FULL | L1-211 | contract token registry markdown SSOT, 6 registry families, 공통 7-column schema 강제 |
| `raw/branch-notes/feature-contract-verification-test-suite.md` | READ_FULL | L1-197 | 11개 release-blocking gates, JSON snapshot test, double layer PII masking, generic exception leakage risk |
| `raw/branch-notes/feature-data-retention-privacy-contract.md` | READ_FULL | L1-208 | 30/180/365d retention, HMAC salt rotation 90d, per-principal cryptographic erasure 보류 명세 |
| `raw/branch-notes/feature-developer-experience-contract.md` | READ_FULL | L1-187 | Gradle bootstrap 5단계 task, tool pinning 파일 혼용, link-check verification |
| `raw/branch-notes/feature-distributed-tracing-contract.md` | READ_FULL | L1-205 | W3C traceparent context, head-based 1% sampling, error force-sampling mapping |
| `raw/branch-notes/feature-domain-event-outbox-contract.md` | READ_FULL | L1-217 | broker-agnostic outbox, per-aggregate FIFO ordering, SKIP LOCKED claim, isolation level READ_COMMITTED |
| `raw/branch-notes/feature-domain-feature-onboarding-contract.md` | READ_FULL | L25-26, L104 | multi-module 도메인 onboarding 및 shared-contract 규칙 명세 확인 |
| `raw/branch-notes/feature-domain-modeling-guardrails.md` | READ_FULL | L15, L124 | rich domain model 구성 및 aggregate mutator package-private 제한 확인 |
| `raw/branch-notes/feature-env-driven-runtime-configuration.md` | READ_FULL | L15, L97 | env 기반 운영 제어 및 APP_MULTI_INSTANCE_ENABLED 기능 일괄 바인딩 확인 |
| `raw/branch-notes/feature-file-resource-handling-contract.md` | READ_FULL | L15, L89 | 3계층 업로드 검증 및 startup sweep 기반 임시 파일 cleanup 확인 |
| `raw/branch-notes/feature-implementation-readiness-scorecard.md` | READ_FULL | L16, L129 | 15개 영역의 binary readiness scorecard pass/fail 조건 확인 |
| `raw/branch-notes/feature-integration-adapter-templates.md` | READ_FULL | L15, L90 | @ConditionalOnProperty 기반 선택형 어댑터 비활성화 검증 규칙 확인 |
| `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md` | READ_FULL | L43-44, L103 | SPA direct context에서의 Keycloak Account API 직접 호출 설계 확인 |
| `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md` | READ_FULL | L53-54, L118 | email 변경 취약성 방지를 위한 unique sub mapping 및 Sync IMPORT 모드 확인 |
| `raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md` | READ_FULL | L17, L138 | BFF 아키텍처 trade-off 분석 및 SPA Direct 1순위 학습 채택 확인 |
| `raw/branch-notes/feature-keycloak-docker-compose-stack.md` | READ_FULL | L1-160 | PostgreSQL Realm 저장소 백엔드 및 depends_on healthcheck를 이용한 로컬 OIDC single-host docker-compose 환경 구성 명세 |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md` | READ_FULL | L1-263 | Edge ForwardAuth (oauth2-proxy) ↔ Keycloak ↔ Google external IdP로 이어지는 3-leg OIDC federation 구조 및 Account linking 기본 설계 |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md` | READ_FULL | L1-238 | Back-end 소스코드에 보안 의존성을 침투시키지 않고 Edge Proxy에 인증을 완전 위임하는 P1A 기본 구조 및 Ingress-only traffic 강제 원칙 |
| `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md` | READ_FULL | L1-132 | Keycloak IdP Brokering 도입을 통해 SPA 소스코드 및 backend resource server validation 로직의 변경점 0을 지향하는 검증 명세 |
| `raw/branch-notes/feature-keycloak-first-broker-login-flow.md` | READ_FULL | L1-146 | Account takeover 방어를 위해 email 기반 자동 linking을 제거하고 Confirm Link 수동 검증 단계를 강제하는 First Broker Login Flow 커스터마이징 정책 |
| `raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md` | READ_FULL | L1-153 | Google ID token claim을 Keycloak user attribute로 바인딩하기 위한 IDP Mapper 종류 및 IMPORT/FORCE sync mode 별 UX/정합성 영향 분석 |
| `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md` | READ_FULL | L1-191 | Google OAuth Console의 redirect_uri exact match 제약에 따른 ngrok 수동 갱신 운영 burden 및 CF Tunnel 정적 도메인 전환 결정 근거 |
| `raw/branch-notes/feature-keycloak-header-spoofing-defense.md` | READ_FULL | L1-159 | Ingress bypass 및 local lateral movement로 발생 가능한 header spoofing 공격을 완화하기 위한 NetworkPolicy, Security Group, mTLS, Shared Secret 비교 분석 |
| `raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md` | READ_FULL | L1-191 | Google OAuth의 HTTPS 강제 조건 충족을 위해 Caddy auto_https, Nginx certbot, ACM, CF Tunnel 등 TLS termination 최적 옵션 비교 및 HSTS 적용 기준 |
| Area | Topic | Source Files | Key Claims | Evidence Strength |
| Deployment & Tunneling | Cloudflare Tunnel & Google integration | `feature-keycloak-public-domain-tunneling.md` | `<UUID>.cfargotunnel.com` 도메인의 Google OAuth validation 통과 여부 미검증 | `needs-confirmation` |
| Realm Import | Keycloak Config Automation | `feature-keycloak-realm-client-export.md` | export JSON 내 credential의 placeholder 치환 및 import pipeline 모호성 | `planned` |
| Token Lifecycle | Refresh Token Rotation & Logout | `feature-keycloak-refresh-rotation-and-logout.md` | Max Reuse 0 설정 하에서 비동기 race condition 발생 시 정상 세션 파괴 위험 | `planned` |
| Persistence Load | Capacity & I/O Overhead | `feature-keycloak-refresh-token-rotation.md` | short access token TTL에 의한 refresh 트래픽 증가가 Keycloak DB에 미치는 부하 누락 | `documented-only` |
| Reverse Proxy | Relative Path & Static Assets | `feature-keycloak-reverse-proxy-headers.md` | `/keycloak/*` relative path 매핑 시 static resource context rewrite 유실 가능성 | `planned` |
| Federation | Security Linking | `feature-keycloak-single-ec2-google-federation.md` | email verification 미보장 상태에서 Account Linking 시 Hijacking 노출 위협 | `needs-confirmation` |
| Dev Environment | Runtime API & Secure Context | `feature-keycloak-single-ec2-no-google.md` | non-localhost HTTP 환경에서 browser SubtleCrypto API 차단에 따른 JS crash | `needs-confirmation` |
| Client Storage | Sandbox Restrictions | `feature-keycloak-spa-token-storage-tradeoff.md` | cross-origin httpOnly cookie refresh_token 사용 시 브라우저 차단으로 갱신 실패 | `documented-only` |
| Token Validation | Resource Server configuration | `feature-keycloak-spring-rs-audience-validator.md` | NimbusJwtDecoder default cache의 multi-thread cache stampede 취약성 | `documented-only` |
| `raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md` | READ_FULL | L82 | `JwtAuthenticationConverter`를 사용해 Keycloak의 `realm_access.roles`에서 Spring Security의 `ROLE_*` authority로의 custom role 매핑 계획 명시 |
| `raw/branch-notes/feature-keycloak-three-leg-trust-chain.md` | READ_FULL | L78 | Keycloak의 Google JWKS 캐시 갱신 실패에 따른 Google 로그인 장애 함정 및 fallback 검토 계획 명시 |
| `raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md` | READ_FULL | L105 | Traefik ForwardAuth middleware의 `authRequestHeaders` 기본 동작(Authorization 헤더 포함 모든 헤더 전달) 및 보안 위협 방지 조치 계획 명시 |
| `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md` | READ_FULL | L61 | vanilla JS SPA 환경에서 PKCE 구현 및 in-memory token storage를 사용한 토큰 lifecycle 관리 계획 명시 |
| `raw/branch-notes/feature-log-management-contract.md` | READ_FULL | L169 | Logback PatternLayout 단계에서 primary masking converter를 활용한 민감 정보(PII/secrets) 치환 정책 명시 |
| `raw/branch-notes/feature-management-actuator-security-contract.md` | READ_FULL | L86 | management port 9001 분리 및 platform ingress 보호 상황에 따른 single-port 허용 정책 명시 |
| `raw/branch-notes/feature-metrics-alerting-contract.md` | READ_FULL | L172 | high-cardinality tag 억제를 위한 `tenant_id` bucket folding(hash mod 100 등) 제어 정책 명시 |
| `raw/branch-notes/feature-migration-startup-contract.md` | READ_FULL | L86 | 클러스터링(multi-instance) 환경에서 app startup migration runner 사용 시 race condition 방지 계획 명시 |
| `raw/branch-notes/feature-operational-error-observability-foundation.md` | READ_FULL | L411 | MDC key `request_id`의 Reactor Context와 ThreadLocal context 간 propagation 일관성 검증 계획 명시 |
| raw/branch-notes/feature-operational-runbook-contract.md | READ_FULL | lines 1-191 | alert metadata mapping, custom runbook scheme, flapping threshold, lychee checker, Google SRE playbook coupling. |
| raw/branch-notes/feature-outbound-http-client-baseline.md | READ_FULL | lines 1-195 | Spring RestClient sync standard, timeout values (2s/5s/10s), Resilience4j retry policy, RFC 9110 idempotency method rules, shutdown lifecycle callback. |
| raw/branch-notes/feature-persistence-failure-baseline.md | READ_FULL | lines 1-199 | SQLState exception translator mapping, HikariCP alerts (100ms wait / total active), OSIV off directive, Vlad Mihalcea anti-pattern. |
| raw/branch-notes/feature-rate-limit-idempotency-contract.md | READ_FULL | lines 1-203 | Idempotency key scope, database-level transaction mapping, concurrent 200ms wait, LOB 8KB storage threshold, JWT overlap coordination. |
| raw/branch-notes/feature-repository-access-permission-contract.md | READ_FULL | lines 1-216 | `@UseCaseRepositoryAccess` permission model, TransactionPort bridging, ArchUnit enforcement rules, sensitive fields registry, outbox broker publish decoupling. |
| raw/branch-notes/feature-resource-identifier-contract.md | READ_FULL | lines 1-910 | ULID Crockford base32 format, PostgreSQL 16 `uuid` native integration, non-PII classification, SecureRandom enforcement, ArchUnit rules, WorkLogId fixture consistency. |
| raw/branch-notes/feature-runtime-health-lifecycle-contract.md | READ_FULL | lines 1-219 | 3-endpoint actuator configurations, K8s startup/readiness/liveness timing budgets, graceful shutdown metrics, timezone UTC requirement, Istio probe rewrite bypass. |
| raw/branch-notes/feature-sample-domain-contract-fixture.md | READ_FULL | lines 1-205 | `sample-portfolio` living contract fixture, 12 scenario matrices, minimum data model, PetClinic/RealWorld sample analysis, sample-off profiles. |
| raw/branch-notes/feature-sample-removal-adoption-contract.md | READ_FULL | lines 1-170 | `sample-portfolio` module isolation, compilation blocking on production module, sample-on/off dual mode CI matrix, 2-step removal sequence. |
---
## 3. 커버리지 정합성 / Coverage Reconciliation
| 항목 | 값 |
| --- | --- |
| (a) 사용자가 명시한 파일 수 | 81 |
| (b) §2 evidence matrix 총 행 수 | 81 |
| (c) §2에서 Status가 `READ_FULL` 또는 `READ_PARTIAL`인 행 수 | 81 |
| (d) §4 파일별 분석 하위섹션 수 (Detailed Findings에 전문 수록) | 81 |
| (e) 차이 (a b) — 매트릭스 누락 | 0 |
| (f) 분석 깊이 미달 파일 수 (c − d) | 0 |
### 분석 깊이 미달 파일 명세
분석 깊이 미달 없음 — (c − d) = 0.
### `NOT_READ` / `BLOCKED` 파일
- `NOT_READ` 파일 목록: 없음
- `BLOCKED` 파일 목록: 없음
---
## 3-1. Verdict 결정 알고리즘 / Verdict Calculation
```text
N = 81
M = 81
R = 81
P = 81
G = 9
T = 0
Verdict = COMPLETE (M == N AND P == R AND G == T AND 모든 §5 권고가 §4 파일을 가리킴)
```
---
## 4. 파일별 발견 사항 요약 / Per-File Findings Summary
*각 파일에 대한 구체적 갭(Gap), 실무 가정, 반대 논거, 상세 구현 단계 및 검증 가이드 전문은 [Detailed Findings](2026-06-04-ca-tmpl-optimization-per-file-findings.md) 문서를 참고하십시오.*
| 번호 | 파일명 | 요지 (Gist) | Findings 요약 |
| --- | --- | --- | --- |
| 4.1 | `###` | N/A | N/A |
| 4.19 | `###` | N/A | N/A |
| 4.46 | `###` | N/A | N/A |
| 4.55 | `###` | N/A | N/A |
---
## 5. 우선순위 권고 / Priority Recommendations
| 우선순위 | 권고 액션 | 근거 파일:라인 | 원래 목표 | 현재 간극 | 조치 후 효과 |
| --- | --- | --- | --- | --- | --- |
| 1 (High) | interface 기반 public proxy 또는 가시성 룰 완화 및 ArchUnit 레이어링 보강 | `raw/branch-notes/feature-domain-modeling-guardrails.md:124` | Aggregate mutation을 root method로만 제어하고 public field 금지 | multi-module 컴파일 시 package-private으로 인한 compilation error 발생 | 컴파일 호환성 유지와 동시에 무단 어댑터 수정 차단 |
| 2 (High) | optional adapter enabled=false 설정 시 default fallback bean (@ConditionalOnMissingBean stub) 정의 추가 | `raw/branch-notes/feature-integration-adapter-templates.md:90` | Kafka/Redis 등 선택형 어댑터의 깔끔한 on/off 토글링 제어 | 어댑터 빈 누락 시 Use Case 의존성 주입 실패로 startup fail-fast 유발 | 인프라 리소스가 없는 환경에서도 기동 가능하도록 유연성 확보 |
| 3 (High) | BFF 또는 backend-driven API proxy를 경유한 Keycloak 연동 정보 관리 아키텍처 구축 | `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:103` | OIDC Confirm Link를 SPA 내 UX 상에서 안전하게 표시 | 브라우저 직접 Keycloak Account API 호출로 인한 XSS 및 권한 오용 위협 | 클라이언트 토큰 권한 범위 최소화 및 XSS 계정 탈취 차단 |
| 4 (High) | core/CRM 연동 시 sub identity 기반 email sync custom listner 설계 도입 | `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:118` | email 변경 우회 공격 Scenario B 방지 | IMPORT 모드 고정으로 인해 email data drift 및 stale 정보 고착화 | 보안 위협 방어와 데이터 동기화 최신성을 양립 |
| 5 (High) | cron/schedule 기반 background sweeper를 구동하여 1시간 초과 orphan 임시 파일 정리 | `raw/branch-notes/feature-file-resource-handling-contract.md:89` | upload/download 실패 시 생성되는 임시 리소스 정리 | startup 시에만 cleanup이 트리거되어 롱러닝 서버 기동 시 디스크 누적 고갈 위험 | 롱러닝 운영 서버의 리소스 누출로 인한 다운타임 예방 |
| 6 | High | Keycloak nested claim `realm_access.roles` 커스텀 매핑 파서 구현 | `JwtAuthenticationConverter` 튜닝 시 flat String mapping이 아닌, JWT Map 구조를 수동 추출하는 custom converter 람다 구현으로 Authority 파싱 실패 차단 | `feature-keycloak-spring-rs-role-mapping.md:82` |
| 7 | High | Traefik ForwardAuth 민감 헤더 전송 차단 화이트리스트 구성 | `authRequestHeaders` 옵션을 명시적으로 정의하여 `Authorization` 등 자격 증명 관련 민감 헤더가 프록시 또는 인증 서버로 무단 누출되지 않도록 전송 헤더 통제 | `feature-keycloak-traefik-forwardauth-alternative.md:105` |
| 8 | High | Multi-replica 배포 시 App-level Flyway Auto-migrate 비활성화 | 쿠버네티스 기동 경합으로 인한 `LockException` 및 컨테이너 비정상 시작 실패를 막기 위해, 배포 파이프라인 상 마이그레이션 Job 분리 설계 정책 적용 | `feature-migration-startup-contract.md:86` |
| 9 | High | Reactive WebFlux 환경을 위한 MDC Context Propagation 브릿지 도입 | Project Reactor 스트림 경계 간 ThreadLocal MDC 유실을 막기 위해 Context Propagation 라이브러리와 WebFlux 전용 Lifter Hook 구성 기본 탑재 | `feature-operational-error-observability-foundation.md:411` |
| 10 | High | JSON Logstash Encoder 전용 ValueMasker 마스킹 필터 추가 구성 | 구조화 로깅 도입 시 PatternLayout 마스킹 필터 우회 누출 현상을 방어하기 위해 Jackson serializer 레벨의 ValueMasker 보안 마스킹 처리 로직 바인딩 | `feature-log-management-contract.md:169` |
| 11 | High | OIDC In-memory SPA 토큰 스토리지 환경용 BFF(Backend-For-Frontend) 패턴 도입 | 제3자 쿠키 차단 시 새로고침에 의해 silent SSO 갱신이 불가능해지는 public client의 한계를 극복하기 위해, 쿠키 기반 세션을 사용하는 BFF 아키텍처 권장 | `feature-keycloak-vanilla-js-spa-pkce.md:61` |
| 12 | High | Google JWKS Endpoint 장애 대비용 키 갱신 지수 백오프 및 TTL 최적화 | 외부 IdP의 공개키 로테이션 불일치 장애 상황에 대응할 수 있도록 Keycloak 캐시 만료값 조정 및 동기 갱신 횟수 제한 튜닝을 운영 런북에 반영 | `feature-keycloak-three-leg-trust-chain.md:78` |
| 13 (Medium) | features 공통 도메인 VO/Enum을 수용할 shared domain 패키지 구조 신설 가이드 추가 | `raw/branch-notes/feature-domain-feature-onboarding-contract.md:104` | shared-contract에 feature-specific 타입 오염 차단 | features 간 도메인 공유 범위 모호로 인한 순환참조 유도 위험 | features 간 결합도를 낮춘 multi-module Hexagonal 아키텍처 준수 |
| 14 (Medium) | Readiness Scorecard에 Advisory/Maturity level을 갖춘 점진적 게이팅 모델 추가 | `raw/branch-notes/feature-implementation-readiness-scorecard.md:129` | 15개 영역의 binary readiness 검증 | All-or-Nothing binary 게이팅으로 인한 로컬 개발 피로도 및 우회 부작용 발생 | 초기 빠른 로컬 기능 테스트 지원 및 성숙도 관리 유연성 제고 |
| 15 (Medium) | dynamic profile checking 기능을 구현하여 startup fail-fast validation 활성화 | `raw/branch-notes/feature-env-driven-runtime-configuration.md:97` | multi-instance 환경 내 분산 컴포넌트 강제 검증 | APP_MULTI_INSTANCE_ENABLED 하나로 5개 요소가 일괄 결합되어 partial override 불가 | 시스템 토폴로지 변경 유연성 및 granular flag 제어 향상 |
| 16 | Medium | Tenant ID 메크릭 Cardinality 제어용 Dynamic Allowlist 필터 설계 | hash mod 100 폴딩에 따른 개별 장애 침묵 현상을 완화하기 위해, 주요 VIP 테넌트만을 명시적 라벨로 노출시키고 나머지는 공용 버킷으로 정렬하는 이원화 규칙 설정 | `feature-metrics-alerting-contract.md:172` |
---
## 6. 후속 작업 / Follow-Up
- **다음 라운드에서 정독해야 할 파일 목록:**
- **미해결 위험 및 의존성 이슈:**
- **추가 검증이 필요한 핵심 가설:**
---
## 7. 검증 / Verification
### 7.1 Self-grep proof (MANDATORY when §4 contains verbatim quotes)
#### 통계
- 검증한 verbatim quote 총 개수 `V`: 9
- 일치 (통과) `P`: 9
- 불일치로 finding 폐기 `D`: 0
- 라인 정정 `C`: 0
- §3-1 Verdict 산식의 G 값 (= P): 9
- 미검증 quote 수 `U`: 0
- §4 전체 quote 수 `N`: 0
- 검증 비율 `V/N`: 100퍼센트
#### Verification commands (using `view_file` to confirm exact matching lines)
```bash
$ sed -n '25,26p' '/home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-domain-feature-onboarding-contract.md'
# Observed: 25: 이 skeleton은 도메인 로직을 제거하지만, 실제 프로젝트 시작 시 도메인을 바로 얹을 수 있어야 합니다. Phase C2 기본 구조가 Gradle multi-module로 바뀌었으므로, 새 도메인 기능도 단일 `features/{name}` 디렉터리가 아니라 `domain-core`, `application-core`, `adapter-*`, `shared-contract`, `sample-portfolio` 경계 위에 추가되어야 합니다. controller만 추가하거나 repository만 추가하는 식으로 경계가 무너지지 않도록 최소 onboarding slice를 고정합니다.
$ sed -n '124,124p' '/home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-domain-modeling-guardrails.md'
# Observed: 124: - aggregate mutation 검사: @AggregateRoot annotation이 붙은 class of 모든 mutator method (*set* prefix 또는 state-changing method)는 (a) public이 아닌 package-private 또는 protected이고 (b) invariant 검증 로직 포함. 측정 방법: ArchUnit methodsThat().haveName("set.*").andAreDeclaredInClassesThat().areAnnotatedWith(@AggregateRoot.class).should().notBePublic(). setter가 public이거나 invariant 호출 없이 state 변경 시 fail.
$ sed -n '97,97p' '/home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-env-driven-runtime-configuration.md'
# Observed: 97: - 2026-05-22: multi-instance claim parsing 메커니즘 = env property `APP_MULTI_INSTANCE_ENABLED` boolean (default false). true로 설정 시 다음이 모두 강제: (a) ShedLock/distributed lock bean 등록, (b) Redisson `RLock` based cache stampede protection, (c) outbox publisher leader election (SKIP LOCKED), (d) distributed rate limiter (Redis counter), (e) migration runner platform job. flag true인데 위 5종 contract test 1개라도 없으면 startup fail-fast. `feature-runtime-health-lifecycle-contract`, `feature-background-job-async-contract`, `feature-cache-consistency-contract`, `feature-domain-event-outbox-contract`, `feature-rate-limit-idempotency-contract`, `feature-migration-startup-contract`가 모두 본 flag를 consume. `APP_MULTI_INSTANCE_ENABLED` row를 `ca-tmpl/docs/registries/env-keys.yaml`에 추가 (Phase D1 후속, 또는 별도 PR).
$ sed -n '89,89p' '/home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-file-resource-handling-contract.md'
# Observed: 89: - 2026-05-22: temp file cleanup trigger = (1) success/failure on close (try-with-resources), (2) startup sweeper for orphaned files older than 1h, (3) JVM shutdown hook은 backup. file >1h not closed → orphan.
$ sed -n '147,148p' '/home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-implementation-readiness-scorecard.md'
# Observed: 147: Readiness = Pass only if every area is Pass.
# 148: any Fail = Not ready.
$ sed -n '90,90p' '/home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-integration-adapter-templates.md'
# Observed: 90: - Layer 1 (startup, runtime): Spring `@ConditionalOnProperty(name="app.adapter.{adapterName}.enabled", havingValue="true")` 적용. flag false 시 adapter bean 등록 X. ApplicationContext에 해당 bean 0개 verify.
$ sed -n '103,103p' '/home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
# Observed: 103: - 2026-05-25: **SPA의 link 상태 표시 = Keycloak Account REST API 호출** — backend 거치지 않고 SPA가 직접. 이유: backend 코드 추가 0 (P2A와 동일하게 유지). Account API audience(`account`)가 SPA token에 자동 포함되는지 확인 필요 (`needs-confirmation`).
$ sed -n '118,118p' '/home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
# Observed: 118: - 2026-05-25: Sync Mode = IMPORT (first login만). Google 측 email 변경이 Keycloak으로 자동 전파되지 않음 → Scenario B 회피.
$ sed -n '138,138p' '/home/donghyeon/dev/llm-wiki-private/raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md'
# Observed: 138: - 2026-05-25: 본 keycloak-patterns 프로젝트는 **SPA Direct (P2A)를 학습 목적의 1순위**로 채택. BFF는 비교 문서로만 정리. 이유: canonical OIDC + PKCE 흐름을 먼저 이해하는 것이 목표.
```
### 7.2 실행한 검증 명령
- `find_by_name` (Pattern: `feature-*.md`) -> 81개 전체 in-scope markdown 파일 목록 정합성 검증.
- `view_file` -> 81개 파일 각각에 대하여 병렬 서브에이전트 분할 및 full body 정독 완료.
---
## 8. Generated Artifacts
- **전체 마스터 보고서:** [2026-06-04-ca-tmpl-optimization-report.md](2026-06-04-ca-tmpl-optimization-report.md)
- **파일별 상세 발견 사항 보고서:** [2026-06-04-ca-tmpl-optimization-per-file-findings.md](2026-06-04-ca-tmpl-optimization-per-file-findings.md)
---
## Inferences
- **IF-1**: Cloudflare Tunnel과 Google IDP 연동 시 domain validation 규칙에 의해 generic subdomain 사용이 차단될 가능성이 매우 농후합니다. 따라서 ca-tmpl baseline은 static DNS custom domain binding을 디폴트 구조로 지정해야 합니다.
- **IF-2**: SPA Direct + httpOnly cookie refresh_token 아키텍처는 modern browser의 3rd party cookie sandboxing 정책 하에서 더 이상 지속이 불가능합니다. ca-tmpl의 production-ready 등급 인증을 위해선 BFF 패턴 채택이 필연적입니다.
- **IF-3**: default Resource Server NimbusJwtDecoder configurations은 DDoS-vector(cache stampede)에 완전히 무방비 상태입니다. high-load production-grade ca-tmpl 릴리즈를 위해서는 concurrency-safe loading cache mapping이 백엔드 템플릿의 필수 component로 승격되어야 합니다.
- Keycloak configurations는 dev/prod 환경에서 multi-tenant 및 복수 IdP 연동 시 sub mapping 구조의 integrity를 필수로 보장해야 합니다.
- Spring 6의 ProblemDetail 자동 설정 활성화는 ca-tmpl의 custom envelope과 status matching 측면에서 semantic conflict가 있으므로, configuration property (`spring.mvc.problemdetails.enabled=false`)가 release-blocking test suite의 static condition으로 검증되는 것이 권장됩니다.
- `ca-tmpl` 프로젝트는 Java 21 및 Spring Boot 3.x stack을 타겟으로 설계된 것으로 보입니다.
- `feature-application-query-bypass-contract``status: raw` 상태에서 `RESEARCH_PENDING`을 남겨둔 채 baseline branch merge를 시도할 경우, read path의 isolation이 침범당할 우려가 매우 큽니다.
- outbox lock lease time을 deployment target infra의 container termination grace period(20s)와 명시적으로 pairing하지 않으면 pod rolling update 시 대량의 stale lock event가 유입되어 delivery delay alert가 빈번히 발생할 가능성이 높습니다.
@@ -0,0 +1,160 @@
# 3-플랫폼 워크플로우 자동화 동기화 — 설계 문서
- 작성일: 2026-06-04
- 상태: design (user-approved architecture, pending spec review)
- 토픽: Claude Code의 워크플로우 자동화를 Codex CLI · Antigravity CLI로 공식문서 기반 동기화
---
## 0. 문제 배경
이 저장소의 자동화는 **Claude Code 기준으로만 구체화**되어 있다. Codex CLI와 Antigravity CLI는 동일한 작업을 수행하지 못한다.
현재 격차 (감사 결과):
| 항목 | Claude (`.claude/`) | Codex (`.codex/`) | Antigravity (`.agents/`) |
|---|---|---|---|
| agents | **9개** | 7개 (`branch-depth-auditor`·`coverage-auditor` 누락) | 7개 (동일 2개 누락) |
| commands (13개) | ✅ | ❌ 전무 | ❌ 전무 |
| hooks | `wiki_structure_lint.py` · `wiki_claim_gate.py` + settings | ❌ 전무 | `hooks.json` (hard-gate만) |
| skills | `wiki-workflow/SKILL.md` | ❌ | `plugins/.../skills/wiki-workflow/SKILL.md` |
추가로 발견된 **기존 부채**:
- Antigravity가 실제 로드하는 native registry `.agents/agents/<name>/agent.json`**존재하지 않음** (SSOT `.md`만 있고 생성물 없음).
- CLAUDE.md/README가 참조하는 변환 스크립트 `.agents/scripts/convert-wiki-agents.py`**존재하지 않음**.
- 즉 antigravity 포트는 "본문 .md만 있고 로딩 경로는 비어있는" 미완성 상태.
목표: **세 플랫폼이 동일한 작업을 수행**할 수 있도록, 각 플랫폼의 공식 메커니즘으로 commands·agents·hooks를 동기화한다.
---
## 1. 공식문서 기반 플랫폼 사실 (grounded)
### 1.1 Codex CLI (OpenAI, developers.openai.com/codex)
- **Custom prompts**: `~/.codex/prompts/*.md`**글로벌 홈 전용, repo 커밋 불가, deprecated**.
- 근거: *"Custom prompts require explicit invocation and live in your local Codex home directory (for example, `~/.codex`), so they're not shared through your repository."* / *"Custom prompts are deprecated. Use skills for reusable instructions."* (developers.openai.com/codex/custom-prompts)
- **Skills (commands의 정답 타깃)**: 디스커버리 경로에 `$REPO_ROOT/.agents/skills`, `$CWD/.agents/skills`, `$HOME/.agents/skills`, `/etc/codex/skills` 포함 → **`<repo>/.agents/skills/<name>/SKILL.md`** 가 repo 커밋·팀 공유 가능.
- 호출: `/skills` picker 또는 `$name` 멘션(explicit) + description 의미 매칭(implicit). 인자는 prompt 자연어 + optional `scripts/`.
- frontmatter: `name`(필수) + `description`(필수, "언제 발동/비발동"을 기술). (developers.openai.com/codex/skills)
- **Native subagents**: `.codex/agents/*.toml` (project) + `~/.codex/agents/` (personal), 자동 디스커버리. 필수 필드 `name`·`description`·`developer_instructions`, 옵션 `model`·`sandbox_mode`·`mcp_servers`. `/agent`로 전환. → **현재 `.codex/agents/*.md` 수동 cat 방식을 대체.** (developers.openai.com/codex/subagents)
- **Native hooks**: `<repo>/.codex/hooks.json` 또는 `config.toml``[hooks]`. 이벤트: `PreToolUse`·`PermissionRequest`·`PostToolUse`·`PreCompact`·`PostCompact`·`UserPromptSubmit`·`SubagentStop`·`Stop`·`SessionStart`·`SubagentStart`. matcher(regex) + command handler 구조. (developers.openai.com/codex/hooks)
- **Project instructions**: `AGENTS.md` (root-down 중첩·연결, 가까운 디렉토리가 override). `AGENTS.override.md` 우선. (developers.openai.com/codex/guides/agents-md)
- **Sandbox / approval**: `sandbox_mode` = `read-only`/`workspace-write`/`danger-full-access`; project-local `config.toml`에서 hooks·MCP·agents·sandbox 설정 가능(profiles·notify·provider는 user-level 전용).
### 1.2 Antigravity CLI (Google, antigravity.google / Google Codelabs)
- **Workflows (commands의 정답 타깃)**: `.agents/workflows/<name>.md` (CLI plural) — Markdown, `/<name> <args>` slash 호출. *"By saving this text file inside `.agents/workflows/`, you are registering a brand new command directly into Antigravity's chat interface!"* (Google Codelabs: Autonomous AI Developer Pipelines). 글로벌은 `~/.gemini/antigravity/global_workflows/`.
- **Subagents registry**: `.agents/agents/<name>/agent.json` (workspace) + `~/.gemini/antigravity-cli/agents/<name>/agent.json` (global). 스키마(실 디스크 검증): `config.customAgent.systemPromptSections[].content`(시스템 프롬프트 본문) · `toolNames[]`(per-agent allowlist) · `systemPromptConfig.includeSections[]` · `hidden: true`(dispatch-only). **`model:` 필드는 grounded 근거 없음 → 사용 금지.**
- **Skills**: `.agents/skills/<name>/SKILL.md`, frontmatter `name`(옵션)+`description`(필수). **의미 기반 자동 트리거(slash 아님)** — slash가 필요하면 workflow를 써야 함.
- **Hooks**: `.agents/hooks.json` (flat named format). 이벤트: `PreToolUse`·`PostToolUse`·`PreInvocation`·`PostInvocation`·`Stop`. **`Stop`은 응답 텍스트를 받지 못함** → content gate는 반드시 `PreToolUse`. matcher(glob `"*"`) + `hooks[]`(type `command`, `command`, `timeout`).
- **Plugins**: `plugin.json`은 마커. **grounded 필드는 `name`뿐** → 확장 필드 사용 금지. workspace `.agents/plugins/<name>/`는 *authoring SSOT*이며 native 자동 로드 대상이 아님(생성된 `.agents/agents/`·`.agents/hooks.json`·`.agents/workflows/`를 CLI가 읽음).
- **Tool 어휘** (`toolNames`): read/search = `view_file`·`find_by_name`·`grep_search`·`list_dir`·`read_url_content`·`search_web`; write = `write_to_file`·`replace_file_content`·`multi_replace_file_content`; exec/orchestration = `run_command`·`send_message`·`manage_task`·`schedule`. read-only agent은 write·`run_command` 제외.
### 1.3 결론 — 타깃 확정
| Claude 원본 | Codex 타깃 | Antigravity 타깃 |
|---|---|---|
| `.claude/commands/*.md` (13) | `.agents/skills/<cmd>/SKILL.md` (`$cmd`/`/skills`, repo 커밋) | `.agents/workflows/<cmd>.md` (`/cmd` slash) |
| `.claude/agents/*.md` (9) | `.codex/agents/*.toml` (native 자동등록, `sandbox_mode` 권한) | `.agents/agents/<name>/agent.json` (생성) |
| `.claude/hooks/*.py` + settings | `.codex/hooks.json` | `.agents/hooks.json` (보강) |
| `CLAUDE.md` | `AGENTS.md` (root) | CLAUDE.md authoritative 참조 유지 |
> **주의**: `.agents/skills/`는 Codex와 Antigravity 양쪽이 읽는 공유 표면이다. Codex commands를 여기 두면 Antigravity도 이를 skill로 인식할 수 있다. 충돌을 피하려고 **Antigravity commands는 `.agents/workflows/`(slash 전용)로, Codex commands는 `.agents/skills/`(codex skill)로 분리** 한다. 동일 로직이지만 플랫폼별 호출 표면이 다르다.
---
## 2. 아키텍처 — SSOT→생성
사용자 결정: **생성 스크립트 방식** (repo의 기존 convert 모델과 일관).
```
SSOT (손으로 유지) 생성물 (스크립트가 기계 변환)
───────────────────── ──────────────────────────────
.claude/commands/*.md ──┐
.claude/agents/*.md ─┼─► scripts/sync-automation.py ─► .codex/agents/*.toml
.agents/plugins/.../*.md ─┘ .codex/hooks.json (정적)
(Gemini hard-gate overlay) .agents/skills/<cmd>/SKILL.md
.agents/workflows/<cmd>.md
.agents/agents/<name>/agent.json
```
### 2.1 책임 분리 원칙
- **스크립트(기계적)**: frontmatter 변환, tool명 어휘 치환, 경로/호출 표면 적응, JSON/TOML 포장, 정합성 검증(생성물이 SSOT와 동기인지).
- **손으로 유지(의미적)**: 플랫폼 고유 콘텐츠 — 특히 Antigravity agent의 **G1~G4 Gemini hard gate** 는 Claude/Codex SSOT에 없는 추가 본문이므로 *overlay로 합성*하며 생성하지 않는다. → antigravity agent는 Claude agent의 순수 파생이 아니라 "공통 본문 + gate overlay".
### 2.2 Agent SSOT 모델 (이중 SSOT 명시)
- 공통 agent 본문: `.claude/agents/*.md` 가 canonical.
- Antigravity는 hard-gate가 더 필요 → `.agents/plugins/wiki-superpowers/agents/*.md` 가 "공통 본문 + gate overlay"를 담는 antigravity SSOT로 계속 유지. 스크립트는 이 plugin `.md`에서 `agent.json`을 생성.
- Codex `.codex/agents/*.toml``.claude/agents/*.md`에서 생성(tool명 일반화 + `sandbox_mode` 매핑 + toml 포장).
- read-only/write 권한 정책은 agent별 메타로 스크립트가 보존: Codex는 `sandbox_mode`, Antigravity는 `toolNames` allowlist.
---
## 3. 생성 스크립트 설계 (`scripts/sync-automation.py`)
단일 진입점, 서브커맨드 또는 `--target` 으로 분기. 멱등(idempotent) — 같은 SSOT → 같은 출력.
변환 책임:
1. **agents → codex toml**: frontmatter(`name`/`description`) + 본문 → `developer_instructions`, tool 표현 일반화, 권한→`sandbox_mode`.
2. **agents → antigravity agent.json**: plugin `.md`(gate 포함) → `systemPromptSections[].content`, 권한→`toolNames` allowlist, read-only는 write/run_command 제외, `hidden:true` 정책 보존.
3. **commands → codex skill**: command `.md``.agents/skills/<cmd>/SKILL.md` (frontmatter `name`+`description`(언제 발동), 본문 tool명 일반화).
4. **commands → antigravity workflow**: command `.md``.agents/workflows/<cmd>.md` (slash 호출 형식 + 인자 표기 + tool명 antigravity 어휘 치환).
5. **검증 모드(`--check`)**: 생성물이 SSOT와 동기인지 비교, drift 발견 시 비0 종료 → CI/hook 가능.
per-agent / per-command 메타(권한 등급, antigravity tool allowlist)는 명시적 매핑 테이블로 스크립트 내부 또는 sidecar로 관리(파일명에서 추정 금지).
---
## 4. 단계 분할 (user-approved 순서: 엔진 → commands → hooks)
### Phase 0 — 생성 엔진 + agents (9개)
- `scripts/sync-automation.py` 골격 작성 (agents 변환부).
- 누락 2개 agent SSOT 추가: codex용 `.claude/agents/`에는 이미 존재 → codex `.toml`·antigravity plugin `.md`(gate 포함) 작성.
- 생성: `.codex/agents/*.toml` ×9, `.agents/agents/<name>/agent.json` ×9.
- **부수 효과**: antigravity 미완성 로딩 경로 복구 + codex 수동 cat → native toml 현대화.
- 검증: `codex` 가 toml agent를 인식하는지(가능하면 실호출), `agent.json` 스키마가 실 디스크 예시와 일치하는지.
### Phase 1 — commands (13×2)
- 생성: `.agents/skills/<cmd>/SKILL.md` ×13 (codex), `.agents/workflows/<cmd>.md` ×13 (antigravity).
- command별 frontmatter `description`을 "언제 발동/비발동" 기준으로 작성(codex implicit trigger 품질 좌우).
- 검증: 대표 command(예: `/depth`, `/ingest`) 1~2개를 각 플랫폼에서 실제 호출해 동작 확인.
### Phase 2 — hooks + AGENTS.md + 문서 정리
- `.codex/hooks.json` 작성: `wiki_structure_lint.py`·`wiki_claim_gate.py``PreToolUse`(Edit|Write matcher)로 연결. hook 스크립트는 공유(top-level 위치 검토).
- `.agents/hooks.json` 보강: structure-lint/claim-gate 반영(단 content gate는 `PreToolUse`만).
- `AGENTS.md`(root) 작성: CLAUDE.md 운영규칙의 codex판(또는 얇은 포인터).
- CLAUDE.md·`.codex/agents/README.md`·antigravity README 갱신: 구식 "수동 cat" 서술 제거, native toml/workflow/skills 경로 반영, 변환 스크립트 사용법 문서화.
각 Phase는 독립 커밋 + 검증 게이트. Phase 0 완료 후 사용자 확인 → Phase 1 진행.
---
## 5. 범위 밖 (YAGNI)
- 코드 작업용 agent(ca-tmpl `ca-implementer` 등)는 본 vault 범위 밖 — 손대지 않음.
- 글로벌 설치(`~/.codex`, `~/.gemini/antigravity-cli/`) 자동화 제외 — repo 커밋 산출물만 생성.
- `rules/`·`templates/`는 이미 3 플랫폼 공유 — 변경 없음.
- Codex custom prompts(`~/.codex/prompts/`) — deprecated·글로벌 전용이라 사용 안 함.
- per-agent `model:` 지정(antigravity) — grounded 필드 없음, 미사용.
- `plugin.json` 확장 필드 — `name`만 grounded, 미사용.
---
## 6. 리스크 / 미해결
- **기계 변환의 fidelity 한계**: Claude command 본문의 Claude-idiom 서술은 완벽히 idiomatic하게 변환되지 않음. 사용자가 생성 방식을 선택하며 수용한 trade-off. 대표 command 실호출로 "동작은 동등"함을 검증해 완화.
- **`.agents/skills/` 공유 표면 충돌**: Codex skill과 Antigravity가 같은 디렉토리를 읽음. commands는 antigravity는 workflow로 분리하므로 직접 충돌은 회피하나, 기존 `wiki-workflow` skill 위치(`.agents/plugins/.../skills/` vs `.agents/skills/`)의 정합성은 Phase 1에서 점검 필요.
- **antigravity `agent.json` 스키마 미공개**: 공식 스키마가 아닌 실 디스크 관측 기반. 스키마 변동 시 변환부 수정 필요. `--check` 모드로 조기 탐지.
- **codex skill 인자 모델**: 구조적 `$ARGUMENTS`는 legacy prompts 기능이고 skill은 자연어 인자 → 일부 command의 인자 전달 UX가 Claude와 다를 수 있음. 대표 검증에서 확인.
---
## 7. 검증 접근
- Phase별 "대표 산출물 실호출" 게이트(스모크): toml agent 1개, workflow 1개, skill 1개를 실제 CLI에서 호출.
- `sync-automation.py --check` 로 SSOT↔생성물 drift 0 확인.
- antigravity `agent.json` 은 실 디스크 예시 스키마와 키 단위 대조.
- 최종 감사표(§0)의 9/13/hooks/skills 모든 칸이 세 플랫폼에서 채워졌는지 재확인.
@@ -0,0 +1,159 @@
# Spec — wiki/projects canonical 템플릿 · source_type 분리
- **작성일**: 2026-06-04
- **상태**: draft (사용자 검토 대기)
- **목표 한 줄**: `raw/project-notes/`(프로젝트 hub)와 `wiki/projects/`(canonical 실무 적용 문서)가 공유하던 단일 `project-template.md` + `source_type: project-note`를 분리해, wiki 레이어에 전용 템플릿 `wiki-project-template.md` + 전용 `source_type: project`를 부여하고, 결정론 린터가 새 템플릿 기준으로 wiki/projects 문서를 검증하게 한다.
---
## 1. 배경 / 문제
`wiki/projects/`는 다른 canonical 레이어(concepts/interview/portfolio/blog)와 달리 **전용 템플릿도 전용 source_type도 없는 유일한 "고아"**다.
| 레이어 | 템플릿 | source_type |
|---|---|---|
| concepts | `concept-template.md` | `concept` |
| interview | `interview-template.md` | `interview` |
| portfolio | `portfolio-template.md` | `portfolio` |
| blog | `blog-template.md` | `blog` |
| **projects** | ❌ raw용 `project-template.md` 공유 | ❌ `project-note` 차용 |
### 측정된 증상 (린터 baseline, 2026-06-04)
`python3 .claude/hooks/wiki_structure_lint.py --all` 기준 **482개 중 238 FAIL**. 그중 `project-note` FAIL **20건** = raw/project-notes 3 + wiki/projects hub 1 + wiki/projects nested 16.
근본 원인: wiki/projects hub·슬라이스(17개)가 `source_type: project-note`로 raw용 `project-template.md`(14개 번호 섹션: 프로젝트 개요/문제 정의/시스템 아키텍처/시퀀스/…)에 매핑되는데, 실제 문서 구조는 전혀 다름(프로젝트 컨텍스트/실제 구현/검증등급/…) → 문서마다 `MISSING_SECTION` 14개씩 발생.
→ 즉 본 작업은 정리(cosmetic)가 아니라 **17개 문서를 FAIL→PASS로 복구하는 버그 수정**이다.
### 성공 기준 (측정 가능)
1. `wiki/projects/` hub 1 + nested 16 = **17개 문서가 린터 C1 PASS**.
2.`templates/wiki-project-template.md``source_type: project`를 선언하고, 린터가 이를 자동 인식(template-driven discovery)해 `project` 문서를 이 템플릿으로 검증한다.
3. raw/project-notes(`project-note`)는 종전대로 `project-template.md`에 매핑된 채 유지(레이어 구분이 frontmatter만으로 가능).
4. `/ingest`·`/projectize`·관련 rules·3-port agent가 "wiki 추출 = wiki-project-template" 으로 일관 참조.
---
## 2. 설계 결정 (확정)
사용자 승인 완료:
- **D1**: wiki/projects 전용 `source_type: project` 신규 (concept/interview/… 와 1:1 정렬). _대안 "project-note 유지+템플릿만 분리"는 §5 신뢰도 의미 충돌로 기각._
- **D2**: 새 wiki 슬라이스 템플릿 1개만 신규 생성. hub은 별도 상세 템플릿 없이 얇은 named-hub MOC(linking-rules §12)로 유지.
- **D3 (Approach A)**: 새 템플릿 필수 섹션 = 기존 16개 슬라이스가 이미 공유하는 9개 섹션을 그대로 성문화(헤더 텍스트 동일). → C1 churn 최소. _대안 B(§15 8요소 엄격), C(하이브리드) 기각._
- **D4**: 기존 17개 문서 일괄 마이그레이션(frontmatter 한 줄 + 1개 문서 섹션 보강).
### 린터 메커니즘 (검증된 사실)
`.claude/hooks/wiki_structure_lint.py`:
- `build_template_index()` (130~152행): `templates/*-template.md`를 스캔, 각 템플릿 frontmatter의 `source_type`을 키로 `source_type→template` 매핑을 동적 구성. **source_type 어휘가 Python에 하드코딩되어 있지 않음** → 새 템플릿이 `source_type: project`를 선언하면 `project`가 자동으로 유효 타입이 됨.
- 템플릿의 "필수 섹션" = 템플릿 본문의 `##` 헤더 중 optional 표시(`선택`/`(optional)` 등)가 없는 것(139~142행). `## source_type 허용값` 등 안내 섹션은 제외.
- `header_tokens()`(88~91행): 헤더를 `( )` 제거 + `/` 분리 + 소문자화한 토큰 집합으로 정규화 후 비교. → 템플릿과 문서의 헤더 텍스트가 동일하면 매칭.
- `classify()`(52~62행): C1/C3 검사 모드를 **경로 깊이**로 결정. `parts[0] in (raw,wiki) and len(parts) > 2 and base not in LINK_ONLY_BASENAMES``full`. 그 외 `links`(C1/C3 면제).
- `wiki/projects/ca-tmpl.md` = 3 parts → 현재 `full` → hub임에도 C1 검사 대상(현재 14 FAIL의 원인).
---
## 3. 변경 명세 (파일 단위)
### ① 신규 템플릿 `templates/wiki-project-template.md`
frontmatter (필수 키 = 기존 슬라이스가 이미 가진 7개):
```yaml
---
title:
source_type: project
status: draft
confidence: unknown
tags: []
related_projects: []
last_reviewed:
---
```
`> Layer: wiki/projects/ — canonical 실무 적용 문서. 일반 개념은 wiki/concepts/, raw hub은 raw/project-notes/(project-template).`
필수 `##` 섹션 (9개, 헤더 텍스트는 기존 슬라이스와 **byte 동일**하게):
1. `## 프로젝트 컨텍스트`
2. `## 실제 구현 내용 (\`actually-implemented\`)`
3. `## 로컬/dev 검증 (\`locally-verified\`)`
4. `## 운영 검증 (\`prod-verified\`)`
5. `## 문서/계획만 존재 (\`documented-only\` / \`planned\`)`
6. `## 면접에서 말할 수 있는 범위`
7. `## 과장 금지 지점`
8. `## 관련 개념`
9. `## Sources`
각 섹션에 작성 가이드 주석 포함. §15 "검토한 선택지/결정 이유"는 "프로젝트 컨텍스트"·"실제 구현 내용"에 접어 안내(별도 필수 섹션 아님 — Approach A).
> **검증 위험 V1**: 헤더 텍스트가 기존 문서와 1글자라도 다르면(예: 백틱·띄어쓰기) `header_tokens` 불일치로 16개가 FAIL. → 구현 시 기존 슬라이스 헤더를 복사해 작성하고, 직후 린터 `--all`로 16개 PASS 회귀 확인(성공 기준 1).
### ② 린터 `classify()` named-hub 면제 (유일한 코드 변경)
`wiki/projects/<slug>.md`에 형제 디렉터리 `wiki/projects/<slug>/`가 존재하면 named-hub(folder-note, linking-rules §12)로 보고 `links` 모드(C1/C3 면제)로 분류.
- 구현: `classify()`가 root 경로 접근이 필요하므로, 형제 폴더 존재 검사를 `classify`(또는 호출부 `iter_docs`/`lint_file`)에 추가. `wiki/<category>/<slug>.md` + sibling `wiki/<category>/<slug>/` 일반 패턴으로 구현하되, 현재 영향 대상은 wiki/projects hub뿐.
- 효과: hub의 현재 14 FAIL 동시 복구. 마이그레이션 후에도 hub이 9개 슬라이스 섹션을 강요받지 않음.
> **검증 위험 V2**: 일반화가 의도치 않은 문서를 면제할 수 있음. → 구현 후 린터 `--all` FAIL 카운트가 **238 → (238 17) = 221 근처로 감소**하고 *새로운* 신규 PASS→FAIL 역전이 0건인지 확인. raw/project-notes 3개는 sibling 폴더 없음 → 영향 없어야 함(회귀 확인).
### ③ source_type 어휘 등록 (SSOT 문서)
- `CLAUDE.md` §4: source_type 허용 어휘에 `project` 추가 + 설명("wiki/projects canonical 실무 적용 문서").
- `CLAUDE.md` §5: 신뢰도 표에 `project` 행 추가 = "wiki/projects canonical, 검증된 실무 사실(`verified` 지향)". `project-note`는 "raw 프로젝트 hub / 포트폴리오 증거 후보"로 의미 유지.
- `CLAUDE.md` §2: 템플릿 목록에 `[[templates/wiki-project-template]]` 추가 + `project-template` 설명을 "`raw/project-notes/` 프로젝트 hub 전용"으로 명확화.
- `rules/tag-taxonomy.md:22`: L1 Type 어휘에 `project` 추가. (44행 project-note 행은 raw 의미로 유지, 필요 시 project 행 보강.)
- `rules/naming-conventions.md`: §2.11(wiki/projects nested) 근처에 "source_type: project" 한 줄 명시(경로 규칙 자체는 불변).
### ④ 기존 문서 마이그레이션 (17개)
- nested 슬라이스 16개(`wiki/projects/ca-tmpl/*.md`): frontmatter `source_type: project-note → project`.
- hub `wiki/projects/ca-tmpl.md`: `source_type: project-note → project`.
- "문서/계획만 존재" 섹션 없는 1개 문서(린터 baseline상 15/16만 보유)에 해당 섹션 추가.
- 편집은 **Write/Edit 도구로만**(hook `wiki_claim_gate.py`가 shell 직접 쓰기 차단). 17개 frontmatter 변경은 파일별 Edit.
### ⑤ 커맨드 · 룰 · agent 참조 정합
"wiki 추출용" 참조만 교정(raw hub 참조는 유지):
- `.claude/commands/ingest.md` 4단계: 프로젝트(`wiki/projects/`) → `wiki-project-template.md`로 교정.
- `.claude/commands/projectize.md:34`: `project-template.md``wiki-project-template.md`.
- `rules/linking-rules.md:224`(wiki/projects nested): `project-template.md``wiki-project-template.md`. **190행(raw/project-notes)은 유지.**
- agent 본문(3-port): `wiki-research-lane`의 "wiki-project 시 project-template.md"(47행) → `wiki-project-template.md`. SSOT 3곳 동기 편집:
- `.claude/agents/wiki-research-lane.md`
- `.codex/agents/wiki-research-lane.md`
- `.agents/plugins/wiki-superpowers/agents/wiki-research-lane.md`
- 이후 `python3 scripts/sync_automation.py``.codex/*.toml`·`.agents/agents/*/agent.json` 재생성.
---
## 4. 범위 밖 (명시적 제외)
- raw/project-notes 3개의 현재 FAIL(아키텍처 다이어그램 미보유 등) — raw 콘텐츠가 raw 템플릿을 안 따르는 별개 이슈. 본 작업은 wiki/raw **템플릿 혼동** 해소에 한정.
- 나머지 FAIL 221건(official-doc 92, branch-note 75 등) — 전부 범위 밖.
- `project-template.md`(raw hub) 자체의 섹션 구조 개편 — 불변.
- portfolio/blog/interview 파생 파이프라인 — 불변.
---
## 5. 검증 계획 (완료 정의)
1. 린터 `--all` 실행 → wiki/projects hub + nested 16 = **17개 모두 PASS**(MISSING_SECTION 0).
2. 린터 `--all` 총 FAIL이 238 → ~221로 감소, raw/project-notes 3개 상태 불변(회귀 0).
3. `templates/` 신규 템플릿 자체도 린터/그래프에서 깨진 링크 0.
4. `scripts/sync_automation.py` 재생성 후 `.agents/agents/*/agent.json` diff가 wiki-research-lane 참조 변경만 반영.
5. `git grep "project-template"` 결과에 "wiki 추출" 의미 잔존 참조 0(전부 raw hub 의미만 남음).
---
## 6. 변경 파일 요약
| # | 파일 | 변경 |
|---|---|---|
| ① | `templates/wiki-project-template.md` | 신규 |
| ② | `.claude/hooks/wiki_structure_lint.py` | `classify()` named-hub 면제 (유일 코드 변경) |
| ③ | `CLAUDE.md` §2/§4/§5 | `project` 어휘·신뢰도·템플릿 목록 |
| ③ | `rules/tag-taxonomy.md`, `rules/naming-conventions.md` | `project` 어휘 등록 |
| ④ | `wiki/projects/ca-tmpl.md` + `wiki/projects/ca-tmpl/*.md`(16) | `source_type` 변경 + 1개 섹션 보강 |
| ⑤ | `.claude/commands/ingest.md`, `projectize.md` | wiki 추출 템플릿 참조 교정 |
| ⑤ | `rules/linking-rules.md:224` | nested 참조 교정(190 유지) |
| ⑤ | `wiki-research-lane.md` ×3 port + `scripts/sync_automation.py` 재생성 | agent 참조 교정 |
@@ -0,0 +1,205 @@
# 투자 자료·전략 파이프라인 — 설계 (Design Spec)
> 날짜: 2026-06-05
> 상태: 설계 승인 대기 → 구현 계획(writing-plans)
> 작성 맥락: 사용자가 개발 프로젝트와 **무관한** 개인 투자/자산관리 문서를 이 wiki에서 관리하고 싶어 한다. 현 자본 60만원을 근거 있는 규율로 운용하기 위해, 기존 `raw → wiki canonical → derived` 파이프라인을 **별도 폴더·별도 명령**으로 복제한다. 자동 실행은 없고 사용자가 명령을 칠 때마다 깊게 조사한다. "넓게(거시) 조사 / 좁게(소액 현실 종목) 실행 / 매매 원장 + 규칙 강제"가 확정 요구사항.
---
## 0. 가장 중요한 고지 (Non-negotiable Disclaimer)
이 시스템은 **"AI 자산관리사"가 아니다.** 진짜 PB(프라이빗뱅킹)와 비교하면:
| 층 | 진짜 PB | 이 시스템 | 수준 |
|---|---|---|---|
| ① 프로세스·규율 | 리스크 프로파일링→자산배분→문서화→리밸런싱 | **동일 구조 재현** | 70~80% |
| ② 리서치 | 전담 애널리스트 + 실시간 독점 데이터 | 공개 웹 조사 + 출처 보존 (실시간 ❌, 환각 위험) | 30~40% |
| ③ 실행·수탁 | 주문 체결 + 자산 보관 | **0% (본인이 증권사에서)** | 0% |
| ④ 세금·계좌구조 | 절세 설계·실행 | 지식·체크만 (인프라 ❌) | 10~20% |
| ⑤ 행동·책임 | 코칭 + **법적 fiduciary 책임** | 가드레일은 됨 / **법적 책임 0** | 40% / 0% |
**종합: 진짜 PB의 35~45% 수준. 단 개인이 따라 할 수 있는 프로세스·규율 층만 보면 70%+.**
정확한 성격은 **"규율을 강제하는 투자 의사결정 저널 + 리서치 보조"**다. Claude는 면허 있는 투자자문이 아니며, 환각으로 틀릴 수 있고, 손실에 책임지지 않는다. 모든 수치는 **조사 시점 기준**이며 사용자가 출처 링크로 교차검증해야 한다. 이 고지는 `wiki/invest-strategy/` 최상단에 고정된다.
---
## 1. 문제 / Problem
- 사용자는 투자를 "전혀 모르는" 출발점에서 시작한다. 따라서 시스템은 (a) 거시 자금 흐름을 매일 조사해 쌓고, (b) 그로부터 검증된 지식·전략을 canonical로 정제하고, (c) 그 위에서 금액·목표·규칙이 있는 투자 계획을 세우고, (d) 실제 매매를 기록하며 규칙 위반을 기계적으로 체크해야 한다.
- 기존 dev wiki와 **섞이면 안 된다** (사용자 명시 요구) → 별도 `raw/invest-*`, `wiki/invest-*` 트리.
- "근거 없는 단정 금지"라는 저장소 제1원칙이 투자 판단에도 그대로 적용되어야 한다. 감(感)이 아니라 출처로 떠받친 규율이 목표.
### 성공 기준 (측정 가능)
- `/invest-daily` 1회로 출처 링크가 달린 거시 스냅샷 raw 노트가 생성된다 (수치마다 출처 + 조사시점).
- `/invest-decide`가 매매를 기록하는 동시에 `wiki/invest-strategy/`의 고정 규칙과 대조해 위반 시 빨간 플래그를 낸다.
- canonical(`wiki/invest-strategy/`)의 모든 규칙이 `raw/invest-research/` 증거 문서로 추적된다 (근거 없는 규칙 0, 또는 `UNSUPPORTED_DECISION` 명시 라벨).
- 외부 산출물(블로그 등) 파생은 기존 게이트(status ≥ reviewed) 그대로 적용.
### 비목표 / Non-goals (YAGNI · 못 하는 것)
- 자동 스케줄 실행(cron/loop) — **범위 밖** (사용자 결정: 수동 트리거).
- 실시간 시세 피드 / 자동 주문 체결 / 자산 보관 — 구조적으로 불가.
- 세금 신고·최적화 실행 인프라 — 지식·체크까지만.
- 외부 공개를 전제한 derived 층 — 1차 범위 밖 (나중에 `/blogify` 재활용 가능).
---
## 2. 아키텍처 — 폴더 분류 (raw + wiki)
```text
raw/ (증거 — 가공 전, 출처 보존)
├── invest-daily/ 매일의 "돈의 흐름" 거시 조사 (날짜별) [자료조사]
├── invest-research/ 특정 분야/자산 심층 조사 (verbatim 인용 보존) [자료조사·심화]
└── invest-ledger/ 실제 매수/매도 기록 (종목·금액·날짜·근거·규칙체크) [매매원장]
wiki/ (canonical — 검증된 판단 근거)
├── invest-concepts/ 검증된 지식 (ETF·금리·환율·분산 메커니즘 등) [넓은 조사 산출]
├── invest-strategy/ 내 전략 규칙 (포지션·손익·행동·절세 + 고지) [투자전략]
├── invest-plan/ 현재 활성 투자 계획 (금액·목표·배분·워치리스트) [계획·분야·금액·목표]
└── invest/invest-hub.md named hub (cluster 루트, index.md 금지)
```
요청 6항목 매핑: ①자료조사→`invest-daily`+`invest-research` / ②문서관리→파이프라인+lint / ③투자계획→`invest-plan` / ④투자분야→`invest-concepts`+plan 워치리스트 / ⑤투자전략→`invest-strategy` / ⑥금액·목표→`invest-plan` 고정 파라미터. 매매원장+규칙강제→`invest-ledger`+`invest-strategy` 규칙.
> `invest-ledger`를 raw에 두는 이유: 매매는 "사실 기록 = 증거"이므로 raw가 맞다. canonical은 *판단 근거*, ledger는 *일어난 일*.
---
## 3. 파이프라인 & 명령 (6종 `/invest-*`)
기존 `/daily → /ingest → ...` 구조를 본뜬다.
```text
[조사] /invest-daily → raw/invest-daily/YYYY-MM-DD.md
/invest-research <주제> → raw/invest-research/<slug>.md
│ (deep-research 하베스트: 웹 fan-out → fetch → 교차검증 → 출처발췌 보존)
[변환] /invest-ingest → wiki/invest-concepts/ (+ 규칙 발견 시 invest-strategy/)
[계획] /invest-plan → wiki/invest-plan/active-plan.md
[결정] /invest-decide <매수/매도> → raw/invest-ledger/ + ⚠️규칙 강제 체크
[리뷰] /invest-review → 포지션 vs 목표 vs 규칙 점검, 리밸런싱·stale 플래그
└────────── (루프백: 리뷰 결과가 다음 조사·계획에 반영) ──────────┘
```
| 명령 | 입력 | 출력 | 핵심 가드 |
|---|---|---|---|
| `/invest-daily` | (오늘) | `raw/invest-daily/` 거시 스냅샷(금리·환율·원자재·지수·코인) + "무엇이 바뀌었나" | 수치마다 **출처 링크 + 조사시점** |
| `/invest-research <주제>` | 분야/자산명 | `raw/invest-research/` 심층 + verbatim 인용 | official-doc vs 블로그 구분 |
| `/invest-ingest` | `raw/invest-*` | `wiki/invest-concepts/`(+`invest-strategy/`) | 검증된 것만, 환각 금지 |
| `/invest-plan` | strategy + 최근 daily | `wiki/invest-plan/active-plan.md` | 모든 항목 canonical 근거 링크 |
| `/invest-decide` | 매매 의도 + 근거 링크 | `raw/invest-ledger/` 기록 + 규칙 체크 리포트 | **규칙 위반 시 경고·차단**, 근거 없으면 기록 거부 |
| `/invest-review` | ledger + plan | 점검 리포트 | 패닉셀·목표이탈·stale 플래그 |
설계 포인트:
- **`/invest-decide`가 규칙 강제의 심장.** 매매 기록 시 `invest-strategy` 고정 규칙(포지션 비중·손익 라벨·패닉셀 쿨다운·절세계좌 조건)을 자동 대조.
- **`/invest-daily`·`/invest-research`는 기존 `deep-research` 스킬을 호출** — "근거하는 내용 기반" 요구를 구조적으로 보장.
---
## 4. 전략·규칙 모델 (`wiki/invest-strategy/`) — **출처 검증 완료**
> 2026-06-05 병렬 리서치로 모든 load-bearing 주장을 권위 출처에 대조함. 판정(KEEP/CORRECT/REJECT)과 출처는 §7. 아래는 검증 후 확정본.
**① 포지션 크기 (자본 구간별)** — `[KEEP]`
| 자본 구간 | 기본 전략 | 근거 |
|---|---|---|
| ~200만 이하 | **광범위 ETF 1~2개로 집중** (개별주 분산 ❌) | 광범위 ETF 1개 = 수백~수천 종목 분산. 소액 개별주 분산은 비효율 [SPIVA, Statman 1987] |
| 200만~1,000만 | ETF 코어 + 위성 1~2 자산군 | 분산효과가 비용 초과 시작 |
| 1,000만~ | 자산군 배분(주식·채권·원자재) 본격화 | 진짜 자산배분 단계 |
> **현 60만원의 정답은 "올인"이 아니라 광범위 ETF 1~2개**(그 자체가 분산). 자본이 늘면 규칙이 자동 전환.
**② 손절/익절** — `[REJECT한 부분 있음 — 대폭 수정]`
- **코어(광범위 ETF): 손절·익절 규칙 없음. 장기보유.** `[KEEP]` (지수 드로다운은 역사적으로 회복 — 단 수년~수십년 걸린 적 있음)
- **개별 베팅(선택 시): 손절/익절은 "근거 있는 규칙"이 아니라 본인의 위험감내 재량.** 두면 반드시 `UNSUPPORTED_DECISION` 라벨 + "이건 근거가 아니라 내 재량" 한 줄. **기계적 익절(+X%)은 근거상 비권장**(승자를 일찍 잘라 복리 손상 — Kaminski-Lo 2014, Haghani 2023). 특정 숫자(15%, +20~30%)는 임의값.
**③ 행동 가드레일** — `[CORRECT — 근거 정교화]`
- **패닉셀 쿨다운**: 급락 보고 24h 내 매도 결정 시 빨간 플래그 + "이유 먼저 쓰라" 강제
- **FOMO 가드**: 단기 급등 종목 신규매수 시 경고
- **거래 빈도 상한**: 주 N회 초과 매매 플래그 [잦은 매매 = 순수익 손상, Barber-Odean 2000]
- **선(先)근거 원칙**: 근거 문서 링크 없는 매매는 `/invest-decide`가 기록 거부
- ⚠️ "최고의 날 놓치면 망한다" 류 논리는 **약함(대칭성 반론)** — 가드 근거로 사용 금지.
**④ 절세계좌 우선순위** — `[CORRECT — 조건부 규칙으로]`
- **먼저 체크**: "낼 소득세(결정세액)가 있나? 이 돈 곧 쓰나?" → 무소득/단기자금이면 연금계좌(연금저축·IRP) **비권장** (중도인출 16.5% 페널티 = 락업). 무조건 "연금 먼저" ❌.
- **숫자 (전부 2025년 시행 기준)**: ISA 연 2,000만/총 1억/비과세 일반 200만(서민 400만)·초과 9.9% 분리과세·3년 의무 / 연금저축 연 600만 세액공제(16.5% 또는 13.2%) / IRP는 연금저축 합산 900만 세액공제·총납입 1,800만.
- **2026 ISA 확대안(연 4,000만·비과세 500만)은 국회 통과 전 — 확정 숫자로 인용 금지.**
**⑤ 목표·금액** — `[KEEP]`
- 시작자본(60만), 목표 금액·기간, 월 추가납입, 최대 감내손실(MDD)을 한 줄씩 명시 → ①~④의 기준점.
- DCA(분할)/일시매수: 일시매수가 평균 ~2/3 우세지만, DCA는 하락·후회 위험을 줄이는 선택 [Vanguard]. **수익전략이 아니라 리스크/심리 전략으로 표기.**
---
## 5. 신규 템플릿 & 메타 등록
| 신규 템플릿 | 대상 | 본뜬 것 |
|---|---|---|
| `templates/invest-daily-template.md` | `raw/invest-daily/` | daily-note-template |
| `templates/invest-research-template.md` | `raw/invest-research/` | raw-source-template (verbatim) |
| `templates/invest-ledger-template.md` | `raw/invest-ledger/` | 신규 (매매 + 규칙체크 필드) |
| `templates/invest-concept-template.md` | `wiki/invest-concepts/` | concept-template |
| `templates/invest-strategy-template.md` | `wiki/invest-strategy/` | 신규 (5규칙군 + 고지) |
| `templates/invest-plan-template.md` | `wiki/invest-plan/` | 신규 (금액·목표·배분·워치리스트) |
추가 등록:
- **`source_type` 어휘 추가** (CLAUDE.md §4 + templates): `invest-daily`, `invest-research`, `invest-ledger`, `invest-concept`, `invest-strategy`, `invest-plan`.
- **신뢰도 표(CLAUDE.md §5)** 에 위 6종 취급 방식 추가.
- **named hub** `wiki/invest/invest-hub.md` ([[rules/linking-rules]] 준수, `index.md` 금지). `wiki/llm-wiki.md` MOC에 등록.
- **태그**: `rules/tag-taxonomy.md``invest`/`finance` 계층 + 하위(equity·etf·macro·tax-account 등) 추가.
- **6개 명령** `.claude/commands/invest-*.md`.
---
## 6. 기존 규율과의 정합
- **증거 우선**: `/invest-research`는 [[rules/evidence-first-research]] 따라 verbatim 인용 + self-grep.
- **링킹**: 모든 raw가 `invest-hub`로 upward link, concept↔strategy↔plan 양방향 ([[rules/linking-rules]]).
- **status 게이트**: canonical이 `reviewed`+ 여야 derived 파생 (1차엔 derived 없음).
- **3-플랫폼 sync 여부 — 결정 필요**: 기존 명령은 `scripts/sync_automation.py`로 Codex/Antigravity에 sync된다. 단 `/project`처럼 **Claude 전용 예외**로 둘 수도 있다 (§8 Open Question).
- **lint**: `/lint`에 invest 트리 검사 항목 추가 (출처 없는 수치, `UNSUPPORTED_DECISION` 미라벨 규칙, 고지 누락).
---
## 7. 검증 근거 (Verified Evidence Base, 2026-06-05)
구현 시 이 출처들은 `raw/invest-research/`의 첫 증거 문서가 된다 (verbatim 인용 보존).
| 주장 | 판정 | 핵심 출처 |
|---|---|---|
| 액티브 펀드 대다수 장기 패배 (10y 84~90%, 20y 92~94%) | **KEEP** | S&P DJI **SPIVA** US YE2024 (공식) |
| 소액=광범위 ETF > 개별주 (10종목 룰 과소; ~3,500종목 보유) | **KEEP** | Statman 1987(JFQA), Evans-Archer 1968, VTI SEC 공시 |
| 잦은 매매가 순수익 손상 (최다거래 11.4% vs 시장 17.9%) | **KEEP** | Barber-Odean 2000 (Journal of Finance) |
| 광범위 지수 손절 불요·드로다운 회복(귀납적) | **KEEP** (단서: 회복 수년~수십년) | S&P500 역사 (다출처) |
| "자산배분이 수익률 결정" | **CORRECT** | 변동성 ~90%일 뿐(수준·펀드간 차이 아님), Ibbotson-Kaplan 2000 / CFA Institute |
| 행동격차(behavior gap) | **CORRECT**: 연 **1.1%p** (DALBAR 3~4% ❌) | Morningstar Mind the Gap 2024 |
| DCA vs 일시매수 | **CORRECT**: 일시 ~2/3 우세, DCA는 리스크·후회↓ | Vanguard 2023 |
| 절세계좌 "연금 먼저" | **CORRECT→조건부**: 결정세액 있어야 + 락업 16.5% | 국세청·금융위·KB (2025 기준) |
| 기계적 손절(−15%) 위험조정수익 개선 | **CORRECT(재작성)**: 지수엔 기대수익↓; 모멘텀에만 조건부 | Kaminski-Lo 2014 (J.Fin.Markets), CFA Institute 2026 |
| 기계적 익절(+20~30%) 장기수익 개선 | **REJECT** (지수투자엔 역효과; 숫자 임의값) | Haghani 2023, Dybvig 1988 |
---
## 8. Open Questions — 결정 완료 (2026-06-05)
1. **3-플랫폼 sync vs Claude 전용** → ✅ **Claude 전용.** `/project`처럼 예외. `scripts/sync_automation.py` 포팅 안 함. CLAUDE.md 3-플랫폼 서술에 예외로 표기.
2. **명령 이름** → ✅ `/invest-*` 접두 확정.
3. **invest-ledger 단위** → ✅ **단일 원장 파일** `raw/invest-ledger/ledger.md` (모든 매매를 표로 누적, 포지션·손익 전체 조망).
4. **`/invest-daily` 조사 범위** → ✅ **고정 체크리스트** (금리·환율·원자재·주요지수·코인 매일 점검) + 그날 이슈는 추가 섹션.
5. **사용자 소득 프로필** → ✅ strategy 문서에 "현재 과세소득(결정세액) 유무" 한 줄 필드. 비우면 절세계좌 규칙이 "조건 미확인"으로 보수 처리(연금계좌 권고 보류). 민감정보이므로 사용자가 직접 채움.
---
## 9. 구현 순서 (writing-plans로 상세화)
1. 폴더 + 6개 템플릿 생성, frontmatter 어휘 등록 (CLAUDE.md §4·§5).
2. `invest-hub` named hub + `wiki/llm-wiki.md` MOC 등록, 태그 taxonomy 추가.
3. `wiki/invest-strategy/` 초기본 작성 (§4 확정본 + §0 고지 + §7 근거 링크). §7 출처를 `raw/invest-research/`로 아카이브.
4. 6개 `/invest-*` 명령 작성 (sync 여부는 Open Q1 결정 후).
5. `/lint` invest 검사 항목 추가.
6. 첫 실사용: `/invest-daily` 1회 → `/invest-plan` 초안 → 검수.
@@ -0,0 +1,166 @@
# project-note 작성 파이프라인 — 설계 (Design Spec)
> 날짜: 2026-06-05
> 상태: 설계 승인 대기 → 구현 계획(writing-plans)
> 작성 맥락: branch-note 에는 `/branch`(스캐폴딩) + `/branch-spec`(깊은 채움) 2단 파이프라인이 있으나, project-note 에는 *깊은 작성* 커맨드가 없다. `wiki-doc-author`(mode=create) 가 생성만 가능. 이 갭을 메운다.
---
## 1. 문제 / Problem
- `raw/project-notes/` 는 프로젝트의 **최상위 hub** (문제정의 · 시스템 아키텍처 · 핵심 시퀀스 · 기술결정 · branch 분해). 모든 branch/error/source 가 여기로 upward link.
- 현재 project-note 를 **ca-skeleton-operational-contract.md 수준(caliber)** 으로 끌어올리는 자동화가 없다. 그 노트는 project-note 작성의 모든 시행착오가 누적된 reference 이며, 다른 프로젝트도 그 *엄격성 수준*으로 작성되어야 한다.
- 강조: **내용을 복제하라는 게 아니다.** ca-skeleton 의 §21 Contract Registry, §22 Sample-portfolio Matrix 등은 *그 프로젝트(contract-skeleton/platform)* 에 특화된 내용이다. 게이트가 강제하는 것은 *깊이·근거·분해 수준* 이고, 내용은 프로젝트마다 다르다.
### 성공 기준 (측정 가능)
- `/project <slug>` 로 빈 project-note 스캐폴딩 생성 (추측 채움 0).
- `/project-spec <slug> <목표>` 로 hub 를 채우면, 끝의 readiness 게이트가 4축(R1~R4) 모두 L2+ 일 때만 `Ready` 를 낸다.
- project-spec 산출물의 **Branch 분해표** 가 그대로 `/branch <slug>` 입력이 되고, 이어 `/branch-spec` 이 그 branch 를 깊게 채운다 (핸드오프 무손실).
---
## 2. 아키텍처 / 신규 산출물
> **플랫폼 범위: Claude Code 전용** (사용자 결정 2026-06-05). 기존 9개 agent·13개 command 는 Claude+Codex+Antigravity 3-플랫폼 패리티지만, 본 파이프라인은 Codex/Antigravity 포팅을 하지 않는다. 따라서 `scripts/sync_automation.py`(최신 커밋에서 삭제됨) 복원·포팅 artifact 생성은 **범위 밖**. CLAUDE.md 의 3-플랫폼 서술에서 이 기능만 명시적 예외로 표기한다.
| 산출물 | 역할 | 대응(parallel) |
|---|---|---|
| `.claude/commands/project.md` | 얇은 스캐폴딩 커맨드. `wiki-doc-author`(mode=create, category=project-note) 위임. 추측 채움 없음 | `branch.md` |
| `.claude/commands/project-spec.md` | 깊은 조사 오케스트레이터 (인자: slug + 프로젝트 목표 prose). 내부에 readiness 게이트 단계 포함 | `branch-spec.md` |
| `rules/project-readiness-gate.md` | 품질 4축(R1~R4) + 깊이 사다리(L0~L3) + 명명된 실패 모드 + **구조-불가지 proxy** 정의 | `branch-depth-gate.md` |
| `.claude/agents/project-readiness-auditor.md` | hub 깊이 *의미* 판정 (read-only). L0 존재 vs L1+ 메커니즘 깊이 구분 | `branch-depth-auditor.md` |
| `.claude/hooks/wiki_structure_lint.py` (확장) | **project 모드**`raw/project-notes/*.md` 를 C1 섹션명 매칭에서 면제하고 구조-불가지 proxy + C2 링크만 검사 | 기존 branch/full/links 모드 |
| `templates/project-template.md` (편집) | **Branch 분해/실행계획 섹션 추가**`/branch`·`/branch-spec` 핸드오프용 {slug \| 목표조건 \| 우선순위} 표 | — |
### 결정론 계층이 *섹션명 매칭*이 아닌 이유 (DISCOVERED 2026-06-05)
- exemplar `ca-skeleton-operational-contract.md` 를 기존 린터로 검사하면 **13건 `MISSING_SECTION` FAIL**. 그 노트는 project-template §1~14 가 아니라 *계약 특화* 자기 구조(§1 목표 … §30 아키텍처 … §33 checklist)를 쓴다.
- 즉 *내용*뿐 아니라 *섹션 구성*도 프로젝트마다 다르다. 따라서 게이트가 "project-template §1~14 섹션 존재"를 강제하면 **exemplar 자신이 탈락**한다.
- 결론: 결정론 계층은 **구조-불가지 proxy** 만 본다 — 임베디드 다이어그램(`![[...drawio` 또는 ```` ```mermaid ````) 존재, Branch 분해표 존재, frontmatter 필수 키, 링크 실재(C2). caliber(R1~R4 깊이)는 전적으로 LLM auditor 가 판정.
> **독립 `/project-readiness` 커맨드는 만들지 않는다.** 게이트는 `/project-spec` 내부 마지막 단계 (branch-spec §8 방식). `/depth` 와 달리 단독 호출 수요가 낮다.
---
## 3. `/project` — 스캐폴딩 커맨드 (얇음)
`branch.md` 와 동형:
1. **인자 검증** — slug 비면 요청. kebab-case. project-note 는 prefix 4종 규칙 비적용 (branch 전용). 슬러그는 프로젝트 이름.
2. **파일 존재 확인**`raw/project-notes/<slug>.md` 있으면 덮어쓰지 말고 경로만 안내(종료).
3. **스캐폴딩**`wiki-doc-author`(mode=create, category=project-note) 위임 또는 `templates/project-template.md` 복사. frontmatter `title`/`status_label: active`/`last_reviewed`(오늘) 치환. 본문 placeholder 보존.
4. **사용자 안내** — "이제 `/project-spec <slug> <프로젝트 목표>` 로 채우세요."
5. `wiki/log.md` 기록 안 함 (branch 와 동일 정책).
> project-note 는 cluster 의 root 이므로 Parent upward link 불요 (자기 자신이 hub). `wiki-doc-author` 가 daily-note·project-note 를 Parent 예외로 이미 처리.
---
## 4. `/project-spec` — 깊은 조사 오케스트레이터
**인자:** `<slug> <프로젝트 목표 자연어>` (목표 prose 는 구체화의 시드).
### 4.1 작업 흐름
```text
1. 전제 확인 — 노트 없으면 /project 먼저 안내(종료). §1 비면 사용자에게 목표 질문.
2. 프로젝트 ground truth 확인 (읽기전용) — 대상 repo 코드/기존 raw/관련 노트.
ca-tmpl류면 그 repo(/home/donghyeon/workspace/ca-tmpl)가 SSOT.
3. 문제정의·성공기준 구체화 — 추상 표현 거부, 측정가능 기준 도출.
★ 명확화 질문 — 정해야 하는데 근거·기본값 없는 '사용자 소유 결정'(범위/우선순위/목표)은
추측·UNSUPPORTED 라벨 대신 AskUserQuestion 으로 직접 묻는다.
4. 아키텍처 + 시퀀스 —
- Mermaid 시퀀스 자동 작성 (happy + error path, autonumber).
- .drawio 아키텍처는 자동생성 불가 → 스캐폴딩 + 'needs-diagram' 표시 →
사용자가 작성/요청 후 wiki-diagram-reviewer 로 ≥95 검수 (게이트가 확인).
5. 기술결정 대안조사 — 주요 결정마다 wiki-decision-researcher dispatch (bounded ≤6).
조사 후에도 근거 없으면 UNSUPPORTED_DECISION 라벨 + trade-off 한 줄.
6. Branch 분해표 — {branch slug(naming-conventions 준수) | 달성 목표 조건(측정가능) | 우선순위} 만.
결정 내용·메커니즘은 hub에 적지 않음 (SSOT 이중화 방지).
이 표가 /branch·/branch-spec 핸드오프.
7. 프로젝트 레벨 고정 결정 — Stack commitment / SSOT owner 등 branch 충돌 방지 결정(내용은 프로젝트별).
8. 검증등급 + 면접·외부공개 경계 (project-template §9·§10).
9. [내부 게이트] project-readiness —
python3 .claude/hooks/wiki_structure_lint.py --mode project --file <path> (1차 구조)
→ 통과 시 project-readiness-auditor dispatch (2차 의미, R1~R4).
판정 Ready(Blocking 0) / Not-ready. Not-ready면 §3~§8로 루프백.
10. 요약 보고 — 짧게: 채운 결정 N / UNSUPPORTED K / 조사 M / branch 분해 B / needs-diagram D /
readiness: Ready|Not-ready (Blocking 축 인용).
```
### 4.2 branch-spec 과의 차이 (핵심)
| 측면 | `/branch-spec` | `/project-spec` |
|---|---|---|
| 대상 | 단일 branch (결정+claim 단위) | 프로젝트 hub (root) |
| 근거 없는 결정 처리 | 자동조사 → 실패 시 `UNSUPPORTED_DECISION` 라벨 | 자동조사 + **사용자 소유 결정은 AskUserQuestion 으로 직접 질의** |
| 핸드오프 출력 | 구현 가이드(코드 착수 명세) | Branch 분해표(자식 branch 네이밍+목표조건) → `/branch` 입력 |
| 게이트 | depth(R1~R4) + coverage | project-readiness(R1~R4, 내부 단계) |
| 다이어그램 | 보통 불요 | 아키텍처 .drawio + 시퀀스 필수 |
### 4.3 규칙 (branch-spec 에서 계승)
- 추측해서 FACT 로 채우지 않는다. 자동조사 → 실패 시 `UNSUPPORTED_*` 또는 **사용자 질의**.
- `actually-implemented``src/` grep 으로만 확정. note→note 자기보고 전이 금지.
- 기존 사용자 작성 본문 보존. 채움은 빈 셀/skeleton 에만.
- 자동조사 bounded (≤6). 초과는 `deferred` 명시.
- 새 agent 만들지 않음 — `wiki-source-summarizer`/`wiki-decision-researcher`/`wiki-doc-author`/`project-readiness-auditor`(신규)만 dispatch.
- `wiki/log.md` 기록 안 함.
---
## 5. project-readiness 게이트 (v1, 사용하며 보강)
`rules/project-readiness-gate.md` 가 정의. **v1 으로 구현 후 실사용하며 부족분 보강** (over-engineering 금지).
### 5.1 4축 × 깊이 사다리
| 축 | 판정 질문 | L0(부족) | L3(충분) |
|---|---|---|---|
| **R1. 문제·성공 구체성** | 측정가능 기준이 있나, 추상 표현인가 | "잘 동작한다" | 수치/구체 시나리오 |
| **R2. 아키텍처·시퀀스 깊이** | 다이어그램 존재 + 컨퍼런스급인가, happy+error 시퀀스인가 | 다이어그램 없음 | `.drawio` ≥95 + error path 시퀀스 |
| **R3. 결정 근거성** | 기술결정이 대안+외부근거로 뒷받침되나, 맨주장인가 | 근거 0 | 대안조사 + 출처 wikilink |
| **R4. Branch 분해 실행가능성** | 각 branch 가 valid slug + 측정가능 목표조건을 갖나 | 목록 없음 | 전부 slug+측정가능 조건 |
- **Ready 조건**: 4축 모두 **L2+** (Blocking 0). 이것이 "ca-skeleton caliber" 의 조작적 정의.
- L1 이하 축은 Blocking finding → §3~§8 루프백.
### 5.2 1차 결정론 린터 (wiki_structure_lint.py project 모드 — 구조-불가지 proxy)
- `classify()``raw/project-notes/*.md` 를 새 모드 `"project"` 로 분기 (C1 섹션명 매칭 면제 — exemplar 비순응 때문, §2 DISCOVERED 참조).
- proxy 검사 (구조-불가지, 섹션명에 의존 안 함):
- `PROJECT_NO_DIAGRAM` — 임베디드 다이어그램 0개 (`![[....drawio` 임베드도, ```` ```mermaid ```` 블록도 없음). R2 proxy.
- `PROJECT_NO_BRANCH_TABLE` — Branch 분해표 부재 (heading 토큰에 `branch`/`브랜치` 포함 + 그 아래 `|---|` 표 행). R4 proxy.
- `MISSING_FRONTMATTER` — project-template frontmatter 필수 키 누락 (기존 check 재사용).
- C2 링크 검사는 그대로 (BROKEN_LINK / BROKEN_MD_LINK / DANGLING_ANCHOR).
- 통과해야 2차 auditor 로 진행. proxy 는 *존재* 만 본다 — *깊이/caliber* 는 auditor.
### 5.3 2차 의미 게이트 (project-readiness-auditor agent)
- read-only. `branch-depth-auditor` 와 동형 KPI: *위반(L0/L1)을 찾는 것이 목표*, 승인이 아님.
- "섹션이 존재한다(L0)" 와 "메커니즘·근거까지 구체화됐다(L2+)" 를 구분.
- 출력: 축별 L 등급 + file:line 근거 + Ready/Not-ready verdict.
---
## 6. 범위 밖 (YAGNI)
- **Codex/Antigravity 포팅 + sync_automation.py 복원** — 범위 밖 (Claude Code 전용, 사용자 결정).
- 독립 `/project-readiness` 커맨드 — 만들지 않음 (내부 단계로 충분).
- ca-skeleton 의 도메인 특화 섹션(Contract Registry/Sample-portfolio Matrix 등) 강제 — 안 함 (프로젝트별 내용).
- `.drawio` 자동 생성 — 불가능, 사용자/`wiki-diagram-reviewer` 협업으로 처리.
- coverage 게이트 별도 분리 — project 단계에선 readiness 1개로 시작 (보강 시 재검토).
- 결정론 proxy 의 *깊이* 판정 — 안 함 (존재만; 깊이는 auditor).
---
## 7. 구현 순서 (writing-plans 에서 상세화)
1. `rules/project-readiness-gate.md` 작성 (branch-depth-gate.md 구조 차용, proxy 정의 포함).
2. `templates/project-template.md` 에 Branch 분해/실행계획 섹션 추가 (기존 §8.1 Branches 는 *완성된* branch 등재용, 신규 섹션은 *계획* 분해표).
3. `wiki_structure_lint.py` project 모드 + proxy 검사 + `test_wiki_structure_lint.py` 테스트.
4. `.claude/agents/project-readiness-auditor.md` (Claude 전용).
5. `.claude/commands/project.md`, `.claude/commands/project-spec.md` (Claude 전용).
6. `CLAUDE.md` agent 목록 +1(Claude 전용 명시) / 커맨드 목록 +2 / 파이프라인 서술.
7. dogfooding — `/project` 로 신규 노트 스캐폴딩 → `/project-spec` → 게이트 통과 확인. 보강.
@@ -0,0 +1,175 @@
---
title: LLM Wiki 하네스 설계 감사 — deep-research 기준 33파일 × 8원칙
source_type: llm-generated
status: draft
confidence: medium
tags: [harness, claude-code, audit, automation, design]
last_reviewed: 2026-06-06
---
# 하네스 설계 감사 보고서 (read-only)
> **⚠️ 시점 스냅샷 (2026-06-06 감사 당시):** 본 보고서는 *구현 전* 상태를 기술한다. 이후 **Spec A 가 G1·G5·G6 을, Spec B 가 G3·G7 을 해소**했다. 특히 §3 G1("`--hook` 이 항상 exit 0")·G6("claim_gate 테스트 0개")는 **현재 코드에서 더 이상 사실이 아니다**(`--hook` fix-up 시 exit 2, `test_wiki_claim_gate.py` 존재). 우선순위 판단 시 [[docs/superpowers/README|하네스 경화 로드맵]]의 Spec A/B = DONE 상태를 함께 보라. 남은 미해소: **G2(Spec D, 보류)**, **G4(Spec C, 진행 예정)**.
**기준선:** Claude Code 내장 `deep-research` Workflow 스크립트 (CLI 바이너리에서 추출, 350행 — `/tmp/deep_research_script.js`).
**대상:** `.claude/` 하네스 33파일 (agents 10 · commands 22 · hooks 3 · settings 2 · skill 1 — 일부는 commands에 포함, 실제 채점 파일 33개).
**방법:** 5개 read-only 감사 에이전트를 병렬 dispatch, 동일한 8원칙 루브릭 + 고정 output-spec 적용 → 본 문서로 합성. **파일 변경 없음.**
> 메타: 이 감사 자체가 deep-research 패턴(fan-out readers → 고정 schema → synthesize)으로 수행됨. dogfooding.
---
## §0. deep-research가 "잘 설계된 하네스"인 8가지 이유 (루브릭)
| # | 원칙 | deep-research 구현 | 한 줄 정의 |
|---|---|---|---|
| **P1** | 강제된 output-spec | `agent()` 마다 JSON `schema` (required/enum/minItems), tool-layer 검증 + 모델 재시도 | 구조화 출력이 **경계에서 검증**되는가, 산문 희망인가 |
| **P2** | 결정론적 오케스트레이션 | `pipeline()`/`[barrier]`가 JS 코드, 각 barrier에 *왜 막는지* 주석 | fan-out/순서가 **코드**인가 controller-LLM 판단인가 |
| **P3** | 적대적 정족수 검증 | 3표/claim, ≥2 refute면 kill, "불확실하면 refuted=true", 기권≠통과 | 독립 회의론자 + **기계적 kill 임계값** vs 단일심사/자문 |
| **P4** | 우아한 저하 + funnel stats | 모든 early-return이 유효한 PARTIAL 리포트 + `stats` funnel | 부분결과 경로 + **깔때기 통계**를 내는가 |
| **P5** | 무삭제 보장 | `budgetDropped[]`/`dupes[]` 추적·노출, 캡이 보임 | 캡/탈락/스킵을 **명시 보고**하는가 silent인가 |
| **P6** | 증거 + grep 자가검증 | claim마다 verbatim 인용 | 원문 인용 + grep 검증 |
| **P7** | 최소권한 + 단일직무 | 작은 단일목적 에이전트 | scoped `tools:`, 하나의 일 |
| **P8** | 명명된 상수 | `MAX_FETCH`, `VOTES_PER_CLAIM` 상단 | 임계값이 한 곳에 명명 vs 산문에 흩어진 매직넘버 |
채점: `MATCH` / `PARTIAL` / `GAP` / `N-A`.
---
## §1. 33파일 × 8원칙 매트릭스
### 1-A. agents (10) — 대부분 GATE(read-only judge) + 3 WORKER + 1 ORCHESTRATOR
| File | P1 | P2 | P3 | P4 | P5 | P6 | P7 | P8 |
|---|---|---|---|---|---|---|---|---|
| branch-depth-auditor | PARTIAL | N-A | PARTIAL no quorum | GAP | N-A | MATCH | MATCH | MATCH |
| coverage-auditor | PARTIAL | PARTIAL | GAP single judge | GAP | N-A | MATCH | PARTIAL | MATCH |
| project-readiness-auditor | PARTIAL | N-A | PARTIAL no vote | GAP | N-A | MATCH | MATCH | MATCH |
| wiki-adversarial-reviewer | MATCH | N-A | **PARTIAL no N-vote/default-refute** | PARTIAL | N-A | MATCH | MATCH | PARTIAL |
| wiki-decision-researcher | PARTIAL | PARTIAL | N-A | PARTIAL | PARTIAL | MATCH | MATCH | PARTIAL |
| wiki-diagram-reviewer | **MATCH** | N-A | **MATCH** measured+HARD-STOP | GAP | MATCH | MATCH | MATCH | **MATCH** |
| wiki-doc-author | MATCH | N-A | N-A | PARTIAL | GAP | PARTIAL | MATCH | PARTIAL |
| wiki-link-verifier | MATCH | N-A | N-A | PARTIAL | MATCH | MATCH | MATCH | PARTIAL |
| wiki-research-lane | MATCH | N-A | PARTIAL | MATCH | PARTIAL | MATCH | MATCH | MATCH |
| wiki-source-summarizer | MATCH | N-A | N-A | PARTIAL | PARTIAL | MATCH | MATCH | MATCH |
### 1-B. capture commands (5) — 2 ORCHESTRATOR + 3 SCAFFOLD
| File | P1 | P2 | P3 | P4 | P5 | P6 | P7 | P8 |
|---|---|---|---|---|---|---|---|---|
| daily | N-A | N-A | N-A | PARTIAL | N-A | N-A | MATCH | N-A |
| branch | PARTIAL | N-A | GAP | PARTIAL | N-A | PARTIAL | MATCH | N-A |
| branch-spec | PARTIAL | **PARTIAL prose-advisory** | PARTIAL LLM-judge | PARTIAL | MATCH | PARTIAL | MATCH | GAP |
| project | PARTIAL | N-A | N-A | PARTIAL | N-A | N-A | MATCH | N-A |
| project-spec | PARTIAL | **PARTIAL prose loop** | PARTIAL LLM-judge | PARTIAL | MATCH | PARTIAL | MATCH | GAP |
### 1-C. transform/quality commands (7) — 2 TRANSFORM + 3 GATE + 1 QUERY + 1 MIGRATION
| File | P1 | P2 | P3 | P4 | P5 | P6 | P7 | P8 |
|---|---|---|---|---|---|---|---|---|
| ingest | GAP | N-A | N-A | GAP | PARTIAL | MATCH | MATCH | N-A |
| tag | GAP | N-A | N-A | GAP | N-A | N-A | MATCH | N-A |
| lint | PARTIAL | PARTIAL (post-hoc) | PARTIAL advisory | PARTIAL | MATCH | PARTIAL | MATCH | PARTIAL |
| query | GAP | N-A | PARTIAL | N-A | N-A | MATCH | MATCH | N-A |
| depth | PARTIAL | **MATCH** linter→LLM gate | PARTIAL single auditor | PARTIAL | N-A | N-A | MATCH | PARTIAL |
| coverage | PARTIAL | **MATCH** gate ordering | PARTIAL single auditor | PARTIAL | N-A | N-A | MATCH | PARTIAL |
| migrate-claims | PARTIAL | **MATCH** Phase0-4 interlock | PARTIAL default-UNSUPPORTED | **MATCH** | MATCH | MATCH | MATCH | PARTIAL |
### 1-D. output + invest commands (10) — 4 DERIVE + RESEARCH/LEDGER/PLAN/REVIEW
| File | P1 | P2 | P3 | P4 | P5 | P6 | P7 | P8 |
|---|---|---|---|---|---|---|---|---|
| projectize | PARTIAL | N-A | N-A | GAP | N-A | PARTIAL | MATCH | GAP |
| interviewize | PARTIAL | PARTIAL gate steps | N-A | GAP | N-A | PARTIAL | MATCH | GAP |
| blogify | MATCH | PARTIAL gate steps | N-A | GAP | N-A | PARTIAL | MATCH | GAP |
| explain | PARTIAL | N-A | N-A | GAP | N-A | PARTIAL | MATCH | N-A |
| invest-daily | PARTIAL | N-A (delegates DR) | N-A | GAP | **GAP** | PARTIAL no grep | MATCH | GAP |
| invest-decide | PARTIAL | PARTIAL gate steps | **PARTIAL soft-block** | GAP | N-A | GAP no grep | MATCH | GAP |
| invest-ingest | PARTIAL | PARTIAL | PARTIAL REJECT-excl | GAP | N-A | PARTIAL | MATCH | N-A |
| invest-plan | PARTIAL | PARTIAL | N-A | GAP | N-A | PARTIAL | MATCH | PARTIAL |
| invest-research | PARTIAL | N-A (delegates DR) | PARTIAL no quorum | GAP | N-A | MATCH self-grep | MATCH | N-A |
| invest-review | PARTIAL | PARTIAL | N-A | GAP | N-A | GAP no grep | MATCH | PARTIAL |
### 1-E. infra: hooks + settings + skill (6) — 결정론 backbone
| File | P1 | P2 | P3 | P4 | P5 | P6 | P7 | P8 |
|---|---|---|---|---|---|---|---|---|
| wiki_structure_lint.py | PARTIAL | **PARTIAL post-hoc, exit0 항상** | N-A | **MATCH** fail_by_type | PARTIAL hook[:10] | PARTIAL | MATCH | **MATCH** templates SSOT |
| wiki_claim_gate.py | **MATCH** | **MATCH** PreToolUse exit-2 | PARTIAL keyword block | PARTIAL | GAP | PARTIAL | MATCH | **GAP** inline 하드코딩 |
| test_wiki_structure_lint.py | PARTIAL | MATCH | N-A | PARTIAL | PARTIAL | PARTIAL | MATCH | PARTIAL |
| settings.json | PARTIAL | PARTIAL Post=after-fact | N-A | N-A | N-A | N-A | MATCH | N-A |
| settings.local.json | N-A | N-A | N-A | N-A | N-A | N-A | **GAP blanket Bash** | N-A |
| wiki-workflow/SKILL.md | PARTIAL | **GAP advisory dispatch** | PARTIAL | PARTIAL | N-A | MATCH | MATCH | PARTIAL re-list |
---
## §2. 강점 — deep-research 수준 이상 (건드리지 말 것)
1. **P6 증거 규율이 최강.** self-grep verbatim 검증이 `wiki-source-summarizer`·`wiki-research-lane`·`wiki-diagram-reviewer`·`/migrate-claims`·`invest-research`에서 **기계적으로** 명세됨(`grep -nF`/`sed -n`). deep-research보다 강함.
2. **P7 최소권한·단일직무가 깨끗.** 9/10 에이전트가 scoped `tools:` + "Shortcut Trap" 반-합리화 가드. (유일 예외: `settings.local.json` blanket `Bash`.)
3. **결정론 layer가 존재.** deep-research에는 없는 `wiki_structure_lint.py`(582행, 이진 PASS/FAIL) + `wiki_claim_gate.py`(exit-2 block) — 문서-쓰기 경계 게이트.
4. **두 개의 "골드 표준형" 파일이 이미 존재 → 내부 템플릿으로 승격 가능:**
- **`wiki-diagram-reviewer.md`** — P1·P3·P5·P8 동시 MATCH. 측정된 카운트 + HARD-STOP→0 + 명명 상수(≤10/≤8/≤1/≥95). 다른 judge 에이전트의 본보기.
- **`/migrate-claims.md`** — P2·P4 MATCH. Phase0-4 hard interlock(Phase2는 Phase1 미완 시 BLOCKED), default-refute(`UNSUPPORTED_DECISION`), >10파일→슬라이스, COMPLETE/PARTIAL/BLOCKED verdict. 다른 오케스트레이터의 본보기.
---
## §3. 횡단 체계적 갭 (deep-research가 더 체계적인 지점) — 영향순
### G1 — 결정론 backbone이 실제로 막지 않는다 ⭐ 가장 구체적·고위험
`wiki_structure_lint.py``--hook` 모드는 **findings가 있어도 항상 `sys.exit(0)`** (line 531) → 풍부한 린터가 **경고 프린터**일 뿐 게이트가 아님. 게다가 PostToolUse라 **사후**. 유일한 실제 차단은 `wiki_claim_gate.py`(PreToolUse + SubagentStop)인데 **5개 path-prefix만 커버**`wiki/projects`, 모든 `invest-*`, 파생 `wiki/interview|portfolio|blog`, `raw/errors`**쓰기-시점 강제 전혀 없음**. (블라스트: H / 레버리지: H)
### G2 — 오케스트레이션이 산문-자문이지 코드가 아니다
`branch-spec`·`project-spec`은 fan-out/barrier를 번호 매긴 LLM 지시("통과 시 dispatch", "8c 루프백 반복")로 표현. 비순응 controller가 단계를 건너뛰어도 **탐지 가능한 위반이 없음**. deep-research의 `[barrier]`(코드)와 대비. (단 `/depth`·`/coverage`·`/migrate-claims`은 P2 MATCH — 이미 깔끔한 linter→LLM 순서.) (블라스트: H / 레버리지: H, 단 Workflow는 Claude 전용)
### G3 — 검증에 verdict 라벨은 있으나 정족수/kill 기계장치가 없다
모든 judge가 **단일 패스**(adversarial-reviewer, depth/coverage/readiness auditor, invest-research KEEP/REJECT). N-vote·default-refute·기권≠통과 없음. **역설:** `wiki-adversarial-reviewer``INSUFFICIENT_CONTEXT`는 불확실성을 PASS 쪽으로 — deep-research(불확실→refute)의 **정반대**. `invest-decide`는 규칙 위반을 soft-flag(설계상 사용자 주권이나 P3 PARTIAL). (블라스트: M / 레버리지: H)
### G4 — funnel stats가 거의 어디에도 없다
`wiki_structure_lint.py``fail_by_type/fail_by_rule`만 유일. 어떤 judge·command도 deep-research funnel(후보→처리→탈락→확정)을 안 냄. **`/ingest`가 promotable 항목을 silent 누락했는지 알 수 없음**; `coverage-auditor`가 관심사를 몇 개 열거했는지 보이지 않음. (블라스트: M / 레버리지: M)
### G5 — P8 SSOT 분열(split-brain)
`structure_lint``templates/`에서 매핑을 도출(강함). 그러나 `claim_gate`는 섹션명·컬럼을 inline 하드코딩, `SKILL.md`는 source_type를 재나열 → **택소노미 사본 3개**, drift 위험. 임계값(90/30/14일, cap 6, ≥5 findings, ≥95)도 산문에 분산. (블라스트: M / 레버리지: M)
### G6 — 차단 훅에 테스트가 없다
유일한 exit-2 차단기 `claim_gate.py`**유닛 테스트 0개**; 비차단 린터는 잘 테스트됨. 고-블라스트 컴포넌트가 덜 테스트됨(비대칭). (블라스트: M / 레버리지: L)
### G7 — 에이전트 *출력*은 어디에서도 schema 검증되지 않는다
모든 에이전트가 산문("첫 글자 `#` + 고정 Output 블록 + STOP 체크리스트")을 반환, **희망으로 검증**. deep-research의 초능력(tool-layer JSON schema + 재시도)이 부재. 단 — 이건 Claude Code가 `Agent` 호출에 schema를 강제하는 native 수단이 없어서(현재 `Workflow``agent({schema})`만 가능) **부분적으로 플랫폼 제약**. (블라스트: M / 레버리지: M)
---
## §4. 우선순위 (레버리지 × 블라스트, 낮은 아키텍처 위험 우선)
| 순위 | 항목 | 갭 | 레버리지 | 블라스트 | 아키텍처 위험 | 3-플랫폼? |
|---|---|---|---|---|---|---|
| 1 | structure_lint `--hook` 차단화(CRITICAL/WARN 티어) + claim_gate 커버리지 확장 + SSOT 중앙화 | G1·G5 | H | H | **낮음**(기존 강화) | ✅ 공유 hook |
| 2 | judge 5종에 return-schema 강제 + adversarial-reviewer에 N-vote/default-refute/기권≠통과 | G3·G7 | H | M | 낮음 | ✅ |
| 3 | reporting-standards에 funnel-stats 계약 + ingest/coverage/depth/decision-researcher 출력에 stats 블록 | G4 | M | M | 낮음 | ✅ |
| 4 | claim_gate 유닛 테스트 + diagram-reviewer/migrate-claims를 "골드형 템플릿"으로 문서화 | G6 | M | L | 낮음 | ✅ |
| 5 | invest-daily/decide 강화: 숫자당 grep self-verify, 규칙 위반 hard-block 옵션 | G3·부분 | M | M | 낮음 | Claude 전용 |
| 6 | branch-spec/project-spec를 결정론 Workflow 스크립트로 변환 | G2 | H | H | **높음** | ❌ Claude 전용 |
> 6번이 "딥 재아키텍처"(앞서 보류). 1~4번이 *기존 아키텍처를 deep-research 메커니즘으로 경화*하는 안전한 길.
---
## §5. spec 분할 권고 (각각 자체 spec→plan 사이클)
- **Spec A — "결정론 backbone을 진짜 게이트로"** ⭐ 첫 슬라이스 후보. (순위 1)
- `wiki_structure_lint.py --hook`: CRITICAL(broken-link/missing-required-section)에 exit-2, WARN은 exit-0 유지 → 사후 경고를 사전 차단으로.
- `wiki_claim_gate.py`: path-prefix 커버리지를 `wiki/projects`·`invest-*`·파생 산출물로 확장.
- SSOT 중앙화: 섹션명·컬럼·source_type 매핑을 단일 모듈로(두 훅 + SKILL이 소비).
- `--hook` findings 잘림(`[:10]`) 시 "N more suppressed" 명시(무삭제).
- **왜 먼저:** 다른 모든 spec이 "backbone이 막는다"를 전제. 위험 최저, 블라스트 최고(비차단-린터는 사실상 잠재 결함).
- **Spec B — "judge 에이전트 schema + quorum"** (순위 2): depth/coverage/readiness/adversarial/diagram에 return-schema; adversarial-reviewer·게이트에 선택적 N-vote + default-refute + 기권≠통과. P1+P3 동시 상승.
- **Spec C — "funnel stats + 무삭제 계약"** (순위 3): reporting-standards + 핵심 명령 출력에 stats 블록.
- **Spec D (보류, 딥) — "branch-spec/project-spec 결정론 오케스트레이션"** (순위 6): Workflow 변환, Claude 전용, 최고 레버리지·최고 위험. 사용자 명시 opt-in 필요.
---
## §6. claim traceability 검사 (감사 메타)
본 감사는 5개 슬라이스 매트릭스에서 파일별 evidence(file:line)를 근거로 했고, 미적용 원칙은 `N-A(이유)`로 분리했다. 임의 보강 없이 갭은 `GAP`으로, 단정 불가는 `PARTIAL`로 표기했다. 이 보고서는 변경을 가하지 않는 read-only 산출물이며, 다음 단계는 사용자가 §5의 첫 슬라이스(Spec A)를 승인할 때 spec→plan으로 진행한다.
**상태:** draft (검토 전). 외부 파생 금지.
@@ -0,0 +1,146 @@
---
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`는 *공유 기계장치*만(정책 아님).
@@ -0,0 +1,263 @@
---
title: "Spec B — judge 출력 verdict 스키마 강제 + adversarial quorum (비-Workflow)"
source_type: llm-generated
status: draft
confidence: medium
tags: [harness, claude-code, hooks, agents, design, automation]
last_reviewed: 2026-06-06
---
# Spec B — judge verdict 스키마 강제 + adversarial quorum
> 상위 감사: [[2026-06-06-harness-audit-report]] §3 G3·G7. Spec A 다음 슬라이스.
> 확정 결정: **(1) 비-Workflow(hook+script)**, **(2) quorum은 adversarial-reviewer만 opt-in N=3**, **(3) 설계 승인 완료**.
## 1. 문제 (감사에서)
- **G7 (P1)**: judge 출력이 산문 markdown("Verdict: … | findings 표"). 경계에서 검증 안 됨. **Claude 일반 subagent 엔 tool-layer schema 강제가 없다**(Workflow `agent({schema})` 만 가능) → deep-research 방식 직접 적용 불가.
- **G3 (P3)**: 단일 judge, N-vote/default-refute/abstain≠pass 없음. `wiki-adversarial-reviewer``INSUFFICIENT_CONTEXT` 가 불확실성을 **PASS 쪽으로** — deep-research(불확실→refute)의 정반대.
## 2. 핵심 원리 / 비목표
- **무게중심을 hook/script(`.claude/hooks/`, 단일 SSOT)에 둔다.** 결정론 가치(파싱·검증·tally)는 테스트 가능. dispatch 는 controller(알려진 한계 — Spec D).
- `scripts/sync_automation.py` 부재(2026-06-06) → agent `.md` 편집은 **수기 3-플랫폼 미러** 비용. 따라서 agent 변경 최소화, 강제는 hook/script 로.
- **비목표**: tool-layer schema(Workflow) → Spec D. depth/coverage/readiness 의 quorum → 범위 밖(이미 1차 결정론 린터 보유, single-judge 유지). branch-spec/project-spec Workflow 변환 → Spec D.
## 3. 설계 결정 (확정)
### DD1 — 비-Workflow 메커니즘
스키마는 **SubagentStop 검증 훅**으로, quorum kill 은 **결정론 tally 스크립트**로 강제. dispatch 는 controller 가 하되 *검증*과 *kill 결정*은 기계적.
### DD2 — quorum 범위
`wiki-adversarial-reviewer` 만. 기본 N=1, 고위험 검증 시 N=3 opt-in 병렬 dispatch → `wiki_quorum.py` tally.
### DD3 — verdict 블록은 *추가*(replace 아님)
사람용 Verdict line/표는 유지하고, 기계 파싱용 `wiki-verdict` fenced 블록을 **병기**. 5 judge 모두.
## 4. 아키텍처
```
.claude/hooks/
wiki_rules.py ← MOD. validate_verdict_block() + tally_quorum() + 상수 추가
wiki_claim_gate.py ← MOD. subagent_stop_gate 가 wiki-verdict 마커 시 스키마 검증·차단
wiki_quorum.py ← NEW CLI. N개 verdict 블록 → per-finding 결정론 tally
test_wiki_rules.py ← MOD. validate_verdict_block + tally_quorum 테스트
test_wiki_quorum.py ← NEW. CLI 통합 테스트
.claude/agents/
branch-depth-auditor.md / coverage-auditor.md / project-readiness-auditor.md
wiki-diagram-reviewer.md ← MOD. Output 에 wiki-verdict 블록 추가(표준형)
wiki-adversarial-reviewer.md ← MOD. per-finding 블록 + default-refute + N=3 quorum 문서
```
### 4.1 `wiki-verdict` 기계 블록
**표준 judge** (depth/coverage/readiness/diagram):
````
```wiki-verdict
agent: branch-depth-auditor
verdict: ready|not-ready|blocked
blocking: N
should_fix: M
advisory: K
```
````
- `diagram-reviewer` 는 점수형이므로 매핑: `verdict: ready`(≥95) / `not-ready`(<95) / `blocked`; `blocking` = HARD-STOP 수.
**adversarial-reviewer** (per-finding):
````
```wiki-verdict
agent: wiki-adversarial-reviewer
finding: 4.1.1 action: KEEP|DOWNGRADE|REJECT
finding: 4.2.1 action: REJECT
```
````
### 4.2 `wiki_rules.py` 추가
```python
import re
VERDICT_FENCE_RE = re.compile(r"```wiki-verdict\s*\n(.*?)\n```", re.S)
VALID_VERDICT = {"ready", "not-ready", "blocked"}
VALID_ACTION = {"KEEP", "DOWNGRADE", "REJECT"}
REFUTATIONS_REQUIRED = 2 # ≥2 REJECT → kill (deep-research 기본값)
def parse_verdict_block(text):
"""본문에서 wiki-verdict fenced 블록을 찾아 dict 로 파싱. 없으면 None."""
m = VERDICT_FENCE_RE.search(text or "")
if not m:
return None
body = m.group(1)
out = {"agent": None, "kv": {}, "findings": []}
for line in body.splitlines():
line = line.strip()
if not line:
continue
fm = re.match(r"finding:\s*(\S+)\s+action:\s*(\S+)", line)
if fm:
out["findings"].append((fm.group(1), fm.group(2)))
continue
kv = re.match(r"([a-z_]+):\s*(.+)$", line)
if kv:
k, v = kv.group(1), kv.group(2).strip()
if k == "agent":
out["agent"] = v
else:
out["kv"][k] = v
return out
def validate_verdict_block(text):
"""(parsed, errors). parsed is None → 마커 없음(=judge 아님, caller 통과).
errors 비어있지 않으면 스키마 위반 → SubagentStop 이 차단."""
parsed = parse_verdict_block(text)
if parsed is None:
return None, []
errors = []
if not parsed["agent"]:
errors.append("wiki-verdict 블록에 `agent:` 누락")
if parsed["agent"] == "wiki-adversarial-reviewer":
if not parsed["findings"]:
errors.append("adversarial verdict 블록에 `finding: <id> action: <act>` 행 ≥1 필요")
for fid, act in parsed["findings"]:
if act not in VALID_ACTION:
errors.append(f"finding {fid}: action '{act}' 비허용(KEEP|DOWNGRADE|REJECT)")
else:
v = parsed["kv"].get("verdict")
if v not in VALID_VERDICT:
errors.append(f"verdict '{v}' 비허용(ready|not-ready|blocked)")
try:
blocking = int(parsed["kv"].get("blocking", ""))
int(parsed["kv"].get("should_fix", ""))
int(parsed["kv"].get("advisory", ""))
except ValueError:
errors.append("blocking/should_fix/advisory 는 정수여야 함")
blocking = None
# 정합성: verdict↔blocking
if blocking is not None and v == "ready" and blocking != 0:
errors.append("verdict=ready 인데 blocking≠0 (모순)")
if blocking is not None and v == "not-ready" and blocking < 1:
errors.append("verdict=not-ready 인데 blocking<1 (모순)")
return parsed, errors
def tally_quorum(block_texts, refutations_required=REFUTATIONS_REQUIRED):
"""N개 adversarial verdict 블록 → per-finding 결정론 판정.
refute = DOWNGRADE 또는 REJECT (원 severity 에 대한 반박).
default-refute: 어떤 pass 가 그 finding 을 *누락*하거나 블록이 malformed →
그 pass 는 해당 finding 에 대해 abstain(=non-KEEP) 로 집계.
결정:
reject ≥ refutations_required → KILL
(reject+downgrade) ≥ refutations_required → DOWNGRADE
keep ≥ refutations_required → KEEP
그 외(정족수 미달) → UNVERIFIED (통과 금지)
"""
n = len(block_texts)
per = {} # fid -> Counter-like
parsed_all = [parse_verdict_block(t) for t in block_texts]
all_fids = set()
for p in parsed_all:
if p:
for fid, _ in p["findings"]:
all_fids.add(fid)
for fid in all_fids:
keep = downgrade = reject = abstain = 0
for p in parsed_all:
act = None
if p:
for f, a in p["findings"]:
if f == fid:
act = a
break
if act == "KEEP":
keep += 1
elif act == "DOWNGRADE":
downgrade += 1
elif act == "REJECT":
reject += 1
else:
abstain += 1 # default-refute: 누락/malformed = non-KEEP
if reject >= refutations_required:
decision = "KILL"
elif (reject + downgrade) >= refutations_required:
decision = "DOWNGRADE"
elif keep >= refutations_required:
decision = "KEEP"
else:
decision = "UNVERIFIED"
per[fid] = {"keep": keep, "downgrade": downgrade, "reject": reject,
"abstain": abstain, "n": n, "decision": decision}
return per
```
### 4.3 SubagentStop 강제 (`wiki_claim_gate.subagent_stop_gate` 확장)
기존 `subagent_stop_gate` 의 COMPLETE 키워드 검사 *뒤에* 추가:
```python
# judge 출력에 wiki-verdict 마커가 있으면 스키마 검증(없으면 judge 아님 → 통과).
parsed, verr = wiki_rules.validate_verdict_block(message)
if parsed is not None and verr and not event.get("stop_hook_active"):
emit_block("judge verdict 블록 스키마 오류:\n- " + "\n- ".join(verr))
```
- 마커 없는 일반 subagent 는 영향 없음(parsed is None → skip). settings.json 무변경(SubagentStop 이미 배선).
### 4.4 `wiki_quorum.py` (신규 CLI)
```
사용:
python3 wiki_quorum.py vote1.md vote2.md vote3.md # 파일 N개
cat votes.md | python3 wiki_quorum.py --stdin # --- 구분 멀티블록
출력: per-finding 표(finding | keep-down-reject-abstain | decision) + 요약
exit: 1 if any KILL or UNVERIFIED, else 0 (controller 가 신호로 사용)
```
내부는 `wiki_rules.tally_quorum` 호출 + 표 출력. dispatch 는 controller 가 N=3 병렬로 `wiki-adversarial-reviewer` 를 띄운 뒤 각 출력을 본 CLI 에 투입.
### 4.5 agent `.md` 편집 (수기 3-플랫폼 미러)
| agent | 변경 | 미러 대상 |
|---|---|---|
| branch-depth-auditor | Output 에 표준 wiki-verdict 블록 추가 | `.agents/plugins/wiki-superpowers/agents/` + `.codex/agents/*.toml` |
| coverage-auditor | 동상 | 동상 |
| wiki-diagram-reviewer | 점수→verdict 매핑 블록 추가 | 동상 |
| project-readiness-auditor | 표준 블록 추가 | **Claude 전용 — 미러 없음** |
| wiki-adversarial-reviewer | per-finding 블록 + **default-refute**(불확실→REJECT 경향, INSUFFICIENT_CONTEXT 는 KEEP 아님) + N=3 quorum 흐름 문서 | 미러 대상 |
미러는 SSOT `.md` 본문을 해당 variant 파일에 **본문 복제**(frontmatter 형식만 플랫폼 규칙대로). 생성기 부재 → 수기.
## 5. 영향 분석 / 위험
| 위험 | 완화 |
|---|---|
| SubagentStop 가 비-judge subagent 를 잘못 차단 | `parsed is None`(마커 부재) 시 무조건 통과. judge 만 마커 방출 → opt-in 검증. |
| 무한 재방출 루프 | 기존 `stop_hook_active` 가드 재사용. |
| agent 가 verdict 블록을 안 내면 게이트 미발동(조용한 우회) | 의도된 설계(Spec B는 *블록이 있을 때* 검증). 모든 judge `.md` 에 블록을 필수 Output 으로 명시 → 누락은 별도 lint 후보(Spec C). |
| 3-플랫폼 미러 drift | 미러 대상 4개만(readiness 제외). 변경이 작음(블록 추가). 미러 체크리스트를 plan 에 포함. |
| quorum tally 가 DOWNGRADE 를 과도 집계 | refute=REJECT+DOWNGRADE 는 *severity 반박*의 보수적 정의. KILL 은 REJECT≥2 로만(엄격). |
## 6. 수용 기준 (검증 가능)
1. `validate_verdict_block`: 표준 블록 정상 → errors=[]. `verdict=ready blocking=2` → "모순" 오류. `verdict=foo` → 비허용 오류. 마커 부재 → (None, []).
2. `validate_verdict_block`: adversarial 블록 `finding: x action: NOPE` → 비허용 오류. finding 0개 → 오류.
3. `tally_quorum`: 3블록 중 finding A가 REJECT×2 → KILL. KEEP×3 → KEEP. REJECT×1+DOWNGRADE×1 → DOWNGRADE. 한 블록만 KEEP, 나머지 누락 → UNVERIFIED(default-refute).
4. `subagent_stop_gate`: `wiki-verdict` 마커 + 스키마 오류 메시지 → exit 2. 마커 없는 메시지 → exit 0. 정상 블록 → exit 0.
5. `wiki_quorum.py`: 3 파일 입력 → per-finding 표 + 요약. KILL 포함 시 exit 1, 전부 KEEP 시 exit 0.
6. `python3 .claude/hooks/test_wiki_rules.py` + `test_wiki_quorum.py` + 기존 3 스위트 전부 green.
7. 5 judge `.md` 각 Output 에 `wiki-verdict` 블록 예시 존재(grep 확인). 미러 4개 variant 에 동일 블록 반영(grep 확인).
## 7. 구현 순서 (writing-plans 에서 단계화)
1. `wiki_rules.py` — `parse_verdict_block`/`validate_verdict_block`/`tally_quorum` + 상수. (TDD: test 먼저)
2. `test_wiki_rules.py` — §6.1~6.3 케이스.
3. `wiki_quorum.py` + `test_wiki_quorum.py` — §6.5.
4. `wiki_claim_gate.subagent_stop_gate` 확장 + 회귀 테스트(마커/오류/정상).
5. `wiki-adversarial-reviewer.md` — per-finding 블록 + default-refute + quorum 문서.
6. 표준 4 judge `.md` — wiki-verdict 블록 추가.
7. 수기 3-플랫폼 미러(4개) + grep 검증.
8. 전체 스위트 + §6 수용 기준 스모크.
## 8. 메모
- Spec A 와 동일 철학: 기존 메커니즘 강화, 저위험, hook/script SSOT 우선. Workflow/quorum-dispatch 자동화는 Spec D.
- `wiki_rules.py` 가 Spec A 에 이어 공유 SSOT 로 계속 성장 — verdict 스키마/tally 는 *기계장치*이지 정책 아님(정책은 각 agent `.md` + quorum 임계값 상수).
@@ -0,0 +1,168 @@
---
title: "Spec C — funnel stats + no-silent-truncation 계약 (G4)"
source_type: llm-generated
status: draft
confidence: medium
tags: [harness, claude-code, hooks, agents, design, automation]
last_reviewed: 2026-06-06
---
# Spec C — funnel stats + no-silent-truncation
> 상위 감사: [[2026-06-06-harness-audit-report]] §3 G4. Spec B 다음 슬라이스. 확정 결정: **Lean(계약+경량 funnel)**, **승인 완료**.
## 1. 문제 (감사에서)
- **G4 (P4)**: funnel stats 가 거의 어디에도 없다. `wiki_structure_lint.py``fail_by_type/fail_by_rule` 만 유일. 어떤 judge·command 도 deep-research funnel(후보→처리→탈락→확정)을 안 냄. **`/ingest` 가 promotable 항목을 silent 누락했는지 알 수 없고**, `coverage-auditor` 가 관심사를 몇 개 열거했는지 보이지 않는다.
## 2. 핵심 원리 / 비목표
- B 와 동일 철학: 결정론 코어(파싱·검증)는 `wiki_rules.py`(단일 SSOT), enforcement 는 SubagentStop hook(Claude·Codex)·G3 in-prompt(Antigravity). dispatch/계수는 controller/agent(self-report).
- self-report stats 는 완벽한 진실이 아니지만, **funnel 균형 검증**(found = processed + dropped)과 **dropped_reason 필수**가 "조용한 누락"을 *보이게* 만든다 — 그게 G4 의 목표(관측가능성), 정확한 회계가 아니다.
- **비목표**: 모든 agent/command 에 stats 강제(YAGNI) → 핵심 4 agent + /ingest 만. branch-spec/project-spec Workflow → Spec D.
## 3. 설계 결정 (확정)
### DD1 — Lean 범위
- 결정론 코어: `wiki_rules.validate_stats_block` + `claim_gate` SubagentStop 검증(B 메커니즘 재사용).
- 계약: `rules/reporting-standards.md` 에 no-silent-truncation 절.
- funnel 방출: 핵심 4 agent(`coverage-auditor`·`branch-depth-auditor`·`wiki-decision-researcher`·`wiki-research-lane`) + `/ingest`(command, advisory).
### DD2 — funnel 균형이 teeth
`found == processed + dropped` 가 깨지면 오류. `dropped>0` 인데 `dropped_reason` 비면 오류. 이 두 검사가 "조용한 truncation" 을 차단한다.
## 4. 아키텍처
```
.claude/hooks/
wiki_rules.py ← MOD. validate_stats_block() + 상수
wiki_claim_gate.py ← MOD. subagent_stop_gate 가 wiki-stats 마커 시 검증
test_wiki_rules.py ← MOD. TestStatsBlock
rules/reporting-standards.md ← MOD. "No silent truncation" 계약 절
.claude/agents/{coverage-auditor,branch-depth-auditor,wiki-decision-researcher,wiki-research-lane}.md
← MOD. Output 에 ## Stats wiki-stats 블록
.claude/commands/ingest.md ← MOD. funnel(found→promoted→skipped+이유) 출력 계약
```
3-플랫폼 미러: 4 agent 의 Antigravity(`.agents/plugins/.../*.md` G3 통합) + Codex(`.codex/agents/*.md`+`*.toml`). (`wiki-decision-researcher`·`wiki-research-lane` 도 shared 9 agent 에 포함 → 미러 대상.)
### 4.1 `wiki-stats` 기계 블록
````
```wiki-stats
agent: coverage-auditor
found: 12
processed: 10
dropped: 2
dropped_reason: 2 out-of-scope (governing §4)
```
````
- `found` = 후보로 식별한 총 항목(관심사/claim/후보/파일).
- `processed` = 실제 **판정한** 수 — *결과 무관*. covered·delegated·**missing**·verified·promoted 등 어떤 판정이든 "다뤘으면" processed. (coverage 의 missing 은 drop 이 아니라 *판정된 gap* 이므로 processed 에 포함된다.)
- `dropped` = **판정하지 않고** 의도적으로 제외한 수(범위 밖/bound 초과). `dropped>0` → `dropped_reason` 필수.
- 불변식: `found = processed + dropped`. (모든 식별 항목은 *판정됨* 이거나 *제외됨* — 제3의 침묵 누락이 없다.)
### 4.2 `wiki_rules.py` 추가
```python
STATS_FENCE_RE = re.compile(r"```wiki-stats\s*\n(.*?)\n```", re.S)
def parse_stats_block(text):
m = STATS_FENCE_RE.search(text or "")
if not m:
return None
out = {"agent": None, "kv": {}}
for line in m.group(1).splitlines():
line = line.strip()
if not line:
continue
kv = re.match(r"([a-z_]+):\s*(.+)$", line)
if kv:
k, v = kv.group(1), kv.group(2).strip()
if k == "agent":
out["agent"] = v
else:
out["kv"][k] = v
return out
def validate_stats_block(text):
"""(parsed, errors). parsed None → 마커 없음(통과). errors → SubagentStop 차단."""
parsed = parse_stats_block(text)
if parsed is None:
return None, []
errors = []
if not parsed["agent"]:
errors.append("wiki-stats 블록에 `agent:` 누락")
nums = {}
for k in ("found", "processed", "dropped"):
try:
nums[k] = int(parsed["kv"].get(k, ""))
except ValueError:
errors.append(f"wiki-stats `{k}` 는 정수여야 함 (funnel 필수 필드)")
if len(nums) == 3:
if nums["found"] != nums["processed"] + nums["dropped"]:
errors.append(
f"funnel 불균형: found({nums['found']}) ≠ processed({nums['processed']}) "
f"+ dropped({nums['dropped']}) — 조용한 누락 의심"
)
if nums["dropped"] > 0 and not parsed["kv"].get("dropped_reason", "").strip():
errors.append("dropped>0 인데 `dropped_reason` 누락 (no-silent-truncation 위반)")
return parsed, errors
```
### 4.3 SubagentStop 강제 (`wiki_claim_gate.subagent_stop_gate` 확장)
verdict 검사 *뒤에* 추가(같은 패턴):
```python
sparsed, serr = wiki_rules.validate_stats_block(message)
if sparsed is not None and serr and not event.get("stop_hook_active"):
emit_block("wiki-stats 블록 오류:\n- " + "\n- ".join(serr))
```
마커 없는 출력 영향 없음. settings.json 무변경.
### 4.4 `rules/reporting-standards.md` — No silent truncation 계약
새 절 추가:
> **No silent truncation.** 출력이 캡/슬라이스/top-N/skip 으로 coverage 를 bound 하면 **드롭한 수 + 이유**를 반드시 보고한다. funnel 은 균형해야 한다: `found = processed + dropped`. agent 출력은 `wiki-stats` 블록으로(SubagentStop 검증), command 는 `## Stats` 절로 보고한다. 침묵 누락은 "전부 다뤘다" 는 거짓 신호다.
### 4.5 agent `.md` 편집 (4, 3-플랫폼 미러)
각 Output 에 `## Stats` 의 `wiki-stats` 블록 추가. funnel 의미 매핑:
- coverage-auditor: found=governing 관심사 수, processed=covered+delegated, dropped=명시 제외(있으면 이유).
- branch-depth-auditor: found=점검한 claim/결정 수, processed=판정 완료, dropped=범위 밖(이유).
- wiki-decision-researcher: found=식별 후보 수, processed=archive 한 수, dropped=bound 초과 제외(이유).
- wiki-research-lane: found=슬라이스 파일 수, processed=정독+추출, dropped=무관/제외(이유).
Antigravity 는 G3 Output Schema 에 `{{ }}` 스타일로 통합("형식 외 응답 금지").
### 4.6 `/ingest` 출력 계약 (advisory)
출력 끝에 `## Stats` funnel: `found promotable 항목 / promoted(canonical 경로별) / skipped(항목+이유)`. daily/branch 특수처리는 추출/미추출 카운트를 명시(이미 "추출 안 한 항목 raw 보존" 규칙 있음 — 카운트만 추가).
## 5. 위험
| 위험 | 완화 |
|---|---|
| agent 가 found/dropped 를 부정확 self-report | funnel 균형 검증이 *내부 모순*은 잡음(완벽한 회계는 아님 — 관측가능성이 목표). |
| SubagentStop 가 비-stats subagent 오차단 | `parsed is None`(마커 부재) 시 통과 — opt-in. |
| 3-플랫폼 미러 drift | B 와 동일 4 agent, grep-verify; 생성기 부재로 수기. |
| dropped_reason 강제가 noise | dropped==0 이면 reason 불요 — 실제 누락 시에만. |
## 6. 수용 기준 (검증 가능)
1. `validate_stats_block`: 균형 블록 → errors=[]. `found=12 processed=10 dropped=0` → 불균형 오류. `dropped=2` + reason 없음 → 오류. 마커 부재 → (None, []).
2. `subagent_stop_gate`: wiki-stats 불균형 마커 → exit 2. 균형 마커 → exit 0. 마커 없음 → exit 0.
3. `rules/reporting-standards.md` 에 "No silent truncation" 절 존재(grep).
4. 4 agent `.md` 각 Output 에 `wiki-stats` 블록 존재 + 3-플랫폼 미러(grep parity).
5. `/ingest.md` 에 `## Stats` funnel 계약 존재(grep).
6. 기존 4 스위트 + 신규 stats 테스트 전부 green.
## 7. 구현 순서 (writing-plans)
1. `wiki_rules.py` `parse_stats_block`/`validate_stats_block` (TDD) + `test_wiki_rules` TestStatsBlock.
2. `claim_gate.subagent_stop_gate` 확장 + subprocess 회귀(균형/불균형/마커없음).
3. `rules/reporting-standards.md` no-silent-truncation 절.
4. 4 agent `.md` (Claude) ## Stats 블록.
5. `/ingest.md` funnel 계약.
6. 3-플랫폼 미러(4 agent) + grep parity.
7. 전체 스위트 + §6 스모크.
## 8. 메모
- `wiki_rules.py` 가 A(구조)·B(verdict)·C(stats) 의 공유 SSOT 로 계속 성장 — 전부 *기계장치*(스키마/검증), 정책은 agent `.md`+rules.
- self-report 한계는 인정 — Spec C 는 "조용한 누락을 보이게" 가 목표(deep-research 의 결정론 funnel 은 Workflow=Spec D 에서만 가능).
@@ -0,0 +1,168 @@
---
title: "Spec D — research-fanout Workflow (scoped, Claude 전용, opt-in)"
source_type: llm-generated
status: draft
confidence: medium
tags: [harness, claude-code, workflow, design, automation]
last_reviewed: 2026-06-06
---
# Spec D — research-fanout Workflow
> 상위 감사: [[2026-06-06-harness-audit-report]] §3 G2. 확정 결정: **scoped research-fanout Workflow**(전체 명령 변환 아님), **승인 완료**. **구현(파일 Write + Workflow 실행)은 opt-in 시.**
## 1. 문제 (감사에서) + 재설계
- **G2 (P2)**: `/branch-spec`·`/project-spec` 오케스트레이션이 산문-자문(번호 매긴 LLM 지시). controller 가 단계를 건너뛰어도 탐지 안 됨. deep-research 의 코드 barrier 와 대비.
- **재설계 (브레인스토밍 발견):** 두 명령은 **비-기계적 부분**이 본질적이다 — `project-spec §3` 의 `AskUserQuestion`(사용자 소유 결정), ca-tmpl ground-truth 정독(판단), 노트 채움(user-content 보존), 다이어그램 작성. **전체를 Workflow 로 변환하면 이 상호작용·판단이 깨진다(YAGNI 위반).**
- 따라서 **순수 fan-out 서브페이즈만** 추출: `/branch-spec §5`(결정별 bounded 자동조사 — `wiki-decision-researcher` ×N, cap 6, deferred). 이것이 deep-research 의 `pipeline(angles → research → schema)` 와 정확히 동형이다.
## 2. 핵심 원리 / 비목표
- **산문 명령은 유지** (3-플랫폼). Claude 만 *옵션으로* Workflow 를 호출해 §5 의 병렬 조사를 빠르고 schema-강제·cap-보장으로 수행. Codex/Antigravity 는 기존 순차 dispatch — **parity 안 깨짐.**
- A/B/C 메커니즘의 **진짜 Workflow 형태**: tool-layer `agent({schema})`(B 를 hope→enforce 로) · funnel `found=processed+dropped`(C) · cap+deferred(C no-silent-truncation).
- **autonomous** 조사(deep-research 처럼 user-approval 없이) — 산문 `wiki-decision-researcher`(WebSearch→승인→fetch)와의 trade-off. 둘 병존, 사용자 선택.
- **비목표**: 전체 명령 변환 · `project-spec` 변환(AskUserQuestion 본질적, v2 후보) · verify gate 의 Workflow 화(별도). branch-note 직접 편집(Workflow 는 input 만 반환, 산문 controller 가 §7 채움).
## 3. 설계 결정 (확정)
### DD1 — scoped fan-out only
Workflow 는 §5(결정별 조사 burst)만. §1~4(전제·ground-truth·결정추출)와 §6~9(라벨·채움·게이트)는 산문 controller.
### DD2 — autonomous + schema-enforced
각 결정을 `agent({schema: ALT_SCHEMA})` 로 병렬 조사. user-approval 없음(autonomous). 결과는 구조화된 비교매트릭스.
### DD3 — Claude 전용, 산문은 불변
`.claude/workflows/research-fanout.*` 신규. `/branch-spec §5` 에 "옵션: Claude 에서 research-fanout Workflow 호출" 한 줄만 추가(산문 흐름·3-플랫폼 미러 불변).
## 4. 아키텍처 — Workflow 스크립트
```js
export const meta = {
name: 'research-fanout',
description: 'Bounded parallel alternatives-research for N branch decisions (autonomous, schema-enforced)',
phases: [
{ title: 'Scope', detail: 'validate + cap decisions (max 6), split deferred' },
{ title: 'Research', detail: 'parallel autonomous web research per decision, schema-enforced' },
{ title: 'Synthesize', detail: 'comparison matrices + funnel stats + branch-note DEM input' },
],
}
const MAX_DECISIONS = 6
const ALT_SCHEMA = {
type: "object", required: ["decision", "alternatives", "recommendation", "confidence"],
properties: {
decision: { type: "string" },
alternatives: { type: "array", minItems: 2, maxItems: 5, items: {
type: "object", required: ["name", "pros", "cons", "sources"],
properties: {
name: { type: "string" },
pros: { type: "string" },
cons: { type: "string" },
sources: { type: "array", items: { type: "string" } }, // URL
quote: { type: "string" }, // verbatim 인용
sourceType: { enum: ["official-doc", "company-tech-blog", "personal-blog", "unknown"] },
},
}},
recommendation: { type: "string" },
confidence: { enum: ["high", "medium", "low"] },
unsupported: { type: "boolean" }, // 조사 후에도 근거 부족 → UNSUPPORTED_DECISION
},
}
// ── Scope: cap + deferred (no silent truncation) ──
phase("Scope")
const decisions = (args && Array.isArray(args.decisions)) ? args.decisions : []
if (!decisions.length) {
return { error: "No decisions. Pass args.decisions = [{topic, parentBranch, constraints, n}]." }
}
const capped = decisions.slice(0, MAX_DECISIONS)
const deferred = decisions.slice(MAX_DECISIONS)
log(`${decisions.length} decisions → research ${capped.length}, defer ${deferred.length}`)
const researchPrompt = (d) =>
"## Alternatives Researcher (autonomous)\n\n" +
"Branch decision: \"" + d.topic + "\"\n" +
"Parent branch: " + (d.parentBranch || "(none)") + "\n" +
"Constraints: " + (d.constraints || "(none)") + "\n\n" +
"## Task\n" +
"1. WebSearch official docs + 대기업 기술블로그 for " + (d.n || 3) + " viable alternatives.\n" +
"2. WebFetch each; extract Pros/Cons + a verbatim quote + source URL + sourceType.\n" +
"3. company-tech-blog 만으로 '공식 best practice' 승격 금지(독립 사례 2+ 또는 official 병행).\n" +
"4. 근거가 한쪽으로 명확하면 그대로 recommendation. 가짜 5:5 균형 금지.\n" +
"5. 조사 후에도 근거 부족하면 unsupported=true.\n\nStructured output only."
// ── Research: 진짜 병렬 + tool-layer schema 강제 ──
phase("Research")
const results = (await parallel(
capped.map(d => () =>
agent(researchPrompt(d), { label: "research:" + d.topic.slice(0, 30), phase: "Research", schema: ALT_SCHEMA })
.then(r => r ? { ...r, topic: d.topic } : null)
)
)).filter(Boolean)
// ── Synthesize: funnel + branch-note DEM input ──
phase("Synthesize")
const failures = capped.length - results.length
const stats = {
found: decisions.length,
processed: results.length,
dropped: deferred.length + failures,
dropped_reason: [
deferred.length ? `${deferred.length} over cap(${MAX_DECISIONS})` : null,
failures ? `${failures} research failed/skipped` : null,
].filter(Boolean).join("; ") || "none",
}
// 불변식: found = processed + dropped (decisions.length = results + deferred + failures) ✓
log(`done: ${results.length} researched, ${stats.dropped} dropped`)
return {
matrices: results,
deferred: deferred.map(d => d.topic),
unsupported: results.filter(r => r.unsupported).map(r => r.topic),
stats,
// 산문 controller 가 §7 Decision Evidence Map 채움에 쓰는 input (Workflow 는 노트 직접 편집 안 함)
branchNoteInput: results.map(r => ({
decision: r.topic, recommendation: r.recommendation, confidence: r.confidence,
sources: r.alternatives.flatMap(a => a.sources), unsupported: !!r.unsupported,
})),
}
```
### 4.1 산문 `/branch-spec` 변경 (한 줄 — Claude 전용 옵션)
§5 에 추가:
> **(옵션, Claude 전용)** 결정 수가 많거나 빠른 병렬 조사를 원하면 `research-fanout` Workflow 를 호출한다(`Workflow({name:'research-fanout', args:{decisions:[...]}})`, ultracode/opt-in 필요). 반환된 `matrices`/`branchNoteInput` 으로 §7 Decision Evidence Map 을 채우고, `stats`/`deferred` 를 §9 요약에 반영. **autonomous 조사라 user-approval 이 없으므로**, 승인-gated 가 필요하면 기존 `wiki-decision-researcher` 순차 dispatch 를 쓴다. Codex/Antigravity 는 항상 순차 dispatch.
## 5. 위험
| 위험 | 완화 |
|---|---|
| autonomous 조사가 user-approval 우회 | 산문 `wiki-decision-researcher`(승인-gated)와 병존 — 사용자가 선택. §5 에 trade-off 명시. |
| Workflow Claude 전용 → parity | 산문 명령 불변(3-플랫폼). Workflow 는 *추가* 옵션. project 파이프라인처럼 명시적 Claude 예외. |
| cap 초과 silent 절단 | `deferred` 명시 반환 + funnel `dropped_reason`. |
| schema 강제로 agent 재시도 비용 | deep-research 와 동일 — tool-layer 검증이 신뢰성↑. cap 6 으로 비용 bound. |
| Workflow 실행 opt-in 필요 | spec/plan 은 지금, 실행은 ultracode opt-in. 산문 경로는 항상 가용(non-opt-in fallback). |
## 6. 수용 기준 (검증 가능 — opt-in 실행 시)
1. `Workflow({name:'research-fanout', args:{decisions:[3개]}})``matrices` 3개 + `stats.found=3, processed+dropped=3`.
2. 8개 decisions → 6 researched + `deferred` 2개 명시 + `stats.dropped≥2 with dropped_reason`.
3. 빈 decisions → `{error}` (graceful).
4. 각 matrix 가 ALT_SCHEMA 충족(alternatives ≥2, sources 배열, recommendation/confidence).
5. `branchNoteInput` 이 산문 §7 채움에 바로 쓰일 형태(decision/recommendation/sources/unsupported).
6. 산문 `/branch-spec` 의 기존 순차 경로(비-Workflow)가 여전히 동작(Workflow 미사용 fallback).
## 7. 구현 순서 (writing-plans — 실행은 opt-in)
1. (opt-in) `.claude/workflows/research-fanout.*` 에 §4 스크립트 Write.
2. (opt-in) `Workflow({name:'research-fanout', args:{decisions:[테스트 2-3개]}})` 실행 → §6.1·6.4 검증.
3. (opt-in) 8개 decisions 로 cap/deferred 검증(§6.2).
4. `/branch-spec §5` 에 옵션 호출 한 줄 추가(§4.1) — 이건 산문 편집이라 opt-in 불요(언제든).
5. (opt-in) §6 전 항목 스모크.
> **경계:** 4번(산문 한 줄)은 비-opt-in 으로 지금 가능. 1~3·5(Workflow Write+실행)는 `Workflow` 툴 opt-in 시. plan 이 이 경계를 task 별로 명시.
## 8. 메모
- 이건 "전체 오케스트레이션 결정론화"(원래 G2 야망)가 아니라 *기계화 가능한 한 조각*만 deep-research 형태로. 나머지 산문 오케스트레이션은 interactive/judgment 라 산문이 옳다 — 그 결론 자체가 D 의 산출.
- A(구조 게이트)·B(verdict schema)·C(stats)·D(research-fanout Workflow)로 deep-research 8원칙이 하네스 전반에 이식됨 — 단 P2(코드 오케스트레이션)는 *기계화 가능한 부분에 한해*.
@@ -0,0 +1,172 @@
# 투자 분야 지식 지도 (Field Map) — 설계
- **날짜**: 2026-06-08
- **상태**: 설계 승인됨 (사용자 승인 2026-06-08)
- **상위 맥락**: `[[docs/superpowers/specs/2026-06-05-investment-wiki-pipeline-design]]` (투자 파이프라인 1차 설계)의 확장. invest 파이프라인은 Claude Code 전용(3-플랫폼 포팅 예외).
- **목적**: 사용자가 *거시 자금흐름을 읽는 "눈/시야"*를 기르도록, 분야(섹터·자산군) 간 인과·상관을 근거와 함께 쌓아가는 **지식 지도(knowledge map)** 문서 구조를 추가한다.
---
## 1. 문제 / Problem
현재 invest 파이프라인은 **캡처(/invest-daily, /invest-research)**·**전략(strategy)**·**계획(plan)**·**원장(ledger)**은 있으나, 다음을 받칠 구조가 **없다**:
- 전체를 **분야(섹터/자산군)로 쪼개 보는** 지도
- 분야 간 **인과·상관** (예: 달러↓→원자재↑, 반도체 사이클→경기, 경기국면별 로테이션)
- **같이 움직이는 종목/분야군**
- 그 흐름을 읽는 *눈의 토대*가 될 거시 지식 — `wiki/invest-concepts/`는 현재 **비어 있음**(.gitkeep만).
`invest-daily`는 "금리·환율·S&P500이 올랐다/내렸다"(자산군 5개 수준)까지만 잡고, *분야·흐름·인과·종목군*은 누적되지 않는다.
## 2. 목표 / Goals
- 분야 간 관계를 **근거(검증) vs 가설**로 구분해 쌓는 canonical 지식 지도.
- 매일 관찰(invest-daily)이 지도를 똑똑하게 만드는 **피드백 루프** = "눈 훈련".
- Obsidian 그래프가 곧 자금흐름 지도가 되도록 **노드(분야 카드) + 엣지(wikilink)** 구조.
## 3. 비목표 / Non-Goals (v1 YAGNI)
| 안 만듦 | 이유 |
|---|---|
| ③ 매수 신호 규칙 ("언제부터 모으나") | 지식 지도(②)가 먼저 쌓여야 신호가 근거를 가짐. 다음 버전 |
| 자동 거래량·자금흐름 수집(API) | v1은 **수동 관찰**만 |
| 전체 산업 섹터 전부 채우기 | 씨앗 8장만, 필요 시 1장씩 추가 |
| 새 슬래시 명령(`/invest-field` 등) | 기존 `/invest-research`+`/invest-ingest` 재사용. 마찰 생기면 그때 |
| 한국 섹터·개별 종목 카드 | 거시+미국 먼저 |
## 4. 설계 결정 (brainstorm 합의)
1. **첫 버전의 심장 = 지식 지도(②)** — 일일 추적(①)·매수 신호(③)는 이 위에 얹음.
2. **지도 단위 = 분야 카드(노드 중심)** — 각 분야 1문서, 관계는 wikilink 엣지.
3. **씨앗 = 2층(거시 자산군 + 관심 섹터 소수)**, 약 8장.
4. **운영 모델 = approach A** — 기존 파이프라인(research·ingest·daily) 재사용 + 카드 템플릿/허브/일일강화 신설. (approach B=너무 얇음, C=단일 거대문서로 wiki 규칙 위반 → 기각.)
## 5. 구성요소 / Components
### 5.1 새로 만드는 것
| # | 산출물 | 경로 |
|---|---|---|
| 1 | 분야 카드 템플릿 | `templates/invest-field-card-template.md` |
| 2 | 지도 허브 (named hub) | `wiki/invest-concepts/field-map.md` |
| 3 | 씨앗 카드 8장 | `wiki/invest-concepts/field-<이름>.md` |
| 4 | invest-daily 템플릿 강화 | `templates/invest-daily-template.md` 에 "분야 관찰" 섹션 추가 |
### 5.2 재사용
- `/invest-research` — 의심 관계를 deep-research 3표로 검증 (raw/invest-research/).
- `/invest-ingest` — 검증된 관계·관찰을 카드에 반영 (raw → wiki/invest-concepts/). 목적지가 invest-concepts라 카드는 ingest 관할.
- Obsidian 그래프 — 카드 간 wikilink = 지도의 엣지.
## 6. 분야 카드 구조 / Field Card Template
```markdown
---
title: [분야명] # 예: 미 달러 / USD
source_type: invest-concept # 기존 canonical 타입 재사용 (taxonomy 무변경)
status: draft # draft→reviewed→verified
confidence: low|medium|high
tags: [invest-concept, field-card, macro-asset] # 섹터면 sector
last_reviewed: YYYY-MM-DD
---
# [분야명]
## Parent
- [[wiki/invest-concepts/field-map]] # 지도 허브로 upward link (linking-rules)
## 한 줄 정의 / What it is
> 이게 뭔지 한 문장.
## 무엇이 이걸 움직이나 / Drivers # "이 분야를 위/아래로 미는 힘" (입력 방향)
| 요인 | 방향 | 메커니즘 | [검증/가설] | 근거 |
|---|---|---|---|---|
## 연결 / Linkages # 엣지 — "이게 움직이면 따라오는 것" (출력 방향) ★핵심
| 이게 ↑하면 | → 따라 | 메커니즘 | [검증/가설] | 근거 |
|---|---|---|---|---|
## 관찰 지표 / What to watch # 매일 invest-daily가 잡을 구체 지표·티커
- ...
## 경기 사이클 위치 / Cycle position
- 회복/확장/둔화/침체 중 언제 강·약
## 검증 상태 / Verification
- [검증] N개 · [가설] M개
## Sources
- [[raw/invest-research/...]]
## Related (= 그래프 엣지)
- [[wiki/invest-concepts/field-...]]
```
### 핵심 장치 2개
1. **모든 행에 `[검증]/[가설]` 라벨 강제** — 뇌피셜과 근거 분리. `[가설]`은 외부 사용 금지, research 통과 시 `[검증]` 승격. (CLAUDE.md "출처 없는 단정 금지" 준수.)
2. **drivers(이걸 미는 힘) vs linkages(이게 미는 것) 분리** — 인과 방향 혼동 방지.
## 7. 씨앗 카드 8장 / Seed Set (v1)
거시 자산군 (6):
- `field-dollar` — 달러 (DXY/USD-KRW)
- `field-us-rates` — 미 10Y 금리
- `field-oil` — 원유 (WTI)
- `field-gold` — 금
- `field-us-equity` — 미국 주식 (S&P500) ← 사용자 보유 TIGER 360750과 직결
- `field-bitcoin` — 비트코인
산업 섹터 (2):
- `field-semiconductors` — 반도체 (사용자 관심)
- `field-bigtech-ai` — 빅테크/AI (반도체·금리와 연결 강함)
> 씨앗 카드는 v1에서 **드라이버·연결 일부를 가설(label=[가설])로 채워 시작**해도 됨 — research로 점진 검증. 단 빈 카드 금지(최소 정의+관찰지표+연결 1행).
## 8. invest-daily 강화 / Daily Capture Enrichment
`templates/invest-daily-template.md` 끝에 섹션 추가:
```markdown
## 분야 관찰 / Field Observations
> 오늘 움직인 카드와, 그 카드가 예측한 연결이 실측과 맞았는지 대조. 루프의 엔진.
| 오늘 움직인 카드 | 방향 | 그 카드 예측 연결이 맞았나?(확인/반증) | 새 가설/메모 |
|---|---|---|---|
| [[wiki/invest-concepts/field-dollar]] | ↑0.8% | 예측대로 금↓ ✓ / 원자재 횡보(반증) | "원자재 공급이슈?" |
```
## 9. 작동 루프 / Operating Loop
```text
매일 /invest-daily → 거시 카드 지표 + "분야 관찰"(예측 vs 실측 대조)
수시 패턴 발견 → /invest-research 로 관계 3표 검증
→ /invest-ingest 로 검증 엣지를 카드 "연결표"에 [검증] 라벨로 박음
결과 카드마다 [검증] 누적 = 지도 강화 = 내일 더 잘 보임
```
## 10. 지도 허브 / field-map.md
- `wiki/invest-concepts/field-map.md` (named hub, `index.md` 금지 — CLAUDE.md §12).
- 8장 카드를 2층(거시 자산군 / 산업 섹터)으로 나열 = "한눈에 보는 지도".
- 모든 카드의 `## Parent`가 이 허브를 가리키고, 허브는 cluster로 카드를 나열(양방향).
- `wiki/llm-wiki.md` MOC + invest-hub에 field-map 엔트리 추가.
## 11. 기존 규칙 정합 / Compliance
- **source_type 무변경**: 카드는 `invest-concept` 재사용 → taxonomy 영향 없음. 구분은 `tags: field-card`.
- **claim gate 검증됨 (2026-06-08 코드 확인)**: `wiki_rules.CLAIM_REQUIREMENTS`의 claims-표 강제는 `wiki/concepts/` prefix에만 걸리고 **`wiki/invest-concepts/`에는 적용 안 됨**(startswith 별개 경로). 따라서 카드에 `## Claims Extracted`/`## Claim-backed Knowledge`**강제 없음** → 카드의 근거 장치는 본 설계의 **`[검증/가설]` 라벨 + `## Sources`**로 충분. (단 `raw/invest-research/`는 여전히 claims-표 강제 — 관계 검증은 거기서.)
- **linking-rules**: 카드 Parent→field-map, field-map→invest-hub upward link.
- **CLAUDE.md §15 파생 규칙**: 카드는 canonical(invest-concepts) — 외부 산출물 아님. `[가설]` 행은 외부 derived 사용 금지.
## 12. 성공 기준 / Success Criteria
1. 씨앗 8장 + 허브 + 강화 일일 템플릿이 생성되고 structure-lint/claim-gate 통과.
2. Obsidian 그래프뷰에서 8장이 wikilink로 연결돼 "지도"로 보임.
3. `/invest-daily` 한 번 돌렸을 때 "분야 관찰" 칸이 카드를 참조해 채워짐.
4. 카드의 모든 연결/드라이버 행에 `[검증]/[가설]` 라벨이 있음(빈 라벨 0).
## 13. 향후 (out of scope, 다음 버전)
- 매수 신호 규칙(③) — 카드 [검증] 엣지가 충분히 쌓인 분야부터 누적 트리거 규칙화.
- 산업 섹터·한국 섹터·개별 종목 카드 확장.
- 자금흐름·거래량 반자동 수집.
- 카드 스캐폴딩 전용 명령(`/invest-field`) — ingest 마찰 시.
@@ -0,0 +1,85 @@
---
title: "Spec E — Antigravity hook 포팅 (공유 스크립트 + --antigravity 어댑터)"
source_type: llm-generated
status: draft
confidence: medium
tags: [harness, antigravity, hooks, design, automation]
last_reviewed: 2026-06-08
---
# Spec E — Antigravity hook 포팅
> Spec A/B/C 의 hook 강제를 Antigravity 로 확장. 핵심: **같은 `.claude/hooks/` 스크립트(SSOT) + 얇은 `--antigravity` I/O 어댑터.** Claude/Codex 와 *구조·로직 동일*, Antigravity 프로토콜이 강제하는 출력 봉투/필드명만 분기.
## 1. 검증된 사실 (외부 리뷰 정정)
로컬 README 의 "Antigravity 는 PreToolUse 에서만 강제 가능"은 **stale/불완전**이었다. 현행 공식 문서(geminicli.com/docs/hooks/reference) 확인:
| 이벤트(Gemini명) | `decision: deny`? | 에이전트 출력 텍스트 검사? | 필드 |
|---|---|---|---|
| BeforeTool | ✅ | — | tool_name, tool_input |
| AfterTool | ✅ | tool_response | tool_input, tool_response |
| **AfterAgent** | ✅ | ✅ **`prompt_response` = 에이전트 최종 텍스트** | prompt, prompt_response |
→ verdict/stats SubagentStop 게이트도 **AfterAgent 로 hook 강제 가능**(deny → retry, reason 이 교정 프롬프트). Antigravity 는 네이티브 이벤트(PreToolUse/PostToolUse/Stop)와 Gemini CLI 이벤트(BeforeTool/AfterTool/AfterAgent)를 둘 다 지원.
## 2. 설계
### DD1 — 공유 스크립트 + 출력 어댑터
검사 로직(`check_markdown_write`/`validate_verdict_block`/`validate_stats_block`/`check_c2`/…)은 **한 줄도 안 바꾼다.** `--antigravity` 플래그 시 출력 봉투만 분기:
- `emit_allow``{"decision":"allow"}` (exit 0)
- `emit_block(reason)``{"decision":"deny","reason":reason}` (exit 0, fail-open 철학)
### DD2 — 필드/이벤트 매핑
- 에이전트 출력: `last_assistant_message`(Claude) **또는** `prompt_response`(Gemini AfterAgent).
- 이벤트명: `SubagentStop`(Claude) **또는** `AfterAgent`/`Stop`(Antigravity).
- tool 경로: claim_gate `target_path` 이 이미 `TargetFile/file_path/...` fallback 보유(Antigravity 호환).
### DD3 — 배선 + repoint
`.agents/hooks.json` 가 repo 의 `.claude/hooks/*.py`**절대경로 + `--antigravity`** 로 호출. 스크립트가 ROOT 를 `__file__` 로 잡으므로 절대경로면 ROOT=이 repo 자동. Antigravity 작업 위치를 `Documents/LLM Wiki`(예전) → `dev/llm-wiki-private`(현재)로 이전.
## 3. 아키텍처
```
.claude/hooks/wiki_claim_gate.py ← MOD. --antigravity 출력 모드 + prompt_response/AfterAgent 매핑
.claude/hooks/wiki_structure_lint.py ← MOD. --antigravity 출력 모드 (run_pre/run_hook → (code,msg) 반환)
.agents/hooks.json ← MOD. BeforeTool/AfterTool/AfterAgent (+native) 배선, 절대경로+--antigravity
(global) ~/.gemini/antigravity-cli/hooks/wiki_hard_gate.py ENFORCE_PREFIX → dev repo (repoint, 별도/보완)
```
배선(`.agents/hooks.json`):
| 이벤트 (네이티브 + Gemini 둘 다) | 스크립트 |
|---|---|
| PreToolUse / BeforeTool | claim_gate --antigravity + structure_lint --pre --antigravity |
| PostToolUse / AfterTool | structure_lint --hook --antigravity |
| Stop / AfterAgent | claim_gate --antigravity (verdict/stats, prompt_response 검사) |
기존 wiki_hard_gate(리포트 품질 G1-G4) 는 보완적이라 유지.
## 4. 수용 기준 (검증 가능)
1. `claim_gate.py --antigravity` + (branch-note DEM 누락) Write 이벤트 → stdout `{"decision":"deny",...}` + exit 0. (비-antigravity: stderr + exit 2.)
2. `claim_gate.py --antigravity` + 정상 → `{"decision":"allow"}` + exit 0.
3. `claim_gate.py --antigravity` + AfterAgent 이벤트(`prompt_response` 에 malformed verdict 블록) → `{"decision":"deny"}`.
4. `structure_lint.py --pre --antigravity` + ghost 링크 → `{"decision":"deny",...}` + exit 0.
5. `.agents/hooks.json` valid JSON + 3 이벤트 + 절대경로 + `--antigravity` 포함.
6. 기존 4 스위트 + 신규 antigravity 모드 테스트 green. (Claude/Codex exit-code 경로 회귀 없음.)
## 5. 실측 미지수 (Antigravity 세션에서 사용자 확인)
- Antigravity 가 실제 fire 하는 이벤트 이름(네이티브 vs Gemini) — 그래서 **둘 다 배선**.
- `AfterAgent`**서브에이전트별** fire 하는가(judge 검증에 필요) vs 메인만 — 미확인. 안 되면 verdict/stats 는 in-prompt G3(이미 완료)가 대체.
- `.gemini/settings.json`(nested 호환) 도 필요한지 — 우선 `.agents/hooks.json` 만, 필요시 추가.
- Antigravity 작업 디렉터리 repoint 방법(사용자 환경 설정).
## 6. 구현 순서
1. claim_gate `--antigravity` 출력 모드 + prompt_response/AfterAgent 매핑 (TDD subprocess).
2. structure_lint `--antigravity` 출력 모드 (run_pre/run_hook → (code,msg)) (TDD).
3. `.agents/hooks.json` 배선.
4. (별도) wiki_hard_gate ENFORCE_PREFIX repoint.
5. 전체 스위트 + §4 스모크 + §5 미지수 사용자 인계 문서화.
## 7. 메모
- Codex 가 "공짜"였던 건 exit-code 규약이 Claude 와 같아서. Antigravity 는 `{decision}` JSON 이라 어댑터 한 겹 필요 — *다른 설계가 아니라 같은 SSOT + 최소 직렬화 분기*.
- 이로써 hook 레벨 강제 + in-prompt G3 이중 안전망. wiki_hard_gate(1500줄 별도)와의 drift 도 점진 흡수 여지.
@@ -0,0 +1,62 @@
---
title: "Spec F — 투자 파이프라인 경화 (구체성 + 검증)"
source_type: llm-generated
status: draft
confidence: medium
tags: [invest, harness, hooks, design, automation]
last_reviewed: 2026-06-08
---
# Spec F — 투자 파이프라인 경화
> 리뷰 발견(체계성 9/10·구체성 7/10): 실제 돈을 움직이는 invest 파이프라인이 우리가 만든 harness(claim_gate traceability + deep-research 적대검증)를 *가장 필요로 하나* 강제가 안 걸려 있고, 실행 단계 구체성(수수료/환율/세금·프로필·리밸런싱·staleness)이 빔. 두 갈래로 경화.
## A. 검증 (Verification)
### V1 — claim_gate 를 `raw/invest-research/` 로 확장 (백본, 테스트 가능)
`wiki_rules.CLAIM_REQUIREMENTS` 에 invest-research 항목 추가. hook 강제(쓰기 시):
```python
{"prefix": ("raw/invest-research/",),
"tables": [("## Claims Extracted",
["Claim ID", "Claim", "Evidence quote", "Strength", "적용 조건", "증명 못 하는 것"])],
"sections": ["## 출처 / Sources", "## 핵심 인용"]},
```
→ 환각된 금융 claim 이 근거표/출처/verbatim 없이 들어오는 걸 쓰기 시점에 차단. structure_lint 는 template-derived C1 으로 이미 invest 섹션/frontmatter 커버(확인만).
### V2 — ledger 근거 traceability
`raw/invest-ledger/``raw/` prefix 라 **structure_lint `--pre` 가 거래행의 깨진 `근거` wikilink 를 이미 차단.** invest-decide 산문이 "근거 없으면 기록 거부" 유지. (행-셀 non-empty 의 hook 강제는 over-engineering — 산문+structure_lint 로 충분.)
### V3 — deep-research 명시 호출
invest-daily/research 프롬프트 강화: **`deep-research` 스킬 명시 호출** + 수치마다 출처+조사시점(이미 있음). full 적대검증(3표 quorum)은 `research-fanout`/deep-research Workflow = **Spec D opt-in** 으로 연결(문서화). 단일 패스 KEEP/CORRECT/REJECT 의 한계 명시.
## B. 구체성 (Concreteness)
### C1 — 프로필 게이트 (G4)
invest-plan 이 프로필 핵심값(목표금액·기간·MDD·과세소득)을 **AskUserQuestion 으로 채움.** 사용자가 거부/미입력하면 `NEEDS_CONTEXT` 로 그 부분만 보류(사용자 주권 — strategy §고지 일관). 민감정보(과세소득)는 강제 안 함.
### C2 — 리밸런싱 밴드 (G5)
strategy 에 **리밸런싱 밴드 ±5% 기본** 추가. 단 *근거가 아니라 위험감내 재량*`UNSUPPORTED_DECISION` 라벨 + "이 숫자는 임의값, 본인 재량" 한 줄(그들의 정직성 일관). 사용자 조정 가능.
### C3 — 원장 수수료/환율/세금 (G6)
invest-ledger 템플릿 + ledger.md 거래표에 **`수수료` · `체결환율`** 컬럼 추가. 해외 ETF **양도세(연 250만 공제 후 22%)** + 환율을 P&L 에 반영해야 함을 메모. (실손익 = 단가×수량 − 수수료 ± 환차 − 양도세.)
### C4 — staleness 가드 (G3)
invest-decide 가 근거 문서 나이 점검: **일일노트 >24h** 또는 **조사노트 >90d** 면 "재조사 먼저" 플래그(체결가 괴리 위험). 기본값 사용자 조정 가능.
## C. 수용 기준
1. `claim_gate`: `raw/invest-research/x.md` 가 Claims Extracted/출처/핵심인용 누락 시 차단. 완비 시 통과. (TDD)
2. `claim_gate --antigravity` 동일 경로도 `{decision:deny}` (3-플랫폼).
3. 기존 5-prefix 회귀 green + 신규 invest-research 테스트.
4. ledger 템플릿/문서에 수수료·체결환율 컬럼 존재(grep).
5. strategy 에 리밸런싱 밴드 + UNSUPPORTED 라벨 존재(grep).
6. invest-plan 에 프로필 AskUserQuestion 게이트, invest-decide 에 staleness 플래그 존재(grep).
7. 전체 hook 스위트 green.
## D. 비목표
- full 적대검증 Workflow(3표) = Spec D opt-in.
- 자동 가격 피드/주문 체결 — 시스템 범위 밖(면허 자문 아님).
- wiki/invest-strategy/concepts 의 claim_gate 강제 — 우선 invest-research(증거층)만. 후속 확장 가능.
## E. 메모
- invest 는 Claude 전용 파이프라인이나 claim_gate/structure_lint 는 공유 SSOT → invest-research 확장이 3-플랫폼(Claude/Codex/Antigravity) 모두에 자동 적용(invest 명령 자체는 Claude 전용이지만 hook 은 경로 기반).
@@ -0,0 +1,271 @@
# Deep-Research 하네스 → Codex / Antigravity CLI 이식 설계
- **상태**: design (구현 전)
- **작성일**: 2026-06-09
- **원본 하네스**: Claude Code `Workflow` 도구 기반 JS 스크립트
`~/.claude/projects/-home-donghyeon-dev-llm-wiki-private/88afa9ca-1e45-4353-9ff0-6361812d9053/workflows/scripts/deep-research-wf_aecef33f-4cc.js`
- **목표**: claude code의 deep-research를 **codex-cli와 antigravity-cli 각각이 그대로 수행**할 수 있도록 단일 외부 Python 드라이버로 이식. 완전 충실(원본 동작·상수·퇴화 경로 1:1).
---
## 0. 한 줄 요약
원본 deep-research는 Claude `Workflow` 도구 위에서 도는 **결정론적 JS 오케스트레이터**다. Codex/Antigravity는 이 도구가 없고 오케스트레이션이 모델 주도이므로, **결정론 제어 로직을 외부 Python 드라이버 1벌**에 이식하고, 각 플랫폼은 `run_agent()` **backend 어댑터**로 흡수한다. 에이전트 실행기는 **각 CLI의 headless 모드**(`codex exec`, `agy -p`)이며, 웹 조사는 **각 CLI의 네이티브 web_search·webfetch 도구**가 수행한다(별도 검색 API·비용 없음). 포팅 = 프롬프트 3벌 복제가 아니라 **backend flag 하나로 분기하는 1벌 도구**.
---
## 1. 배경 / 문제
### 1.1 원본 하네스의 2층 구조
| 층 | 내용 | 이식 난이도 |
|---|---|---|
| **층1 — 결정론 제어** (JS 코드, 모델이 못 건드림) | URL 정규화·dedup, fetch 예산 회계, claim 랭킹, 3-vote 정족수 산식, barrier 동기화, stats | 플랫폼 무관 — 코드로 1:1 이식 |
| **층2 — 에이전트 프롬프트 + 스키마 + 페이즈** | Scope→Search→Fetch→Verify→Synthesize 6단계 프롬프트, 5개 JSON 스키마, 3-vote adversarial 패턴 | 플랫폼 무관 콘텐츠 — 그대로 이식 |
### 1.2 플랫폼 능력 검증 결과 (공식 소스 기반)
| 능력 | Claude Code | Codex CLI (`codex exec`) | Antigravity CLI (`agy`) |
|---|---|---|---|
| 결정론적 코드 제어 | Workflow 네이티브 | ❌ (모델 주도) → **외부 드라이버 필요** | ❌ (모델 주도) → **외부 드라이버 필요** |
| 병렬 실행 | `parallel()` | subprocess 병렬 | Async Subagent / subprocess 병렬 |
| headless 단발 실행 | — | `codex exec` | `agy -p "prompt"` (Command Mode) |
| **네이티브 웹검색** | WebSearch | ✅ `codex exec --json`에 web_search 항목 | ✅ harness 내 web/research 도구 |
| **네이티브 웹페치** | WebFetch | ✅ | ✅ webfetch |
| **스키마 강제 출력** | tool-layer 검증+재시도 | ✅ `--output-schema <jsonschema>` | ⚠️ 미확인 → 드라이버 검증·재시도로 보강 |
| 설정 포맷 | JS | `.toml` agent / CLI flag | `agent.json` / CLI flag |
> **검증 메모**: 초기 조사 때 "Codex/Antigravity 서브에이전트는 웹·스키마 없음"이라 판단했으나, 그건 *interactive subagent* 문서만 본 오판이었다. `codex exec` **headless 모드**에는 `--json`(web_search 이벤트 포함) + `--output-schema`가 있고, `agy -p`도 동일 harness의 웹 도구를 쓴다. 따라서 별도 검색 API(Tavily 등)·bare SDK는 불필요하며, **사용자가 이미 구독으로 쓰는 CLI 네이티브 도구**를 그대로 활용한다.
출처:
- Codex non-interactive(`--json`/`--output-schema`/web_search): https://developers.openai.com/codex/noninteractive
- Codex CLI 웹검색 설정: https://codex.danielvaughan.com/2026/05/09/codex-cli-web-search-configuration-cached-live-domain-allow-lists-prompt-injection-defence/
- Antigravity CLI features: https://antigravity.google/docs/cli-features
- OpenAI Agents SDK 언어(Py/TS): https://openai.github.io/openai-agents-js/
- Antigravity Python SDK: https://github.com/google-antigravity/antigravity-sdk-python
### 1.3 왜 SDK가 아니라 CLI headless인가
- **비용**: CLI는 구독제 → 호출당 추가 과금 없음. bare SDK + 검색 API는 중복 비용.
- **충실도**: 원본은 에이전트가 *직접* WebSearch/WebFetch를 호출한다. CLI headless도 에이전트가 네이티브 웹도구를 호출 → 동일 의미.
- **스키마**: Codex `--output-schema`가 tool-layer 검증을 대체. Antigravity는 드라이버가 Pydantic 검증·재시도로 보강.
---
## 2. 목표 / 비목표
### 목표
1. 원본 6페이즈·상수·퇴화 경로를 **1:1 충실** 이식.
2. `python -m deep_research --backend {codex|antigravity} "<질문>"` 단일 진입점으로 두 플랫폼에서 동일 동작.
3. 결정론 제어 로직을 **순수 함수**로 분리 → 단위테스트로 원본 동작 회귀 검증.
4. 웹·스키마를 각 CLI 네이티브 기능으로 충족(추가 비용 0).
### 비목표
- Claude를 backend로 추가하지 않음(원본 그 자체이므로 불필요).
- repo의 `.claude/.codex/.agents` 프롬프트 미러 체계에 편입하지 않음(이 드라이버는 그 밖의 새 카테고리 — 1벌 도구).
- 원본 상수·페이즈 구조 변경/최적화 없음(충실 우선).
- GUI·서버·스케줄러 없음(단발 CLI 실행만).
---
## 3. 아키텍처
```
scripts/deep-research/
├── pyproject.toml # deps: pydantic>=2, (stdlib asyncio/subprocess/json)
├── README.md # 사용법, 인증 전제, backend별 주의
├── deep_research/
│ ├── __init__.py
│ ├── __main__.py # CLI 진입점, argparse, .env/키 점검, report 출력
│ ├── config.py # 상수 (원본과 동일): VOTES_PER_CLAIM 등 + 동시성 cap
│ ├── schemas.py # 5 Pydantic v2 모델 + JSON Schema export 헬퍼
│ ├── core.py # 결정론 순수함수 (JS 1:1): dedup/budget/rank/tally/assemble
│ ├── pipeline.py # asyncio 오케스트레이션 (무배리어 pipeline / verify 배리어)
│ ├── prompts.py # 6단계 프롬프트 (원본 문자열 이식)
│ ├── backends/
│ │ ├── base.py # AgentBackend ABC: async run_agent(prompt, schema) -> BaseModel
│ │ ├── codex.py # codex exec --json --output-schema 호출·파싱
│ │ ├── antigravity.py # agy -p 호출·파싱 + Pydantic 검증·재시도
│ │ └── mock.py # 테스트 더블 (고정 응답 주입)
│ └── report.py # Report → markdown + JSON dump
└── tests/
├── test_core.py # dedup/budget/tally/rank 단위테스트
└── test_pipeline_mock.py # MockBackend 통합 스모크 + 퇴화 경로 3종
```
### 3.1 모듈 책임 (단일 책임 / 격리)
- **core.py** — 네트워크·LLM·플랫폼 의존 0. 입력 dict/list → 출력 dict/list 순수 변환. 단위테스트 100% 가능. *충실도의 심장.*
- **backends/** — 유일하게 플랫폼을 아는 곳. interface `run_agent(prompt, schema) -> 검증된 Pydantic 객체`. 한 backend 교체가 core·pipeline에 무영향.
- **pipeline.py** — core 함수 + backend 호출을 asyncio로 엮는 곳. 원본 `pipeline()`(무배리어) / `parallel()`(배리어) 의미를 asyncio로 재현.
- **web 계층 없음** — 웹은 backend가 호출하는 CLI의 네이티브 도구가 수행. 드라이버는 URL 문자열만 다룬다(정규화·dedup).
### 3.2 backend 인터페이스 계약
```python
class AgentBackend(ABC):
@abstractmethod
async def run_agent(self, prompt: str, schema: type[BaseModel],
*, label: str) -> BaseModel | None:
"""프롬프트를 1개 CLI headless 에이전트로 실행, schema로 검증된 객체 반환.
실패/사용자-skip → None (원본 .filter(Boolean) 의미)."""
```
- **codex.py**: `codex exec --json --output-schema <tmp.json> --cd <neutral_dir> -s read-only "<prompt>"` subprocess. JSONL stdout에서 최종 메시지 파싱 → Pydantic 검증. 웹은 codex 네이티브 web_search.
- **antigravity.py**: `agy -p "<prompt>" ...` subprocess. 출력 파싱 → Pydantic 검증, 실패 시 "스키마에 맞춰 JSON만" 재프롬프트 N회(기본 2). 웹은 agy 네이티브 도구.
- **mock.py**: 생성자에 `{label_prefix: 응답객체}` 주입. 네트워크·subprocess 없이 즉답. 테스트 전용.
> **중립 작업 디렉터리**: CLI executor는 repo가 아닌 임시/중립 `--cd`에서 호출 → 코드베이스 컨텍스트(AGENTS.md/CLAUDE.md)를 안 물고 순수 리서치 에이전트로 동작. read-only 샌드박스.
---
## 4. 상수 (원본과 동일 — `config.py`)
```python
VOTES_PER_CLAIM = 3 # claim당 verifier 수
REFUTATIONS_REQUIRED = 2 # 2/3 refute면 kill
MAX_FETCH = 15 # fetch 예산 (전체 novel URL 상한)
MAX_VERIFY_CLAIMS = 25 # 검증 대상 claim 상한
ANGLES_TARGET = 5 # scope 분해 목표(스키마 minItems3/maxItems6)
CONCURRENCY = 10 # asyncio.Semaphore (원본 Workflow cap 대응, 설정 가능)
ANTIGRAVITY_RETRIES = 2 # 스키마 검증 실패 시 재프롬프트 횟수
```
---
## 5. 스키마 (`schemas.py` — 5 Pydantic v2 모델)
원본 5 SCHEMA를 Pydantic으로 1:1. `model_json_schema()`로 Codex `--output-schema`용 JSON Schema export.
- **Scope**: `question:str`, `summary:str`, `angles: list[Angle](min 3, max 6)` / `Angle{label, query, rationale?}`
- **Search**: `results: list[SearchResult](max 6)` / `SearchResult{url, title, snippet?, relevance: Enum[high|medium|low]}`
- **Extract**: `sourceQuality: Enum[primary|secondary|blog|forum|unreliable]`, `publishDate?`, `claims: list[Claim](max 5)` / `Claim{claim, quote, importance: Enum[central|supporting|tangential]}`
- **Verdict**: `refuted:bool`, `evidence:str`, `confidence: Enum[high|medium|low]`, `counterSource?`
- **Report**: `summary`, `findings: list[Finding]` / `Finding{claim, confidence, sources:list[str], evidence, vote?}`, `caveats`, `openQuestions?: list[str]`
---
## 6. 데이터 흐름 (6 페이즈 — 원본 충실)
### 6.0 Scope (1 agent)
질문 → 5개 상보적 검색 각도(`Scope`). 빈 질문이면 즉시 에러 반환(원본과 동일).
### 6.1 Search (각도별 1 agent, 무배리어 pipeline 1단계)
- 에이전트가 **CLI 네이티브 web_search**로 검색(쿼리 정제 가능) → 원질문 기준 관련도 재랭킹·SEO 스팸 제거 → top 4-6(`Search`).
- *드라이버는 검색을 직접 하지 않음.* 에이전트의 인지 단계(관련도 판단) 보존.
### 6.2 Dedup (순수 드라이버 — `core.dedup`)
원본 로직 1:1:
- `normURL(u)`: hostname `www.` 제거 + pathname 후행 `/` 제거 + lowercase. 파싱 실패 시 `u.lower()`.
- `seen: set`, relevance 순(high<medium<low) 정렬 후 순회.
- 이미 본 키 → `dupes`에 적재, 스킵.
- `fetchSlots<=0` 이고 relevance가 medium/low(rank≥1) → `budgetDropped`에 적재, 스킵.
- 통과 시 `seen`에 추가, `fetchSlots--`.
### 6.3 Fetch + Extract (novel URL별 1 agent, pipeline 2단계 parallel)
- 에이전트가 **CLI 네이티브 webfetch**로 본문 취득 → 출처품질 평가 + falsifiable claim 2-5개(직접 인용 포함) 추출(`Extract`).
- fetch 실패/무관/페이월 → `claims:[]`, `sourceQuality:"unreliable"`(원본과 동일).
- backend가 None 반환(실패/skip) → 드롭(원본 `.filter(Boolean)`).
### 6.4 Rank (순수 — `core.rank_claims`)
`importance`(central<supporting<tangential) → `sourceQuality`(primary<…<unreliable) 순 정렬 → 상위 `MAX_VERIFY_CLAIMS=25`.
### 6.5 Verify (배리어 — claim당 3 agent 병렬)
- claim당 `VOTES_PER_CLAIM=3` verifier를 병렬, "refute 우선" 프롬프트(`Verdict`).
- **정족수 산식 (`core.tally`, 원본 1:1, abstention 엣지 포함)**:
- `valid = [v for v in verdicts if v is not None]` (None=기권)
- `refuted = sum(v.refuted for v in valid)`
- `survives = (len(valid) >= REFUTATIONS_REQUIRED) and (refuted < REFUTATIONS_REQUIRED)`
- ⚠️ **all-abstain → refuted=0 → 거짓 생존 금지**: `len(valid) >= 2` 조건이 이를 차단(원본 주석과 동일 의도). 이 엣지는 단위테스트로 고정.
### 6.6 Synthesize (1 agent)
생존 claim → 의미중복 병합·findings 그룹화·confidence 부여·3-5문장 요약·caveats·open questions(`Report`). refuted 목록은 투명성 위해 첨부.
### 6.7 퇴화 경로 3종 (원본 1:1 — 반드시 보존)
1. **claim 0개**: 검증 0건 요약 + sources/stats 반환.
2. **전건 refute**: "adversarial verification에서 전부 기각, 결론 불가" 요약 + refuted 목록 반환.
3. **synthesis 실패/skip**: 보고서 대신 생존 claim raw salvage 반환(전체 run 폐기 금지).
### 6.8 stats (원본 키 보존)
`angles, sourcesFetched, claimsExtracted, claimsVerified, confirmed, killed, afterSynthesis, urlDupes, budgetDropped, agentCalls(=1+angles+sources+voted*3+1)`.
---
## 7. 동시성 (`pipeline.py`)
- `asyncio.Semaphore(CONCURRENCY)`로 동시 CLI subprocess 수 제한.
- **무배리어 pipeline (Search→Dedup→Fetch)**: 각도별 async 체인 — A각도가 Fetch 도는 동안 B각도는 Search 가능. 단, **dedup의 `seen`/`fetchSlots`는 공유 상태**이므로 각 Search 완료 직후 dedup을 적용하는 지점에서만 갱신(원본은 pipeline 2단계 진입 시 순차 도착하며 갱신). → asyncio에서는 dedup 임계구역을 단일 코루틴/락으로 직렬화해 경쟁 제거.
- **배리어 (Verify)**: 전체 claim 풀이 모인 뒤 `asyncio.gather`로 일괄 검증(원본 의도적 barrier).
- agent 호출 1개 = CLI subprocess 1개. 콜드스타트 ~97콜/run → 구독제라 비용 무관, 속도는 동시성으로 완화.
---
## 8. 에러 처리
| 상황 | 처리 (원본 의미) |
|---|---|
| backend run_agent 실패/타임아웃 | None 반환 → 상위에서 filter |
| fetch 실패/페이월 | `unreliable` + `claims:[]` |
| verifier 기권(None) | valid에서 제외, 정족수 미달 시 미생존 |
| Codex `--output-schema` 위반 | codex 재시도; 그래도 실패 시 None |
| Antigravity 스키마 검증 실패 | 드라이버가 "JSON만" 재프롬프트 `ANTIGRAVITY_RETRIES`회 후 None |
| 빈 질문 / scope 실패 | 즉시 에러 dict 반환 |
| 키/CLI 미설치 | 시작 시 명확한 에러로 중단 |
---
## 9. 인증 / 설정 전제
- **codex backend**: `codex` CLI 설치 + 로그인(구독). 웹검색 활성(`--json` web_search).
- **antigravity backend**: `agy` CLI 설치 + 로그인(구독). 웹 도구 활성.
- 별도 검색 API 키 **불필요**.
- 시작 시 선택된 backend의 CLI 존재·로그인 점검, 미충족 시 안내 후 종료.
---
## 10. 테스트 전략
### 10.1 core 순수함수 단위테스트 (`test_core.py`)
- `normURL`: www/trailing-slash/대소문자/파싱실패 케이스.
- `dedup`: 중복 적재, budget-drop(slot 소진 시 medium/low만), high는 slot 무시 통과 여부(원본 `relRank>=1` 조건 정확 재현).
- `tally`: 3-0/2-1/1-2/0-3, **all-abstain(생존 금지)**, 1 valid+2 abstain(미달 미생존), 2 valid 0 refute(생존).
- `rank_claims`: importance·quality 2차 정렬 순서.
### 10.2 MockBackend 통합 스모크 (`test_pipeline_mock.py`)
- 고정 응답으로 6페이즈 정상 흐름 1건 + **퇴화 경로 3종** 재현. 네트워크·키·subprocess 0.
- stats 키·`agentCalls` 계산식 검증.
### 10.3 실 backend 스모크 (수동, 선택)
- 짧은 질문 1건을 `--backend codex` / `--backend antigravity`로 각 1회 실행, 보고서·stats 육안 확인.
---
## 11. 리스크 / 미해결 (구현 1차에서 확인)
| 리스크 | 영향 | 완화 |
|---|---|---|
| `agy` output-schema 플래그 유무 미확인 | Antigravity 스키마 보장 약함 | 드라이버 Pydantic 검증·재시도로 보강; 플래그 있으면 채택 |
| CLI 콜드스타트 누적 지연 | run 시간 증가 | 동시성 cap 상향, Antigravity Go 런타임, (후속) 세션 재사용/`spawn_agents_on_csv` |
| `codex exec` JSONL 최종 메시지 추출 형식 | 파싱 취약 | 이벤트 타입 기반 파서 + 스키마 검증으로 방어 |
| 무배리어 pipeline의 dedup 공유 상태 경쟁 | dedup 비결정성 | dedup 임계구역 직렬화(락/단일 코루틴) |
| repo 내 `scripts/` 부재(기존 메모) | 신규 디렉터리 | `scripts/deep-research/` 신설 — sync 생성기와 무관(별 카테고리) |
---
## 12. 거버넌스 / repo 정합
- 이 드라이버는 프롬프트 미러(`.claude`/`.codex`/`.agents`) 대상이 **아님** — backend 어댑터로 분기하는 1벌 도구.
- 위치: `scripts/deep-research/`. 구현 완료 후 CLAUDE.md에 **"외부 도구: deep-research 드라이버"** 한 줄 등재 검토(별 카테고리, 3-플랫폼 패리티 예외 — invest/project 파이프라인과 유사한 비미러 항목).
- 원본 JS 경로를 README에 명시해 충실도 대조 기준 보존.
---
## 13. 구현 순서(개요 — 상세는 plan 단계)
1. `schemas.py` + `config.py` + `prompts.py`(원본 문자열 이식).
2. `core.py` 순수함수 + `test_core.py`(원본 동작 고정).
3. `backends/base.py` + `mock.py` + `test_pipeline_mock.py`(퇴화 경로 포함) — 여기까지 **네트워크 0으로 충실도 검증 완료**.
4. `pipeline.py`(asyncio, dedup 임계구역 직렬화) — MockBackend로 통과.
5. `backends/codex.py`(`codex exec --json --output-schema`) → 실 스모크.
6. `backends/antigravity.py`(`agy -p` + 검증·재시도) → 실 스모크.
7. `report.py` + `__main__.py` + README.
@@ -0,0 +1,174 @@
# 2026-06-10 — `.claude/` 문서관리 하네스 전수 감사 보고
- **의뢰**: "하네스가 잘 동작하긴 하는데 한번씩 하네스대로 동작하지 않고 결과물을 내는 경우가 있다. agents·command·hooks·skills 를 하나씩 분석해 극한으로 동작시키기 위한 개선점을 찾아라."
- **범위**: `.claude/agents/` 10 · `.claude/commands/` 22 · `.claude/hooks/` 8 · `settings*.json` 2 · `.claude/skills/wiki-workflow/SKILL.md` 1 (+ 교차참조: `CLAUDE.md`, `rules/`, `templates/`, 3-플랫폼 미러)
- **방법**: 집행 레이어(hooks·settings)는 컨트롤러 직접 정독, agents/commands 는 병렬 research lane 4개(agents / capture·transform commands / output·invest commands / Claude Code 훅 스키마 공식 문서 검증), 핵심 가설 3건은 **라이브 세션 실측**으로 확정.
- **Verdict: NOT-READY** (개선 필요 — P0 6건 / P1 9건 / P2 7건). 골격은 견고하나 배선 결함 3종이 증상을 직접 유발.
---
## §0. 한 줄 결론
하네스의 골격(2단 게이트 파이프라인 + 훅 4종)은 잘 설계되어 있고 **실제로 작동 중**이다. 그러나 "한번씩 하네스대로 동작하지 않는" 증상의 주범은 *모델의 우회*가 아니라 — **① 훅↔에이전트 출력 계약 불일치로 인한 오차단→이탈 재시도, ② 진입점(SKILL.md) 라우팅 공백으로 인한 폴스루, ③ 파생·invest 레이어의 결정론 게이트 부재** — 즉 하네스 자체의 배선 결함 3종이다.
## §1. 실측으로 확인한 사실 (라이브 세션 재현)
훅에 임시 이벤트 덤프를 넣고 probe subagent 를 돌려 검증 (실험 후 원복 완료, `git diff` 클린 확인):
| 검증 항목 | 결과 |
|---|---|
| `SubagentStop.last_assistant_message` 존재 여부 | ✅ 존재 — 게이트는 살아있음 (공식 문서엔 스키마 미기재였음) |
| `SubagentStart` `additionalContext` 주입 | ✅ subagent transcript 에서 주입 문자열 확인 |
| **증상 재현** | ✅ **재현됨** — probe(Explore 에이전트)가 `Verdict: COMPLETE` 라고 말하자 게이트가 차단 → probe 는 본래 임무 출력 대신 **사과문 형태의 이탈된 재시도 출력**을 반환 |
| `stop_hook_active=true` 재시도 | ✅ 재시도 메시지는 **무검증 통과** (one-retry enforcement) |
| 게이트 적용 범위 | ⚠️ 이벤트에 `agent_type` 필드가 있는데도 게이트가 **모든 subagent**(Explore/general-purpose/Plan 포함)에 무차별 적용 |
| PreToolUse `matcher: "*"` | ⚠️ Read/Grep 포함 **모든 도구 호출마다** python 프로세스 1개 스폰 확인 |
→ 사용자가 보는 "하네스 미준수 출력"의 상당수는 **게이트가 안 돌아서가 아니라, 게이트가 잘못된 대상을 차단한 뒤 그 이탈 재시도가 무검증으로 통과**하는 메커니즘.
확인된 SubagentStop 이벤트 필드 (실측): `session_id, transcript_path, cwd, permission_mode, agent_id, agent_type, hook_event_name, stop_hook_active, agent_transcript_path, last_assistant_message, background_tasks, session_crons`
## §2. 근본 원인 체인 5개
### RC1 — 훅↔출력계약 불일치 (오차단 → 이탈 → 무검증 통과) [최우선]
`wiki_claim_gate.py:162-179` 의 SubagentStop 게이트와 에이전트 출력 템플릿의 충돌:
| 에이전트 | 충돌 | 결과 |
|---|---|---|
| `wiki-adversarial-reviewer.md:102` | `**Verdict:** COMPLETE` 를 강제하면서 출력 템플릿이 요구 문자열 3종(`Claim ID`/`Decision Evidence Map`/`UNSUPPORTED_DECISION`) 출력을 보장하지 않음 (`UNSUPPORTED_DECISION` 은 파일 전체에 부재) | COMPLETE 선언 시 상시 차단 위험 |
| `wiki-research-lane.md:122` | 동일 — 절차(step 5)엔 3종이 있으나 출력 템플릿엔 없음. official-doc 슬라이스처럼 3종이 정당하게 무관한 corpus 에서 확정 차단 | 동일 |
| `wiki-diagram-reviewer.md:195` | `NEEDS_FIX`(60≤점수<95, hard-stop 0) → `verdict: not-ready` + `blocking: 0` 인데 훅은 `not-ready ∧ blocking<1` 을 모순으로 차단 (`wiki_rules.py:245-246`) | **가장 흔한 채점 등급에서 결정론적 오차단** |
| 게이트 전체 | `agent_type` 무시 → 위키와 무관한 범용 에이전트도 `Verdict: COMPLETE` 한 마디에 차단 (§1 재현) | 무관 작업의 이탈 |
| 전 에이전트 공통 | `wiki-verdict`/`wiki-stats` 기계 블록이 출력 템플릿 바깥(부록 위치)에 정의됨 + 훅은 마커 부재 시 통과(presence-conditional) → 템플릿만 충실히 재현하면 블록 누락 = 게이트 silent never-fire | 미검증 통과 |
| 전 에이전트 공통 | `verdict: blocked` 경로 미정의 (입력 불량 시 즉흥 대응) + 예시 블록이 happy-path 리터럴 값(anchor-copy 위험) | 스키마 이탈 |
### RC2 — 진입점 라우팅 공백 (게이트 우회의 구조적 원인)
`SKILL.md` dispatch tree 가:
- 10개 에이전트 중 4개를 모름 (`wiki-diagram-reviewer`, `branch-depth-auditor`, `coverage-auditor`, `project-readiness-auditor`)
- `/branch-spec`·`/depth`·`/coverage`·`/project-spec` 게이트 파이프라인으로 라우팅하지 않음
- **파생 산출물(/interviewize·/blogify·/explain·portfolio) 분기가 아예 없음** → 자연어 요청이 "메인 에이전트 직접 응답"으로 폴스루 = status 게이트 미적용
- dispatch 시 필수 입력 3개 에이전트분 과소 전달 (adversarial-reviewer corpus path, doc-author mode/claim-evidence, source-summarizer source_type/정당화 결정) → `BLOCKED`/`NEEDS_CONTEXT` 왕복이 "flaky"처럼 보임
### RC3 — main agent 는 무검증 지대
- **Stop 훅 없음** — `/ingest`·`/lint`·`/branch-spec` 등 main agent 인라인 명령의 최종 보고(`## Stats` funnel, 종합 판정)는 어떤 훅도 검증 안 함. `validate_stats_block` 검증기가 있는데 subagent 에만 배선
- `/lint``wiki_structure_lint.py --all` 을 한 번도 호출하지 않고 깨진 링크·frontmatter 검사를 LLM 수기 재연 (`lint.md:44-53`)
- `wiki_quorum.py` 는 어느 명령도 호출하지 않는 dead tooling (유일 참조: adversarial-reviewer N=3 고위험 경로, 기본 N=1)
### RC4 — 결정론 게이트의 경로 공백
`CLAIM_REQUIREMENTS`(`wiki_rules.py:152-171`) 커버 prefix 는 5개뿐:
| 미커버 경로 | 위험 |
|---|---|
| `wiki/projects/` | canonical 절반이 증거 등급 구조 무게이트 — interview/portfolio 가 인용해야 할 바로 그 층 |
| `wiki/interview/`·`wiki/blog/`·`wiki/portfolio/`·`wiki/explainer/` | §11 최대 금지(canonical 우회·status 미달 파생)가 주 1회 LLM lint 에만 의존. 쓰기 시점 차단 가능한데 안 함 |
| `raw/invest-daily/`·`raw/invest-ledger/`·`wiki/invest-plan/` | **실제 돈 경로**가 narrative 신뢰 + LLM 산술로 운영. 환각 수치가 모든 훅 통과 |
| branch-note 파일명 | numbered-hierarchy 금지(§11)가 결정론 검사 없음 |
### RC5 — SSOT drift (복붙 재진술 엔진)
- `CLAUDE.md` 인벤토리: `/coverage`·`/migrate-claims` 누락, `/explain` "예정" 표기(실존+3플랫폼 미러 완료)
- `depth.md:2` description "2차 LLM 의미 게이트는 P2" — 본문은 이미 2차 dispatch 함 (stale)
- drift 실증 2건: `/ingest` source_type 어휘(daily-task 특수 절차 부재), `blogify.md:36` 인라인 섹션 리스트 vs 템플릿
- `migrate-claims.md:92` **`rg -L` 버그** — ripgrep `-L``--follow`(symlink)이지 files-without-match 가 아님. 유일한 결정론 검증 단계가 거짓 데이터 산출
- 명령 22개 전부 `allowed-tools`/`model` frontmatter 부재 — 판정 명령(/depth /coverage /query)과 실돈 명령(/invest-decide)이 전권 보유
- Claude 본판 에이전트에 Antigravity 포트의 G1(Pre-Read Proof)·G3(`{{ }}` placeholder 스키마)·G4(STOP 조건 열거) 하드 게이트 부재 — 본판이 포트보다 약한 역전 상태
## §3. 우선순위 수정안
### P0 — 오차단 제거 (증상의 직접 원인)
1. SubagentStop 게이트에 `agent_type` 스코핑 — 위키 에이전트 10종 외 즉시 allow
2. `wiki-diagram-reviewer.md:195` blocking 재정의 — blocking = 95점 미만 다이어그램 수(NEEDS_FIX 시 최소 1), hard-stop 수는 prose 로
3. COMPLETE 어휘 에이전트 2개(adversarial-reviewer, research-lane)에 `## Claim Traceability Check` 고정 섹션 — 3문자열을 "none found" 라도 항상 literal 출력
4. 기계 블록을 출력 템플릿 안으로 이동 + `verdict: blocked` 경로 + 예시 값 `{{ }}` placeholder 화
5. SKILL.md dispatch tree 보강 (누락 4 에이전트 + 게이트 파이프라인 + 파생 분기 + Required Inputs)
6. `migrate-claims.md` `rg -L``rg --files-without-match` (3 copies)
### P1 — 결정론 커버리지 확장
7. `CLAIM_REQUIREMENTS` 확장 (+6 엔트리): `wiki/projects/` · 파생 4종(`## Sources` 에 canonical wikilink 요구, portfolio 는 projects 필수) · `raw/invest-daily/`(수치 행 URL+날짜 토큰)
8. 파생 status 게이트 결정론화 — `check_markdown_write` 에서 Sources 의 canonical 링크 frontmatter `status:` 검사 (explainer 면제)
9. Stop 훅 추가 — main agent 최종 메시지의 wiki-stats/Verdict 를 동일 validator 로 검증
10. `/lint` 1단계 = `wiki_structure_lint.py --all` 실행 + §15.5 "(예정)" 3검사 구현
11. `/coverage` 1차 기계화 (`--coverage-pre` 모드) + branch-note 파일명 regex 검사
12. branch-spec 루프 천장 2회 (project-spec 과 정렬) + 최종 보고 wiki-stats funnel
13. PreToolUse claim_gate matcher 를 `Bash|Write|Edit|MultiEdit|NotebookEdit` 로 축소 (성능)
14. `allowed-tools` frontmatter 도입 (판정 명령 read-only, /invest-decide 최소권한)
15. CLAUDE.md 인벤토리 동기화 + `depth.md` description 갱신
### P2 — 구조 개선
16. G1/G3/G4 하드 게이트 Claude 본판 역이식
17. invest 경화: `invest_ledger_check.py`(근거 링크 실존·staleness·주간 거래 수·row 스키마·P&L 재계산) + quorum 검증 invest 수치 opt-out 기본값 반전 + 임계값 strategy.md 일원화
18. 복붙 재진술 제거 (명령 내 템플릿 섹션 리스트 → "템플릿 Read 가 SSOT", §6 등급표·명명 규칙 → 참조 1줄)
19. `wiki_quorum.py` 배선 (`/lint --fix-plan` CRITICAL ≥5건 시 adversarial N=3 + quorum) 또는 폐기 명시
20. 판정 에이전트 3종(adversarial/depth/readiness) 모델 상향 검토 (sonnet→opus)
21. `wiki-decision-researcher` 모순 해소 — "controller 에 dispatch 요청 방출"로 재서술 + `Write` 도구 제거
22. one-retry 완화 검토 — `stop_hook_active` 시 스키마 오류 재차단 허용 (Claude Code 자체 8회 연속 차단 cap 존재; 단 P0 1~4 선행 후)
## §4. 잘 되어 있는 것 (유지)
- `/depth`·`/project-spec` 의 "1차 결정론 스크립트 → 2차 의미 감사" 파이프라인 — 22개 명령 중 모범
- `wiki_structure_lint.py` 의 C2 링크 검사(코드 스팬·fence·escaped pipe)와 projected-content 방식
- 완성-선언 시에만 C1/C3 를 켜는 `is_completeness_checkable` 설계 (false-positive 소음 차단)
- Bash 직접 쓰기 차단 + Write/Edit 강제 경유 — 새 CLAIM_REQUIREMENTS 엔트리가 진짜로 우회 불가능해지는 토대
## §5. Claim Traceability Check (본 감사의 추적성 자기검사)
- **Claim ID / Claims Extracted 검사**: 본 감사는 raw source 를 신규 생성하지 않음 — `Claims Extracted` 표 생성 의무 해당 없음. 근거는 전부 hooks/agents/commands 파일의 file:line 인용으로 추적 가능.
- **Decision Evidence Map 검사**: branch-note 를 생성/수정하지 않음 — 해당 없음. (게이트 자체의 `Decision Evidence Map` 요구 로직은 `wiki_rules.py:157-160` 에서 검증함.)
- **UNSUPPORTED_DECISION 검사**: 본 보고의 권고 중 외부 근거 없이 사용자 trade-off 로 결정해야 하는 항목 — P2-20(모델 상향: 비용 증가 vs 판정 신뢰), P2-22(one-retry 완화: 루프 위험 vs 집행 강도) — 은 측정 근거가 아니라 설계 판단이며, 채택 전 사용자 결정 필요.
- **실측 증거**: §1 표의 6개 항목은 2026-06-10 라이브 세션에서 직접 재현·관측한 1차 증거 (probe agent transcript + 이벤트 덤프).
## §6. P0 구현 추적
| # | 항목 | 상태 (2026-06-10) |
|---|---|---|
| P0-1 | SubagentStop agent_type 스코핑 (`WIKI_AGENT_TYPES` SSOT in `wiki_rules.py` + `subagent_stop_gate` 분기) | ✅ 완료 — 테스트 5건 추가, 4개 suite 전부 OK |
| P0-2 | diagram-reviewer blocking 재정의 (= 95점 미만 다이어그램 수, hard-stop 수는 prose) | ✅ 완료 |
| P0-3 | Claim Traceability Check 고정 섹션 (adversarial-reviewer · research-lane) + bare-token/공백금지/BLOCKED 무방출 규칙 | ✅ 완료 |
| P0-4 | 기계 블록 템플릿 내장(4-backtick 외곽 fence) + `verdict: blocked` 경로 + `{{ }}` placeholder — 7개 에이전트 | ✅ 완료 |
| P0-5 | SKILL.md dispatch tree 보강 (4 에이전트 + 게이트 파이프라인 + 파생 분기 + Required Inputs + STOP 14항) | ✅ 완료 |
| P0-6 | migrate-claims `rg -L``rg --files-without-match` (3 copies) | ✅ 완료 |
| 후속 | 3-플랫폼 미러 동기화: SSOT `.agents/plugins/wiki-superpowers/agents/` 6 + Antigravity `agent.json` 6 (JSON 검증 OK) + Codex `.md`/`.toml` 12 (TOML 검증 OK, body 패리티 확인). platform-aware — Codex/Antigravity 에 SubagentStop 문구 미도입, 기존 G3/hard-gate 어휘 사용 | ✅ 완료 |
검증: `test_wiki_claim_gate`·`test_wiki_rules`·`test_wiki_structure_lint`·`test_wiki_quorum` 전부 OK / 편집된 에이전트 7파일 4-backtick fence 짝 균형 / 본 보고서 `wiki_structure_lint --file` PASS.
## §7. P1 구현 추적 (2026-06-10)
| # | 항목 | 상태 |
|---|---|---|
| P1-7 | `CLAIM_REQUIREMENTS` +5 엔트리: `wiki/projects/`(실제 구현 내용+Sources, named-hub 면제) · `wiki/interview\|blog\|explainer`(Sources+canonical 링크 `content_regex`) · `wiki/portfolio/`(projects 링크 필수) · `raw/invest-daily/`(고정 체크리스트+출처 섹션) + 수치행 출처/조사시점 정책(`invest_daily_numeric_failures`) | ✅ 기존 파일 전수 dry-run — 오차단 1건(2026-06-06 invest-daily, 템플릿 도입 전)은 light-touch 마이그레이션으로 해소 |
| P1-8 | 파생 status 게이트 결정론화: `derived_source_status_failures``## Sources` 의 canonical 링크 frontmatter `status` 전부 ∈ {reviewed, verified, published-ready} 필요 (explainer 면제) | ✅ |
| P1-9 | main agent `Stop` 훅 (`--main-stop` 플래그): fenced `wiki-stats` 만 검증 — COMPLETE trap/wiki-verdict 는 메타 대화 오차단 위험으로 제외. Antigravity native `Stop`(subagent 의미) 라우팅과 플래그로 분리 | ✅ (신규 세션부터 활성 — 훅 설정은 세션 시작 시 스냅샷) |
| P1-10 | `/lint` 개정: 절차 1단계 `--all`·2단계 `--stale`(신규 린터 모드) 결정론 선행 + §A1(§15.5 3검사) + C/D군 린터 출력 SSOT 화 + E군 write-time backstop 주석 | ✅ |
| P1-11 | 린터 `--coverage-pre`(0 PASS/1 FAIL/3 EXEMPT) + `NAMING_VIOLATION`(신규 생성만 차단, 기존 파일 편집 허용) + `/coverage` 1차 스크립트화 | ✅ 기존 branch-note 파일명 위반 0건 사전 확인 |
| P1-12 | `/branch-spec` 루프 천장 2회(§8c) + §9 `wiki-stats` funnel(Stop 훅 검증) + §8b `--coverage-pre` 연결 | ✅ |
| P1-13 | PreToolUse claim_gate matcher `*``Bash\|Write\|Edit\|MultiEdit\|NotebookEdit` (동작 동일, 프로세스 스폰 감소) | ✅ |
| P1-14 | `disallowed-tools`: depth/query(Write·Edit·NotebookEdit 금지) · coverage/lint(NotebookEdit·WebSearch·WebFetch — 정당한 쓰기 경로 보존) · invest-decide(WebSearch·WebFetch — 결정 시점 즉흥 조사 차단) | ✅ /coverage·/lint 전면 쓰기 금지는 §Coverage 기록·--fix-plan 적용을 깨뜨려 제외 |
| P1-15 | CLAUDE.md 동기화(§1 흐름·§2 인벤토리·branch-spec 항목·§15 /explain 해제·§15.5 "(예정)" 해제) + depth description 갱신 + **depth.md 의 환각된 `--fix --apply` 플래그 참조 제거** (덤 발견) | ✅ |
| 후속 | 명령 미러 동기화: lint·coverage·branch-spec·depth × Codex skill + Antigravity workflow (8파일, platform-aware — Stop 훅 문구 일반화, disallowed-tools 미전파, invest 절 미전파) | ✅ |
검증: 4개 test suite 전부 OK (123 tests, 신규 ~30 포함) / `settings.json` JSON valid / `--coverage-pre`·`--stale` 실데이터 스모크 PASS / 신규 게이트 전수 dry-run 오차단 0건.
## §8. P2 구현 추적 (2026-06-10 — 22개 권고 전체 완료)
| # | 항목 | 상태 |
|---|---|---|
| P2-16 | G1 Pre-Read Proof(10/10) + G4 STOP 조건 열거(10/10, 기존 blocked 규칙 참조 통합) + G2(쓰기 2종 Post-Write Validation, read-only 판정자 self-grep 증거 규칙) Claude 본판 역이식 — 에이전트당 ≤40줄, G3 는 P0-4 기완료 | ✅ |
| P2-17 | `invest_ledger_check.py` 신규(--check: 11열 스키마·근거 실존·staleness 24h/90d·주간 거래 수 / --report: P&L 기계 재계산) + 테스트 8건 + `/invest-decide` §5 배선·인라인 임계값 제거(strategy.md ①~⑤ SSOT) + `/invest-review` §3 배선 + ledger CLAIM_REQUIREMENTS 엔트리 + **quorum 기본값 반전**: invest-daily/research 고위험 수치 = read-only 검증 subagent ×3 + `wiki_quorum.py` 합산이 기본(opt-out 명시제, Workflow 는 상위 옵션 유지) | ✅ |
| P2-18 | 재진술 제거 15파일(blogify·interviewize·explain·projectize·branch × SSOT+미러 2): 템플릿 섹션 리스트 → "템플릿 Read 가 SSOT", §6 등급 열거 → 참조, 명명 규칙 → 1줄+린터 위임. drift 실증 문자열 잔존 0건 grep 확인 | ✅ |
| P2-19 | `wiki_quorum.py` 배선 — `/lint --fix-plan` CRITICAL ≥5건 시 adversarial N=3 + `/tmp/lint-vote-*` + 결정론 합산(KILL 제외/UNVERIFIED 보류/DOWNGRADE 하향). dead tooling 해소 (+미러 2) | ✅ |
| P2-20 | 판정 3종(adversarial/depth/readiness) `model: opus` 상향 — **사용자 승인** | ✅ |
| P2-21 | decision-researcher 모순 해소: description "dispatch 요청 방출(실 dispatch 는 controller)" + `Write` 제거 + SKILL.md/CLAUDE.md 정렬 + 변형 4벌(plugin SSOT·agent.json toolNames·codex toml sandbox read-only·codex md) 동기화 | ✅ |
| P2-22 | one-retry 폐지(위키 에이전트 한정) — `stop_hook_active` 무검증 통과 제거, Claude Code 8회 cap 이 무한루프 방지. main-stop 은 one-retry 유지 — **사용자 승인** | ✅ |
최종 검증: **5개 test suite 전부 OK** (test_invest_ledger_check 신규 포함) / G1 Pre-Read Proof 10/10 / 4-backtick fence 균형 / 본 보고서 lint PASS.
**잔여 follow-up (P0~P2 범위 밖, 기록만)**: ① Stop 훅·재차단 정책은 신규 세션부터 활성(훅 설정 세션 스냅샷) ② strategy ③ 주간 거래상한 N 미정 — 사용자가 정하면 `/invest-decide``--weekly-cap` 에 반영 ③ Bash claim gate 의 명령 문자열 path-매칭 false-positive(작업 중 2회 재현)는 redirect 타깃 기반 검사로 정밀화 후보 ④ concept source_type drift(메모리 2a) 별도 마이그레이션.
**Stats**: 분석 대상 45 파일 / 병렬 lane 4 / 실측 검증 3건 / findings 60+ → 권고 22건 (P0 6 · P1 9 · P2 7)
@@ -0,0 +1,37 @@
# 2026-06-10 — Consistency 계층 설계·구현 (문서 간 모순 탐지 + 동기화 자동검사)
- **의뢰**: feature 브랜치 노트들이 구현 중 추가 조사·결정으로 진화하며, 위임(delegated)된 내용이 문서 간에 서로 달라지는 모순 발생. 문서 정리 시 일관성을 맞추는 체계 요구. 예: `ca-skeleton-operational-contract`(project note) → 브랜치 노트들의 세부 결정.
- **사용자 결정**: 3층 풀스택 / 역참조 전파는 비차단 경고 / 재진술은 retro 수거 + 신규 경고.
## §1. 진단 (실증)
모순은 탐지 문제이기 전에 **복제 문제** — A 가 B 소유 결정을 풀어 쓰면 사본이 생기고, B 의 정당한 진화가 A 를 조용히 낡게 만든다. 실측 증거: 같은 파일 내 "D17 의 5개 rule" vs "4개 rule"(`feature-boundary...:101 vs :278`), 존재 불명 브랜치 cross-cite "(있다면)", 전파 의존을 prose 로만 기록("sibling 매핑이 바뀌면 ... 영향"), wikilink/bare 참조 혼용, 관심사 ID(C5b) ad-hoc.
## §2. 구현 (3층)
| 층 | 산출물 | 내용 |
|---|---|---|
| **계약** | `rules/consistency-contract.md` | Single-Owner(결정·관심사당 owner 1개: branch DEM `D<n>` 또는 project `§<n>`) + Reference-Only(`[[owner]] D<n>` + 1줄 요약만, 세부 재진술 금지 `RESTATED_FOREIGN_DECISION`) + 참조 형식 표준(wikilink + 같은 줄 후방 100자) + 해소 우선순위(owner 우선 / hub-vs-branch 는 사용자 판정 / 항상 승인 후) |
| **결정론** | `.claude/hooks/wiki_consistency_check.py` (+테스트 18) | Decision Registry(표 행 첫 셀 `D<n>` 정의, 주석 부가 형태 수용) + 참조 추출. 검사 5종: `DANGLING_DECISION_REF`(--pre 차단) / `BARE_DECISION_REF` / `BARE_OWNER_REF` / `DUAL_OWNERSHIP` / `DANGLING_SECTION_REF`. `--impact <slug>` 역참조 목록. `--post` = **역참조 충격 알림**(DEM 행 편집 감지 시 참조자 목록 비차단 전달) — "B 의 정당한 진화 + A 의 무통보" 를 끊는 동기화 트리거 |
| **의미** | `.claude/agents/wiki-consistency-auditor.md` (opus, read-only, 4-플랫폼 변형) | 참조 엣지 단위(전수 pairwise 금지) 양쪽 verbatim 대조 → `CONSISTENT / STALE_SUMMARY / CONTRADICTION / RESTATED` + owner-우선 해소 제안. wiki-verdict(blocking=CONTRADICTION 수)+wiki-stats 훅 검증, WIKI_AGENT_TYPES 등록 |
| **워크플로** | `/sync` (+미러 2) | 검사기 → auditor fan-out(엣지 >20 분할) → fix-plan(owner-우선, RESTATED→참조+1줄 교체, 승인 후 적용) → 재검사 천장 2회 + wiki-stats funnel |
배선: 양 repo settings.json — wiki(PreToolUse `--pre` + PostToolUse `--post`), ca-tmpl(절대경로 동일 — cross-repo 쓰기도 게이트). CLAUDE.md(§1·§2·§14, 명령 23/미러 15) + SKILL.md tree/lanes + lint E군 경계.
## §3. 파서 정밀도 (오탐 제거 이력 — 차단 훅의 전제)
| 반복 | DANGLING | 원인/수정 |
|---|---|---|
| v1 | 28 | DEM 첫 셀 `D9 (2026-05-31 보강)` 형태 미인식 → 첫 셀 시작-매칭으로 수정 |
| v2 | 26 | **귀속 모호성**: 외부 링크 후방 윈도의 D-id 가 *인용자 자신의* 결정인 경우("의존 — 우회(D13)") → 자기 DEM 보유 id 는 침묵(의미 귀속은 Layer 2) |
| v3 | **1 — 실모순** | `feature-repository-access-permission-contract.md:220``[[feature-rate-limit-idempotency-contract]] (D14)` 로 오귀속 — D14 의 실제 owner 는 `feature-application-port-usecase-contract`(같은 파일 :250 이 올바른 귀속을 증명) |
전수 dry-run 최종: **190 findings** (BARE_DECISION_REF 129 · BARE_OWNER_REF 60 · DANGLING 1) — retro 수거는 `/sync` fix-plan 으로 점진 진행.
## §4. 한계 (명시)
- 관심사 어휘 비정규 → `DUAL_OWNERSHIP` 은 정규화 exact-match 만 (fuzzy 는 Layer 2). coverage-matrix 를 관심사 레지스트리로 승격하면 정밀도 상승 (후속 후보).
- 귀속 모호 케이스(양쪽 모두 가진 D-id)는 결정론이 침묵 — auditor 가 엣지 판정 시 해소.
- 의미 모순(값·정책 충돌)은 Layer 2 LLM 판정 — `/sync` 실행 시점에만 (쓰기 시점 의미 대조는 비용상 제외).
검증: 테스트 5 suite OK (신규 18 포함) / settings ×2·agent.json·toml valid / 신규 파일 8종 실존 / `/sync` 스킬 레지스트리 등록 확인.
@@ -0,0 +1,45 @@
# 2026-06-10 — Tiered Extraction 아키텍처 (토큰 계층화: codex/agy 구독 + haiku 위임)
- **의뢰**: 메인 모델(opus) 토큰 소모 과다. 발췌(context 의 대부분)를 codex-cli/agy-cli(둘 다 구독) → haiku 순으로 위임하고, 판단·웹조사·작업 지시는 메인 모델 유지. 품질 최상 유지 조건.
- **사용자 결정**: 전체 구현 / 작업별 분담(codex=구조화 발췌, agy=web·요약, quorum 양쪽 1표씩) / 보수적 다운그레이드.
## §0. 핵심 원리
**발췌의 품질은 발췌자의 지능이 아니라 검증 게이트가 보장한다.** verbatim 인용은 re-grep, 커버리지는 funnel, 형태는 schema — 전부 기계 검증 가능. 따라서 발췌자는 가장 싼 엔진이어도 되며, 규율은 하나: **무검증 발췌는 절대 소비하지 않는다.**
## §1. 구성 (규약: `rules/extraction-tiering.md`)
| Tier | 엔진 | 담당 |
|---|---|---|
| T0 | 결정론 | 린터·`--packets`(엣지 ±맥락 0토큰 추출) |
| T1 | codex / agy | bulk 발췌(`deep_research.extract`)·cross-vendor 표(`deep_research.vote`) |
| T2 | haiku | `extraction-broker` agent(드라이버 구동+실패분 재발췌)·link-verifier(다운그레이드) |
| T3 | sonnet | 레포 쓰기 에이전트(훅 경유 필수 — doc-author/source-summarizer 유지) |
| T4 | opus | 판단·웹조사 방향·작업 지시·모순 판결·판정 3종 |
5계명: ① 외부 CLI = read-only 추출기(드라이버가 파일 내용을 프롬프트에 내장 — 엔진은 repo 미접근, 레포 쓰기는 Claude 훅 경유만) ② 무검증 발췌 소비 금지 ③ engine funnel(no silent swap) ④ opus 에 raw corpus 반입 금지(digest+file:line 만) ⑤ fallback 사다리 codex→agy→haiku→sonnet.
## §2. 신규/변경 컴포넌트
- `scripts/deep-research/deep_research/extract.py` — 발췌 드라이버 + **quote-verifier 내장**(PASS/CORRECTED/DROPPED) + digest(wiki-stats funnel)
- `scripts/deep-research/deep_research/vote.py` — cross-vendor 적대 표 생성 → `wiki_quorum.py` 가 엔진 불가지로 집계
- `wiki_consistency_check.py --packets` — /sync 의미 판정 입력의 T0 화
- `.claude/agents/extraction-broker.md` (haiku) + WIKI_AGENT_TYPES 등록 (wiki-stats 훅 검증)
- 라우팅: SKILL.md(bulk 정독 broker 1순위), `/lint` quorum·invest 수치 검증 = **cross-vendor 1+1+1**(Claude+codex+agy, 실패 시 Claude 대체+funnel), `/sync` 팩킷 선행
- `wiki-link-verifier` sonnet→haiku
## §3. 실 CLI 스모크에서 잡은 잠복 버그 4건 (전부 수정·실증)
| 버그 | 수정 |
|---|---|
| codex: 비-tty 에서 stdin 대기 블록 | `stdin=DEVNULL` |
| codex: /tmp 비신뢰 디렉터리 즉사 | `--skip-git-repo-check` |
| codex: JSONL 이벤트가 `item.completed`/중첩 `agent_message` — 파서 미인식 | 중첩 형태 파싱 추가 |
| codex: OpenAI strict schema — `additionalProperties:false`+전 required 요구 | `_strict_schema` 변환 |
| agy: `--cd`/`-m` 플래그 부재로 usage 즉사 | `cwd=` subprocess 인자 + `--model` |
**E2E 실증**: codex 발췌 성공(인용 PASS, Claude 토큰 0) / agy 발췌 성공 — 그리고 agy 가 낸 의역 인용 1건을 verifier 가 **폐기**(trust boundary 라이브 작동) / fallback 사다리 작동(agy 실패 시 codex 로, funnel 에 정직 기록).
## §4. 검증
deep-research 39 tests + wiki hooks 96 tests 전부 OK / 실 CLI E2E 양 엔진 성공 / 미러(lint·sync) 반영. **이월**: wiki-consistency-auditor 의 3-플랫폼 변형에 packets 입력 줄 동기화, extraction-broker 타 플랫폼 포팅 미정(Claude 전용).
@@ -0,0 +1,352 @@
---
title: 하네스 정합성 스위프 (Tier A) — 설계
date: 2026-07-14
status: draft
kind: design-spec
slice: 1-of-N (harness review 적용)
revision: 3 (spec 리뷰 2차 반영 — A1 portable-only 경로 + A3 BLOCKED 고정 + A1 hook별 E2E 표)
---
# 하네스 정합성 스위프 (Tier A)
## 0. 한 줄 목표
문서·설정이 실제 repo 상태와 어긋난 곳을 바로잡는다. **아키텍처는 건드리지 않는다.**
## 1. 배경
외부에서 받은 "LLM Wiki 하네스 구조·효율성 리뷰"를 적용하기로 했다. 리뷰는 partial Repomix
스냅샷(raw/·wiki/ 본문 미포함) 기반이라, 적용 전 각 주장을 실제 repo 근거로 **선별(triage)** 했다.
선별 결과 ~8개 semi-independent 프로젝트가 6 phase 로 나온다 — 단일 spec 으로 다룰 수 없다.
이 문서는 그중 **첫 슬라이스 = Tier A(정합성 스위프)** 만 다룬다. 나머지(B/C 티어)는 §7 백로그에 기록만 한다.
Tier A 선택 이유: 근거 확실, 소규모, 아키텍처 lock-in 0, live bug 포함, 빠름.
> **rev.2 노트:** 이 spec 자체를 리뷰받아 반영했다. 주요 변경 — (1) A1 완료 기준을 "직접 실행"이 아닌
> "실제 Hook E2E 관측"으로 격상, (2) A3 를 모호한 "트랜잭션"이 아닌 **명시적 허용 write set** 으로 고정 +
> **agent runtime contract 변경**으로 재분류, (3) A4 범위를 live 문서 전체로 확대(historical 기록 제외),
> (4) JSON/TOML 구문 검증·기존 hook test·의미 패리티 표·git-diff allowlist 추가.
## 2. 선별 근거 (verified)
| 주장 | 판정 | 근거 |
|---|---|---|
| Antigravity 훅 command 대상 절대경로가 부재 | **CONFIRMED** | `.agents/hooks.json``/home/donghyeon/dev/llm-wiki-private/...`; `/home/donghyeon/dev` 자체가 부재(`ls` 실패). |
| 해당 claim/structure Python 검사가 정상 실행 불가 | **CONFIRMED** | 경로 부재로 `python3 <경로>` 실패. |
| Hook 실패 시 Antigravity 가 작업을 완전 허용(fail-open)한다 / "강제 0" | **UNVERIFIED** | Antigravity 의 command-실패 처리(fail-open vs fail-closed)를 실행 로그로 확인 안 함. 단정 금지. global `wiki-hard-gate` 만 생존은 **LIKELY**(로그 확인 필요). |
| Codex 가 CLAUDE.md fallback 로드 (문서 서술) | **CONFIRMED (문서 오류)** | 루트에 `AGENTS.md` 존재 → `.codex/config.toml:2-3` 주석대로 fallback 은 "AGENTS.md 없을 때만" 발동. `CLAUDE.md:82`(Hooks bullet) "codex 가 본 CLAUDE.md 를 읽음(AGENTS.md 중복 불필요)" 은 거꾸로. |
| wiki-doc-author "1 file" vs Parent 갱신 모순 | **CONFIRMED** | STOP #5 / "What NOT" 이 "2개 이상 파일 수정 금지"인데 정상 create/migrate 는 target + Parent hub 2개 Edit. |
| doc-author 카테고리 "7 vs 8" | **CONFIRMED (SSOT·agent.json 만)** | SSOT `.agents/plugins/.../wiki-doc-author.md:73` "Category ∉ 7 허용"·agent.json STOP #2/table — 8개 나열하며 7. Claude 사본은 "8종"(드리프트). |
| doc-author validator "8개 모두" vs grep 그룹 7개 | **CONFIRMED (agent.json 만)** | agent.json output schema `Post-Write Validator ... (8개 모두)` 인데 validator 블록 grep 그룹 7개. |
| doc-author migrate "본문 보존 vs 정규화 모순" | **REJECTED (오독)** | body 보존 + frontmatter/Parent/Sources placeholder **추가**만 분리(L144·L156-157 line-count BLOCKED). 결함 아님. |
| 인벤토리 숫자 stale | **CONFIRMED** | 실측(§4 A4 명령): templates 27, rules 14, Claude agents 12, Antigravity executable agents 10, agent-prompt SSOT 10, commands 23. |
| `scripts/` 디렉터리 자체 부재 | **CONFIRMED (stale)** | `scripts/` 는 존재(`scripts/deep-research/`). `sync_automation.py` 만 부재. |
## 3. 범위
### In scope
- A1: Antigravity 훅 경로 복구 (Hook runtime configuration 변경)
- A2: Codex 문서 오류 정정 (운영 문서 정정)
- A3: wiki-doc-author 계약 결함 3종 정정 (**agent runtime contract 변경**)
- A4: 인벤토리 숫자 정정 (운영 문서 정정)
### Explicitly out of scope (→ §7 백로그)
harness/ 재구조화 · rules→skills 이관 · template-v2 · SSOT 생성기(B1) · 3-채널 출력(B2) ·
deep-research 런타임 하드닝(B3) · 훅 증분화+wiki-index(B4) · 훅 matcher 과범위(`*`) 최적화 ·
AGENTS.md source_type 목록 완전성 · **historical 기록**(`docs/superpowers/plans/*`, `notes/*`)의 stale 숫자.
**이번 슬라이스에서 절대 손대지 않는다.**
## 4. 항목별 설계
### A1 — Antigravity 훅 부활 (Hook runtime configuration)
**대상:** `.agents/hooks.json` 의 다음 **4개 command entry** (전부 절대경로 `/home/donghyeon/dev/...` 사용):
1. `wiki-claim-gate.PreToolUse`
2. `wiki-claim-gate.Stop`
3. `wiki-structure-gate.PreToolUse`
4. `wiki-structure-gate.PostToolUse`
`wiki-hard-gate`(global `~/.gemini/antigravity-cli/hooks/...`)는 정상 — 건드리지 않음.
**핵심 미확정:** Antigravity 가 hook command 를 어느 cwd 에서 실행하는지 불명(hook input payload 의 `cwd`
필드 ≠ command 실행 cwd). 따라서 단순 상대경로가 repo 루트로 resolve 된다고 **가정 금지**.
**구현 전략 (진단 우선):**
1. **CWD 진단** — 임시 진단 command(예: `pwd`, `git rev-parse --show-toplevel`, `env` 를 파일에
기록)로 Antigravity hook 의 실제 실행 cwd·환경변수를 관측한다.
2. **경로 형태 확정 (portable-only 우선순위 사다리).** 절대경로를 "올바른 현재 경로"로 교체하는 것은
같은 버그를 다른 절대경로로 연장할 뿐(이동·clone 시 재사망) → **임시 진단 외 최종안으로 채택 금지**.
다음 우선순위로 확정한다:
- **1순위:** Antigravity 가 제공하는 workspace/repository 환경변수(있다면). 진단 §5-A1 로 존재 확인.
- **2순위:** `git rev-parse --show-toplevel` 로 repo root 도출.
- **3순위:** launcher 파일(`.agents/hooks/run-wiki-hook.py`)이 자기 파일 위치 기준으로 repo root
탐색 후 `.claude/hooks/<script>.py` 실행. shell expansion 미지원/불확실 시 이 방식.
- **분기 규칙(구현 전 확정):** 진단상 cwd == repo root 면 `.agents/hooks.json` 만 상대경로로 수정하고
launcher 불필요. cwd ≠ repo root 면 launcher 파일 추가를 허용하고 §5 allowlist 에 조건부 포함.
3. **E2E 검증** — §5 A1 참조. 실제 Hook 실행 관측 전에는 구현 verdict = PARTIAL(DONE 금지).
**되돌리기:** git revert (config 1파일).
### A2 — Codex 문서 오류 정정 (운영 문서)
**대상:** `CLAUDE.md:82` (§2 Hooks bullet) 의 codex fallback 서술 — 현행 "`project_doc_fallback_filenames`
로 codex 가 본 CLAUDE.md 를 프로젝트 지침으로 읽음(AGENTS.md 중복 불필요)".
**증거 경계 (중요):** 이 정정의 증거는 **Codex 의 `project_doc_fallback_filenames` 동작뿐**이다.
Codex 에 대해서만 단정하고, Antigravity 로딩 메커니즘을 이 증거로 확장하지 않는다.
**변경 문구(취지):**
> Codex 는 루트 `AGENTS.md` 를 프로젝트 지침으로 로드한다. `.codex/config.toml` 의
> `project_doc_fallback_filenames = ["CLAUDE.md"]` 는 해당 디렉터리에 `AGENTS.md` 가 **없을 때만**
> 쓰이는 fallback 이다. 루트 `AGENTS.md` 가 존재하는 현재 구조에서 Codex 가 `CLAUDE.md` 를 자동
> 프로젝트 지침으로 직접 선택한다고 가정해서는 안 된다. `CLAUDE.md` 는 Claude Code 용 전체 운영
> 지침이자 모든 모델 공통 운영-규칙 SSOT 로 유지되며, `AGENTS.md`(≤150줄 요약)가 이를 가리킨다.
Antigravity 진입점은 `AGENTS.md:3` 의 기존 self-claim("Antigravity CLI / Codex CLI ... 가 자동 로드")을
참조하되, 그 로딩 동작을 이 슬라이스에서 새로 certify 하지는 않는다(백로그: 3-플랫폼 로딩 검증).
**범위 밖:** 리뷰의 "AGENTS.md 를 단일 공통 진입점으로 만들고 core-invariants 분리" 는 아키텍처 → 백로그.
**용어 주의:** `CLAUDE.md` 를 무자격 "full SSOT" 로 부르지 않는다. 이 repo 엔 복수 SSOT 존재 —
운영 규칙=`CLAUDE.md`, agent system prompt SSOT=`.agents/plugins/.../agents/*.md`, rules=`rules/*.md`.
따라서 "운영-규칙 SSOT" 로 한정 표기한다.
### A3 — wiki-doc-author 계약 결함 3종 (agent runtime contract)
세 결함이 **3개 플랫폼 계열의 5개 수기-동기화 표현 파일**에 불균등 분포. 사본마다 표현이 달라 개별 편집.
**결함 D1 — 파일수 vs Parent 갱신 모순.** "1 dispatch = 1 file" / "2개 이상 파일 수정 금지" 를
아래 **명시적 허용 write set** 으로 대체("트랜잭션" 추상어 대신 열거로 고정):
> 1 dispatch 는 정확히 하나의 target raw document 를 생성 또는 마이그레이트한다.
> 허용되는 write set 은 다음으로 **제한**한다:
> - `daily-note` · `project-note`: target document 1개.
> - 그 외 child category: target document 1개 **+** 그 문서가 직접 가리키는 Parent hub 1개
> (`## Cluster` 링크 유지 목적에 한함).
>
> 다른 raw 문서 / rule / template / derived wiki 문서는 수정하지 않는다.
`description` 의 "Writes exactly 1 file" → "creates/migrates one target document (maintaining its Parent
hub link)".
**원자성 오해 방지(ACID 아님).** 위 write set 은 *논리적 change set* 이며 기술적 atomic rollback 을
보장하지 않는다. target 또는 Parent 중 일부만 변경된 경우 — **단일 상태로 고정**(실행 계약에서 "또는"
금지, 플랫폼별 상이 해석 차단):
- DONE 반환 금지 → **Status = BLOCKED**. (`PARTIAL` 은 현행 doc-author output schema 에 미정의 —
`DONE | NEEDS_CONTEXT | BLOCKED` 뿐. `PARTIAL` 정식 도입은 B2 command-result-v1 로 이연.)
- 변경에 성공한 파일 + 실패한 단계를 모두 보고.
- 자동 rollback 은 구현된 경우에만 수행(현재 미구현 — 부분변경은 BLOCKED + 정직 보고).
(이는 기존 계약과 일관 — 본문 손실 시 BLOCKED 규칙이 이미 존재.)
**결함 D2 — 카테고리 "7" 오기.** SSOT·agent.json 의 "Category ∉ 7 허용" / "Category in 7 allowed"
**8**. 허용 8종: branch-note, error-note, interview-prep, job-posting, blog-topic, lecture-note,
project-note, daily-note.
**결함 D3 — validator 개수 하드코딩 제거(8→7 금지).** agent.json 의 `... (8개 모두)` 를 숫자 7로
바꾸지 **않는다** — 검증 "그룹" 수와 실제 shell command 수가 달라 재-stale 위험. 대신 숫자를 제거:
> Post-Write Validator: 아래 정의된 **모든 적용 가능 항목**을 실행하고 실제 결과를 첨부한다.
출력 schema 도 `{{위 Validator 의 모든 적용 가능 항목 실행 결과}}` 로.
**플랫폼 팬아웃 (수기 — 생성기 부재):**
| 표현 파일 | 계열 | D1 | D2 | D3 |
|---|---|---|---|---|
| `.claude/agents/wiki-doc-author.md` | Claude(md) | 有 | 無(이미 8) | 無 |
| `.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md` | plugin SSOT(md) | 有 | 有 | 편집 시 확인 |
| `.codex/agents/wiki-doc-author.md` | Codex(md) | 有 | 無(이미 8) | 편집 시 확인 |
| `.codex/agents/wiki-doc-author.toml` | Codex(toml) | 有 | 無(이미 8) | 편집 시 확인 |
| `.agents/agents/wiki-doc-author/agent.json` | Antigravity(json) | 有 | 有 | 有 |
각 사본 개별 Read → 해당 결함만 정밀 편집. **의미(계약)만 바꾸고 나머지 프롬프트 구조 보존.**
migrate 본문-보존 계약은 변경하지 않음(정상).
**범위 밖:** doc-author 를 중립 정의+생성기로 만드는 것(B1). 이번엔 기존 5표현 수기 정합화.
### A4 — 인벤토리 숫자 정정 (운영 문서)
**권위값 산출 명령(고정, 2026-07-14 실측 스냅샷):**
```bash
find templates -maxdepth 1 -type f -name '*.md' | wc -l # 27
find rules -maxdepth 1 -type f -name '*.md' | wc -l # 14
find .claude/agents -maxdepth 1 -type f -name '*.md' | wc -l # 12
find .agents/agents -mindepth 2 -maxdepth 2 -type f -name 'agent.json' | wc -l # 10 (Antigravity executable)
find .agents/plugins/wiki-superpowers/agents -maxdepth 1 -type f -name '*.md' | wc -l # 10 (agent-prompt SSOT)
find .claude/commands -maxdepth 1 -type f -name '*.md' | wc -l # 23
```
> **주의:** `.agents/agents`(=10, executable registry)와 `.agents/plugins/.../agents`(=10, prompt SSOT)는
> **물리적으로 별개인 두 디렉터리이며 각각 10개**다. "10 공유" 로 합쳐 쓰지 않는다.
**live 문서 정정 대상 (repo-wide grep 으로 확정, historical 제외):**
| 대상 | 현재(stale) | 정정 |
|---|---|---|
| `AGENTS.md:23` | templates "14개" | **27개** |
| `AGENTS.md:24` | rules "7개 rule" | **14개 rule** |
| `AGENTS.md:50` | "7 agents ... 정의" | **10 agents** (7 core + branch-depth-auditor·coverage-auditor·wiki-consistency-auditor) |
| `CLAUDE.md:79` | "동일 9개 agent" / "(위 7개 + branch-depth-auditor·coverage-auditor)" | **10개** / "(위 7개 + branch-depth-auditor·coverage-auditor·wiki-consistency-auditor)" |
| `CLAUDE.md:79` | "`scripts/` 디렉터리 자체 부재" | "`scripts/` 존재(deep-research), `sync_automation.py` 만 부재" |
| `.codex/agents/README.md:9` | "`scripts/` 디렉터리 자체 부재" | 동상 정정 |
| `.agents/plugins/wiki-superpowers/README.md:11` | "the `scripts/` directory itself is absent" | "scripts/ exists (deep-research); only sync_automation.py is absent" |
| `.agents/plugins/wiki-superpowers/README.md:46` | "동일한 7개 rule 파일" | **14개 rule 파일** |
> `CLAUDE.md:81` 은 "생성기 부재(2026-06-06 확인)"만 서술 — 여전히 참이므로 **변경하지 않음**.
> `docs/superpowers/plans/*`·`notes/*` 의 동일 문구는 **dated 기록이라 정정 대상 아님**(역사 보존).
**자동 치환 금지:** "14개"/"7개" 는 다른 의미로도 쓰이므로, repo-wide grep 후보는 **수동 판정**해
inventory 문맥만 편집.
**스냅샷 명시:** 정정 위치(또는 인접)에 "2026-07-14 실측 수기 스냅샷, B1 생성기 도입 전까지 수기 관리"
취지 1줄 병기.
**트레이드오프(인지):** B1 이 훗날 자동 생성 시 A4 는 버려질 작업. 현재 오독 유발 중이라 지금 정정.
## 5. 검증 계획
**항목별:**
| 항목 | 검증 |
|---|---|
| A1 | (1) `python3 -m json.tool .agents/hooks.json` 통과. (2) **Antigravity 실제 Tool 이벤트**에서 4개 hook 실행 관측 + 의도한 PASS/BLOCK 이 Antigravity 에 전달됨을 로그로 확인. (3) 직접 스크립트 실행만 통과했으면 DONE 아닌 **PARTIAL**. |
| A2 | `grep -n "AGENTS.md 중복 불필요" CLAUDE.md` → 0. 새 문구 존재. Antigravity 단정 없음 확인. |
| A3 | 아래 grep 으로 5표현 잔존 0 + 의미 패리티 표(아래) 전 PASS. `python3 -m json.tool .agents/agents/wiki-doc-author/agent.json``tomllib``.codex/agents/wiki-doc-author.toml` 구문 통과. |
| A4 | 위 `find` 명령 재실행값 == 문서값. repo-wide stale grep(§4 A4) 에서 live 문서 잔존 0(historical 제외). |
**A1 Hook별 E2E 트리거 표 (구현자가 각 hook 을 어떤 이벤트로 발생시킬지 명시):**
| Hook | 발생시킬 이벤트 | 기대 결과 |
|---|---|---|
| claim gate PreToolUse | Write 또는 Edit 실행 | hook process 실행 로그 확인 |
| claim gate Stop | Agent 실행 정상 종료 | 종료 hook 실행 로그 확인 |
| structure gate PreToolUse | Markdown 대상 Write/Edit 실행 | pre 검사 실행 확인 |
| structure gate PostToolUse | Markdown 쓰기 완료 | post 검사 실행 확인 |
| 차단 검증 | 의도적으로 규칙 위반한 임시 입력 | 비정상 종료코드 또는 BLOCK 전달 |
| 정상 검증 | 규칙 만족하는 임시 입력 | PASS 후 작업 계속 |
> 검증용 임시 파일을 repo 내부에 생성했다면 **테스트 후 삭제** — 최종 `git diff` 에 남지 않아야 함.
**A3 잔존-문구 grep (넓힘):**
```bash
grep -RInE '1 dispatch = 1 file|Writes exactly 1 file|Write 1 file|write the file once|2개 이상 파일|여러 파일을 한 번에 생성.?수정 금지|여러 파일 동시 생성.?수정 금지|Category ∉ 7|Category in 7 allowed|8개 모두' \
.claude/agents/wiki-doc-author.md \
.codex/agents/wiki-doc-author.md \
.codex/agents/wiki-doc-author.toml \
.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md \
.agents/agents/wiki-doc-author/agent.json
# 기대: 0 hit. 이후 새 write-set 계약 문구 존재도 각 파일에서 확인.
```
**A3 의미 패리티 표 (grep 만으로 불충분 — 계약 동등성 확인):**
| Contract | Claude md | plugin SSOT md | Codex md | Codex toml | agent.json |
|---|---|---|---|---|---|
| target document 1개 | | | | | |
| Parent hub edit 허용 | | | | | |
| unrelated 문서 edit 금지 | | | | | |
| 허용 category = 8 | | | | | |
| validator 개수 비고정 | | | | | |
| migrate 본문 보존 유지 | | | | | |
(각 셀 PASS 확인. 문장은 달라도 계약값은 동일해야 함.)
**구문 검증 (grep 통과 ≠ 유효 문법):**
```bash
python3 -m json.tool .agents/hooks.json >/dev/null
python3 -m json.tool .agents/agents/wiki-doc-author/agent.json >/dev/null
python3 -c 'import tomllib; tomllib.load(open(".codex/agents/wiki-doc-author.toml","rb")); print("toml OK")'
```
**기존 hook test 회귀 (존재 확인됨):**
```bash
python3 -m pytest .claude/hooks -q # test_wiki_structure_lint / _consistency_check / _claim_gate 등
```
**git-diff allowlist (범위 이탈 감시):** 아래 10개 파일(+ 본 spec) 외 변경 시 중단·사유 확인.
```
.agents/hooks.json
CLAUDE.md
AGENTS.md
.codex/agents/README.md
.agents/plugins/wiki-superpowers/README.md
.claude/agents/wiki-doc-author.md
.agents/plugins/wiki-superpowers/agents/wiki-doc-author.md
.codex/agents/wiki-doc-author.md
.codex/agents/wiki-doc-author.toml
.agents/agents/wiki-doc-author/agent.json
# 조건부(A1 launcher 경로 채택 시에만):
.agents/hooks/run-wiki-hook.py
```
검증: `git diff --name-only` 가 위 집합에 한정(launcher 미채택 시 11번째 파일은 나오면 안 됨).
검증용 임시 진단/테스트 파일은 최종 diff 에 잔존 0.
**전역 주의:** 편집 대상이 config/문서/프롬프트라 기존 hook(claim gate·structure lint)이 편집 자체를
차단할 수 있음 — 차단 시 사유 확인 후 우회 아닌 정합 편집으로 해결.
## 6. 리스크 & 롤백
**변경 종류 분류:**
| 항목 | 변경 종류 |
|---|---|
| A1 | Hook runtime configuration 변경 |
| A2 | 운영 문서 사실 정정 |
| A3 | **agent runtime contract 변경**(Parent edit 이 STOP 위반→허용) |
| A4 | 운영 문서 인벤토리 정정 |
- **A1·A3 는 런타임 계약을 바꾼다.** A1 은 hook 실행 경로, A3 는 doc-author 의 허용 write set. 순수
문서 변경은 A2·A4 뿐.
- **A1 불확실성:** 대상 Python 경로가 무효인 것은 CONFIRMED 이나, Antigravity 의 command-실패 처리
(fail-open/closed)는 미확인 — "현재도 dead 라 악화 없음" 으로 단정하지 않는다. 변경 전후 실제 hook
이벤트/오류 동작을 비교해 판단.
- A2·A4 는 문서 텍스트 — git revert 즉시 롤백.
- **3-플랫폼 패리티:** A3 은 5표현을 손으로 맞춰야 함. 하나라도 누락 시 새 드리프트 — §5 grep·패리티
표를 5표현 전부에 실행해 방지.
## 7. 백로그 (이번 슬라이스 밖 — 기록만)
**Tier B (근거 있음, 각자 프로젝트, 별도 spec):**
- B1: SSOT 생성기 복원(`generate_harness.py` + drift `check`). 최고 레버리지·최대 빌드. A4 흡수. 기존 드리프트 선(先) 정합 필요.
- B2: 3-채널 출력(command-result-v1 + run ledger + 사용자 응답 15줄화). 모든 명령/에이전트 접촉.
- B3: deep-research 런타임 하드닝(subprocess timeout[현재 부재 확인] + retry + resume + cache + stderr 보존).
- B4: 훅 증분화 + `wiki-index.json`. 현재 `rglob("*.md")` 전체 순회 확인됨.
- (신규) 3-플랫폼 project-지침 로딩 검증 — Codex/Antigravity 가 실제로 어느 파일을 로드하는지 E2E 확인.
**Tier C (opinionated/위험/저ROI — wholesale 비권장):**
- harness/ 메가 재구조화 — 동작하는 3-플랫폼 레이아웃 폐기. 아이디어만 선별 채택.
- rules→skills wholesale + template-v2(27개 전부). "컨텍스트 과다" 전제는 사실이나 fix 침습적. B1 이후 점진.
## 8. 완료 기준
**A1**
- `.agents/hooks.json` 대상 command 4개 전부 수정 + JSON 구문 통과.
- 최종 경로 방식이 **portable**(env var / `git rev-parse` / launcher self-locate) — 새 하드코딩 절대경로 0.
- launcher 채택 시 `.agents/hooks/run-wiki-hook.py` 만 추가되고 allowlist 에 포함(그 외 신규 파일 0).
- Antigravity 실제 Tool 이벤트에서 각 hook 실행 관측(§5 A1 표 6행) + 의도한 PASS/BLOCK 전달 확인.
- 직접 Python 실행만 성공 시 구현 verdict = **PARTIAL**(DONE 아님).
**A2**
- Codex fallback 설명이 실제 동작과 일치.
- 검증 안 된 Antigravity 자동로딩 단정 없음.
- "AGENTS.md 중복 불필요" 기존 문구 0건.
- `CLAUDE.md` 를 "운영-규칙 SSOT" 로 한정 표기(무자격 "full SSOT" 아님).
**A3**
- 5표현 전부 "target 1개 + Parent link 유지" 계약으로 정합, **허용 write set 명시**.
- 부분 실패 시 DONE 금지 규칙 존재.
- 허용 category 8 통일, validator 개수 하드코딩 제거.
- JSON·TOML 구문 통과.
- migrate 본문-보존 계약 불변.
- 의미 패리티 표 전 셀 PASS.
**A4**
- 모든 숫자를 고정 `find` 명령으로 재산출.
- repo-wide stale 검색 수행, live 문서 잔존 0(historical 제외).
- `.agents/agents`(10)와 `.agents/plugins/.../agents`(10)를 별개 디렉터리로 구분 표기.
- 2026-07-14 수기 스냅샷·B1 자동화 예정 명시.
**전역**
- `git diff --name-only` 가 §5 allowlist 10파일(+spec)에 한정.
- 범위 밖 구조 변경 없음.
- `python3 -m pytest .claude/hooks -q` 통과.
@@ -0,0 +1,10 @@
# CA Skeleton Frontend Operational Contract — Adversarial Review
전체 8개 finding의 falsification 결과:
- KEEP: 1
- DOWNGRADE: 7
- REJECT: 0
상세 표: `2026-07-18-ca-skeleton-frontend-operational-contract-review/adversarial-review.md`
@@ -0,0 +1,169 @@
# CA Skeleton Frontend Operational Contract — Controller Verification
**일자 / Date:** 2026-07-18
**범위 / Scope:** 2개 project-note의 사전 검토 artifact
**Verdict:** COMPLETE
**요청 언어 / User language:** ko
## Controller Recomputed Coverage
아래 값은 lane의 자기 보고를 신뢰하지 않고 controller가 디스크에서 다시 계산한 값이다. finding coverage는 보존된 사전 스냅샷과 backend benchmark를 기준으로 했고, 교정된 live target은 별도의 integrity gate로 확인했다. 재계산 결과는 `N=2`, `M=2`, `R=2`, `P=2`, `T=8`, `G=8`이다.
### Gate 1 — Scope
```bash
$ printf '%s\n' raw/project-notes/ca-skeleton-frontend-operational-contract.md raw/project-notes/ca-skeleton-operational-contract.md | wc -l
2
$ ls raw/project-notes/ca-skeleton-frontend-operational-contract.md raw/project-notes/ca-skeleton-operational-contract.md >/dev/null && echo 'source paths: 2 present'
source paths: 2 present
```
| Metric | Observed | Pass condition | Result |
| --- | ---: | --- | --- |
| N (in-scope paths) | 2 | 두 source path가 존재 | PASS |
### Gate 2 — Matrix
```bash
$ rg -c '^\| `raw/project-notes/[^`]+` \| READ_FULL \|' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/evidence-matrix.md
2
$ rg -o 'raw/project-notes/[a-z0-9-]+\.md' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/evidence-matrix.md | sort -u | wc -l
2
$ rg -o 'raw/project-notes/[a-z0-9-]+\.md' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/evidence-matrix.md | sort | uniq -d | wc -l
0
```
| Metric | Observed | Pass condition | Result |
| --- | ---: | --- | --- |
| M (matrix rows) / R (read rows) | 2 / 2 | `M==N`, unique paths 2, duplicate 0 | PASS |
### Gate 3 — Finding
```bash
$ rg -c '^### L1-F[0-9]{2}:' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/lanes/lane-01-target-review.md
8
$ rg -c '^### 4\.[12] ' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review-report.md
2
$ printf '%s\n' raw/official-docs/vite-build-tool-official.md raw/official-docs/react-ui-library-official.md raw/official-docs/tailwind-css-utility-first-official.md raw/official-docs/tanstack-query-server-state-official.md raw/official-docs/zod-runtime-schema-validation-official.md raw/official-docs/react-router-official.md | while IFS= read -r f; do rg -c '^## Claims Extracted' "$f"; done | awk '{sum += $1} END {print sum}'
6
$ rg -o '(VITE-C[0-9]+|REACT-UI-C[0-9]+|TAILWIND-UTIL-C[0-9]+|TSQ-C[0-9]+|ZOD-VALID-C[0-9]+|REACT-ROUTER-C[0-9]+)' raw/project-notes/ca-skeleton-frontend-operational-contract.md | sort -u | wc -l
13
$ rg -n '^\| `FE-D00[3-8]` ' raw/project-notes/ca-skeleton-frontend-operational-contract.md | rg -v '\[\[raw/official-docs/' | wc -l
0
$ rg -n 'UNSUPPORTED_DECISION|BROKEN_CLAIM_REFERENCE|MISSING_CLAIMS_EXTRACTED' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review | wc -l
0
```
| Metric | Observed | Pass condition | Result |
| --- | ---: | --- | --- |
| T (findings) / P (file subsections) | 8 / 2 | target 8 findings, benchmark reference-only, `P==R` | PASS |
| source-backed FE-D003~008 / official source `Claims Extracted` / unique Claim IDs | 6 / 6 / 13 | 각 decision row는 source와 actual Claim ID를 가진다 | PASS |
| source 없는 FE-D003~008 row | 0 | 6개 row 모두 official source로 traceable | PASS |
| traceability failure markers (current-run artifacts) | 0 | marker 없음 | PASS |
### Gate 4 — Quote
```bash
$ rg -c '^\$ sed -n' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/sed-proofs.md
16
$ rg -c '\[이미 기록된 16개 sed proof\]' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review-report.md
1
```
| Metric | Observed | Pass condition | Result |
| --- | ---: | --- | --- |
| V / P_quotes / C / D / U | 16 / 16 / 0 / 0 / 0 | 16 proof commands가 16개 quote와 일치 | PASS |
| G (verified findings) / T | 8 / 8 | `G==T` | PASS |
### Gate 5 — Adversarial
```bash
$ rg -c '^\| L1-F[0-9]{2} \|' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/adversarial-review.md
8
$ rg 'KEEP —' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/adversarial-review.md | wc -l
1
$ rg 'DOWNGRADE —' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/adversarial-review.md | wc -l
7
$ rg 'REJECT' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/adversarial-review.md | wc -l
0
```
| Metric | Observed | Pass condition | Result |
| --- | ---: | --- | --- |
| adversarial rows / T | 8 / 8 | 모든 finding ID가 한 행씩 존재 | PASS |
| KEEP / DOWNGRADE / REJECT | 1 / 7 / 0 | master §4-1과 일치 | PASS |
### Gate 6 — Priority
```bash
$ rg -o 'L1-F[0-9]{2}' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/priority-recommendations.md | sort -u | wc -l
8
$ comm -3 <(rg -o 'L1-F[0-9]{2}' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/lanes/lane-01-target-review.md | sort -u) <(rg -o 'L1-F[0-9]{2}' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/priority-recommendations.md | sort -u) | wc -l
0
```
| Metric | Observed | Pass condition | Result |
| --- | ---: | --- | --- |
| priority finding IDs / unmatched IDs | 8 / 0 | §5의 각 권고가 §4.1 finding에 매핑 | PASS |
### Gate 7 — Link
```bash
$ for p in $(rg --no-filename -o 'docs/superpowers/specs/[A-Za-z0-9_./-]+\.md' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review-report.md docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review-controller-verification.md | sort -u); do test -f "$p" || echo "$p"; done | wc -l
0
```
| Metric | Observed | Pass condition | Result |
| --- | ---: | --- | --- |
| 존재하지 않는 report artifact link | 0 | 참조한 artifact path가 모두 존재 | PASS |
### Gate 8 — Language
```bash
$ forbidden_pattern=$(sed -n '103p' AGENTS.md | sed -E 's/.*\((.*)\).*/\1/' | tr -d '`' | sed -E 's/, */|/g'); rg -n "$forbidden_pattern" docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review-report.md docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review-controller-verification.md | wc -l
0
```
| Metric | Observed | Pass condition | Result |
| --- | ---: | --- | --- |
| 금지 마케팅 표현 hit | 0 | 본문 hit 없음 | PASS |
### Gate 9 — Artifact
```bash
$ find docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review -type f | sort | wc -l
13
$ for f in docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/{evidence-matrix.md,per-file-findings.md,priority-recommendations.md,sed-proofs.md,adversarial-review.md,diagram-review.md,lanes/lane-01-target-review.md,lanes/lane-02-benchmark-patterns.md,process/pre-review-source.md}; do test -s "$f" || echo "MISSING $f"; done | wc -l
0
```
| Metric | Observed | Pass condition | Result |
| --- | ---: | --- | --- |
| current-run auxiliary artifacts / empty required artifacts | 13 / 0 | matrix, lanes, proofs, priority, adversarial, diagram, snapshot 존재 | PASS |
## Final Live Target Integrity
```text
$ python3 .claude/hooks/wiki_structure_lint.py --file raw/project-notes/ca-skeleton-frontend-operational-contract.md
PASS raw/project-notes/ca-skeleton-frontend-operational-contract.md
$ python3 .claude/hooks/wiki_structure_lint.py --file raw/project-notes/ca-skeleton-operational-contract.md
PASS raw/project-notes/ca-skeleton-operational-contract.md
$ xmllint --noout raw/diagrams/ca-skeleton-frontend/architecture-overview-2026-07-18.drawio raw/diagrams/ca-skeleton-frontend/architecture-deployment-2026-07-18.drawio
exit 0
```
| Check | Observed | Result |
| --- | --- | --- |
| FE-D unique / definitions / dangling | 25 / 25 / 0 | PASS |
| FE-OC unique / definitions / dangling | 26 / 26 / 0 | PASS |
| Registry / acceptance gate / runbook / NFR | 8 / 26 / 5 / 15 | PASS |
| FE-OC-002..025 primary owner | 각 ID exact-one, §2 owner와 일치 | PASS |
| §20 dependency slug | 27개 branch row의 선행 slug가 모두 §20 row에 존재 | PASS |
| Source Claim IDs | 13개가 각 source의 `Claims Extracted` row에 존재 | PASS |
| Frontmatter / Parent / Cluster / Sources | 7 / 0(root 면제) / 1 / 1 | PASS |
| Diagram stale marker | 0 | PASS |
| Independent semantic re-audit | latest target SHA `daa0941a8dde21e6`, actionable finding 0 | PASS |
## Traceability Note
사전 스냅샷의 검토 근거는 line quote와 sed proof로 보존했다. 후속 live target에서는 source-backed `FE-D003`~`FE-D008`이 6개 official source의 `Claims Extracted` heading과 13개 actual Claim ID로 연결되어 있다. `FE-D006`의 port ownership/non-duplication처럼 외부 source가 직접 증명하지 않는 설계 부분은 `project decision`으로 라벨링하며, project-local/inference decision도 그 경계를 유지해야 한다. `UNSUPPORTED_DECISION`, `BROKEN_CLAIM_REFERENCE`, `MISSING_CLAIMS_EXTRACTED` marker는 current-run artifact에서 0건이었다. 구현 repository와 실행 artifact가 없으므로 프로젝트 준비도는 계속 `NOT_READY`다.
@@ -0,0 +1,165 @@
# CA Skeleton Frontend Operational Contract 검토 보고서
**일자 / Date:** 2026-07-18
**범위 / Scope:** 2개 project-note (사전 검토 스냅샷 + benchmark)
**Verdict:** COMPLETE
**요청 언어 / User language:** ko
## 0. Source roots
해당 없음 / N/A. 모든 증거와 산출물은 이 워크스페이스 안에 있다. finding의 검토 범위는 보존된 `process/pre-review-source.md` 사전 스냅샷(509행)으로 고정했다. 교정된 live 원문의 구조·ID·owner·link·diagram integrity는 §7.2에서 별도로 검증했다.
## 1. 한눈 요약 / Executive Summary
사전 검토 대상과 backend benchmark를 모두 정독하여, frontend hub의 계약 완결성을 비교했다. Evidence Matrix는 2/2 행이며, 두 파일 모두 `READ_FULL`이다. 사전 스냅샷에서는 구현 증거 경계, hub minimum, CA port owner, retry 종료 의미, decision registry, auth session 경계, NFR/rollback에 관한 8개 간극을 찾았다. 적대 검토 후 1개는 유지하고 7개는 범위를 축소했으며, 삭제된 finding은 없다. 인용 검증은 16/16 command가 일치했고, 감사 자체의 Verdict는 `COMPLETE`이나 코드 repository와 실행 증거가 없는 프로젝트 준비도는 `NOT_READY`다.
## 2. Evidence Matrix
| Path | Status | Evidence | Extracted facts |
| --- | --- | --- | --- |
| `raw/project-notes/ca-skeleton-frontend-operational-contract.md` | READ_FULL | 사전 스냅샷 `process/pre-review-source.md:1-509` | hub 목표, branch 위임 경계, 계층 규칙, retry·auth·NFR·증거 등급을 검토했다. |
| `raw/project-notes/ca-skeleton-operational-contract.md` | READ_FULL | `raw/project-notes/ca-skeleton-operational-contract.md:26-48,796-916,962-985,1177-1227,1992-2115` | registry, failure matrix, acceptance gate, evidence grade, runbook, owner map을 비교 기준으로 사용했다. |
## 3. 커버리지 정합성 / Coverage Reconciliation
| 항목 | 값 |
| --- | ---: |
| (a) in-scope 파일 수 N | 2 |
| (b) §2 Evidence Matrix 행 수 M | 2 |
| (c) `READ_FULL` 또는 `READ_PARTIAL` 행 수 R | 2 |
| (d) §4 파일별 분석 하위섹션 수 P | 2 |
| (e) matrix 누락 NM | 0 |
| (f) 분석 깊이 미달 RP | 0 |
| 전체 finding 수 T | 8 |
| 인용 검증을 통과한 finding 수 G | 8 |
분석 깊이 미달 없음 — `RP = 0`. `NOT_READ` 파일과 `BLOCKED` 파일도 없다. controller가 재계산한 명령과 관측값은 [controller verification](2026-07-18-ca-skeleton-frontend-operational-contract-review-controller-verification.md)에 있다.
### 3-1. Verdict 결정 / Verdict Calculation
`M==N``2==2`, `P==R``2==2`, `G==T``8==8`이다. §5의 각 우선순위는 §4.1의 finding ID만 가리킨다. 따라서 감사의 범위·분석·인용 검증 조건은 충족하여 `Verdict: COMPLETE`다.
이는 구현 또는 운영 상태의 판정이 아니다. 사전 스냅샷은 코드 repository가 미생성이며 모든 구현 주장을 `planned`로 표시한다([사전 스냅샷:29](2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md)); 그러므로 프로젝트 준비도는 `NOT_READY`로 별도 유지한다.
## 4. 파일별 발견 사항 / Per-File Findings
### 4.1 `raw/project-notes/ca-skeleton-frontend-operational-contract.md` (Status: READ_FULL)
- **원래 목표 / Original goal:** 화면·기능이 달라도 API 호출, 실패 분류, 계층 의존 규칙, 계약 테스트를 같은 방식으로 다루는 frontend skeleton을 정의한다([사전 스냅샷:24](2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md)).
- **검토 항목 / Items reviewed:** 구현 증거 경계, hub/branch ownership, architecture artifact, decision registry, port ownership, retry, auth session 경계, NFR·rollback.
- **간극 / Gap:** 사전 스냅샷은 branch의 상세를 위임하지만 공통 minimum·owner·evidence closure가 충분히 고정되지 않은 부분이 있었다.
- **조치 / Action:** branch-local algorithm은 복제하지 않고 hub에 shared default, owner pointer, registry, evidence/rollback gate를 추가한다. 이 방향은 hub에 결정 메커니즘을 중복하지 말라는 원문 경계([사전 스냅샷:318](2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md))와 양립한다.
- **가정·반대 논거 / Assumption and counterargument:** 구현 repository가 아직 확인되지 않았고 hub가 후속 branch의 기준점이라는 가정에서만 다음 gaps가 성립한다. 모든 세부를 hub에 복제하면 branch SSOT와 drift하므로, 아래 조치는 project-wide minimum으로 한정한다.
- **구현·검증 / Implementation and verification:** 상세의 원문 인용, 무효 조건, 검증 명령은 [target lane](2026-07-18-ca-skeleton-frontend-operational-contract-review/lanes/lane-01-target-review.md)과 [sed proofs](2026-07-18-ca-skeleton-frontend-operational-contract-review/sed-proofs.md)에 보존했다.
| Finding ID | 최종 심각도 | 사전 스냅샷의 간극 | 적대 검토 후 필요한 조치 |
| --- | --- | --- | --- |
| L1-F01 | Medium | prospective contract가 구현 사실로 오인될 수 있음 | evidence grade·path·not-run 상태를 계약별로 표기 |
| L1-F02 | Medium | branch 위임 전에 project-wide minimum owner가 없음 | minimum default와 owner pointer만 hub에 유지 |
| L1-F03 | Medium | required architecture artifact가 미등록 상태 | overview는 등록하고 deployment는 필요할 때만 유지 |
| L1-F04 | Medium | decision count와 stable owner pointer가 정합하지 않음 | `FE-Dxxx` registry, status, revisit trigger 연결 |
| L1-F05 | High | port owner 표현이 상충하여 lint 기준이 모호함 | application-owned output port와 adapter implementation을 명시 |
| L1-F06 | Medium | timeout·retry의 cap, abort, terminal 의미가 없음 | cross-branch minimum과 owner branch 상세를 분리 |
| L1-F07 | Low | auth 위임과 guarded route 사이 session port가 없음 | token lifecycle을 복제하지 않는 얇은 session interface 추가 |
| L1-F08 | Medium | NFR evidence schema와 rollback owner가 없음 | provider-neutral evidence/rollback gate를 먼저 고정 |
### 4.2 `raw/project-notes/ca-skeleton-operational-contract.md` (Status: READ_FULL)
- **원래 목표 / Original goal:** backend operational contract의 invariant, registry, failure, acceptance, evidence schema를 기준으로 제공한다([benchmark lane](2026-07-18-ca-skeleton-frontend-operational-contract-review/lanes/lane-02-benchmark-patterns.md)).
- **검토 항목 / Items reviewed:** invariant-oriented goal, registry owner/test, scenario/failure matrix, Decision/Allowed/Forbidden/Test/Failure schema, acceptance gate, evidence grade tracker.
- **0-finding 정당화:** 이 파일은 변경 대상이 아닌 reference-only benchmark다. 위 6개 항목은 frontend에 그대로 복사할 구현 세부가 아니라 owner·failure semantics·acceptance/evidence closure라는 비교 schema를 제공한다. backend의 Gradle/Spring/JPA/Kafka/DB lifecycle과 HTTP status 정책은 frontend에 이식하지 않는다는 경계도 lane에 명시되어 있다.
- **검증 / Verification:** 비교 근거와 frontend adaptation은 [benchmark lane](2026-07-18-ca-skeleton-frontend-operational-contract-review/lanes/lane-02-benchmark-patterns.md)에 있다.
## 4-1. 적대 리뷰 결과 / Adversarial Review Results
적대 리뷰는 8개 finding 모두를 다뤘다. 전체 표는 [adversarial review](2026-07-18-ca-skeleton-frontend-operational-contract-review/adversarial-review.md)에 있다.
| Finding ID | 적대 리뷰 조치 | 컨트롤러 반영 |
| --- | --- | --- |
| L1-F01 | DOWNGRADE → Medium | Accept |
| L1-F02 | DOWNGRADE → Medium | Accept |
| L1-F03 | DOWNGRADE → Medium | Accept |
| L1-F04 | DOWNGRADE → Medium | Accept |
| L1-F05 | KEEP → High | Accept |
| L1-F06 | DOWNGRADE → Medium | Accept |
| L1-F07 | DOWNGRADE → Low | Accept |
| L1-F08 | DOWNGRADE → Medium | Accept |
- KEEP: 1
- DOWNGRADE: 7
- REJECT: 0
- Override: 0
## 5. 우선순위 권고 / Priority Recommendations
| 우선순위 | 연결 finding | 권고 액션 | 근거 | 검증 |
| --- | --- | --- | --- | --- |
| P0 | L1-F05 | application-owned output port, adapter implementation, composition-root wiring을 matrix와 lint fixture로 고정 | 사전 스냅샷:140,145 | forbidden-import fixture와 owner matrix |
| P1 | L1-F02, L1-F06 | `FE-OC` minimum defaults와 single-owner map을 추가하고 request 종료 의미는 owner branch에 연결 | 사전 스냅샷:318,213 | 각 contract row에 owner/default/test/failure 존재 |
| P1 | L1-F04 | stable `FE-Dxxx` registry에 status, revisit trigger, supersedes를 기록 | 사전 스냅샷:282,395 | unique ID와 count 검사 |
| P2 | L1-F08 | tool-neutral evidence schema와 immutable release rollback gate를 작성 | 사전 스냅샷:302,312 | Trigger→Recovery와 evidence artifact path 확인 |
| P2 | L1-F01, L1-F07 | evidence boundary와 `AuthSessionPort` owner를 명시하고 backend authorization을 대체하지 않음을 test로 고정 | 사전 스냅샷:29,158,337 | repository artifact가 생기기 전 readiness는 `NOT_READY` 유지 |
L1-F03의 사전 검토 간극은 이후 [diagram review](2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md)에서 두 draw.io artifact가 `PASS`로 기록되어, 신규 우선순위가 아니라 live 원문에 등록 상태를 다시 확인할 항목으로 §6에 둔다.
## 6. 후속 작업 / Follow-Up
- live target의 frontmatter와 embed가 두 diagram artifact를 가리키며 두 XML이 parse되는 것을 최종 확인했다. 이후 architecture 변경 시 같은 gate를 다시 실행한다.
- frontend repository URL, commit, package/lockfile, contract test·CI 결과를 계약 ID와 연결한다. 그 전까지 구현/로컬/운영 등급을 올리지 않는다.
- live target의 source-backed decision은 Gate 3 재측정 시점에 `FE-D003`~`FE-D008`에서 6개 official source와 13개 Claim ID를 연결했다. `FE-D006`의 port ownership/non-duplication처럼 source가 직접 증명하지 않는 부분은 `project decision`으로 라벨링한다. project-local/inference decision도 같은 경계 라벨을 유지해야 한다. 이 live mapping은 사전 검토 finding의 범위를 변경하지 않으며, 계산 결과는 controller의 Gate 3에 있다.
- provider가 정해진 뒤 deploy command, cache 정책, recovery artifact를 L1-F08 owner branch에 채운다.
## 7. 검증 / Verification
### 7.1 Self-grep proof
8개 finding의 original-goal/current-state 인용 16개는 [이미 기록된 16개 sed proof](2026-07-18-ca-skeleton-frontend-operational-contract-review/sed-proofs.md)로 대조했다. 예시는 다음과 같으며, 전체 명령과 실제 출력은 linked artifact에 있다.
```bash
$ sed -n '145p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
# Observed: | `application` (hooks / use-cases) | 화면 로직 · 상태 오케스트레이션 · use-case 훅 | React hooks + TanStack Query | adapters port (인터페이스) |
```
- V (기록된 quote command): 16
- P (출력 일치): 16
- C (라인 정정): 0
- D (finding 폐기): 0
- G (=P): 8 findings
- U (미검증 quote): 0
- §4 전체 quote 수: 16
### 7.2 실행한 검증 명령
| 명령 | 관측 결과 |
| --- | --- |
| `rg -c '^\\| \`raw/project-notes/[^\`]+\` \\| READ_FULL \\|' .../evidence-matrix.md` | 2 |
| `rg -c '^### L1-F[0-9]{2}:' .../lanes/lane-01-target-review.md` | 8 |
| `rg -c '^\\$ sed -n' .../sed-proofs.md` | 16 |
| `rg -c '^\\| L1-F[0-9]{2} \\|' .../adversarial-review.md` | 8 |
| `comm -3 <(findings) <(priority finding IDs) \| wc -l` | 0 |
| `python3 .claude/hooks/wiki_structure_lint.py --file <target>` / `--file <benchmark>` | 둘 다 `PASS` |
| `python3 .claude/hooks/wiki_structure_lint.py --all` | exit 1, PASS 700 / FAIL 255 — 기존 out-of-scope 문서 finding |
| `python3 .claude/hooks/wiki_structure_lint.py --stale` | exit 1, 49건 — 기존 out-of-scope stale finding |
| `xmllint --noout <overview.drawio> <deployment.drawio>` | exit 0 |
| FE-D / FE-OC unique·definition 대조 | 25/25 · 26/26, dangling 0 |
| §20 primary owner / dependency slug 대조 | FE-OC-002..025 exact-one, 미해결 dependency 0 |
| source Claim ID target 존재 여부 대조 | 13개 모두 source `Claims Extracted` row에 존재 |
| frontmatter / Parent / Cluster / Sources count | 7 / 0(root 면제) / 1 / 1 |
전체 9개 gate의 명령과 원시 관측값은 [controller verification](2026-07-18-ca-skeleton-frontend-operational-contract-review-controller-verification.md)에 있다. 이 응답에서 새로 작성한 wiki 문서는 0개이고, 새 audit report는 2개다.
## 8. Generated Artifacts
| 역할 | 경로 |
| --- | --- |
| Master report | `docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review-report.md` |
| Controller verification | `docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review-controller-verification.md` |
| Evidence matrix | `docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/evidence-matrix.md` |
| Finding detail | `docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/lanes/lane-01-target-review.md` |
| Benchmark detail | `docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/lanes/lane-02-benchmark-patterns.md` |
| Quote proofs | `docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/sed-proofs.md` |
| Adversarial review | `docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/adversarial-review.md` |
| Diagram review | `docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md` |
| Root adversarial summary | `docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review-adversarial-review.md` |
| 교정된 operational contract | `raw/project-notes/ca-skeleton-frontend-operational-contract.md` |
| Architecture diagrams | `raw/diagrams/ca-skeleton-frontend/architecture-overview-2026-07-18.drawio`, `raw/diagrams/ca-skeleton-frontend/architecture-deployment-2026-07-18.drawio` |
@@ -0,0 +1,13 @@
# Adversarial Review
| Finding ID | Original Claim | Strongest Counterargument | Evidence Needed To Falsify | Falsification Result | Verdict | Final Severity |
| --- | --- | --- | --- | --- | --- | --- |
| L1-F01 | 구현 증거가 없어 backlog에 가깝다 | 규범 계약은 구현보다 먼저 존재할 수 있고 문서도 `planned` 승격 경계를 명시한다. 로컬 미발견은 원격 부재를 입증하지 않으므로 "계약이 아님"까지 확대할 수 없다. | repository URL·commit·test artifact와 계약 매핑 | evidence boundary는 유효, 계약 부정은 과함 | DOWNGRADE — prospective contract로 재서술 | Medium |
| L1-F02 | hub에 contract mechanism이 남지 않았다 | 모든 algorithm을 hub에 복제하면 branch SSOT와 drift하며, 기존 문서에도 envelope·성능 default·branch 목표 일부가 있다. 다만 branch 미생성 상태라 minimum owner는 필요하다. | 생성된 branch의 stable owner/default/test와 hub owner map | 전면 중앙화는 기각, minimum만 유지 | DOWNGRADE — project-wide minimum으로 축소 | Medium |
| L1-F03 | architecture/deployment diagram이 필수인데 없다 | architecture는 필수지만 deployment는 template에서 선택이고 누락을 `needs-diagram`으로 공개했다. readiness 결함이지 숨은 구조 결함은 아니다. | 실제 artifact·embed·frontmatter·review | architecture 요구만 유지 | DOWNGRADE — deployment 무조건 의무 제거 | Medium |
| L1-F04 | ID/lifecycle/count 정합성이 없다 | 모든 ADR lifecycle을 hub에 복제할 필요는 없지만 실제 행 수와 "6개" 문구의 불일치, owner pointer 부재는 반증되지 않는다. | stable D-ID → owner branch mapping과 count check | 경량 registry만 요구 | DOWNGRADE — owner pointer와 count 수정 | Medium |
| L1-F05 | CA port ownership이 모호하다 | `adapters port`가 adapter가 구현하는 output port를 뜻했을 수 있고 실제 import 위반은 아직 UNVERIFIED다. 그러나 문서만으로 interface owner를 판정할 수 없다는 문제는 남는다. | directory/import matrix와 lint fixture | 문서 모호성 확인 | KEEP — implementation 전 정정 필요 | High |
| L1-F06 | timeout/retry/error 의미가 없다 | malformed payload와 일부 error path는 이미 있으며 모든 status·jitter 세부를 hub에 두면 중복이다. 공용 client의 cap·abort·unsafe mutation·terminal semantics만 cross-branch minimum이다. | owner branch 상세 matrix·tests와 hub minimum | 범위를 minimum으로 축소 | DOWNGRADE — retry 종료 의미 중심 | Medium |
| L1-F07 | auth ownership gap이 있다 | named 위임처는 이미 있고 route guard는 security boundary가 아니라 UX일 수 있다. token lifecycle을 skeleton으로 가져오면 범위 확장이다. | injected session port와 backend authz 비대체 test | session-state interface만 누락 | DOWNGRADE — 얇은 integration port만 | Low |
| L1-F08 | NFR/runbook이 없다 | provider 미정인데 vendor command와 dashboard를 쓰면 사실을 발명한다. threshold·degrade 방향은 이미 있으므로 tool-neutral evidence/rollback owner만 먼저 고정해야 한다. | provider 확정 후 command·artifact·recovery evidence | exact command 요구는 연기 | DOWNGRADE — evidence schema와 rollback gate | Medium |
@@ -0,0 +1,19 @@
# Diagram Review
**Reviewer:** `wiki-diagram-reviewer`
**Standard:** `rules/diagram-standards.md` v2
**Final verdict:** PASS
| Diagram | Vertex | Edge | Boundary | Callout | Legend | Score | Verdict |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| `raw/diagrams/ca-skeleton-frontend/architecture-overview-2026-07-18.drawio` | 5 | 6 | 0 | 0 | body caption 1 | 100 | PASS |
| `raw/diagrams/ca-skeleton-frontend/architecture-deployment-2026-07-18.drawio` | 4 | 4 | 0 | 0 | 2 | 100 | PASS |
## Verified alignment
- Overview는 compile-time dependency ownership 한 질문만 다룬다.
- Application이 output port를 소유하고 Adapter가 Application을 향해 구현한다.
- Deployment는 immutable assets와 mutable `/config.json` 전달 한 질문만 다룬다.
- `/config.json`, cache policy, release meaning은 project-note §6·§12와 일치한다.
- 두 XML 모두 well-formed이고 label/element/color budget을 통과했다.
@@ -0,0 +1,15 @@
# Evidence Matrix
| Path | Status | Evidence | Extracted facts |
| --- | --- | --- | --- |
| `raw/project-notes/ca-skeleton-frontend-operational-contract.md` | READ_FULL | pre-review L1-509; snapshot: `process/pre-review-source.md` | 구조 린터는 통과하지만 구현 repository가 없고, 계약 세부·증거·검증·rollback은 대부분 future branch로 위임됨 |
| `raw/project-notes/ca-skeleton-operational-contract.md` | READ_FULL | L1-2226 | registry, failure matrix, acceptance gate, evidence grade, runbook, owner map을 반복 가능한 schema로 제공하는 비교 기준 |
## Scope reconciliation
- N = 2
- Matrix rows M = 2
- READ_FULL/READ_PARTIAL R = 2
- Missing paths = 0
- Duplicate paths = 0
@@ -0,0 +1,159 @@
## Lane Inventory
| Path | Status | Evidence Lines | Extracted Facts |
| --- | --- | --- | --- |
| `raw/project-notes/ca-skeleton-frontend-operational-contract.md` | READ_FULL | L1-509, preserved at `../process/pre-review-source.md` | prospective contract 상태, 20개 branch backlog, 구현·diagram·검증 artifact 부재 |
## Evidence Matrix
| Path | Status | Evidence | Extracted facts |
| --- | --- | --- | --- |
| `raw/project-notes/ca-skeleton-frontend-operational-contract.md` | READ_FULL | L1-509 | target의 구조, 계약 위임, 의존 규칙, 실패·NFR·증거 경계를 확인 |
## Goal → Assumption → Action
- Goal: backend 기준 문서와 같은 강제력을 갖는 frontend prospective operational contract.
- Assumption: repository가 아직 없더라도 project-wide minimum default와 owner는 hub에서 고정할 수 있다.
- Action: branch 세부 구현을 복제하지 않고 stable ID, minimum default, failure vocabulary, registry owner, acceptance/evidence schema를 hub에 추가한다.
- Counterargument: 모든 구현 세부를 hub에 쓰면 branch SSOT와 drift한다. 따라서 branch-local algorithm·library configuration은 owner branch로 위임한다.
### L1-F01: 검증되지 않은 prospective contract
- Source file: `raw/project-notes/ca-skeleton-frontend-operational-contract.md`
- Original goal quote: "도메인/비즈니스 로직을 제거한 **Clean Architecture 프론트엔드 skeleton**. 어떤 화면·기능을 붙여도 (1) 같은 방식으로 백엔드 API 를 호출하고, (2) 같은 방식으로 실패를 분류·표시하고, (3) 같은 계층 의존 규칙을 강제하고, (4) 같은 계약 테스트로 깨짐을 감지하는 React SPA 뼈대."
- Original goal line: `process/pre-review-source.md:24`
- Source quote: "메인: `<미생성 — Vite + React + Tailwind SPA 레포 예정>` · `NO_GROUND_TRUTH`: 아직 코드 레포가 없어 본 hub 의 모든 구현 주장은 `planned` 등급이다(§9). `actually-implemented` 승격은 레포 생성 후 `src/` grep 으로만."
- Source line: `process/pre-review-source.md:29`
- Severity: Medium
- Claim: 문서는 계약이 될 수 있으나 현재는 실행·검증되지 않은 prospective contract다.
- Assumptions: 접근 가능한 로컬 범위에 구현 repository가 없고 원격 repository는 UNVERIFIED다.
- Failure mode: 현재형 계약 문장이 부분 인용되면 구현 사실로 오인된다.
- Falsification condition: repository URL·commit·test artifact가 계약 ID와 연결되면 이 finding은 해소된다.
- Counterargument: 규범 계약은 구현보다 먼저 작성될 수 있으며 target도 `planned` 경계를 명시한다.
- Recommendation: 문서 전체에 evidence boundary를 두고 계약별 grade·evidence path·not-run 상태를 기록한다.
- Verification command: `sed -n '29p' 'docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md'`
- Verification result: " - 메인: `<미생성 — Vite + React + Tailwind SPA 레포 예정>` · `NO_GROUND_TRUTH`: 아직 코드 레포가 없어 본 hub 의 모든 구현 주장은 `planned` 등급이다(§9). `actually-implemented` 승격은 레포 생성 후 `src/` grep 으로만."
### L1-F02: branch 위임 전 project-wide minimum 부재
- Source file: `raw/project-notes/ca-skeleton-frontend-operational-contract.md`
- Original goal quote: "본 문서는 **프로젝트의 최상위 hub**. 프로젝트 전체 컨텍스트 / 문제 정의 / 시스템 아키텍처 / 핵심 시퀀스가 여기에 집중."
- Original goal line: `process/pre-review-source.md:17`
- Source quote: "각 branch 의 *결정 내용·메커니즘은 여기 적지 않는다* (SSOT 이중화 방지) — 그건 `/branch <slug>` 로 생성 후 `/branch-spec` 가 깊게 채운다."
- Source line: `process/pre-review-source.md:318`
- Severity: Medium
- Claim: branch가 아직 없으므로 owner·minimum default·acceptance gate까지 위임하면 안전한 handoff가 성립하지 않는다.
- Assumptions: §8.1의 branch는 아직 생성되지 않았고 target이 후속 branch의 기준점이다.
- Failure mode: 서로 다른 branch가 timeout, retry, error, config의 기본값을 각각 정의해 충돌한다.
- Falsification condition: 모든 branch가 생성되고 stable owner와 역링크가 있으며 hub owner map이 존재하면 해소된다.
- Counterargument: algorithm과 library option을 hub에 복제하면 branch SSOT와 drift한다.
- Recommendation: hub에는 project-wide minimum과 owner pointer만 두고 세부 algorithm은 branch로 위임한다.
- Verification command: `sed -n '318p' 'docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md'`
- Verification result: "> **`/project-spec` 가 채우는 핸드오프 섹션.** 이 hub 에서 깊은 조사로 도출된 *자식 branch 의 네이밍과 달성 목표 조건만* 적는다. 각 branch 의 *결정 내용·메커니즘은 여기 적지 않는다* (SSOT 이중화 방지) — 그건 `/branch <slug>` 로 생성 후 `/branch-spec` 가 깊게 채운다."
### L1-F03: required architecture diagram 미등록
- Source file: `raw/project-notes/ca-skeleton-frontend-operational-contract.md`
- Original goal quote: "**필수 섹션.** 아키텍처 다이어그램이 없는 project-note 는 hub 역할을 못 함."
- Original goal line: `process/pre-review-source.md:62`
- Source quote: "**`needs-diagram` (`DIAGRAM_PENDING_USER`)** — 아키텍처 `.drawio``/project-spec` 가 자동 생성할 수 없다."
- Source line: `process/pre-review-source.md:115`
- Severity: Medium
- Claim: 최소 architecture diagram은 project template의 hub gate인데 pre-review 문서에는 활성 artifact가 없다.
- Assumptions: static dependency와 runtime call을 그림으로 구분해야 review가 가능하다.
- Failure mode: CA dependency direction과 runtime HTTP 흐름을 같은 화살표로 해석한다.
- Falsification condition: 실제 draw.io 파일, frontmatter 등록, embed, reviewer score가 존재하면 해소된다.
- Counterargument: deployment diagram은 template에서 선택이고 누락 상태도 숨기지 않았다.
- Recommendation: overview를 필수로 생성하고 deployment는 topology 설명 가치가 있을 때 유지한다.
- Verification command: `sed -n '115p' 'docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md'`
- Verification result: "> ⏳ **`needs-diagram` (`DIAGRAM_PENDING_USER`)** — 아키텍처 `.drawio``/project-spec` 가 자동 생성할 수 없다. 아래 대상 파일을 사용자가 직접 작성한 뒤, 백틱을 풀어 활성 임베드(`![[...]]`)로 바꾼다. 그때까지는 **inline code span** 이라 구조 린터의 `BROKEN_LINK` 를 발생시키지 않는다. 이 항목은 readiness 게이트에서 **Blocking 이 아니라 Should-fix** 로 취급되어 판정이 `Ready-pending-user` 가 된다."
### L1-F04: stable decision owner와 내부 count 정합성 부재
- Source file: `raw/project-notes/ca-skeleton-frontend-operational-contract.md`
- Original goal quote: "주요 기술 선택과 이유. 트레이드오프 + 근거 자료 link 필수."
- Original goal line: `process/pre-review-source.md:282`
- Source quote: "| 기술 결정 | `documented-only` | §6 6개 결정이 official 근거 보유 — 단 *결정*일 뿐 구현 아님 |"
- Source line: `process/pre-review-source.md:395`
- Severity: Medium
- Claim: §6의 실제 선택 행 수와 §9의 6개 집계가 불일치하고 stable owner pointer가 없다.
- Assumptions: 다수 branch가 같은 stack·config 결정을 소비한다.
- Failure mode: 한 결정이 변경돼도 영향 branch와 superseded 결정을 추적하지 못한다.
- Falsification condition: 각 decision이 stable ID·owner·status·revisit trigger와 연결되고 count 검사에 통과하면 해소된다.
- Counterargument: 모든 ADR lifecycle metadata를 hub에 복제할 필요는 없다.
- Recommendation: 경량 `FE-Dxxx` registry와 owner pointer를 두고 branch-local evidence는 link로 유지한다.
- Verification command: `sed -n '395p' 'docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md'`
- Verification result: "| 기술 결정 | `documented-only` | §6 6개 결정이 official 근거 보유 — 단 *결정*일 뿐 구현 아님 |"
### L1-F05: port ownership 모호성
- Source file: `raw/project-notes/ca-skeleton-frontend-operational-contract.md`
- Original goal quote: "의존 방향은 항상 바깥→안(presentation → application → domain). adapters 는 domain 이 정의한 port 를 구현해 바깥에서 주입된다."
- Original goal line: `process/pre-review-source.md:140`
- Source quote: "| `application` (hooks / use-cases) | 화면 로직 · 상태 오케스트레이션 · use-case 훅 | React hooks + TanStack Query | adapters port (인터페이스) |"
- Source line: `process/pre-review-source.md:145`
- Severity: High
- Claim: prose의 domain-owned port와 표의 adapters port가 함께 존재해 application import 방향을 하나로 판정할 수 없다.
- Assumptions: 이 hub가 dependency lint rule의 기준이 된다.
- Failure mode: application이 adapter module의 interface를 import해 inward dependency rule을 위반한다.
- Falsification condition: application-owned output port와 adapter implementation, composition-root wiring이 matrix와 lint fixture로 고정되면 해소된다.
- Counterargument: `adapters port`가 "adapter가 구현하는 output port"를 뜻했을 수 있고 실제 코드 위반은 UNVERIFIED다.
- Recommendation: port owner를 application으로 고정하고 adapter는 구현만, composition root만 concrete adapter를 안다.
- Verification command: `sed -n '145p' 'docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md'`
- Verification result: "| `application` (hooks / use-cases) | 화면 로직 · 상태 오케스트레이션 · use-case 훅 | React hooks + TanStack Query | adapters port (인터페이스) |"
### L1-F06: cross-branch timeout·retry 종료 의미 부재
- Source file: `raw/project-notes/ca-skeleton-frontend-operational-contract.md`
- Original goal quote: "도메인 없는 **sample feature slice** 화면이 `api-client → zod 검증 → error boundary → async 4-state → CA 의존 규칙`을 모두 관통하고, **계약 위반 시 실패하는 contract test** 가 CI 에서 green."
- Original goal line: `process/pre-review-source.md:54`
- Source quote: "Q-->>Q: retryable=true 면 exponential backoff 재시도"
- Source line: `process/pre-review-source.md:213`
- Severity: Medium
- Claim: retry 여부만 있고 timeout owner, retry cap, abort, terminal handoff, unsafe mutation default가 없다.
- Assumptions: 공용 `apiClient`와 Query wrapper가 여러 feature에서 재사용된다.
- Failure mode: cancellation 뒤 retry가 계속되거나 non-idempotent mutation이 중복 실행된다.
- Falsification condition: owner branch에 request class별 default와 contract test가 있고 hub가 minimum을 연결하면 해소된다.
- Counterargument: `Retry-After`·jitter algorithm 등 모든 세부를 hub에 두면 중복이다.
- Recommendation: hub에는 cross-branch minimum만 고정하고 status별 상세 mapping은 api-client owner branch로 위임한다.
- Verification command: `sed -n '213p' 'docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md'`
- Verification result: " Q-->>Q: retryable=true 면 exponential backoff 재시도"
### L1-F07: auth 위임과 route guard 사이 session port 누락
- Source file: `raw/project-notes/ca-skeleton-frontend-operational-contract.md`
- Original goal quote: "토큰 획득/저장/refresh 는 본 skeleton 범위 밖 → [[raw/project-notes/keycloak-patterns-overview]] 로 위임."
- Original goal line: `process/pre-review-source.md:158`
- Source quote: "| `feature-routing-navigation-guard-contract` | route 구조 + guarded route(비인증 redirect)·error/loading route·404 정의, 네비게이션 test | P2 | - |"
- Source line: `process/pre-review-source.md:337`
- Severity: Low
- Claim: token lifecycle은 범위 밖이지만 route guard가 소비할 session-state port와 owner dependency가 없다.
- Assumptions: route guard는 backend authorization을 대체하지 않고 탐색 UX만 담당한다.
- Failure mode: routing code가 token storage 구현을 직접 참조하거나 redirect loop를 만든다.
- Falsification condition: 주입형 session port와 401 mapping, backend authz 비대체 규칙이 존재하면 해소된다.
- Counterargument: named 위임처 `keycloak-patterns-overview`는 이미 존재한다.
- Recommendation: 얇은 `AuthSessionPort` 계약만 소유하고 token 획득·저장·refresh는 계속 위임한다.
- Verification command: `sed -n '337p' 'docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md'`
- Verification result: "| `feature-routing-navigation-guard-contract` | route 구조 + guarded route(비인증 redirect)·error/loading route·404 정의, 네비게이션 test | P2 | - |"
### L1-F08: NFR evidence schema와 rollback owner 부재
- Source file: `raw/project-notes/ca-skeleton-frontend-operational-contract.md`
- Original goal quote: "측정 가능한 비기능 목표. 없으면 명시적으로 \"해당 없음\"."
- Original goal line: `process/pre-review-source.md:302`
- Source quote: "- **재해 복구 / DR**: 정적 자산 재배포로 복구(RTO 낮음), 클라이언트 무상태(RPO 무관)."
- Source line: `process/pre-review-source.md:312`
- Severity: Medium
- Claim: `RTO 낮음`은 측정 기준이 아니며 미정 hosting 환경에서도 tool-neutral evidence·rollback owner는 필요하다.
- Assumptions: 문서가 CI, immutable asset 배포, cache와 release recovery를 project goal로 둔다.
- Failure mode: bad asset가 배포됐을 때 이전 release 선택, cache 처리, recovery verification 순서가 결정되지 않는다.
- Falsification condition: hosting 확정 후 command·artifact·owner·recovery evidence가 runbook에 연결되면 해소된다.
- Counterargument: provider 미정 상태에서 vendor command나 dashboard URL을 쓰면 구현 사실을 발명한다.
- Recommendation: hub에는 evidence schema와 provider-neutral rollback gate를 두고 exact provider command는 deploy branch로 위임한다.
- Verification command: `sed -n '312p' 'docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md'`
- Verification result: "- **재해 복구 / DR**: 정적 자산 재배포로 복구(RTO 낮음), 클라이언트 무상태(RPO 무관)."
## §7.1 Self-grep proof
- 8개 finding의 cited current-state quote는 `../sed-proofs.md`의 16개 command 중 대응 command로 검증했다.
- V = 16 quote commands, P = 16, C = 0, D = 0, U = 0.
@@ -0,0 +1,32 @@
## Lane Inventory
| Path | Status | Evidence Lines | Extracted Facts |
| --- | --- | --- | --- |
| `raw/project-notes/ca-skeleton-operational-contract.md` | READ_FULL | L1-2226 | frontend가 재사용할 schema와 backend 고유 세부를 분리 |
## Evidence Matrix
| Path | Status | Evidence | Extracted facts |
| --- | --- | --- | --- |
| `raw/project-notes/ca-skeleton-operational-contract.md` | READ_FULL | L26-48, L796-916, L962-985, L1177-1227, L1992-2115 | goal/non-goal, registry, failure matrix, work-item contract, acceptance/readiness, evidence grade tracker |
## Reusable benchmark patterns
| Pattern | Evidence | Frontend adaptation |
| --- | --- | --- |
| invariant-oriented goal | L26-38 | 어떤 feature도 같은 API/error/state/dependency contract를 사용 |
| registry row + owner + test | L796-897 | route/env/storage/error/query-key/telemetry/release registry |
| scenario + expected failure | L899-916 | happy/degraded/recovery/removal fixture matrix |
| Decision/Allowed/Forbidden/Test/Failure | L962-985 | `FE-OC` contract schema |
| universal acceptance gate | L1177-1200 | branch completion 전에 10개 binary 질문 |
| evidence-grade tracker | L1992-2005, L2110-2115 | planned → implemented → local → prod evidence path |
## Do not copy
- Gradle/Spring/JPA/Kafka/DB transaction, JVM/container lifecycle, backend-owned response status 정책.
- Frontend는 envelope를 소비·검증하고 browser/runtime/cache/chunk/config/render/storage 실패를 소유한다.
## Counterargument
기준 문서의 분량과 registry row 수를 그대로 복제하면 frontend prospective contract에 불필요한 무게가 생긴다. 비교 기준은 줄 수가 아니라 stable owner, failure semantics, acceptance/evidence closure다.
@@ -0,0 +1,7 @@
# Per-File Findings Index
| Path | Status | Finding IDs | Canonical detail | Sed proof |
| --- | --- | --- | --- | --- |
| `raw/project-notes/ca-skeleton-frontend-operational-contract.md` | READ_FULL | L1-F01, L1-F02, L1-F03, L1-F04, L1-F05, L1-F06, L1-F07, L1-F08 | `lanes/lane-01-target-review.md` | `sed-proofs.md` |
| `raw/project-notes/ca-skeleton-operational-contract.md` | READ_FULL | reference-only; 0 target findings | `lanes/lane-02-benchmark-patterns.md` | N/A |
@@ -0,0 +1,11 @@
# Priority Recommendations
| Priority | Finding | Why now | Concrete change | Verification |
| --- | --- | --- | --- | --- |
| 1 | L1-F05 | dependency lint의 기준 문장이 모호함 | application-owned output port, adapter implementation, composition-root wiring matrix | forbidden-import fixture가 실패하는 planned command + 이후 artifact |
| 2 | L1-F02, L1-F06 | branch가 아직 없어 cross-branch default owner가 없음 | `FE-OC` minimum default와 single-owner map 추가 | contract index 모든 row에 owner/default/test/failure 존재 |
| 3 | L1-F04 | decision count와 owner pointer가 drift함 | `FE-Dxxx` stable registry + status/revisit/supersedes | unique ID/count grep |
| 4 | L1-F08 | NFR·rollback이 측정/복구 closure로 연결되지 않음 | tool-neutral evidence schema + immutable release rollback runbook | runbook Trigger→Recovery와 evidence artifact path 존재 |
| 5 | L1-F03 | project-note architecture gate 미충족 | planned overview/deployment draw.io 생성·등록·독립 review | XML parse + reviewer score |
| 6 | L1-F01, L1-F07 | 현재형 문장과 auth 위임 경계가 부분 인용 시 오해 가능 | evidence boundary + `AuthSessionPort` owner, backend authz 비대체 규칙 | readiness score remains NOT_READY until code evidence |
@@ -0,0 +1,8 @@
# Implementation Plan
1. 대상·기준 문서와 로컬 구현 evidence를 분리 검토한다.
2. 8 findings를 적대 리뷰해 범위를 project-wide minimum으로 축소한다.
3. target의 stable decision/contract ID, owner registries, failure semantics, acceptance/evidence/runbook을 보강한다.
4. planned architecture/deployment draw.io를 생성하고 독립 review한다.
5. 구조 lint, link, XML, ID/count, 금지어, diff를 검증한다.
@@ -0,0 +1,509 @@
---
title: CA Skeleton Frontend Operational Contract
source_type: project-note
status: draft
confidence: unknown
tags: [project-note, ca-skeleton, frontend, clean-architecture]
related_projects: [ca-skeleton-frontend]
last_reviewed: 2026-07-18
diagrams: []
architecture_review:
status_label: active
---
# CA Skeleton Frontend Operational Contract
> Layer: `raw/project-notes/` (primary, hub) → `/ingest` 후 검증된 사실은 `wiki/projects/` 로 추출.
> 본 문서는 **프로젝트의 최상위 hub**. 프로젝트 전체 컨텍스트 / 문제 정의 / 시스템 아키텍처 / 핵심 시퀀스가 여기에 집중. 모든 branch / errors / interviews / lectures / job-postings / blog-topics / sources 가 본 문서로 upward link.
> `status_label`: `active` | `paused` | `completed` | `archived`
## 1. 프로젝트 개요 / Overview
> 외부인이 1분 안에 "이게 뭐 하는 프로젝트인가" 이해할 수 있어야 함.
- **한 줄 요약**: 도메인/비즈니스 로직을 제거한 **Clean Architecture 프론트엔드 skeleton**. 어떤 화면·기능을 붙여도 (1) 같은 방식으로 백엔드 API 를 호출하고, (2) 같은 방식으로 실패를 분류·표시하고, (3) 같은 계층 의존 규칙을 강제하고, (4) 같은 계약 테스트로 깨짐을 감지하는 React SPA 뼈대. 백엔드 [[raw/project-notes/ca-skeleton-operational-contract]] 의 프론트엔드 대응물이며, 개인 학습·포트폴리오·재사용 starter 로 쓴다.
- **기간**: 2026-07-18 ~ in-progress (**기획 단계** — 코드 미착수, `/branch-spec` 로 계약별 상세 설계 예정)
- **현재 상태**: `active`
- **나의 역할 / Role**: 설계자 / 구현자 (개인 프로젝트)
- **저장소 / Repo**:
- 메인: `<미생성 — Vite + React + Tailwind SPA 레포 예정>` · `NO_GROUND_TRUTH`: 아직 코드 레포가 없어 본 hub 의 모든 구현 주장은 `planned` 등급이다(§9). `actually-implemented` 승격은 레포 생성 후 `src/` grep 으로만.
- 부속: —
## 2. 문제 정의 / Problem
> 추상화 금지. 구체 시나리오·수치로.
### 2.1 현재 상태의 문제
- 문제 1: 도메인 없는 React 프로젝트를 새로 시작할 때마다 **API 호출 래퍼·에러 분류·로딩/빈/에러 UI 상태·응답 검증을 매번 다르게 재구현**한다. 결과적으로 화면마다 실패 처리가 제각각이라, "이 화면은 500 을 어떻게 보여주지?"를 화면 수만큼 되묻게 된다.
- 문제 2: **JS(무타입) 환경**에서 백엔드 응답 스키마가 바뀌면 컴파일타임이 아니라 런타임에서야 깨짐이 드러난다. 경계에서 검증 계약이 없으면 `undefined` 접근 에러가 UI 트리 깊숙한 곳에서 터져, 원인이 API 계약 변경임을 추적하기 어렵다.
- 문제 3: "Clean Architecture"를 표방해도 **강제 장치가 없으면** presentation 컴포넌트가 API DTO 를 직접 만지고 의존 방향이 역전된다(백엔드의 ArchUnit 대응물이 프론트엔드엔 부재). 리뷰어의 눈에만 의존하면 시간이 지나며 반드시 새어나간다.
### 2.2 왜 지금 해결해야 하는가
- 트리거 (왜 지금): 백엔드 [[raw/project-notes/ca-skeleton-operational-contract]] 운영 계약을 구축 완료했다. 같은 "도메인 제거 + 운영 계약 우선" 철학을 프론트엔드에 **대칭 적용**할 시점 — 백엔드가 응답 envelope·error category·trace 를 계약화했으므로 프론트엔드도 그 계약의 소비 규약을 고정해야 짝이 맞는다.
- 비용 (해결 안 했을 때 손실): 계약 없이 화면부터 만들면, 나중에 횡단 관심사(에러 분류·로깅·검증·접근성·성능 예산)를 **소급 적용하는 비용이 화면 수에 비례해 폭증**한다. 특히 무타입 JS 라 런타임 깨짐이 QA/운영 단계로 밀린다.
- 기회 (해결 시 가치): 도메인 없는 **재사용 skeleton 1벌** → 이후 모든 프론트엔드 프로젝트의 출발점 + 면접/포트폴리오에서 "계층·계약·강제 장치를 어떻게 설계했는가"를 증거로 말할 수 있는 자산.
### 2.3 성공 기준 / Success criteria
> 측정 가능해야 함. "잘 동작한다" 같은 모호 표현 금지.
> 아래 임계 수치는 **초기 기본값(default)** 이며, 각 계약 branch(`/branch-spec`)에서 측정·조정한다. "잘 동작한다" 류 추상 표현을 쓰지 않는 것이 목적.
- 기준 1 (계약 관통): 도메인 없는 **sample feature slice** 화면이 `api-client → zod 검증 → error boundary → async 4-state → CA 의존 규칙`을 모두 관통하고, **계약 위반 시 실패하는 contract test** 가 CI 에서 green.
- 기준 2 (의존 규칙 강제): CA 계층 의존 규칙 lint(ESLint boundaries / dependency-cruiser) **위반 0건**. `presentation → infrastructure` 직접 의존 등 역방향 import 발생 시 **빌드 실패**.
- 기준 3 (4-state 강제): 모든 async 화면 표면이 `loading / empty / error / success` **4상태를 렌더**. sample slice 에 4상태 각각의 component test(RTL) 존재.
- 기준 4 (성능 예산): 프로덕션 초기 JS 번들 **< 200KB gzip**(default, route-level code splitting 후), web-vitals **LCP < 2.5s · CLS < 0.1 · INP < 200ms** 임계를 CI 게이트가 검사.
- 기준 5 (재사용성): 새 화면을 만들 때 개발자가 API 에러/로딩/검증을 **재구현하지 않고** 계약 훅·컴포넌트(`useApiQuery`, `<AsyncBoundary>`, `apiClient`)를 재사용 — sample slice 복제로 새 화면 골격이 나옴.
## 3. 시스템 아키텍처 / System architecture
> **필수 섹션.** 아키텍처 다이어그램이 없는 project-note 는 hub 역할을 못 함.
### Diagram tool 선택 — 엄격한 분리
| 다이어그램 종류 | 도구 | 이유 |
|---|---|---|
| **시스템 아키텍처 / 컴포넌트 구성도 / 배포 토폴로지 / 데이터 흐름 (정적 구조)** | **draw.io XML (`.drawio` 또는 `.drawio.svg`)** | 자유 배치 / 시각적 그룹화 / 신뢰 경계 / 색상 코딩 / Obsidian draw.io 플러그인 native 편집 |
| **시퀀스 다이어그램** | **Mermaid `sequenceDiagram`** | 텍스트 기반·git diff 친화, 시간축 표현에 최적 |
| **ER 다이어그램 (데이터 모델, 선택)** | **Mermaid `erDiagram`** | 텍스트 기반·관계 카디널리티 표기 직관적 |
| 작은 결정 트리 / 짧은 플로우차트 | Mermaid `flowchart` 도 허용 (작은 규모 한정) | 시퀀스가 아닌 단순 분기 |
**금지**:
- 시스템 아키텍처를 Mermaid `graph TD`/`graph LR` 로 작성 — 시각 표현력 부족, draw.io 사용 의무
- 시퀀스 흐름을 draw.io 로 작성 — 시간축 표현 불편, Mermaid 사용 의무
### Diagram 컨퍼런스급 표준 (필수 정독)
> [[rules/diagram-standards]] 에서 **컨퍼런스급(Toss SLASH / Kakao if(dev) / Naver DEVIEW 수준) 다이어그램 표준 v2 (minimalist-first)** 를 정의한다. 본 template 본문에 별도 기준을 두지 않는다 — 항상 `rules/diagram-standards.md` 를 정독.
>
> **핵심 원칙: "적을수록 좋다" (Less is more)**. 정보를 다이어그램에 몰아넣으면 청중이 어디부터 봐야 할지 모른다.
>
> v2 의 요약 (전체는 rules 정독):
>
> - **요소 수 상한** (HARD): Vertex ≤ 10 / Edge ≤ 8 / Callout ≤ 1 / Boundary group ≤ 3 / Legend 항목 ≤ 6 / 색상 ≤ 4
> - **박스 라벨 ≤ 2줄**, **화살표 라벨 ≤ 5단어**
> - **80% 회색/흑백 + 강조색 ≤ 2** (color salad 금지)
> - **Boundary 는 정보 있을 때만** (장식용 boundary 금지)
> - **Legend 는 표준 컨벤션이면 생략** (점선=외부 / cylinder=DB / 실선=동기 / 점선=비동기 는 legend 불필요)
> - **Callout 1개** (있을 때만) — 비자명한 함정·결정에만
> - **출처 wikilink 는 본문/캡션에**, 다이어그램 안에 박지 말 것
> - **스케일 어노테이션 (QPS/latency)** 은 다이어그램의 질문이 *성능* 일 때만
> - **5초 룰 + 30초 룰** 통과
>
> **8항 self-check checklist** ([[rules/diagram-standards]] §14) 를 모두 ✓ 해야 컨퍼런스 발표 가능 수준. 1개라도 미달 → 분할 또는 단순화.
>
> `wiki-diagram-reviewer` agent 가 위 기준으로 `.drawio` XML 을 grep-카운트 후 0~100 점수 부여, ≥95 PASS.
### 3.1 아키텍처 다이어그램 (draw.io XML)
> 컴포넌트 구성도. **저장 경로**: `raw/diagrams/<project-slug>/` 하위에 `.drawio` 또는 `.drawio.svg` 형식으로 저장. Obsidian draw.io 플러그인으로 더블클릭 편집.
>
> **파일 명명 규약**: `architecture-{viewpoint}-YYYY-MM-DD.drawio.svg`
> 예: `architecture-overview-2026-05-25.drawio.svg`, `architecture-deployment-2026-05-25.drawio.svg`, `architecture-data-flow-2026-05-25.drawio.svg`
>
> **임베드 작성 방법**: 아래 code block 형식을 참고해 실제 파일명을 채워 wikilink 작성. **placeholder 그대로 두지 말 것** — Obsidian이 placeholder를 파일명으로 채택해 root에 orphan 파일을 생성함.
```markdown
실제 사용 예 (drawio 파일 생성 후 placeholder 부분을 실제 값으로 치환):
![[raw/diagrams/my-project/architecture-overview-2026-05-25.drawio.svg]]
```
<!-- 본 템플릿 사용자: 위 code block 안의 line을 일반 wikilink로 옮기되, my-project 와 날짜를 실제 값으로 치환한 뒤에만 사용. -->
> ⏳ **`needs-diagram` (DIAGRAM_PENDING_USER)** — 아키텍처 `.drawio` 는 `/project-spec` 가 자동 생성할 수 없다. 아래 대상 파일을 사용자가 직접 작성한 뒤, 백틱을 풀어 활성 임베드(`![[...]]`)로 바꾼다. 그때까지는 **inline code span** 이라 구조 린터의 `BROKEN_LINK` 를 발생시키지 않는다. 이 항목은 readiness 게이트에서 **Blocking 이 아니라 Should-fix** 로 취급되어 판정이 `Ready-pending-user` 가 된다.
>
> 작성 대상 (권장 viewpoint 2개, 저장 경로 `raw/diagrams/ca-skeleton-frontend/`):
> - `![[raw/diagrams/ca-skeleton-frontend/architecture-overview-2026-07-18.drawio.svg]]` — CA 계층(presentation → application → domain, adapters 는 바깥) + 백엔드 API 신뢰 경계 + cross-cutting(error boundary / observability / config). 화살표는 **의존 방향**(안쪽을 향함)을 보여준다.
> - `![[raw/diagrams/ca-skeleton-frontend/architecture-deployment-2026-07-18.drawio.svg]]` — 정적 호스팅(CDN/오브젝트 스토리지) → 브라우저 SPA → 백엔드 API 토폴로지 (서버 런타임 없음).
>
> 작성 후: `wiki-diagram-reviewer` 로 ≥95 점 확인(게이트가 판정 못 하는 **권고 단계** — `rules/project-readiness-gate.md` R2 ≥95 주). 이후 frontmatter `diagrams:` 배열에 두 파일 등재 + `architecture_review:` 날짜 기입.
**다이어그램 작성 요약 (v2 minimalist, 상세는 [[rules/diagram-standards]] 정독):**
- **컴포넌트 라벨**: 시스템 이름 (Bold 1줄) + 핵심 한 줄 (Stack OR 역할, 둘 중 하나만). 절대 ≥3 줄 금지.
예: `**User Service**` / `Spring Boot 3.4 · :8080` (2줄)
- **화살표 라벨**: `<step?> <verb/protocol> <object>` — 5단어 이내
예: `① GET /`, `proxy_pass :8080`, `Kafka publish user.signed-up`
- **외부 시스템**: 점선 (`#D0D7DE`) + fill `#F6F8FA`. Legend 불필요 (표준 컨벤션)
- **Boundary**: Trust / Network / External — **정보 있을 때만**. 모든 컴포넌트를 boundary 1개 안에 넣지 말 것 (정보 0)
- **색상 ≤ 4** — 80% 회색 + 강조 ≤ 2 (blue / orange 한 family씩) + (선택) warning red callout
- **Legend 생략 가능** — 점선=외부 / cylinder=DB / 실선=동기 같은 표준 컨벤션이면 legend 불필요. 비표준 색·기호 있을 때만 ≤6 항목 legend.
- **데이터 모델 카디널리티** 는 ER 다이어그램 (Mermaid `erDiagram`)에서만. 아키텍처 다이어그램의 화살표에 `1..N` 같은 cardinality 박지 말 것.
### 3.2 컴포넌트 책임 분담
> 다이어그램의 각 컴포넌트가 정확히 무엇을 책임지는지 표로.
> 컴포넌트 = **Clean Architecture 계층**. 의존 방향은 항상 바깥→안(presentation → application → domain). adapters 는 domain 이 정의한 port 를 구현해 바깥에서 주입된다. 이 방향을 `feature-frontend-architecture-enforcement-lint-contract` branch 가 lint 로 강제한다.
| 컴포넌트 (계층) | 역할 | 기술 스택 | 의존하는 외부 |
|---|---|---|---|
| `presentation` (components / pages) | UI 렌더 · 사용자 입력 · **view-model 만** 소비 (raw API DTO 직접 접근 금지) | React 19 + Tailwind | (하위 계층만) |
| `application` (hooks / use-cases) | 화면 로직 · 상태 오케스트레이션 · use-case 훅 | React hooks + TanStack Query | adapters port (인터페이스) |
| `domain` (models / policies) | 도메인 제거 skeleton 이라 최소 — view-model 스키마 · 불변식 · 순수 함수 | 순수 JS + zod schema | **없음** (가장 안쪽, 의존 0) |
| `adapters/infrastructure` (api-client / storage / logger) | HTTP client · 응답 envelope 파싱 · zod 검증 · storage · 로깅 sink | fetch + zod + TanStack Query | Backend API · 브라우저 storage |
| `cross-cutting` (error-boundary / observability / config) | 에러 경계 · 구조화 로깅 · env config · trace 전파 | React ErrorBoundary + logger | Backend(trace) · error sink |
### 3.3 외부 의존성 / External dependencies
| 외부 시스템 | 용도 | 통신 방식 | 장애 시 영향 (degrade / fail / fallback) |
|---|---|---|---|
| Backend API | 데이터 read/write (인증 자체는 위임 — 아래) | REST/JSON, structured envelope (`{success,data,meta}` / `{success:false,error}`) | async 4-state 의 `error` 로 degrade + TanStack Query 가 `retryable` 만 backoff 재시도 |
| 브라우저 런타임 | 실행 환경 (fetch / history / storage) | Web API | 미지원 API 는 feature-detect + fallback, 앱 셸은 유지 |
| (선택) 에러 트래킹 sink | 관측 (`feature-frontend-observability-logging-trace-contract`) | HTTPS, best-effort async | fail 시 콘솔 fallback, 화면 영향 0 |
> **인증 경계 주의**: 토큰 획득/저장/refresh 는 본 skeleton 범위 밖 → [[raw/project-notes/keycloak-patterns-overview]] 로 위임. 본 skeleton 은 "이미 인증된 요청" 을 전제로 API 호출 계약만 다룬다(§7 보안 참조).
### 3.4 배포 다이어그램 / Deployment (선택)
> 운영 환경 토폴로지가 비자명하면 별도 draw.io.
```markdown
실제 사용 예 (placeholder 치환 후 사용):
![[raw/diagrams/my-project/architecture-deployment-2026-05-25.drawio.svg]]
```
## 4. 핵심 시퀀스 / Key sequences
> **필수 섹션.** 최소 1개의 주요 user flow 를 Mermaid sequence diagram 으로. happy path + 주요 error path 함께.
### 4.1 인증된 API 호출 + 응답 검증 + 에러 정규화 (핵심 계약 흐름)
**시나리오**: 이미 인증된 사용자가 화면에 진입해 백엔드 데이터를 읽는다. api-client 가 응답 envelope 을 파싱하고 zod 로 검증한 뒤, 성공/스키마위반/envelope에러를 각각 정규화해 async 4-state 로 표시한다. 이 흐름이 skeleton 의 대다수 계약(api-client · runtime 검증 · error 분류 · async UI state)을 관통한다.
```mermaid
sequenceDiagram
autonumber
actor User
participant UI as Presentation (React)
participant Hook as Application (use-case hook)
participant Q as TanStack Query
participant Client as api-client (adapter)
participant Zod as zod schema
participant API as Backend API
User->>UI: 화면 진입 / 액션
UI->>Hook: useResource()
Hook->>Q: useQuery(key, fetcher)
Q->>Client: GET /api/v1/resource
Client->>API: HTTP GET (traceparent 전파)
alt 200 OK
API-->>Client: {success:true, data, meta}
Client->>Zod: schema.parse(data)
alt 스키마 유효
Zod-->>Client: 검증된 model
Client-->>Q: model
Q-->>Hook: {data, isLoading:false}
Hook-->>UI: view-model
UI-->>User: success 상태 렌더
else 스키마 위반 (SCHEMA_MISMATCH)
Zod-->>Client: ZodError
Client-->>Q: throw ContractError(non-retryable)
Q-->>Hook: {error}
Hook-->>UI: error 상태
UI-->>User: 안전 메시지 (원본/스택 미노출)
end
else 4xx / 5xx (envelope error)
API-->>Client: {success:false, error:{code,category,retryable}}
Client-->>Q: throw ApiError(category)
Q-->>Q: retryable=true 면 exponential backoff 재시도
Q-->>Hook: {error}
Hook-->>UI: error 상태 (category 별 분기)
UI-->>User: error 상태 렌더 (retry 버튼 등)
end
```
**시퀀스 작성 표준 (필수 준수):**
- **`autonumber` 활성화** — 본문에서 "단계 3에서 ..." 처럼 참조 가능
- **`actor` vs `participant`**: 사람은 `actor`, 시스템은 `participant`
- **순서**: User → Frontend → Backend → External (좌→우)
- **화살표 라벨 명세**:
- HTTP: `METHOD /path {body 요약}` (예: `POST /api/v1/login {email, password}`)
- 메시징: `event-name {payload 요약}` (예: `user.signed-up {userId}`)
- 메서드 호출: `method()` (예: `validateCredentials()`)
- **응답**: `-->>` (점선 화살표)
- **alt / opt / loop**: 분기·옵션·반복은 명시적 블록
- **`Note over X,Y`**: 비자명한 동작은 노트로 명시
- **에러 경로 1개 이상 필수**: happy path 만 그리면 미완성
### 4.2 앱 부팅 + 런타임 config 검증 (fail-fast)
**시나리오**: SPA 부팅 시 `VITE_*` env 를 zod 로 검증한다. 필수 값 누락/형식 오류면 화면을 렌더하지 않고 부팅 에러로 즉시 실패시켜 "잘못된 config 로 반쯤 동작하는 상태" 를 차단한다. (Vite 는 `import.meta.env` 로만 env 를 노출하고 `VITE_` 접두 변수는 번들에 박히므로 secret 은 여기 두지 않는다 — [[raw/official-docs/vite-build-tool-official]].)
```mermaid
sequenceDiagram
autonumber
participant Boot as main.jsx (bootstrap)
participant Cfg as config loader (adapter)
participant Env as import.meta.env
participant Zod as env schema (zod)
participant App as React App
Boot->>Cfg: loadConfig()
Cfg->>Env: read VITE_* 변수
Cfg->>Zod: envSchema.parse(raw)
alt env 유효
Zod-->>Cfg: 검증된 config
Cfg-->>Boot: config
Boot->>App: render(App with config)
else 필수 env 누락 / 형식 오류
Zod-->>Cfg: ZodError
Cfg-->>Boot: throw ConfigError (fail-fast)
Boot-->>Boot: 렌더 중단 + 부팅 에러 화면 (secret 미노출)
end
```
## 5. 데이터 모델 / Data model (선택)
> 핵심 엔터티가 5~10개 이상이면 ER 다이어그램으로. 그 미만이면 글로만.
>
> **본 skeleton: 해당 없음 (N/A).** 도메인/비즈니스 로직을 제거했으므로 영속 엔터티가 없다. sample feature slice 의 **최소 view-model 만 zod schema 로** 정의하며(런타임 계약), 관계형 ER 다이어그램은 불요. 도메인 채택 시 각 feature branch 가 자체 view-model schema 를 정의한다.
<!-- ER 다이어그램 없음: 도메인 제거 skeleton (위 N/A 참조). 도메인 채택 시 feature branch 가 view-model zod schema 를 정의. -->
**ER 작성 표준:**
- **PK / FK 표시 필수**
- **관계 카디널리티 기호**:
- `||--||` (1:1)
- `||--o{` (1:N)
- `}o--o{` (M:N)
- `||..o{` (identifying vs non-identifying 표현)
- **관계 라벨**: 동사로 (예: `places`, `contains`, `ordered as`)
- 핵심 엔터티만 (5~10개 이내). 모든 테이블 그리지 말 것.
## 6. 기술 결정 / Tech decisions
> 주요 기술 선택과 이유. 트레이드오프 + 근거 자료 link 필수.
> **범례**: `deferred` = 이번 `/project-spec` 자동조사 6개 한도 밖으로 미룬 결정 — 근거는 해당 branch 의 `/branch-spec` 단계에서 채운다(R3 Blocking 면제). *사용자 결정* 은 근거 자료 없이도 확정된 제약이다.
| 결정 영역 | 선택 | 검토한 대안 | 채택 이유 | 트레이드오프 | 근거 자료 |
|---|---|---|---|---|---|
| 언어 | JavaScript (ESM) | TypeScript · JS+JSDoc | 사용자 지정 제약. 진입 장벽·빌드 단순 | ⚠️ 컴파일타임 타입 계약 **부재** → 경계 계약을 런타임(zod)이 짊. UNSUPPORTED 아니라 사용자 결정 | (사용자 결정) |
| 빌드/번들 | Vite (client-only SPA) | Next.js · CRA · Remix | 서버 런타임 불필요·백엔드 API 와 분리, native ESM dev server + HMR, `import.meta.env` 로 env 노출(secret 은 번들 금지) | SSR/SEO 필요 시 부적합(범위 밖) | `[[raw/official-docs/vite-build-tool-official]]` |
| UI 라이브러리 | React 19 | Vue · Svelte · SolidJS | 컴포넌트 기반 UI 분해, 생태계·채용시장, 기존 SPA 자산 | 러닝커브·리렌더 관리 | `[[raw/official-docs/react-ui-library-official]]` |
| 스타일링 | Tailwind CSS | CSS Modules · styled-components · vanilla-extract | utility-first + theme 토큰으로 magic number 제거, 런타임 CSS-in-JS 비용 0 | 클래스 verbosity·초기 학습 | `[[raw/official-docs/tailwind-css-utility-first-official]]` |
| server-state | TanStack Query | SWR · RTK Query · 수제 | server-state 를 client-state 와 분리, 캐시·동기화·stale 계약을 표준 제공(수제 시 놓침) | 의존성·개념 학습 | `[[raw/official-docs/tanstack-query-server-state-official]]` |
| 런타임 검증 | zod | yup · valibot · 수제 | 무타입 JS 에서 경계 계약을 `.parse()` 런타임으로 강제, `ZodError` 로 실패 신호 | 번들 크기·스키마 유지 | `[[raw/official-docs/zod-runtime-schema-validation-official]]` |
| 라우팅 | React Router (Declarative Mode) | TanStack Router · wouter | client-only SPA nested route 표준(SSR/framework 모드 불요) | 타입-라우팅은 TanStack 우위; guard/loader 는 별도 계약(branch) | `[[raw/official-docs/react-router-official]]` |
| client-state | Context / Zustand (경량) | Redux Toolkit · Jotai | server-state 는 Query 담당 → client-state 경량으로 충분 | 규모 증가 시 재검토 | `deferred``/branch-spec` |
| 테스트 | Vitest + RTL + Playwright | Jest · Cypress | Vite 정합(Vitest), 컴포넌트(RTL)·e2e(Playwright) 계층 분리 | 도구 3종 셋업 | `deferred``/branch-spec` |
| 아키텍처 패턴 | Clean Architecture (계층 + 의존 규칙) | Feature-Sliced Design · Layered · Atomic | 의존 방향 강제 + 도메인 순수성(백엔드 대칭) | 프론트 적용 시 boilerplate | `deferred` — branch 단계 `wiki-decision-researcher` 로 FSD 심층 비교 |
| 계층 강제 도구 | ESLint boundaries / dependency-cruiser | 수동 리뷰 | 의존 규칙 자동 강제(백엔드 ArchUnit 대응) | 규칙 유지보수 | `deferred``/branch-spec` |
## 7. 비기능 요구사항 / Non-functional
> 측정 가능한 비기능 목표. 없으면 명시적으로 "해당 없음".
> 수치는 초기 default. 각 값의 측정·강제는 대응 계약 branch 가 담당(§8.0).
- **성능**: 초기 JS 번들 **< 200KB gzip**(default) · route-level code splitting · web-vitals **LCP < 2.5s / CLS < 0.1 / INP < 200ms** 를 CI 게이트가 검사 → `feature-web-vitals-performance-budget-contract` (선택 포함).
- **가용성**: 정적 자산이므로 프론트 자체 다운타임은 CDN 가용성에 종속. 백엔드 장애 시 async 4-state 의 `error`**부분 degrade**(앱 셸 유지), retryable 요청만 backoff 재시도.
- **확장성**: 정적 자산 CDN 배포, 상태는 클라이언트 → 수평 확장 무관(서버 인스턴스 없음).
- **보안**: 인증 토큰 획득/저장/refresh 는 **범위 밖 → [[raw/project-notes/keycloak-patterns-overview]] 로 위임**. 본 skeleton 은 (1) secret 의 클라이언트 번들 유입 금지(`VITE_*` 에 secret 금지), (2) XSS 기본 방어(`dangerouslySetInnerHTML` 금지·CSP 권고), (3) 응답 원본/스택/토큰의 화면·로그 노출 금지만 계약화.
- **운영 / Observability**: 구조화 클라이언트 로깅 + 백엔드로 `traceparent` 전파 + (선택) 에러 트래킹 sink. 토큰·PII 로그 금지 → `feature-frontend-observability-logging-trace-contract` (선택 포함).
- **접근성 / a11y**: WCAG 2.1 AA 기본선 — 키보드 네비게이션·focus 관리·ARIA·a11y lint → `feature-accessibility-baseline-contract` (선택 포함).
- **재해 복구 / DR**: 정적 자산 재배포로 복구(RTO 낮음), 클라이언트 무상태(RPO 무관).
- **컴플라이언스**: **해당 없음** — 도메인·PII 미수집(skeleton). 도메인 채택 시 재평가.
- **i18n**: **범위 밖 (deferred)** — 이번 skeleton 미포함(사용자 미선택). 후속 프로젝트에서 별도 계약으로.
## 8.0 Branch 분해 / 실행계획 (Branch decomposition)
> **`/project-spec` 가 채우는 핸드오프 섹션.** 이 hub 에서 깊은 조사로 도출된 *자식 branch 의 네이밍과 달성 목표 조건만* 적는다. 각 branch 의 *결정 내용·메커니즘은 여기 적지 않는다* (SSOT 이중화 방지) — 그건 `/branch <slug>` 로 생성 후 `/branch-spec` 가 깊게 채운다.
>
> 작성 규칙:
> - `branch slug` 는 `rules/naming-conventions.md` §2.1 준수 (prefix 4종 `feature-`/`fix-`/`chore-`/`experiment-` 중 하나 + kebab-case, numbered hierarchy 금지).
> - `달성 목표 조건` 은 **측정가능**해야 함 ("잘 된다" 금지). 그 branch 가 "끝났다"고 말할 수 있는 검증 가능한 결과.
> - `우선순위` 는 실행 순서(P1 먼저). 의존이 있으면 `의존` 칸에 선행 branch slug.
| branch slug | 달성 목표 조건 (측정가능) | 우선순위 | 의존 |
|---|---|---|---|
| `feature-frontend-clean-architecture-layering-contract` | presentation/application/domain/adapters 4계층 디렉터리 + 계층별 허용 import 규칙 문서화, sample import 예시가 규칙과 일치 | P1 | - |
| `feature-api-client-response-envelope-contract` | fetch 래퍼가 성공/실패 envelope 파싱 + requestId/traceId 전파, 계약 위반 응답 시 정의된 에러 throw (contract test green) | P1 | - |
| `feature-runtime-schema-validation-contract` | 경계(API 응답·form)에서 zod parse 강제, 위반 시 SCHEMA_MISMATCH 로 정규화 (test) | P1 | `feature-api-client-response-envelope-contract` |
| `feature-frontend-error-classification-boundary-contract` | 에러 category(retryable/non-retryable/auth/validation) 분류 + React ErrorBoundary + 원본/스택 미노출 test | P1 | `feature-api-client-response-envelope-contract` |
| `feature-frontend-env-runtime-config-contract` | 부팅 시 VITE_ env 를 zod 검증, 필수값 누락 시 fail-fast, secret 번들 미유입 lint (test) | P1 | - |
| `feature-frontend-architecture-enforcement-lint-contract` | ESLint boundaries/dependency-cruiser 가 역방향 import 시 CI 실패, 위반 0건 리포트 | P1 | `feature-frontend-clean-architecture-layering-contract` |
| `feature-async-ui-state-contract` | 모든 async 표면이 loading/empty/error/success 4상태 렌더, sample 에 4상태 각각 RTL test | P2 | `feature-frontend-error-classification-boundary-contract` |
| `feature-boundary-mapper-viewmodel-contract` | API DTO → view-model 매핑 강제(presentation 이 raw DTO 미접근), 매핑 누락 시 test 실패 | P2 | `feature-runtime-schema-validation-contract` |
| `feature-server-state-caching-contract` | TanStack Query query-key 규약·staleTime·invalidation·retry 정책 + 캐시 무효화 시나리오 test | P2 | `feature-api-client-response-envelope-contract` |
| `feature-client-state-management-contract` | client-state 경계: server data 를 client store 에 중복 저장 금지, 위반 감지 test | P2 | `feature-server-state-caching-contract` |
| `feature-routing-navigation-guard-contract` | route 구조 + guarded route(비인증 redirect)·error/loading route·404 정의, 네비게이션 test | P2 | - |
| `feature-tailwind-design-token-styling-contract` | theme 토큰(spacing/color scale) 정의 + arbitrary value 금지 lint + 컴포넌트 스타일 경계 규칙 | P2 | - |
| `feature-frontend-test-taxonomy-contract` | unit/component(RTL)/integration(msw)/e2e(Playwright) 4계층 정의 + 각 계층 예제 test 1개 | P2 | - |
| `feature-frontend-observability-logging-trace-contract` | 구조화 로거 + traceparent 전파 + 토큰/PII redaction, 금지 필드 로그 유입 시 test 실패 | P2 | `feature-api-client-response-envelope-contract` |
| `feature-sample-feature-slice-contract-fixture` | 도메인 없는 sample slice 가 api-client·zod·error·4-state·CA 계층 모두 관통 + 계약 위반 시 실패 test 세트, 채택 시 제거 가능 표시 | P2 | `feature-async-ui-state-contract` |
| `feature-frontend-build-bundle-supply-chain-contract` | Vite 프로덕션 빌드 + code splitting + lockfile 고정 + 번들 분석, 번들 예산 초과 시 CI 실패 | P3 | - |
| `feature-web-vitals-performance-budget-contract` | LCP/CLS/INP 임계 + 번들 크기 예산을 CI 게이트로 측정, 초과 시 실패 | P3 | `feature-frontend-build-bundle-supply-chain-contract` |
| `feature-accessibility-baseline-contract` | WCAG 2.1 AA 기본선 + 키보드 네비·focus·ARIA + a11y lint(axe) CI 게이트, sample slice 통과 | P3 | `feature-async-ui-state-contract` |
| `feature-frontend-ci-quality-gates-contract` | lint·test·번들예산·a11y 게이트를 CI 로 통합, 하나라도 실패 시 머지 차단 | P3 | `feature-frontend-test-taxonomy-contract` |
| `feature-frontend-api-compatibility-contract` | 백엔드 API additive 변경 관대 + breaking 감지(스키마 버전) 정책, 불일치 시 명시적 실패 | P3 | `feature-runtime-schema-validation-contract` |
> 채운 뒤: `/branch <slug>` → `/branch-spec <slug> <근거 URL...>` → `/depth <slug>` 순으로 각 branch 를 깊게 작성.
## 8. Cluster / 묶음 (이 프로젝트에 묶이는 모든 raw 자료)
> 본 project-note 는 cluster 의 entry point. 모든 branch / errors / interviews / lectures / job-postings / blog-topics / sources 가 여기로 upward link. hub 측에서도 카테고리별 명시.
### 8.1 Branches (project 의 직접 자식 branch — `parent_branch:` 비어있음)
> project-note 가 직접 가리키는 branch 들. 자식 branch 가 있는 branch 는 자기 Cluster 섹션에서 자식들을 참조하므로 여기에는 등재 안 함.
> 아직 branch 미생성. §8.0 분해표의 각 slug 를 `/branch <slug>` → `/branch-spec <slug> <근거>` → `/depth <slug>` 로 전개하면 여기에 등재한다. P1 6개(layering / api-client / runtime-validation / error-boundary / env-config / arch-enforcement-lint)부터 착수 권장.
### 8.2 Sources (프로젝트 전체 차원 foundational 조사)
> 특정 branch 에 묶이지 않는 전체 프로젝트 단위 근거 자료.
- `[[raw/official-docs/vite-build-tool-official]]` — Vite 빌드/dev server + `import.meta.env` env·secret 계약 근거
- `[[raw/official-docs/react-ui-library-official]]` — React 컴포넌트 모델
- `[[raw/official-docs/tailwind-css-utility-first-official]]` — Tailwind utility-first + theme 토큰(magic number 제거)
- `[[raw/official-docs/tanstack-query-server-state-official]]` — server-state vs client-state 구분 + 캐시 계약
- `[[raw/official-docs/zod-runtime-schema-validation-official]]` — 무타입 JS 런타임 경계 검증(`.parse`/`.safeParse`)
- `[[raw/official-docs/react-router-official]]` — Declarative Mode nested routing (SSR/framework 모드 아님)
### 8.3 Errors (branch 외 발생한 환경·운영 이슈)
- (아직 없음 — 구현 착수 후 환경·운영 이슈 발생 시 등재)
### 8.4 Interview prep (프로젝트 전체 차원 면접 질문)
- (아직 없음)
### 8.5 Blog topics / job-posting tie-ins
- (아직 없음)
### 8.6 Derived wiki documents
- (아직 없음 — canonical 승급은 verified 사실 발생 후 `/ingest``wiki/projects/` 에)
## 9. 검증 등급 / Verification status
> 본 project-note 의 각 부분이 어느 등급까지 검증되었는지. CLAUDE.md §15 lifecycle 참조.
| 영역 | 등급 | 근거 |
|---|---|---|
| 아키텍처 다이어그램 | `planned` (`needs-diagram`) | §3.1 사용자 작성 예정, 아직 파일 없음 |
| 시퀀스 다이어그램 | `documented-only` | §4 Mermaid 2개(설계 흐름), 코드 대응물 없음 |
| 기술 결정 | `documented-only` | §6 6개 결정이 official 근거 보유 — 단 *결정*일 뿐 구현 아님 |
| 비기능 요구사항 | `planned` | §7 임계는 목표치, 측정값 없음(코드 미착수) |
### 9.1 실제 구현 내용 (`actually-implemented`)
> 코드에 존재하는 것만. 파일·함수 단위로 구체적으로. 면접에서 "구현했다"고 말해도 되는 부분.
- **없음** — 레포 미생성(`NO_GROUND_TRUTH`). 이 항목은 Vite 레포 생성 + `src/` grep 확인 후에만 채운다.
### 9.2 로컬/dev 검증 (`locally-verified`)
> 로컬 또는 dev 환경에서 동작 확인한 부분. 어떻게 검증했는지(로그/테스트/측정값).
- **없음** (기획 단계).
### 9.3 운영 검증 (`prod-verified`)
> 운영(prod) 환경에서 동작·성능을 확인한 부분. 근거(릴리즈 노트 / 운영 로그 / 모니터링 대시보드 / 인시던트 보고서)를 함께 명시.
- **없음** (기획 단계).
### 9.4 문서/계획만 존재 (`documented-only` / `planned`)
> 설계 문서에만 있고 아직 구현 안 된 것. 면접에서 "구현했다"고 말하면 안 되는 부분.
- **본 hub 전체가 현재 이 등급** — §1~§8 의 모든 계약·아키텍처·branch 분해는 `planned`(설계만). 외부에 "구현했다"고 말하면 안 됨.
- 아키텍처 `.drawio` = `needs-diagram`(§3.1, 사용자 작성 예정).
- 각 계약의 `actually-implemented` 승격은 대응 branch 착수 + 코드 커밋 + contract test green 후 `src/` grep 으로 확정.
## 10. 면접·외부 공개 답변 경계
### 10.1 자신 있게 답할 수 있는 범위
- 이 skeleton 이 **왜** 필요한가 — 도메인 없는 운영 계약을 프론트엔드에 대칭 적용한 설계 의도(§1·§2)와 백엔드 skeleton 과의 대응 관계.
- 각 스택의 **설계 선택과 트레이드오프**(§6) — Vite/React/Tailwind/TanStack Query/zod/React Router 를 왜 골랐는지 + 검토한 대안 + 근거(official 문서).
- CA 계층을 프론트엔드에 적용하고 lint 로 의존 방향을 강제하려는 **설계**(§3, §8.0), 무타입 JS 에서 zod 로 계약을 런타임 강제하는 이유.
### 10.2 적당히 답할 수 있는 범위
- 각 계약의 구체 구현 메커니즘 — 아직 branch 설계 단계이므로 "이렇게 **설계**했다" 수준까지만. "이미 구현했다"로 넘어가면 안 됨.
### 10.3 답하면 안 되는 / "공식 문서 다시 확인" 해야 하는 범위
- "이 skeleton 을 실제로 **구현/운영**했다" — 거짓(코드 미착수). "설계·계획했다"만 사실.
- 성능 수치(200KB, LCP/CLS/INP)를 **측정 결과**처럼 — 전부 목표치.
- 인증 토큰 처리 세부 — 본 skeleton 범위 밖([[raw/project-notes/keycloak-patterns-overview]]).
### 10.4 과장 금지 지점
> 외부에 설명할 때 사실보다 부풀려지기 쉬운 표현. 자기 검열용.
- "프로덕션 검증된 프론트엔드 아키텍처" ❌ → `planned`.
- "번들 200KB 달성 / web-vitals 통과" ❌ → 목표치, 미측정.
- "TanStack Query 로 캐시 최적화 경험" ❌ → 설계 결정일 뿐 구현 경험 아님.
## 11. Architecture Review Checklist (작성 / 갱신 시 self-check)
> 본 project-note 가 hub 역할을 제대로 하려면 모두 ✓ 여야 함.
- [ ] 한 줄 요약 + 현재 상태 + 나의 역할 채워짐 (§1)
- [ ] 측정 가능한 성공 기준 1개 이상 (§2.3)
- [ ] **아키텍처 다이어그램 (`.drawio.svg`) 1개 이상 첨부** (§3.1)
- [ ] 다이어그램이 [[rules/diagram-standards]] v2 minimalist 통과 — `wiki-diagram-reviewer` 로 ≥95 점 (vertex ≤ 10 / edge ≤ 8 / callout ≤ 1 / 박스 라벨 ≤ 2줄 / 화살표 라벨 ≤ 5단어 / 80% 회색 + 강조 ≤ 2 / boundary 정보 있을 때만 / 5초 + 30초 룰)
- [ ] 외부 시스템이 점선 + 회색 fill 로 시각적 구분 (legend 불필요 — 표준 컨벤션)
- [ ] **시퀀스 다이어그램 1개 이상 (Mermaid)** — happy path + error path 함께 (§4)
- [ ] 데이터 모델은 5~10개 이상 엔터티 시에만 ER 그림 (§5)
- [ ] 주요 기술 결정 표에 트레이드오프 + 근거 자료 link 명시 (§6)
- [ ] 비기능 요구사항이 측정 가능한 수치 (§7)
- [ ] **Branch 분해표 채워짐** — 각 자식 branch 가 naming-conventions slug + 측정가능 목표조건 (§8.0)
- [ ] Cluster 섹션의 project 직접 자식 branch 목록 채워짐 (§8.1)
- [ ] 검증 등급이 각 영역별로 매겨짐 (§9)
- [ ] 면접 답변 경계 명시 (§10)
- [ ] 마지막 architecture review 날짜 frontmatter `architecture_review:` 에 기록
## 12. 다이어그램 파일 관리 가이드
> draw.io 파일과 Mermaid 코드 모두 본 project-note 와 함께 라이프사이클 관리.
### 12.1 draw.io (`.drawio.svg`)
- 저장 위치: `raw/diagrams/<project-slug>/`
- 명명: `architecture-{viewpoint}-{YYYY-MM-DD}.drawio.svg`
- viewpoint 예: `overview`, `deployment`, `data-flow`, `security`, `network`
- 갱신 시: 새 날짜로 파일 추가 + frontmatter `diagrams:` 에 모든 활성 다이어그램 나열
- 폐기 시: 파일 삭제하지 말고 `diagrams/<project>/archived/` 하위로 이동 + project-note 에서 link 제거
- Obsidian 임베딩 문법: `![[architecture-overview-2026-05-25.drawio.svg]]`
### 12.2 Mermaid
- 본 문서 본문에 직접. 외부 파일로 분리 안 함.
- 갱신 시: code block 그대로 수정 (git diff 친화적)
- 너무 커지면 (>50줄) 별도 sub-branch 의 branch-note 로 분리하고 본문에서는 요약만
### 12.3 그림 변경 시 의무
- 아키텍처가 변경되면 본 project-note 의 `architecture_review:` frontmatter 날짜 갱신
- 변경 사유는 §6 "기술 결정" 표에 한 줄 추가 (예: "2026-06-01: PostgreSQL → Aurora 변경 — 이유: 가용성 SLO 99.99%")
- 폐기된 결정도 표에서 지우지 말고 status 컬럼 추가로 표시 (`active` / `deprecated` / `superseded-by-<row>`)
## 13. 관련 개념 / Related concepts
> §3~§6 표에 등장하지 않은 보조 개념·자료.
- `[[raw/project-notes/ca-skeleton-operational-contract]]` — 백엔드 대응 skeleton (운영 계약 철학의 원본, 본 프로젝트의 대칭 기준)
- `[[raw/project-notes/keycloak-patterns-overview]]` — 인증 토큰 처리 위임처(본 skeleton 범위 밖 경계)
- `[[rules/diagram-standards]]` — §3 아키텍처 / §4 시퀀스 다이어그램 컨퍼런스급 표준
- `[[rules/naming-conventions]]` — §8.0 branch slug 규칙
## 14. 다음 단계 / Next steps
- [ ] 아키텍처 `.drawio` 2개 작성(§3.1) → `wiki-diagram-reviewer` ≥95 확인 → frontmatter `diagrams:`/`architecture_review:` 기입 (readiness `Ready-pending-user` 해소)
- [ ] Vite + React + Tailwind 레포 생성 (§1 저장소 채우기 — 이후 `actually-implemented` 승격 근거)
- [ ] P1 6개 branch 착수: `/branch feature-frontend-clean-architecture-layering-contract``/branch-spec``/depth` (나머지 P1 동일)
- [ ] `deferred` 결정(client-state / 테스트 / 계층강제 도구 / 아키텍처 패턴)을 branch 단계에서 `wiki-decision-researcher` 로 대안 심층 조사
- [ ] frontmatter `tags:` 확장 검토 — 필요 시 taxonomy 에 L4 `vite`·`tanstack-query`·`zod` 등재 후 반영
@@ -0,0 +1,7 @@
# Task
- User request: frontend operational contract를 backend benchmark 이상으로 review·작성.
- In-scope documents: 2.
- Write scope: target project note, two frontend diagrams, current-run audit artifacts, `wiki/log.md` one lint entry.
- Out of scope: frontend application code, remote repository creation, branch-note scaffolding.
@@ -0,0 +1,13 @@
# Walkthrough
- Deterministic lint: target and benchmark each per-file PASS; global failures are unrelated.
- Code evidence: accessible local filesystem에서 target repository 미발견; code claims remain UNVERIFIED.
- Semantic review: 8 findings.
- Adversarial result: KEEP 1, DOWNGRADE 7, REJECT 0.
- Remediation intent: branch SSOT를 복제하지 않고 hub minimum·owner·evidence closure를 추가.
- Diagram review: 3회차에 두 파일 100/100 PASS.
- Final target integrity: FE-D 25/25, FE-OC 26/26, registry 8, gate 26, runbook 5, NFR 15; dangling ID 0.
- Ownership/dependency: FE-OC-002..025 primary owner exact-one, §2 owner mismatch 0, §20 dependency slug 미해결 0.
- Traceability: FE-D003..008의 source-backed decision은 6개 official source의 13개 실제 Claim ID에 연결.
- Repository-wide lint remains out of scope: `--all` PASS 700 / FAIL 255, `--stale` 49 findings; target와 benchmark per-file lint는 PASS.
- Independent semantic QA: 초기 12건, 2차 6건, 3차 5건을 교정한 뒤 최신 target SHA `daa0941a8dde21e6`에서 PASS, actionable regression 0.
@@ -0,0 +1,46 @@
# Sed Proofs
Source snapshot: `process/pre-review-source.md`. It is byte-identical to the target before remediation.
```bash
$ sed -n '24p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
- **한 줄 요약**: 도메인/비즈니스 로직을 제거한 **Clean Architecture 프론트엔드 skeleton**. 어떤 화면·기능을 붙여도 (1) 같은 방식으로 백엔드 API 를 호출하고, (2) 같은 방식으로 실패를 분류·표시하고, (3) 같은 계층 의존 규칙을 강제하고, (4) 같은 계약 테스트로 깨짐을 감지하는 React SPA 뼈대. 백엔드 [[raw/project-notes/ca-skeleton-operational-contract]] 의 프론트엔드 대응물이며, 개인 학습·포트폴리오·재사용 starter 로 쓴다.
$ sed -n '29p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
- 메인: `<미생성 — Vite + React + Tailwind SPA 레포 예정>` · `NO_GROUND_TRUTH`: 아직 코드 레포가 없어 본 hub 의 모든 구현 주장은 `planned` 등급이다(§9). `actually-implemented` 승격은 레포 생성 후 `src/` grep 으로만.
$ sed -n '17p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
> 본 문서는 **프로젝트의 최상위 hub**. 프로젝트 전체 컨텍스트 / 문제 정의 / 시스템 아키텍처 / 핵심 시퀀스가 여기에 집중. 모든 branch / errors / interviews / lectures / job-postings / blog-topics / sources 가 본 문서로 upward link.
$ sed -n '318p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
> **`/project-spec` 가 채우는 핸드오프 섹션.** 이 hub 에서 깊은 조사로 도출된 *자식 branch 의 네이밍과 달성 목표 조건만* 적는다. 각 branch 의 *결정 내용·메커니즘은 여기 적지 않는다* (SSOT 이중화 방지) — 그건 `/branch <slug>` 로 생성 후 `/branch-spec` 가 깊게 채운다.
$ sed -n '62p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
> **필수 섹션.** 아키텍처 다이어그램이 없는 project-note 는 hub 역할을 못 함.
$ sed -n '115p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
> ⏳ **`needs-diagram` (`DIAGRAM_PENDING_USER`)** — 아키텍처 `.drawio``/project-spec` 가 자동 생성할 수 없다. 아래 대상 파일을 사용자가 직접 작성한 뒤, 백틱을 풀어 활성 임베드(`![[...]]`)로 바꾼다. 그때까지는 **inline code span** 이라 구조 린터의 `BROKEN_LINK` 를 발생시키지 않는다. 이 항목은 readiness 게이트에서 **Blocking 이 아니라 Should-fix** 로 취급되어 판정이 `Ready-pending-user` 가 된다.
$ sed -n '282p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
> 주요 기술 선택과 이유. 트레이드오프 + 근거 자료 link 필수.
$ sed -n '395p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
| 기술 결정 | `documented-only` | §6 6개 결정이 official 근거 보유 — 단 *결정*일 뿐 구현 아님 |
$ sed -n '140p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
> 컴포넌트 = **Clean Architecture 계층**. 의존 방향은 항상 바깥→안(presentation → application → domain). adapters 는 domain 이 정의한 port 를 구현해 바깥에서 주입된다. 이 방향을 `feature-frontend-architecture-enforcement-lint-contract` branch 가 lint 로 강제한다.
$ sed -n '145p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
| `application` (hooks / use-cases) | 화면 로직 · 상태 오케스트레이션 · use-case 훅 | React hooks + TanStack Query | adapters port (인터페이스) |
$ sed -n '54p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
- 기준 1 (계약 관통): 도메인 없는 **sample feature slice** 화면이 `api-client → zod 검증 → error boundary → async 4-state → CA 의존 규칙`을 모두 관통하고, **계약 위반 시 실패하는 contract test** 가 CI 에서 green.
$ sed -n '213p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
Q-->>Q: retryable=true 면 exponential backoff 재시도
$ sed -n '158p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
> **인증 경계 주의**: 토큰 획득/저장/refresh 는 본 skeleton 범위 밖 → [[raw/project-notes/keycloak-patterns-overview]] 로 위임. 본 skeleton 은 "이미 인증된 요청" 을 전제로 API 호출 계약만 다룬다(§7 보안 참조).
$ sed -n '337p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
| `feature-routing-navigation-guard-contract` | route 구조 + guarded route(비인증 redirect)·error/loading route·404 정의, 네비게이션 test | P2 | - |
$ sed -n '302p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
> 측정 가능한 비기능 목표. 없으면 명시적으로 "해당 없음".
$ sed -n '312p' docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/process/pre-review-source.md
- **재해 복구 / DR**: 정적 자산 재배포로 복구(RTO 낮음), 클라이언트 무상태(RPO 무관).
```
## Stats
- V = 16
- P = 16
- C = 0
- D = 0
- U = 0
@@ -0,0 +1,9 @@
# Unresolved Risk Register
| Risk | Status | Owner | Trigger | Resolution condition |
| --- | --- | --- | --- | --- |
| 실제 frontend repository/remote 미발견 | UNVERIFIED | project owner | repository 생성 또는 URL 제공 | commit + package/lock/test/CI evidence path 연결 |
| package manager·hosting·CI provider 미확정 | OPEN | FE-D owner | bootstrap branch 착수 | decision status accepted + executable commands |
| auth session provider shape 미확정 | OPEN | routing/auth integration owner | guarded route 구현 전 | injected session port + 401/redirect-loop tests |
| performance profile 미실행 | OPEN | performance branch | preview artifact 생성 | browser/device/network profile + artifact |
| draw.io 독립 review | RESOLVED | controller | diagram 생성 후 | 두 파일 reviewer 100/100 PASS |
@@ -0,0 +1,15 @@
# Keycloak Branch-note Adversarial Review
**일자:** 2026-07-18
**대상:** 4개 lane의 finding 60개
**검토 결과:** KEEP 21, DOWNGRADE 27, REJECT 12
**최종 retained severity:** High 10, Medium 18, Low 20
60개 finding별 Original Claim, Strongest Counterargument, falsification evidence, verdict, final severity의 정본은 [adversarial-review.md](2026-07-18-keycloak-branch-note-consistency/adversarial-review.md)에 있다.
## Controller disposition
- 60개 적대 권고를 모두 수용했다. Override는 0개다.
- 중복 제거 대표: issuer/network는 `L1-F08`, audience는 `L4-F08`, SPA Direct/TMB는 `L4-F04`.
- `REJECT`는 원문에 문제가 없다는 뜻이 아니라, 중복이거나 active contradiction으로 보기 위한 전제가 무너졌다는 뜻이다.
- 원문 branch-note는 수정하지 않았다.
@@ -0,0 +1,135 @@
# Controller Verification
## Recomputed metrics
| Check | Command | Expected | Actual | Result |
|---|---|---|---|---|
| raw_file_count | python3 .../process/controller_verify.py | 38 | 38 | PASS |
| raw_line_count | same | observed | 10,649 | PASS |
| matrix_row_count | same | 38 | 38 | PASS |
| matrix_path_set | same | missing=0, extra=0 | 0, 0 | PASS |
| duplicate_paths | same | 0 | 0 | PASS |
| nonexistent_paths | same | 0 | 0 | PASS |
| malformed_matrix_rows | same | 0 | 0 | PASS |
| READ_FULL / BLOCKED | same | 38 / 0 | 38 / 0 | PASS |
| per_file_sections | same | 38 | 38 | PASS |
| lane_finding_count / unique | same | 60 / 60 | 60 / 60 | PASS |
| duplicate / missing finding IDs | same | 0 / 0 | 0 / 0 | PASS |
| finding_schema_failures | same | 0 | 0 | PASS |
| sed_proof_rows / unique | same | 60 / 60 | 60 / 60 | PASS |
| quote commands / failed | python3 .../process/verify_quotes.py | greater than 0 / 0 | 107 / 0 | PASS |
| verified quote output lines | same | at least 120 | 158 | PASS |
| adversarial rows / unique | controller_verify.py | 60 / 60 | 60 / 60 | PASS |
| adversarial generic hits | same | 0 | 0 | PASS |
| unresolved priority IDs | same | 0 | 0 | PASS |
| broken internal links | same | 0 | 0 | PASS |
| forbidden word hits | same | 0 | 0 | PASS |
| missing required artifacts | same | 0 | 0 | PASS |
| Decision Evidence Map files | same | 38 | 38 | PASS |
| Claims Extracted files / missing | same | 38 / 0 for full Claim traceability | 2 / 36 | FAIL |
| UNSUPPORTED_DECISION files / occurrences | same | reported | 32 / 125 | PASS |
| BROKEN_CLAIM_REFERENCE literal labels | same | reported | 0 | PASS |
## Exact observed output — quote validation
~~~text
$ python3 docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/process/verify_quotes.py
inventory_rows=38
inventory_unique=38
read_full=38
finding_ids=60
expected_finding_ids=60
commands_passed=107
commands_failed=0
verified_output_lines=158
findings_without_commands=0
~~~
## Exact observed output — controller validation
~~~text
$ python3 docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/process/controller_verify.py
raw_file_count=38
raw_line_count=10649
matrix_rows=38
read_full_rows=38
blocked_rows=0
missing_paths=0
extra_paths=0
duplicate_paths=0
nonexistent_paths=0
malformed_matrix_rows=0
lane_finding_count=60
unique_finding_ids=60
duplicate_finding_ids=0
missing_finding_ids=0
finding_schema_failures=0
per_file_sections=38
sed_proof_rows=60
sed_proof_unique_ids=60
adversarial_rows=60
adversarial_unique_ids=60
adversarial_generic_hits=0
priority_unique_ids=17
unresolved_priority_ids=0
broken_internal_links=0
forbidden_word_hits=0
missing_required_artifacts=0
decision_evidence_map_files=38
claims_extracted_files=2
missing_claims_extracted=36
unsupported_decision_files=32
unsupported_decision_occurrences=125
broken_claim_reference_literal_occurrences=0
~~~
## Deterministic consistency checker
| Metric | Observed |
|---|---:|
| Repository files | 102 |
| Repository findings | 144 |
| BARE_OWNER_REF | 74 |
| BARE_DECISION_REF | 70 |
| Keycloak-scope BARE_DECISION_REF | 8 |
| Keycloak-scope DANGLING_DECISION_REF | 0 |
| Keycloak-scope DANGLING_SECTION_REF | 0 |
| Keycloak-scope exact DUAL_OWNERSHIP | 0 |
## Nine gates
| Gate | Expected | Actual | Result |
|---|---|---|---|
| scope_gate | path sets equal, duplicate=0, nonexistent=0 | 38=38, 0, 0 | PASS |
| matrix_gate | 38 valid four-column rows and allowed status | 38 valid, READ_FULL=38 | PASS |
| finding_gate | 38 file sections, 60 unique IDs, full finding schema, Claim traceability | 38, 60, schema failure=0, Claims Extracted missing=36 | FAIL |
| quote_gate | one VERIFIED proof row per finding and source output | 60 rows, 158 output lines | PASS |
| adversarial_gate | 60 substantive rows, no generic rows | 60, generic=0 | PASS |
| priority_gate | each referenced ID exists | unresolved=0 | PASS |
| link_gate | all internal Markdown links resolve | broken=0 | PASS |
| language_gate | forbidden terms outside quoted source=0 | 0 | PASS |
| artifact_gate | master, two root files, six subfolder files exist | missing=0 | PASS |
## Claim traceability status
| Metric | Value | Interpretation |
|---|---:|---|
| Decision Evidence Map | 38/38 | branch decision tables present |
| Claims Extracted | 2/38 | 36 files require migration or equivalent source-claim audit |
| UNSUPPORTED_DECISION | 125 occurrences in 32 files | labels reported; label presence alone is not a defect |
| BROKEN_CLAIM_REFERENCE literal | 0 | absence of the label does not prove every target resolves |
| Lane 2 lexical sample | 152/153 IDs resolved | KC-HOST-C6 is the one unarchived candidate and was not supporting evidence |
## Verdict
| Rule | Result |
|---|---|
| Every gate PASS | no |
| scope_gate and matrix_gate PASS, source reads available, at least one non-core gate FAIL | yes |
| Computed verdict | PARTIAL |
## Failed gates
| Gate | Expected | Actual |
|---|---|---|
| finding_gate | full local finding schema plus source Claim traceability | local schema 60/60, Claims Extracted only 2/38 |
@@ -0,0 +1,357 @@
# Keycloak Branch-note 설계 일관성 리뷰
**일자 / Date:** 2026-07-18
**범위 / Scope:** raw/branch-notes에서 파일명 또는 본문에 keycloak이 있는 38개 파일, 10,649줄
**Verdict:** PARTIAL — derived from controller-verification.md
**요청 언어 / User language:** ko
이 verdict는 [controller-verification.md](2026-07-18-keycloak-branch-note-consistency-controller-verification.md)의 gate 결과에서 자동 산출되었다.
## 0. Source roots
해당 없음 / N/A. 인용한 source는 모두 현재 workspace 안에 있다.
## 1. 한눈 요약 / Executive Summary
38개 branch-note를 38/38 READ_FULL로 판독했으며 원문은 수정하지 않았다. 1차 finding 60개를 인용 158줄로 확인한 뒤 적대 리뷰에서 KEEP 21, DOWNGRADE 27, REJECT 12로 정리했고, 최종 High는 10개다. 가장 큰 실행 위험은 audience 값, container issuer 도달성, SPA endpoint origin, Caddy path strip, account-link hard-reject 범위, SPA Direct와 TMB 경계다. 결정론 checker는 Keycloak 범위에서 BARE_DECISION_REF 8건을 찾았고 dangling 또는 exact dual-ownership finding은 내지 않았다. 문서 전문 판독은 끝났지만 Claims Extracted가 38개 중 2개에만 있고 298 logical reference edge 중 의미 대조는 고위험 후보 20건으로 제한했으므로 원천 Claim 및 전체 edge 수준에서는 추가 검증이 남는다.
## 2. Evidence Matrix
정본: [evidence-matrix.md](2026-07-18-keycloak-branch-note-consistency/evidence-matrix.md)
| Path | Status | Evidence | Extracted facts |
|---|---|---|---|
| raw/branch-notes/experiment-nplus1-highlight-feed.md | READ_FULL | L1-L233 | Keycloak은 예정 sibling 주제로만 등장한다. |
| raw/branch-notes/feature-authentication-authorization-contract.md | READ_FULL | L1-L341 | raw role과 permission registry 경계. |
| raw/branch-notes/feature-boundary-validation-mapping-contract.md | READ_FULL | L1-L407 | Keycloak은 sample seed 설명에 등장한다. |
| raw/branch-notes/feature-keycloak-account-linking-spa-ux.md | READ_FULL | L1-L228 | account-link UX, First Broker Login, unlink, CIAL. |
| raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md | READ_FULL | L1-L279 | sub key, collision, re-auth, unlink, Sync Mode. |
| raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md | READ_FULL | L1-L274 | SPA Direct와 BFF 선택. |
| raw/branch-notes/feature-keycloak-docker-compose-stack.md | READ_FULL | L1-L287 | Compose topology, import, issuer/network. |
| raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md | READ_FULL | L1-L325 | P1B brokering과 child ownership. |
| raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md | READ_FULL | L1-L316 | P1A ForwardAuth와 cookie session. |
| raw/branch-notes/feature-keycloak-federation-spa-zero-change.md | READ_FULL | L1-L193 | P2A→P2B login-path zero-change. |
| raw/branch-notes/feature-keycloak-first-broker-login-flow.md | READ_FULL | L1-L261 | Confirm Link와 silent-link policy. |
| raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md | READ_FULL | L1-L238 | attribute mapping과 IMPORT. |
| raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md | READ_FULL | L1-L261 | redirect URI와 JavaScript origins. |
| raw/branch-notes/feature-keycloak-header-spoofing-defense.md | READ_FULL | L1-L255 | trusted header 방어 경계. |
| raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md | READ_FULL | L1-L284 | TLS termination과 HSTS. |
| raw/branch-notes/feature-keycloak-idp-brokering-google-client.md | READ_FULL | L1-L251 | Google IdP client와 trustEmail. |
| raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md | READ_FULL | L1-L148 | role mapping, FORCE, email verification. |
| raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md | READ_FULL | L1-L512 | P2B composition hub와 foreign detail. |
| raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md | READ_FULL | L1-L416 | P2A SPA Direct와 token storage. |
| raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch.md | READ_FULL | L1-L287 | issuer/JWKS 대안과 owner conflict. |
| raw/branch-notes/feature-keycloak-nginx-auth-request-integration.md | READ_FULL | L1-L306 | auth_request body와 failure routing. |
| raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md | READ_FULL | L1-L275 | OIDC flow와 discovery dependency. |
| raw/branch-notes/feature-keycloak-patterns.md | READ_FULL | L1-L205 | pattern taxonomy와 naming. |
| raw/branch-notes/feature-keycloak-pkce-flow-stages.md | READ_FULL | L1-L167 | PKCE S256와 Admin terminology. |
| raw/branch-notes/feature-keycloak-public-domain-tunneling.md | READ_FULL | L1-L220 | quick/named tunnel과 hostname. |
| raw/branch-notes/feature-keycloak-realm-client-export.md | READ_FULL | L1-L238 | realm import와 credential export. |
| raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md | READ_FULL | L1-L254 | rotation demonstration과 logout. |
| raw/branch-notes/feature-keycloak-refresh-token-rotation.md | READ_FULL | L1-L284 | rotation contract와 receiver scope. |
| raw/branch-notes/feature-keycloak-reverse-proxy-headers.md | READ_FULL | L1-L297 | proxy headers와 Caddy path. |
| raw/branch-notes/feature-keycloak-single-ec2-google-federation.md | READ_FULL | L1-L370 | integration parent와 tunnel/proxy delegation. |
| raw/branch-notes/feature-keycloak-single-ec2-no-google.md | READ_FULL | L1-L342 | base topology와 component ownership. |
| raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md | READ_FULL | L1-L256 | token storage, TMB, CSRF. |
| raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md | READ_FULL | L1-L268 | audience owner와 validator 선택. |
| raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md | READ_FULL | L1-L231 | RBAC와 audience delegation. |
| raw/branch-notes/feature-keycloak-three-leg-trust-chain.md | READ_FULL | L1-L229 | federation/API validation hop. |
| raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md | READ_FULL | L1-L237 | Traefik ForwardAuth와 pattern attribution. |
| raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md | READ_FULL | L1-L257 | oidc-client-ts/manual PKCE와 endpoint URL. |
| raw/branch-notes/feature-security-operational-baseline.md | READ_FULL | L1-L417 | auth code, CORS, JWT, public-path contract. |
## 3. Controller Recomputed Coverage
모든 값은 controller가 disk의 source, lane, matrix, proof, adversarial artifact에서 다시 계산했다.
| Metric | Command | Observed |
|---|---|---:|
| N scope files | controller_verify.py: content match enumeration | 38 |
| Source lines | controller_verify.py: splitlines sum | 10,649 |
| M matrix rows | controller_verify.py: evidence row parse | 38 |
| R READ_FULL | controller_verify.py: status count | 38 |
| B BLOCKED | controller_verify.py: status count | 0 |
| Missing / extra / duplicate / nonexistent paths | controller_verify.py: set reconciliation | 0 / 0 / 0 / 0 |
| P per-file index sections | controller_verify.py: section parse | 38 |
| T unique lane finding IDs | controller_verify.py: heading parse | 60 |
| Finding schema failures | controller_verify.py: source, severity, falsification, action check | 0 |
| Quote proof rows | controller_verify.py: sed-proofs parse | 60 |
| Quote commands / failed | verify_quotes.py | 107 / 0 |
| Verified quote output lines | verify_quotes.py | 158 |
| A adversarial rows / generic rows | controller_verify.py | 60 / 0 |
| Priority unresolved IDs | controller_verify.py | 0 |
| Broken internal links | controller_verify.py | 0 |
| Forbidden term hits in controller-generated prose | controller_verify.py | 0 |
| Decision Evidence Map files | controller_verify.py | 38 |
| Claims Extracted files / missing | controller_verify.py | 2 / 36 |
| UNSUPPORTED_DECISION files / occurrences | controller_verify.py | 32 / 125 |
| BROKEN_CLAIM_REFERENCE literal labels | controller_verify.py | 0 |
~~~text
$ python3 docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/process/verify_quotes.py
inventory_rows=38
inventory_unique=38
read_full=38
finding_ids=60
expected_finding_ids=60
commands_passed=107
commands_failed=0
verified_output_lines=158
findings_without_commands=0
~~~
### 3-1. Verdict Calculation
Coverage 식은 N=38, M=38, R=38, P=38, T=60, G=60으로 M==N, P==R, G==T를 만족한다. 9-gate controller에서는 claim source 역추적 미완료를 finding integrity에 포함했고, Claims Extracted가 2/38이므로 finding_gate만 FAIL이다. scope_gate와 matrix_gate는 PASS이고 source first reads도 완료되어 계산 결과는 PARTIAL이다.
## Failed Gates from controller-verification.md
- finding_gate: 60개 finding의 local source quote와 schema는 충족했지만 36개 파일에 Claims Extracted가 없어 raw Claim 의미까지의 traceability가 완료되지 않았다.
## 4. 파일별 발견 사항 / Per-File Findings
파일별 index: [per-file-findings.md](2026-07-18-keycloak-branch-note-consistency/per-file-findings.md)
인용 proof: [sed-proofs.md](2026-07-18-keycloak-branch-note-consistency/sed-proofs.md)
lane 상세: [lane 01](2026-07-18-keycloak-branch-note-consistency/lanes/lane-01-keycloak-sorted-01-10.md), [lane 02](2026-07-18-keycloak-branch-note-consistency/lanes/lane-02-keycloak-sorted-11-20.md), [lane 03](2026-07-18-keycloak-branch-note-consistency/lanes/lane-03-keycloak-sorted-21-30.md), [lane 04](2026-07-18-keycloak-branch-note-consistency/lanes/lane-04-keycloak-sorted-31-38.md)
아래는 60개 finding registry다. 각 finding의 원문 quote, 가정, 반증 조건, action은 연결된 lane이 소유하며 quote status는 모두 VERIFIED다.
#### L1-F01 — ROLE_*와 raw-role registry 입력
- Adversarial: KEEP → Medium · Evidence: lane 01 · Quote: VERIFIED
#### L1-F02 — unlink guard evidence status
- Adversarial: DOWNGRADE → Low · Evidence: lane 01 · Quote: VERIFIED
#### L1-F03 — delegated email_verified decision 복제
- Adversarial: DOWNGRADE → Low · Evidence: lane 01 · Quote: VERIFIED
#### L1-F04 — password re-auth와 email verification profile
- Adversarial: DOWNGRADE → Medium · Evidence: lane 01 · Quote: VERIFIED
#### L1-F05 — IMPORT 선택 이유 stale
- Adversarial: DOWNGRADE → Low · Evidence: lane 01 · Quote: VERIFIED
#### L1-F06 — sub key와 collision locator
- Adversarial: REJECT → N/A · Evidence: lane 01 · Quote: VERIFIED
#### L1-F07 — OAuth 2.1 BFF 조건 요약
- Adversarial: DOWNGRADE → Low · Evidence: lane 01 · Quote: VERIFIED
#### L1-F08 — localhost issuer와 bridge reachability
- Adversarial: KEEP → High · Evidence: lane 01 · Quote: VERIFIED
#### L1-F09 — Google sub key dual owner
- Adversarial: KEEP → High · Evidence: lane 01 · Quote: VERIFIED
#### L1-F10 — P1A/P1B backend trust
- Adversarial: KEEP → High · Evidence: lane 01 · Quote: VERIFIED
#### L1-F11 — oauth2-proxy와 Traefik 선택축
- Adversarial: DOWNGRADE → Medium · Evidence: lane 01 · Quote: VERIFIED
#### L1-F12 — browser token custody 표현
- Adversarial: DOWNGRADE → Low · Evidence: lane 01 · Quote: VERIFIED
#### L1-F13 — zero-change scope
- Adversarial: REJECT → N/A · Evidence: lane 01 · Quote: VERIFIED
#### L1-F14 — zero-change audience ambiguity
- Adversarial: REJECT → N/A · Evidence: lane 01 · Quote: VERIFIED
#### L2-F01 — Confirm Link version gate
- Adversarial: REJECT → N/A · Evidence: lane 02 · Quote: VERIFIED
#### L2-F02 — silent-link prevention과 hard-reject
- Adversarial: KEEP → High · Evidence: lane 02 · Quote: VERIFIED
#### L2-F03 — IMPORT와 FORCE scope
- Adversarial: DOWNGRADE → Low · Evidence: lane 02 · Quote: VERIFIED
#### L2-F04 — Google basic-scope test-user 설명
- Adversarial: KEEP → Low · Evidence: lane 02 · Quote: VERIFIED
#### L2-F05 — JavaScript origins
- Adversarial: DOWNGRADE → Low · Evidence: lane 02 · Quote: VERIFIED
#### L2-F06 — Caddy HSTS default
- Adversarial: KEEP → Medium · Evidence: lane 02 · Quote: VERIFIED
#### L2-F07 — Reference-Only와 foreign detail
- Adversarial: KEEP → Medium · Evidence: lane 02 · Quote: VERIFIED
#### L2-F08 — brokering zero-change ownership
- Adversarial: KEEP → Medium · Evidence: lane 02 · Quote: VERIFIED
#### L2-F09 — SPA Direct 안의 BFF variant
- Adversarial: REJECT → N/A · Evidence: lane 02 · Quote: VERIFIED
#### L3-F01 — auth_request body 설명
- Adversarial: DOWNGRADE → Medium · Evidence: lane 03 · Quote: VERIFIED
#### L3-F02 — sign-in TODO 적용 범위
- Adversarial: REJECT → N/A · Evidence: lane 03 · Quote: VERIFIED
#### L3-F03 — token introspection 용어
- Adversarial: KEEP → Low · Evidence: lane 03 · Quote: VERIFIED
#### L3-F04 — bare decision reference
- Adversarial: KEEP → Low · Evidence: lane 03 · Quote: VERIFIED
#### L3-F05 — retired numbered naming
- Adversarial: DOWNGRADE → Low · Evidence: lane 03 · Quote: VERIFIED
#### L3-F06 — PKCE S256 D-row 중복
- Adversarial: REJECT → N/A · Evidence: lane 03 · Quote: VERIFIED
#### L3-F07 — PKCE Admin label
- Adversarial: KEEP → Low · Evidence: lane 03 · Quote: VERIFIED
#### L3-F08 — quick tunnel hostname
- Adversarial: KEEP → High · Evidence: lane 03 · Quote: VERIFIED
#### L3-F09 — realm import wiring scope
- Adversarial: DOWNGRADE → Low · Evidence: lane 03 · Quote: VERIFIED
#### L3-F10 — credential export certainty
- Adversarial: DOWNGRADE → Medium · Evidence: lane 03 · Quote: VERIFIED
#### L3-F11 — rotation contract와 execution
- Adversarial: DOWNGRADE → Medium · Evidence: lane 03 · Quote: VERIFIED
#### L3-F12 — back-channel receiver owner
- Adversarial: DOWNGRADE → Medium · Evidence: lane 03 · Quote: VERIFIED
#### L3-F13 — Max Reuse semantics
- Adversarial: DOWNGRADE → Medium · Evidence: lane 03 · Quote: VERIFIED
#### L3-F14 — Caddy prefix strip
- Adversarial: KEEP → High · Evidence: lane 03 · Quote: VERIFIED
#### L3-F15 — tunnel provider dual owner
- Adversarial: KEEP → Medium · Evidence: lane 03 · Quote: VERIFIED
#### L3-F16 — parent proxy bundle ownership
- Adversarial: DOWNGRADE → Medium · Evidence: lane 03 · Quote: VERIFIED
#### L4-F01 — localhost와 extra_hosts 대안
- Adversarial: REJECT → N/A · Evidence: lane 04 · Quote: VERIFIED
#### L4-F02 — audience owner pointer stale
- Adversarial: KEEP → Medium · Evidence: lane 04 · Quote: VERIFIED
#### L4-F03 — PKCE manual-first 대 library-first
- Adversarial: REJECT → N/A · Evidence: lane 04 · Quote: VERIFIED
#### L4-F04 — SPA Direct와 TMB 경계
- Adversarial: KEEP → High · Evidence: lane 04 · Quote: VERIFIED
#### L4-F05 — evidence status stale
- Adversarial: DOWNGRADE → Low · Evidence: lane 04 · Quote: VERIFIED
#### L4-F06 — cookie-CSRF owner gap
- Adversarial: DOWNGRADE → Low · Evidence: lane 04 · Quote: VERIFIED
#### L4-F07 — custom validator와 property-first
- Adversarial: DOWNGRADE → Low · Evidence: lane 04 · Quote: VERIFIED
#### L4-F08 — backend audience와 spa-client
- Adversarial: KEEP → High · Evidence: lane 04 · Quote: VERIFIED
#### L4-F09 — role note의 audience 재명세
- Adversarial: DOWNGRADE → Low · Evidence: lane 04 · Quote: VERIFIED
#### L4-F10 — realm-role-only 전제
- Adversarial: DOWNGRADE → Low · Evidence: lane 04 · Quote: VERIFIED
#### L4-F11 — 3-leg taxonomy
- Adversarial: REJECT → N/A · Evidence: lane 04 · Quote: VERIFIED
#### L4-F12 — nonce 자동 처리 evidence
- Adversarial: KEEP → Medium · Evidence: lane 04 · Quote: VERIFIED
#### L4-F13 — trust-chain foreign summary
- Adversarial: REJECT → N/A · Evidence: lane 04 · Quote: VERIFIED
#### L4-F14 — P3A pattern misattribution
- Adversarial: KEEP → High · Evidence: lane 04 · Quote: VERIFIED
#### L4-F15 — 302 생성 주체
- Adversarial: DOWNGRADE → Low · Evidence: lane 04 · Quote: VERIFIED
#### L4-F16 — relative SPA endpoint origin
- Adversarial: KEEP → High · Evidence: lane 04 · Quote: VERIFIED
#### L4-F17 — oidc-client-ts storage default
- Adversarial: DOWNGRADE → Low · Evidence: lane 04 · Quote: VERIFIED
#### L4-F18 — automaticSilentRenew mechanism
- Adversarial: REJECT → N/A · Evidence: lane 04 · Quote: VERIFIED
#### L4-F19 — Phase C2 active table
- Adversarial: DOWNGRADE → Medium · Evidence: lane 04 · Quote: VERIFIED
#### L4-F20 — JWT decoder와 clock skew
- Adversarial: DOWNGRADE → Medium · Evidence: lane 04 · Quote: VERIFIED
#### L4-F21 — public-path verification contract
- Adversarial: DOWNGRADE → Medium · Evidence: lane 04 · Quote: VERIFIED
## 4-1. 적대 리뷰 결과 / Adversarial Review Results
정본: [adversarial-review.md](2026-07-18-keycloak-branch-note-consistency/adversarial-review.md)
| Metric | Count |
|---|---:|
| Reviewed | 60 |
| KEEP | 21 |
| DOWNGRADE | 27 |
| REJECT | 12 |
| Final High | 10 |
| Final Medium | 18 |
| Final Low | 20 |
| Controller override | 0 |
Controller는 60개 권고를 모두 수용했다. REJECT는 중복 또는 active contradiction 전제 붕괴를 뜻하며 원문 상태의 무결함을 뜻하지 않는다.
## 5. 우선순위 권고 / Priority Recommendations
상세 Decision Conditions와 fix-plan: [priority-recommendations.md](2026-07-18-keycloak-branch-note-consistency/priority-recommendations.md)
| Rank | Finding | Action | Verification |
|---:|---|---|---|
| 1 | L4-F08 | API audience symbol과 owner를 하나로 고정 | realm export, token aud, 정상 및 wrong-audience test |
| 2 | L1-F08 | issuer identity와 container retrieval address를 실행 profile 하나로 종결 | rendered Compose, discovery와 JWKS log |
| 3 | L4-F16 | relative SPA endpoint를 same-origin proxy 또는 runtime absolute URL 중 하나로 선택 | browser와 access logs |
| 4 | L3-F14 | Caddy와 Keycloak path prefix 전략을 맞춤 | caddy adapt와 upstream path |
| 5 | L2-F02 | silent-link와 hard-reject policy를 분리 | flow export, SPI artifact, negative E2E |
| 6 | L4-F04 | pure SPA와 TMB variant를 분리 | refresh endpoint, CSRF, rotation E2E |
| 7 | L1-F10 | P1A와 P1B의 backend trust를 한 invariant로 확정 | SecurityFilterChain과 forwarding diff |
| 8 | L4-F14 | deployment 축과 authentication pattern 축을 분리 | pattern matrix와 component flow |
| 9 | L3-F08 | quick tunnel과 managed static hostname을 분리 | restart hostname과 redirect E2E |
| 10 | L1-F09 | Google sub key의 mutable owner를 하나로 선택 | owner map과 inbound references |
## 6. 후속 작업 / Follow-Up
- 사용자 승인 전 branch-note 원문 수정은 하지 않는다.
- High 10건은 개별 승인 단위로, L3-F04의 reference syntax 8건은 low-risk 묶음 승인 단위로 제안한다.
- logical edge 298건 전체 의미 대조가 필요하면 20건 이하 lane으로 분할하는 별도 sync round가 필요하다.
- Claims Extracted 36개 누락과 raw Claim target 검증은 migrate-claims 또는 전용 traceability audit로 분리한다.
- runtime 미검증과 code/canonical 범위는 [unresolved-risk-register.md](2026-07-18-keycloak-branch-note-consistency/unresolved-risk-register.md)에 기록했다.
## 7. 검증 / Verification
### 7.1 Self-grep proof
- Finding T=60
- Finding별 proof row G=60
- 실행 command=107, 실패=0
- command stdout의 verified quote lines=158
- 미검증 finding=0
- 명령과 observed output은 [sed-proofs.md](2026-07-18-keycloak-branch-note-consistency/sed-proofs.md)와 각 lane에 있다.
### 7.2 실행한 검증
~~~text
$ python3 .claude/hooks/wiki_consistency_check.py --all
repository files=102
repository findings=144
BARE_OWNER_REF=74
BARE_DECISION_REF=70
Keycloak-scope BARE_DECISION_REF=8
Keycloak-scope DANGLING_DECISION_REF=0
Keycloak-scope DANGLING_SECTION_REF=0
Keycloak-scope exact DUAL_OWNERSHIP=0
$ python3 docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/process/controller_verify.py
raw_file_count=38
matrix_rows=38
read_full_rows=38
blocked_rows=0
missing_paths=0
extra_paths=0
duplicate_paths=0
nonexistent_paths=0
malformed_matrix_rows=0
lane_finding_count=60
unique_finding_ids=60
duplicate_finding_ids=0
missing_finding_ids=0
finding_schema_failures=0
per_file_sections=38
sed_proof_rows=60
adversarial_rows=60
adversarial_generic_hits=0
unresolved_priority_ids=0
broken_internal_links=0
forbidden_word_hits=0
missing_required_artifacts=0
decision_evidence_map_files=38
claims_extracted_files=2
missing_claims_extracted=36
unsupported_decision_occurrences=125
~~~
## 8. Generated Artifacts
| Artifact | Path |
|---|---|
| Master | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency-report.md |
| Controller verification | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency-controller-verification.md |
| Adversarial entry | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency-adversarial-review.md |
| Evidence matrix | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/evidence-matrix.md |
| Per-file index | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/per-file-findings.md |
| Quote proofs | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/sed-proofs.md |
| Priority and fix-plan | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/priority-recommendations.md |
| Risk register | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/unresolved-risk-register.md |
| Adversarial matrix | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/adversarial-review.md |
| Lane 01 | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/lanes/lane-01-keycloak-sorted-01-10.md |
| Lane 02 | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/lanes/lane-02-keycloak-sorted-11-20.md |
| Lane 03 | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/lanes/lane-03-keycloak-sorted-21-30.md |
| Lane 04 | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/lanes/lane-04-keycloak-sorted-31-38.md |
| Semantic sample | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/lanes/lane-05-semantic-risk-sample.md |
| Process plan | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/process/implementation_plan.md |
| Process task | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/process/task.md |
| Process walkthrough | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/process/walkthrough.md |
| Controller script | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/process/controller_verify.py |
| Quote script | docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/process/verify_quotes.py |
@@ -0,0 +1,105 @@
# Keycloak Branch-note Adversarial Review
대상은 lane 4개에서 추출한 60개 finding 전부다. 각 행은 원 주장을 실무성, 과장, 전제의 세 관점에서 반증하려고 시도한 결과다.
## Summary
| Metric | Count |
|---|---:|
| KEEP | 21 |
| DOWNGRADE | 27 |
| REJECT | 12 |
| Final High | 10 |
| Final Medium | 18 |
| Final Low | 20 |
| Removed | 12 |
## Per-finding Falsification
| Finding ID | Original Claim | Strongest Counterargument | Evidence Needed To Falsify | Falsification Result | Verdict | Final Severity |
|---|---|---|---|---|---|---|
| L1-F01 | ROLE_* 입력과 raw-role registry 계약이 다르다. | ROLE_*는 Spring Security 경계에서 생성되는 GrantedAuthority이고 raw role은 그 이전 AuthenticatedUser 저장 형식일 수 있다. registry 호출 직전에 명시적 변환이 있다면 서로 다른 처리 단계를 말하므로 충돌하지 않는다. | AuthorizationAdapter 호출 직전 값, registry key, binding test에서 admin과 ROLE_ADMIN 중 실제 lookup key를 확인한다. | 중간 변환 증거가 없고 명세는 registry가 raw role을 소비한다고 적는다. | KEEP | Medium |
| L1-F02 | unlink guard의 consumer 상태가 owner보다 오래됐다. | owner 근거가 Keycloak main 소스라면 실제 배포 release tag의 guard와 다를 수 있다. consumer의 needs-confirmation은 stale 상태가 아니라 배포 이미지 기준 확인을 남긴 보수적 gate일 수 있으므로 영향은 라벨 정합에 한정될 수 있다. | 사용 image tag의 LinkedAccountsResource와 마지막 identity unlink 응답을 main과 비교한다. | 배포 tag 동일성이 확인되지 않아 영향 강도를 낮춘다. | DOWNGRADE | Low |
| L1-F03 | 위임한 email_verified 정책이 active owned decision으로 남았다. | 같은 Decisions 섹션 뒤에 재프레이밍과 owner 위임이 있어 앞 문장은 역사 보존으로 읽을 수 있다. 최신 DEM과 구현 가이드만 실행 기준으로 쓰는 저장소 관행이 확실하다면 실제 오귀속 가능성은 제한된다. | 신규 구현자가 선택하는 owner와 checker가 오래된 bullet을 active D-row로 파싱하는지 확인한다. | 위임은 유효하지만 오래된 active-looking 문구의 영향은 낮다. | DOWNGRADE | Low |
| L1-F04 | password 확인 고정 흐름과 SMTP email verification 기본이 갈린다. | SPA UX는 security-first profile을 설명하고 owner는 그 profile에서 email authenticator를 끄고 password 재인증을 고를 수 있게 한다. 제품 기본값과 프로젝트 강화 profile은 동시에 성립할 수 있어 무조건적인 기술 모순은 아니다. | realm export의 First Broker Login execution과 SMTP 설정, password 화면 E2E를 확인한다. | 조건 없는 문구 drift만 남고 profile이 고정되면 양립한다. | DOWNGRADE | Medium |
| L1-F05 | IMPORT의 takeover 근거가 최신 DEM과 반대다. | 오래된 사용자 결정 뒤 correction memo와 D5가 잘못된 인과를 이미 정정한다. DEM을 정본으로 소비하면 잘못된 IMPORT 선택은 실행되지 않으며 문제는 active 정책보다 역사 문장의 상태 표시다. | 실제 realm Sync Mode, ADR 선택 이유, correction을 보지 않은 구현 사례를 확인한다. | 정정이 이미 있어 실무 영향은 낮다. | DOWNGRADE | Low |
| L1-F06 | persistent sub key와 collision locator가 합쳐졌다. | D1과 구현 가이드는 IdP alias와 sub를 저장 key로 두면서 OOTB collision discovery는 email 또는 username임을 별도 위험으로 기록한다. 이미 저장 key와 최초 후보 탐색을 나눴으므로 finding이 전제한 단일 key 강제가 없다. | E2E에서 email은 후보 탐색에만 쓰고 federated identity에는 sub가 저장되는지 확인한다. | 원문이 두 단계를 이미 분리한다. | REJECT | N/A |
| L1-F07 | OAuth 2.1의 조건부 BFF 권고를 일반 권고처럼 표시한다. | 같은 문서 D5와 인접 설명에 client credentials 조건이 있어 전체를 읽으면 선택 오류가 줄어든다. 조직 정책으로 BFF를 우선할 수도 있으므로 matrix 한 셀만으로 실제 architecture 오선택까지 단정하기는 어렵다. | matrix만 사용한 ADR 결과와 전체 문서를 사용한 결정 결과를 비교한다. | wording drift는 확인되나 failure 인과가 약하다. | DOWNGRADE | Low |
| L1-F08 | bridge network의 localhost issuer와 extra_hosts 기본은 함께 동작하지 않는다. | rendered Compose가 host network, localhost host-gateway alias, 또는 별도 jwk-set-uri를 사용하면 정상일 수 있다. 그러나 현재 문서는 host.docker.internal만 추가하면서 discovery URL을 localhost로 유지해 그 반론을 뒷받침하지 않는다. | docker compose config, 컨테이너 DNS, discovery URL, JWKS fetch log를 함께 확인한다. | 현재 선택 기본을 구제하는 구성이 문서에 없다. | KEEP | High |
| L1-F09 | parent와 child가 Google sub federation key를 함께 소유한다. | parent가 pattern requirement를, child가 Keycloak mechanism을 소유하는 분할은 가능하다. 하지만 parent가 D1~D5를 설정 SSOT라고 선언하고 양쪽이 같은 key와 email 금지를 결정하므로 concern 층위 분리가 현재 텍스트에는 없다. | requirement와 mechanism concern ID, inbound reference, 변경 승인 주체를 확인한다. | 같은 mutable key 정책에 두 SSOT가 있다. | KEEP | High |
| L1-F10 | P1B가 P1A와 동일하다면서 backend trust를 바꾼다. | P1B가 defense-in-depth JWT validation을 추가한 별도 variant라면 달라도 된다. 그러나 비교 표가 backend 검증 행을 동일이라고 직접 표시해 별도 variant 해석이 현재 텍스트와 양립하지 않는다. | 두 pattern의 dependency, SecurityFilterChain, token과 header forwarding을 diff한다. | 동일 표시와 header-only owner 계약이 직접 충돌한다. | KEEP | High |
| L1-F11 | oauth2-proxy와 Traefik ForwardAuth를 대안으로 쓰지만 함께 사용한다. | Traefik ForwardAuth가 middleware와 외부 auth service 묶음을 줄인 표현일 수 있고 숙련자는 forwardAuth.address가 oauth2-proxy를 호출함을 알아낼 수 있다. 따라서 architecture 모순보다 선택축 이름이 부정확한 문제에 가깝다. | 각 option의 service와 forwardAuth target을 적은 배포표로 신규 구현자의 선택을 관찰한다. | 상호 배타적 표현은 고쳐야 하나 심각도는 낮춘다. | DOWNGRADE | Medium |
| L1-F12 | 브라우저 token 부재와 cookie token 저장이 같은 threat model에 섞였다. | 노출 없음은 브라우저가 bytes를 보유하지 않는다는 뜻보다 JavaScript가 raw token을 읽지 못한다는 축약일 수 있다. cookie session과 server-side store를 조건부 대안으로 설명했다면 두 문장이 같은 배치를 뜻하지도 않는다. | session-store-type, cookie payload, Redis 사용 여부로 실제 custody를 구분한다. | 물리 보유와 JS 접근성의 용어 drift만 확정된다. | DOWNGRADE | Low |
| L1-F13 | zero-change 범위가 hosted login보다 넓게 읽힌다. | 문서는 Account Linking을 명시적으로 out of scope로 두고 기존 SPA login path 유지로 목표를 제한한다. 제외된 별도 기능까지 제목이 포함한다고 해석하면 이미 제공된 scope boundary를 무시하게 된다. | parent acceptance와 branch TODO에 CIAL 또는 linked-account API가 배정됐는지 확인한다. | explicit out-of-scope가 원 주장을 반증한다. | REJECT | N/A |
| L1-F14 | zero-change aud owner와 값이 모호하다. | 더 직접적인 L4-F08이 같은 audience mapper와 validator root cause 및 remediation을 포괄한다. 독립 유지하면 동일 결함을 두 priority로 계산하므로 위험 수를 부풀리게 된다. | realm client ID, mapper target, backend expected value를 한 matrix에서 비교한다. | L4-F08과 독립 조치가 없는 중복이다. | REJECT | N/A |
| L2-F01 | 미아카이브 CVE 때문에 Confirm Link에 version gate가 필요하다. | draft가 해당 CVE의 raw 출처와 영향 버전을 UNVERIFIED로 분류한다. 존재와 영향 범위가 확인되지 않은 외부 메모만으로 source-backed owner의 보장을 충돌로 판정하면 severity가 검증되지 않은 주장 하나에 의존한다. | 공식 advisory, CVE record, 영향 및 패치 버전, 배포 image tag와 재현 flow를 확인한다. | 핵심 반증 근거가 trace되지 않아 독립 finding으로 유지하지 않는다. | REJECT | N/A |
| L2-F02 | email_verified=false가 silent-link 차단과 hard-reject 사이에서 갈린다. | 다른 repository나 deferred branch에 custom SPI가 이미 있다면 hard-reject consumer 문장이 맞을 수 있다. 그러나 corpus는 SPI를 out of scope로 두고 consumer는 구현 근거 없이 링크와 생성 거부를 acceptance처럼 사용한다. | authentication flow export, provider JAR, false-email E2E 응답으로 custom reject 존재를 증명한다. | corpus 안에 hard-reject owner와 artifact가 없다. | KEEP | High |
| L2-F03 | IdP IMPORT와 role mapper FORCE의 적용 범위가 불명확하다. | IdP default IMPORT 위에 특정 mapper의 Sync Mode Override FORCE를 두는 계층 설정은 의도적으로 공존할 수 있다. attribute owner도 role freshness가 필요한 sibling의 FORCE를 설명해 실제 contradiction보다 범위 표기가 부족한 문제일 가능성이 높다. | export에서 IdP syncMode와 mapper syncModeOverride, 로그인 후 profile과 role 갱신을 확인한다. | 공존 가능한 설정이므로 심각도를 낮춘다. | DOWNGRADE | Low |
| L2-F04 | basic-scope 예외를 오래된 test-user 설명이 덮는다. | 실제 authorize request에 sensitive scope가 더 있거나 Console audience 조건이 다르면 옛 TODO가 별도 scenario로 맞을 수 있다. 그러나 현재 D5는 openid profile email만 고정해 그 조건이 문서에 없다. | 실제 scope와 Console audience, 비등록 계정 동의 및 만료 동작을 확인한다. | 현재 선택 scope에서는 authoritative D5와 어긋난다. | KEEP | Low |
| L2-F05 | JavaScript origins를 owner는 비우고 consumer는 등록한다. | 향후 Google browser SDK를 쓴다면 origin이 필요할 수 있고 불필요한 origin 하나가 즉시 인증 실패를 만들지는 않는다. 현재 server-side brokering에 한정하면 stale이지만 영향은 낮다. | browser가 Google SDK를 직접 호출하는지 network trace와 두 client E2E로 확인한다. | 현재 topology에서 불필요하지만 낮은 위험이다. | DOWNGRADE | Low |
| L2-F06 | Caddy default HSTS 설명이 명시적 구성과 상충한다. | Cloudflare나 상위 load balancer가 HSTS를 넣으면 최종 응답에는 header가 있을 수 있다. 그래도 제품 default와 외부 주입은 다른 사실이므로 Caddy default라는 설명을 구제하지 못한다. | 최소 Caddyfile와 production edge 응답을 각각 캡처해 header 생성 주체를 확인한다. | default 설명은 현재 authoritative 구성과 어긋난다. | KEEP | Medium |
| L2-F07 | Reference-Only를 선언하고 foreign detail을 유지한다. | composition hub의 end-to-end 상세는 유용하고 owner 우선 주석이 사람에게 정본을 알려줄 수 있다. 하지만 저장소 계약은 자동 생성 view가 아닌 수동 상세 복제를 금지하므로 규칙 변경이나 동기화 장치 없이는 반론이 성립하지 않는다. | owner 변경 시 consumer가 자동 갱신되거나 CI가 drift를 검출하는지 확인한다. | 현 상태는 수동 상세 복제다. | KEEP | Medium |
| L2-F08 | P2A와 P2B가 brokering zero-change를 함께 소유한다. | baseline은 확장 가능성, P2B는 실제 acceptance를 소유한다고 나눌 수 있다. 그러나 두 D-row가 같은 code-zero invariant를 담고 consumer가 이중 주장으로 표기해 change authority 경계가 없다. | 두 D-row의 concern ID, approval owner, zero-change branch inbound map을 확인한다. | 동일 mutable invariant가 여러 D-row에 있다. | KEEP | Medium |
| L2-F09 | SPA Direct 안의 HttpOnly cookie는 사실상 BFF다. | consumer가 이미 BFF 변형이라고 표시해 pure SPA 기본을 몰래 바꾼 결정이 아닐 수 있다. 더 직접적인 owner finding L4-F04가 architecture tension과 조치를 포괄한다. | P2A 기본 storage와 refresh endpoint 유무를 L4-F04 owner와 비교한다. | explicit variant 표기와 중복 때문에 제거한다. | REJECT | N/A |
| L3-F01 | auth_request body 설명과 proxy_pass_request_body default가 충돌한다. | 첫 문장은 auth_request core subrequest, 표는 proxy handler가 upstream으로 body를 전달하는 별도 단계를 설명할 수 있다. 층위를 분리하면 양립하며 실행 config는 이미 off를 선택한다. | off 제거 전후 auth upstream의 Content-Length와 body bytes, nginx -T를 비교한다. | 설명 층위는 섞였지만 실행 실패는 현재 방지된다. | DOWNGRADE | Medium |
| L3-F02 | sign-in TODO가 browser와 API 모두 302로 만들 수 있다. | TODO는 named location을 작성하라는 작업일 뿐 어느 route에 적용하라는 지시가 아니다. 뒤 D9가 browser만 연결하고 API는 401로 두므로 전 route 적용 가정은 텍스트에서 나오지 않는다. | rendered config의 error_page가 붙은 location 목록과 route별 E2E 응답을 확인한다. | named location 생성과 적용 범위를 혼동했다. | REJECT | N/A |
| L3-F03 | local JWT 동작을 token introspection이라고 부른다. | 팀 내부 shorthand로만 쓰면 운영 영향이 작을 수 있다. 그러나 RFC 7662 network call 여부는 latency, revocation, firewall 설계를 바꾸므로 protocol 용어를 그대로 두면 실제 오해가 생긴다. | oauth2-proxy debug log와 outbound capture에서 introspection endpoint 호출을 확인한다. | rename 비용이 작고 오해 위험이 남는다. | KEEP | Low |
| L3-F04 | 세 decision reference가 parser 형식이 아니다. | 사람은 bare slug와 D-id를 이해하고 owner 변경이 드물면 drift가 없을 수 있다. 하지만 저장소가 deterministic impact analysis를 목표로 하고 checker가 wikilink 형식을 요구해 사람 가독성만으로 계약을 충족하지 못한다. | 수정 전후 checker packet과 inbound reference에 세 edge가 들어오는지 비교한다. | deterministic reference contract를 충족하지 않는다. | KEEP | Low |
| L3-F05 | 폐기한 numbered naming이 active-looking D2로 남았다. | 폐기 경고가 앞에 있고 D2가 역사 bullet이라 checker가 active owner로 보지 않을 수 있다. 독자가 경고를 먼저 보면 신규 branch가 옛 규칙을 채택할 가능성도 낮다. | D-row parser 결과, inbound D2 reference, scaffold가 numbered slug를 만드는지 확인한다. | stale 표현은 맞지만 영향은 낮다. | DOWNGRADE | Low |
| L3-F06 | D1과 D5가 S256 enforcement를 함께 소유한다. | D5는 TODO step을 D1에 매핑한 trace row로 읽히고 같은 문서 안에서 값도 같다. 별도 owner 경쟁이나 상충 값이 없어 task mapping을 독립 mutable decision으로 본 것이 과하다. | inbound reference가 D5를 owner로 쓰는지와 변경 승인 주체를 확인한다. | 독립 ownership 증거가 없다. | REJECT | N/A |
| L3-F07 | 오래된 PKCE Admin label이 TODO에 남았다. | Keycloak version과 locale에 따라 옛 label이 남을 수 있고 최신 구현 가이드도 함께 있다. 하지만 pinned target에서 TODO는 실행 checklist이므로 실제 UI와 다르면 작업을 방해한다. | target image Admin Console과 realm export field명으로 실제 label을 고정한다. | version 확인 뒤 국소 수정할 유효 drift다. | KEEP | Low |
| L3-F08 | trycloudflare.com을 영구와 random으로 함께 설명한다. | 특정 client가 named hostname을 지속 제공한다면 첫 설명이 맞을 수 있다. 그러나 보존 source model은 random quick tunnel과 managed custom hostname을 나누며 parent도 정적 redirect domain을 요구한다. | tunnel create와 restart 전후 hostname, DNS route, Google redirect E2E를 기록한다. | 현재 source model에서는 두 유형이 양립하지 않는다. | KEEP | High |
| L3-F09 | realm export가 import wiring을 scope에 남겼다. | 자동 import는 system outcome이고 mount path와 startup flag의 변경 권한을 주장하지 않을 수 있다. 명시적 trace가 stack owner를 지목하므로 실제 ownership 충돌보다 scope 문구가 모호한 문제다. | 두 branch의 artifact 목록과 실제 PR 변경 파일로 wiring 수정 주체를 확인한다. | owner trace가 있어 severity를 낮춘다. | DOWNGRADE | Low |
| L3-F10 | credential export를 근거 없이 사실로 단정한다. | 기본 realm export와 별도 user export는 다른 mode라 password 없음과 특정 credentials 포함 가능이 동시에 맞을 수 있다. 문제는 상호 모순보다 pinned version의 primary evidence가 없다는 점이다. | target kc.sh export help, JSON credentials, reimport 로그인 결과를 보존한다. | unsupported certainty는 남지만 모순 서사는 약하다. | DOWNGRADE | Medium |
| L3-F11 | rotation concept와 execution note가 같은 sequence를 함께 소유한다. | 한쪽은 protocol contract이고 다른 쪽은 executable scenario라 기대값 반복은 자연스럽다. P3A D5가 시연으로 명명되고 양쪽이 미근거 family behavior를 표시해 역할 분리도 일부 존재한다. | inbound reference와 approval owner로 P3A가 concept D4를 consume만 하는지 확인한다. | owner 경쟁보다 invariant와 test split일 가능성이 높다. | DOWNGRADE | Medium |
| L3-F12 | back-channel receiver가 위임됐지만 destination은 optional이다. | optional demonstration에 receiver 구현과 token validation이 포함될 수 있어 한 줄만으로 owner 부재를 단정할 수 없다. 다만 planned artifact와 endpoint acceptance가 없어 구현자가 포함 범위를 알기 어렵다. | P3A planned files의 endpoint, validator, provider-trigger E2E를 확인한다. | owner gap 가능성은 있으나 전체 context가 부족하다. | DOWNGRADE | Medium |
| L3-F13 | Max Reuse >0을 무의미와 약한 대안으로 함께 쓴다. | 무의미는 strict stolen-token demonstration에 부적합하다는 축약일 수 있고 다른 note도 값이 커지면 탐지력이 약해져 해당 시연이 불가하다고 적는다. 범위를 좁히면 같은 trade-off다. | target에서 값 0과 1, 병렬 refresh, RT 재사용 결과를 측정한다. | incompatible policy보다 범위 없는 표현 문제다. | DOWNGRADE | Medium |
| L3-F14 | Caddy handle_path가 method B의 prefix를 제거한다. | 다른 rewrite가 prefix를 복구하거나 Keycloak이 root path로 실행되면 동작할 수 있다. 그러나 copyable draft와 D3에는 그런 layer가 없고 note 자체도 handle로 바꾸라고 정정한다. | caddy adapt와 upstream access log에서 discovery, auth, token path를 확인한다. | 현재 선택 조합은 직접 404를 만들 수 있다. | KEEP | High |
| L3-F15 | parent와 child가 Cloudflare 우선 선택을 함께 소유한다. | integration parent가 provider를 고르고 child가 운영 detail을 소유하는 분할은 가능하다. 그러나 양쪽 D-row가 provider 우선순위와 fallback 조건까지 결정해 selection과 detail의 경계가 없다. | owner map과 inbound D3 및 D1 reference, 변경 승인 주체를 확인한다. | 같은 mutable preference가 두 normative D-row에 있다. | KEEP | Medium |
| L3-F16 | parent exact proxy bundle과 child detail owner가 겹친다. | integration parent는 여러 child 결과를 조립한 deployable bundle을 소유할 수 있고 detail owner는 설명과 근거만 뜻할 수 있다. child의 변수별 D-row를 함께 보지 않으면 exact 이중 ownership은 입증되지 않는다. | child DEM의 다섯 변수 D-row와 parent D4의 change authority를 비교한다. | assembly와 detail 경계 context가 부족하다. | DOWNGRADE | Medium |
| L4-F01 | localhost와 extra_hosts 대안이 실행 구성으로 종결되지 않았다. | L1-F08이 실제 Compose 기본의 hostname 불일치를 더 직접적으로 포착하고 iss-mismatch note는 대안과 미승인 상태를 상세히 기록한다. 이 행은 같은 root cause를 덜 정확하게 반복한다. | parent D3, child D6, rendered Compose의 DNS와 discovery 역할을 비교한다. | L1-F08과 조치가 같은 중복이다. | REJECT | N/A |
| L4-F02 | audience owner 정정 뒤 parent 표가 옛 owner를 지목한다. | 표가 과거 child inventory라면 history일 수 있다. 그러나 셀은 현재 component 구현 근거를 직접 가리키고 정정과 같은 문서에 있으면서 역사 표시가 없다. | pointer 수정 전후 packet과 구현자가 선택한 owner를 비교한다. | current reference가 명시적 owner split과 충돌한다. | KEEP | Medium |
| L4-F03 | PKCE 순서가 parent manual-first와 child library-first로 반대다. | parent의 이어지는 문구가 child D1을 정본으로 두고 실제 coding은 library-first라고 drift를 이미 명시한다. 앞 절반만 취하면 해소된 경고를 active contradiction처럼 계산한다. | parent 전체 D4 cell과 implementation plan이 쓰는 순서를 확인한다. | source가 child order를 canonical로 확정한다. | REJECT | N/A |
| L4-F04 | SPA Direct의 HttpOnly refresh cookie는 TMB를 요구한다. | taxonomy가 기존 Resource Server의 작은 refresh endpoint를 AP1 변형으로 허용하면 명칭은 유지할 수 있다. 그래도 server-side custody, Set-Cookie, CSRF, rotation 책임이 추가되어 trust boundary가 달라진다. | architecture owner의 AP1 정의와 실제 refresh endpoint, cookie issuer, CSRF test를 확인한다. | 문서도 사실상 TMB라 인정하지만 기본 선택을 닫지 않았다. | KEEP | High |
| L4-F05 | 근거를 해소한 D3와 D6가 Claims에서 unsupported로 남았다. | Claims 표가 조사 전 history라면 모순이 아니고 runtime E2E는 여전히 남는다. 다만 날짜와 resolved 표시 없이 active queue에 있어 문헌 근거와 runtime 검증을 혼동시킨다. | Claims 상태 transition과 후속 task의 중복 조사 여부를 확인한다. | stale이지만 영향은 재조사 비용에 가깝다. | DOWNGRADE | Low |
| L4-F06 | cookie-CSRF를 위임한 audience owner에 관련 decision이 없다. | TMB 변형을 채택하지 않으면 cookie CSRF concern이 생기지 않고 unseen BFF owner가 소유할 수도 있다. 두 note만으로 project-wide owner 부재를 확정하기에는 enumeration이 부족하다. | 선택 storage profile, refresh endpoint owner, CSRF D-row와 negative test를 확인한다. | 조건부 concern이고 전역 context가 부족하다. | DOWNGRADE | Low |
| L4-F07 | custom audience validator 필수 목표와 property-first 선택이 어긋난다. | custom validator를 학습 예제로 두고 production baseline은 property-first로 둘 수 있다. D1과 구현 가이드가 단일 audience는 property, 복합은 custom으로 이미 조건을 기록해 실제 선택은 종결됐다. | TODO와 plan에서 baseline과 비교 학습 단계가 분리됐는지 확인한다. | 상단 wording만 stale이라 severity를 낮춘다. | DOWNGRADE | Low |
| L4-F08 | expected audience가 backend-client-id와 spa-client로 갈린다. | API audience를 의도적으로 spa-client와 같게 둘 수 있다. 그러나 owner는 backend mapper를 필수로 하고 consumer는 다른 literal을 고정하며 그 동일성 결정이 없어 정상 acceptance를 신뢰할 수 없다. | realm client IDs, Audience mapper, token aud, backend expected value와 negative test를 캡처한다. | 별도 client 전제에서 401 또는 잘못된 허용으로 이어진다. | KEEP | High |
| L4-F09 | audience owner 위임 뒤 role note가 세부를 재명세한다. | 한 파일에서 RS setup과 RBAC를 함께 구현하는 fold-in snapshot일 수 있고 owner 주석이 정본을 알려준다. 다만 복제된 spa-client 값이 owner와 이미 갈려 drift가 현실화했다. | role PR이 owner 없이 audience 값을 바꾸는지와 checker 추적 결과를 확인한다. | ownership 문제는 남지만 L4-F08과 영향이 겹친다. | DOWNGRADE | Low |
| L4-F10 | realm-role-only의 single-client 전제가 multi-client base와 stale하다. | RBAC branch는 현재 한 client만 대상으로 하거나 여러 client에 같은 realm-global policy를 의도할 수 있다. 서로 다른 rollout 단계와 authz scope를 비교해 stale이라고 단정했다. | rollout target과 각 client permission namespace 필요를 realm export로 확인한다. | multi-client가 곧 client role 필요를 뜻하지 않는다. | DOWNGRADE | Low |
| L4-F11 | 3-leg 표기에 Backend가 없지만 Hop 3 verifier는 Backend다. | 3-leg는 federation actor chain의 학습 약칭이고 hop table은 API validation을 더한 별도 sequence일 수 있다. 실제 표에는 Backend가 있어 구현 verifier 누락이라는 failure도 이미 완화된다. | diagram과 acceptance가 Backend validation을 빠뜨리는지 확인한다. | naming ambiguity만으로 독립 defect를 구성하기 어렵다. | REJECT | N/A |
| L4-F12 | 면접 문장은 nonce 자동 처리를 사실로 말하지만 D3는 미검증이다. | OIDC broker가 nonce를 처리할 것이라는 기대는 합리적이고 표준 의무를 요약했을 수 있다. 그래도 특정 Keycloak version의 자동 처리를 외부 답변으로 말하려면 표준 의무와 제품 관측을 나눠야 한다. | source 또는 HAR에서 nonce 발행, upstream token, callback 대조와 거부를 확인한다. | 문서가 제품 동작 미검증을 인정하면서 외부 문장은 사실형이다. | KEEP | Medium |
| L4-F13 | out-of-scope mapper와 linking을 구체 정책으로 재진술한다. | 해당 row는 관심사 이름과 owner link만 한 줄로 요약해 상세 mechanism 복제보다 Reference-Only가 허용하는 pointer와 요약에 가깝다. 실제 설정값이나 예외도 재명세하지 않는다. | row가 owner D-number와 한 줄 조건만 갖고 구체 값이 없는지 확인한다. | 상세 재진술 분류가 source 추상도와 맞지 않는다. | REJECT | N/A |
| L4-F14 | Traefik note가 P3A를 edge-auth pattern으로 잘못 귀속한다. | 같은 EC2에 oauth2-proxy를 추가한 배포 변형은 가능하다. 그러나 P3A owner가 SPA Direct와 backend JWT validation을 정의하므로 배포 가능성만으로 인증 pattern ID를 바꿀 수 없다. | AP taxonomy, P3A component diagram, Traefik target pattern을 대조한다. | 배포 축과 authentication architecture 축이 혼동됐다. | KEEP | High |
| L4-F15 | middleware가 302를 생성한다는 설명과 pass-through D3가 갈린다. | browser 관점에서는 Traefik endpoint에서 302를 받으므로 middleware가 발급했다는 관찰 축약일 수 있다. 생성 주체와 전달 주체 구분은 장애 분석에 유용하지만 architecture 자체를 바꾸는 차이는 아니다. | oauth2-proxy와 Traefik log, Location header origin을 함께 추적한다. | 책임 표현 drift는 맞지만 영향이 낮다. | DOWNGRADE | Low |
| L4-F16 | relative API와 token URL이 static-only nginx로 향한다. | 실제 nginx나 dev server에 문서 밖 proxy가 있으면 정상이다. 그러나 parent가 nginx를 static-only로 명시하고 3-port CORS topology를 선택해 그런 proxy를 배제한다. | deployed nginx config, browser resolved URL, backend와 Keycloak access log를 확인한다. | 현재 명세대로면 두 request가 nginx에서 실패한다. | KEEP | High |
| L4-F17 | oidc-client-ts default를 localStorage라 하면서 미증명으로 둔다. | pinned version에서 실제 default가 localStorage라면 기술값은 맞고 raw evidence만 보강하면 된다. stateStore와 userStore가 다른 default를 가져 단일 문장이 지나치게 뭉쳤을 수도 있다. | lock version의 공식 API와 runtime storage keys로 두 store와 token persistence를 확인한다. | evidence drift는 맞지만 기술값 오류는 미확정이다. | DOWNGRADE | Low |
| L4-F18 | silent SSO는 제외했는데 automaticSilentRenew mechanism은 미결정이다. | 문서가 이를 Claims To Verify로 명시해 아직 선택되지 않은 planned work로 관리한다. 미결정 상태는 양립 불가능한 active decision이 아니고 network trace로 닫는 gate도 있다. | pinned config와 expiry trace에서 refresh grant 또는 iframe 호출을 확인한다. | explicit open question을 consistency defect로 올렸다. | REJECT | N/A |
| L4-F19 | Phase C2 뒤 active 표가 codes와 CORS와 redaction을 미구현으로 둔다. | raw branch note가 설계 시점과 as-built audit를 시간순으로 보존하고 하단 C2 표가 최신임을 표시한다면 오래된 행은 history다. 신규 task가 옛 표를 소비하지 않으면 runtime 위험보다 탐색 비용 문제다. | current code와 C2 artifact, 옛 표의 downstream 참조를 확인한다. | stale은 맞지만 실제 영향 근거가 부족하다. | DOWNGRADE | Medium |
| L4-F20 | decoder와 clock skew가 세 상태로 기록된다. | custom wrapper 안에서 vendor default validator를 쓸 수 있어 custom bean과 default leeway는 동시에 성립한다. code owner를 보지 않고 세 문장을 모두 상호 배타적으로 분류한 부분이 있다. | bean graph, validator chain, skew boundary test로 현재 mechanism을 확인한다. | custom 미작성 drift는 남지만 source만으로 정확한 상태를 못 정한다. | DOWNGRADE | Medium |
| L4-F21 | public-path 계약이 reflection과 env snapshot을 함께 유지한다. | 앞쪽 정정이 reflection 폐기와 env gate의 한계를 명시하고 C2 표도 current task를 제공하면 뒤 Test Contract는 역사 초안으로 식별할 수 있다. 실제 구현자가 정정을 읽는다면 옛 task 재생성 가능성은 제한된다. | snapshot task, CI wiring, reflection command의 downstream 참조를 확인한다. | active-looking command drift는 남지만 current mechanism은 정정됐다. | DOWNGRADE | Medium |
## Consolidation
- Issuer/network: L1-F08을 유지하고 L4-F01은 중복 제거.
- Audience: L4-F08을 대표로 두고 L1-F14는 제거, L4-F02와 L4-F09는 보조 finding.
- SPA Direct/TMB: L4-F04를 대표로 두고 L2-F09는 제거, L4-F06은 조건부 owner risk.
- Account linking: L2-F02를 대표로 유지하고 L1-F03은 낮은 우선순위로 조정.
- Rotation: L3-F11, L3-F12, L3-F13을 한 승인 단위에서 함께 검증.
- Security baseline: L4-F19, L4-F20, L4-F21을 as-built 대조 한 번으로 처리.
## Machine Summary
~~~wiki-verdict
agent: wiki-adversarial-reviewer
KEEP: L1-F01,L1-F08,L1-F09,L1-F10,L2-F02,L2-F04,L2-F06,L2-F07,L2-F08,L3-F03,L3-F04,L3-F07,L3-F08,L3-F14,L3-F15,L4-F02,L4-F04,L4-F08,L4-F12,L4-F14,L4-F16
DOWNGRADE: L1-F02,L1-F03,L1-F04,L1-F05,L1-F07,L1-F11,L1-F12,L2-F03,L2-F05,L3-F01,L3-F05,L3-F09,L3-F10,L3-F11,L3-F12,L3-F13,L3-F16,L4-F05,L4-F06,L4-F07,L4-F09,L4-F10,L4-F15,L4-F17,L4-F19,L4-F20,L4-F21
REJECT: L1-F06,L1-F13,L1-F14,L2-F01,L2-F09,L3-F02,L3-F06,L4-F01,L4-F03,L4-F11,L4-F13,L4-F18
~~~
~~~wiki-stats
agent: wiki-adversarial-reviewer
found: 60
processed: 60
dropped: 0
~~~
@@ -0,0 +1,194 @@
# Keycloak branch-note consistency 적용 상태
**일자:** 2026-07-18
**범위:** Keycloak 관련 branch-note 38개
**상태:** COMPLETE
**원본 리뷰:** `docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency-report.md`
## 1. Summary
- 38개 문서를 4개 비중복 lane으로 `READ_FULL` 처리했다.
- 36개 문서를 수정했고, 2개 문서는 리뷰 finding과 직접 관련된 변경이 없어 본문을 유지했다.
- adversarial review의 60개 finding을 모두 판정했다. `KEEP 21 + DOWNGRADE 27`의 retained 48건은 반영했고, `REJECT 12`는 독립 결함으로 재도입하지 않았다.
- `REJECT` 중 다른 retained finding과 같은 원인을 공유하거나 active 문구 정리가 필요했던 항목은 대표 owner 수정에 통합했다.
- 후속 의미 재감사 20건은 `Ready`, blocking 0, should-fix 0으로 종료됐다.
## 2. Evidence Matrix
| Path | Status | Evidence | Application |
|---|---|---|---|
| `raw/branch-notes/experiment-nplus1-highlight-feed.md` | READ_FULL | lines 1-326 | MODIFIED — branch-note 필수 구조 보강 |
| `raw/branch-notes/feature-authentication-authorization-contract.md` | READ_FULL | lines 1-341 | MODIFIED — raw role / authority 경계 정렬 |
| `raw/branch-notes/feature-boundary-validation-mapping-contract.md` | READ_FULL | lines 1-407 | NO_CHANGE — 적용 대상 finding 없음 |
| `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md` | READ_FULL | lines 1-228 | MODIFIED — linking owner 직접 참조와 UX 책임 분리 |
| `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md` | READ_FULL | lines 1-279 | MODIFIED — persistent federation key 단일 owner |
| `raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md` | READ_FULL | lines 1-274 | MODIFIED — SPA/TMB 조건과 stale risk 정리 |
| `raw/branch-notes/feature-keycloak-docker-compose-stack.md` | READ_FULL | lines 1-286 | MODIFIED — issuer/JWKS profile owner 위임과 Compose wiring 분리 |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md` | READ_FULL | lines 1-323 | MODIFIED — P1B header-only trust 정렬 |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md` | READ_FULL | lines 1-316 | MODIFIED — ingress/auth-service 선택축과 cookie custody 구분 |
| `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md` | READ_FULL | lines 1-193 | MODIFIED — backend validation 실제 owner 직접 참조 |
| `raw/branch-notes/feature-keycloak-first-broker-login-flow.md` | READ_FULL | lines 1-263 | MODIFIED — silent-link 방지와 custom SPI variant 분리 |
| `raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md` | READ_FULL | lines 1-238 | MODIFIED — IdP sync owner와 role freshness owner 분리 |
| `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md` | READ_FULL | lines 1-262 | MODIFIED — basic scope 예외·JS origins·decision ref 정렬 |
| `raw/branch-notes/feature-keycloak-header-spoofing-defense.md` | READ_FULL | lines 1-255 | NO_CHANGE — 적용 대상 finding 없음 |
| `raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md` | READ_FULL | lines 1-284 | MODIFIED — HSTS explicit configuration 경계 |
| `raw/branch-notes/feature-keycloak-idp-brokering-google-client.md` | READ_FULL | lines 1-251 | MODIFIED — server-side brokering JS origins owner 참조 |
| `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md` | READ_FULL | lines 1-153 | MODIFIED — mapper-level override와 linking owner 위임 |
| `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md` | READ_FULL | lines 1-467 | MODIFIED — foreign detail을 owner pointer로 축소 |
| `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md` | READ_FULL | lines 1-416 | MODIFIED — token custody/audience owner 직접 참조 |
| `raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch.md` | READ_FULL | lines 1-284 | MODIFIED — public issuer/internal JWKS profile owner |
| `raw/branch-notes/feature-keycloak-nginx-auth-request-integration.md` | READ_FULL | lines 1-306 | MODIFIED — auth_request/proxy body forwarding 층위 구분 |
| `raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md` | READ_FULL | lines 1-275 | MODIFIED — local JWT validation 용어와 decision ref 정렬 |
| `raw/branch-notes/feature-keycloak-patterns.md` | READ_FULL | lines 1-223 | MODIFIED — superseded naming과 hub 필수 구조 보강 |
| `raw/branch-notes/feature-keycloak-pkce-flow-stages.md` | READ_FULL | lines 1-188 | MODIFIED — target UI label과 필수 구조 보강 |
| `raw/branch-notes/feature-keycloak-public-domain-tunneling.md` | READ_FULL | lines 1-220 | MODIFIED — provider selection/operational profile owner 분리 |
| `raw/branch-notes/feature-keycloak-realm-client-export.md` | READ_FULL | lines 1-239 | MODIFIED — import wiring 위임과 credential 검증 상태 정리 |
| `raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md` | READ_FULL | lines 1-255 | MODIFIED — 실행 관찰 owner로 범위 축소 |
| `raw/branch-notes/feature-keycloak-refresh-token-rotation.md` | READ_FULL | lines 1-283 | MODIFIED — policy owner와 Max Reuse 검증 계약 정리 |
| `raw/branch-notes/feature-keycloak-reverse-proxy-headers.md` | READ_FULL | lines 1-297 | MODIFIED — Caddy prefix 보존과 하위 owner 경계 |
| `raw/branch-notes/feature-keycloak-single-ec2-google-federation.md` | READ_FULL | lines 1-349 | MODIFIED — integration invariant와 child detail owner 분리 |
| `raw/branch-notes/feature-keycloak-single-ec2-no-google.md` | READ_FULL | lines 1-344 | MODIFIED — audience owner, library-first, pure-SPA 기준선 |
| `raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md` | READ_FULL | lines 1-258 | MODIFIED — pure SPA memory-only와 TMB/BFF variant 분리 |
| `raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md` | READ_FULL | lines 1-270 | MODIFIED — expected audience `backend-client-id` 단일화 |
| `raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md` | READ_FULL | lines 1-216 | MODIFIED — RBAC 범위와 audience owner 참조 분리 |
| `raw/branch-notes/feature-keycloak-three-leg-trust-chain.md` | READ_FULL | lines 1-229 | MODIFIED — OIDC nonce 의무와 제품 동작 검증 상태 분리 |
| `raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md` | READ_FULL | lines 1-238 | MODIFIED — AP4 분류와 redirect 생성/전달 주체 분리 |
| `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md` | READ_FULL | lines 1-259 | MODIFIED — library-first, absolute/runtime endpoint, explicit memory store |
| `raw/branch-notes/feature-security-operational-baseline.md` | READ_FULL | lines 1-416 | MODIFIED — historical/current-known/needs-confirmation 분리 |
## 3. Finding Disposition
상태 의미:
- `APPLIED`: retained finding을 owner-first 기준으로 반영.
- `CONSOLIDATED`: adversarial `REJECT`를 독립 결함으로 적용하지 않고 대표 retained finding 또는 active 문구 정리에 통합.
- `REJECTED_NO_CHANGE`: adversarial 반증을 수용해 별도 수정하지 않음.
| Finding | Adversarial | Application |
|---|---|---|
| L1-F01 | KEEP | APPLIED |
| L1-F02 | DOWNGRADE | APPLIED |
| L1-F03 | DOWNGRADE | APPLIED |
| L1-F04 | DOWNGRADE | APPLIED |
| L1-F05 | DOWNGRADE | APPLIED |
| L1-F06 | REJECT | REJECTED_NO_CHANGE |
| L1-F07 | DOWNGRADE | APPLIED |
| L1-F08 | KEEP | APPLIED |
| L1-F09 | KEEP | APPLIED |
| L1-F10 | KEEP | APPLIED |
| L1-F11 | DOWNGRADE | APPLIED |
| L1-F12 | DOWNGRADE | APPLIED |
| L1-F13 | REJECT | REJECTED_NO_CHANGE |
| L1-F14 | REJECT | CONSOLIDATED — L4-F08 audience owner/value 정리 |
| L2-F01 | REJECT | CONSOLIDATED — unsupported CVE 문구의 active 영향만 축소 |
| L2-F02 | KEEP | APPLIED |
| L2-F03 | DOWNGRADE | APPLIED |
| L2-F04 | KEEP | APPLIED |
| L2-F05 | DOWNGRADE | APPLIED |
| L2-F06 | KEEP | APPLIED |
| L2-F07 | KEEP | APPLIED |
| L2-F08 | KEEP | APPLIED |
| L2-F09 | REJECT | CONSOLIDATED — L4-F04 token custody 경계에 통합 |
| L3-F01 | DOWNGRADE | APPLIED |
| L3-F02 | REJECT | REJECTED_NO_CHANGE |
| L3-F03 | KEEP | APPLIED |
| L3-F04 | KEEP | APPLIED |
| L3-F05 | DOWNGRADE | APPLIED |
| L3-F06 | REJECT | REJECTED_NO_CHANGE |
| L3-F07 | KEEP | APPLIED |
| L3-F08 | KEEP | APPLIED |
| L3-F09 | DOWNGRADE | APPLIED |
| L3-F10 | DOWNGRADE | APPLIED |
| L3-F11 | DOWNGRADE | APPLIED |
| L3-F12 | DOWNGRADE | APPLIED |
| L3-F13 | DOWNGRADE | APPLIED |
| L3-F14 | KEEP | APPLIED |
| L3-F15 | KEEP | APPLIED |
| L3-F16 | DOWNGRADE | APPLIED |
| L4-F01 | REJECT | CONSOLIDATED — L1-F08 issuer/network profile에 통합 |
| L4-F02 | KEEP | APPLIED |
| L4-F03 | REJECT | CONSOLIDATED — active manual-first 문구를 historical/superseded 처리 |
| L4-F04 | KEEP | APPLIED |
| L4-F05 | DOWNGRADE | APPLIED |
| L4-F06 | DOWNGRADE | APPLIED |
| L4-F07 | DOWNGRADE | APPLIED |
| L4-F08 | KEEP | APPLIED |
| L4-F09 | DOWNGRADE | APPLIED |
| L4-F10 | DOWNGRADE | APPLIED |
| L4-F11 | REJECT | REJECTED_NO_CHANGE |
| L4-F12 | KEEP | APPLIED |
| L4-F13 | REJECT | REJECTED_NO_CHANGE |
| L4-F14 | KEEP | APPLIED |
| L4-F15 | DOWNGRADE | APPLIED |
| L4-F16 | KEEP | APPLIED |
| L4-F17 | DOWNGRADE | APPLIED |
| L4-F18 | REJECT | REJECTED_NO_CHANGE |
| L4-F19 | DOWNGRADE | APPLIED |
| L4-F20 | DOWNGRADE | APPLIED |
| L4-F21 | DOWNGRADE | APPLIED |
## 4. Owner Decisions Applied
1. Audience는 backend API용 `backend-client-id` 하나를 owner 값으로 사용한다.
2. Docker dev는 public issuer identity와 bridge-container JWKS retrieval address를 분리하며, exact profile은 issuer mismatch branch D6이 소유한다.
3. P1A/P1B backend trust는 header-only이고 Google federation 여부만 다르다.
4. Google persistent federation key는 `(IdP alias, sub)` owner 하나로 수거한다.
5. `email_verified=false` core 정책은 silent auto-link 차단이며, hard-reject는 구현된 custom SPI variant가 있을 때만 별도 채택한다.
6. Pure SPA는 access/refresh token 모두 memory-only이고 reload 시 재인증한다. HttpOnly refresh cookie는 TMB/BFF variant다.
7. Stable Google callback은 named tunnel과 managed custom domain을 사용하고 quick tunnel은 임시 dev 용도로 제한한다.
8. Caddy method B는 `/keycloak` prefix를 보존한다.
9. 실제 SPA 구현 순서는 `oidc-client-ts` 우선이며 manual PKCE는 비교 학습 단계다.
10. Traefik + oauth2-proxy는 edge ForwardAuth AP4이며, SPA 요청 URL은 static-only nginx topology에 맞춘 runtime/absolute endpoint를 사용한다.
## 5. Verification
| Gate | Result |
|---|---|
| Scope partition | 38 = 10 + 10 + 10 + 8, duplicate 0, missing 0 |
| Full-read coverage | 38/38 READ_FULL |
| Structure lint | 38/38 PASS |
| Deterministic consistency, Keycloak scope | 0 findings |
| Deterministic consistency, whole repository | 136 pre-existing out-of-scope findings (`BARE_OWNER_REF` 74, `BARE_DECISION_REF` 62) |
| Link validation | 38/38 per-file lint PASS; Keycloak scope broken link 0 |
| Semantic audit round 1 | Not-ready: blocking 1, should-fix 3 |
| Semantic audit round 2 | Ready: blocking 0, should-fix 0; 20/20 processed |
| Semantic round 2 classifications | original candidates: CONSISTENT 11, NOT_AN_EDGE 9; current actual/replacement edges 14/14 CONSISTENT |
| `git diff --check` | PASS |
실행한 대표 명령:
```bash
python3 .claude/hooks/wiki_structure_lint.py --file <각 scope 문서>
python3 .claude/hooks/wiki_consistency_check.py --all
python3 .claude/hooks/wiki_structure_lint.py --all --links-only
git diff --check -- <scope 문서>
```
## 6. Residual Risk
문서 간 의미 모순은 이번 scope에서 닫혔다. 다음 항목은 문서에 `planned` 또는 `needs-confirmation`으로 유지했다.
- target Keycloak release의 account unlink guard와 Admin UI label 확인
- Docker Linux host-gateway JWKS E2E 및 key rotation
- realm export의 credential/secret 포함 범위
- `email_verified=false`와 First Broker Login 실제 flow E2E
- audience mapper가 발급 token의 `aud``backend-client-id`를 넣는지 확인
- nonce wire trace, Traefik redirect 전달, Caddy route, tunnel callback E2E
- Max Reuse 0/1과 refresh token reuse 영향 측정
- pinned SPA library의 storage/renew 동작 확인
- Phase C2 security baseline의 실제 code/test 재확인
저장소 전체 consistency 136건과 links-only 29건은 Keycloak scope 밖의 기존 항목이며 이번 적용에서 수정하지 않았다.
## 7. Stats
```wiki-stats
found: 60
processed: 60
dropped: 0
modified_files: 36
no_change_files: 2
semantic_candidates: 20
semantic_ready: true
```
@@ -0,0 +1,52 @@
# Evidence Matrix
범위 정의: `raw/branch-notes/` 아래 Markdown 중 파일명 또는 본문에 대소문자 무관 `keycloak`이 있는 파일. `rg -il --glob '*.md' 'keycloak' raw/branch-notes | sort` 결과를 고정 입력으로 사용했다.
| Path | Status | Evidence | Extracted facts |
|---|---|---|---|
| `raw/branch-notes/experiment-nplus1-highlight-feed.md` | READ_FULL | L1-L233 | N+1 실험·측정 계약이며 Keycloak은 예정 형제 주제로만 등장한다. |
| `raw/branch-notes/feature-authentication-authorization-contract.md` | READ_FULL | L1-L341 | application authz PEP, raw role→permission registry, Keycloak role claim 입력 경계. |
| `raw/branch-notes/feature-boundary-validation-mapping-contract.md` | READ_FULL | L1-L407 | validation/mapping 경계 계약이며 Keycloak은 sample seed 설명에 등장한다. |
| `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md` | READ_FULL | L1-L228 | SPA account-link UX, First Broker Login, link/unlink, CIAL, linked-account read path. |
| `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md` | READ_FULL | L1-L279 | `sub` federation key, local-account collision, re-auth, unlink lockout, Sync Mode. |
| `raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md` | READ_FULL | L1-L274 | SPA Direct/BFF 선택, token holder, session, OAuth 2.1 조건. |
| `raw/branch-notes/feature-keycloak-docker-compose-stack.md` | READ_FULL | L1-L287 | Keycloak/Postgres/app/nginx Compose topology, health, import, issuer/network. |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md` | READ_FULL | L1-L325 | P1B Google brokering, P1A 불변식, account matching, child-owner 위임. |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md` | READ_FULL | L1-L316 | P1A header-trust ForwardAuth, oauth2-proxy/nginx/Traefik, cookie session. |
| `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md` | READ_FULL | L1-L193 | P2A→P2B login-path zero-change, token claim baseline, account-link exclusion. |
| `raw/branch-notes/feature-keycloak-first-broker-login-flow.md` | READ_FULL | L1-L261 | Confirm Link 소유증명, silent auto-link 차단, hard-reject SPI 별도 트랙. |
| `raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md` | READ_FULL | L1-L238 | attribute-mapping owner, IdP default IMPORT, role mapping 위임. |
| `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md` | READ_FULL | L1-L261 | exact redirect URI, basic-scope 예외, JavaScript origins 비움. |
| `raw/branch-notes/feature-keycloak-header-spoofing-defense.md` | READ_FULL | L1-L255 | NetworkPolicy·SG·shared secret·trusted proxy 방어 경계. |
| `raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md` | READ_FULL | L1-L284 | TLS 종단 대안과 명시적 HSTS 구성. |
| `raw/branch-notes/feature-keycloak-idp-brokering-google-client.md` | READ_FULL | L1-L251 | Google IdP client, `trustEmail=false`, callback·origin 위임. |
| `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md` | READ_FULL | L1-L148 | role/RBAC owner, FORCE, email verification 강제 주장. |
| `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md` | READ_FULL | L1-L512 | P2B composition hub, zero-change brokering, SMTP 사실, foreign-decision 상세. |
| `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md` | READ_FULL | L1-L416 | P2A SPA Direct 정의, refresh-token 저장, brokering 비교. |
| `raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch.md` | READ_FULL | L1-L287 | issuer/JWKS 분리 대안, 미승인 대안, 외부 owner 충돌. |
| `raw/branch-notes/feature-keycloak-nginx-auth-request-integration.md` | READ_FULL | L1-L306 | `auth_request` body handling, browser/API failure routing, upstream ownership edges. |
| `raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md` | READ_FULL | L1-L275 | bearer-token 용어, nginx decision references, discovery startup dependency. |
| `raw/branch-notes/feature-keycloak-patterns.md` | READ_FULL | L1-L205 | retired numbered naming과 retained D2. |
| `raw/branch-notes/feature-keycloak-pkce-flow-stages.md` | READ_FULL | L1-L167 | PKCE S256 ownership과 Admin Console terminology. |
| `raw/branch-notes/feature-keycloak-public-domain-tunneling.md` | READ_FULL | L1-L220 | quick/named Cloudflare Tunnel domain과 tunnel-choice ownership. |
| `raw/branch-notes/feature-keycloak-realm-client-export.md` | READ_FULL | L1-L238 | import wiring ownership, credential export assertions, PKCE terminology. |
| `raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md` | READ_FULL | L1-L254 | rotation demonstration, Max Reuse semantics, back-channel logout scope. |
| `raw/branch-notes/feature-keycloak-refresh-token-rotation.md` | READ_FULL | L1-L284 | rotation contract, family invalidation, receiver endpoint delegation. |
| `raw/branch-notes/feature-keycloak-reverse-proxy-headers.md` | READ_FULL | L1-L297 | Caddy path behavior, Keycloak proxy variables, delegated security decisions. |
| `raw/branch-notes/feature-keycloak-single-ec2-google-federation.md` | READ_FULL | L1-L370 | parent integration decisions와 delegated tunnel/proxy details. |
| `raw/branch-notes/feature-keycloak-single-ec2-no-google.md` | READ_FULL | L1-L342 | base topology, issuer reachability, PKCE order, component ownership. |
| `raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md` | READ_FULL | L1-L256 | access/refresh token storage, TMB 경계, CSRF 위임. |
| `raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md` | READ_FULL | L1-L268 | audience 검증 owner와 property/custom 구현 선택. |
| `raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md` | READ_FULL | L1-L231 | RBAC/role mapping과 audience-owner 위임. |
| `raw/branch-notes/feature-keycloak-three-leg-trust-chain.md` | READ_FULL | L1-L229 | federation/API validation hop, nonce, foreign claim/linking 정책. |
| `raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md` | READ_FULL | L1-L237 | Traefik ForwardAuth routing과 P3A pattern 비교. |
| `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md` | READ_FULL | L1-L257 | oidc-client-ts/manual PKCE, API/token URL, silent renew. |
| `raw/branch-notes/feature-security-operational-baseline.md` | READ_FULL | L1-L417 | auth code/CORS/redaction/JWT/public-path operational contracts. |
## Completeness
- Enumerated `N=38`
- Evidence rows `M=38`
- `READ_FULL R=38`, `BLOCKED B=0`
- Path-set difference: missing `0`, duplicate `0`, extra `0`
- Completeness formula: `M == N AND P == R`에서 per-file index section `P=38`; 결과 `38 == 38 AND 38 == 38`.
@@ -0,0 +1,615 @@
## Lane Inventory
| Path | Status | Evidence Lines | Extracted Facts |
|---|---|---|---|
| `raw/branch-notes/experiment-nplus1-highlight-feed.md` | READ_FULL | L1-L233 | N+1 실험·측정 계약. Keycloak은 예정 형제 주제로만 언급되며 Keycloak 결정 없음. |
| `raw/branch-notes/feature-authentication-authorization-contract.md` | READ_FULL | L1-L341 | application authz PEP, raw role→permission registry, Keycloak role claim 입력 경계. |
| `raw/branch-notes/feature-boundary-validation-mapping-contract.md` | READ_FULL | L1-L407 | validation/mapping 경계 계약. Keycloak은 sample seed 설명에만 등장하며 Keycloak 결정 없음. |
| `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md` | READ_FULL | L1-L228 | SPA account-link UX, First Broker Login, link/unlink, CIAL, linked-account read path. |
| `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md` | READ_FULL | L1-L279 | `sub` federation key, local-account collision, re-auth, unlink lockout, Sync Mode. |
| `raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md` | READ_FULL | L1-L274 | SPA Direct/BFF 선택, token holder, session, OAuth 2.1 조건. |
| `raw/branch-notes/feature-keycloak-docker-compose-stack.md` | READ_FULL | L1-L287 | Keycloak/Postgres/app/nginx Compose topology, health, import, issuer/network. |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md` | READ_FULL | L1-L325 | P1B Google brokering, P1A 불변식, account matching, child-owner 위임. |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md` | READ_FULL | L1-L316 | P1A header-trust ForwardAuth, oauth2-proxy/nginx/Traefik, cookie session. |
| `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md` | READ_FULL | L1-L193 | P2A→P2B login-path zero-change, token claim baseline, account-link exclusion. |
## 1. `experiment-nplus1-highlight-feed.md`
**0-finding justification:** Keycloak 일관성 관점에서 3개 항목을 확인했다.
1. Keycloak ownership — 해당 없음: 예정 형제 주제로만 언급됨(`:25`).
2. 인증 흐름 — 해당 없음: feed 측정은 HTTP 인증과 무관하다고 경계를 둠(`:108`).
3. Decision Evidence Map — D-01~D-10은 N+1·CQRS 실험 결정이며 Keycloak 설정을 소유하지 않음(`:64-77`).
따라서 이 파일에 대한 Keycloak inconsistency finding은 0건이다. 문서 전체 품질 판정은 본 lane 범위가 아니다.
## 2. `feature-authentication-authorization-contract.md`
### L1-F01: D3의 `ROLE_*` 입력과 실제 raw-role registry 계약이 다르다
- **Source file:** `raw/branch-notes/feature-authentication-authorization-contract.md`
- **Source quote:**
> AuthorizationPort 가 `ROLE_*` authorities → role→permission registry 확장 → 요구 permission 포함 판정. 매핑 source = app-side config 기본
- **Counterpart/current quote:**
> ca-tmpl `AuthenticatedUser.roles`(`adapter-web`, `Set<String>`)는 **raw role 문자열**(`admin`/`user` — Keycloak 원본, prefix 없음)을 담는다.
- **Source lines:** `raw/branch-notes/feature-authentication-authorization-contract.md:124`, `raw/branch-notes/feature-authentication-authorization-contract.md:169`
- **Severity:** Medium
- **Classification:** `FACT — STALE_SUMMARY`; 실패 영향은 `INFERENCE`.
- **Claim:** D3는 registry 입력을 `ROLE_*` authority로 정의하지만 구현 명세는 prefix 없는 raw role을 canonical 입력으로 정의한다.
- **Real-world assumptions:**
1. 구현이 `:169`의 raw-role contract를 따른다.
- **Falsifies if:** adapter가 registry 호출 전에 raw role을 `ROLE_*`로 변환한다.
- **Verify:** `AuthorizationAdapter` 입력과 `RolePermissionProperties` key를 함께 확인한다.
2. 운영자가 D3를 보고 config key를 작성한다.
- **Falsifies if:** config schema가 raw key만 허용하고 startup에서 prefix를 거부한다.
- **Verify:** `application.yml` binding test에 `admin``ROLE_ADMIN` 케이스를 추가한다.
- **Concrete failure mode:** 문서대로 `ROLE_ADMIN`을 registry key로 넣고 runtime이 `admin`을 조회하면 fail-closed로 permission 0개가 되어 정당한 요청이 403으로 거부된다.
- **Falsification condition:** `ROLE_*`는 Spring adapter 내부의 중간 표현일 뿐이며 D3가 raw-role 변환 경계를 명시하도록 이미 갱신돼 있다면 finding은 무효다.
- **Required action:** D3를 “Keycloak raw role → adapter에서 Spring authority 파생, authz registry는 raw role consume”으로 고치고 security-baseline owner 결정을 pointer로 연결한다.
- **Why this action:** as-built contract와 config vocabulary를 하나로 만들어 prefix drift를 없앤다.
- **Alternatives considered:**
1. **Raw role canonical** — 현재 코드와 맞고 문서만 수정한다.
2. **`ROLE_*` canonical** — Spring layer에는 단순하지만 registry/config migration이 필요하다.
3. **Permission claim 직접 consume** — IdP 중앙집중형에 맞지만 Keycloak 결합이 커진다.
4. **명시적 principal DTO에서 raw role/authority 둘 다 보존** — 경계가 선명하지만 모델이 늘어난다.
- **Counterargument:** D3의 `ROLE_*`가 단지 security-baseline 출력의 설명이고 실제 registry 입력은 `:169`에서 명확하다고 볼 수 있다. 사용자 확인 방법은 D3만 읽은 신규 운영자가 config key를 올바르게 작성할 수 있는지 문서 리뷰로 재현하는 것이다.
- **Required action verification:** raw-role/authority 용어를 grep하고 D3·§3·config example이 같은 vocabulary를 쓰는지 확인한다.
- **Verification command and observed output:**
```text
$ grep -nFo -- 'AuthorizationPort 가 `ROLE_*` authorities → role→permission registry 확장 → 요구 permission 포함 판정. 매핑 source = app-side config 기본' 'raw/branch-notes/feature-authentication-authorization-contract.md'
124:AuthorizationPort 가 `ROLE_*` authorities → role→permission registry 확장 → 요구 permission 포함 판정. 매핑 source = app-side config 기본
$ grep -nFo -- 'ca-tmpl `AuthenticatedUser.roles`(`adapter-web`, `Set<String>`)는 **raw role 문자열**(`admin`/`user` — Keycloak 원본, prefix 없음)을 담는다.' 'raw/branch-notes/feature-authentication-authorization-contract.md'
169:ca-tmpl `AuthenticatedUser.roles`(`adapter-web`, `Set<String>`)는 **raw role 문자열**(`admin`/`user` — Keycloak 원본, prefix 없음)을 담는다.
```
**Single-finding justification:** authN/authZ ownership, permission model, failure mapping, method-security enforcement, Keycloak role input의 5개 항목을 검토했다. lane 내부에서 양립하지 않는 항목은 role vocabulary 1건이다. security-baseline의 실제 owner 결정은 unseen이므로 cross-lane 후보로 분리했다.
## 3. `feature-boundary-validation-mapping-contract.md`
**0-finding justification:** Keycloak 일관성 관점에서 3개 항목을 확인했다.
1. In/Out scope와 decisions는 validation/mapping 책임만 다룸(`:60-101`).
2. Decision Evidence Map D1~D15는 mapper·validation·ArchUnit 계약임(`:258-278`).
3. Keycloak은 sample seed 설명에만 등장함(`:350`).
따라서 Keycloak 설계·흐름 inconsistency finding은 0건이다.
## 4. `feature-keycloak-account-linking-spa-ux.md`
### L1-F02: unlink 안전정책의 owner 요약이 승격 전 상태에 머문다
- **Source file:** `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md`
- **Source quote:**
> orphan 거부 메커니즘은 owner 브랜치 D3(역시 `needs-confirmation`)에서 추적
- **Counterpart/current quote:**
> Account Console self-service unlink lockout 방지는 **Keycloak 엔진이 서버에서 이미 강제**
- **Source lines:** `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:119`, `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:152`
- **Severity:** Medium
- **Classification:** `FACT — STALE_SUMMARY`.
- **Claim:** owner D3가 engine-source-backed 결정으로 승격됐지만 SPA UX의 D4 요약은 여전히 `needs-confirmation`으로 남아 있다.
- **Real-world assumptions:**
1. 배포 버전에서도 owner가 인용한 unlink guard가 유지된다.
- **Falsifies if:** 배포 release tag에서 guard가 제거·변경됐다.
- **Verify:** 배포 tag의 `LinkedAccountsResource`와 dev unlink 응답을 확인한다.
- **Concrete failure mode:** SPA UX 설계자가 server guard를 모른 채 중복 lockout API를 만들거나, 반대로 상태를 미확정으로 보고 UX 계약을 계속 보류한다.
- **Falsification condition:** 배포 버전 검증이 실패해 owner D3가 다시 `needs-confirmation`으로 내려가면 현재 요약은 stale하지 않다.
- **Required action:** D4·§Unlink·Claims의 요약을 `[[...sub-vs-email]] D3 — server guard confirmed; release-tag verification remains` 한 줄로 갱신한다.
- **Why this action:** evidence strength와 deployment-specific risk를 동시에 보존한다.
- **Alternatives considered:**
1. **Pointer + current summary** — 읽기 편하고 drift 전파가 필요하다.
2. **Pointer only** — drift가 적지만 UX 문맥이 약해진다.
3. **Version-pinned summary** — 정확하지만 Keycloak upgrade마다 갱신 비용이 든다.
- **Counterargument:** owner 근거가 `main` branch이므로 배포판에는 여전히 불확실성이 있다. 다만 이는 `UNSUPPORTED`가 아니라 “source-backed, release-tag 확인 필요” 상태다.
- **Verification command and observed output:**
```text
$ grep -nFo -- 'orphan 거부 메커니즘은 owner 브랜치 D3(역시 `needs-confirmation`)에서 추적' 'raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
119:orphan 거부 메커니즘은 owner 브랜치 D3(역시 `needs-confirmation`)에서 추적
$ grep -nFo -- 'Account Console self-service unlink lockout 방지는 **Keycloak 엔진이 서버에서 이미 강제**' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
152:Account Console self-service unlink lockout 방지는 **Keycloak 엔진이 서버에서 이미 강제**
```
### L1-F03: delegated `email_verified` 정책이 active owned decision으로도 남아 있다
- **Source file:** `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md`
- **Source quote:**
> **`email_verified=true`만 link 허용**
- **Counterpart/current quote:**
> `email_verified` 는 Keycloak *linking gate* 가 아니라 Trust Email(계정 생성 시 verified 표시) 설정
- **Source lines:** `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:102`, `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:117`
- **Severity:** Medium
- **Classification:** `FACT — RESTATED_FOREIGN_DECISION + ambiguous mechanism`.
- **Claim:** active Decisions는 이 branch가 link gate를 소유하는 것처럼 단정하지만, DEM은 owner 위임과 Trust Email/custom-step 구분을 기록한다.
- **Real-world assumptions:**
1. 구현자가 `:102`를 Keycloak 기본 `Trust Email` 설정으로 해석한다.
- **Falsifies if:** owner D2가 custom First Broker Login step을 구현하고 참조자가 이를 명시한다.
- **Verify:** unseen idp-mappers D2와 realm export의 authenticator 구성을 확인한다.
- **Concrete failure mode:** `Trust Email=true`만으로 linking을 제한했다고 오인해 Confirm Link/re-auth 통제가 빠지고, threat model과 실제 flow가 달라진다.
- **Falsification condition:** owner D2가 `email_verified` custom gate를 실제 소유·구현하고 `:102`가 그 pointer 요약으로만 읽히도록 고쳐졌다면 finding은 해소된다.
- **Required action:** `:102`의 owned decision을 제거하고 owner D2 pointer로 치환하며, Trust Email과 custom linking gate를 별개 용어로 적는다.
- **Why this action:** Reference-Only를 지키면서 설정 lever 혼동을 막는다.
- **Alternatives considered:**
1. **Trust Email만 사용** — 계정 생성 verified 표시에 적합하지만 linking proof는 아니다.
2. **Custom `email_verified` gate** — 정책은 강하지만 SPI/flow 구현이 필요할 수 있다.
3. **Confirm Link + secret proof** — account ownership을 검증하지만 UX 마찰이 있다.
4. **Collision linking 비활성화** — 단순하지만 기존 local account 연결을 제공하지 않는다.
- **Counterargument:** `:102`가 native setting이 아니라 상위 정책을 말한 것이라면 행동 자체는 유지할 수 있다. 그래도 owner와 mechanism을 함께 명시해야 오독을 막을 수 있다.
- **Verification command and observed output:**
```text
$ grep -nFo -- '**`email_verified=true`만 link 허용**' 'raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
102:**`email_verified=true`만 link 허용**
$ grep -nFo -- '`email_verified` 는 Keycloak *linking gate* 가 아니라 Trust Email(계정 생성 시 verified 표시) 설정' 'raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
117:`email_verified` 는 Keycloak *linking gate* 가 아니라 Trust Email(계정 생성 시 verified 표시) 설정
```
### L1-F04: SPA UX가 password 확인을 고정 흐름으로 말하지만 lane owner 후보는 email verification 기본을 기록한다
- **Source file:** `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md`
- **Source quote:**
> 또는 로그인 화면에서 Google로 처음 로그인했을 때 같은 email의 기존 계정이 있으면 'Confirm Link Existing Account' authenticator가 비밀번호 확인을 요구합니다.
- **Counterpart/current quote:**
> SMTP 설정 realm 은 "Verify Existing Account By Email" 이 기본 `ALTERNATIVE` 이므로 admin 이 명시 DISABLE 해야 password 재인증 실행
- **Source lines:** `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:34`, `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:151`
- **Severity:** High
- **Classification:** `FACT — CONTRADICTION`; authoritative flow owner는 unseen이므로 `NEEDS_CROSS_LANE_AUDIT`.
- **Claim:** 동일 email collision UX의 기본 verification method가 password와 email 두 가지로 서술된다.
- **Real-world assumptions:**
1. realm에 SMTP가 설정돼 있고 built-in first-broker-login flow를 사용한다.
- **Falsifies if:** Email authenticator가 disabled이거나 SMTP가 없다.
- **Verify:** First Broker Login execution config와 realm SMTP 설정을 확인한다.
2. SPA UX 문장이 사용자 안내·테스트 기대값으로 사용된다.
- **Falsifies if:** 문서가 security-first profile 한정이라고 표시돼 있다.
- **Verify:** UX copy와 E2E expected step을 비교한다.
- **Concrete failure mode:** 문서는 password 화면을 기대하지만 실제 realm은 email verification을 보여 테스트·사용자 안내가 어긋나거나, security-first 의도와 달리 secret proof가 생략된다.
- **Falsification condition:** first-broker owner D2가 Email authenticator disable을 고정하고 realm export가 이를 보장한다면 SPA 문장은 성립한다.
- **Required action:** first-broker owner에서 `email-default``password-step-up` profile 중 하나를 선택하고, SPA UX는 그 D-row를 pointer+1줄로만 참조한다.
- **Why this action:** UX, threat model, realm execution config를 같은 선택 조건에 묶는다.
- **Alternatives considered:**
1. **Email verification 기본** — SMTP 운영이 가능하고 마찰을 낮출 때.
2. **Password re-auth 강제** — 기존 local secret 소유 증명이 필요할 때.
3. **조건부 step-up** — 위험도에 따라 email/password를 나눌 때.
4. **기존-account linking 미제공** — duplicate account 비용을 감수할 때.
- **Counterargument:** 이 문장이 이미 security-first profile을 전제한다고 해석할 수 있다. 그러나 선택 조건이 본문에 없고 default flow 설명처럼 쓰였다는 점은 남는다.
- **Verification command and observed output:**
```text
$ grep -nFo -- "또는 로그인 화면에서 Google로 처음 로그인했을 때 같은 email의 기존 계정이 있으면 'Confirm Link Existing Account' authenticator가 비밀번호 확인을 요구합니다." 'raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
34:또는 로그인 화면에서 Google로 처음 로그인했을 때 같은 email의 기존 계정이 있으면 'Confirm Link Existing Account' authenticator가 비밀번호 확인을 요구합니다.
$ grep -nFo -- 'SMTP 설정 realm 은 "Verify Existing Account By Email" 이 기본 `ALTERNATIVE` 이므로 admin 이 명시 DISABLE 해야 password 재인증 실행' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
151:SMTP 설정 realm 은 "Verify Existing Account By Email" 이 기본 `ALTERNATIVE` 이므로 admin 이 명시 DISABLE 해야 password 재인증 실행
```
## 5. `feature-keycloak-account-linking-sub-vs-email.md`
### L1-F05: active Sync Mode decision의 takeover 근거가 current DEM과 반대다
- **Source file:** `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md`
- **Source quote:**
> Sync Mode = IMPORT (first login만). Google 측 email 변경이 Keycloak으로 자동 전파되지 않음 → Scenario B 회피.
- **Counterpart/current quote:**
> **본 branch(takeover)와 직교** — Sync Mode 는 이미 링크된 계정의 attribute(email/name) 최신성(FORCE) vs 안정성(IMPORT) trade-off 이며 link key(sub)를 안 바꾸므로 takeover 무관.
- **Source lines:** `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:137`, `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:154`
- **Severity:** Medium
- **Classification:** `FACT — STALE_SUMMARY + RESTATED_FOREIGN_DECISION`.
- **Claim:** Decisions는 IMPORT가 Scenario B를 방어한다고 말하지만 current D5는 Sync Mode와 takeover가 직교하며 값 owner도 sibling이라고 정정한다.
- **Real-world assumptions:**
1. 운영자가 takeover 방어를 이유로 IMPORT를 선택한다.
- **Falsifies if:** IMPORT는 attribute ownership 정책으로 별도 채택된다.
- **Verify:** realm export와 ADR의 선택 근거를 확인한다.
- **Concrete failure mode:** 보안상 필요하지 않은 IMPORT가 고정돼 Google-authoritative profile freshness 요구를 막거나, FORCE를 선택하면 takeover가 생긴다는 잘못된 경보를 만든다.
- **Falsification condition:** IMPORT가 로컬 attribute 편집 보존이라는 별도 요구로 선택되고 active Decisions의 이유가 갱신되면 finding은 사라진다.
- **Required action:** `:137`을 historical decision으로 이동하고 active D5는 google-claim owner D3 pointer와 freshness/stability 조건만 남긴다.
- **Why this action:** federation key 보안과 attribute synchronization을 독립 축으로 유지한다.
- **Alternatives considered:**
1. **IMPORT** — 로컬 편집을 보존할 때.
2. **FORCE** — Google을 attribute authority로 둘 때.
3. **INHERIT/LEGACY** — realm default를 따르지만 upgrade drift를 감수한다.
4. **Owner-only delegation** — 이 노트는 takeover 영향만 “직교”로 기록한다.
- **Counterargument:** email 충돌의 운영 영향을 줄이기 위해 IMPORT가 여전히 유효할 수 있다. 다만 그 이유는 takeover 방지가 아니라 attribute stability다.
- **Verification command and observed output:**
```text
$ grep -nFo -- 'Sync Mode = IMPORT (first login만). Google 측 email 변경이 Keycloak으로 자동 전파되지 않음 → Scenario B 회피.' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
137:Sync Mode = IMPORT (first login만). Google 측 email 변경이 Keycloak으로 자동 전파되지 않음 → Scenario B 회피.
$ grep -nFo -- '**본 branch(takeover)와 직교** — Sync Mode 는 이미 링크된 계정의 attribute(email/name) 최신성(FORCE) vs 안정성(IMPORT) trade-off 이며 link key(sub)를 안 바꾸므로 takeover 무관.' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
154:**본 branch(takeover)와 직교** — Sync Mode 는 이미 링크된 계정의 attribute(email/name) 최신성(FORCE) vs 안정성(IMPORT) trade-off 이며 link key(sub)를 안 바꾸므로 takeover 무관.
```
### L1-F06: persistent federation key와 local-account collision locator가 `sub-only` 하나로 합쳐져 있다
- **Source file:** `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md`
- **Source quote:**
> federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned**
- **Counterpart/current quote:**
> OOTB 는 email/username 매칭 → sub-only 원하면 커스텀 authenticator (별도 branch)
- **Source lines:** `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:150`, `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:172`
- **Severity:** High
- **Classification:** `FACT — ambiguous semantics / incompatible scope assumption`; 실패 영향은 `INFERENCE`.
- **Claim:** 이미 연결된 federated identity의 안정 key와, 아직 연결되지 않은 local account 후보를 찾는 locator가 구분되지 않아 D1을 구현하려면 out-of-scope custom authenticator가 필요하다는 결론에 이른다.
- **Real-world assumptions:**
1. 기존 local account에는 아직 Google `sub` mapping이 없다.
- **Falsifies if:** 외부 directory에 local-user↔Google-sub 사전 매핑이 존재한다.
- **Verify:** first-login 전 `FEDERATED_IDENTITY`와 별도 mapping source를 확인한다.
2. OOTB flow가 email/username으로 후보를 찾고 별도 verification을 수행한다.
- **Falsifies if:** 배포판에 sub-based collision authenticator가 있다.
- **Verify:** owner flow와 authenticator source를 확인한다.
- **Concrete failure mode:** “sub-only collision detection”을 그대로 구현하면 기존 local account를 찾지 못해 duplicate user가 생기거나, custom authenticator가 제외돼 D1이 실행 불가능한 정책으로 남는다.
- **Falsification condition:** D1의 “link key”가 오직 persistent federated identity lookup만 의미하고 email candidate discovery+re-auth를 허용한다고 명시하면 모순은 용어 문제로 축소된다.
- **Required action:** 결정을 두 개로 분리한다: `(IdP alias, external sub)`는 persistent federation key, email/username은 existing-account 후보 locator일 수 있으나 link 승인 proof는 아니다.
- **Why this action:** stable identity와 initial discovery의 서로 다른 위협 모델을 분리한다.
- **Alternatives considered:**
1. **Email candidate + re-auth** — OOTB 활용, email은 proof가 아님.
2. **기존 계정 로그인 후 CIAL** — 안정적이나 사용자가 두 단계 수행한다.
3. **사전 directory mapping** — 기업 환경에 맞지만 동기화 운영이 필요하다.
4. **항상 신규 계정 생성** — 단순하지만 account fragmentation을 감수한다.
- **Counterargument:** “primary key”가 DB 저장 key만 뜻했다면 D1 자체는 맞다. 이 경우 custom authenticator 요구를 D1에서 분리하고 용어만 바로잡으면 된다.
- **Verification command and observed output:**
```text
$ grep -nFo -- 'federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned**' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
150:federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned**
$ grep -nFo -- 'OOTB 는 email/username 매칭 → sub-only 원하면 커스텀 authenticator (별도 branch)' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
172:OOTB 는 email/username 매칭 → sub-only 원하면 커스텀 authenticator (별도 branch)
```
## 6. `feature-keycloak-bff-vs-spa-direct.md`
### L1-F07: 요약 매트릭스가 OAuth 2.1의 조건부 BFF 권고를 일반 권고처럼 표시한다
- **Source file:** `raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md`
- **Source quote:**
> | OAuth 2.1 draft 권고 | (허용) | ✅ (browser-based app) |
- **Counterpart/current quote:**
> public client + PKCE 만이면 SPA Direct 도 표준 허용 — "browser-based app 전반 의무" 아님
- **Source lines:** `raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md:130`, `raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md:165`
- **Severity:** Medium
- **Classification:** `FACT — STALE_SUMMARY`.
- **Claim:** 상단 matrix는 browser-based app이면 BFF가 표준 우위인 것처럼 보이지만 D5는 client credentials 사용 시에만 해당한다고 제한한다.
- **Real-world assumptions:**
1. 독자가 matrix를 architecture choice의 요약 SSOT로 사용한다.
- **Falsifies if:** D5 조건을 반드시 함께 읽도록 표가 연결돼 있다.
- **Verify:** matrix만 떼어 ADR 입력으로 사용해도 조건이 보이는지 확인한다.
- **Concrete failure mode:** public SPA+PKCE가 요구에 맞는데도 “표준 권고”를 이유로 stateful BFF를 선택해 session store·CSRF 통제가 불필요하게 추가된다.
- **Falsification condition:** 조직 보안 정책이 표준과 별개로 BFF를 요구하면 선택은 유지할 수 있지만 표준 근거의 일반화는 여전히 수정 대상이다.
- **Required action:** matrix cell을 “client credentials가 필요한 browser app에서 BFF”로 좁히고 D5 pointer를 붙인다.
- **Why this action:** 표준 조건과 프로젝트 risk preference를 분리한다.
- **Alternatives considered:**
1. **Public SPA + PKCE** — stateless와 multi-client가 중요할 때.
2. **Confidential BFF** — browser token exposure를 줄이고 server session을 운영할 때.
3. **Edge ForwardAuth** — 여러 backend가 공통 ingress 인증을 쓸 때.
4. **Token-mediating backend** — session과 API fan-out을 절충할 때.
- **Counterargument:** 금융·의료 등 내부 정책이 BFF를 요구할 수 있다. 그 경우 근거는 내부 risk decision으로 적어야 한다.
- **Verification command and observed output:**
```text
$ grep -nFo -- '| OAuth 2.1 draft 권고 | (허용) | ✅ (browser-based app) |' 'raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md'
130:| OAuth 2.1 draft 권고 | (허용) | ✅ (browser-based app) |
$ grep -nFo -- 'public client + PKCE 만이면 SPA Direct 도 표준 허용 — "browser-based app 전반 의무" 아님' 'raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md'
165:public client + PKCE 만이면 SPA Direct 도 표준 허용 — "browser-based app 전반 의무" 아님
```
**Single-finding justification:** token holder, session state, Spring client type, mobile split, OAuth 2.1 조건의 5개 항목을 검토했다. lane 내부의 명시적 mismatch는 OAuth 2.1 matrix 요약 1건이다. P2A/token-storage/refresh/audience owner 내용은 unseen이라 cross-lane으로 분리했다.
## 7. `feature-keycloak-docker-compose-stack.md`
### L1-F08: bridge-network 기본값과 app의 `localhost` issuer URL은 함께 동작하지 않는다
- **Source file:** `raw/branch-notes/feature-keycloak-docker-compose-stack.md`
- **Source quote:**
> Spring Boot Resource Server, `issuer-uri=http://localhost:8080/realms/<realm>`
- **Counterpart/current quote:**
> **target env = 단일 EC2 Linux 를 기본 가정하므로 기본값은 `extra_hosts: [host.docker.internal:host-gateway]`** (backend 가 `host.docker.internal:8080` 로 JWKS/issuer 도달).
- **Source lines:** `raw/branch-notes/feature-keycloak-docker-compose-stack.md:138`, `raw/branch-notes/feature-keycloak-docker-compose-stack.md:207`
- **Severity:** High
- **Classification:** `FACT — CONTRADICTION`.
- **Claim:** app container는 `host.docker.internal`로 host-published Keycloak에 접근한다고 선택했지만 Resource Server URL은 app container 자신을 가리키는 `localhost`로 남아 있다.
- **Real-world assumptions:**
1. app과 Keycloak이 별도 bridge containers로 실행된다.
- **Falsifies if:** app이 host process이거나 `network_mode: host`를 쓴다.
- **Verify:** rendered Compose의 network mode를 확인한다.
2. Resource Server가 startup에 `issuer-uri` discovery를 수행한다.
- **Falsifies if:** reachable `jwk-set-uri`를 별도로 두고 issuer validation만 localhost로 한다.
- **Verify:** Spring security property와 startup log의 discovery URL을 확인한다.
- **Concrete failure mode:** app이 자기 container의 `localhost:8080`으로 discovery/JWKS를 호출해 connection refused로 기동 실패하거나 첫 JWT 검증에서 실패한다.
- **Falsification condition:** 실제 default가 `network_mode: host`이고 port-table 무효화를 수용한다면 localhost는 도달 가능하다.
- **Required action:** public issuer와 internal retrieval URL을 분리하거나, browser와 containers가 함께 resolve하는 hostname을 채택한다.
- **Why this action:** token `iss` 일치와 network reachability를 동시에 만족시킨다.
- **Alternatives considered:**
1. **Host network + localhost issuer** — Linux 한정, `ports:` 의미가 사라진다.
2. **Public `issuer-uri` + internal `jwk-set-uri`** — issuer는 유지하고 JWKS만 reachable URL로 가져온다.
3. **공통 DNS hostname** — browser/app/Keycloak이 같은 host를 resolve하도록 한다.
4. **app을 host에서 실행** — 학습은 단순하지만 4-service Compose 목표가 바뀐다.
- **Counterargument:** 이 노트는 iss mismatch 실험을 의도하므로 실패 구성이 교육용일 수 있다. 그러나 P3A “정상 기동 stack”과 “함정 시연” profile을 분리해야 한다.
- **Verification command and observed output:**
```text
$ grep -nFo -- 'Spring Boot Resource Server, `issuer-uri=http://localhost:8080/realms/<realm>`' 'raw/branch-notes/feature-keycloak-docker-compose-stack.md'
138:Spring Boot Resource Server, `issuer-uri=http://localhost:8080/realms/<realm>`
$ grep -nFo -- '**target env = 단일 EC2 Linux 를 기본 가정하므로 기본값은 `extra_hosts: [host.docker.internal:host-gateway]`** (backend 가 `host.docker.internal:8080` 로 JWKS/issuer 도달).' 'raw/branch-notes/feature-keycloak-docker-compose-stack.md'
207:**target env = 단일 EC2 Linux 를 기본 가정하므로 기본값은 `extra_hosts: [host.docker.internal:host-gateway]`** (backend 가 `host.docker.internal:8080` 로 JWKS/issuer 도달).
```
**Single-finding justification:** service topology, DB env, health port, realm import, secret mapping, issuer/network의 6개 항목을 검토했다. 다른 항목은 선택 조건·risk가 함께 적혀 있으며, 함께 선택된 default끼리 양립하지 않는 것은 issuer reachability 1건이다.
## 8. `feature-keycloak-edge-forwardauth-google-federation.md`
### L1-F09: parent D5와 child D1이 같은 `sub` mapping 결정을 각각 소유한다
- **Source file:** `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md`
- **Source quote:**
> 사용자 매핑 식별자 = Google `sub` (영구 식별자), email 은 primary key 금지
- **Ownership quote:**
> 이 브랜치의 D1~D5 가 이들의 설정 SSOT.
- **Counterpart quote:**
> federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned**
- **Source lines:** `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:195`, `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:253`, `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:150`
- **Severity:** High
- **Classification:** `FACT — DUAL_OWNERSHIP + RESTATED_FOREIGN_DECISION`.
- **Claim:** 동일 concern인 Google `sub` federation key를 parent D5와 child D1이 모두 owner로 선언한다.
- **Real-world assumptions:**
1. 두 D-row가 같은 persistent mapping key를 뜻한다.
- **Falsifies if:** parent는 pattern requirement, child는 별도 implementation mechanism을 소유한다고 concern을 분리한다.
- **Verify:** concern 이름과 change authority를 owner map에서 비교한다.
- **Concrete failure mode:** child가 candidate-discovery와 `sub` semantics를 정정해도 parent D5가 그대로 남아 참조자가 서로 다른 결론을 가져간다.
- **Falsification condition:** parent D5가 “requirement only”라는 별도 concern ID를 갖고 child D1만 concrete key policy owner임을 명시하면 dual ownership이 해소된다.
- **Required action:** 한 문서만 D-row owner로 남긴다. 권장 구조는 parent가 P1B topology만 소유하고 D5를 child D1 pointer+1줄 요약으로 바꾸는 것이다.
- **Why this action:** child의 위협 모델·evidence 변화가 한 owner에서 전파되게 한다.
- **Alternatives considered:**
1. **Child owner, parent pointer** — 현재 문서 분해와 가장 잘 맞는다.
2. **Parent owner, child 분석-only** — hub가 무거워지지만 owner는 하나다.
3. **Child를 parent로 병합** — drift는 줄지만 문서 깊이가 커진다.
4. **Requirement/mechanism concern 분리** — 두 owner를 유지하되 동일 결정이 아니도록 명명한다.
- **Counterargument:** parent D5가 requirement이고 child D1이 상세라고 볼 수 있다. 그러나 현재 두 문구는 같은 key와 email 금지를 결정하며 경계가 명시되지 않았다.
- **Verification command and observed output:**
```text
$ grep -nFo -- '사용자 매핑 식별자 = Google `sub` (영구 식별자), email 은 primary key 금지' 'raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md'
195:사용자 매핑 식별자 = Google `sub` (영구 식별자), email 은 primary key 금지
$ grep -nFo -- '이 브랜치의 D1~D5 가 이들의 설정 SSOT.' 'raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md'
253:이 브랜치의 D1~D5 가 이들의 설정 SSOT.
$ grep -nFo -- 'federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned**' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
150:federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned**
```
### L1-F10: P1B가 “P1A와 동일”이라면서 backend trust를 JWT signature로 바꾼다
- **Source file:** `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md`
- **Source quote:**
> | Backend의 token 검증 | Keycloak signature | 동일 (Google token은 backend까지 안 옴) |
- **Counterpart quote:**
> **백엔드는 JWT 검증을 하지 않고 헤더만 신뢰**
- **Source lines:** `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:145`, `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:112`
- **Severity:** High
- **Classification:** `FACT — CONTRADICTION`.
- **Claim:** P1A는 header-only trust를 정의하지만 P1B 비교표는 P1A와 P1B 모두 backend가 Keycloak signature를 검증한다고 요약한다.
- **Real-world assumptions:**
1. P1B가 P1A에 Google leg만 추가하는 변형이다.
- **Falsifies if:** P1B가 defense-in-depth JWT validation을 추가하는 별도 variant다.
- **Verify:** backend dependency/config diff를 확인한다.
2. backend가 한 trust contract를 따라 구현된다.
- **Falsifies if:** route별로 header-only와 bearer validation을 명시적으로 나눈다.
- **Verify:** ingress header/token forwarding과 SecurityFilterChain을 대조한다.
- **Concrete failure mode:** 구현팀 일부는 ingress header만 신뢰하고 다른 일부는 bearer token을 요구해, 같은 보호 route에서 401 또는 spoofing 방어 수준 차이가 발생한다.
- **Falsification condition:** P1B가 새 variant임을 명시하고 “P1A와 동일” 문구를 제거하면 contradiction이 아니다.
- **Required action:** P1 contract를 header-only, JWT-only, 또는 header+JWT 중 하나로 고정하고 P1B는 그 contract를 그대로 참조한다.
- **Why this action:** Google federation 추가와 backend trust 변경을 독립 decision으로 만든다.
- **Alternatives considered:**
1. **Header-only** — backend 코드가 적지만 ingress isolation이 load-bearing이다.
2. **JWT-only** — backend가 self-contained validation을 수행한다.
3. **Header + JWT AND 검증** — 방어가 늘지만 운영·코드 비용도 늘어난다.
4. **Route별 variant** — 유연하지만 routing contract가 필요하다.
- **Counterargument:** oauth2-proxy가 access token을 선택적으로 넘기므로 backend마다 다르게 쓸 수 있다. 그렇다면 “동일 패턴”이 아니라 두 variant를 명명해야 한다.
- **Verification command and observed output:**
```text
$ grep -nFo -- '| Backend의 token 검증 | Keycloak signature | 동일 (Google token은 backend까지 안 옴) |' 'raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md'
145:| Backend의 token 검증 | Keycloak signature | 동일 (Google token은 backend까지 안 옴) |
$ grep -nFo -- '**백엔드는 JWT 검증을 하지 않고 헤더만 신뢰**' 'raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md'
112:**백엔드는 JWT 검증을 하지 않고 헤더만 신뢰**
```
## 9. `feature-keycloak-edge-forwardauth-no-google.md`
### L1-F11: oauth2-proxy와 Traefik ForwardAuth를 대안으로 놓지만 구현안에서는 함께 사용한다
- **Source file:** `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md`
- **Source quote:**
> oauth2-proxy vs Traefik ForwardAuth plugin 선택 기준 정리.
- **Counterpart quote:**
> oauth2-proxy `/oauth2/auth` 로 위임, 2XX=allow + 원본 요청 진행
- **Source lines:** `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:167`, `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:227`
- **Severity:** High
- **Classification:** `FACT — CONTRADICTION / incompatible alternatives`.
- **Claim:** D1은 oauth2-proxy와 Traefik ForwardAuth를 교체 가능한 단일 축으로 취급하지만 Traefik middleware는 실제로 oauth2-proxy auth endpoint를 호출한다.
- **Real-world assumptions:**
1. “Traefik ForwardAuth”는 middleware 자체를 뜻하며 OIDC session provider가 아니다.
- **Falsifies if:** 문서가 별도 all-in-one `traefik-forward-auth` 제품을 뜻한다.
- **Verify:** image/name/config와 `forwardAuth.address` target을 확인한다.
- **Concrete failure mode:** “Traefik 선택”만으로 OIDC callback/session 기능이 생긴다고 보고 oauth2-proxy 같은 auth service를 배치하지 않아 로그인 흐름이 성립하지 않는다.
- **Falsification condition:** 정확한 auth provider 제품을 명명하고 Traefik middleware와의 역할을 분리하면 finding은 해소된다.
- **Required action:** 선택 축을 `(Ingress: nginx|Traefik)``(Auth service: oauth2-proxy|다른 OIDC agent)`의 2차원으로 다시 작성한다.
- **Why this action:** middleware와 identity-aware auth service의 책임을 분리한다.
- **Alternatives considered:**
1. **Nginx + oauth2-proxy**`auth_request` 방식.
2. **Traefik + oauth2-proxy**`forwardAuth.address` 방식.
3. **Traefik + 별도 ForwardAuth service** — 대체 provider를 명시한다.
4. **oauth2-proxy reverse-proxy mode** — 별도 ingress subrequest 없이 upstream을 직접 proxy한다.
- **Counterargument:** D1이 “Ingress-Nginx stack과 Traefik stack”을 줄여 쓴 표현일 수 있다. 그러나 현재 제목과 표는 component 대 component 비교로 읽힌다.
- **Verification command and observed output:**
```text
$ grep -nFo -- 'oauth2-proxy vs Traefik ForwardAuth plugin 선택 기준 정리.' 'raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md'
167:oauth2-proxy vs Traefik ForwardAuth plugin 선택 기준 정리.
$ grep -nFo -- 'oauth2-proxy `/oauth2/auth` 로 위임, 2XX=allow + 원본 요청 진행' 'raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md'
227:oauth2-proxy `/oauth2/auth` 로 위임, 2XX=allow + 원본 요청 진행
```
### L1-F12: “브라우저에 access token 없음”과 “access token을 cookie에 저장”이 같은 threat model에 섞여 있다
- **Source file:** `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md`
- **Source quote:**
> **토큰 클라이언트 노출 없음**: `access_token`을 브라우저에 두지 않음 → XSS로 토큰이 탈취될 surface 제거. BFF-like.
- **Counterpart quote:**
> **세션 쿠키 크기**: access_token까지 쿠키에 저장하면 nginx의 기본 4kb 헤더 한도를 넘어 분할 쿠키 처리 로직 필요.
- **Source lines:** `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:120`, `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:127`
- **Severity:** Medium
- **Classification:** `FACT — CONTRADICTION / incompatible token-storage assumptions`.
- **Claim:** server-side session과 encrypted cookie session을 구분하지 않아 browser token possession과 JS raw-token readability가 같은 의미로 쓰인다.
- **Real-world assumptions:**
1. oauth2-proxy가 cookie-backed session에 token material을 넣을 수 있다.
- **Falsifies if:** chosen config가 Redis/server-side store만 사용하고 cookie에는 opaque ID만 둔다.
- **Verify:** oauth2-proxy session-store type과 cookie payload 크기를 확인한다.
- **Concrete failure mode:** 보안 설계가 server-side BFF 수준의 token custody를 가정하지만 실제로는 browser-held encrypted cookie가 bearer-equivalent session이 되어 replay·size·rotation 위험이 남는다.
- **Falsification condition:** cookie가 opaque session identifier뿐이고 token은 server store에만 있으면 `:120`의 보관 경계가 성립한다.
- **Required action:** session mode를 결정하고 “raw token JS 접근”, “encrypted cookie 보유”, “server-side token 보관”을 별도 행으로 기술한다.
- **Why this action:** XSS exfiltration, cookie replay, header-size 문제를 각각 맞는 통제로 연결한다.
- **Alternatives considered:**
1. **Redis/server-side session** — token custody가 명확하지만 state store가 필요하다.
2. **Opaque cookie ID + server memory** — 단일 인스턴스 학습에 맞지만 scale-out이 약하다.
3. **Encrypted cookie에 token 포함** — 운영은 단순하지만 size/replay 경계를 문서화해야 한다.
4. **Access token upstream 전달** — backend JWT validation contract가 추가된다.
- **Counterargument:** httpOnly encrypted cookie는 JS가 raw access token을 읽지 못하므로 “직접 노출 없음”이라는 보안 이점은 남는다. 다만 “브라우저에 두지 않음”이라는 물리적 보관 문구는 좁혀야 한다.
- **Verification command and observed output:**
```text
$ grep -nFo -- '**토큰 클라이언트 노출 없음**: `access_token`을 브라우저에 두지 않음 → XSS로 토큰이 탈취될 surface 제거. BFF-like.' 'raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md'
120:**토큰 클라이언트 노출 없음**: `access_token`을 브라우저에 두지 않음 → XSS로 토큰이 탈취될 surface 제거. BFF-like.
$ grep -nFo -- '**세션 쿠키 크기**: access_token까지 쿠키에 저장하면 nginx의 기본 4kb 헤더 한도를 넘어 분할 쿠키 처리 로직 필요.' 'raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md'
127:**세션 쿠키 크기**: access_token까지 쿠키에 저장하면 nginx의 기본 4kb 헤더 한도를 넘어 분할 쿠키 처리 로직 필요.
```
## 10. `feature-keycloak-federation-spa-zero-change.md`
### L1-F13: zero-change는 hosted-login 경로에만 성립하지만 제목·목표는 P2B 전체처럼 읽힌다
- **Source file:** `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md`
- **Source quote:**
> **P2A 코드 그대로 두고 Keycloak에 Google IdP만 추가**해서 SPA가 변경 없이 Google 로그인 가능함을 실증한다.
- **Counterpart quote:**
> SPA 가 link 를 트리거하는 공식 메커니즘 = Client Initiated Account Linking (서명된 redirect URL fabrication)
- **Scope quote:**
> Account Linking 정책 — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]
- **Source lines:** `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:31`, `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:105`, `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:53`
- **Severity:** Medium
- **Classification:** `FACT — ambiguous scope / incompatible defaults`.
- **Claim:** hosted Keycloak login button만 쓰면 login entry는 zero-change지만, in-SPA account linking을 포함하면 URL fabrication·hashing·상태 표시 코드가 필요하다.
- **Real-world assumptions:**
1. 상위 P2B 독자가 “Google federation 기능 전체”에 zero-change를 일반화한다.
- **Falsifies if:** 상위 문서도 hosted-login-only로 범위를 명시한다.
- **Verify:** P2B parent의 In/Out scope와 TODO를 확인한다.
- **Concrete failure mode:** account-link button/status 요구가 있는데도 SPA change가 없다고 계획해 CIAL, CSRF state, linked-account read path가 누락된다.
- **Falsification condition:** 제품 요구가 Keycloak-hosted login page에서 Google을 선택하는 것뿐이고 account UX를 제공하지 않으면 zero-change는 성립한다.
- **Required action:** 제목·목표를 “Google IdP sign-in entry zero-change”로 좁히고 account-link 기능별 change matrix를 추가한다.
- **Why this action:** brokering의 login-path 불변식과 선택적 account UX 비용을 함께 보존한다.
- **Alternatives considered:**
1. **Hosted login only** — SPA diff 0.
2. **`idpHint` 버튼** — 작은 SPA 변경으로 Google을 직접 선택한다.
3. **Account Console 위임** — SPA account-link 코드는 줄지만 별도 UX로 이동한다.
4. **In-SPA linking/status** — CIAL과 read API/backend 구현이 필요하다.
- **Counterargument:** 이 branch는 `:53`에서 Account Linking을 명시적으로 제외한다. finding은 branch 내부 구현이 아니라 제목·목표가 상위 P2B 범위로 일반화될 위험을 지적한다.
- **Verification command and observed output:**
```text
$ grep -nFo -- '**P2A 코드 그대로 두고 Keycloak에 Google IdP만 추가**해서 SPA가 변경 없이 Google 로그인 가능함을 실증한다.' 'raw/branch-notes/feature-keycloak-federation-spa-zero-change.md'
31:**P2A 코드 그대로 두고 Keycloak에 Google IdP만 추가**해서 SPA가 변경 없이 Google 로그인 가능함을 실증한다.
$ grep -nFo -- 'SPA 가 link 를 트리거하는 공식 메커니즘 = Client Initiated Account Linking (서명된 redirect URL fabrication)' 'raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
105:SPA 가 link 를 트리거하는 공식 메커니즘 = Client Initiated Account Linking (서명된 redirect URL fabrication)
$ grep -nFo -- 'Account Linking 정책 — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]' 'raw/branch-notes/feature-keycloak-federation-spa-zero-change.md'
53:Account Linking 정책 — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]
```
### L1-F14: zero-change token baseline에서 `aud` owner와 기대값이 모호하다
- **Source file:** `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md`
- **Source quote:**
> `aud=<spa-client-id>` 확인
- **Counterpart quote:**
> `aud`=backend/SPA client, `azp`=SPA client → **구조 동일**
- **Source lines:** `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:78`, `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:131`
- **Severity:** High
- **Classification:** `FACT — ambiguous default`; P2A audience owner가 unseen이므로 `NEEDS_CROSS_LANE_AUDIT`.
- **Claim:** 동일 token expectation에서 `aud`가 SPA client로 고정되기도 하고 backend 또는 SPA client로 열려 있기도 하다.
- **Real-world assumptions:**
1. SPA와 Resource API가 별도 clients이고 backend가 audience를 강제한다.
- **Falsifies if:** 하나의 client가 SPA와 API audience를 겸하도록 의도됐다.
- **Verify:** realm export의 client/audience mapper와 backend validator를 확인한다.
- **Concrete failure mode:** token `aud`가 SPA client뿐인데 backend가 API audience를 기대하면 401이 발생한다. 반대로 backend가 SPA audience를 수용하면 cross-client token reuse 경계가 약해질 수 있다.
- **Falsification condition:** P2A owner가 `aud=<spa-client-id>`를 명시적으로 선택했고 backend validator도 같은 값을 SSOT로 참조한다면 모호성은 해소된다.
- **Required action:** P2A audience owner D-row를 직접 참조하고 expected claims를 `iss`, `aud`, `azp` 각각 정확한 client ID로 고정한다.
- **Why this action:** zero-change 검증은 기준선이 한 값으로 정의돼야 의미가 있다.
- **Alternatives considered:**
1. **`aud=api-client`, `azp=spa-client`** — SPA/API 분리가 선명하다.
2. **`aud=spa-client`** — 단일-client 학습에는 단순하지만 backend trust를 명시해야 한다.
3. **복수 audience** — 여러 API에 유연하지만 token scope가 넓어진다.
4. **BFF audience** — BFF가 token holder인 별도 pattern이다.
- **Counterargument:** `backend/SPA client`가 환경별 placeholder를 뜻할 수 있다. 그렇다면 zero-change test가 실제 어떤 값을 assert할지 선택 조건을 추가해야 한다.
- **Verification command and observed output:**
```text
$ grep -nFo -- '`aud=<spa-client-id>` 확인' 'raw/branch-notes/feature-keycloak-federation-spa-zero-change.md'
78:`aud=<spa-client-id>` 확인
$ grep -nFo -- '`aud`=backend/SPA client, `azp`=SPA client → **구조 동일**' 'raw/branch-notes/feature-keycloak-federation-spa-zero-change.md'
131:`aud`=backend/SPA client, `azp`=SPA client → **구조 동일**
```
## Grep verification
- Findings: 14
- Verbatim quotes: 30
- 실행한 quote-specific `grep -nFo`: 30
- 비어 있지 않은 관측 결과: 30
- Line corrections: 0
- Dropped quotes/findings: 0
- 모든 finding의 source/counterpart quote가 위 명령 결과와 일치했다.
## Claim Traceability Check
- Claim ID 연결 검사: 10개 branch-note의 DEM 구조를 읽었다. 같은 lane에 counterpart가 있는 decision edge는 의미 대조했으며, raw claim source body와 unseen owner D-row는 고정 slice 밖이므로 `BROKEN_CLAIM_REFERENCE` 여부를 판정하지 않았다.
- Decision Evidence Map: 10개 검토. lane-local semantic drift는 L1-F01~L1-F14에 반영했다.
- UNSUPPORTED_DECISION: 표기 자체만으로 inconsistency로 올리지 않았다. categorical summary/current DEM/owner summary가 어긋난 경우만 finding으로 다뤘다.
- **Repository changes:** 없음. 읽기 전용.
## Cross-Lane Candidates
| Citing file:line | Referenced owner/decision | Status | One-line reason |
|---|---|---|---|
| `raw/branch-notes/feature-authentication-authorization-contract.md:124,219` | `feature-security-operational-baseline``JwtToAuthenticatedUserConverter`, AUTHZ code SSOT | NEEDS_CROSS_LANE_AUDIT | `ROLE_*` authority와 raw role seam의 실제 owner vocabulary 확인 필요. |
| `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:181` | `feature-keycloak-first-broker-login-flow` D1/D2 | NEEDS_CROSS_LANE_AUDIT | auto-link disable과 Email/Re-auth execution profile의 authoritative D-row 확인 필요. |
| `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:183` | `feature-keycloak-idp-mappers-claim-to-role` D2 | NEEDS_CROSS_LANE_AUDIT | `email_verified`가 custom gate인지 Trust Email 설정인지 owner 판정 필요. |
| `raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:184` | `feature-keycloak-google-claim-attribute-mapping` D1/D4 | NEEDS_CROSS_LANE_AUDIT | `sub`/email claim mapping과 collision input의 owner summary 확인 필요. |
| `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:211` | `feature-keycloak-first-broker-login-flow` D1/D2/D4 | NEEDS_CROSS_LANE_AUDIT | password/email verification 및 `email_verified=false` 방어 flow가 unseen owner에 위임됨. |
| `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:212` | `feature-keycloak-google-claim-attribute-mapping` D3/D4 | NEEDS_CROSS_LANE_AUDIT | Sync Mode와 email Attribute Importer의 current owner 결정을 확인해야 함. |
| `raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md:226-229` | P2A parent D1, token-storage D1/D5, refresh D1/D2, audience D1/D4 | NEEDS_CROSS_LANE_AUDIT | SPA Direct 기준선 네 축이 본 lane 밖 owner에 의존함. |
| `raw/branch-notes/feature-keycloak-docker-compose-stack.md:236,240` | `feature-keycloak-single-ec2-no-google` D3; `feature-keycloak-iss-claim-hostname-mismatch` | NEEDS_CROSS_LANE_AUDIT | public issuer/internal reachability contract의 authoritative network profile 확인 필요. |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:252` | root `feature-keycloak-patterns` F3/F4 | NEEDS_CROSS_LANE_AUDIT | shared realm/client와 secret ownership이 P1B Google client 설계 전제임. |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:253` | idp-client, first-broker, claim-mapping child owners | NEEDS_CROSS_LANE_AUDIT | parent D1-D5와 child owner 경계를 전체 hierarchy에서 확인해야 함. |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:248` | `feature-keycloak-header-spoofing-defense` D1/D3/D4 | NEEDS_CROSS_LANE_AUDIT | header-only trust의 load-bearing network isolation 결정이 unseen owner에 있음. |
| `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:249-250` | oauth2-proxy flow D2; nginx auth-request D2/D3/D5 | NEEDS_CROSS_LANE_AUDIT | P1A hub가 세부 config를 재진술했는지 owner D-row와 의미 대조 필요. |
| `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:145` | `feature-keycloak-internal-spa-direct-no-google` D3 | NEEDS_CROSS_LANE_AUDIT | `iss`/signature/expiry/audience baseline과 정확한 `aud` 값을 확인해야 함. |
| `raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:146-149` | P2B parent, first-broker, claim-mapping, three-leg-trust owners | NEEDS_CROSS_LANE_AUDIT | zero-change 전제가 현재 delegated owners의 D-row와 맞는지 확인 필요. |
```wiki-stats
agent: wiki-research-lane
found: 10
processed: 10
dropped: 0
```
@@ -0,0 +1,477 @@
## Lane Inventory
| Path | Status | Evidence Lines | Extracted Facts |
| --- | --- | --- | --- |
| `raw/branch-notes/feature-keycloak-first-broker-login-flow.md` | READ_FULL | 1261 | D1/D2 Confirm Link 소유증명, D4 silent auto-link 차단, hard-reject SPI 별도 트랙 |
| `raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md` | READ_FULL | 1238 | attribute-mapping owner, D3 IdP default IMPORT, D5 role mapping 위임 |
| `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md` | READ_FULL | 1261 | exact redirect URI, basic-scope 예외, JavaScript origins 비움 |
| `raw/branch-notes/feature-keycloak-header-spoofing-defense.md` | READ_FULL | 1255 | K8s NetworkPolicy·EC2 SG·shared secret·trusted proxy 방어 경계 |
| `raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md` | READ_FULL | 1284 | TLS 종단 대안과 명시적 HSTS 구성 |
| `raw/branch-notes/feature-keycloak-idp-brokering-google-client.md` | READ_FULL | 1251 | Google IdP client, `trustEmail=false`, callback·origin 위임 |
| `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md` | READ_FULL | 1148 | role/RBAC owner, D1 FORCE, D2 email verification 강제 주장 |
| `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md` | READ_FULL | 1512 | P2B composition hub, D8 zero-change brokering, D10 SMTP 사실, foreign-decision 상세 재진술 |
| `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md` | READ_FULL | 1416 | P2A SPA Direct 정의, D2 refresh-token 저장, D4 brokering 비교 |
| `raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch.md` | READ_FULL | 1287 | issuer/JWKS 분리 대안, 미승인 F, 외부 owner 충돌 명시 |
## File 1 — `feature-keycloak-first-broker-login-flow.md`
### L2-F01: Confirm Link 보장의 버전 전제가 owner에 반영되지 않음
- **Severity:** High
- **Failure modes:** `STALE_SUMMARY`, `UNSUPPORTED_DECISION`
- **Source files:** first-broker-login-flow, idp-brokering-google-client, internal-spa-direct-google-federation
- **Original goal:**
- 인용:
```text
core 차단(D1+D2)은 `trustEmail`·`email_verified` 검증과 무관하게 성립
```
- 위치: `raw/branch-notes/feature-keycloak-first-broker-login-flow.md:135`
- **Current state:**
- 인용:
```text
이는 `trustEmail=false`+email 인증 상태에서도 Confirm Link 흐름이 우회 가능했던 실제(패치완료) 취약점
```
- 위치: `raw/branch-notes/feature-keycloak-idp-brokering-google-client.md:214`
- 인용:
```text
`CVE-2026-9087` 은 **raw 에 아카이브된 출처가 없다**
```
- 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:338`
- **Claim:** 문서 상태에 관한 FACT는 세 노트가 서로 다른 보장 강도를 제공한다는 것이다. 실제 취약점·영향 버전은 아직 `INFERENCE/UNVERIFIED`다.
- **Assumptions:** F의 취약점 보고가 정확하고 배포 버전이 영향 범위일 때만 owner의 무조건부 보장이 실패한다.
- **Concrete failure:** 구현자가 line 135만 소비하면 패치 상태를 확인하지 않고 Confirm Link를 충분한 차단으로 취급할 수 있다.
- **Falsification:** 공식 Keycloak advisory가 해당 흐름에 영향이 없다고 밝히거나, 배포 버전이 영향 범위 밖임을 증명하면 보안 충돌은 사라진다.
- **Required action / synthesis recommendation:** 공식 advisory를 raw로 보존한 뒤 owner D4에 버전 조건을 추가한다. 그 전에는 F의 “실제 취약점” 표현을 `needs-confirmation`으로 낮추고, owner에는 미검증 version gate만 둔다.
- **Why:** 보안 owner의 보장은 소비자가 한 줄만 읽어도 유효 조건을 알 수 있어야 한다.
- **Alternatives:** ① 패치 버전 allowlist 게이트, ② 취약 버전 배포 차단, ③ 별도 custom SPI 방어, ④ Google brokering 비활성화.
- **Counterargument:** 현재 지원 버전만 사용하면 Confirm Link 보장은 유효할 수 있다. 그렇더라도 owner 문서에 적용 버전 조건을 적으면 이 반론과 충돌하지 않는다.
- **Verification:**
```bash
grep -nFo -- 'core 차단(D1+D2)은 `trustEmail`·`email_verified` 검증과 무관하게 성립' raw/branch-notes/feature-keycloak-first-broker-login-flow.md
# Observed: 135:core 차단(D1+D2)은 `trustEmail`·`email_verified` 검증과 무관하게 성립
grep -nFo -- '이는 `trustEmail=false`+email 인증 상태에서도 Confirm Link 흐름이 우회 가능했던 실제(패치완료) 취약점' raw/branch-notes/feature-keycloak-idp-brokering-google-client.md
# Observed: 214:이는 `trustEmail=false`+email 인증 상태에서도 Confirm Link 흐름이 우회 가능했던 실제(패치완료) 취약점
grep -nFo -- '`CVE-2026-9087` 은 **raw 에 아카이브된 출처가 없다**' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md
# Observed: 338:`CVE-2026-9087` 은 **raw 에 아카이브된 출처가 없다**
```
### L2-F02: `email_verified=false` 정책이 silent-link 차단과 hard-reject 사이에서 갈림
- **Severity:** High
- **Failure modes:** `CONTRADICTION`, `RESTATED_FOREIGN_DECISION`
- **Source files:** first-broker-login-flow, idp-mappers-claim-to-role, internal-spa-direct-google-federation
- **Original goal:**
- 인용:
```text
`email_verified=false` 계정의 **silent auto-link 차단**
```
- 위치: `raw/branch-notes/feature-keycloak-first-broker-login-flow.md:135`
- 인용:
```text
`email_verified=false` 를 flow 진입 즉시 *hard-reject* 하려면 커스텀 SPI authenticator 필요 → 별도 트랙
```
- 위치: `raw/branch-notes/feature-keycloak-first-broker-login-flow.md:135`
- **Current state:**
- 인용:
```text
`email_verified=true` 강제 (Attribute Importer + First Broker Login Flow custom step)
```
- 위치: `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md:120`
- 인용:
```text
Google `email_verified=false` 계정 | 링크/생성 거부
```
- 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:387`
- **Claim:** owner D4는 소유증명 없는 auto-link만 차단한다. G와 H는 별도 SPI가 구현된 것처럼 전체 링크·생성을 거부한다고 요약한다.
- **Assumptions:** slice 안에는 hard-reject custom authenticator의 별도 owner나 구현 증거가 없다.
- **Concrete failure:** `email_verified=false` 사용자가 실제로는 Confirm Link 또는 신규 계정 흐름에 진입하는데, 테스트가 즉시 거부를 기대해 보안 정책과 구현이 어긋난다.
- **Falsification:** 별도 owner가 custom authenticator 구현과 E2E 거부 결과를 제시하면 G/H의 정책이 성립한다.
- **Required action / synthesis recommendation:** G/H를 owner D4의 “silent-link prevention only” 포인터로 축소한다. hard-reject가 필요하면 SPI branch를 단일 owner로 만들고 그 결정이 확정된 뒤 세 문서를 갱신한다.
- **Why:** 계정 연결 방어와 모든 계정 생성 거부는 서로 다른 정책·구현 계약이다.
- **Alternatives:** ① Confirm Link만 유지, ② custom SPI로 즉시 거부, ③ 통제된 enterprise IdP만 허용, ④ Google federation 자체를 사용하지 않음.
- **Counterargument:** Google이 일반적으로 verified email을 제공하므로 차이가 드물 수 있다. 발생 빈도와 정책 보장은 별개이며, 문서는 미검증 claim의 실제 동작을 정의해야 한다.
- **Verification:**
```bash
grep -nFo -- '`email_verified=false` 계정의 **silent auto-link 차단**' raw/branch-notes/feature-keycloak-first-broker-login-flow.md
# Observed: 135:`email_verified=false` 계정의 **silent auto-link 차단**
grep -nFo -- '`email_verified=false` 를 flow 진입 즉시 *hard-reject* 하려면 커스텀 SPI authenticator 필요 → 별도 트랙' raw/branch-notes/feature-keycloak-first-broker-login-flow.md
# Observed: 135:`email_verified=false` 를 flow 진입 즉시 *hard-reject* 하려면 커스텀 SPI authenticator 필요 → 별도 트랙
grep -nFo -- '`email_verified=true` 강제 (Attribute Importer + First Broker Login Flow custom step)' raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md
# Observed: 120:`email_verified=true` 강제 (Attribute Importer + First Broker Login Flow custom step)
grep -nFo -- 'Google `email_verified=false` 계정 | 링크/생성 거부' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md
# Observed: 387:Google `email_verified=false` 계정 | 링크/생성 거부
```
## File 2 — `feature-keycloak-google-claim-attribute-mapping.md`
### L2-F03: IMPORT owner와 FORCE 소비자의 적용 범위가 불명확함
- **Severity:** Medium
- **Failure modes:** `AMBIGUOUS_OWNERSHIP`, 조건부 `CONTRADICTION`
- **Source files:** google-claim-attribute-mapping, idp-mappers-claim-to-role
- **Original goal:**
- 인용:
```text
그쪽이 attribute-mapping **owner**
```
- 위치: `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md:19`
- **Current state:**
- 인용:
```text
| D1 | Sync Mode = `FORCE`
```
- 위치: `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md:119`
- 인용:
```text
| D3 | Sync Mode = IMPORT (first login 시점 매핑만)
```
- 위치: `raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md:130`
- 인용:
```text
IdP-level `Sync Mode` = 모든 mapper default
```
- 위치: `raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md:130`
- **Claim:** B가 IdP-level default를 IMPORT로 소유하지만, G의 D1은 FORCE가 global인지 role mapper override인지 밝히지 않는다.
- **Assumptions:** G D1이 IdP-level 설정을 가리키면 두 값은 공존할 수 없다. mapper-level override이고 Keycloak이 해당 override를 지원한다면 공존 가능하다.
- **Concrete failure:** 운영자가 동일 Admin UI 필드를 두 지침대로 번갈아 설정하거나, 모든 attribute를 로그인마다 덮어써 사용자 편집값을 잃을 수 있다.
- **Falsification:** G D1을 특정 role mapper의 override로 명시하고 공식 근거·Admin UI 실측으로 override 우선순위를 확인하면 충돌이 해소된다.
- **Required action / synthesis recommendation:** B D3를 IdP default owner로 유지하고, G D1은 role mapper 전용 override로 증명·범위화하거나 제거한다.
- **Why:** fresh role과 보존해야 할 profile attribute는 서로 다른 갱신 정책을 가질 수 있으나 설정 계층을 분명히 해야 한다.
- **Alternatives:** ① global IMPORT + role mapper FORCE, ② 전부 IMPORT, ③ 전부 FORCE, ④ role 계산을 별도 authorization 계층으로 이동.
- **Counterargument:** role freshness에는 FORCE가 합리적이다. 이 finding은 FORCE 자체가 아니라 어느 설정 계층에 적용하는지 불명확한 점을 지적한다.
- **Verification:**
```bash
grep -nFo -- '그쪽이 attribute-mapping **owner**' raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md
# Observed: 19:그쪽이 attribute-mapping **owner**
grep -nFo -- '| D1 | Sync Mode = `FORCE`' raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md
# Observed: 119:| D1 | Sync Mode = `FORCE`
grep -nFo -- '| D3 | Sync Mode = IMPORT (first login 시점 매핑만)' raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md
# Observed: 130:| D3 | Sync Mode = IMPORT (first login 시점 매핑만)
grep -nFo -- 'IdP-level `Sync Mode` = 모든 mapper default' raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md
# Observed: 130:| D3 | Sync Mode = IMPORT (first login 시점 매핑만)
# Observed: 173:IdP-level `Sync Mode` = 모든 mapper default
```
## File 3 — `feature-keycloak-google-redirect-uri-policy.md`
### L2-F04: basic-scope 예외를 옛 test-user 한도 설명이 덮음
- **Severity:** Low
- **Failure mode:** `STALE_SUMMARY`
- **Source file:** google-redirect-uri-policy
- **Original goal:**
- 인용:
```text
이 basic identity scope 조합은 test-user allowlist 등록·100명 상한·7일 만료·unverified 경고가 모두 면제됨
```
- 위치: `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md:143`
- **Current state:**
- 인용:
```text
학습용은 `openid email profile`만 사용 → unverified로 100명 test users까지 동작
```
- 위치: `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md:92`
- **Claim:** D5의 정정 이후에도 TODO가 이전 한도 모델을 유지한다.
- **Assumptions:** 요청 scope가 정말 `openid email profile`에 한정된다.
- **Concrete failure:** 불필요한 test-user 등록·인원 관리 절차를 구현 계획에 넣고, 서로 다른 Google quota를 혼동한다.
- **Falsification:** sensitive/restricted scope가 실제로 추가되면 TODO의 한도 설명이 별도 시나리오에서 다시 필요하다.
- **Required action / synthesis recommendation:** line 92를 D5 포인터로 바꾸거나 삭제하고, sensitive-scope 시나리오는 별도 조건문으로 분리한다.
- **Why:** 한 파일 안의 TODO가 authoritative D-row보다 오래된 운영 지침을 제공한다.
- **Alternatives:** ① TODO 삭제 후 D5 포인터, ② 최신 예외 설명으로 갱신, ③ sensitive-scope 실험용 별도 TODO로 범위화.
- **Counterargument:** Testing 상태의 다른 quota를 기억시키려는 문장일 수 있다. 그렇다면 basic identity scope와 다른 quota임을 명시해야 한다.
- **Verification:**
```bash
grep -nFo -- '이 basic identity scope 조합은 test-user allowlist 등록·100명 상한·7일 만료·unverified 경고가 모두 면제됨' raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md
# Observed: 143:이 basic identity scope 조합은 test-user allowlist 등록·100명 상한·7일 만료·unverified 경고가 모두 면제됨
grep -nFo -- '학습용은 `openid email profile`만 사용 → unverified로 100명 test users까지 동작' raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md
# Observed: 92:학습용은 `openid email profile`만 사용 → unverified로 100명 test users까지 동작
```
### L2-F05: JavaScript origins가 owner와 consumer에서 다름
- **Severity:** Medium
- **Failure modes:** `STALE_SUMMARY`, `RESTATED_FOREIGN_DECISION`
- **Source files:** google-redirect-uri-policy, idp-brokering-google-client
- **Original goal:**
- 인용:
```text
| Authorized JavaScript origins | (비움) |
```
- 위치: `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md:167`
- **Current state:**
- 인용:
```text
Authorized JavaScript origins: `https://<kc-host>`
```
- 위치: `raw/branch-notes/feature-keycloak-idp-brokering-google-client.md:81`
- **Claim:** redirect-policy owner는 server-side flow 때문에 origins를 비우지만, Google-client TODO는 외부 Keycloak host를 등록하라고 한다.
- **Assumptions:** Google OAuth 교환은 Keycloak 서버가 수행하며 브라우저 Google SDK를 직접 사용하지 않는다.
- **Concrete failure:** 불필요한 browser origin을 허용하고, 동일 client를 재현하는 사람마다 다른 Console 설정을 만든다.
- **Falsification:** Google Console 실측에서 해당 client type이 origin을 요구하거나 실제 browser-side Google SDK 흐름이 추가되면 host 등록이 필요하다.
- **Required action / synthesis recommendation:** F의 origin 값을 삭제하고 C D6 포인터로 교체한다. 브라우저 흐름이 생기면 별도 client와 owner를 둔다.
- **Why:** callback URI와 JavaScript origin은 다른 정책 필드이며, consumer가 owner 값을 재정의하면 drift가 생긴다.
- **Alternatives:** ① 비워 둠, ② browser flow 도입 시 최소 origin만 등록, ③ browser용 Google client 분리.
- **Counterargument:** host 하나를 미리 등록해도 당장 실패하지 않을 수 있다. 그러나 사용하지 않는 권한 표면과 재현 차이는 남는다.
- **Verification:**
```bash
grep -nFo -- '| Authorized JavaScript origins | (비움) |' raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md
# Observed: 167:| Authorized JavaScript origins | (비움) |
grep -nFo -- 'Authorized JavaScript origins: `https://<kc-host>`' raw/branch-notes/feature-keycloak-idp-brokering-google-client.md
# Observed: 81:Authorized JavaScript origins: `https://<kc-host>`
```
## File 4 — `feature-keycloak-header-spoofing-defense.md`
**0 independent findings.** D3(K8s)과 D4(EC2)는 환경별 대안이며 동시에 적용해야 하는 상충 결정이 아니다. D5는 두 환경의 보조 계층, D6는 Keycloak proxy 신뢰 경계로 분리돼 있다. 다만 부모·proxy owner가 slice 밖이므로 lines 202206은 `NEEDS_CROSS_LANE_AUDIT`로 이관한다.
## File 5 — `feature-keycloak-https-termination-caddy-nginx.md`
### L2-F06: Caddy HSTS 기본값 설명이 authoritative 구성과 상충함
- **Severity:** Medium
- **Failure mode:** `STALE_SUMMARY`
- **Source file:** https-termination-caddy-nginx
- **Original goal:**
- 인용:
```text
**명시 필요** (default 로 단정 금지)
```
- 위치: `raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md:173`
- **Current state:**
- 인용:
```text
Caddy는 디폴트로 HTTPS redirect + HSTS
```
- 위치: `raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md:43`
- **Claim:** 구현 표는 HSTS를 명시하라고 정정했지만 초기 설계 답변은 여전히 자동 적용이라고 말한다.
- **Assumptions:** 별도 global Caddy 설정이나 upstream edge가 HSTS를 주입하지 않는다.
- **Concrete failure:** line 43을 따라 구성하면 HTTPS redirect는 동작해도 HSTS 헤더가 빠질 수 있다.
- **Falsification:** 대상 Caddy 버전의 공식 문서나 응답 헤더 실측이 자동 HSTS를 입증하면 line 43이 맞다.
- **Required action / synthesis recommendation:** line 43을 “automatic HTTPS redirect, HSTS explicit”으로 정정하고 D2/D5에만 값을 소유시킨다.
- **Why:** TLS 자동화와 HSTS 정책은 서로 다른 기능이다.
- **Alternatives:** ① Caddy `header` 명시, ② nginx에서 명시, ③ Cloudflare edge에서 단일 owner로 주입, ④ ephemeral dev에서는 의도적으로 생략하고 범위를 표시.
- **Counterargument:** 학습 환경에서는 HSTS가 필요하지 않을 수 있다. 그렇다면 dev-only 생략 결정으로 적어야지 default 기능으로 설명하면 안 된다.
- **Verification:**
```bash
grep -nFo -- 'Caddy는 디폴트로 HTTPS redirect + HSTS' raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md
# Observed: 43:Caddy는 디폴트로 HTTPS redirect + HSTS
grep -nFo -- '**명시 필요** (default 로 단정 금지)' raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md
# Observed: 173:**명시 필요** (default 로 단정 금지)
```
## File 6 — `feature-keycloak-idp-brokering-google-client.md`
**0 additional independent findings.** 이 파일은 L2-F01의 미아카이브 CVE 주장과 L2-F05의 stale JavaScript-origin TODO에 직접 참여한다. 그 외 D6의 `trustEmail=false` 값 선택과 default 미확정 구분은 내부적으로 일관된다. 부모가 정하는 `<kc-host>`는 slice 밖이므로 line 192를 교차 lane 후보로 남긴다.
## File 7 — `feature-keycloak-idp-mappers-claim-to-role.md`
**0 additional independent findings.** 이 파일의 실제 상충은 L2-F02의 hard-reject 주장과 L2-F03의 FORCE 적용 범위로 모두 수거했다. role/RBAC만 소유한다는 line 19와 attribute-level D1/D2가 함께 남아 있는 것이 두 finding의 공통 원인이다.
## File 8 — `feature-keycloak-internal-spa-direct-google-federation.md`
### L2-F07: Reference-Only를 선언하면서 foreign decision 상세를 의도적으로 유지함
- **Severity:** Medium
- **Failure mode:** `RESTATED_FOREIGN_DECISION`
- **Source file:** internal-spa-direct-google-federation
- **Original goal:**
- 인용:
```text
본 노트는 `rules/consistency-contract.md` 의 **Reference-Only** 규약에 따라 포인터 + 1줄 요약으로만 인용한다
```
- 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:22`
- **Current state:**
- 인용:
```text
**유지 + 위임 고지** — 학습용 흐름 이해에 필요한 개요이므로 삭제하지 않고
```
- 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:443`
- **Claim:** 위임 고지는 Reference-Only의 “포인터 + 1줄” 제한을 대체하지 않는다. 파일 스스로 detailed restatement를 식별하면서 유지하기로 했다.
- **Assumptions:** 현재 consistency-contract의 Single-Owner/Reference-Only 규칙이 그대로 적용된다.
- **Concrete failure:** attribute mapping 또는 trust-chain owner가 바뀔 때 H의 상세 표가 같은 변경을 수동으로 따라가지 못해 stale copy가 된다.
- **Falsification:** 규칙이 synchronized derived view를 허용하도록 바뀌고, owner 변경 때 자동 검증되는 생성물이 되면 상세 유지가 가능하다.
- **Required action / synthesis recommendation:** branch-note에서는 owner 포인터와 한 줄 요약만 남긴다. 학습용 상세 설명은 canonical concept나 owner에서 생성되는 view로 이동한다.
- **Why:** “owner가 우선”이라는 주석은 독자가 stale 상세를 실행하는 것을 막지 못한다.
- **Alternatives:** ① 한 줄 pointer-only, ② canonical explainer로 이동, ③ owner 기반 자동 생성 view, ④ H가 새 owner가 되고 기존 owner를 pointer로 전환.
- **Counterargument:** composition hub에는 end-to-end 이해를 위한 상세가 필요하다. 그 요구는 canonical explainer나 생성 view로 충족할 수 있다.
- **Verification:**
```bash
grep -nFo -- '본 노트는 `rules/consistency-contract.md` 의 **Reference-Only** 규약에 따라 포인터 + 1줄 요약으로만 인용한다' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md
# Observed: 22:본 노트는 `rules/consistency-contract.md` 의 **Reference-Only** 규약에 따라 포인터 + 1줄 요약으로만 인용한다
grep -nFo -- '**유지 + 위임 고지** — 학습용 흐름 이해에 필요한 개요이므로 삭제하지 않고' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md
# Observed: 443:**유지 + 위임 고지** — 학습용 흐름 이해에 필요한 개요이므로 삭제하지 않고
```
### L2-F08: brokering zero-change payload에 두 owner가 존재함
- **Severity:** Medium
- **Failure mode:** `DUAL_OWNERSHIP`
- **Source files:** internal-spa-direct-google-federation, internal-spa-direct-no-google
- **Original goal:**
- 인용:
```text
P2B 의 federation 은 **Keycloak 레이어에서만** broker
```
- 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:324`
- **Current state:**
- 인용:
```text
brokering 은 SPA/Backend **코드 0줄**로 얹히는 cross-cutting
```
- 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md:282`
- 인용:
```text
**이중 주장 (`/sync` 수거 대상)**
```
- 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:402`
- **Claim:** H D8과 I D4가 동일한 zero-change 결론을 각각 결정 payload로 보유하며 H도 이를 이중 주장으로 인정한다.
- **Assumptions:** “Keycloak-only broker”와 “SPA/backend code zero”가 같은 구조적 보장의 두 표현이다.
- **Concrete failure:** `kc_idp_hint`, 로그인 버튼 노출 기본값, SPA 변경 조건이 진화할 때 두 D-row가 다른 경계를 가질 수 있다.
- **Falsification:** I D4를 범위 선택과 H D8 pointer만 남기도록 축소하면 소유권이 분리된다.
- **Required action / synthesis recommendation:** H D8을 composition owner로 지정하고 I D4를 포인터로 전환한다. 별도 `federation-spa-zero-change` owner를 택하려면 먼저 그 unseen branch와 교차 감사를 수행한다.
- **Why:** 아키텍처 불변성은 여러 구현 결정의 전제가 되므로 한 곳에서 조건과 예외를 소유해야 한다.
- **Alternatives:** ① H D8 owner, ② I D4 owner, ③ zero-change 전용 child owner, ④ project hub의 고정 결정으로 승격.
- **Counterargument:** baseline과 extension 양쪽에 같은 설명이 있어야 읽기 쉽다. pointer와 한 줄 요약이면 가독성을 유지하면서 owner를 하나로 둘 수 있다.
- **Verification:**
```bash
grep -nFo -- 'P2B 의 federation 은 **Keycloak 레이어에서만** broker' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md
# Observed: 324:P2B 의 federation 은 **Keycloak 레이어에서만** broker
grep -nFo -- 'brokering 은 SPA/Backend **코드 0줄**로 얹히는 cross-cutting' raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md
# Observed: 282:brokering 은 SPA/Backend **코드 0줄**로 얹히는 cross-cutting
grep -nFo -- '**이중 주장 (`/sync` 수거 대상)**' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md
# Observed: 402:**이중 주장 (`/sync` 수거 대상)**
```
L2-F01과 L2-F02도 이 파일의 version gate 및 `email_verified` edge table에 직접 적용된다. D10의 SMTP 소유권은 실제 docker-compose owner가 slice 밖이라 finding으로 확정하지 않고 교차 lane 후보로 남겼다.
## File 9 — `feature-keycloak-internal-spa-direct-no-google.md`
### L2-F09: SPA Direct 정의 안에 BFF refresh-cookie 선택을 넣음
- **Severity:** High
- **Failure modes:** `INCOMPATIBLE_ASSUMPTIONS`, `RESTATED_FOREIGN_DECISION`
- **Source file:** internal-spa-direct-no-google
- **Original goal:**
- 인용:
```text
**Edge proxy 없음.**
```
- 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md:17`
- **Current state:**
- 인용:
```text
**secure httpOnly cookie** (BFF 변형)
```
- 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md:188`
- 인용:
```text
| D2 | refresh token 저장 위치 = secure httpOnly cookie 권장
```
- 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md:280`
- **Claim:** `INFERENCE` — HttpOnly refresh cookie를 발급·회전·소비하려면 token-mediating server가 필요하다. 정의된 backend는 Resource Server일 뿐이고 edge도 없으므로 D2 권장은 현재 패턴 안에서 실행할 주체가 없다.
- **Assumptions:** 브라우저 SPA는 HttpOnly cookie 값을 읽을 수 없고, Keycloak이 이 프로젝트 origin용 refresh cookie를 대신 관리하도록 설계되지 않았다.
- **Concrete failure:** 구현자가 Resource Server에 token exchange·cookie issuance·CSRF 방어를 뒤늦게 추가하면서 P2A를 사실상 BFF/AP3로 바꾸지만 문서·threat model은 SPA Direct로 남는다.
- **Falsification:** 별도 mediator 없이 Keycloak/browser adapter가 usable HttpOnly refresh cookie를 제공한다는 공식 근거와 E2E 결과가 있거나, 이미 mediator가 아키텍처에 포함돼 있음을 증명하면 성립하지 않는다.
- **Required action / synthesis recommendation:** P2A 기본은 memory-only로 두거나, HttpOnly 선택 시 명시적으로 token-mediating/BFF 패턴으로 전환하고 CSRF·rotation owner를 함께 연결한다. unseen token-storage owner도 교차 감사해야 한다.
- **Why:** storage 선택이 토큰 보유 주체와 인증 아키텍처를 바꾸고 있다.
- **Alternatives:** ① memory-only, ② Web Worker 격리, ③ 최소 token-mediator 추가, ④ full BFF, ⑤ browser storage 사용 후 XSS 위험 수용.
- **Counterargument:** reload 후 세션 유지에는 HttpOnly cookie UX가 낫다. 그 장점은 mediator 도입 비용과 CSRF 계약을 명시적으로 수용할 때만 비교할 수 있다.
- **Verification:**
```bash
grep -nFo -- '**Edge proxy 없음.**' raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md
# Observed: 17:**Edge proxy 없음.**
grep -nFo -- '**secure httpOnly cookie** (BFF 변형)' raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md
# Observed: 188:**secure httpOnly cookie** (BFF 변형)
grep -nFo -- '| D2 | refresh token 저장 위치 = secure httpOnly cookie 권장' raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md
# Observed: 280:| D2 | refresh token 저장 위치 = secure httpOnly cookie 권장
```
L2-F08의 zero-change 이중 소유도 이 파일 D4에 적용된다.
## File 10 — `feature-keycloak-iss-claim-hostname-mismatch.md`
**0 slice-internal findings.** D6의 `jwk-set-uri` 대안은 부모와 audience-validator owner가 승인하기 전까지 권고안으로만 유지되고, 현재 기본을 C로 명시해 파일 내부에서 두 구성을 동시에 채택하지 않는다. `KC-HOST-C6`는 미아카이브 claim 후보라고 lines 142·230·247에 명시돼 Supporting Claim으로 위장하지 않는다. 외부 owner 충돌은 lines 204209의 교차 lane 후보로 분리한다.
## Synthesis
이 slice에는 High 3건, Medium 5건, Low 1건의 정합 gap이 있다. 가장 직접적인 보안 오해는 `email_verified=false`를 silent-link 차단과 hard-reject로 혼용한 L2-F02다. L2-F01의 버전 위험은 파급은 크지만 공식 advisory가 raw에 없어 실재·영향 범위를 아직 FACT로 취급할 수 없다. IMPORT/FORCE, JavaScript origins, HSTS는 owner 값보다 오래된 소비자·초기 설명이 남은 drift다. P2B는 Reference-Only를 선언하면서 상세 foreign decisions를 유지하고, zero-change 결론도 P2A와 공동 소유한다. P2A의 HttpOnly-cookie 권장은 SPA Direct와 BFF의 경계를 흐리므로 구현 전 owner 결정이 필요하다. Header-spoofing과 issuer-mismatch 파일은 slice 내부에서는 대안 조건과 미승인 상태를 구분하고 있다.
## Inferences
1. CVE 보고가 정확할 때만 owner의 무조건부 Confirm Link 보장이 영향 버전에서 실패한다 — Based on: `feature-keycloak-idp-brokering-google-client.md:214`, `feature-keycloak-internal-spa-direct-google-federation.md:338`.
2. G D1이 mapper-level override라면 IMPORT/FORCE는 공존할 수 있다 — Based on: `feature-keycloak-google-claim-attribute-mapping.md:130`, `feature-keycloak-idp-mappers-claim-to-role.md:119`.
3. HttpOnly refresh cookie에는 token-mediating server가 필요하다 — Based on: `feature-keycloak-internal-spa-direct-no-google.md:17,188,280`. 실제 adapter 동작으로 반증 가능하다.
## Out of Scope
- Slice 밖 parent/project/owner branch의 본문 의미 감사
- Keycloak·Google Console·Caddy 실제 구동
- CVE 공식 advisory 외부 fetch 및 raw 보존
- branch-note 수정 또는 canonical 문서 생성
## Coverage Gaps
- Files marked NOT_READ or BLOCKED: none
- Whole-corpus conclusions: INCOMPLETE — 아래 unseen owner/decision을 교차 lane에서 확인해야 한다.
## Grep Verification
각 finding 안에 25개 quote pattern의 절대경로 명령과 observed output을 기록했다. 대표 확인:
```bash
grep -nFo -- 'core 차단(D1+D2)은 `trustEmail`·`email_verified` 검증과 무관하게 성립' raw/branch-notes/feature-keycloak-first-broker-login-flow.md
# Observed: 135:core 차단(D1+D2)은 `trustEmail`·`email_verified` 검증과 무관하게 성립
grep -nFo -- '| D2 | refresh token 저장 위치 = secure httpOnly cookie 권장' raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md
# Observed: 280:| D2 | refresh token 저장 위치 = secure httpOnly cookie 권장
```
- 검증한 verbatim quote 총 개수 V: 25
- 일치 P: 25
- 라인 정정 C: 0
- 폐기 D: 0
## Claim Traceability Check
- Claim ID 연결 검사: slice에서 추출한 unique Claim ID 153개 중 152개가 branch/project note 밖의 raw source에서 lexical resolve. `KC-HOST-C6` 1개는 iss-claim-hostname-mismatch:142·230·247이 명시한 미아카이브 후보이며 Supporting Claim으로 사용되지 않아 `BROKEN_CLAIM_REFERENCE`는 none found.
- Decision Evidence Map: 10/10 파일에서 1개씩, 총 10개 검토. 이번 lane은 raw Claim ID 존재성까지 확인했으며 각 raw claim의 의미 재감사는 범위 밖이다.
- UNSUPPORTED_DECISION: 정합 finding과 직접 연결된 3개 축 — CVE version gate(H:338), JavaScript origins 비움(C:73·167), P2A brokering scope/zero-change(I:282). 그 밖의 project-scope·implementation caveat는 파일이 명시적으로 라벨링해 상충 finding으로 승격하지 않았다.
## Cross-Lane Audit Candidates
| File:line | Referenced owner / decision | Reason |
| --- | --- | --- |
| `feature-keycloak-google-redirect-uri-policy.md:202-205` | single-EC2-google-federation D4/D6; public-domain-tunneling D1/D2 | callback host/path와 tunnel 선택 owner가 unseen |
| `feature-keycloak-header-spoofing-defense.md:202-206` | edge-forwardauth-no-google D2/D4; nginx-auth-request; reverse-proxy-headers | 부모 enforcement 위임과 신규 `X-Internal-Auth-Token` 계약 확인 필요 |
| `feature-keycloak-https-termination-caddy-nginx.md:232-233` | reverse-proxy-headers D1; public-domain-tunneling | TLS 종단 위치와 forwarded-header mode 결합 검증 필요 |
| `feature-keycloak-idp-brokering-google-client.md:189,192` | account-linking-sub-vs-email; edge-forwardauth-google-federation | `trustEmail` 소비 의미와 `<kc-host>` owner 확인 필요 |
| `feature-keycloak-internal-spa-direct-google-federation.md:403` | 미지정 CSRF/`state` owner | 파일이 project-wide owner 부재를 주장하므로 다른 lane 확인 필요 |
| `feature-keycloak-internal-spa-direct-google-federation.md:404` | docker-compose-stack / realm configuration | H D10의 SMTP deployment-fact 소유권이 실제 stack owner와 겹치는지 확인 필요 |
| `feature-keycloak-internal-spa-direct-google-federation.md:407-408` | account-linking-sub-vs-email D1; three-leg-trust-chain D1/D2/D5 | L2-F07의 foreign-detail summary가 실제 owner와 같은지 확인 필요 |
| `feature-keycloak-internal-spa-direct-no-google.md:365-368` | audience-validator; token-storage; refresh-rotation; PKCE-flow | Hubs의 foreign D-row 요약과 L2-F09 token-storage owner 확인 필요 |
| `feature-keycloak-iss-claim-hostname-mismatch.md:204-209` | single-EC2-no-google D3; audience-validator D6; docker-compose D3/D5 | 파일이 이미 owner conflict·trigger erosion·3자 재진술을 명시함 |
## Stats
```wiki-stats
agent: wiki-research-lane
found: 10
processed: 10
dropped: 0
```
@@ -0,0 +1,386 @@
# Lane 3 — Keycloak Branch-Note Consistency Audit
## Lane Inventory
| Path | Status | Evidence Lines | Extracted Facts |
| --- | --- | --- | --- |
| `raw/branch-notes/feature-keycloak-nginx-auth-request-integration.md` | READ_FULL | 1306 | `auth_request` body handling, browser/API failure routing, upstream ownership edges |
| `raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md` | READ_FULL | 1275 | bearer-token terminology, nginx decision references, discovery startup dependency |
| `raw/branch-notes/feature-keycloak-patterns.md` | READ_FULL | 1205 | retired numbered naming versus retained D2 |
| `raw/branch-notes/feature-keycloak-pkce-flow-stages.md` | READ_FULL | 1167 | PKCE S256 ownership and Admin Console terminology |
| `raw/branch-notes/feature-keycloak-public-domain-tunneling.md` | READ_FULL | 1220 | quick versus named Cloudflare Tunnel domains and tunnel-choice ownership |
| `raw/branch-notes/feature-keycloak-realm-client-export.md` | READ_FULL | 1238 | import wiring ownership, credential export assertions, PKCE terminology |
| `raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md` | READ_FULL | 1254 | rotation demonstration, Max Reuse semantics, back-channel logout scope |
| `raw/branch-notes/feature-keycloak-refresh-token-rotation.md` | READ_FULL | 1284 | rotation contract, family invalidation, receiving endpoint delegation |
| `raw/branch-notes/feature-keycloak-reverse-proxy-headers.md` | READ_FULL | 1297 | Caddy path behavior, Keycloak proxy variables, delegated security decisions |
| `raw/branch-notes/feature-keycloak-single-ec2-google-federation.md` | READ_FULL | 1370 | parent integration decisions and delegated tunnel/proxy details |
## 1. `feature-keycloak-nginx-auth-request-integration.md`
### L3-F01: `auth_request` body behavior contradicts its configuration table
- **Severity / class:** High — `CONTRADICTION`
- **Original goal / current state:** line 101 says the body is not automatically copied; line 163 says nginx defaults to forwarding it.
- **Claim:** Both statements cannot govern the same configuration.
- **Assumption:** `proxy_pass_request_body off` can be omitted in the rendered config. This is falsified if every applicable location explicitly contains it.
- **Failure / falsification:** Request bodies reach the auth upstream, causing unwanted disclosure, buffering, or size failures. Falsify with `nginx -T` and upstream request capture.
- **Action / why:** Rewrite line 101 to say body forwarding must be disabled explicitly, and make the table canonical.
- **Alternatives:** (1) explicit `off`, (2) intentional bounded forwarding with documented limits, (3) a separate body-less auth endpoint.
- **Counterargument:** oauth2-proxy may ignore the body. That does not establish that nginx did not transmit it.
```text
$ sed -n '101p;163p' 'raw/branch-notes/feature-keycloak-nginx-auth-request-integration.md'
- nginx `auth_request`는 subrequest이므로 **POST body가 자동 복제되지 않는다**. `/oauth2/auth`는 HEAD-like check이므로 body 불필요 → `proxy_pass_request_body off` 권장.
| `proxy_pass_request_body` | `off` | `O2PN-C8`, `NGAR-C8`. **생략하면 default `on`(`NGXPM-C1`) 이라 body 가 전달됨** |
```
### L3-F02: unconditional sign-in TODO is stale after browser/API response split
- **Severity / class:** Medium — `STALE_SUMMARY`
- **Original goal / current state:** line 89 gives one 302 sign-in task; lines 193194 later divide browser 302 from API 401.
- **Claim:** The implementation checklist can be read as applying the redirect to every protected location.
- **Assumption:** Implementers use the TODO before the later D9 table. Falsified if generated config and tests always preserve the location split.
- **Failure / falsification:** API clients receive HTML redirects, parsing failures, or loops. Verify both browser and `/api/` requests.
- **Action / why:** Restrict the TODO to browser locations and add an explicit API 401 task.
- **Alternatives:** (1) location split, (2) 401 everywhere with frontend login handling, (3) redirect allowlist, (4) content-negotiated behavior.
- **Counterargument:** The TODO may describe only creation of the named location. Its application scope nevertheless remains unstated there.
```text
$ sed -n '89p;193p;194p' 'raw/branch-notes/feature-keycloak-nginx-auth-request-integration.md'
- [ ] named location `@oauth2_signin` 작성: `return 302 https://$host/oauth2/start?rd=$scheme://$host$request_uri;` — 등급: `planned`
| browser-facing (`location /`) | `auth_request /oauth2/auth;` | `error_page 401 = @oauth2_signin;` → 302 | D2, `O2PN-C5` |
| API/machine (`location /api/`) | `auth_request /oauth2/auth;` | `error_page 401 =401;` → **plain 401 pass-through** | D9, `O2PN-C9` |
```
## 2. `feature-keycloak-oauth2-proxy-oidc-flow.md`
### L3-F03: locally validated JWT behavior remains labeled “token introspection”
- **Severity / class:** Low — `STALE_SUMMARY`, `NAMING_DRIFT`
- **Original goal / current state:** the TODO calls the flags an introspection mode, while its own audit says no introspection endpoint call is documented.
- **Claim:** The term implies a network protocol behavior not supported by the notes evidence.
- **Assumption:** Readers use the label to infer latency, revocation, and firewall dependencies. Falsified by an observed outbound introspection request.
- **Failure / falsification:** Architecture is designed around an endpoint call that may not happen. Verify debug logs and outbound traffic.
- **Action / why:** Use neutral “bearer-token mode” or the verified local-validation/bypass behavior.
- **Alternatives:** (1) local JWT validation, (2) bearer-token bypass, (3) separately implement RFC 7662 introspection, (4) retain a neutral label pending measurement.
- **Counterargument:** “Introspection” might be informal internal vocabulary, but it conflicts with the protocols established meaning.
```text
$ sed -n '85p;101p' 'raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md'
- [ ] token introspection 모드 (`--skip-jwt-bearer-tokens`, `--extra-jwt-issuers`)의 의미 정리 — 등급: `planned`
- **2026-07-17** (drift): 본 노트가 쓰는 **"token introspection 모드" 명칭이 부정확할 가능성**을 확인 — 공식 문서는 introspection endpoint 호출을 서술하지 않음. 사용자 작성 본문이라 자동 수정하지 않고 §Audit & Findings `NAMING_DRIFT` 로 남김. rename 여부는 사용자 판단.
```
### L3-F04: three decision references are not machine-resolvable
- **Severity / class:** Low — `BARE_DECISION_REF`, ambiguous ownership
- **Original goal / current state:** all three passages intend to identify an owner decision but use bare slugs or quoted prose instead of the repositorys decision-reference syntax.
- **Claim:** Impact analysis cannot reliably bind these references to their owners.
- **Assumption:** Deterministic consistency tooling is part of change propagation. Falsified only if the tool and contract are changed to recognize these forms.
- **Failure / falsification:** Owner changes do not surface in consumer impact reports. Re-run the consistency checker after repair.
- **Action / why:** Replace each with `[[raw/branch-notes/<slug>]] D<n>`.
- **Alternatives:** N/A — reference syntax is fixed by `rules/consistency-contract.md`.
- **Counterargument:** A person can understand the references, but that does not make them machine-traceable.
```text
$ sed -n '214p;228p' 'raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md'
| `SESSION_STORAGE_4KB_ABSENT` | session storage 공식 페이지에 `4k`/`4096`/`split` 문자열이 **0건** — 4kb cookie split 함정의 근거는 이 페이지가 아니라 **nginx 통합 페이지**([[raw/official-docs/oauth2-proxy-nginx-integration-official]], "Nginx normally only copies the first `Set-Cookie` header ... if your cookies are larger than 4kb, you will need to extract additional cookies manually")에 있다. | `raw/official-docs/oauth2-proxy-session-storage-official.md` §Usage Boundaries | D5a 의 Open Risk 에 반영 완료. 4kb 대응의 owner 는 형제 branch `feature-keycloak-nginx-auth-request-integration` `D5`. |
| **인가 거부 시 실제 응답 코드/본문** (401 vs 403 vs 로그인 루프) | D4 Open Risk 가 자인 — `O2PK-C3` 은 인가 실패 코드의 의미를 명시 안 하고, `O2PBEH-C2`~`C4` 는 authentication 단계 전용. 형제 branch `feature-keycloak-nginx-auth-request-integration` `D2` 의 `error_page` 분기가 이 값에 의존 | 허용 role 이 **없는** 사용자로 로그인 후 보호 경로 요청 → 응답 코드/본문 확인. `/oauth2/auth` subrequest 응답도 함께 확인(202/401 만 반환하는지 — `O2PE-C5` 와 대조) | `needs-confirmation` |
```
```text
$ sed -n '182p' 'raw/branch-notes/feature-keycloak-realm-client-export.md'
- [[raw/branch-notes/feature-keycloak-single-ec2-no-google]] `#D3` — `KC_HOSTNAME=localhost` + issuer-uri + network 계약 owner. 이 hostname 계약이 D2 의 redirect URI 값(`http://localhost/*`, `http://127.0.0.1/*`)의 전제. parent `#D3` 이 hostname/포트를 바꾸면 본 realm-export.json 의 redirect URI(D2) 를 동기화해야 함. (sibling `feature-keycloak-docker-compose-stack` 도 동일 의존을 "parent D3" 로 인용). 아울러 본 branch 의 client scope(realm 1개 + `spa-client` public + PKCE S256) 는 parent `#D5` 의 실 구현.
```
## 3. `feature-keycloak-patterns.md`
### L3-F05: retired numbered naming remains an active D2 decision
- **Severity / class:** Medium — `CONTRADICTION`, `STALE_SUMMARY`
- **Original goal / current state:** line 60 retires numbered naming; line 134 still presents it as D2.
- **Claim:** A prohibited convention remains encoded as an active-looking decision.
- **Assumption:** D-rows denote current decisions. Falsified if D2 is structurally marked historical and excluded from owner resolution.
- **Failure / falsification:** New branches or consumers adopt the retired numbered hierarchy. Search inbound D2 references.
- **Action / why:** Move D2 to a retired-decision section or apply an explicit retired status.
- **Alternatives:** N/A — numbered branch hierarchy is prohibited by repository policy.
- **Counterargument:** The warning at line 60 is visible, but downstream D-reference semantics remain ambiguous.
- **Single-finding justification:** Other content is legacy-to-AP mapping; no second lane-local decision conflict was found.
```text
$ sed -n '60p;134p' 'raw/branch-notes/feature-keycloak-patterns.md'
> **⚠️ 갱신 (2026-07-14)**: 아래 6패턴 인덱스는 **Phase 0 legacy(배치×federation 축)**. 현 실행계획 SSOT 는 [[raw/project-notes/keycloak-patterns-overview]] 의 **§Branch 분해 / 실행계획(R4)** — 인증 아키텍처 4패턴(AP1~AP4) + 19 Tier-2. 신규 작업은 hub 분해표를 따르며, 아래 슬러그는 hub §2.3 매핑대로 AP 로 re-map 대상. D2(`-{N}-{M}` numbered 명명)는 CLAUDE.md §11 위반으로 폐기(각 sub-sub 는 이미 content-descriptive 슬러그라 실제 영향은 프레이밍뿐).
- **D2** 2026-05-25: sub-sub-branch 명명은 `-{N}-{M}` dash-숫자 형식. M은 학습/구현 단계 순서.
```
## 4. `feature-keycloak-pkce-flow-stages.md`
### L3-F06: D1 and D5 both own S256 enforcement
- **Severity / class:** Medium — `DUAL_OWNERSHIP`
- **Original goal / current state:** D1 and D5 both encode the S256 formula and Keycloak enforcement, with different evidence freshness.
- **Claim:** Two decisions own the same mutable contract.
- **Assumption:** Downstream notes treat a D-number as owner identity. Falsified if D5 is structurally non-normative.
- **Failure / falsification:** UI-label or rejection-semantics updates reach only one row. Compare inbound D1/D5 references.
- **Action / why:** Keep D1 canonical and turn D5 into a reference-only TODO mapping.
- **Alternatives:** (1) D1 owner/D5 pointer, (2) D5 owner/D1 scope summary, (3) merge rows, (4) split formula from vendor enforcement.
- **Counterargument:** D5 may exist only to map TODO step 2, but its current table form still looks normative.
```text
$ sed -n '118p;122p' 'raw/branch-notes/feature-keycloak-pkce-flow-stages.md'
| D1 | PKCE method = `S256` 만 정리 대상 (`plain` 은 비교용 1줄). Keycloak client 설정에서 PKCE method 옵션(정식 UI 라벨 "PKCE method")을 S256 으로 지정 | `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C3` (S256 공식: `BASE64URL-ENCODE(SHA256(ASCII(verifier)))`), `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C1` (OAuth 2.1: "Clients MUST use code_challenge and code_verifier ..."), `raw/official-docs/keycloak-client-pkce-method-enforcement-official.md#KC-PKCE-C1` (Admin UI 옵션 정식 명칭·위치), `#KC-PKCE-C3` (S256 선택 시 서술) | `official-standard + official-standard + official-vendor-doc` | OA21-C1 은 PKCE 사용 자체를 MUST 로 강제하지만 "S256 강제 / plain 금지" 라는 정확한 문장은 OA21-C1 인용에 포함 안 됨 — §7.5.1 예외 조건 확인 필요. 단, RFC 7636 + OAuth 2.1 종합 권고로 보면 정당. **2026-07-17 업데이트**: `KC-PKCE-C1` 이 UI 라벨 오류를 정정("Proof Key for Code Exchange Code Challenge Method" 가 아니라 "PKCE method", Capability Config 섹션)했으나, `KC-PKCE-C3` 은 "Keycloak applies... S256" 이라고만 서술 — **S256 설정 시 `code_challenge_method=plain` 요청을 실제로 거부(reject)한다는 명시적 문장은 여전히 없음**. 아래 Claims To Verify 의 "plain 메서드 요청을 거부" 항목은 `needs-confirmation` 유지 |
| D5 (TODO 표 step 2) | S256 공식 `code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))` + Keycloak client 에 S256 강제 | `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C3` (S256 공식), `raw/official-docs/keycloak-securing-apps-overview-official.md#KC-SECAPP-C1` (Keycloak 이 OIDC/OAuth 2.0 stack 을 secure 가능) | `official-standard` + `official-vendor-doc` (Keycloak overview) | Keycloak client 의 "Proof Key for Code Exchange Code Challenge Method = S256" 정확한 admin UI 위치는 KC-SECAPP-C1 인용 범위 밖 — Keycloak Securing Applications 의 client adapter / settings 섹션 별도 인용 필요 |
```
### L3-F07: deprecated PKCE Admin label survives in two TODOs
- **Severity / class:** Low — `STALE_SUMMARY`
- **Original goal / current state:** two implementation tasks use the old label; the audit identifies `PKCE method` as the actual label.
- **Claim:** The correction has not reached the operational checklist.
- **Assumption:** The pinned Keycloak version uses the audited label. Falsified by the target Admin Console displaying the older text.
- **Failure / falsification:** Implementers cannot find or select the intended field. Inspect the target-version UI.
- **Action / why:** Update both TODOs to `PKCE method = S256`; preserve the old label only as version history.
- **Alternatives:** N/A — use the label observed in the pinned target version.
- **Counterargument:** Labels can vary by Keycloak version or locale; record that variant explicitly if confirmed.
```text
$ sed -n '73p' 'raw/branch-notes/feature-keycloak-pkce-flow-stages.md'
- Keycloak client 설정: `Proof Key for Code Exchange Code Challenge Method = S256` 강제
```
```text
$ sed -n '81p;202p' 'raw/branch-notes/feature-keycloak-realm-client-export.md'
- [ ] Client Advanced 탭: `Proof Key for Code Exchange Code Challenge Method = S256` — 등급: `planned`
- **`NAMING_DRIFT`** (Advisory, user prose 보존) — §목표·§TODO 의 라벨 "Proof Key for Code Exchange Code Challenge Method" 는 `KC-PKCE-C1` 기준 부정확. 실제 Admin Console 옵션명은 **"PKCE method"** (Capability Config 섹션 하위). 구현 시 이 라벨로 찾을 것. 사용자 작성 prose 는 verbatim 보존, 정정은 §구현 가이드 1 에 반영.
```
## 5. `feature-keycloak-public-domain-tunneling.md`
### L3-F08: `trycloudflare.com` is described as both permanent and random
- **Severity / class:** High — `CONTRADICTION`, `STALE_SUMMARY`
- **Original goal / current state:** line 69 offers a permanent `<sub>.trycloudflare.com`; line 77 says quick-tunnel URLs are random, while the parent repeats the static-looking form.
- **Claim:** Quick-tunnel hostnames and named-tunnel managed DNS are conflated.
- **Assumption:** Google requires a stable registered redirect and the described quick-tunnel behavior applies. Falsified by an account providing a persistent named `trycloudflare.com` hostname.
- **Failure / falsification:** Restarting the tunnel invalidates the Google redirect URI. Create/restart the tunnel and compare hostnames.
- **Action / why:** Distinguish random quick tunnels from named tunnels backed by managed DNS; synchronize the parent summary.
- **Alternatives:** (1) random URL plus console updates, (2) named tunnel plus managed domain, (3) ngrok reserved domain, (4) another stable public endpoint.
- **Counterargument:** Product capabilities may have changed; verify against the exact account and client version.
```text
$ sed -n '69p;77p' 'raw/branch-notes/feature-keycloak-public-domain-tunneling.md'
- [ ] **Cloudflare Tunnel 동작 확인** — `cloudflared tunnel create <name>` + `cloudflared tunnel route dns <name> <subdomain>` → 영구 도메인 (`<sub>.trycloudflare.com` 무료 또는 자체 도메인 CNAME) — 등급: `planned`
- Cloudflare Tunnel의 `trycloudflare.com` quick tunnel은 무료지만 URL이 random (ngrok와 유사). 정적 도메인 원하면 Cloudflare account + 자체 도메인 (Cloudflare DNS로 위임) + named tunnel 필요.
```
```text
$ sed -n '356p' 'raw/branch-notes/feature-keycloak-single-ec2-google-federation.md'
- **비교 핵심**: P3A 대비 **추가 운영 비용은 "public 접근 가능한 callback URL"** 1가지. Google OAuth client console에 등록할 redirect_uri는 **HTTPS 필수 + localhost 제외** (Google 명시 — `http://localhost` 만 예외, IP 주소 X). 학습 환경에서는 **ngrok** (`https://<random>.ngrok-free.app` 임시 URL) 또는 **Cloudflare Tunnel** (`<name>.trycloudflare.com` 또는 자체 도메인) 사용. ngrok은 무료 plan에서 URL이 매 세션 변경 → Google client console 매번 업데이트 필요 → Cloudflare Tunnel이 학습 단계에 더 적합 (정적 도메인). **Keycloak `KC_PROXY_HEADERS=xforwarded` + `KC_HOSTNAME=<public-domain>`** 명시 안 하면 reverse proxy 뒤에서 redirect URL이 internal `keycloak:8080`으로 떨어져 깨짐.
```
This file is also the child-side source for `L3-F15`.
## 6. `feature-keycloak-realm-client-export.md`
### L3-F09: import wiring remains in scope after ownership was delegated
- **Severity / class:** Medium — `STALE_SUMMARY`, `RESTATED_FOREIGN_DECISION`
- **Original goal / current state:** line 53 includes automatic container import; line 168 says this branch authors JSON only and must not restate wiring.
- **Claim:** Scope and ownership trace assign the same concern differently.
- **Assumption:** Scope bullets denote implementation responsibility. Falsified if line 53 is explicitly reclassified as a consumer assumption.
- **Failure / falsification:** Realm and stack branches independently change mount paths or startup flags. Inspect actual changed artifacts.
- **Action / why:** Keep JSON authorship here and make import wiring reference-only to the stack owner.
- **Alternatives:** (1) JSON/stack split, (2) this branch owns full import lifecycle, (3) dedicated import-lifecycle owner.
- **Counterargument:** Line 53 might describe desired behavior, not ownership; its current placement does not state that distinction.
```text
$ sed -n '53p;168p' 'raw/branch-notes/feature-keycloak-realm-client-export.md'
- Keycloak 컨테이너 `--import-realm` 옵션으로 부트 시 자동 import
> **Trace / R3**: volume mount(`./realm-export.json:/opt/keycloak/data/import/realm-export.json`) + Keycloak 부트 command `--import-realm` 는 sibling [[raw/branch-notes/feature-keycloak-docker-compose-stack]] `#D4` 가 owner (`KC-IMPORT-C1/C2/C3/C4` 인용). 본 branch 는 **realm-export.json 저작만** — 배선은 restate 하지 않는다. 배선 계약이 바뀌면(import dir 경로/flag) 본 export 파일 배치에 영향(§엣지·의존).
```
### L3-F10: credential export is asserted before its evidence gate
- **Severity / class:** High — `CONTRADICTION`, unsupported certainty
- **Original goal / current state:** line 99 states credential behavior; line 193 says the password-inclusive `usersExport` claim has no primary raw source.
- **Claim:** Operational prose presents an unverified export mode as available.
- **Assumption:** Bootstrap automation may rely on exported users or passwords. Falsified by version-matched documentation and inspected export output.
- **Failure / falsification:** Imported users cannot authenticate, or sensitive credentials are handled under a false assumption.
- **Action / why:** Move credential claims to verification status until the pinned images help and actual export are inspected.
- **Alternatives:** (1) no users and separate bootstrap, (2) verified user export with secret controls, (3) Admin API provisioning, (4) disposable test credentials.
- **Counterargument:** The behavior may be remembered from another Keycloak version; that does not establish the pinned version.
```text
$ sed -n '99p;193p' 'raw/branch-notes/feature-keycloak-realm-client-export.md'
- Realm export JSON은 **user password를 포함하지 않음** (보안). user는 ID/role만 export됨 → 부트 후 admin이 password 재설정 필요. 또는 realm export에서 `usersExport=true`로 별도 export 가능.
| `usersExport=true` 옵션으로 user (password 포함) export 가능 | 본 branch 진행 중 메모 — 1차 raw 미수집 | Keycloak `kc.sh export --help` 또는 admin REST API 문서 발췌 | `planned` |
```
This file is also implicated in `L3-F04` and `L3-F07`.
## 7. `feature-keycloak-refresh-rotation-and-logout.md`
### L3-F11: concept and execution notes both own the same rotation sequence
- **Severity / class:** High — `DUAL_OWNERSHIP`, `RESTATED_FOREIGN_DECISION`
- **Original goal / current state:** the concept note declares a concept/execution split, but both notes retain a normative D-row for the same family-invalidation flow.
- **Claim:** Mutable expected behavior has two decision owners.
- **Assumption:** D-rows are normative. Falsified if P3A D5 is structurally converted into a non-normative test procedure.
- **Failure / falsification:** A failed family-kill test updates only one note. Inspect incoming D4/D5 references and modification ownership.
- **Action / why:** Keep policy/expected behavior in the concept owner and make P3A reference it from its test procedure.
- **Alternatives:** (1) concept owner/execution pointer, (2) execution owner/concept principle, (3) explicit invariant/procedure split, (4) separate verified-behavior owner.
- **Counterargument:** Definition and demonstration are naturally repeated, but both current rows make the repeated value independently mutable.
```text
$ sed -n '159p;240p' 'raw/branch-notes/feature-keycloak-refresh-token-rotation.md'
| D4 | rotation flow 4단계 (RT_1 사용 → invalidate → AT_2+RT_2 발급 → 재사용 시 family invalidate) | N/A — rotation ON 이면 항상 이 flow. 단 재사용 시 무효화 **범위**(단일 토큰 vs family 전체)는 verify 대상 | `raw/official-docs/keycloak-refresh-token-rotation-sessions-official.md#KC-RTROT-C2` (RT 1회 사용 후 무효 + 최신 RT 저장 강제 = flow 1~3단계 근거) + `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3` (refresh token = scope/RS bound MUST, 배경) + `UNSUPPORTED_DECISION` (4단계 "family 전체 invalidate" 는 `KC-RTROT-C6` doc text 부재) | `official-vendor-doc (1~3단계) + official-standard (배경) + UNSUPPORTED_DECISION (family invalidate)` | family invalidate 동작을 docker-compose 실측(RT_1 재사용 → RT_2 도 거부되는지)으로 확인 — Claims To Verify 에 계획됨 |
- cousin(다른 phase) [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] (P3A) — 본 개념 계약의 **실측·시연** 소유. 본 노트 = 개념/계약, 그쪽 = 실행.
```
```text
$ sed -n '124p' 'raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md'
| D5 | rotation flow 시연 (RT_1 사용 → AT_2+RT_2 → RT_1 재사용 → family 전체 invalidate → 재로그인 강제) | N/A (rotation ON 전제의 단일 시연 시나리오). rotation OFF 면 재사용 탐지 자체가 불가 → D1 이 전제 | `raw/official-docs/keycloak-refresh-token-rotation-reuse-admin-official.md#KC-ROT-C1` (rotation = 사용된 RT 무효화 + 새 발급) + `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3`·`#OA21-C6` (RT bound MUST, code grant → AT+RT) + `raw/official-docs/oidc-client-ts-library.md#OIDCTS-C4`·`#OIDCTS-C5` (Refresh Token Grant + Silent Refresh — SPA 자동 갱신 경로) + `UNSUPPORTED_DECISION` ("RT_1 재사용 → family 전체 invalidate" 자동 동작 = `KC-ROT-C6` 문서 부재) | `official-vendor-doc (KC-ROT-C1, OIDCTS-C4/C5) + official-standard (OA21-C3/C6) + UNSUPPORTED_DECISION (family invalidate 구체 동작)` | docker-compose 실측: RT 재사용 시 4xx + RT_2 동시 invalidate(family kill) 확인 (Claims To Verify #1) |
```
### L3-F12: delegated back-channel receiver is out of scope at its destination
- **Severity / class:** High — `CONTRADICTION`, ambiguous ownership
- **Original goal / current state:** the concept note delegates receiver implementation to P3A; P3A lists provider-triggered logout as an optional demonstration only.
- **Claim:** No note unambiguously owns the receiving endpoint.
- **Assumption:** “옵션 시연만” excludes backend implementation. Falsified by an explicit endpoint artifact and validation plan in P3A.
- **Failure / falsification:** Logout Token reception is relied upon but never built or verified.
- **Action / why:** Add the endpoint to P3A, remove the delegation, or create a dedicated owner.
- **Alternatives:** (1) implement in P3A, (2) remove from scope, (3) dedicated branch, (4) provider-side demonstration with explicit absence of receiver.
- **Counterargument:** The optional demonstration might include the endpoint; planned files and acceptance tests must show that.
```text
$ sed -n '67p' 'raw/branch-notes/feature-keycloak-refresh-token-rotation.md'
- **back-channel logout 수신** backend 구현(Logout Token 수신 endpoint) — D3, P3A 실 구현 시
```
```text
$ sed -n '61p' 'raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md'
- back-channel logout (Keycloak admin → 모든 session kill) — 옵션 시연만
```
### L3-F13: Max Reuse greater than zero is both meaningless and a weaker alternative
- **Severity / class:** High — `CONTRADICTION`, incompatible policy assumptions
- **Original goal / current state:** one note says any value above zero makes reuse detection meaningless; the other treats one or more as weaker but viable.
- **Claim:** The policy models the setting with incompatible semantics.
- **Assumption:** Max Reuse is a bounded-use counter in the pinned version. If the field differs or is absent, both claims need replacement.
- **Failure / falsification:** Concurrency mitigation either disables the intended test or rejects legitimate refreshes. Test values 0 and 1 with concurrent requests.
- **Action / why:** Replace certainty with one measured semantics table shared by both notes.
- **Alternatives:** (1) zero plus refresh serialization, (2) measured bounded reuse, (3) BFF/server-side serialization, (4) defer selection until measurement.
- **Counterargument:** “의미 없음” may mean only “unsuitable for D5”; the note does not currently state that narrower scope.
```text
$ sed -n '127p' 'raw/branch-notes/feature-keycloak-refresh-token-rotation.md'
- `Refresh Token Max Reuse > 0`이면 reuse detection 의미 없음
```
```text
$ sed -n '120p' 'raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md'
| D1 | rotation ON + Max Reuse 0 강제 — stolen token 탐지 시연 | stolen-token 탐지를 *시연*하려면 rotation ON + Max Reuse 0. 정상 SPA 가 동시 갱신(race)으로 같은 RT 를 2회 보낼 위험이 크면 대안: Max Reuse ≥1 로 완화(단 탐지력 약화, D5 시연 불가) | `raw/official-docs/keycloak-refresh-token-rotation-reuse-admin-official.md#KC-ROT-C1` ("Revoke Refresh Token" ON = 사용된 RT revoke + 새 토큰 발급 = rotation) + `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3` (RT scope/RS bound MUST — rotation 권고 배경) + `UNSUPPORTED_DECISION` ("Refresh Token Max Reuse" 설정명 + 재사용 시 family invalidate 자동 동작 = `KC-ROT-C6` 문서 부재) | `official-vendor-doc (KC-ROT-C1) + official-standard (OA21-C3 배경) + UNSUPPORTED_DECISION (Max Reuse 0 라벨 + family invalidate)` | admin UI 실측으로 "Refresh Token Max Reuse" 필드 실재·라벨 + RT 재사용 시 family kill 동작 확인 (Claims To Verify #1) |
```
## 8. `feature-keycloak-refresh-token-rotation.md`
This file is the co-source for `L3-F11`, `L3-F12`, and `L3-F13`. No additional lane-local inconsistency remained after those shared findings; its remaining related D-rows retain explicit `UNSUPPORTED_DECISION` labels rather than asserting family invalidation as verified.
## 9. `feature-keycloak-reverse-proxy-headers.md`
### L3-F14: executable Caddy draft strips a prefix the selected method requires
- **Severity / class:** High — `STALE_SUMMARY`, cannot-coexist configuration
- **Original goal / current state:** the copyable draft uses `handle_path`; its later audit says method B requires prefix-preserving `handle`/`reverse_proxy`.
- **Claim:** The executable draft and `KC_HTTP_RELATIVE_PATH=/keycloak` cannot coexist without another rewrite.
- **Assumption:** Standard `handle_path` stripping applies and Keycloak runs at `/keycloak`. Falsified by a verified rewrite or root-path Keycloak configuration.
- **Failure / falsification:** Discovery and authorization endpoints reach the wrong path and return 404. Test all OIDC endpoints through Caddy.
- **Action / why:** Repair the snippet itself or explicitly switch the design to method A.
- **Alternatives:** (1) prefix-preserving `handle`, (2) method A with deliberate stripping, (3) explicit verified rewrite, (4) nginx prefix preservation.
- **Counterargument:** Another layer may restore the prefix; no such layer is shown in the draft.
```text
$ sed -n '125p;202p' 'raw/branch-notes/feature-keycloak-reverse-proxy-headers.md'
handle_path /keycloak/* {
> ⚠️ **method B ↔ proxy 라우팅 정합 함정 (depth 감사 2026-07-18 F1)**: §진행 중 메모의 **Caddy 초안 `handle_path /keycloak/*`** 은 `/keycloak` prefix 를 **strip** 한다 (= method A 동작). 그런데 D3 은 method B(`KC_HTTP_RELATIVE_PATH=/keycloak`, Keycloak 이 스스로 `/keycloak/*` 발급)를 채택했으므로, prefix 가 strip 된 요청이 prefix 를 기대하는 Keycloak 에 도달 → **이중 strip 으로 404**. 초안의 "Caddy 환경변수는 nginx case 와 동일" 은 *환경변수*는 맞으나 *라우팅*은 다름을 가린다. nginx `location /keycloak/ { proxy_pass http://127.0.0.1:8080; }` 는 prefix 를 **보존**하므로 method B 와 정합. **method B 를 Caddy 로 쓰려면 `handle_path`(strip) 대신 `handle`/`reverse_proxy` 로 `/keycloak` prefix 를 보존**해야 한다. Caddy vendor doc 미보존(D4 UNSUPPORTED)이라 FACT 단정 대신 §Claims To Verify 로 실측 위임.
```
### L3-F16: parent owns exact proxy values while delegating proxy details
- **Severity / class:** High — `DUAL_OWNERSHIP`, `RESTATED_FOREIGN_DECISION`
- **Original goal / current state:** parent D4 fixes five exact variables; its ownership summary delegates proxy-header detail to the child.
- **Claim:** The parent contains a mutable configuration bundle that overlaps delegated owners.
- **Assumption:** Parent D4 is consumed as normative configuration. Falsified if it is generated from one canonical source and children cease owning values.
- **Failure / falsification:** A routing or security owner changes a value while the parent remains deployable and stale.
- **Action / why:** Keep integration invariants in the parent and reference each variables owner decision.
- **Alternatives:** (1) child owners plus parent references, (2) parent owns final bundle and children provide evidence, (3) explicit per-variable ownership matrix.
- **Counterargument:** An integration parent may need the assembled bundle, but then it must be the declared canonical owner rather than also delegating details.
```text
$ sed -n '239p;301p' 'raw/branch-notes/feature-keycloak-single-ec2-google-federation.md'
| D4 | Keycloak `KC_HOSTNAME=https://kc.example.com` + `KC_HTTP_RELATIVE_PATH=/keycloak` + `KC_PROXY_HEADERS=xforwarded` + `KC_HTTP_ENABLED=true` + `KC_PROXY_TRUSTED_ADDRESSES=127.0.0.1` | `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C2`, `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C5`, `raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C2`, `raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C3`, `raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C4`, `raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C5`, `raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C6` | `official-vendor-doc` | `KC_HTTP_RELATIVE_PATH=/keycloak` 와 `X-Forwarded-Prefix` 가 nginx → Keycloak 경로에서 OIDC discovery (`.well-known/openid-configuration`) 의 `issuer` 값을 어떻게 결정하는지 실 검증 필요 (`KC-RP-C6` 두 방법의 trade-off 미해소) |
- sub-sub-branch 관심사 위임: [[raw/branch-notes/feature-keycloak-public-domain-tunneling]](tunnel 상세) · [[raw/branch-notes/feature-keycloak-reverse-proxy-headers]](proxy header 상세) · [[raw/branch-notes/feature-keycloak-google-redirect-uri-policy]](redirect URI 정책) · [[raw/branch-notes/feature-keycloak-https-termination-caddy-nginx]](TLS termination) 가 각 관심사 detail owner.
```
## 10. `feature-keycloak-single-ec2-google-federation.md`
### L3-F15: parent and child both decide Cloudflare-over-ngrok
- **Severity / class:** Medium — `DUAL_OWNERSHIP`
- **Original goal / current state:** parent D3 and child D1 both own the same provider preference, while the child supplies the selection conditions.
- **Claim:** Tunnel-selection authority is duplicated.
- **Assumption:** Both D-rows are normative and independently mutable. Falsified by a declared selection/detail split without restating the mutable preference.
- **Failure / falsification:** A provider-policy change updates only one branch. Inspect inbound D3/D1 references and approval ownership.
- **Action / why:** Make the child the selection owner and reduce the parent to required outcomes plus a decision reference.
- **Alternatives:** (1) child owns selection, (2) parent owns selection and child operationalizes, (3) parent owns requirements while child maps conditions to providers.
- **Counterargument:** Parent selection plus child implementation can be valid, but both current rows phrase the provider preference as their own decision.
```text
$ sed -n '238p' 'raw/branch-notes/feature-keycloak-single-ec2-google-federation.md'
| D3 | 개발용 public URL 노출 수단은 Cloudflare Tunnel 우선, ngrok 차선 | `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C1`, `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C2`, `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C4`, `raw/official-docs/ngrok-http-tunnel-official.md#NGROK-C1`, `raw/official-docs/ngrok-http-tunnel-official.md#NGROK-C3`, `raw/official-docs/ngrok-http-tunnel-official.md#NGROK-C4`, `raw/official-docs/google-oauth2-redirect-uri-validation-official.md#GOOGLE-REDIR-C3`, `raw/official-docs/google-oauth2-redirect-uri-validation-official.md#GOOGLE-REDIR-C5` | `official-vendor-doc + official-vendor-doc + official-vendor-doc` | Cloudflare Tunnel `<UUID>.cfargotunnel.com` 도메인이 Google OAuth client redirect URI 정책 (`GOOGLE-REDIR-C2` raw IP 금지 / `C3` exact match) 을 통과하는지 직접 검증 안 됨 |
```
```text
$ sed -n '106p' 'raw/branch-notes/feature-keycloak-public-domain-tunneling.md'
| D1 | 학습 환경 1순위 **Cloudflare Tunnel** (named tunnel + 정적 URL + 무료 TLS + inbound port 0) | 정적 public URL + inbound port 0 + 무료 TLS 가 필요하고 Cloudflare 에 위임한 관리 도메인 1개를 확보할 수 있을 때. 도메인이 없거나 1회성 데모면 → **D2**(ngrok) 또는 `trycloudflare.com` quick tunnel(random URL) | `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C1` (cloudflared outbound), `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C2` (firewall inbound 차단 권장), `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C3` (tunnel `<UUID>.cfargotunnel.com` subdomain 자동 부여), `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C4` (사용자 hostname CNAME → cfargotunnel.com), `raw/official-docs/google-oauth2-redirect-uri-validation-official.md#GOOGLE-REDIR-C2` (Google redirect URI raw IP 금지 → 도메인 필요) | `official-vendor-doc + official-vendor-doc` | Cloudflare Tunnel 의 `<UUID>.cfargotunnel.com` generic subdomain 이 Google Cloud Console redirect URI 정책에 통과하는지는 `CLOUDFLARE-TUNNEL-C4` "Does not prove" 단서에 명시된 미검증 사항 — P3B 실 검증 필요 |
```
This parent is also implicated in `L3-F08` and `L3-F16`.
## Grep verification
- 실제 실행해 위에 출력한 `sed`/`grep` source lines V: 37
- byte-exact 일치 P: 37
- 라인 정정 C: 0
- 폐기 D: 0
## Claim Traceability Check
- Claim ID 연결 검사: lane-local decision references를 대조했고 `L3-F04`의 bare references 3건을 확인했다. 외부 raw Claim ID source truth 재검증은 slice 밖이다.
- Decision Evidence Map: 10개 branch note의 DEM을 검토했다. 중복 또는 stale ownership은 `L3-F06`, `L3-F11`, `L3-F15`, `L3-F16`에 기록했다.
- UNSUPPORTED_DECISION: 새 unsupported decision을 사실로 승격하지 않았다. 원문의 Max Reuse, family invalidation, proxy 관련 labels는 그대로 보존했다.
## Cross-lane candidates
| Source edge | External owner / decision | Reason |
| --- | --- | --- |
| `feature-keycloak-nginx-auth-request-integration.md:50` | `feature-keycloak-edge-forwardauth-no-google D1` | standalone nginx 예시와 parent Kubernetes/Traefik split의 정합성 — `NEEDS_CROSS_LANE_AUDIT` |
| `feature-keycloak-nginx-auth-request-integration.md:249` | `feature-keycloak-edge-forwardauth-no-google D2` | forwarded-header naming의 canonical owner — `NEEDS_CROSS_LANE_AUDIT` |
| `feature-keycloak-nginx-auth-request-integration.md:253` | `feature-keycloak-header-spoofing-defense D5` | 요구하는 `X-Internal-Auth-Token`이 현재 nginx D4에 없음 — `NEEDS_CROSS_LANE_AUDIT` |
| `feature-keycloak-oauth2-proxy-oidc-flow.md:194` | `feature-keycloak-docker-compose-stack D3` | discovery startup gating과 healthcheck 책임 — `NEEDS_CROSS_LANE_AUDIT` |
| `feature-keycloak-realm-client-export.md:168` | `feature-keycloak-docker-compose-stack D4` | realm import mount/command ownership — `NEEDS_CROSS_LANE_AUDIT` |
| `feature-keycloak-realm-client-export.md:182` | `feature-keycloak-single-ec2-no-google D3`, docker-compose D3 | hostname/redirect URI 전제와 bare sibling 귀속 — `NEEDS_CROSS_LANE_AUDIT` |
| `feature-keycloak-reverse-proxy-headers.md:151` | `feature-keycloak-https-termination-caddy-nginx` | Caddy/nginx selection ownership — `NEEDS_CROSS_LANE_AUDIT` |
| `feature-keycloak-reverse-proxy-headers.md:153`, `:258` | `feature-keycloak-header-spoofing-defense D6` | `KC_PROXY_TRUSTED_ADDRESSES` owner 정규화 — `NEEDS_CROSS_LANE_AUDIT` |
| `feature-keycloak-reverse-proxy-headers.md:154` | `feature-keycloak-iss-claim-hostname-mismatch` | `KC_HOSTNAME` value owner와 parent D4의 중복 여부 — `NEEDS_CROSS_LANE_AUDIT` |
| `feature-keycloak-single-ec2-google-federation.md:240`, `:279` | first-broker/account-linking siblings | parent D5와 consume-only 구현 절의 경계 — `NEEDS_CROSS_LANE_AUDIT` |
| `feature-keycloak-single-ec2-google-federation.md:241`, `:301` | `feature-keycloak-google-redirect-uri-policy` | parent D6 exact URI와 delegated policy owner의 경계 — `NEEDS_CROSS_LANE_AUDIT` |
```wiki-stats
agent: wiki-research-lane
found: 10
processed: 10
dropped: 0
```
@@ -0,0 +1,564 @@
## Lane Inventory
| Path | Status | Evidence Lines | Extracted Facts |
|---|---|---:|---:|
| `raw/branch-notes/feature-keycloak-single-ec2-no-google.md` | READ_FULL | 1342 | 3 findings |
| `raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md` | READ_FULL | 1256 | 3 findings |
| `raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md` | READ_FULL | 1268 | 2 findings |
| `raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md` | READ_FULL | 1231 | 2 findings |
| `raw/branch-notes/feature-keycloak-three-leg-trust-chain.md` | READ_FULL | 1229 | 3 findings |
| `raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md` | READ_FULL | 1237 | 2 findings |
| `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md` | READ_FULL | 1257 | 3 findings |
| `raw/branch-notes/feature-security-operational-baseline.md` | READ_FULL | 1417 | 3 findings |
## `raw/branch-notes/feature-keycloak-single-ec2-no-google.md`
### L4-F01 — `localhost` issuer와 `extra_hosts` 해결책이 실행 가능한 한 가지 구성으로 종결되지 않음
- **Severity / 유형**: High — ambiguous/incompatible default flow
- **Source quotes**:
- `"extra_hosts: [host.docker.internal:host-gateway]"``raw/branch-notes/feature-keycloak-single-ec2-no-google.md:146`
- `"backend도 \`issuer-uri=http://localhost:8080/...\` 사용. 컨테이너 간 통신은 \`network_mode: host\` 또는 \`extra_hosts\` 사용."` — `raw/branch-notes/feature-keycloak-single-ec2-no-google.md:181`
- **Claim**: bridge-network backend 컨테이너에서 `localhost`는 backend 자신이다. `extra_hosts``host.docker.internal` 이름을 추가할 뿐 `localhost`를 호스트로 바꾸지 않는다. 두 메커니즘을 동일한 해결책처럼 병기하면 기본 구성이 실행 불가능하다.
- **Assumptions**: backend도 Docker bridge network에서 실행되고, 별도 `jwk-set-uri` override가 없다.
- **Failure mode**: OIDC discovery/JWKS 접근 실패로 backend startup 실패 또는 전 요청 401.
- **Falsification**: 실제 기본이 `network_mode: host`, backend host-process 실행, 또는 `issuer-uri=localhost``jwk-set-uri=host.docker.internal`을 명시적으로 분리한 구성이라면 비판이 무효화된다.
- **Required action / Why**: `feature-keycloak-iss-claim-hostname-mismatch` owner에서 기본 한 가지를 고정하고 이 노트는 포인터만 유지한다. issuer 문자열 일치와 네트워크 도달성은 서로 다른 조건이기 때문이다.
- **Alternatives**:
1. host network + `issuer-uri=localhost`
2. bridge + `issuer-uri=localhost` + 명시적 `jwk-set-uri=host.docker.internal`
3. front/backchannel 동적 URL을 명시적으로 구성
- **Counterargument**: `extra_hosts`로 호스트 접근은 가능하다. 다만 접근 hostname은 `host.docker.internal`이므로, 현재 적힌 `localhost` discovery URI와 자동으로 결합되지는 않는다.
- **Self-grep**:
```text
$ grep -nFo -e 'backend도 `issuer-uri=http://localhost:8080/...` 사용. 컨테이너 간 통신은 `network_mode: host` 또는 `extra_hosts` 사용.' -e 'extra_hosts: [host.docker.internal:host-gateway]' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
146:extra_hosts: [host.docker.internal:host-gateway]
181:backend도 `issuer-uri=http://localhost:8080/...` 사용. 컨테이너 간 통신은 `network_mode: host` 또는 `extra_hosts` 사용.
```
### L4-F02 — `aud` validator owner 정정 후 컴포넌트 표가 옛 owner를 계속 지목
- **Severity / 유형**: Medium — `STALE_SUMMARY`
- **Source quotes**:
- `"Spring RS 셋업·\`aud\` 검증 = [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 owner"` — `...single-ec2-no-google.md:110`
- `"\`aud\` validator 는 child [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] D2"` — `...single-ec2-no-google.md:215`
- **Claim**: 같은 문서가 owner를 `audience-validator`로 정정한 뒤 실행 표에서는 여전히 deferred RBAC 문서의 D2를 가리킨다.
- **Assumptions**: line 110의 2026-07-18 owner split이 최신 결정이다.
- **Failure mode**: 구현자가 role-mapping의 복제된 validator를 수정하고 정본과 다시 갈라진다.
- **Falsification**: role-mapping D2가 여전히 공동 owner라면 Single-Owner 선언 자체를 철회해야 한다.
- **Required action / Why**: line 215와 동일 요약들을 `[[...spring-rs-audience-validator]] D1`로 바꾸고 role-mapping은 RBAC D4D6만 가리킨다.
- **Alternatives**: RS/audience 전체를 role-mapping으로 다시 이관할 수도 있으나 두 문서 중 하나만 owner여야 한다.
- **Counterargument**: 표가 역사적 child 목록일 수 있다. 그러나 셀 문구가 현재 구현 근거를 직접 지목하므로 역사 기록으로 읽히지 않는다.
- **Self-grep**:
```text
$ grep -nFo -e 'Spring RS 셋업·`aud` 검증 = [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 owner' -e '`aud` validator 는 child [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] D2' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
110:Spring RS 셋업·`aud` 검증 = [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 owner
215:`aud` validator 는 child [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] D2
```
### L4-F03 — PKCE 구현 순서가 parent와 실행 owner에서 반대
- **Severity / 유형**: Medium — `CONTRADICTION`
- **Source quotes**:
- `"vanilla JS는 manual fetch + \`crypto.subtle\` 기반 PKCE 구현 우선"` — `...single-ec2-no-google.md:182`
- `"\`oidc-client-ts\` 우선, manual은 별도 단계."` — `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md:106`
- **Claim**: parent는 manual-first, 실행 owner는 library-first를 결정한다.
- **Assumptions**: 두 문구 모두 실제 코딩 순서를 뜻한다.
- **Failure mode**: 작업 순서·완료 조건·테스트 baseline이 달라진다.
- **Falsification**: 전자를 사전 학습 순서, 후자를 repo 구현 순서로 명시하면 양립 가능하다.
- **Required action / Why**: 실행 순서는 child D1을 정본으로 두고 parent D4를 “manual 사전 학습 선택지”로 재분류한다.
- **Alternatives**: manual spike → 폐기 → library 구현, 또는 library E2E → manual 비교 실습.
- **Counterargument**: 결국 둘 다 수행하므로 충돌이 작다. 하지만 단계별 구현 branch의 착수 순서와 acceptance criteria에는 직접 영향을 준다.
- **Self-grep**:
```text
$ grep -nFo -e 'vanilla JS는 manual fetch + `crypto.subtle` 기반 PKCE 구현 우선' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
182:vanilla JS는 manual fetch + `crypto.subtle` 기반 PKCE 구현 우선
$ grep -nFo -e '`oidc-client-ts` 우선, manual은 별도 단계.' -- raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md
106:`oidc-client-ts` 우선, manual은 별도 단계.
```
## `raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md`
### L4-F04 — “SPA Direct, BFF 미채택” 기본이 실제로는 TMB를 요구
- **Severity / 유형**: High — incompatible architecture default
- **Source quotes**:
- `"P2A 권장 조합 = access_token: 메모리 + refresh_token: secure httpOnly cookie"``...spa-token-storage-tradeoff.md:140`
- `"사실상 최소 Token-Mediating Backend(TMB). \"순수 no-server SPA Direct\" 와의 긴장"``...spa-token-storage-tradeoff.md:165`
- **Claim**: 브라우저 JS는 HttpOnly cookie를 설정할 수 없다. D1을 구현하려면 backend가 refresh token을 받아 `Set-Cookie`하고 `/refresh`를 소유해야 하므로 순수 SPA Direct가 아니다.
- **Assumptions**: “SPA Direct”가 token endpoint를 브라우저가 직접 호출하고 server-side token custody가 없다는 뜻이다.
- **Failure mode**: HttpOnly 저장이 구현되지 않거나, 암묵적으로 TMB를 추가하면서 CSRF·rotation·logout 계약이 빠진다.
- **Falsification**: 프로젝트 taxonomy가 Resource Server의 `/refresh` 추가를 여전히 AP1로 명시적으로 허용하면 무효화된다.
- **Required action / Why**: D1을 순수 AP1과 TMB 변형으로 분리하고 기본을 하나 고정한다. token custody가 인증 패턴을 바꾸기 때문이다.
- **Alternatives**:
1. 순수 SPA: 양 토큰 memory-only + reload 재로그인
2. TMB: refresh token HttpOnly cookie + `/refresh`
3. BFF: 모든 token server-side
- **Counterargument**: 기존 Resource Server에 endpoint 하나만 추가하면 된다. 그래도 refresh token 보유·CSRF·cookie lifecycle 책임이 추가되어 trust boundary가 달라진다.
- **Self-grep**:
```text
$ grep -nFo -e 'P2A 권장 조합 = access_token: 메모리 + refresh_token: secure httpOnly cookie' -e '사실상 최소 Token-Mediating Backend(TMB). "순수 no-server SPA Direct" 와의 긴장' -- raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md
140:P2A 권장 조합 = access_token: 메모리 + refresh_token: secure httpOnly cookie
165:사실상 최소 Token-Mediating Backend(TMB). "순수 no-server SPA Direct" 와의 긴장
```
### L4-F05 — D3·D6 근거 상태를 해소한 뒤 Claims To Verify가 계속 `UNSUPPORTED`로 기록
- **Severity / 유형**: Medium — `STALE_SUMMARY`
- **Source quotes**:
- `"D3 를 브라우저·토폴로지 조건부로 **정밀화**"``...spa-token-storage-tradeoff.md:131`
- `"D6 를 \`UNSUPPORTED\` 에서 해소"` — `...spa-token-storage-tradeoff.md:132`
- `"본 branch D3 의 근거가 UNSUPPORTED"``...spa-token-storage-tradeoff.md:214`
- `"D6 가 UNSUPPORTED"``...spa-token-storage-tradeoff.md:216`
- **Claim**: active decision과 검증 큐가 같은 근거 상태를 다르게 표시한다.
- **Assumptions**: 2026-07-18 decision update가 최신이다.
- **Failure mode**: 이미 조사한 근거를 재조사하거나, 실제로 남은 runtime 검증과 문헌 근거 갭을 혼동한다.
- **Falsification**: Claims 행이 과거 상태를 보존하는 역사 섹션이면 날짜와 resolved 표기가 있어야 한다.
- **Required action / Why**: D3는 “근거 확보, topology별 E2E planned”, D6는 “inference 근거 확보, 저장 동작 E2E planned”로 분리한다.
- **Alternatives**: 해소된 행을 resolved archive로 이동하고 새 empirical claim만 남긴다.
- **Counterargument**: 구현 검증은 아직 안 됐다. 그렇더라도 “문헌 unsupported”와 “runtime unverified”는 다른 상태다.
- **Self-grep**:
```text
$ grep -nFo -e 'D3 를 브라우저·토폴로지 조건부로 **정밀화**' -e '본 branch D3 의 근거가 UNSUPPORTED' -e 'D6 를 `UNSUPPORTED` 에서 해소' -e 'D6 가 UNSUPPORTED' -- raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md
131:D3 를 브라우저·토폴로지 조건부로 **정밀화**
132:D6 를 `UNSUPPORTED` 에서 해소
214:본 branch D3 의 근거가 UNSUPPORTED
216:D6 가 UNSUPPORTED
```
### L4-F06 — cookie-CSRF 구현 위임 대상에 소유 Decision이 없음
- **Severity / 유형**: Medium — ambiguous owner / owner-less concern
- **Source quotes**:
- `"Spring Security \`CsrfTokenRepository\` 설정/검증은 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 계열 RS 결정으로 위임"` — `...spa-token-storage-tradeoff.md:63`
- `"본 노트는 발급된 토큰의 **검증 측**만."``...spring-rs-audience-validator.md:64`
- **Claim**: storage 문서는 CSRF 실 구현을 audience-validator “계열”로 보냈지만, 대상 문서는 bearer JWT 검증만 소유하며 CSRF D-row가 없다. 포인터도 `D<n>` 없이 광범위하다.
- **Assumptions**: HttpOnly refresh cookie/TMB 변형을 실제 선택한다.
- **Failure mode**: bearer API의 `csrf.disable()`를 cookie refresh endpoint에도 적용하거나, CSRF token 발급·검증을 아무 branch도 구현하지 않는다.
- **Falsification**: 별도 unseen owner가 CSRF endpoint 계약을 이미 소유한다면 그 owner D-row를 직접 가리키면 된다.
- **Required action / Why**: TMB/BFF refresh endpoint owner에 CSRF Decision을 두고 여기서는 정확한 D 포인터만 유지한다.
- **Alternatives**: audience-validator가 범위를 확장할 경우 명시적 D-row·cookie endpoint scope·음성 테스트를 추가한다.
- **Counterargument**: 같은 `SecurityFilterChain`에서 구현할 수 있다. 구현 위치와 결정 owner는 별개이며 bearer와 cookie 요청은 CSRF 위협 모델이 다르다.
- **Self-grep**:
```text
$ grep -nFo -e 'Spring Security `CsrfTokenRepository` 설정/검증은 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 계열 RS 결정으로 위임' -- raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md
63:Spring Security `CsrfTokenRepository` 설정/검증은 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 계열 RS 결정으로 위임
$ grep -nFo -e '본 노트는 발급된 토큰의 **검증 측**만.' -- raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md
64:본 노트는 발급된 토큰의 **검증 측**만.
223:본 노트는 발급된 토큰의 **검증 측**만.
```
## `raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md`
### L4-F07 — “custom validator 필수” 요약이 최신 property-first 선택 조건과 어긋남
- **Severity / 유형**: Medium — `STALE_SUMMARY`
- **Source quotes**:
- `"custom \`OAuth2TokenValidator<Jwt>\`를 추가해야 하는 이유"` — `...spring-rs-audience-validator.md:17`
- `"단일 audience** 면 property 한 줄이 단순·안전(권장)"``...spring-rs-audience-validator.md:183`
- **Claim**: 목적·본문은 custom validator를 필수로 표현하지만 최신 구현 가이드는 단일 audience에서 Boot `audiences` property를 권장 baseline으로 둔다.
- **Assumptions**: 본 프로젝트는 우선 단일 expected audience다.
- **Failure mode**: 불필요한 custom decoder wiring에서 default issuer/timestamp validator를 덮어쓰거나 누락한다.
- **Falsification**: 다중 audience/`azp` 조건부 검증이 고정 요구라면 custom이 필수일 수 있다.
- **Required action / Why**: “aud 검증은 필수, 구현 방식은 단일=property / 복합=custom”으로 목표·TODO·면접 문장을 동기화한다.
- **Alternatives**: custom 방식은 비교 학습 예제로 남긴다.
- **Counterargument**: custom 구현 학습이 목적이다. 학습 예제라는 점과 production 기본을 분리하면 된다.
- **Self-grep**:
```text
$ grep -nFo -e 'custom `OAuth2TokenValidator<Jwt>`를 추가해야 하는 이유' -e '단일 audience** 면 property 한 줄이 단순·안전(권장)' -- raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md
17:custom `OAuth2TokenValidator<Jwt>`를 추가해야 하는 이유
183:단일 audience** 면 property 한 줄이 단순·안전(권장)
```
### L4-F08 — expected audience가 `backend-client-id``spa-client`로 갈림
- **Severity / 유형**: High — `CONTRADICTION`
- **Source quotes**:
- `"backend client_id 로 \`aud\` 를 검증"` — `...spring-rs-audience-validator.md:156`
- `"\`aud=spa-client\`를 강제했습니다"` — `...spring-rs-role-mapping.md:38`
- **Claim**: 정본은 API/backend용 audience를 요구하지만 소비 문서는 SPA public client ID를 expected audience로 고정한다.
- **Assumptions**: `backend-client-id``spa-client`는 별도 client다.
- **Failure mode**: 정상 토큰이 모두 401이 되거나, 잘못된 resource audience를 허용해 cross-resource token reuse 방어가 무효화된다.
- **Falsification**: API audience 식별자를 의도적으로 `spa-client`와 동일하게 정의했다면 두 문구는 같은 값을 뜻할 수 있다. 이 경우 owner가 그 동일성부터 명시해야 한다.
- **Required action / Why**: realm/client-export owner와 함께 expected audience 심볼 하나를 고정하고 mapper·validator·음성 테스트에 동일하게 사용한다.
- **Alternatives**: backend client ID, API audience URI, 또는 의도적으로 SPA client ID를 API audience로 재사용.
- **Counterargument**: Keycloak public client가 access token audience일 수 있다. 그렇다면 “backend client_id mapper 필수” 결정이 잘못된 것이므로 둘 중 하나를 고쳐야 한다.
- **Self-grep**:
```text
$ grep -nFo -e 'backend client_id 로 `aud` 를 검증' -- raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md
156:backend client_id 로 `aud` 를 검증
$ grep -nFo -e '`aud=spa-client`를 강제했습니다' -- raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md
38:`aud=spa-client`를 강제했습니다
```
## `raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md`
### L4-F09 — RS/audience 위임 선언 뒤 active scope·TODO에서 세부를 계속 재명세
- **Severity / 유형**: Medium — `RESTATED_FOREIGN_DECISION`
- **Source quotes**:
- `"Spring RS 공통 셋업 + \`aud\` 검증 내용은 형제 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 **owner**"` — `...spring-rs-role-mapping.md:19`
- `"custom \`OAuth2TokenValidator<Jwt>\` for \`aud=spa-client\`"` — `...spring-rs-role-mapping.md:50`
- `"여기서 재명세하지 않고"``...spring-rs-role-mapping.md:128`
- **Claim**: owner 정리는 정확하지만 제목·WHY·In scope·TODO가 custom validator, issuer, decoder를 실행 항목으로 계속 보유한다.
- **Assumptions**: line 19/128의 owner 이관이 최신이다.
- **Failure mode**: audience 구현이 두 branch에서 독립 진화하며 audience 값과 wiring이 갈라진다.
- **Falsification**: 이 파일이 역사적 migration snapshot이라면 active scope/TODO가 아니라 Audit 섹션에 있어야 한다.
- **Required action / Why**: 파일을 RBAC-only로 축소하고 RS-common은 `[[...audience-validator]] D1/D6 — 1줄 요약`만 남긴다.
- **Alternatives**: 반대로 전체 RS를 이 파일로 합치고 audience-validator를 폐기할 수 있으나 Single-Owner는 하나여야 한다.
- **Counterargument**: 구현 편의를 위한 fold-in이다. 세부 복제는 owner 진화 시 현재처럼 `aud` 값이 갈라지는 원인이 된다.
- **Self-grep**:
```text
$ grep -nFo -e 'Spring RS 공통 셋업 + `aud` 검증 내용은 형제 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 **owner**' -e 'custom `OAuth2TokenValidator<Jwt>` for `aud=spa-client`' -e '여기서 재명세하지 않고' -- raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md
19:Spring RS 공통 셋업 + `aud` 검증 내용은 형제 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 **owner**
50:custom `OAuth2TokenValidator<Jwt>` for `aud=spa-client`
128:여기서 재명세하지 않고
```
### L4-F10 — realm-role-only 결정의 “단일 client” 전제가 4-pattern/client base와 stale
- **Severity / 유형**: Medium — assumption drift / `STALE_SUMMARY`
- **Source quotes**:
- `"단일 client 환경(client-level role 분리 의미 적음)"``...spring-rs-role-mapping.md:122`
- `"4 패턴 통합 base 로 확장 시 realm 은 공유 1개 유지, client 는 패턴당 1개"``...single-ec2-no-google.md:199`
- **Claim**: RBAC D4의 선택 조건은 client가 하나라는 전제인데 현재 배포 base는 패턴별 여러 client를 둔다.
- **Assumptions**: deferred authZ를 4개 패턴 통합 base에 적용할 예정이다.
- **Failure mode**: 패턴별 권한 분리가 필요한데 realm role이 모든 client에 전역 적용되거나, 반대로 필요한 공통 role이 중복 정의된다.
- **Falsification**: RBAC가 모든 client에 의도적으로 동일한 realm-global 정책이면 유지 가능하다.
- **Required action / Why**: D4 선택 조건을 “realm-global 권한” 대 “client-specific 권한”으로 재작성하고 client 수를 근거로 삼지 않는다.
- **Alternatives**: realm-only, `resource_access` only, 공통 realm role + 민감 endpoint client role 병행.
- **Counterargument**: authZ는 deferred라 지금 결정할 필요가 없다. 그렇다면 “단일 client라 의미 없음”이라는 확정 이유도 deferred로 낮춰야 한다.
- **Self-grep**:
```text
$ grep -nFo -e '단일 client 환경(client-level role 분리 의미 적음)' -- raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md
122:단일 client 환경(client-level role 분리 의미 적음)
$ grep -nFo -e '4 패턴 통합 base 로 확장 시 realm 은 공유 1개 유지, client 는 패턴당 1개' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
199:4 패턴 통합 base 로 확장 시 realm 은 공유 1개 유지, client 는 패턴당 1개
```
## `raw/branch-notes/feature-keycloak-three-leg-trust-chain.md`
### L4-F11 — 3-leg 표기에는 Backend가 없지만 실제 Hop 3의 검증자는 Backend
- **Severity / 유형**: Medium — ambiguous trust-chain taxonomy
- **Source quotes**:
- `"P2A는 2-leg trust (Browser ↔ Keycloak)였으나, P2B는 **3-leg trust** (Browser ↔ Keycloak ↔ Google)."``...three-leg-trust-chain.md:33`
- `"**Hop 3: SPA → Backend**"``...three-leg-trust-chain.md:47`
- **Claim**: WHY의 actor chain은 BrowserKeycloakGoogle만 표시하지만 계약 표는 Google→Keycloak→SPA→Backend의 세 hop과 네 actor를 다룬다.
- **Assumptions**: “leg”가 실제 검증 hop을 뜻한다.
- **Failure mode**: trust-boundary 그림에서 backend validator가 빠지거나, Browser↔Keycloak leg와 Keycloak→SPA 발급 leg가 서로 다른 방식으로 계산된다.
- **Falsification**: “3-leg”가 federation actor 수를 뜻한다면 Hop 3 명칭을 다른 체계로 분리해야 한다.
- **Required action / Why**: “3-hop / four-party validation chain”으로 정의하거나 federation chain과 API validation chain을 별도 도식으로 나눈다.
- **Alternatives**: `Google→Keycloak`, `Keycloak→SPA`, `SPA→Backend`를 canonical hop 정의로 고정.
- **Counterargument**: 제목은 학습용 약칭이다. 신뢰 경계 owner 문서에서는 약칭도 verifier 누락을 만들지 않도록 정의가 필요하다.
- **Self-grep**:
```text
$ grep -nFo -e 'P2A는 2-leg trust (Browser ↔ Keycloak)였으나, P2B는 **3-leg trust** (Browser ↔ Keycloak ↔ Google).' -e '**Hop 3: SPA → Backend**' -- raw/branch-notes/feature-keycloak-three-leg-trust-chain.md
33:P2A는 2-leg trust (Browser ↔ Keycloak)였으나, P2B는 **3-leg trust** (Browser ↔ Keycloak ↔ Google).
47:**Hop 3: SPA → Backend**
```
### L4-F12 — 면접 답변은 Keycloak nonce 자동 처리를 사실로 말하지만 D3는 미검증
- **Severity / 유형**: Medium — evidence-status contradiction
- **Source quotes**:
- `"\`nonce\`를 확인합니다."` — `...three-leg-trust-chain.md:36`
- `"Keycloak 의 *자동 처리 동작*은 미검증"``...three-leg-trust-chain.md:120`
- **Claim**: normative OIDC RP 의무는 확보됐지만 Keycloak adapter가 해당 요청/검증을 자동 수행하는지는 아직 확인하지 않았다. 면접 문장은 이 구분을 제거한다.
- **Assumptions**: line 36은 추후 파생 가능한 답변 초안이다.
- **Failure mode**: 구현 관측 없이 Keycloak-specific 동작을 외부에 확정적으로 주장한다.
- **Falsification**: Keycloak source, 공식 IdP 문서 또는 HAR로 nonce 송신·대조가 확인되면 승격 가능하다.
- **Required action / Why**: 검증 전에는 “OIDC RP가 nonce를 검증해야 하며, Keycloak 자동 처리는 확인 예정”으로 제한한다.
- **Alternatives**: CV6을 닫은 뒤 현재 문장 복원.
- **Counterargument**: 표준 준수 제품이라 예상 가능하다. 표준이 요구하는 것과 특정 버전·설정이 실제 수행하는 것은 별도 증거다.
- **Self-grep**:
```text
$ grep -nFo -e '`nonce`를 확인합니다.' -e 'Keycloak 의 *자동 처리 동작*은 미검증' -- raw/branch-notes/feature-keycloak-three-leg-trust-chain.md
36:`nonce`를 확인합니다.
120:Keycloak 의 *자동 처리 동작*은 미검증
```
### L4-F13 — out-of-scope claim/linking 정책을 구체 검증 항목으로 재진술
- **Severity / 유형**: Medium — `RESTATED_FOREIGN_DECISION`
- **Source quotes**:
- `"claim mapping — [[raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role]]"``...three-leg-trust-chain.md:57`
- `"Account Linking — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]"``...three-leg-trust-chain.md:58`
- `"\`email_verified\`, \`hd\`, Account Linking 결정"` — `...three-leg-trust-chain.md:95`
- **Claim**: 이 노트는 mapper/account-linking을 위임하면서 내부 hop 표에서는 `email_verified`·`hd`·linking을 Keycloak이 검증하는 고정 정책처럼 적는다. 이들은 설정/flow 의존이며 owner D 포인터도 없다.
- **Assumptions**: 기본 brokering만으로 세 항목이 모두 강제되지는 않는다.
- **Failure mode**: `hd` enforcement 또는 verified-email linking guard가 구성되지 않았는데 trust check가 존재한다고 오인한다.
- **Falsification**: unseen owners가 세 항목을 명시적으로 강제하고 이 문서가 정확한 D-row를 가리키면 1줄 요약으로 유지 가능하다.
- **Required action / Why**: row를 “First Broker Login + mapper 정책(config-dependent)”으로 낮추고 각 owner D-row만 연결한다.
- **Alternatives**: 이 검증들을 본 문서가 소유하려면 out-of-scope 선언을 제거하고 Single-Owner를 재배치해야 한다.
- **Counterargument**: 단순 요약이다. 현재 요약은 어떤 검사가 실제 활성인지까지 암시하므로 메커니즘 복제에 해당한다.
- **Self-grep**:
```text
$ grep -nFo -e '`email_verified`, `hd`, Account Linking 결정' -e 'claim mapping — [[raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role]]' -e 'Account Linking — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]' -- raw/branch-notes/feature-keycloak-three-leg-trust-chain.md
57:claim mapping — [[raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role]]
58:Account Linking — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]
95:`email_verified`, `hd`, Account Linking 결정
```
## `raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md`
### L4-F14 — P3A를 nginx+oauth2-proxy edge-auth 패턴으로 잘못 귀속
- **Severity / 유형**: High — `CONTRADICTION`
- **Source quotes**:
- `"nginx+oauth2-proxy 확정(P3A)"``...traefik-forwardauth-alternative.md:169`
- `"client (vanilla JS SPA via nginx static)"` / `"Browser → Keycloak → Backend Resource Server JWT validation"``...single-ec2-no-google.md:33`
- **Claim**: Traefik 문서는 P3A를 edge ForwardAuth 구성으로 고정하지만 P3A owner는 SPA-direct PKCE + backend JWT validation이며 nginx는 static server다.
- **Assumptions**: P3A 최신 재매핑은 AP1 + single-EC2 deployment base다.
- **Failure mode**: oauth2-proxy를 잘못 추가해 browser token custody, backend validation 책임, 인증 redirect 흐름이 모두 바뀐다.
- **Falsification**: “AP4 edge-auth를 single-EC2 위에 올린 별도 실행 변형”을 뜻했다면 P3A가 아니라 그 조합으로 명명해야 한다.
- **Required action / Why**: nginx+oauth2-proxy는 P1A/AP4 owner로 돌리고, single-EC2는 배포 축으로만 참조한다.
- **Alternatives**: `AP1-on-single-EC2``AP4-on-single-EC2`를 별도 matrix row로 둔다.
- **Counterargument**: 같은 EC2에서 oauth2-proxy도 실행할 수 있다. 물리 배포 가능성과 인증 패턴 정체성은 별도 축이다.
- **Self-grep**:
```text
$ grep -nFo -e 'nginx+oauth2-proxy 확정(P3A)' -- raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md
169:nginx+oauth2-proxy 확정(P3A)
$ grep -nFo -e 'client (vanilla JS SPA via nginx static)' -e 'Browser → Keycloak → Backend Resource Server JWT validation' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
33:client (vanilla JS SPA via nginx static)
33:Browser → Keycloak → Backend Resource Server JWT validation
```
### L4-F15 — redirect를 middleware가 생성한다고 적었지만 D3는 auth server 응답 전달로 정의
- **Severity / 유형**: Medium — `STALE_SUMMARY`
- **Source quotes**:
- `"middleware가 직접 302를 발급"``...traefik-forwardauth-alternative.md:97`
- `"비 2XX → 인증 서버 응답 그대로 client 에 반환"``...traefik-forwardauth-alternative.md:112`
- **Claim**: ForwardAuth middleware가 redirect를 생성하는 것과 auth service의 302를 전달하는 것은 다른 책임 모델이다.
- **Assumptions**: D3의 vendor-backed contract가 정본이다.
- **Failure mode**: redirect loop나 `Location` 손실을 Traefik 설정 문제로만 진단하고 oauth2-proxy 응답을 점검하지 않는다.
- **Falsification**: 사용 중인 auth plugin이 Traefik 내부에서 직접 redirect를 생성한다면 별도 D10 경로로 분리해야 한다.
- **Required action / Why**: line 97을 “auth server의 302를 Traefik이 그대로 전달”로 갱신한다.
- **Alternatives**: oauth2-proxy 외부 서버와 in-process OIDC plugin의 redirect owner를 표에서 분리한다.
- **Counterargument**: browser 입장에서는 Traefik에서 302가 온다. 그러나 장애 분석에는 302 생성 주체와 전달 주체 구분이 필요하다.
- **Self-grep**:
```text
$ grep -nFo -e 'middleware가 직접 302를 발급' -e '비 2XX → 인증 서버 응답 그대로 client 에 반환' -- raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md
97:middleware가 직접 302를 발급
112:비 2XX → 인증 서버 응답 그대로 client 에 반환
```
## `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md`
### L4-F16 — relative `/api/me`·`/token` URL이 static-only nginx topology에서 잘못된 origin으로 감
- **Severity / 유형**: High — `CONTRADICTION`
- **Source quotes**:
- `"fetch('/api/me', { headers: { Authorization: 'Bearer ' + user.access_token } })"``...vanilla-js-spa-pkce.md:176`
- `"manual \`fetch('/token'"` — `...vanilla-js-spa-pkce.md:190`
- `"nginx는 단순 static 파일 서빙"``...single-ec2-no-google.md:159`
- **Claim**: SPA origin이 `http://localhost`이고 nginx가 static-only이면 두 relative URL은 nginx:80으로 향한다. 문서의 backend:8081·Keycloak:8080 직노출 topology와 양립하지 않는다.
- **Assumptions**: nginx에 `/api`·`/token` reverse proxy가 없다.
- **Failure mode**: API는 404, token exchange는 404/405가 되어 E2E가 시작되지 않는다.
- **Falsification**: 실제 nginx config가 두 path를 proxy한다면 parent의 static-only·3-port/CORS 계약이 stale다.
- **Required action / Why**: direct topology면 absolute `http://localhost:8081/api/me`와 discovered Keycloak token endpoint를 사용하고, same-origin topology면 nginx proxy를 owner 결정으로 승격한다.
- **Alternatives**:
1. 3-port direct + CORS
2. nginx `/api`·OIDC proxy + same-origin
3. runtime config에서 endpoint base URL 주입
- **Counterargument**: dev proxy가 상대 URL을 처리할 수 있다. 현재 산출물은 vanilla static nginx이며 dev proxy가 명세에 없다.
- **Self-grep**:
```text
$ grep -nFo -e 'fetch('\''/api/me'\'', { headers: { Authorization: '\''Bearer '\'' + user.access_token } })' -e 'manual `fetch('\''/token'\''' -- raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md
84:fetch('/api/me', { headers: { Authorization: 'Bearer ' + user.access_token } })
176:fetch('/api/me', { headers: { Authorization: 'Bearer ' + user.access_token } })
190:manual `fetch('/token'
$ grep -nFo -e 'nginx는 단순 static 파일 서빙' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
159:nginx는 단순 static 파일 서빙
```
### L4-F17 — oidc-client-ts default가 localStorage라는 단정과 “미증명” 상태가 공존
- **Severity / 유형**: Medium — evidence-status `CONTRADICTION`
- **Source quotes**:
- `"oidc-client-ts 기본은 \`WebStorageStateStore\` (localStorage)."` — `...vanilla-js-spa-pkce.md:98`
- `"default store 종류(localStorage 여부)는 본 Sources 로 미증명"``...vanilla-js-spa-pkce.md:168`
- **Claim**: 같은 파일이 library default를 사실과 미확인으로 동시에 표시한다. `stateStore``userStore` default도 구분되지 않았다.
- **Assumptions**: library version별 default가 달라질 수 있고 현재 raw source가 이를 고정하지 않는다.
- **Failure mode**: 불필요하거나 잘못된 override로 redirect transaction state가 소실되거나, 실제 token이 persistent storage에 남는다.
- **Falsification**: pin된 oidc-client-ts 버전 공식 API/default 문서가 localStorage를 명시하면 첫 문장을 근거화할 수 있다.
- **Required action / Why**: 검증 전 line 98을 inference로 낮추고 두 store의 default와 token 저장 여부를 각각 확인한다.
- **Alternatives**: 명시적 custom store로 version default에 의존하지 않되 full-page redirect state 생존 테스트를 추가한다.
- **Counterargument**: 어차피 memory override를 쓸 예정이다. `stateStore`까지 memory로 바꾸면 callback state가 사라질 수 있어 default 확인이 여전히 필요하다.
- **Self-grep**:
```text
$ grep -nFo -e 'oidc-client-ts 기본은 `WebStorageStateStore` (localStorage).' -e 'default store 종류(localStorage 여부)는 본 Sources 로 미증명' -- raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md
98:oidc-client-ts 기본은 `WebStorageStateStore` (localStorage).
168:default store 종류(localStorage 여부)는 본 Sources 로 미증명
```
### L4-F18 — iframe silent SSO는 out-of-scope인데 `automaticSilentRenew` 메커니즘은 미결정
- **Severity / 유형**: Medium — ambiguous flow/default
- **Source quotes**:
- `"iframe 기반 silent SSO (deprecated, 대신 refresh token 사용)"``...vanilla-js-spa-pkce.md:58`
- `"어느 메커니즘이 default 인지 본 인용 범위 밖"``...vanilla-js-spa-pkce.md:217`
- **Claim**: iframe을 제외하고 refresh grant를 채택했다고 쓰면서 실제 library가 iframe과 refresh grant 중 무엇을 쓰는지 확인하지 않았다.
- **Assumptions**: E2E acceptance에는 reload·expiry 갱신 경로가 포함된다.
- **Failure mode**: Safari/cross-site에서 silent renew 실패, reload 후 in-memory user 복구 실패, 또는 rotation 시연이 실행되지 않는다.
- **Falsification**: pin된 library 설정과 network trace로 refresh-token grant만 사용함을 확인하면 out-of-scope 선언과 정합해진다.
- **Required action / Why**: refresh grant를 baseline으로 고정하고 필요한 scope/client 설정·reload bootstrap을 명세하거나 iframe을 조건부 경로로 다시 scope에 넣는다.
- **Alternatives**: memory-only + 명시적 재로그인.
- **Counterargument**: library가 런타임에 적절한 방식을 택한다. 브라우저 정책에 따라 결과가 달라지므로 학습 E2E에는 결정적 기대 동작이 필요하다.
- **Self-grep**:
```text
$ grep -nFo -e 'iframe 기반 silent SSO (deprecated, 대신 refresh token 사용)' -e '어느 메커니즘이 default 인지 본 인용 범위 밖' -- raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md
58:iframe 기반 silent SSO (deprecated, 대신 refresh token 사용)
217:어느 메커니즘이 default 인지 본 인용 범위 밖
```
## `raw/branch-notes/feature-security-operational-baseline.md`
### L4-F19 — Phase C2 완료 뒤 active 구현 표가 codes·CORS·redaction을 계속 미구현으로 표시
- **Severity / 유형**: High — `STALE_SUMMARY`
- **Source quotes**:
- `"production enum \`OperationalError\` 미구현"` — `...feature-security-operational-baseline.md:222`
- `"CorsSettings\` 가 \`[\"*\"]\`+\`allowCredentials=true\` 조합을 startup 거부하지 **않음**"` — `...feature-security-operational-baseline.md:269`
- `"token leak contract test"` (`planned`) — `...feature-security-operational-baseline.md:279`
- `"12 fine-grained AUTH/AUTHZ/INTERNAL codes"``...feature-security-operational-baseline.md:360`
- `"token/PII redaction"``...feature-security-operational-baseline.md:363`
- `"CORS wildcard+credentials 정적 거부"``...feature-security-operational-baseline.md:365`
- `CODE_GRANULARITY_DRIFT ... RESOLVED``...feature-security-operational-baseline.md:372`
- **Claim**: Phase C2 as-built 표는 세 항목을 locally verified로 승격했지만 앞선 Implementation Specification은 여전히 planned/미구현 상태다.
- **Assumptions**: Phase C2 표가 최신 코드 대조 결과다.
- **Failure mode**: 동일 기능을 재구현하거나 canonical 추출에서 구현 사실을 누락하고, 현재 테스트 범위를 잘못 판단한다.
- **Falsification**: Phase C2 표가 이후 revert된 상태라면 해당 표를 downgrade해야 한다.
- **Required action / Why**: active 구현 가이드와 Claims를 Phase C2 as-built에 맞추고 과거 상태는 Audit history로만 남긴다.
- **Alternatives**: 각 오래된 표에 “pre-2026-06-08 historical” 라벨을 붙인다.
- **Counterargument**: 아래 Audit에서 해소 사실을 찾을 수 있다. 상단 active specification과 검증 큐가 stale이면 소비자는 끝까지 읽지 않는 한 반대 결론을 얻는다.
- **Self-grep**:
```text
$ grep -nFo -e 'production enum `OperationalError` 미구현' -e '12 fine-grained AUTH/AUTHZ/INTERNAL codes' -e 'CODE_GRANULARITY_DRIFT' -e 'CorsSettings` 가 `["*"]`+`allowCredentials=true` 조합을 startup 거부하지 **않음**' -e 'token leak contract test' -e 'CORS wildcard+credentials 정적 거부' -e 'token/PII redaction' -- raw/branch-notes/feature-security-operational-baseline.md
144:CODE_GRANULARITY_DRIFT
214:CODE_GRANULARITY_DRIFT
222:production enum `OperationalError` 미구현
269:CorsSettings` 가 `["*"]`+`allowCredentials=true` 조합을 startup 거부하지 **않음**
279:token leak contract test
317:CODE_GRANULARITY_DRIFT
360:12 fine-grained AUTH/AUTHZ/INTERNAL codes
363:token/PII redaction
365:CORS wildcard+credentials 정적 거부
372:CODE_GRANULARITY_DRIFT
372:12 fine-grained AUTH/AUTHZ/INTERNAL codes
```
### L4-F20 — JWT decoder와 clock-skew의 현재 메커니즘이 세 가지 상태로 기록
- **Severity / 유형**: High — `CONTRADICTION`
- **Source quotes**:
- `"auto-config** (custom decoder 미작성)"``...feature-security-operational-baseline.md:197`
- `"custom \`JwtDecoder\` bean"` — `...feature-security-operational-baseline.md:364`
- `"60s 는 코드에 명시되지 않고 Spring default leeway 에 의존"``...feature-security-operational-baseline.md:374`
- **Claim**: active guide는 auto-config/default skew, Phase C2 표는 custom decoder와 explicit skew, 후속 Audit bullet은 다시 명시 설정 부재라고 말한다.
- **Assumptions**: 세 진술 모두 같은 2026-06-08 이후 코드 상태를 설명한다.
- **Failure mode**: 잘못된 bean을 수정하거나 default drift 위험·startup IdP 의존성·validator chain 테스트 범위를 오판한다.
- **Falsification**: `SupplierJwtDecoder`가 custom wrapper이지만 내부적으로 auto-config/default skew를 그대로 사용한다면 line 364의 “explicit clock skew” 표현이 잘못된 것이다.
- **Required action / Why**: 실제 code owner에서 `JwtDecoderConfig`의 validator chain을 대조한 뒤 한 상태만 남긴다.
- **Alternatives**:
1. custom decoder + explicit `JwtTimestampValidator(60s)`
2. auto-config + vendor default 60s
- **Counterargument**: custom wrapper와 auto-config를 함께 쓸 수 있다. 그래도 “custom decoder 미작성”과 “custom bean locally verified”는 동시에 참일 수 없다.
- **Self-grep**:
```text
$ grep -nFo -e 'auto-config** (custom decoder 미작성)' -e 'custom `JwtDecoder` bean' -e '60s 는 코드에 명시되지 않고 Spring default leeway 에 의존' -- raw/branch-notes/feature-security-operational-baseline.md
197:auto-config** (custom decoder 미작성)
364:custom `JwtDecoder` bean
374:60s 는 코드에 명시되지 않고 Spring default leeway 에 의존
```
### L4-F21 — public-path 검증 계약이 폐기된 reflection 방식과 구현된 env-snapshot 방식을 함께 유지
- **Severity / 유형**: High — `STALE_SUMMARY` / incompatible verification command
- **Source quotes**:
- `"SecurityFilterChain.getFilters()\` introspection"` — `...feature-security-operational-baseline.md:305`
- `"gradle task 미존재"``...feature-security-operational-baseline.md:318`
- `"public path snapshot gate"` (`locally-verified`) — `...feature-security-operational-baseline.md:366`
- **Claim**: 구현 가이드 line 243250은 env-based snapshot으로 정정됐지만 Test Contract와 Claims는 reflection 및 task 부재를 계속 전제로 한다.
- **Assumptions**: Phase C2의 `verifyPublicPathSnapshot` task가 현행이다.
- **Failure mode**: 존재하는 task를 다시 작성하거나 reflection coverage를 기대해 Java hard-coded `permitAll()`까지 탐지된다고 오판한다.
- **Falsification**: task가 이후 제거됐다면 Phase C2와 구현 가이드를 downgrade해야 한다.
- **Required action / Why**: Test Contract와 Claims를 env `SECURITY_PUBLIC_PATHS` snapshot command·승인 flag·hard-coded `permitAll()` 미검출 한계로 동기화한다.
- **Alternatives**: 별도 reflection/behavioral test를 추가해 env gate의 blind spot을 보완한다.
- **Counterargument**: line 243에 정정 설명이 있다. 하지만 실행 명령을 제공하는 §테스트 계약과 active Claims가 반대 상태여서 운영자가 잘못된 검증을 수행한다.
- **Self-grep**:
```text
$ grep -nFo -e 'SecurityFilterChain.getFilters()` introspection' -e 'gradle task 미존재' -e 'public path snapshot gate' -- raw/branch-notes/feature-security-operational-baseline.md
243:SecurityFilterChain.getFilters()` introspection
305:SecurityFilterChain.getFilters()` introspection
318:gradle task 미존재
318:SecurityFilterChain.getFilters()` introspection
366:public path snapshot gate
```
## Cross-lane owner edges
| L4 finding | Unseen owner / governing source | 필요한 대조 | 상태 |
|---|---|---|---|
| L4-F01 | `feature-keycloak-iss-claim-hostname-mismatch` D6, `feature-keycloak-docker-compose-stack` | bridge/host network 기본과 `issuer-uri`/`jwk-set-uri` 조합 | NEEDS_CROSS_LANE_AUDIT |
| L4-F02, F10, F14 | `raw/project-notes/keycloak-patterns-overview` F3/F5/§8 | 최신 AP taxonomy·owner split·single-EC2 deployment 축 | NEEDS_CROSS_LANE_AUDIT |
| L4-F04, F06 | `feature-keycloak-internal-spa-direct-no-google`, `feature-keycloak-bff-vs-spa-direct` | TMB 허용 여부와 cookie/CSRF owner | NEEDS_CROSS_LANE_AUDIT |
| L4-F08 | `feature-keycloak-realm-client-export` | 실제 client IDs와 Audience mapper target | NEEDS_CROSS_LANE_AUDIT |
| L4-F10 | `feature-keycloak-realm-client-export` D5 | realm roles 대 client roles의 발급 계약 | NEEDS_CROSS_LANE_AUDIT |
| L4-F12, F13 | `feature-keycloak-first-broker-login-flow`, `feature-keycloak-idp-mappers-claim-to-role`, `feature-keycloak-account-linking-spa-ux` | nonce 자동 처리·`email_verified`·`hd`·linking 실제 정책 | NEEDS_CROSS_LANE_AUDIT |
| L4-F14, F15 | `feature-keycloak-edge-forwardauth-no-google`, `feature-keycloak-oauth2-proxy-oidc-flow` | AP4 owner 및 302 생성/전달 주체 | NEEDS_CROSS_LANE_AUDIT |
| L4-F18 | `feature-keycloak-refresh-token-rotation`, `feature-keycloak-refresh-rotation-and-logout` | refresh grant·rotation·reload 복구 계약 | NEEDS_CROSS_LANE_AUDIT |
| L4-F19F21 | `wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md` + ca-tmpl code owner | Phase C2 as-built와 canonical/current code 정합 | NEEDS_CROSS_LANE_AUDIT |
## Claim Traceability Check
- Claim ID 연결 검사: 8개 branch-note의 Supporting Claims 구조를 모두 확인했다. exact slice 밖 raw-source Claim 본문은 재열람하지 않았으므로 외부 Claim byte 검증은 범위 밖이다. slice 내부 owner 의미 오귀속은 L4-F02·F06·F09로 보고했다.
- Decision Evidence Map: 8개 모두 검토. 최신 D-row와 본문/소비자 요약의 semantic drift는 L4-F02·F03·F05·F07F10·F12·F19F21에 반영했다.
- UNSUPPORTED_DECISION: 새 미라벨 결정으로 확정한 건은 없으며, 기존 label을 무시한 상위 요약 과장은 L4-F12에 포함했다.
## Grep verification
- 실행한 `grep -nFo` self-check: 25회
- 검증한 verbatim quote fragment V: 55
- 일치 P: 55
- 라인 정정 C: 0
- 폐기 D: 0
- 모든 command exit code: 0
- 이 lane은 파일 수정 명령을 실행하지 않았다. 공유 worktree에는 선행 변경이 존재하므로 workspace-clean 주장은 하지 않는다.
## Stats
```wiki-stats
agent: wiki-research-lane
found: 8
processed: 8
dropped: 0
```
@@ -0,0 +1,69 @@
# Keycloak Semantic Edge Risk Sample
이 lane은 valid reference occurrence 647건 전체가 아니라 lane finding 중 고위험 후보 20건을 표본 대조했다. 실제 D 또는 § edge는 9건이었고 11건은 내부 모순, D-id 없는 비교, self-reference여서 NOT_AN_EDGE로 분리했다.
## Counts
| Classification | Count |
|---|---:|
| CONSISTENT | 2 |
| STALE_SUMMARY | 1 |
| CONTRADICTION | 1 |
| RESTATED_FOREIGN_DECISION | 5 |
| NOT_AN_EDGE | 11 |
## Edge Verdicts
| Lane ID | Citing → owner | Ref | Verbatim evidence | Verdict | Action |
|---|---|---|---|---|---|
| L1-F02 | account-linking-spa-ux:119 → account-linking-sub-vs-email:152 | D3 | Citing: “orphan 거부 메커니즘은 owner 브랜치 D3(역시 needs-confirmation)에서 추적” / Owner: “Account Console self-service unlink lockout 방지는 Keycloak 엔진이 서버에서 이미 강제” | STALE_SUMMARY | Citing을 server guard 확인 및 release-tag 재확인 필요라는 한 줄로 갱신. |
| L1-F03 | account-linking-spa-ux:117 → idp-mappers-claim-to-role:120 | D2 | Citing은 email_verified=true와 Attribute Importer와 custom step을 위임하면서 복제하고, owner D2도 같은 mechanism을 결정한다. | RESTATED_FOREIGN_DECISION | Citing에는 owner D2 pointer와 linking trust gate consume 한 줄만 남김. |
| L1-F05 | account-linking-sub-vs-email:154 → google-claim-attribute-mapping:130 | D3 | Citing과 owner가 IMPORT와 FORCE의 값 및 local edit 대 Google 최신성 조건을 함께 보유한다. | RESTATED_FOREIGN_DECISION | Takeover와 Sync Mode가 직교한다는 한 줄과 owner pointer만 유지. |
| L1-F08 | docker-compose-stack:236 → single-ec2-no-google:197 | D3 | Citing은 KC_HOSTNAME, issuer-uri, host network 또는 extra_hosts를 복제하고 owner D3도 같은 bundle을 결정한다. | RESTATED_FOREIGN_DECISION | Docker note는 issuer/network wiring을 consume한다는 한 줄로 축소. |
| L1-F14 | federation-spa-zero-change:130 → internal-spa-direct-no-google:281 | D3 | 양쪽이 iss, signature, exp, aud 4종 backend validation을 같은 의미로 사용한다. | CONSISTENT | 이 edge에는 조치 없음. 정확한 audience 값은 별도 L4-F08에서 처리. |
| L2-F03 | google-claim-attribute-mapping:130 → idp-mappers-claim-to-role:119 | D1 | Citing의 role freshness 목적 FORCE 요약이 owner D1의 최신 정보 반영 FORCE와 양립한다. | CONSISTENT | 적용 범위는 owner에서 명확화하되 edge summary 수정은 없음. |
| L2-F09 | internal-spa-direct-no-google:342 → spa-token-storage-tradeoff:140 | D1 | Hub와 owner가 access memory와 refresh secure HttpOnly cookie 조합을 같은 값으로 결정한다. | RESTATED_FOREIGN_DECISION | Hub는 storage owner D1 pointer와 의존 한 줄만 유지. |
| L3-F15 | public-domain-tunneling:106,169 → single-ec2-google-federation:238 | D3 | Parent D3와 child D1이 Cloudflare 우선, ngrok 차선의 provider 순서를 각각 normative decision으로 둔다. | RESTATED_FOREIGN_DECISION | Parent를 provider order owner로 두고 child는 운영 detail만 소유. |
| L4-F03 | single-ec2-no-google:182 → vanilla-js-spa-pkce:124 | D1 | Citing: “manual fetch와 crypto.subtle 기반 PKCE 구현 우선” / Owner: “oidc-client-ts 우선 채택, manual PKCE는 비교 학습용 별도 단계” | CONTRADICTION | Owner D1을 실행 순서 정본으로 두고 citing의 manual-first를 historical learning order로 격하. |
## NOT_AN_EDGE
| Lane ID | Evidence | Reason |
|---|---|---|
| L1-F01 | authentication-authorization-contract:73,169 | 자신의 D3와 같은 문서 raw-role 설명의 불일치이며 foreign edge가 아니다. |
| L1-F07 | bff-vs-spa-direct:130,165 | 같은 문서 요약 matrix와 D5의 불일치다. |
| L1-F10 | edge-forwardauth-google-federation:145 대 edge-forwardauth-no-google:112 | cross-doc 비교는 가능하나 target D 또는 § reference가 없다. |
| L1-F11 | edge-forwardauth-no-google:167,227 | 같은 문서의 선택축과 implementation table 문제다. |
| L2-F05 | idp-brokering-google-client:81 대 google-redirect-uri-policy:167 | 값은 다르지만 consumer에 owner D6 reference가 없다. |
| L3-F01 | nginx-auth-request-integration:101,163 | 같은 문서 body-forwarding 설명의 불일치다. |
| L3-F11 | refresh-token-rotation:159 대 refresh-rotation-and-logout:124 | 두 D-row의 중복 관찰이지만 연결 문구에 target D-id가 없다. |
| L3-F12 | refresh-token-rotation:67 대 refresh-rotation-and-logout:61 | destination wikilink와 D-id가 없는 범위 위임이다. |
| L3-F13 | refresh-token-rotation:127 대 refresh-rotation-and-logout:120 | Max Reuse 정책 사이에 D 또는 § reference edge가 없다. |
| L3-F16 | single-ec2-google-federation:239,301 | 같은 parent 내부 bundle과 D-id 없는 detail 위임의 ownership 문제다. |
| L4-F08 | role-mapping:38,115 대 audience-validator:156 | 값 drift는 있으나 packet의 D4는 role-mapping 자신의 RBAC D4라 유효 foreign D4 edge가 아니다. |
## Reconciliation with adversarial review
Semantic edge 판정과 finding 존속 판정은 목적이 다르다. 예를 들어 L4-F03 edge 문구는 CONTRADICTION이지만 citing 자체가 DECISION_DRIFT를 표시하고 owner를 정본으로 인정하므로 적대 리뷰는 독립 priority에서 REJECT했다. 원문을 고칠 때는 여전히 stale active 문구와 D-row를 수거하는 것이 맞다.
## Limits
- literal reference 647건과 logical edge 298건 중 고위험 후보 20건만 선택했다.
- 20건 중 edge verdict가 가능한 것은 9건이었다.
- 전체 logical edge 의미 대조는 별도 사용자 확인 후 20건 이하 lane으로 나눠야 한다.
~~~wiki-verdict
agent: wiki-consistency-auditor
verdict: not-ready
blocking: 1
should_fix: 6
advisory: 0
~~~
~~~wiki-stats
agent: wiki-consistency-auditor
found: 20
processed: 9
dropped: 11
dropped_reason: NOT_AN_EDGE
~~~
@@ -0,0 +1,193 @@
# Per-file Findings Index
이 파일은 인덱스다. finding의 주장·인용·반론·수정안은 lane 보고서가 소유한다. 동일 finding이 여러 파일을 비교하면 관련 파일 양쪽에 같은 ID가 나타날 수 있다.
## `experiment-nplus1-highlight-feed.md`
- Finding: 없음. Keycloak 결정은 없고 sibling 주제 언급만 있다.
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md)
## `feature-authentication-authorization-contract.md`
- Findings: `L1-F01`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md)
## `feature-boundary-validation-mapping-contract.md`
- Finding: 없음. Keycloak 결정은 sample seed 설명에 한정된다.
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md)
## `feature-keycloak-account-linking-spa-ux.md`
- Findings: `L1-F02`, `L1-F03`, `L1-F04`, `L1-F13`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md)
## `feature-keycloak-account-linking-sub-vs-email.md`
- Findings: `L1-F02`, `L1-F04`, `L1-F05`, `L1-F06`, `L1-F09`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md)
## `feature-keycloak-bff-vs-spa-direct.md`
- Findings: `L1-F07`, `L2-F09`, `L4-F04`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 02](lanes/lane-02-keycloak-sorted-11-20.md), [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-docker-compose-stack.md`
- Findings: `L1-F08`, `L3-F09`, `L4-F01`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 03](lanes/lane-03-keycloak-sorted-21-30.md), [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-edge-forwardauth-google-federation.md`
- Findings: `L1-F09`, `L1-F10`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md)
## `feature-keycloak-edge-forwardauth-no-google.md`
- Findings: `L1-F10`, `L1-F11`, `L1-F12`, `L4-F14`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-federation-spa-zero-change.md`
- Findings: `L1-F13`, `L1-F14`, `L2-F08`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 02](lanes/lane-02-keycloak-sorted-11-20.md)
## `feature-keycloak-first-broker-login-flow.md`
- Findings: `L1-F02`, `L1-F04`, `L2-F01`, `L2-F02`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 02](lanes/lane-02-keycloak-sorted-11-20.md)
## `feature-keycloak-google-claim-attribute-mapping.md`
- Findings: `L1-F05`, `L2-F03`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 02](lanes/lane-02-keycloak-sorted-11-20.md)
## `feature-keycloak-google-redirect-uri-policy.md`
- Findings: `L2-F04`, `L2-F05`; deterministic `BARE_DECISION_REF` 3건.
- Detail: [lane 02](lanes/lane-02-keycloak-sorted-11-20.md)
## `feature-keycloak-header-spoofing-defense.md`
- Finding: lane-local contradiction 없음; cross-lane trusted-header owner edge가 남아 있다.
- Detail: [lane 02](lanes/lane-02-keycloak-sorted-11-20.md)
## `feature-keycloak-https-termination-caddy-nginx.md`
- Findings: `L2-F06`
- Detail: [lane 02](lanes/lane-02-keycloak-sorted-11-20.md)
## `feature-keycloak-idp-brokering-google-client.md`
- Findings: `L2-F01`, `L2-F05`; deterministic `BARE_DECISION_REF` 1건.
- Detail: [lane 02](lanes/lane-02-keycloak-sorted-11-20.md)
## `feature-keycloak-idp-mappers-claim-to-role.md`
- Findings: `L1-F03`, `L2-F02`, `L2-F03`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 02](lanes/lane-02-keycloak-sorted-11-20.md)
## `feature-keycloak-internal-spa-direct-google-federation.md`
- Findings: `L2-F01`, `L2-F02`, `L2-F07`, `L2-F08`
- Detail: [lane 02](lanes/lane-02-keycloak-sorted-11-20.md)
## `feature-keycloak-internal-spa-direct-no-google.md`
- Findings: `L2-F08`, `L2-F09`, `L4-F04`
- Detail: [lane 02](lanes/lane-02-keycloak-sorted-11-20.md), [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-iss-claim-hostname-mismatch.md`
- Findings: `L1-F08`, `L4-F01`; 이 문서 자체는 owner 충돌을 명시적으로 기록한다.
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 02](lanes/lane-02-keycloak-sorted-11-20.md), [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-nginx-auth-request-integration.md`
- Findings: `L3-F01`, `L3-F02`; deterministic bare reference의 owner 대상.
- Detail: [lane 03](lanes/lane-03-keycloak-sorted-21-30.md)
## `feature-keycloak-oauth2-proxy-oidc-flow.md`
- Findings: `L3-F03`, `L3-F04`; deterministic `BARE_DECISION_REF` 3건.
- Detail: [lane 03](lanes/lane-03-keycloak-sorted-21-30.md)
## `feature-keycloak-patterns.md`
- Findings: `L3-F05`
- Detail: [lane 03](lanes/lane-03-keycloak-sorted-21-30.md)
## `feature-keycloak-pkce-flow-stages.md`
- Findings: `L3-F06`, `L3-F07`
- Detail: [lane 03](lanes/lane-03-keycloak-sorted-21-30.md)
## `feature-keycloak-public-domain-tunneling.md`
- Findings: `L3-F08`, `L3-F15`
- Detail: [lane 03](lanes/lane-03-keycloak-sorted-21-30.md)
## `feature-keycloak-realm-client-export.md`
- Findings: `L3-F04`, `L3-F07`, `L3-F09`, `L3-F10`; deterministic `BARE_DECISION_REF` 1건.
- Detail: [lane 03](lanes/lane-03-keycloak-sorted-21-30.md)
## `feature-keycloak-refresh-rotation-and-logout.md`
- Findings: `L3-F11`, `L3-F12`, `L3-F13`
- Detail: [lane 03](lanes/lane-03-keycloak-sorted-21-30.md)
## `feature-keycloak-refresh-token-rotation.md`
- Findings: `L3-F11`, `L3-F12`, `L3-F13`
- Detail: [lane 03](lanes/lane-03-keycloak-sorted-21-30.md)
## `feature-keycloak-reverse-proxy-headers.md`
- Findings: `L3-F14`, `L3-F16`
- Detail: [lane 03](lanes/lane-03-keycloak-sorted-21-30.md)
## `feature-keycloak-single-ec2-google-federation.md`
- Findings: `L3-F08`, `L3-F15`, `L3-F16`
- Detail: [lane 03](lanes/lane-03-keycloak-sorted-21-30.md)
## `feature-keycloak-single-ec2-no-google.md`
- Findings: `L4-F01`, `L4-F02`, `L4-F03`, `L4-F10`, `L4-F14`, `L4-F16`; deterministic `BARE_DECISION_REF` 1건(`fix-plan D4`, false-positive 후보).
- Detail: [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-spa-token-storage-tradeoff.md`
- Findings: `L2-F09`, `L4-F04`, `L4-F05`, `L4-F06`
- Detail: [lane 02](lanes/lane-02-keycloak-sorted-11-20.md), [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-spring-rs-audience-validator.md`
- Findings: `L1-F14`, `L4-F02`, `L4-F06`, `L4-F07`, `L4-F08`, `L4-F09`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-spring-rs-role-mapping.md`
- Findings: `L4-F02`, `L4-F08`, `L4-F09`, `L4-F10`
- Detail: [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-three-leg-trust-chain.md`
- Findings: `L4-F11`, `L4-F12`, `L4-F13`
- Detail: [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-traefik-forwardauth-alternative.md`
- Findings: `L1-F11`, `L4-F14`, `L4-F15`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-keycloak-vanilla-js-spa-pkce.md`
- Findings: `L4-F03`, `L4-F16`, `L4-F17`, `L4-F18`
- Detail: [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
## `feature-security-operational-baseline.md`
- Findings: `L1-F01`, `L4-F19`, `L4-F20`, `L4-F21`
- Detail: [lane 01](lanes/lane-01-keycloak-sorted-01-10.md), [lane 04](lanes/lane-04-keycloak-sorted-31-38.md)
@@ -0,0 +1,57 @@
# Priority Recommendations
적대 리뷰에서 살아남은 High 10건만 우선순위에 올렸다. 각 행은 하나의 finding과 1:1로 대응한다. 원문 수정은 아직 승인되지 않았다.
| Priority | Finding | Why Now | Concrete Change | Verification | Risk / Approval |
|---:|---|---|---|---|---|
| 1 | L4-F08 | mapper가 넣는 API audience와 backend expected audience가 다르면 정상 token이 401이 되거나 다른 resource token을 허용할 수 있다. | realm client ID, API audience symbolic name, mapper target, Spring audiences 값을 한 owner D-row에서 하나로 고정하고 role-mapping의 복제 값을 pointer로 바꾼다. | realm export, 발급 token aud, 정상 및 wrong-audience 401 test를 함께 확인한다. | high / 개별 승인 |
| 2 | L1-F08 | 선택된 bridge-network 기본이 localhost discovery와 host.docker.internal reachability를 동시에 요구한다. | issuer identity와 JWKS network address를 분리하거나 host network를 기본으로 택해 한 실행 profile로 종결한다. | docker compose config, app container DNS, startup discovery와 JWKS log를 확인한다. | high / 개별 승인 |
| 3 | L4-F16 | copyable SPA code의 relative API와 token URL이 static-only nginx origin으로 향한다. | nginx에 명시적 proxy route를 추가하거나 SPA config에 backend와 Keycloak absolute base URL을 주입하고 topology owner에서 한 방식을 선택한다. | browser Network panel, nginx access log, backend와 Keycloak log로 resolved destination을 확인한다. | high / 개별 승인 |
| 4 | L3-F14 | Caddy handle_path가 Keycloak relative path를 제거해 discovery와 authorization endpoint가 404가 될 수 있다. | method B를 유지하면 handle 또는 prefix-preserving reverse_proxy로 snippet을 바꾸고, strip 방식을 택하면 Keycloak root-path 설계로 owner decision을 바꾼다. | caddy adapt와 upstream request path로 discovery, auth, token endpoint를 검사한다. | high / 개별 승인 |
| 5 | L2-F02 | silent auto-link 차단과 email_verified=false 전체 hard-reject가 같은 acceptance처럼 쓰인다. | First Broker Login owner에서 silent-link policy와 custom SPI hard-reject policy를 분리하고, SPI artifact가 없으면 consumer의 링크와 생성 거부 문장을 제거한다. | realm flow export, provider JAR, false-email 로그인 E2E로 선택 정책을 증명한다. | high / 개별 승인 |
| 6 | L4-F04 | HttpOnly refresh cookie를 기본으로 두면 server-side token custody와 CSRF 책임이 생겨 SPA Direct 경계가 바뀐다. | pure SPA memory-only와 TMB refresh-cookie variant를 별도 decision으로 나누고 기본 하나와 fallback 조건을 고정한다. | refresh endpoint와 cookie issuer 존재, CSRF negative test, reload 및 rotation E2E를 확인한다. | high / 개별 승인 |
| 7 | L1-F10 | P1B 표는 P1A와 동일이라 적지만 backend trust는 JWT signature와 header-only로 갈린다. | P1B가 P1A trust invariant를 유지할지 새 JWT-validation variant가 될지 선택하고 pattern ID, component table, security tests를 함께 갱신한다. | 두 pattern의 SecurityFilterChain, forwarded headers와 tokens, spoofing negative test를 diff한다. | high / 개별 승인 |
| 8 | L4-F14 | Traefik note가 SPA Direct P3A를 edge-auth AP4로 연결해 배포 축과 인증 architecture 축을 섞는다. | target pattern pointer를 실제 AP owner로 바꾸고 physical deployment alternative는 별도 행으로 분리한다. | project pattern matrix와 component flow에서 browser token holder와 backend verifier가 일치하는지 확인한다. | high / 개별 승인 |
| 9 | L3-F08 | random quick-tunnel URL을 static redirect hostname처럼 사용하면 restart 뒤 Google callback 등록이 stale해진다. | quick tunnel과 named tunnel plus managed hostname을 분리하고 static callback 요구 시 허용되는 provider profile을 하나로 고정한다. | tunnel restart 전후 hostname, DNS route, Google redirect E2E를 기록한다. | high / 개별 승인 |
| 10 | L1-F09 | Google sub federation key의 변경 권한이 parent와 child 양쪽 SSOT에 있다. | parent는 requirement만 두고 child D1을 mechanism owner로 삼거나, 반대로 parent owner를 유지하고 child를 pointer와 implementation detail로 축소한다. | inbound decision reference와 owner map에서 mutable key policy owner가 하나인지 확인한다. | high / 개별 승인 |
## Decision Conditions
| Finding | Goal | Assumption and invalidation | Strongest counterargument | Decision |
|---|---|---|---|---|
| L4-F08 | API token acceptance와 resource isolation을 같은 audience contract로 묶는다. | spa-client와 backend-client-id가 별도 client라는 전제다. realm export에서 동일 identifier로 의도적으로 정의됐다면 값 충돌은 사라진다. | public client ID를 API audience로 재사용할 수 있다. | identifier 재사용 여부를 owner에서 명시하고 mapper와 validator에 같은 symbol을 사용한다. |
| L1-F08 | issuer identity와 container reachability가 함께 성립하는 기본을 만든다. | backend가 bridge container라는 전제다. host process나 host network가 실제 기본이면 반론이 성립한다. | extra_hosts로 host 접근이 가능하다. | extra_hosts hostname과 issuer hostname은 자동으로 같아지지 않으므로 rendered config를 기준으로 profile을 고정한다. |
| L4-F16 | copyable SPA request가 의도한 origin으로 간다. | nginx가 static-only라는 parent 결정이 전제다. 실제 proxy route가 있으면 finding은 약해진다. | relative URL은 same-origin 배포에서 단순하다. | same-origin proxy를 문서와 config에 추가하거나 absolute runtime config를 쓴다. |
| L3-F14 | proxy path와 Keycloak relative path를 같은 routing contract로 만든다. | Caddy handle_path의 prefix strip과 method B가 동시에 선택됐다는 전제다. 별도 rewrite가 있으면 반론이 가능하다. | upstream에서 prefix를 복구할 수 있다. | 보이지 않는 rewrite에 의존하지 말고 copyable snippet에 선택을 표현한다. |
| L2-F02 | account-link policy의 실제 거부 범위를 acceptance와 맞춘다. | custom hard-reject SPI가 corpus에 없다는 전제다. 별도 artifact가 확인되면 consumer 문장을 유지할 수 있다. | Google verified email이 일반적이라 edge case가 드물다. | 빈도와 보안 계약은 별개이므로 flow export와 negative test로 범위를 고정한다. |
| L4-F04 | token custody에 맞는 architecture와 CSRF contract를 선택한다. | HttpOnly refresh cookie를 실제 기본으로 쓴다는 전제다. memory-only를 고르면 TMB concern이 사라진다. | 기존 Resource Server에 endpoint 하나만 추가할 수 있다. | endpoint 수보다 custody와 browser-triggered credential 책임을 기준으로 pattern을 분류한다. |
| L1-F10 | P1A와 P1B의 backend trust boundary를 명시한다. | 동일이라는 표가 security invariant까지 포함한다는 전제다. federation 부분만 동일하다고 범위를 좁히면 별도 variant가 가능하다. | defense-in-depth JWT validation을 P1B에 추가할 수 있다. | 그 경우 동일 표현을 제거하고 새 pattern invariant와 test를 둔다. |
| L4-F14 | pattern taxonomy와 배포 alternative를 서로 다른 축으로 유지한다. | P3A owner가 SPA Direct라는 전제다. project hub가 이미 AP4로 재분류했다면 owner 문서를 먼저 바꿔야 한다. | 같은 EC2에 oauth2-proxy를 추가할 수 있다. | 배치 가능성은 architecture ID 변경 근거가 아니므로 두 축을 분리한다. |
| L3-F08 | redirect URI가 tunnel lifecycle 동안 안정적으로 유지되게 한다. | quick tunnel URL이 random이라는 보존 source model이 전제다. target account가 persistent hostname을 제공하면 profile을 수정할 수 있다. | 특정 plan이나 managed domain은 static일 수 있다. | quick, named, managed custom hostname을 별도 조건으로 문서화한다. |
| L1-F09 | federation key 변경 권한을 한 owner에 둔다. | 두 D-row가 같은 mutable policy를 결정한다는 전제다. requirement와 mechanism concern ID가 분리되면 양립한다. | integration parent가 requirement를 반복할 수 있다. | 값과 선택 조건은 child owner에 두고 parent는 invariant pointer만 유지한다. |
## Fix Plan
| Finding | 필요한 수정 | 대상 파일:line | 위험 | 승인 필요? | 패치 범위 |
|---|---|---|---|---|---|
| L4-F08 | audience identifier와 owner 통합 | `raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md:156`, `raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md:38` | high | yes, 개별 | D-row, mapper, validator, test contract |
| L1-F08 | issuer와 network profile 한 가지 선택 | `raw/branch-notes/feature-keycloak-docker-compose-stack.md:138`, `raw/branch-notes/feature-keycloak-docker-compose-stack.md:207` | high | yes, 개별 | Compose 및 D3 관련 문단 |
| L4-F16 | SPA endpoint routing 방식 선택 | `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md:176`, `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md:190`, `raw/branch-notes/feature-keycloak-single-ec2-no-google.md:159` | high | yes, 개별 | code snippet과 topology decision |
| L3-F14 | Caddy prefix-preserving snippet 적용 | `raw/branch-notes/feature-keycloak-reverse-proxy-headers.md:125`, `raw/branch-notes/feature-keycloak-reverse-proxy-headers.md:202` | high | yes, 개별 | Caddy snippet과 D3 trace |
| L2-F02 | silent-link와 hard-reject 분리 | `raw/branch-notes/feature-keycloak-first-broker-login-flow.md:135`, `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md:120`, `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:387` | high | yes, 개별 | policy D-row와 consumer summaries |
| L4-F04 | pure SPA와 TMB variant 분리 | `raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md:140`, `raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md:165` | high | yes, 개별 | D1, architecture taxonomy, CSRF owner |
| L1-F10 | P1B backend trust variant 확정 | `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:145`, `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:112` | high | yes, 개별 | comparison table와 security invariant |
| L4-F14 | P3A pointer를 실제 pattern owner로 교체 | `raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md:169`, `raw/branch-notes/feature-keycloak-single-ec2-no-google.md:33` | medium | yes, 개별 | taxonomy pointer와 설명 |
| L3-F08 | tunnel hostname 유형 분리 | `raw/branch-notes/feature-keycloak-public-domain-tunneling.md:69`, `raw/branch-notes/feature-keycloak-public-domain-tunneling.md:77`, `raw/branch-notes/feature-keycloak-single-ec2-google-federation.md:356` | medium | yes, 개별 | provider matrix와 redirect assumptions |
| L1-F09 | sub key owner 하나 선택 | `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:195`, `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:253`, `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:150` | high | yes, 개별 | owner map와 D-row |
| L3-F04 | bare D-reference를 wikilink plus D-id로 교체 | `raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md:214`, `raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md:228`, `raw/branch-notes/feature-keycloak-realm-client-export.md:182` 및 deterministic warning 5건 | low | yes, 묶음 | 8개 reference syntax |
| L3-F11,L3-F12,L3-F13 | rotation owner, receiver scope, Max Reuse 표현을 한 계약에서 검증 | `raw/branch-notes/feature-keycloak-refresh-token-rotation.md:159`, `raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md:124` | medium | yes, 묶음 | D4/D5, scope, measured semantics |
| L4-F19,L4-F20,L4-F21 | Phase C2 as-built 기준으로 history와 active spec 분리 | `raw/branch-notes/feature-security-operational-baseline.md:243` 및 Phase C2 sections | medium | yes, as-built 확인 후 | active table, Claims, Test Contract |
## Verification order
1. Realm export와 token sample로 L4-F08을 먼저 닫는다.
2. Rendered Compose와 proxy config로 L1-F08, L3-F14, L4-F16을 함께 검증한다.
3. First Broker Login export와 negative E2E로 L2-F02를 확인한다.
4. Token storage architecture 선택 뒤 L4-F04의 CSRF와 rotation owner를 정한다.
5. 나머지 ownership 및 taxonomy cleanup을 적용하고 consistency checker를 다시 실행한다.
@@ -0,0 +1,161 @@
from __future__ import annotations
import pathlib
import re
ROOT = pathlib.Path(__file__).resolve().parents[5]
TOPIC = "2026-07-18-keycloak-branch-note-consistency"
SPEC = ROOT / "docs/superpowers/specs"
BASE = SPEC / TOPIC
MASTER = SPEC / f"{TOPIC}-report.md"
CONTROLLER = SPEC / f"{TOPIC}-controller-verification.md"
ADV_ROOT = SPEC / f"{TOPIC}-adversarial-review.md"
scope = sorted(
str(path.relative_to(ROOT))
for path in (ROOT / "raw/branch-notes").glob("*.md")
if "keycloak" in path.name.lower() or "keycloak" in path.read_text(encoding="utf-8").lower()
)
matrix = (BASE / "evidence-matrix.md").read_text(encoding="utf-8")
matrix_rows = []
malformed = []
for line in matrix.splitlines():
if re.match(r"^\| \x60raw/branch-notes/[^\x60]+\.md\x60 \|", line):
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
if len(cells) != 4:
malformed.append(line)
continue
matrix_rows.append((cells[0].strip("\x60"), cells[1], cells[2], cells[3]))
if cells[1] not in {"READ_FULL", "READ_PARTIAL", "NOT_READ", "BLOCKED"}:
malformed.append(line)
matrix_paths = [row[0] for row in matrix_rows]
missing = sorted(set(scope) - set(matrix_paths))
extra = sorted(set(matrix_paths) - set(scope))
duplicates = sorted({path for path in matrix_paths if matrix_paths.count(path) > 1})
nonexistent = sorted(path for path in matrix_paths if not (ROOT / path).is_file())
lane_files = sorted((BASE / "lanes").glob("lane-0[1-4]-*.md"))
sections = {}
schema_failures = []
for lane in lane_files:
text = lane.read_text(encoding="utf-8")
matches = list(re.finditer(r"(?m)^### (L[1-4]-F\d{2})\b[^\n]*", text))
for index, match in enumerate(matches):
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
sections.setdefault(match.group(1), []).append(text[match.start():end])
expected_ids = (
[f"L1-F{i:02d}" for i in range(1, 15)]
+ [f"L2-F{i:02d}" for i in range(1, 10)]
+ [f"L3-F{i:02d}" for i in range(1, 17)]
+ [f"L4-F{i:02d}" for i in range(1, 22)]
)
for finding_id, entries in sections.items():
section = entries[0]
checks = {
"source_file": "raw/branch-notes/" in section,
"severity": bool(re.search(r"Severity|심각도", section, re.I)),
"falsification": bool(re.search(r"Falsification|Falsified|무효|반증", section, re.I)),
"recommendation": bool(re.search(r"Required action|Action / why|synthesis recommendation|권고|조치", section, re.I)),
}
failed = [name for name, ok in checks.items() if not ok]
if failed:
schema_failures.append(f"{finding_id}:{','.join(failed)}")
per_file = (BASE / "per-file-findings.md").read_text(encoding="utf-8")
per_file_sections = len(re.findall(r"(?m)^## \x60[^\x60]+\.md\x60$", per_file))
proofs = (BASE / "sed-proofs.md").read_text(encoding="utf-8")
proof_ids = re.findall(r"(?m)^\| \x60(L[1-4]-F\d{2})\x60 \|", proofs)
adversarial = (BASE / "adversarial-review.md").read_text(encoding="utf-8")
adv_ids = re.findall(r"(?m)^\| (L[1-4]-F\d{2}) \|", adversarial)
generic = ["수동 보완책이 존재함", "일부 비핵심 경로", "치명적인 영향이 없음"]
generic_hits = sum(adversarial.count(term) for term in generic)
priority = (BASE / "priority-recommendations.md").read_text(encoding="utf-8")
priority_ids = sorted(set(re.findall(r"L[1-4]-F\d{2}", priority)))
required = [
MASTER,
CONTROLLER,
ADV_ROOT,
BASE / "evidence-matrix.md",
BASE / "per-file-findings.md",
BASE / "sed-proofs.md",
BASE / "priority-recommendations.md",
BASE / "unresolved-risk-register.md",
BASE / "adversarial-review.md",
]
missing_artifacts = [str(path.relative_to(ROOT)) for path in required if not path.is_file()]
link_files = [path for path in required if path.is_file()] + lane_files
broken_links = []
for source in link_files:
text = source.read_text(encoding="utf-8")
for match in re.finditer(r"\[[^\]]+\]\(([^)]+\.md)(?:#[^)]+)?\)", text):
target = pathlib.Path(match.group(1))
if str(target).startswith(("http:/", "https:/")):
continue
resolved = target if target.is_absolute() else (source.parent / target).resolve()
if not resolved.is_file():
broken_links.append(f"{source.relative_to(ROOT)}->{target}")
banned = [
"100%", "완벽", "완전", "극한", "극단", "정밀한", "흔들림 없이",
"절대로", "최강", "역사상 가장", "명품", "원천 차단", "보증", "폭사",
]
forbidden = []
for path in [MASTER, CONTROLLER, BASE / "priority-recommendations.md", BASE / "unresolved-risk-register.md"]:
if not path.is_file():
continue
text = re.sub(r"\x60\x60\x60.*?\x60\x60\x60", "", path.read_text(encoding="utf-8"), flags=re.S)
text = re.sub(r"\x60[^\x60]*\x60", "", text)
for line_number, line in enumerate(text.splitlines(), 1):
for term in banned:
if term in line:
forbidden.append(f"{path.relative_to(ROOT)}:{line_number}:{term}")
claim_texts = [(ROOT / path).read_text(encoding="utf-8") for path in scope]
values = {
"raw_file_count": len(scope),
"raw_line_count": sum(len(text.splitlines()) for text in claim_texts),
"matrix_rows": len(matrix_rows),
"read_full_rows": sum(row[1] == "READ_FULL" for row in matrix_rows),
"blocked_rows": sum(row[1] == "BLOCKED" for row in matrix_rows),
"missing_paths": len(missing),
"extra_paths": len(extra),
"duplicate_paths": len(duplicates),
"nonexistent_paths": len(nonexistent),
"malformed_matrix_rows": len(malformed),
"lane_finding_count": sum(len(entries) for entries in sections.values()),
"unique_finding_ids": len(sections),
"duplicate_finding_ids": sum(len(entries) != 1 for entries in sections.values()),
"missing_finding_ids": len(set(expected_ids) - set(sections)),
"finding_schema_failures": len(schema_failures),
"per_file_sections": per_file_sections,
"sed_proof_rows": len(proof_ids),
"sed_proof_unique_ids": len(set(proof_ids)),
"adversarial_rows": len(adv_ids),
"adversarial_unique_ids": len(set(adv_ids)),
"adversarial_generic_hits": generic_hits,
"priority_unique_ids": len(priority_ids),
"unresolved_priority_ids": len(set(priority_ids) - set(expected_ids)),
"broken_internal_links": len(broken_links),
"forbidden_word_hits": len(forbidden),
"missing_required_artifacts": len(missing_artifacts),
"decision_evidence_map_files": sum("## Decision Evidence Map" in text for text in claim_texts),
"claims_extracted_files": sum("## Claims Extracted" in text for text in claim_texts),
"missing_claims_extracted": sum("## Claims Extracted" not in text for text in claim_texts),
"unsupported_decision_files": sum("UNSUPPORTED_DECISION" in text for text in claim_texts),
"unsupported_decision_occurrences": sum(text.count("UNSUPPORTED_DECISION") for text in claim_texts),
"broken_claim_reference_literal_occurrences": sum(text.count("BROKEN_CLAIM_REFERENCE") for text in claim_texts),
}
for key, value in values.items():
print(f"{key}={value}")
for label, entries in [
("missing", missing), ("extra", extra), ("duplicates", duplicates),
("nonexistent", nonexistent), ("malformed", malformed),
("schema_failures", schema_failures), ("missing_artifacts", missing_artifacts),
("broken_links", broken_links), ("forbidden_hits", forbidden),
]:
if entries:
print(f"{label}=" + " || ".join(entries))
@@ -0,0 +1,11 @@
# Implementation Plan
1. `rg -il --glob '*.md' 'keycloak' raw/branch-notes`로 범위를 확정한다.
2. 38개 파일을 10/10/10/8개 lane으로 분할하고 각 파일을 정확히 한 lane에 배정한다.
3. `wiki_consistency_check.py --all``--packets`로 결정론 finding 및 참조 팩킷을 만든다.
4. lane별 READ_FULL evidence matrix와 line-verified finding을 수집한다.
5. cross-lane 후보를 `wiki-consistency-auditor`로 의미 대조한다.
6. finding이 5개 이상이면 `wiki-adversarial-reviewer`로 전 항목을 반증 시도한다.
7. controller가 파일 집합·행 수·finding ID·인용·artifact·금지어를 재계산한다.
8. 원문을 수정하지 않고 위험도·승인 단위가 포함된 fix-plan을 보고한다.
@@ -0,0 +1,7 @@
# Task
- 요청: `raw/branch-notes/`에서 Keycloak 관련 문서를 모두 읽고 설계 간 비일관성을 리뷰한다.
- 범위 산정: 파일명 또는 본문에 `keycloak`이 포함된 branch-note 38개.
- 원문 수정: 하지 않음.
- 산출물: evidence matrix, lane별 finding, semantic edge audit, adversarial review, fix-plan, controller verification, master report.
@@ -0,0 +1,80 @@
from __future__ import annotations
import pathlib
import re
import shlex
import subprocess
ROOT = pathlib.Path(__file__).resolve().parents[5]
LANES = ROOT / "docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/lanes"
FINDING = re.compile(r"^###\s+(L[1-4]-F\d{2})\b")
commands: dict[str, list[tuple[pathlib.Path, int, str]]] = {}
inventory: list[tuple[str, str]] = []
for lane in sorted(LANES.glob("lane-0[1-4]-*.md")):
current = None
for line_number, raw in enumerate(lane.read_text(encoding="utf-8").splitlines(), 1):
match = FINDING.match(raw)
if match:
current = match.group(1)
commands.setdefault(current, [])
continue
row = re.match(r"^\| (?:\x60)?(raw/branch-notes/[^\x60|]+\.md)(?:\x60)? \| (READ_FULL|NOT_READ|BLOCKED) \|", raw)
if row:
inventory.append((row.group(1), row.group(2)))
candidate = raw.strip()
if candidate.startswith("$ "):
candidate = candidate[2:]
if current and candidate.startswith(("grep ", "sed ")):
commands[current].append((lane, line_number, candidate))
expected = (
[f"L1-F{i:02d}" for i in range(1, 15)]
+ [f"L2-F{i:02d}" for i in range(1, 10)]
+ [f"L3-F{i:02d}" for i in range(1, 17)]
+ [f"L4-F{i:02d}" for i in range(1, 22)]
)
passed = 0
verified_output_lines = 0
failures = []
no_commands = []
for finding_id in expected:
if not commands.get(finding_id):
no_commands.append(finding_id)
continue
for lane, line_number, command in commands[finding_id]:
try:
argv = shlex.split(command)
except ValueError as exc:
failures.append(f"{finding_id}:{lane.name}:{line_number}:parse:{exc}")
continue
if not argv or argv[0] not in {"grep", "sed"}:
failures.append(f"{finding_id}:{lane.name}:{line_number}:unsafe")
continue
paths = []
for token in argv[1:]:
if token.endswith(".md"):
path = pathlib.Path(token)
paths.append((path if path.is_absolute() else ROOT / path).resolve())
if not paths or any(ROOT not in path.parents for path in paths):
failures.append(f"{finding_id}:{lane.name}:{line_number}:path")
continue
result = subprocess.run(argv, cwd=ROOT, text=True, capture_output=True)
if result.returncode != 0 or not result.stdout.strip():
failures.append(f"{finding_id}:{lane.name}:{line_number}:exit={result.returncode}")
continue
passed += 1
verified_output_lines += len([line for line in result.stdout.splitlines() if line.strip()])
print(f"inventory_rows={len(inventory)}")
print(f"inventory_unique={len(set(path for path, _ in inventory))}")
print(f"read_full={sum(status == 'READ_FULL' for _, status in inventory)}")
print(f"finding_ids={len(commands)}")
print(f"expected_finding_ids={len(expected)}")
print(f"commands_passed={passed}")
print(f"commands_failed={len(failures)}")
print(f"verified_output_lines={verified_output_lines}")
print(f"findings_without_commands={len(no_commands)}")
if failures:
print("failures=" + " || ".join(failures))
@@ -0,0 +1,11 @@
# Walkthrough
- 2026-07-18: Keycloak 관련 branch-note 38개, 10,649줄을 식별했다.
- 2026-07-18: 분할 감사 `10 + 10 + 10 + 8 = 38`, 중복 0, 누락 0을 확인했다.
- 2026-07-18: 결정론 검사 전체 결과 144건 중 Keycloak 범위의 `BARE_DECISION_REF` 8건을 분리했다.
- 2026-07-18: 유효 참조 647건과 logical edge 298개를 산출했다.
- 2026-07-18: lane finding 60건의 107개 인용 명령을 controller가 재실행해 실패 0, source output 158줄을 확인했다.
- 2026-07-18: semantic risk sample 20건을 대조해 actual edge 9건, `NOT_AN_EDGE` 11건으로 분류했다.
- 2026-07-18: 적대 리뷰에서 KEEP 21, DOWNGRADE 27, REJECT 12로 판정했으며 retained High는 10건이다.
- 2026-07-18: 9개 controller gate 중 claim traceability를 포함한 `finding_gate`만 FAIL로 계산해 master Verdict를 PARTIAL로 확정했다.
- 2026-07-18: branch-note 원문은 수정하지 않고 report artifacts만 생성했다.
@@ -0,0 +1,81 @@
# Quote Verification Ledger
각 lane에 기록된 `grep -nFo` 또는 `sed -n` 명령을 controller가 allowlist 방식으로 다시 실행했다. 실행 파일은 `grep|sed`만 허용했고, 대상 `.md` 경로가 workspace 내부인지 확인했으며, 종료 코드 `0`과 non-empty stdout을 모두 요구했다.
```text
$ python3 /tmp/validate_keycloak_review.py
inventory_rows=38
inventory_unique=38
read_full=38
finding_ids=60
expected_finding_ids=60
commands_passed=107
commands_failed=0
verified_output_lines=158
findings_without_commands=0
```
실제 인용 출력은 각 lane의 finding 아래 code block에 보존되어 있다. 아래 표는 finding별 controller 재실행 결과다.
| Finding ID | Commands | Controller rerun | Evidence owner |
|---|---:|---|---|
| `L1-F01` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F02` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F03` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F04` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F05` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F06` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F07` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F08` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F09` | 3 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F10` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F11` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F12` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F13` | 3 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L1-F14` | 2 | VERIFIED | [lane 01](lanes/lane-01-keycloak-sorted-01-10.md) |
| `L2-F01` | 3 | VERIFIED | [lane 02](lanes/lane-02-keycloak-sorted-11-20.md) |
| `L2-F02` | 4 | VERIFIED | [lane 02](lanes/lane-02-keycloak-sorted-11-20.md) |
| `L2-F03` | 4 | VERIFIED | [lane 02](lanes/lane-02-keycloak-sorted-11-20.md) |
| `L2-F04` | 2 | VERIFIED | [lane 02](lanes/lane-02-keycloak-sorted-11-20.md) |
| `L2-F05` | 2 | VERIFIED | [lane 02](lanes/lane-02-keycloak-sorted-11-20.md) |
| `L2-F06` | 2 | VERIFIED | [lane 02](lanes/lane-02-keycloak-sorted-11-20.md) |
| `L2-F07` | 2 | VERIFIED | [lane 02](lanes/lane-02-keycloak-sorted-11-20.md) |
| `L2-F08` | 3 | VERIFIED | [lane 02](lanes/lane-02-keycloak-sorted-11-20.md) |
| `L2-F09` | 5 | VERIFIED | [lane 02](lanes/lane-02-keycloak-sorted-11-20.md) |
| `L3-F01` | 1 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F02` | 1 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F03` | 1 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F04` | 2 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F05` | 1 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F06` | 1 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F07` | 2 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F08` | 2 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F09` | 1 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F10` | 1 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F11` | 2 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F12` | 2 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F13` | 2 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F14` | 1 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F15` | 2 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L3-F16` | 1 | VERIFIED | [lane 03](lanes/lane-03-keycloak-sorted-21-30.md) |
| `L4-F01` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F02` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F03` | 2 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F04` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F05` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F06` | 2 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F07` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F08` | 2 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F09` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F10` | 2 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F11` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F12` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F13` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F14` | 2 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F15` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F16` | 2 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F17` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F18` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F19` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F20` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
| `L4-F21` | 1 | VERIFIED | [lane 04](lanes/lane-04-keycloak-sorted-31-38.md) |
@@ -0,0 +1,25 @@
# Unresolved Risk Register
| Risk ID | 상태 | 근거 | 영향 | 다음 검증 |
|---|---|---|---|---|
| `R-01` Claim traceability migration | OPEN | 38개 중 `Decision Evidence Map`은 38개에 있으나 `## Claims Extracted`는 2개에만 있다. `UNSUPPORTED_DECISION` literal은 32개 파일, 125회다. | branch decision의 raw Claim 의미를 모든 파일에서 역검증했다고 말할 수 없다. | `/migrate-claims` 범위를 별도로 승인한 뒤 claim reference lexical/semantic audit. |
| `R-02` Reference-edge semantic breadth | OPEN | scope 안 유효 wikilink reference occurrence 647건, logical `(citing, owner, D-id)` edge 298건. 이번 Layer 2는 고위험 후보 20개만 표본 대조했다. | 저위험으로 분류된 stale summary나 foreign-decision restatement가 더 남을 수 있다. | 전체 298 logical edge 대조는 `/sync` 규칙에 따라 별도 확인 후 20개 이하 lane으로 분할한다. |
| `R-03` Runtime behavior | OPEN | branch-note와 archived source를 비교했으며 Keycloak/Google/Caddy/nginx/Traefik을 구동하지 않았다. | Max Reuse, logout family behavior, issuer/JWKS reachability, redirect/origin 같은 동작은 문서 간 정합과 실제 동작이 다를 수 있다. | pinned version compose/E2E matrix 실행 후 `locally-verified` evidence 추가. |
| `R-04` Security baseline as-built | OPEN | `feature-security-operational-baseline.md`의 현재 코드/canonical 비교 후보가 lane 밖 ca-tmpl code와 `wiki/projects/`를 요구한다. | `L4-F19`~`L4-F21`의 stale 판정 강도는 실제 코드 상태에 따라 달라진다. | 별도 code/canonical audit로 현행 artifact와 branch-note를 대조한다. |
| `R-05` Unarchived security/version claim | OPEN | `L2-F01`의 CVE/version 전제는 repo raw에 공식 advisory가 없다고 문서 자체가 표시한다. | Confirm Link 흐름의 적용 버전 조건을 확정할 수 없다. | 공식 advisory를 raw로 보존하고 배포 Keycloak version과 교차 확인한다. |
| `R-06` Deterministic false-positive candidate | OPEN | `feature-keycloak-single-ec2-no-google.md:109``fix-plan D4`를 checker가 branch owner로 오인한 후보. | 자동 묶음 수정 시 존재하지 않는 wikilink를 만들 수 있다. | checker tokenization을 확인해 false-positive면 규칙/fixture로 분리한다. |
| `R-07` Source edits pending approval | DEFERRED | 이번 요청은 리뷰이며 branch-note 원문을 수정하지 않았다. | 발견된 owner drift와 contradictory default가 계속 남아 있다. | priority/fix-plan에서 사용자가 항목별 또는 low-risk 묶음 승인을 선택한다. |
## Traceability counts
```text
scope files=38
Decision Evidence Map files=38
Claims Extracted files=2
MISSING_CLAIMS_EXTRACTED files=36
UNSUPPORTED_DECISION files=32
UNSUPPORTED_DECISION occurrences=125
BROKEN_CLAIM_REFERENCE literal labels=0
```
`BROKEN_CLAIM_REFERENCE` literal 0은 모든 raw Claim target이 검증됐다는 뜻이 아니다. lane 2의 lexical sample에서는 unique Claim ID 153개 중 152개가 resolve되고 `KC-HOST-C6` 1개가 미아카이브 후보였으며, 다른 lane은 raw source body 의미를 범위 밖으로 남겼다.
+147
View File
@@ -0,0 +1,147 @@
# 하네스 v2
이 디렉터리는 규칙의 중립 원본, 플랫폼 어댑터, 결정론 실행기와 테스트를 소유한다. `raw/``wiki/`는 지식 콘텐츠이며 하네스 구현의 원본이 아니다. 기존 플랫폼 경로는 호환 표면으로 유지하되 `harness/source/`에서만 생성한다.
## 디렉터리 책임
| 경로 | 책임 | 편집 정책 |
|---|---|---|
| `harness/source/` | workflow·agent·실행 프로필의 중립 원본 | 직접 편집 |
| `harness/adapters/` | Claude·Codex·Antigravity 표현 생성 | 생성기만 편집 |
| `harness/runtime/` | transaction·graph·MOC·proof·문체 검사 | 직접 편집 |
| `harness/tests/` | 고정 fixture와 회귀 테스트 | 직접 편집 |
| `.claude/`, `.codex/`, `.agents/` | 플랫폼 호환 산출물 | 직접 편집 금지, 생성기 사용 |
| `raw/`, `wiki/` | 현재 vault 호환 콘텐츠 | 문서 workflow로 편집 |
| `vault/` | project-first 점진 이관 경계 | `vault/README.md`의 이관 규칙 적용 |
한국어 사용자 용어의 기준은 `harness/source/terminology/ko.json`이다. frontmatter key·stable ID·status·failure code·CLI·path·정확한 기술명은 보존하고, 사용자 제목과 설명은 한국어 하나로 쓴다.
## 하네스 제어 흐름
질문: project Work Item에서 branch가 생성될 때 어떤 검사를 통과한 뒤 저장되는가?
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant Flow as 워크플로
participant Packet as 계약 패킷
participant Writer as 문서 작성기
participant Gate as 결정론 검사
participant Disk as 저장소
User->>Flow: project + WI 요청
Flow->>Packet: revision과 결정을 고정
Packet->>Writer: branch 초안 전달
Writer->>Gate: graph와 link 검사
alt 모든 검사 통과
Gate->>Disk: 원자적 반영
else 검사 실패
Gate-->>User: 실패 코드 반환
end
```
핵심 결정은 검사가 끝난 파일만 한 transaction으로 교체한다는 점이다. `harness/runtime/branch_from_project.py`가 이 순서를 구현한다.
## 문서 수명주기
질문: 외부 근거가 공개 산출물로 이어질 때 어떤 기준 문서를 거치는가?
```mermaid
sequenceDiagram
autonumber
participant Source as 근거 자료
participant Project as 프로젝트 계약
participant Branch as 브랜치 계약
participant Code as 구현 증거
participant Wiki as 기준 지식
participant Publish as 공개 문서
Source->>Project: Claim을 뒷받침
Project->>Branch: Decision을 상속
Branch->>Code: 구현·검증 조건 지정
Code-->>Branch: 증거 등급 반환
Branch->>Wiki: 검증된 내용 승격
Wiki->>Publish: 면접·블로그 파생
```
`raw/`에서 공개 문서로 바로 이동하는 경로는 없다. 공개 문서는 `reviewed` 이상의 `wiki/concepts/` 또는 `wiki/projects/`에서만 파생한다.
## 결정 소유권과 예외 승인
질문: branch가 project 결정을 다르게 적용하려면 어떤 절차가 필요한가?
```mermaid
sequenceDiagram
autonumber
participant Branch as 브랜치
participant Graph as 그래프 검사기
participant Project as 프로젝트 결정
actor Owner as 승인자
Branch->>Graph: inherited ref 제출
Graph->>Project: revision·summary 대조
alt 결정을 그대로 적용
Project-->>Graph: 일치
Graph-->>Branch: 진행 허용
else 다른 동작 필요
Branch->>Owner: 이유·영향 승인 요청
Owner-->>Graph: 승인 ID 전달
Graph-->>Branch: override 기록 후 허용
end
```
프로젝트 불변식은 Project Decision이, 단일 branch 구현 선택은 Branch-local Decision이 소유한다. 차이는 `overrides``Declared Overrides` 표 양쪽에 같은 revision과 승인 ID가 있을 때만 허용한다.
## Obsidian 연결 갱신
질문: 자식의 Parent 한 곳만 편집해도 부모 MOC가 어떻게 맞춰지는가?
```mermaid
sequenceDiagram
autonumber
actor Editor as 작성자
participant Child as 자식 frontmatter
participant Indexer as MOC 생성기
participant Parent as 부모 generated block
participant Graph as 그래프 검사기
participant Disk as 저장소
Editor->>Child: project·parent 저장
Child->>Indexer: 기준 edge 제공
Indexer->>Parent: 자식 목록 재생성
Parent->>Graph: reverse view 검사
alt edge 일치
Graph->>Disk: 함께 반영
else edge 불일치
Graph-->>Editor: MISSING_EXPECTED_EDGE
end
```
자식의 `project``parent_branch`가 정방향 기준이다. 부모의 generated block은 `harness/runtime/moc_indexer.py`가 만들며 사람이 수정하지 않는다.
## 실행 명령
```bash
python3 harness/adapters/generate.py --check
python3 harness/adapters/generate_rules.py --check
python3 harness/runtime/source_hygiene.py --check
python3 harness/runtime/branch_from_project.py --help
python3 harness/runtime/execution_profile.py --workflow branch-spec
python3 harness/runtime/fix_bare_refs.py --check
python3 harness/runtime/moc_indexer.py --check
python3 harness/runtime/layout_check.py
python3 .claude/hooks/wiki_graph_contract_check.py --all
python3 -m unittest discover -s harness/tests -p 'test_*.py' -v
```
## 다이어그램 자체 점검
- [x] 각 다이어그램은 하나의 질문만 다룬다.
- [x] participant는 4~6개이고 메시지는 5~6개다.
- [x] 라벨은 두 줄을 넘지 않는다.
- [x] 메시지 라벨은 다섯 단어 이내다.
- [x] 색과 범례를 추가하지 않았다.
- [x] 장식용 boundary가 없다.
- [x] 진입점은 actor 또는 첫 participant로 분명하다.
- [x] 세부 책임과 구현 경로는 본문 표와 설명에 분리했다.
+667
View File
@@ -0,0 +1,667 @@
#!/usr/bin/env python3
"""Generate every platform adapter from repository-neutral workflow/role sources.
The generator intentionally uses only the Python standard library. Neutral
source bodies live below ``harness/source``; platform-only execution metadata
(tool names, model selection, and sandbox mode) lives beside this adapter.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
DEFAULT_ROOT = Path(__file__).resolve().parents[2]
MANIFEST_REL = Path("harness/source/generation-manifest.json")
PLATFORM_METADATA_REL = Path("harness/adapters/platform-metadata.json")
EXECUTION_PROFILES_REL = Path("harness/source/execution-profiles.json")
MARKDOWN_MARKER = "<!-- GENERATED from {source} sha256:{digest}; DO NOT EDIT -->"
COMMENT_MARKER = "# GENERATED from {source} sha256:{digest}; DO NOT EDIT"
JSON_MARKER = "GENERATED from {source} sha256:{digest}; DO NOT EDIT"
SUPPORTED_SOURCE_KINDS = {"workflow", "agent"}
SUPPORTED_TARGET_KINDS = {
"agent-skill",
"agent-workflow",
"claude-command",
"plugin-agent-md",
"claude-agent-md",
"codex-agent-md",
"codex-agent-toml",
"antigravity-agent-json",
}
SUPPORTED_HEADING_LOCALES = {"en", "ko-KR", "mixed"}
TARGET_KIND_PREFIXES = {
"agent-skill": ".agents/skills/",
"agent-workflow": ".agents/workflows/",
"claude-command": ".claude/commands/",
"plugin-agent-md": ".agents/plugins/wiki-superpowers/agents/",
"claude-agent-md": ".claude/agents/",
"codex-agent-md": ".codex/agents/",
"codex-agent-toml": ".codex/agents/",
"antigravity-agent-json": ".agents/agents/",
}
EXECUTION_KINDS = {"orchestrated", "deterministic"}
EXECUTION_PROFILES = {"capture", "design", "audit", "publish"}
EXECUTION_RISKS = {"low", "medium", "high", "critical"}
sys.path.insert(0, str(DEFAULT_ROOT / "harness/runtime"))
from fs_transaction import replace_many # noqa: E402
class GenerationError(ValueError):
"""Raised when the neutral-source contract is internally inconsistent."""
@dataclass(frozen=True)
class Target:
kind: str
path: str
@dataclass(frozen=True)
class Source:
kind: str
identifier: str
description: str
argument_hint: str | None
heading_locale: str
metadata_path: str
body_path: str
body: str
digest: str
execution_contract: dict[str, Any] | None
targets: tuple[Target, ...]
@dataclass(frozen=True)
class Catalog:
root: Path
manifest: dict[str, Any]
platform_metadata: dict[str, Any]
sources: tuple[Source, ...]
@property
def targets(self) -> tuple[tuple[Source, Target], ...]:
return tuple((source, target) for source in self.sources for target in source.targets)
@dataclass(frozen=True)
class GenerationResult:
stale: tuple[str, ...] = ()
missing: tuple[str, ...] = ()
extra: tuple[str, ...] = ()
written: tuple[str, ...] = ()
@property
def drift(self) -> tuple[str, ...]:
return self.stale + self.missing
@property
def ok(self) -> bool:
return not (self.stale or self.missing or self.extra)
def _read_json(path: Path) -> dict[str, Any]:
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise GenerationError(f"JSON root must be an object: {path}")
return data
def _safe_rel(value: Any, *, field: str) -> str:
if not isinstance(value, str) or not value.strip():
raise GenerationError(f"{field} must be a non-empty path")
path = Path(value)
if path.is_absolute() or ".." in path.parts:
raise GenerationError(f"{field} escapes repository: {value}")
return path.as_posix()
def _non_empty_string(data: dict[str, Any], key: str, *, source: str) -> str:
value = data.get(key)
if not isinstance(value, str) or not value.strip():
raise GenerationError(f"{source}: missing non-empty {key}")
return value
def _composite_digest(metadata_raw: bytes, body_path: str, body_raw: bytes) -> str:
digest = hashlib.sha256()
digest.update(metadata_raw)
digest.update(b"\0")
digest.update(body_path.encode("utf-8"))
digest.update(b"\0")
digest.update(body_raw)
return digest.hexdigest()
def _expand_globs(root: Path, patterns: Iterable[str], excludes: Iterable[str] = ()) -> set[str]:
excluded = set(excludes)
found: set[str] = set()
for pattern in patterns:
found.update(
path.relative_to(root).as_posix()
for path in root.glob(pattern)
if path.is_file()
)
return found - excluded
def _validate_source_inventory(root: Path, manifest: dict[str, Any]) -> None:
inventory = manifest.get("source_inventory")
if not isinstance(inventory, dict):
raise GenerationError("manifest source_inventory must be an object")
patterns = inventory.get("include")
if not isinstance(patterns, list) or not all(isinstance(item, str) for item in patterns):
raise GenerationError("manifest source_inventory.include must be a string list")
declared_entries = manifest.get("sources")
if not isinstance(declared_entries, list) or not declared_entries:
raise GenerationError("manifest sources must be a non-empty list")
declared_metadata: list[str] = []
declared_bodies: list[str] = []
for entry in declared_entries:
if not isinstance(entry, dict):
raise GenerationError("manifest source entries must be objects")
declared_metadata.append(_safe_rel(entry.get("metadata"), field="source metadata"))
declared_bodies.append(_safe_rel(entry.get("body"), field="source body"))
duplicates = sorted(
path for path in set(declared_metadata) if declared_metadata.count(path) > 1
)
if duplicates:
raise GenerationError("duplicate source mapping: " + ", ".join(duplicates))
declared = set(declared_metadata) | set(declared_bodies)
actual = _expand_globs(root, patterns)
missing = sorted(declared - actual)
extra = sorted(actual - declared)
if missing:
raise GenerationError("missing source files: " + ", ".join(missing))
if extra:
raise GenerationError("extra source files: " + ", ".join(extra))
def _load_source(root: Path, entry: dict[str, Any]) -> Source:
metadata_path = _safe_rel(entry.get("metadata"), field="source metadata")
body_path = _safe_rel(entry.get("body"), field="source body")
metadata_abs = root / metadata_path
body_abs = root / body_path
metadata_raw = metadata_abs.read_bytes()
body_raw = body_abs.read_bytes()
data = json.loads(metadata_raw.decode("utf-8"))
if not isinstance(data, dict):
raise GenerationError(f"{metadata_path}: JSON root must be an object")
kind = data.get("source_kind")
if kind not in SUPPORTED_SOURCE_KINDS:
raise GenerationError(f"{metadata_path}: unsupported source_kind: {kind}")
expected_schema = 2 if kind == "workflow" else 1
if data.get("schema_version") != expected_schema:
raise GenerationError(f"{metadata_path}: expected schema_version {expected_schema}")
identifier = _non_empty_string(data, "id", source=metadata_path)
description = _non_empty_string(data, "description", source=metadata_path)
heading_locale = data.get("heading_locale")
if heading_locale not in SUPPORTED_HEADING_LOCALES:
raise GenerationError(
f"{metadata_path}: heading_locale must be one of {sorted(SUPPORTED_HEADING_LOCALES)}"
)
if data.get("body_path") != body_path:
raise GenerationError(
f"{metadata_path}: body_path {data.get('body_path')!r} does not match manifest {body_path!r}"
)
argument_hint: str | None = None
execution_contract: dict[str, Any] | None = None
if kind == "workflow":
argument_hint = _non_empty_string(data, "argument_hint", source=metadata_path)
execution_contract = _validate_execution_contract(root, data, metadata_path)
elif "execution_contract" in data:
raise GenerationError(f"{metadata_path}: agents cannot declare execution_contract")
raw_targets = data.get("targets")
if not isinstance(raw_targets, list) or not raw_targets:
raise GenerationError(f"{metadata_path}: targets must be a non-empty list")
targets: list[Target] = []
for raw_target in raw_targets:
if not isinstance(raw_target, dict):
raise GenerationError(f"{metadata_path}: targets must be objects")
target_kind = raw_target.get("kind")
if target_kind not in SUPPORTED_TARGET_KINDS:
raise GenerationError(f"{metadata_path}: unsupported target kind: {target_kind}")
target_path = _safe_rel(raw_target.get("path"), field="target path")
if not target_path.startswith(TARGET_KIND_PREFIXES[target_kind]):
raise GenerationError(
f"{metadata_path}: {target_path} does not match target kind {target_kind}"
)
if kind == "workflow" and target_kind not in {
"agent-skill",
"agent-workflow",
"claude-command",
}:
raise GenerationError(f"{metadata_path}: workflow cannot generate {target_kind}")
if kind == "agent" and target_kind in {
"agent-skill",
"agent-workflow",
"claude-command",
}:
raise GenerationError(f"{metadata_path}: agent cannot generate {target_kind}")
targets.append(Target(target_kind, target_path))
body = body_raw.decode("utf-8")
if not body.strip():
raise GenerationError(f"{body_path}: source body must not be empty")
if not body.endswith("\n"):
raise GenerationError(f"{body_path}: source body must end with a newline")
digest = _composite_digest(metadata_raw, body_path, body_raw)
return Source(
kind=kind,
identifier=identifier,
description=description,
argument_hint=argument_hint,
heading_locale=heading_locale,
metadata_path=metadata_path,
body_path=body_path,
body=body,
digest=digest,
execution_contract=execution_contract,
targets=tuple(targets),
)
def _validate_execution_contract(
root: Path,
data: dict[str, Any],
metadata_path: str,
) -> dict[str, Any]:
if "profile" in data or "default_risk" in data:
raise GenerationError(
f"{metadata_path}: profile/default_risk must be nested under execution_contract"
)
contract = data.get("execution_contract")
if not isinstance(contract, dict):
raise GenerationError(f"{metadata_path}: execution_contract must be an object")
allowed = {
"kind",
"profile",
"default_risk",
"entrypoint",
"dry_run_first",
"result_schema",
"design_bearing",
}
extra = sorted(set(contract) - allowed)
if extra:
raise GenerationError(
f"{metadata_path}: unsupported execution_contract fields: {', '.join(extra)}"
)
kind = contract.get("kind")
profile = contract.get("profile")
default_risk = contract.get("default_risk")
if kind not in EXECUTION_KINDS:
raise GenerationError(f"{metadata_path}: unsupported execution kind: {kind}")
if profile not in EXECUTION_PROFILES:
raise GenerationError(f"{metadata_path}: unsupported execution profile: {profile}")
if default_risk not in EXECUTION_RISKS:
raise GenerationError(f"{metadata_path}: unsupported default_risk: {default_risk}")
if not isinstance(contract.get("design_bearing"), bool):
raise GenerationError(f"{metadata_path}: design_bearing must be boolean")
deterministic_fields = {"entrypoint", "dry_run_first", "result_schema"}
present_deterministic = deterministic_fields.intersection(contract)
if kind == "deterministic":
missing = sorted(deterministic_fields - present_deterministic)
if missing:
raise GenerationError(
f"{metadata_path}: deterministic contract missing: {', '.join(missing)}"
)
entrypoint = _safe_rel(contract.get("entrypoint"), field="execution entrypoint")
if not entrypoint.endswith(".py") or not (root / entrypoint).is_file():
raise GenerationError(
f"{metadata_path}: deterministic entrypoint is not executable source: {entrypoint}"
)
if contract.get("dry_run_first") is not True:
raise GenerationError(
f"{metadata_path}: deterministic workflow must set dry_run_first=true"
)
result_schema = contract.get("result_schema")
if not isinstance(result_schema, str) or not result_schema.strip():
raise GenerationError(f"{metadata_path}: result_schema must be non-empty")
elif present_deterministic:
raise GenerationError(
f"{metadata_path}: orchestrated workflow cannot declare deterministic fields"
)
return dict(contract)
def load_catalog(root: Path = DEFAULT_ROOT) -> Catalog:
root = root.resolve()
manifest_path = root / MANIFEST_REL
platform_path = root / PLATFORM_METADATA_REL
manifest = _read_json(manifest_path)
if manifest.get("schema_version") != 1:
raise GenerationError("generation manifest has unsupported schema_version")
_validate_source_inventory(root, manifest)
execution_profiles = _read_json(root / EXECUTION_PROFILES_REL)
if execution_profiles.get("schema_version") != "execution-profiles/v1":
raise GenerationError("execution profile source has unsupported schema_version")
if set(execution_profiles.get("profiles", {})) != EXECUTION_PROFILES:
raise GenerationError("generator workflow profiles drift from execution profile SSOT")
if set(execution_profiles.get("risk_levels", [])) != EXECUTION_RISKS:
raise GenerationError("generator workflow risks drift from execution profile SSOT")
platform_metadata = _read_json(platform_path)
if platform_metadata.get("schema_version") != 1:
raise GenerationError("platform metadata has unsupported schema_version")
sources = tuple(_load_source(root, entry) for entry in manifest["sources"])
identifiers: set[tuple[str, str]] = set()
mapped_targets: dict[str, str] = {}
for source in sources:
source_key = (source.kind, source.identifier)
if source_key in identifiers:
raise GenerationError(f"duplicate logical source: {source.kind}:{source.identifier}")
identifiers.add(source_key)
for target in source.targets:
previous = mapped_targets.get(target.path)
if previous is not None:
raise GenerationError(
f"duplicate target mapping: {target.path} ({previous}, {source.metadata_path})"
)
mapped_targets[target.path] = source.metadata_path
expected_count = manifest.get("expected_target_count")
if not isinstance(expected_count, int) or expected_count <= 0:
raise GenerationError("manifest expected_target_count must be a positive integer")
if len(mapped_targets) != expected_count:
raise GenerationError(
f"mapped target count {len(mapped_targets)} != expected_target_count {expected_count}"
)
configured_agents = platform_metadata.get("agents")
if not isinstance(configured_agents, dict):
raise GenerationError("platform metadata agents must be an object")
agent_ids = {source.identifier for source in sources if source.kind == "agent"}
extra_agent_metadata = sorted(set(configured_agents) - agent_ids)
if extra_agent_metadata:
raise GenerationError("extra platform agent metadata: " + ", ".join(extra_agent_metadata))
for source in sources:
if source.kind != "agent":
continue
required_platforms = {
"claude" if target.kind == "claude-agent-md" else
"codex" if target.kind.startswith("codex-agent-") else
"antigravity" if target.kind == "antigravity-agent-json" else
"plugin"
for target in source.targets
}
metadata = configured_agents.get(source.identifier)
if not isinstance(metadata, dict):
raise GenerationError(f"missing platform metadata for agent {source.identifier}")
missing_platforms = sorted(required_platforms - set(metadata))
if missing_platforms:
raise GenerationError(
f"missing platform metadata for {source.identifier}: {', '.join(missing_platforms)}"
)
return Catalog(root, manifest, platform_metadata, sources)
def _yaml_string(value: str) -> str:
return json.dumps(value, ensure_ascii=False)
def _toml_string(value: str) -> str:
return json.dumps(value, ensure_ascii=False)
def _markdown_marker(source: Source) -> str:
return MARKDOWN_MARKER.format(source=source.metadata_path, digest=source.digest)
def _workflow_policy_block(workflow: str) -> str:
return f"""## 실행 정책 해석
1. 작업을 시작하기 전에 다음 baseline resolver를 실행하고 `status: PLANNED`, `phase: baseline` 확인한다.
```bash
python3 harness/runtime/workflow_dispatch.py {workflow} --root . --phase baseline
```
2. `mandatory_gates`에서 `required` gate는 risk와 무관하게 생략하지 않는다. review dispatch와 응답 형식은 resolver의 `dispatch`, `review_intensity`, `output_contract` 따른다.
3. finding과 claim 집계가 끝나면 최종 응답 전에 같은 workflow를 `--phase final --finding-count <N>`으로 다시 resolve한다. claim이 있으면 `--claims-present`, 공개 claim이면 `--public-claims-present` argv 항목으로 추가한다.
4. final resolver의 필수 gate·review·output contract를 반영하기 전에는 완료를 선언하지 않는다."""
def _render_workflow(source: Source, target: Target) -> str:
assert source.argument_hint is not None
assert source.execution_contract is not None
contract = json.dumps(source.execution_contract, ensure_ascii=False, separators=(",", ":"))
if target.kind == "claude-command":
frontmatter = (
"---\n"
f"description: {_yaml_string(source.description)}\n"
f"argument-hint: {source.argument_hint}\n"
f"execution_contract: {contract}\n"
"---"
)
arguments = "$ARGUMENTS"
prefix = ""
elif target.kind == "agent-skill":
frontmatter = (
"---\n"
f"name: {source.identifier}\n"
f"description: {_yaml_string(f'{source.description} (입력: {source.argument_hint})')}\n"
f"execution_contract: {contract}\n"
"---"
)
arguments = source.argument_hint
prefix = ""
elif target.kind == "agent-workflow":
frontmatter = (
"---\n"
f"description: {_yaml_string(source.description)}\n"
f"execution_contract: {contract}\n"
"---"
)
arguments = source.argument_hint
prefix = (
f"사용자가 `/{source.identifier} {source.argument_hint}` 를 입력하면 아래 절차를 수행한다.\n\n"
)
else: # pragma: no cover - source validation prevents this path
raise GenerationError(f"unsupported workflow target kind: {target.kind}")
body = source.body.replace("{{arguments}}", arguments)
policy = _workflow_policy_block(source.identifier)
return f"{frontmatter}\n{_markdown_marker(source)}\n\n{prefix}{policy}\n\n{body}".rstrip() + "\n"
def _agent_platform(catalog: Catalog, source: Source, platform: str) -> dict[str, Any]:
metadata = catalog.platform_metadata["agents"][source.identifier].get(platform)
if not isinstance(metadata, dict):
raise GenerationError(f"missing {platform} metadata for {source.identifier}")
return metadata
def _render_agent(catalog: Catalog, source: Source, target: Target) -> str:
marker = _markdown_marker(source)
if target.kind in {"plugin-agent-md", "codex-agent-md"}:
frontmatter = (
"---\n"
f"name: {source.identifier}\n"
f"description: {_yaml_string(source.description)}\n"
"---"
)
return f"{frontmatter}\n{marker}\n\n{source.body}".rstrip() + "\n"
if target.kind == "claude-agent-md":
metadata = _agent_platform(catalog, source, "claude")
tools = metadata.get("tools")
model = metadata.get("model")
if not isinstance(tools, str) or not tools or not isinstance(model, str) or not model:
raise GenerationError(f"invalid Claude metadata for {source.identifier}")
frontmatter = (
"---\n"
f"name: {source.identifier}\n"
f"description: {_yaml_string(source.description)}\n"
f"tools: {tools}\n"
f"model: {model}\n"
"---"
)
return f"{frontmatter}\n{marker}\n\n{source.body}".rstrip() + "\n"
if target.kind == "codex-agent-toml":
metadata = _agent_platform(catalog, source, "codex")
sandbox_mode = metadata.get("sandbox_mode")
if not isinstance(sandbox_mode, str) or not sandbox_mode:
raise GenerationError(f"invalid Codex metadata for {source.identifier}")
if "'''" in source.body:
raise GenerationError(f"{source.body_path}: cannot embed triple single quote in TOML")
comment = COMMENT_MARKER.format(source=source.metadata_path, digest=source.digest)
return (
f"{comment}\n"
f"name = {_toml_string(source.identifier)}\n"
f"description = {_toml_string(source.description)}\n"
f"sandbox_mode = {_toml_string(sandbox_mode)}\n"
"developer_instructions = '''\n"
f"{source.body.rstrip()}\n"
"'''\n"
)
if target.kind == "antigravity-agent-json":
metadata = _agent_platform(catalog, source, "antigravity")
hidden = metadata.get("hidden")
tool_names = metadata.get("tool_names")
include_sections = metadata.get("include_sections")
if not isinstance(hidden, bool):
raise GenerationError(f"invalid Antigravity hidden flag for {source.identifier}")
if not isinstance(tool_names, list) or not all(isinstance(item, str) for item in tool_names):
raise GenerationError(f"invalid Antigravity tool_names for {source.identifier}")
if not isinstance(include_sections, list) or not all(
isinstance(item, str) for item in include_sections
):
raise GenerationError(f"invalid Antigravity include_sections for {source.identifier}")
payload = {
"_generated": JSON_MARKER.format(
source=source.metadata_path,
digest=source.digest,
),
"name": source.identifier,
"description": source.description,
"hidden": hidden,
"config": {
"customAgent": {
"systemPromptSections": [
{"title": "Agent System Instructions", "content": source.body.rstrip("\n")}
],
"toolNames": tool_names,
"systemPromptConfig": {"includeSections": include_sections},
}
},
}
return json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
raise GenerationError(f"unsupported agent target kind: {target.kind}")
def render_target(catalog: Catalog, source: Source, target: Target) -> str:
if source.kind == "workflow":
return _render_workflow(source, target)
return _render_agent(catalog, source, target)
def _target_inventory(catalog: Catalog) -> set[str]:
inventory = catalog.manifest.get("target_inventory")
if not isinstance(inventory, dict):
raise GenerationError("manifest target_inventory must be an object")
include = inventory.get("include")
exclude = inventory.get("exclude", [])
if not isinstance(include, list) or not all(isinstance(item, str) for item in include):
raise GenerationError("target_inventory.include must be a string list")
if not isinstance(exclude, list) or not all(isinstance(item, str) for item in exclude):
raise GenerationError("target_inventory.exclude must be a string list")
return _expand_globs(catalog.root, include, exclude)
def _render_inventory(catalog: Catalog) -> dict[Path, bytes]:
rendered: dict[Path, bytes] = {}
for source, target in catalog.targets:
path = (catalog.root / target.path).resolve()
if path in rendered:
raise GenerationError(f"duplicate rendered target: {target.path}")
rendered[path] = render_target(catalog, source, target).encode("utf-8")
if len(rendered) != catalog.manifest["expected_target_count"]:
raise GenerationError("rendered target inventory count drift")
return rendered
def generate(root: Path = DEFAULT_ROOT, *, check: bool = True) -> GenerationResult:
catalog = load_catalog(root)
declared = {target.path for _, target in catalog.targets}
rendered = _render_inventory(catalog)
rendered_rel = {path.relative_to(catalog.root).as_posix() for path in rendered}
if rendered_rel != declared:
raise GenerationError("rendered target inventory differs from declared inventory")
actual = _target_inventory(catalog)
extra = tuple(sorted(actual - declared))
missing = tuple(sorted(declared - actual))
if extra:
return GenerationResult(missing=missing, extra=extra)
stale: list[str] = []
for path, expected in rendered.items():
relative = path.relative_to(catalog.root).as_posix()
actual_bytes = path.read_bytes() if path.exists() else None
if actual_bytes != expected and path.exists():
stale.append(relative)
if check:
return GenerationResult(
stale=tuple(sorted(stale)),
missing=missing,
extra=extra,
)
if stale or missing:
# One transaction owns the complete generated inventory. Passing all
# rendered bytes (not only drifted targets) prevents mixed revisions if
# the declaration or render order changes during future maintenance.
replace_many(rendered)
return GenerationResult(written=tuple(sorted(declared)))
return GenerationResult()
def _print_result(result: GenerationResult, *, check: bool) -> None:
if result.stale:
print("stale generated targets: " + ", ".join(result.stale), file=sys.stderr)
if result.missing:
print("missing generated targets: " + ", ".join(result.missing), file=sys.stderr)
if result.extra:
print("extra unmapped targets: " + ", ".join(result.extra), file=sys.stderr)
if not check:
for path in result.written:
print(f"generated {path}")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--check", action="store_true", help="fail on mapping or content drift")
mode.add_argument("--write", action="store_true", help="write every missing or stale target")
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT, help=argparse.SUPPRESS)
args = parser.parse_args(argv)
try:
result = generate(args.root, check=args.check)
except (OSError, GenerationError, json.JSONDecodeError, UnicodeDecodeError) as exc:
print(f"adapter generation failed: {exc}", file=sys.stderr)
return 2
_print_result(result, check=args.check)
if result.extra or (args.check and not result.ok):
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Generate platform rule slices from repository-neutral root rule SSOT files."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import sys
from typing import Any
DEFAULT_ROOT = Path(__file__).resolve().parents[2]
CONFIG = Path("harness/source/rule-adapters.json")
sys.path.insert(0, str(DEFAULT_ROOT / "harness/runtime"))
from fs_transaction import replace_many # noqa: E402
class RuleGenerationError(ValueError):
pass
def _safe_path(value: Any) -> Path:
if not isinstance(value, str) or not value:
raise RuleGenerationError("path must be a non-empty string")
path = Path(value)
if path.is_absolute() or ".." in path.parts:
raise RuleGenerationError(f"path escapes repository: {value}")
return path
def _slice(text: str, start: str, end: str | None, source: Path) -> str:
lines = text.splitlines(keepends=True)
try:
start_index = next(index for index, line in enumerate(lines) if line.rstrip("\n") == start)
except StopIteration as exc:
raise RuleGenerationError(f"{source}: missing start heading {start!r}") from exc
end_index = len(lines)
if end is not None:
try:
end_index = next(
index for index, line in enumerate(lines[start_index + 1 :], start_index + 1)
if line.rstrip("\n") == end
)
except StopIteration as exc:
raise RuleGenerationError(f"{source}: missing end heading {end!r}") from exc
return "".join(lines[start_index:end_index]).rstrip() + "\n"
def render(root: Path) -> dict[Path, str]:
config_path = root / CONFIG
config = json.loads(config_path.read_text(encoding="utf-8"))
if config.get("schema_version") != "rule-adapters/v1":
raise RuleGenerationError("expected rule-adapters/v1")
groups = config.get("groups")
if not isinstance(groups, list) or not groups:
raise RuleGenerationError("groups must be a non-empty list")
rendered: dict[Path, str] = {}
for group in groups:
if not isinstance(group, dict):
raise RuleGenerationError("group must be an object")
source_rel = _safe_path(group.get("source"))
target_dir = _safe_path(group.get("target_dir"))
source = root / source_rel
source_text = source.read_text(encoding="utf-8")
digest = hashlib.sha256(source_text.encode("utf-8")).hexdigest()
slices = group.get("slices")
if not isinstance(slices, list) or not slices:
raise RuleGenerationError(f"{source_rel}: slices must be non-empty")
links: list[str] = []
for item in slices:
target_name = _safe_path(item.get("target"))
if len(target_name.parts) != 1:
raise RuleGenerationError("rule slice target must be a filename")
start = item.get("start")
end = item.get("end")
if not isinstance(start, str) or (end is not None and not isinstance(end, str)):
raise RuleGenerationError("slice start/end must be headings")
target = target_dir / target_name
if target in rendered:
raise RuleGenerationError(f"duplicate rule target: {target}")
marker = f"<!-- GENERATED from {source_rel.as_posix()} sha256:{digest}; DO NOT EDIT -->"
rendered[target] = (
f"{marker}\n\n"
f"Root SSOT: [`{source_rel.as_posix()}`](../../../../../{source_rel.as_posix()})\n\n"
f"{_slice(source_text, start, end, source_rel)}"
)
links.append(f"- [`{target_name.as_posix()}`]({target_name.as_posix()}): `{start}`")
index_name = _safe_path(group.get("index"))
index = target_dir / index_name
marker = f"<!-- GENERATED from {source_rel.as_posix()} sha256:{digest}; DO NOT EDIT -->"
rendered[index] = (
f"{marker}\n\n"
f"# 생성된 rule index\n\n"
f"Root SSOT: [`{source_rel.as_posix()}`](../../../../../{source_rel.as_posix()})\n\n"
"아래 파일은 root SSOT의 heading 구간에서 생성된다. 직접 편집하지 않는다.\n\n"
+ "\n".join(links)
+ "\n"
)
return rendered
def generate(root: Path, check: bool) -> tuple[list[str], list[str]]:
expected = render(root)
stale = [path.as_posix() for path, text in expected.items() if not (root / path).is_file() or (root / path).read_text(encoding="utf-8") != text]
written: list[str] = []
if not check and stale:
changes = {root / path: expected[path].encode("utf-8") for path in expected if path.as_posix() in stale}
replace_many(changes)
written = sorted(stale)
return sorted(stale), written
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--check", action="store_true")
mode.add_argument("--write", action="store_true")
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
args = parser.parse_args(argv)
try:
root = args.root.resolve(strict=True)
stale, written = generate(root, args.check)
result = {
"schema_version": "rule-adapter-result/v1",
"status": "DRIFT" if args.check and stale else "UPDATED" if written else "CURRENT",
"stale": stale,
"written": written,
"target_count": len(render(root)),
}
json.dump(result, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
sys.stdout.write("\n")
return 1 if args.check and stale else 0
except (OSError, UnicodeError, json.JSONDecodeError, RuleGenerationError) as exc:
json.dump({"schema_version": "rule-adapter-result/v1", "status": "FAIL", "error": str(exc)}, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
return 2
if __name__ == "__main__":
raise SystemExit(main())
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the repository-wide neutral adapter generator."""
from __future__ import annotations
import sys
from pathlib import Path
ADAPTER_DIR = Path(__file__).resolve().parent
if str(ADAPTER_DIR) not in sys.path:
sys.path.insert(0, str(ADAPTER_DIR))
from generate import ( # noqa: E402,F401
DEFAULT_ROOT,
MANIFEST_REL,
MARKDOWN_MARKER,
GenerationError,
GenerationResult,
load_catalog,
main,
render_target,
)
from generate import generate as _generate # noqa: E402
# Kept for callers of the original one-workflow module.
SOURCE_REL = Path("harness/source/workflows/branch-from-project.json")
MARKER = MARKDOWN_MARKER
def generate(root: Path, check: bool = False) -> list[str]:
"""Return changed paths using the legacy list-shaped API."""
result = _generate(root, check=check)
return list(result.drift if check else result.written)
if __name__ == "__main__":
raise SystemExit(main())
+355
View File
@@ -0,0 +1,355 @@
{
"schema_version": 1,
"agents": {
"branch-depth-auditor": {
"claude": {
"tools": "Read, Grep, Glob",
"model": "opus"
},
"plugin": {},
"codex": {
"sandbox_mode": "read-only"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
},
"coverage-auditor": {
"claude": {
"tools": "Read, Grep, Glob, Bash",
"model": "sonnet"
},
"plugin": {},
"codex": {
"sandbox_mode": "read-only"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir",
"run_command"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
},
"extraction-broker": {
"claude": {
"tools": "Read, Bash, Grep, Glob",
"model": "haiku"
}
},
"project-readiness-auditor": {
"claude": {
"tools": "Read, Grep, Glob",
"model": "opus"
}
},
"wiki-adversarial-reviewer": {
"claude": {
"tools": "Read, Grep, Glob, Bash",
"model": "opus"
},
"plugin": {},
"codex": {
"sandbox_mode": "read-only"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir",
"run_command"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
},
"wiki-consistency-auditor": {
"claude": {
"tools": "Read, Grep, Glob, Bash",
"model": "opus"
},
"plugin": {},
"codex": {
"sandbox_mode": "read-only"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir",
"run_command"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
},
"wiki-semantic-coherence-auditor": {
"claude": {
"tools": "Read, Grep, Glob, Bash",
"model": "opus"
},
"plugin": {},
"codex": {
"sandbox_mode": "read-only"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir",
"run_command"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
},
"wiki-decision-researcher": {
"claude": {
"tools": "Read, Bash, Grep, Glob, WebSearch, WebFetch",
"model": "sonnet"
},
"plugin": {},
"codex": {
"sandbox_mode": "read-only"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir",
"run_command",
"read_url_content",
"search_web"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
},
"wiki-diagram-reviewer": {
"claude": {
"tools": "Read, Grep, Glob, Bash",
"model": "sonnet"
},
"plugin": {},
"codex": {
"sandbox_mode": "read-only"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir",
"run_command"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
},
"wiki-doc-author": {
"claude": {
"tools": "Read, Edit, Write, Bash, Grep, Glob",
"model": "sonnet"
},
"plugin": {},
"codex": {
"sandbox_mode": "workspace-write"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir",
"write_to_file",
"replace_file_content",
"multi_replace_file_content",
"run_command"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
},
"wiki-link-verifier": {
"claude": {
"tools": "Read, Grep, Glob, Bash",
"model": "haiku"
},
"plugin": {},
"codex": {
"sandbox_mode": "read-only"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir",
"run_command"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
},
"wiki-research-lane": {
"claude": {
"tools": "Read, Grep, Glob, Bash",
"model": "sonnet"
},
"plugin": {},
"codex": {
"sandbox_mode": "read-only"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir",
"run_command"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
},
"wiki-source-summarizer": {
"claude": {
"tools": "Read, Edit, Write, Bash, Grep, Glob, WebFetch",
"model": "sonnet"
},
"plugin": {},
"codex": {
"sandbox_mode": "workspace-write"
},
"antigravity": {
"hidden": true,
"tool_names": [
"send_message",
"view_file",
"find_by_name",
"grep_search",
"list_dir",
"write_to_file",
"replace_file_content",
"multi_replace_file_content",
"run_command",
"read_url_content"
],
"include_sections": [
"user_information",
"mcp_servers",
"skills",
"subagent_reminder",
"messaging",
"artifacts",
"user_rules"
]
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More