<!-- task=m-stream-evidence-gate-core/21+14,19_filter_observation_contract plan=2 tag=REVIEW_FILTER_OBSERVATION_CONTRACT -->

# Plan - FilterObservation lifecycle 계약 보완

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

구현·테스트·검증 evidence만 소유한다. 체크리스트와 검증을 완료한 뒤 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 내용과 stdout/stderr를 채우고 active pair를 그대로 둔 채 review ready를 보고한다. 차단되면 정확한 blocker, 시도한 명령/출력, 재개 조건만 구현 소유 evidence에 기록한다. 사용자 질문, user-input 도구, `USER_REVIEW.md`, 상태 분류, log archive, `complete.log`, roadmap 수정은 수행하지 않는다.

## 배경

현재 envelope는 컴파일과 race 검증을 통과하지만 실제 arbitration action을 보존하지 않고, derived failure disposition과 lifecycle state의 모순된 조합을 허용한다. SDD S08의 filter/coordinator timeline을 후속 hook이 안전하게 소비하려면 contract 단계에서 시작/완료와 action/state 불변식을 닫아야 한다.

## Archive Evidence Snapshot

- 이전 task path: `agent-task/m-stream-evidence-gate-core/21+14,19_filter_observation_contract/`
- 종료 pair: `plan_local_G08_1.log`, `code_review_cloud_G08_1.log`
- 판정: FAIL; Required 4, Suggested 1, Nit 0
- 문제 요약:
  - `arbitration_decided`가 action/base disposition 없이 유효해 결정 종류를 복원할 수 없다.
  - observation failure disposition이 canonical outcome/enforcement truth table과 달라도 허용된다.
  - filter epoch와 response/release/terminal commit state의 불가능 조합이 허용된다.
  - closed lifecycle vocabulary에 병렬 filter evaluation start가 없다.
  - raw-free 구조 검사가 직접 포함된 carrier graph 일부를 누락한다.
- 영향 파일: `packages/go/streamgate/filter_observation.go`, `packages/go/streamgate/filter_observation_test.go`
- 실제 검증: host Go `1.26.2`; `go test -count=1 ./packages/go/streamgate`, `go test -race -count=1 ./packages/go/streamgate`, `go build`, `go vet`, `gofmt -l`, `git diff --check` 모두 PASS. 테스트 통과는 위 semantic gap을 검출하지 못했다.
- Roadmap carryover: approved SDD S08의 contract foundation만 보완한다. lifecycle hook/full timeline은 sibling `22`/`23` 범위이며 이 pair에는 `Roadmap Targets`가 없다.
- 좁은 재열람 허용: `agent-task/m-stream-evidence-gate-core/21+14,19_filter_observation_contract/plan_local_G08_1.log`, `agent-task/m-stream-evidence-gate-core/21+14,19_filter_observation_contract/code_review_cloud_G08_1.log`

## 분석 결과

### 읽은 파일

