<!-- task=m-stream-evidence-gate-core/10+09_policy_hook plan=2 tag=REVIEW_API -->
# Stream Evidence Gate Core — Multi-requirement Policy Hook 리뷰 후속 계획

## 이 파일을 읽는 구현 에이전트에게

- 아래 체크리스트와 검증 명령을 순서대로 실행하고 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션에 실제 변경·출력·차이를 기록한다.
- 구현 완료 뒤 active 파일을 그대로 두고 review-ready로 보고한다. 판정, archive, `complete.log`, 다음 상태 분류는 code-review skill 소유다.
- 차단되면 정확한 blocker, 시도한 명령/출력, 재개 조건만 구현 소유 evidence 필드에 기록한다. 사용자에게 묻거나 user-input 도구·control-plane stop 파일을 만들거나 직접 다음 상태를 분류하지 않는다.
- 이 partial task는 S07 생산 기반만 고친다. `policy-hook` Roadmap Task 완료는 후속 `11+09,10_policy_hook_contract`가 S06/S07 consumer closure를 통과할 때만 요청한다.

## 배경

이전 구현은 filter schedule을 보존했지만 applicability를 current event와 channel 전역 rune 수로 계산해 terminal, 혼합 event kind, blocking/observe 중첩에서 잘못된 epoch를 만든다. plan/resolved/applicability 집합도 정확히 검증하지 않고 raw boolean 호환 API를 남겨 registry snapshot이 정책 source of truth가 되지 못했다. 이 후속은 single buffer/token/commit 구조를 유지하면서 세 Required 결함과 같은 storage envelope 경로를 닫는다.

## Archive Evidence Snapshot

