--- 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 에서만 가능).