- 규칙/환경: `agent-ops/rules/project/rules.md`, `agent-ops/rules/project/domain/platform-common/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.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`
- 현재 구현/테스트: `packages/go/streamgate/filter_observation.go`, `packages/go/streamgate/filter_observation_test.go`
- 연결 계약: `packages/go/streamgate/decision_arbiter.go`, `packages/go/streamgate/evaluation_contract.go`, `packages/go/streamgate/filter_contract.go`, `packages/go/streamgate/event.go`, `packages/go/streamgate/terminal.go`, `packages/go/streamgate/recovery_plan.go`, `packages/go/streamgate/evidence_tail.go`
- prior evidence: 위 `Archive Evidence Snapshot`의 두 log와 선행 `14`, `19`의 `complete.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`; 상태 `[승인됨]`, 잠금 해제.
- 대상 Scenario: S08 / Milestone Task `filter-observation`.
- Evidence Map: multi-consumer timeline이 config/attempt/epoch/staging/failure/resume mode와 raw absence를 보존해야 한다.
- 적용: 이 pair는 S08 전체 완료가 아니라 hook 전 contract foundation이다. checklist는 action 식별, filter start/complete, epoch/commit state, canonical failure policy, raw-free carrier 검증을 닫고 full timeline/Edge sink는 제외한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/platform-common-smoke.md`를 읽었다.
- 현재 checkout의 PATH 첫 Go(`/config/.local/bin/go` → `/config/opt/go/bin/go`)를 유지하고 fresh target/race test를 사용한다.
- profile의 proto setup은 `make proto`로 확인한다. 이 변경은 proto/config/Edge-Node wire나 사용자 실행 경로를 바꾸지 않으므로 transport test와 full-cycle 실제 구동은 적용하지 않는다.
- package contract fallback은 프로젝트 규칙의 target package test, `gofmt`, `git diff --check`다. `<확인 필요>` 값은 없다.

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

- arbitration action/base disposition: 기존 테스트가 누락 상태를 valid로 기대한다. 다섯 action과 missing/unknown 경계가 필요하다.
- failure disposition: decisionKind 유무만 검사하고 canonical outcome/decision/enforcement matrix를 검사하지 않는다.
- lifecycle state: filter epoch 0을 valid로 기대하며 response/release/terminal commit-state matrix가 없다.
- lifecycle vocabulary: filter evaluation start 정상/누락 필드 테스트가 없다.
- raw-free/immutability: `SanitizedEvidence`, `FailureCauseChain` carrier graph와 hold/recovery/preparer/causes accessor copy가 완전히 검증되지 않는다.

### 심볼 참조

- renamed/removed symbol: 없음.
- 새 observation API는 현재 `filter_observation.go`와 해당 test 외 production call site가 없으며 sibling `22`/`23`이 후속 소비한다.

### 분할 판단

- split policy를 평가했다. 이미 Milestone split subtask `21+14,19_filter_observation_contract`이며 이번 finding은 동일 public contract와 같은 test file을 원자적으로 고쳐야 해 추가 분할하지 않는다.
- predecessor `14`: `agent-task/archive/2026/07/m-stream-evidence-gate-core/14+13_recovery_execution/complete.log`로 충족.
- predecessor `19`: `agent-task/archive/2026/07/m-stream-evidence-gate-core/19+18_core_runtime_loop/complete.log`로 충족.

### 범위 결정 근거

- 수정 범위는 `filter_observation.go`와 `filter_observation_test.go`뿐이다.
- `parallel_evaluation.go`, `runtime.go`, `recovery_coordinator.go` hook은 sibling `22`/`23`, Edge sink/correlation adapter는 sibling `24` 범위라 수정하지 않는다.
- agent-contract, agent-spec, roadmap은 외부/wire/living spec 변경이 아니며 이 pair에서 갱신하지 않는다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`; finalizer=`finalize-task-policy.sh`; finalizer_mode=`pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 true. 근거는 두 파일의 확정 findings, approved S08, fresh unit/race output, sibling 범위 분리, Milestone 결정 필요 없음이다.
- build route: `local-fit`; capability gap: none; scores=`scope_coupling=2,state_concurrency=1,blast_irreversibility=1,evidence_diagnosis=1,verification_complexity=1`; lane/G=`local/G06`; filename=`PLAN-local-G06.md`.
- build loop-risk: 10+ lifecycle states와 invalid/failure paths, actor 2+ atomic sequence, Core/evaluator/runtime/recovery/sink와 2+ consumer의 immutable boundary, kind × epoch × commit/action 조합으로 `temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product`가 matched; structured interpretation은 없음. floor: none.
- review closures: 모두 true; 동일 source/SDD/test를 독립 대조할 수 있다.
- review route: `official-review`; capability gap: none; scores=`2,1,1,1,1`; lane/G=`cloud/G06`; filename=`CODE_REVIEW-cloud-G06.md`; target=`Codex gpt-5.6-sol xhigh`. floor: none.

## 구현 체크리스트

- [x] [REVIEW_FILTER_OBSERVATION_CONTRACT-1] arbitration summary, filter evaluation start와 kind별 epoch/commit-state 불변식을 구현하고 정상/불가능 조합 테스트를 추가한다.
- [x] [REVIEW_FILTER_OBSERVATION_CONTRACT-2] observation failure disposition을 canonical outcome/decision/enforcement truth table에서 파생하고 전체 matrix 테스트를 추가한다.
- [x] [REVIEW_FILTER_OBSERVATION_CONTRACT-3] raw-free carrier graph와 모든 pointer/slice accessor defensive copy를 검증하고 local proto/unit/race/format/diff 검증을 수행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_FILTER_OBSERVATION_CONTRACT-1] Lifecycle와 arbitration 불변식

- 문제: `filter_observation.go:406-421`에는 arbitration payload가 없고 `filter_observation.go:574-605`는 `arbitration_decided`를 빈 payload로 허용한다. `filter_observation.go:16-57`에는 filter evaluation start kind가 없으며 epoch/commit state를 kind와 결합하지 않는다.
- 해결 방법:

```go
// Before: action 없이 kind만 기록한다.
Kind ObservationKind