- `agent-task/m-stream-evidence-gate-core/10+09_policy_hook/plan_local_G05_0.log`, `agent-task/m-stream-evidence-gate-core/10+09_policy_hook/code_review_cloud_G06_0.log`: 구현 증거와 판정이 없는 최초 쌍이며 strongest-wins 전제를 남겨 대체됐다.
- `agent-task/m-stream-evidence-gate-core/10+09_policy_hook/plan_local_G08_1.log`, `agent-task/m-stream-evidence-gate-core/10+09_policy_hook/code_review_cloud_G08_1.log`: `FAIL`(Required 3, Suggested 0, Nit 0). filter별 pending/applicability·storage envelope, exact plan/resolved/applicability 집합, caller boolean 정책 seam을 수정해야 한다. 영향 파일은 `evidence_tail.go`, `filter_registry.go`와 해당 package tests다.
- fresh 검증에서 집중/전체/race, `make proto`, Edge/Node transport, `git diff --check`는 통과했지만 focused reproducer는 terminal pending `present=false`, blocking/observe overlap `signal=none`, cross-kind rolling 조기 threshold, resolved subset 및 missing applicability 수용을 재현했다.
- 선행 의존성은 `agent-task/archive/2026/07/m-stream-evidence-gate-core/09_decision_arbiter/complete.log` 하나로 충족된다. Roadmap Targets는 없고 `policy-hook` completion은 후속 11에 유지된다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/PHASE.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md`
- `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`
- `agent-spec/index.md`, `agent-contract/index.md`
- `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.md`
- `packages/go/streamgate/evidence_tail.go`
- `packages/go/streamgate/filter_registry.go`
- `packages/go/streamgate/evidence_tail_test.go`
- `packages/go/streamgate/filter_registry_test.go`
- `packages/go/streamgate/evaluation_contract_test.go`
- `packages/go/streamgate/stream_release_test.go`

### SDD 기준

- SDD는 `[승인됨]`, 잠금 해제, 사용자 리뷰/잠금 항목 없음이다.
- 대상은 Acceptance Scenario `S07`과 Milestone Task `policy-hook`의 rolling/terminal/fragment/none mixed composition이다. Evidence Map `S07`은 mixed-mode/channel-bound tests와 strongest bounded requirement/single buffer 증거를 요구한다.
- REVIEW_API-1/2가 filter별 readiness, hard bound, exact evaluation set을 고정하고 REVIEW_API-3가 effective enforcement의 단일 source를 강제하며 VERIFY-4가 fresh mixed-mode/lifecycle 증거를 만든다.
- `S06` consumer decision/intent matrix와 Roadmap Completion은 후속 11 소유이므로 이 계획에는 `Roadmap Targets`를 두지 않는다.

### 테스트 환경 규칙

- `test_env=local`. `agent-test/local/rules.md`와 matched profile `agent-test/local/platform-common-smoke.md`가 존재해 읽었다.
- 현재 checkout은 `/config/workspace/iop-s0`, branch `stream-evidence-gate-core`, HEAD `5b56add0`이다. local rule의 고정 repo root `/config/workspace/iop`와 다르지만 모든 명령은 현재 checkout 상대 경로이고 외부 checkout·service·credential을 사용하지 않으므로 test-rule 유지보수는 필요 없다.
- 호스트 Go는 `/config/.local/bin/go` → `/config/opt/go/bin/go`, `go1.26.2 linux/arm64`, `GOROOT=/config/opt/go`로 source가 일치했다. 최종 검증에서 다시 기록하며 `-count=1`로 Go cache를 허용하지 않는다.
- package 변경이므로 `make proto`, fresh streamgate 집중/전체/race, Edge/Node transport, symbol search, `git diff --check`를 적용한다. wire/runtime 사용자 경로는 바뀌지 않아 외부 full-cycle은 생략한다.

### 테스트 커버리지 공백

- terminal event에서 기존 pending subscribed evidence를 합치는 검증이 없다.
- 동일 kind의 blocking/observe overlap과 filter별 event-kind rune threshold 검증이 없다.
- mixed terminal+rolling envelope가 최대 rolling look-behind를 보존하는 검증이 없다.
- plan/resolved/applicability missing·extra·subset exact-set 경계 검증이 없다.
- boolean 정책 seam 제거와 typed applicability id mismatch 검증이 없다.

### 심볼 참조

- `NewFilterHoldBinding(..., bool)`은 typed enforcement 인자로 변경한다. 참조 70곳은 `evidence_tail.go`, `evidence_tail_test.go`, `stream_release_test.go`에 있다.
- `ResolvedFilter.BindEpoch(epochID, subscribed, ready, optionalBlocksRelease...)`는 `BindEpoch(epochID, FilterApplicability)`로 변경한다. 참조 7곳은 `evidence_tail.go`, `filter_registry_test.go`, `evaluation_contract_test.go`에 있다.
- `optionalBlocksRelease`는 제거한다. 다른 production call site는 없다.

### 분할 판단

- split policy를 평가했다. applicability 계산, exact-set bind, typed policy seam은 같은 `EvidencePlan`/`EvidenceEpoch`/`ResolvedFilter` 계약을 동시에 바꾸므로 분리하면 컴파일 불가 또는 일부 filter 누락의 중간 상태가 생긴다.
- 변경은 한 Go package의 2개 production 파일과 4개 test 파일에 국한되고 독립 transport/config/provider 산출물이 없다. 단일 계획이 원자적 contract migration과 회귀 검증에 더 안전하다.
- 디렉터리 이름의 predecessor index `09`는 위 archived `complete.log` 정확히 하나로 충족되며 새 dependency는 추가하지 않는다.

### 범위 결정 근거

- detector decision/RecoveryIntent, consumer family fixture, recovery/retry/rebuild/dispatch는 후속 11 및 다른 milestone task 소유라 제외한다.
- proto/config/provider SDK/Edge/Node production 코드는 public wire surface가 바뀌지 않아 수정하지 않는다.
- dispatcher 소유 `agent-task/m-stream-evidence-gate-core/WORK_LOG.md`, 공통 rules/skills, roadmap/spec/contract 문서는 수정하지 않는다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`; finalizer: `finalize-task-policy.sh`; finalizer_mode: `pair`(분석 완료 후 1회 실행).
- Build closures: scope/context/verification/evidence/ownership/decision 모두 true. 범위와 source/test call site가 닫혔고 focused failure가 결정적이며 외부 권한·결정이 없다. route basis `local-fit`, capability gap `none`.
- Build scores: scope=2, state=2, blast=1, evidence=1, verification=2, 합계 8 → `local G08`, `PLAN-local-G08.md`.
- Build loop-risk: `temporal_state`, `boundary_contract`, `structured_interpretation`, `variant_product`가 match해 triggered=true다. 새 concurrent actor/lock order는 없어 `concurrent_consistency`는 match하지 않는다.
- Review closures도 모두 true. official review가 mixed state/API exactness를 독립 재검증하며 capability gap은 `none`이다. scores 2/2/1/1/2 → `cloud G08`, `CODE_REVIEW-cloud-G08.md`, Codex `gpt-5.6-sol` xhigh.
- missing evidence/blocked reason은 `none`; repetition/signature/G floor는 lane이나 grade에 적용하지 않았다.

