169 lines
9.0 KiB
Markdown
169 lines
9.0 KiB
Markdown
---
|
|
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 에서만 가능).
|