// After: 기존 closed type을 재사용한 immutable summary를 둔다.
Arbitration *ObservationArbitrationInfo // action + base disposition
```

  - [x] `ObservationKindFilterEvaluationStarted`를 추가하고 attribution+non-zero epoch를 요구한다.
  - [x] `filter_evaluated`는 attribution+decision policy+non-zero epoch, `arbitration_decided`는 arbitration summary+non-zero epoch를 요구한다.
  - [x] response staged/release committed/terminal committed가 각각 `transport_uncommitted`/`stream_open`/`terminal_committed`와 일치하게 한다.
  - [x] recovery resume mode와 관측 시점 commit state의 허용 matrix를 명시하고 impossible combination을 거부한다.
- 수정 파일 및 체크리스트:
  - [x] `packages/go/streamgate/filter_observation.go`: new kind, arbitration info/accessor/copy, lifecycle validation.
  - [x] `packages/go/streamgate/filter_observation_test.go`: 다섯 arbitration action, unknown/missing summary, start/complete epoch, commit-state table tests.
- 테스트 작성: `TestFilterObservation_ArbitrationInfoRequiredAndClosed`, `TestFilterObservation_FilterEvaluationStartedContract`, `TestFilterObservation_KindStateMatrix`를 추가한다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run '^(TestFilterObservation_(ArbitrationInfoRequiredAndClosed|FilterEvaluationStartedContract|KindStateMatrix))$'`가 PASS해야 한다.
- **실제 검증 stdout:**
```
=== RUN   TestFilterObservation_ArbitrationInfoRequiredAndClosed
--- PASS: TestFilterObservation_ArbitrationInfoRequiredAndClosed (0.00s)
=== RUN   TestFilterObservation_FilterEvaluationStartedContract
--- PASS: TestFilterObservation_FilterEvaluationStartedContract (0.00s)
=== RUN   TestFilterObservation_KindStateMatrix
=== RUN   TestFilterObservation_KindStateMatrix/response_staged_requires_transport_uncommitted
=== RUN   TestFilterObservation_KindStateMatrix/response_staged_valid_with_transport_uncommitted
=== RUN   TestFilterObservation_KindStateMatrix/release_committed_requires_stream_open
=== RUN   TestFilterObservation_KindStateMatrix/release_committed_valid_with_stream_open
=== RUN   TestFilterObservation_KindStateMatrix/terminal_committed_requires_terminal_committed_state
=== RUN   TestFilterObservation_KindStateMatrix/terminal_committed_valid_with_terminal_committed_state
=== RUN   TestFilterObservation_KindStateMatrix/replace_attempt_recovery_requires_transport_uncommitted
=== RUN   TestFilterObservation_KindStateMatrix/replace_attempt_recovery_valid_with_transport_uncommitted
=== RUN   TestFilterObservation_KindStateMatrix/continue_stream_recovery_requires_stream_open
=== RUN   TestFilterObservation_KindStateMatrix/continue_stream_recovery_valid_with_stream_open
=== RUN   TestFilterObservation_KindStateMatrix/filter_evaluated_valid_with_non-zero_epoch_and_transport_uncommitted
--- PASS: TestFilterObservation_KindStateMatrix (0.00s)
```
- 11개 subtest 모두 PASS.

### [REVIEW_FILTER_OBSERVATION_CONTRACT-2] Canonical failure policy

- 문제: `filter_observation.go:225-264`는 caller가 전달한 failure disposition의 enum만 검사해 기존 `evaluation_contract.go:474-516` truth table과 모순된 관측을 만든다.
- 해결 방법:

```go
// Before
NewObservationDecisionPolicy(outcome, decisionKind, enforcement, failureDisposition)

// After
NewObservationDecisionPolicy(outcome, decisionKind, enforcement) // disposition은 내부 파생
```

  - [x] evaluated decision kind와 enforcement로 canonical disposition을 파생하고 evaluation_error/not-applicable/deferred도 기존 truth table과 동일하게 처리한다.
  - [x] `Validate()`는 package-local forged value도 expected disposition과 대조한다.
- 수정 파일 및 체크리스트:
  - [x] `packages/go/streamgate/filter_observation.go`: constructor/derive/Validate 정합성.
  - [x] `packages/go/streamgate/filter_observation_test.go`: decision/outcome × enforcement matrix와 forged mismatch rejection.