## 구현 체크리스트

- [ ] [REVIEW_API-1] filter별 pending evidence/rune과 channel storage envelope로 terminal·hard-bound·overlap applicability를 바로잡는다.
- [ ] [REVIEW_API-2] plan/resolved/epoch applicability exact-set을 검증하고 typed applicability만 `ResolvedFilter`에 bind한다.
- [ ] [REVIEW_API-3] caller boolean 정책 seam을 typed enforcement/applicability seam으로 교체하고 모든 호출부를 갱신한다.
- [ ] [VERIFY-4] mixed matrix, exact-set, lifecycle, fresh package/race/proto/transport 검증을 완료한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 의존 관계 및 구현 순서

1. index 09 `complete.log`가 정확히 하나인지 확인한다.
2. REVIEW_API-1 → REVIEW_API-2 → REVIEW_API-3 순으로 production 계약을 변경하고 각 집중 테스트를 통과시킨다.
3. VERIFY-4 전체 검증 뒤 review stub을 채운다.

### [REVIEW_API-1] Filter별 applicability와 storage envelope

문제: `evidence_tail.go:1217-1233`은 event가 있으면 pending scan을 건너뛰어 terminal/hard-bound에 기존 text evidence가 있어도 `subscribed=false`가 된다. `:1269-1271`은 channel 전역 `pendingRunes`로 다른 kind의 runes까지 threshold에 포함하고, `:1387-1390`은 observe union에 포함된 blocking event를 buffer 전에 반환한다. `:1393`은 strongest representative requirement의 `EvidenceRunes()`를 사용해 mixed terminal+rolling의 look-behind가 max buffer로 변한다.

해결 방법:

Before (`evidence_tail.go:1216`):

```go
subscribed := false
if event != nil {
	subscribed = isKindSubscribed(event.Kind(), req.SubscribedKinds())
} else if isFragmentComplete {
	// pending scan
}
```

After:

```go
stats := subscribedPendingStats(cs, req.SubscribedKinds())
if input.event != nil && !input.eventBuffered && isKindSubscribed(input.event.Kind(), req.SubscribedKinds()) {
	stats.present = true
	stats.runes += runeCountForEvent(*input.event)
}
subscribed := stats.present
ready := deriveTriggerReady(binding, stats, input)
```

