Files
llm-wiki/docs/superpowers/plans/2026-06-06-spec-c-funnel-stats-no-silent-truncation.md
T

15 KiB

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.pyparse_stats_block, validate_stats_block, STATS_FENCE_RE.
  • Modify .claude/hooks/test_wiki_rules.pyTestStatsBlock.
  • Modify .claude/hooks/wiki_claim_gate.pysubagent_stop_gate validates wiki-stats.
  • Modify .claude/hooks/test_wiki_claim_gate.pyTestSubagentStopStats.
  • 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__:

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):

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):

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()):

    # 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:

## 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:

## 출력: 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:

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:

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):

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:

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.