- 테스트 작성: `TestObservationDecisionPolicy_CanonicalDispositionMatrix`와 `TestObservationDecisionPolicy_CanonicalDispositionConsistency`를 추가한다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run '^TestObservationDecisionPolicy_'`가 PASS해야 한다.
- **실제 검증 stdout:**
```
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/evaluated_pass_blocking_→_none
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/evaluated_pass_observe_only_→_none
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/evaluated_observe_blocking_→_none
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/evaluated_observe_observe_only_→_none
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/evaluated_replacement_blocking_→_none
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/evaluated_replacement_observe_only_→_none
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/evaluated_violation_blocking_→_blocking_fatal
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/evaluated_violation_observe_only_→_observe_error
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/evaluated_fatal_blocking_→_blocking_fatal
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/evaluated_fatal_observe_only_→_observe_error
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/eval_error_blocking_→_blocking_fatal
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/eval_error_observe_only_→_observe_error
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/not_applicable_→_none
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/not_applicable_→_none_observe
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/deferred_→_none
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionMatrix/deferred_→_none_observe
--- PASS: TestObservationDecisionPolicy_CanonicalDispositionMatrix (0.00s)
=== RUN   TestObservationDecisionPolicy_CanonicalDispositionConsistency
--- PASS: TestObservationDecisionPolicy_CanonicalDispositionConsistency (0.00s)
```
- 16개 matrix subtest + consistency test 모두 PASS.

### [REVIEW_FILTER_OBSERVATION_CONTRACT-3] Raw-free와 immutable closure

- 문제: `filter_observation_test.go:322-331`의 carrier 목록이 direct nested `SanitizedEvidence`와 `FailureCauseChain`을 누락하고 accessor test는 attribution/policy/evidence만 검사한다.
- 해결 방법: streamgate-owned carrier type을 재귀 순회하거나 명시 allowlist로 모두 검사하되 `time.Time` 같은 표준 라이브러리 구현 세부는 leaf로 취급한다. hold/recovery/preparer/causes 반환값도 변형 후 원본 불변을 확인한다.
- 수정 파일 및 체크리스트:
  - [x] `packages/go/streamgate/filter_observation_test.go`: `TestFilterObservation_RawFreeCarrierGraph`, `TestFilterObservation_AllAccessorsDefensive` 추가/보강.
  - [x] 전체 fresh unit/race/format/diff 검증 결과를 review stub에 기록한다.
- 테스트 작성: raw-free graph와 모든 mutable carrier accessor의 defensive copy 경계를 직접 검증한다.
- 중간 검증: `go test -race -count=1 ./packages/go/streamgate`가 PASS하고 `gofmt -l` 출력이 없어야 한다.
- **실제 검증 stdout:**
```
=== RUN   TestFilterObservation_RawFreeCarrierGraph
--- PASS: TestFilterObservation_RawFreeCarrierGraph (0.00s)
=== RUN   TestFilterObservation_AllAccessorsDefensive
--- PASS: TestFilterObservation_AllAccessorsDefensive (0.00s)
=== RUN   TestFilterObservation_TerminalReasonAndOccurrenceAreImmutable
--- PASS: TestFilterObservation_TerminalReasonAndOccurrenceAreImmutable (0.00s)
```
- 전체 테스트 결과:
```
$ go test -race -count=1 ./packages/go/streamgate
ok  	iop/packages/go/streamgate	1.925s
$ gofmt -l packages/go/streamgate/filter_observation.go packages/go/streamgate/filter_observation_test.go
(no output)
$ git diff --check
(no output)
$ go vet ./packages/go/streamgate/
(no output)
```

## 의존 관계 및 구현 순서

`14`, `19` 완료 → CONTRACT-1/2 → CONTRACT-3. sibling hook 작업은 이 contract PASS 뒤 소비한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/go/streamgate/filter_observation.go` | CONTRACT-1, CONTRACT-2: new kind, arbitration info type/clone/accessor, canonical disposition derive, lifecycle validation, recovery commit state matrix |
| `packages/go/streamgate/filter_observation_test.go` | CONTRACT-1, CONTRACT-2, CONTRACT-3: 7 new test functions (~30 subtests), existing test updates for new invariants |

## 최종 검증

- [x] `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`
- [x] `make proto`
- [x] `go test -count=1 ./packages/go/streamgate -run '^(TestFilterObservation_(ArbitrationInfoRequiredAndClosed|FilterEvaluationStartedContract|KindStateMatrix|RawFreeCarrierGraph|AllAccessorsDefensive)|TestObservationDecisionPolicy_CanonicalDispositionMatrix)$'` → PASS
- [x] `go test -count=1 ./packages/go/streamgate` → PASS (전체)
- [x] `go test -race -count=1 ./packages/go/streamgate` → PASS (전체, 1.925s)
- [x] `test -z "$(gofmt -l packages/go/streamgate/filter_observation.go packages/go/streamgate/filter_observation_test.go)"` → PASS (no output)
- [x] `git diff --check` → PASS (no output)
- [x] `go vet ./packages/go/streamgate/` → PASS (no output)

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