- pending entry를 filter subscription별로 세어 rolling threshold, terminal final evidence, fragment/hard-bound readiness에 사용한다. 다른 kind/filter의 runes는 합산하지 않는다.
- normal append는 buffer 후 계산, terminal/overflow candidate는 `eventBuffered=false`인 typed input으로 계산해 중복 계수를 막는다.
- observe-only pass-through는 해당 kind에 blocking subscriber가 없을 때만 적용한다. blocking+observe overlap은 단일 pending buffer에 한 번 저장한다.
- compiled plan의 `maxBufferRunes[channel]`와 `rollingEvidenceRunes[channel]`를 typed storage envelope accessor로 노출하고 `getOrCreateChannel`에는 representative mode가 아닌 envelope 값을 전달한다.
- `NewFilterApplicability` 오류를 무시하지 말고 append/fragment 경로로 반환한다.

수정 파일 및 체크리스트:

- [ ] `packages/go/streamgate/evidence_tail.go`: per-filter stats, applicability input/error, overlap, storage envelope를 구현한다.
- [ ] `packages/go/streamgate/evidence_tail_test.go`: mixed applicability/envelope 회귀를 추가한다.

테스트 작성:

- `TestMultiRequirementPolicyHookApplicabilityMatrix`: terminal pending present+ready, blocking/observe overlap buffer, text rolling이 tool runes를 세지 않음, hard-bound final readiness, provider-error none을 table로 검증한다.
- `TestMultiRequirementPolicyHookStorageEnvelope`: 최소 hard bound와 최대 rolling look-behind가 mixed strongest mode와 무관하게 적용되고 incompatible aggregate는 거절되는지 검증한다.

중간 검증:

```bash
go test -count=1 ./packages/go/streamgate -run 'TestMultiRequirementPolicyHook(ApplicabilityMatrix|StorageEnvelope|ScheduleAndLifecycle)'
```

예상: 모든 matrix가 통과하고 cache 결과는 허용하지 않는다.

### [REVIEW_API-2] Exact-set epoch binding

문제: `evidence_tail.go:814-850`은 전달된 resolved slice만 순회해 plan subset을 수용하고 missing applicability를 text_delta 기반 값으로 합성한다. 이 경계는 모든 expected filter가 evaluated/deferred/not-applicable 중 하나를 가져야 하는 09 arbiter 입력을 불완전하게 만든다.

해결 방법:

Before (`evidence_tail.go:814`):

```go
if len(resolvedFilters) == 0 {
	return nil, nil
}
for i, rf := range resolvedFilters {
	app, ok := epoch.ApplicabilityFor(rf.FilterID())
	if !ok {
		app = FilterApplicability{triggerReady: false}
	}
```

After:

```go
planIDs := p.bindingIDs()
resolvedByID, err := indexResolvedFilters(resolvedFilters)
appsByID, err := indexApplicabilities(epoch.Applicabilities())
if err := requireExactFilterIDs(planIDs, resolvedByID, appsByID); err != nil {
	return nil, err
}
for _, id := range planIDs {
	out = append(out, mustBind(resolvedByID[id], epoch.ID(), appsByID[id]))
}
```

- tail이 모든 plan binding에 applicability를 생성하고 다른 channel filter는 explicit `(false,false)`로 둔다.
- plan/resolved/applicability ID를 stable order로 index하고 missing, extra, duplicate, empty subset을 deterministic error로 거절한다. fallback 합성은 제거한다.
- `ResolvedFilter.BindEpoch`는 applicability filter ID가 resolved filter ID와 같은지 재검증한다.

수정 파일 및 체크리스트:

- [ ] `packages/go/streamgate/evidence_tail.go`: all-plan applicability와 exact-set validation을 구현한다.
- [ ] `packages/go/streamgate/filter_registry.go`: typed applicability bind 및 identity check를 구현한다.
- [ ] `packages/go/streamgate/evidence_tail_test.go`: exact-set matrix를 추가한다.
- [ ] `packages/go/streamgate/filter_registry_test.go`: applicability ID mismatch와 identity mutation 경계를 갱신한다.

테스트 작성:

- `TestMultiRequirementPolicyHookExactSetBinding`: resolved empty/subset/extra/duplicate, applicability missing/extra/duplicate, cross-channel explicit not-applicable를 table로 검증한다.
- `TestResolvedFilterBindEpochRejectsApplicabilityIDMismatch`: typed value가 다른 filter ID를 가지면 거절되는지 검증한다.

중간 검증:

```bash
go test -count=1 ./packages/go/streamgate -run 'Test(MultiRequirementPolicyHookExactSetBinding|ResolvedFilterBindEpochRejectsApplicabilityIDMismatch|FilterRegistryRejectsIdentityMutationAtBoundaries)'
```

예상: 모든 malformed set/id가 오류이고 정상 exact set은 stable order로 bind된다.

### [REVIEW_API-3] Typed policy source of truth

문제: `evidence_tail.go:370`의 `NewFilterHoldBinding(..., blocksRelease bool)`은 non-none policy를 caller가 위조할 수 있다. `filter_registry.go:653-657`의 `optionalBlocksRelease ...bool`과 raw readiness booleans도 Core-derived 계약을 API에 남긴다.

해결 방법:

Before (`evidence_tail.go:370`, `filter_registry.go:653`):

```go
func NewFilterHoldBinding(filterID string, req FilterHoldRequirement, blocksRelease bool) (FilterHoldBinding, error)
func (r ResolvedFilter) BindEpoch(epochID uint64, subscribedEventPresent, triggerReady bool, optionalBlocksRelease ...bool) (EpochFilter, error)
```

After:

```go
func NewFilterHoldBinding(filterID string, req FilterHoldRequirement, enforcement FilterEnforcement) (FilterHoldBinding, error)
func (r ResolvedFilter) BindEpoch(epochID uint64, app FilterApplicability) (EpochFilter, error)
```

- binding constructor는 enforcement를 validate하고 `enforcement == blocking && req.IsBlocking()`으로만 blocksRelease를 파생한다. none은 enforcement와 무관하게 non-blocking이다.
- resolved-to-plan seam은 snapshot의 `rf.Enforcement()`만 전달한다. optional bool 및 raw readiness 인자를 제거한다.
- 70개 binding 참조와 7개 BindEpoch 참조를 typed 값으로 갱신한다. boolean literal 호환 wrapper는 만들지 않는다.

수정 파일 및 체크리스트:

- [ ] `packages/go/streamgate/evidence_tail.go`, `filter_registry.go`: typed signatures와 validation을 구현한다.
- [ ] `packages/go/streamgate/evidence_tail_test.go`, `stream_release_test.go`: binding 호출을 enforcement enum으로 갱신한다.
- [ ] `packages/go/streamgate/filter_registry_test.go`, `evaluation_contract_test.go`: typed applicability 호출과 기대값을 갱신한다.

테스트 작성:

- `TestMultiRequirementPolicyHookTypedPolicySeam`: blocking/observe/none 및 invalid enforcement에서 파생 결과를 검증한다.
- 기존 priority/context/stream releaser tests는 typed call-site migration 회귀로 유지한다.

중간 검증:

```bash
go test -count=1 ./packages/go/streamgate -run 'Test(MultiRequirementPolicyHookTypedPolicySeam|ResolvedFilterPriorityPreservedFromRegistry|ResolvedFilterAdapterPassesFullContextThroughCoordinator|StreamReleaser)'
```

예상: typed seam만 컴파일되고 기존 priority/context/release lifecycle이 통과한다.

### [VERIFY-4] Fresh 전체 검증

문제: 기존 focused/full/race suite가 실제 Required 결함을 검출하지 못했으므로 새 matrix와 symbol removal을 포함한 fresh 증거가 필요하다.

해결 방법: 아래 최종 검증을 그대로 실행하고 stdout/stderr와 exit status를 review stub에 기록한다. 생성물 drift가 생기면 범위를 확인하고 직접 생성물을 편집하지 않는다.

수정 파일 및 체크리스트:

- [ ] 새 집중 tests, 전체 streamgate, race, proto/transport를 fresh 실행한다.
- [ ] raw boolean/variadic symbol search와 `git diff --check`를 통과시킨다.
- [ ] `CODE_REVIEW-cloud-G08.md`에 실제 결과를 기록한다.

테스트 작성: VERIFY 전용 새 파일은 만들지 않는다. REVIEW_API-1~3 regression과 기존 package suite를 실행한다.

중간 검증:

```bash
go test -count=1 ./packages/go/streamgate -run 'TestMultiRequirementPolicyHook'
```

예상: review follow-up regression 전체가 fresh PASS다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/go/streamgate/evidence_tail.go` | REVIEW_API-1, REVIEW_API-2, REVIEW_API-3 |
| `packages/go/streamgate/filter_registry.go` | REVIEW_API-2, REVIEW_API-3 |
| `packages/go/streamgate/evidence_tail_test.go` | REVIEW_API-1, REVIEW_API-2, REVIEW_API-3 |
| `packages/go/streamgate/filter_registry_test.go` | REVIEW_API-2, REVIEW_API-3 |
| `packages/go/streamgate/evaluation_contract_test.go` | REVIEW_API-3 |
| `packages/go/streamgate/stream_release_test.go` | REVIEW_API-3 |
| `CODE_REVIEW-cloud-G08.md` | VERIFY-4 evidence |

## 최종 검증

```bash
command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT
bash -lc 'shopt -s nullglob; logs=(agent-task/m-stream-evidence-gate-core/09_*/complete.log agent-task/m-stream-evidence-gate-core/09+*/complete.log); if (( ${#logs[@]} == 0 )); then logs=(agent-task/archive/*/*/m-stream-evidence-gate-core/09_*/complete.log agent-task/archive/*/*/m-stream-evidence-gate-core/09+*/complete.log); fi; printf "%s\n" "${logs[@]}" | sort; test "${#logs[@]}" -eq 1'
gofmt -w packages/go/streamgate/evidence_tail.go packages/go/streamgate/filter_registry.go packages/go/streamgate/evidence_tail_test.go packages/go/streamgate/filter_registry_test.go packages/go/streamgate/evaluation_contract_test.go packages/go/streamgate/stream_release_test.go
go test -count=1 ./packages/go/streamgate -run 'TestMultiRequirementPolicyHook(ApplicabilityMatrix|StorageEnvelope|ScheduleAndLifecycle)'
go test -count=1 ./packages/go/streamgate -run 'Test(MultiRequirementPolicyHookExactSetBinding|ResolvedFilterBindEpochRejectsApplicabilityIDMismatch|FilterRegistryRejectsIdentityMutationAtBoundaries)'
go test -count=1 ./packages/go/streamgate -run 'Test(MultiRequirementPolicyHookTypedPolicySeam|ResolvedFilterPriorityPreservedFromRegistry|ResolvedFilterAdapterPassesFullContextThroughCoordinator|StreamReleaser)'
go test -count=1 ./packages/go/streamgate -run 'TestMultiRequirementPolicyHook'
go test -count=1 ./packages/go/streamgate -run 'Test(MultiRequirementPolicyHook|EvidencePlan|EvidenceTail|FilterRegistry|ResolvedFilter|StreamReleaser)'
go test -count=1 ./packages/go/streamgate
go test -race -count=1 ./packages/go/streamgate
make proto
go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport
! rg --sort path -n 'optionalBlocksRelease|NewFilterHoldBinding\([^\n]*, (true|false)\)|\.BindEpoch\([^\n]*, (true|false)' packages/go/streamgate
rg --sort path -n '\.BindEpoch\(|NewFilterHoldBinding\(' packages/go/streamgate
git diff --check
```

예상: 필수 matrix와 기존 package/race/proto/transport 회귀가 fresh PASS하고 stale boolean/variadic seam이 없다. Go test cache 출력은 허용하지 않는다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.
