<!-- task=m-stream-evidence-gate-core/23+22_filter_observation_recovery plan=2 tag=REVIEW_FILTER_OBSERVATION_CLOSURE -->

# Plan - Recovery failure observation과 실제 coordinator 증거 폐쇄

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

구현·테스트·증거만 소유한다. 아래 검증을 정확히 실행하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경·출력을 채운 뒤 active pair를 유지하고 리뷰 준비 상태를 보고한다. 차단되면 정확한 blocker, 시도한 명령/출력, 재개 조건만 구현 소유 evidence에 기록한다. 사용자 질문, user-input 도구, `USER_REVIEW.md`, 다음 상태 분류, log archive, `complete.log` 작성은 하지 않는다.

## 배경

두 번째 리뷰에서 dispatch attempt handoff 구현은 확인됐지만 failure producer와 검증 증거가 아직 계획을 충족하지 못했다. 일부 plan/preparer 실패는 observation이 없거나 failure cause append 전에 emit되며, 제출된 targeted 명령은 존재하지 않는 두 테스트 이름을 조용히 건너뛰었다. 실제 RecoveryCoordinator result/concurrency와 integrated attempt 전환을 결정론적으로 닫는다.

## Archive Evidence Snapshot

- 이전 계획: `agent-task/m-stream-evidence-gate-core/23+22_filter_observation_recovery/plan_cloud_G07_1.log`
- 이전 리뷰: `agent-task/m-stream-evidence-gate-core/23+22_filter_observation_recovery/code_review_cloud_G08_1.log`
- 판정: FAIL
- 발견: Required 3, Suggested 0, Nit 0
- 핵심 문제: pre-plan/preparer 실패 observation 누락·선행 emit, 실제 result/concurrency fixture 부재, complete runtime timeline의 old→new attempt assertion 누락
- 영향 파일: `packages/go/streamgate/recovery_coordinator.go`, `packages/go/streamgate/filter_observation_test.go`
- 이전 검증: Go 1.26.2에서 full package unit/race/diff는 통과했다. 그러나 `go test -list`는 targeted 정규식 중 `ObservationHooks`, `CompleteTimelineOrder`, `RecoveryFailureContract`만 존재하고 `ObservationResultMatrix`와 `ObservationConcurrency`는 없음을 확인했다.
- Roadmap carryover: 이번 follow-up은 하위 recovery observation closure이며 Milestone Task를 직접 완료하지 않으므로 `Roadmap Targets`를 두지 않는다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/filter_observation.go`
- `packages/go/streamgate/recovery_coordinator.go`
- `packages/go/streamgate/filter_observation_test.go`
- `packages/go/streamgate/recovery_coordinator_test.go`
- `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-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-spec/index.md`
- `agent-contract/index.md`
- `agent-task/archive/2026/07/m-stream-evidence-gate-core/22+21_filter_observation_runtime/complete.log`
- `agent-task/m-stream-evidence-gate-core/23+22_filter_observation_recovery/plan_cloud_G07_1.log`
- `agent-task/m-stream-evidence-gate-core/23+22_filter_observation_recovery/code_review_cloud_G08_1.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- S08 / `filter-observation`: coordinator lifecycle의 failure와 attempt/epoch correlation을 bounded sanitized cause로 같은 timeline에 기록해야 한다. 항목 1·3의 exact result/correlation assertion에 반영한다.
- S16 / `vertical-slice`: recovery 한 사이클의 abort→rebuild→dispatch→terminal 순서와 single dispatch를 실제 경로에서 증명해야 한다. 항목 2의 coordinator concurrency fixture에 반영한다.
- S20 / `recovery-coordinator`: abort가 끝나기 전 두 번째 cycle/dispatch를 허용하지 않아야 한다. 항목 2의 abort barrier와 busy rejection assertion에 반영한다.
- Evidence Map은 raw-free one timeline과 request-to-recovery one-cycle 증거를 요구하므로 source-name 존재 확인, targeted unit/race, full package unit/race를 최종 검증에 함께 둔다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/platform-common-smoke.md`를 읽었다.
- PATH host Go identity를 먼저 기록하고 fresh `-count=1` 및 `-race`를 사용한다. 이번 범위는 protobuf/config/wire를 바꾸지 않으므로 profile의 `make proto`와 transport/full-cycle은 적용하지 않는다.
- 외부 runner/provider/credential/port는 필요 없다. 내부 observation producer와 package test만 변경하므로 repo Edge-Node 진단, 보조 E2E smoke, live full-cycle은 해당 없음이다.
- 존재하지 않는 테스트를 Go `-run`이 성공으로 건너뛰지 못하도록 `rg --sort path`로 exact 함수 선언 두 개를 먼저 검증한다. test-rule 유지보수는 필요 없다.

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

- plan materialization의 snapshot/preparer mismatch와 contributor failure가 `recovery_plan_rejected`를 emit하는 producer assertion이 없다.
- preparer unavailable/mismatch/reentry/error/deadline/invalid 결과의 stable cause와 exact one-row assertion이 없다.
- `TestRequestRuntime_RecoveryObservationRace`는 coordinator를 실행하지 않는 sequencer-only fixture다.
- complete runtime timeline은 lifecycle kind 순서만 확인하고 dispatch 전후 attempt id 전환을 확인하지 않는다.

### 심볼 참조

- rename/제거 없음. additive test 함수와 `Execute` 내부 failure helper만 다룬다.

### 분할 판단

- failure producer, result matrix, actual concurrency, integrated handoff는 하나의 observation truth table을 함께 만족해야 하므로 한 plan으로 유지한다.
- subtask `23+22`의 선행 index `22`는 `agent-task/archive/2026/07/m-stream-evidence-gate-core/22+21_filter_observation_runtime/complete.log`로 충족됐다.

### 범위 결정 근거

- `filter_observation.go`의 additive failure kind/validation과 dispatch success handoff는 현재 계약을 충족하므로 변경하지 않는다.
- recovery eligibility/budget, rebuilder/dispatcher semantics, Edge sink 저장 정책, public OpenAI 응답, roadmap와 agent-spec은 변경하지 않는다. `agent-spec/index.md`와 `agent-contract/index.md`에는 이 내부 observation closure에 매칭되는 별도 living spec/contract 문서가 없다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`, `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 true. capability gap 없음.
- build scores: scope=2, state/concurrency=2, blast=1, evidence=2, verification=1 → G08. `large_indivisible_context=false`.
- positive loop risks: `temporal_state`, `concurrent_consistency`, `boundary_contract`; count=3.
- recovery signals: `review_rework_count=2`, `evidence_integrity_failure=true`; base=`local-fit`, route basis=`recovery-boundary` → `PLAN-cloud-G08.md`.
- review closures 모두 true; scores 2/2/1/2/1 → G08; route basis=`official-review` → `CODE_REVIEW-cloud-G08.md`.

## 구현 체크리스트

- [ ] REVIEW_FILTER_OBSERVATION_CLOSURE-1 모든 pre-plan 및 preparer 실패를 appended stable cause와 exact one-row observation으로 수렴시킨다.
- [ ] REVIEW_FILTER_OBSERVATION_CLOSURE-2 실제 RecoveryCoordinator result matrix와 abort-barrier concurrency fixture를 구현한다.
- [ ] REVIEW_FILTER_OBSERVATION_CLOSURE-3 complete runtime timeline에서 이전→새 attempt handoff와 이후 row correlation을 exact assertion한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_FILTER_OBSERVATION_CLOSURE-1] Failure producer 단일 결과 계약

- 문제: `packages/go/streamgate/recovery_coordinator.go:475-488`은 materialized plan 전 snapshot/preparer/contributor 실패를 observation 없이 반환한다. `packages/go/streamgate/recovery_coordinator.go:532-587`은 selected plan의 일부 preparer 실패를 emit하지 않거나 stable failure cause를 append하기 전에 emit한다.
- 해결 방법: pre-plan failure는 먼저 `withFailure(code)`를 적용한 동일 result로 `recovery_plan_rejected`를 정확히 한 번 emit하고 반환한다. selected plan의 모든 preparer 실패는 failure result를 먼저 만들고, stable preparer status/deadline outcome 및 그 cause를 가진 `recovery_prepared`를 정확히 한 번 emit한다. raw host error는 observation에 전달하지 않는다.

Before (`packages/go/streamgate/recovery_coordinator.go:478`):

```go
if input.PreparationSnapshot == nil {
    return result.withFailure("preparation_snapshot_missing"), ErrRecoveryPreparationInvalid
}
```

After:

```go
failed := result.withFailure("preparation_snapshot_missing")
emitPlanRejected(failed)
return failed, ErrRecoveryPreparationInvalid
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/recovery_coordinator.go`: pre-plan rejection helper와 selected-preparer failure helper를 두어 각 return이 appended result를 emit하도록 정리한다.
  - [ ] `packages/go/streamgate/filter_observation_test.go`: result matrix에서 rejection/preparer failure의 exact kind, cause, preparer outcome, side-effect barrier를 검증한다.
- 테스트 작성: 작성. `TestRecoveryCoordinator_ObservationResultMatrix`에 snapshot missing/unexpected, invalid contributor, unavailable/mismatch/reentry, preparer error/deadline/invalid, abort/rebuild/dispatch 실패와 성공을 표로 둔다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run '^TestRecoveryCoordinator_ObservationResultMatrix$'`가 PASS해야 한다.

### [REVIEW_FILTER_OBSERVATION_CLOSURE-2] 실제 coordinator concurrency 증거

- 문제: `packages/go/streamgate/filter_observation_test.go:2021-2079`의 race fixture는 `ObservationSequencer.Emit`만 병렬 호출하고 RecoveryCoordinator의 busy guard, abort ownership, single dispatch를 실행하지 않는다. 요구된 `ObservationResultMatrix`/`ObservationConcurrency` 함수도 현재 없다.
- 해결 방법: 기존 `coordinatorController.started/release`, recorder, dispatcher helper를 사용해 첫 Execute를 abort에서 정지하고 두 번째 Execute를 경쟁시킨다. 두 번째 cycle은 cause=`cycle_in_progress`인 plan rejection 한 번, 첫 cycle은 selected→aborted→rebuilt→dispatched 한 번이어야 하며 sequence는 unique contiguous이고 dispatch는 한 번이어야 한다.

Before (`packages/go/streamgate/filter_observation_test.go:2033`):

```go
for g := 0; g < goroutines; g++ {
    go func(gID int) {
        _, err = seq.Emit(context.Background(), input)
    }(g)
}
```

After:

```go
go coordinator.Execute(firstCtx, firstInput)
<-abortStarted
_, secondErr := coordinator.Execute(secondCtx, secondInput)
close(abortRelease)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/filter_observation_test.go`: sequencer-only fixture를 `TestRecoveryCoordinator_ObservationConcurrency`로 교체하고 exact lifecycle/sequence/cause/dispatch/handoff를 assertion한다.
  - [ ] `packages/go/streamgate/recovery_coordinator_test.go`: 기존 helper만 재사용하고 수정하지 않는다.
- 테스트 작성: 작성. 실제 coordinator fixture가 `go test -race`에서 abort barrier와 single outbound dispatch를 실행한다.
- 중간 검증: `go test -race -count=1 ./packages/go/streamgate -run '^TestRecoveryCoordinator_ObservationConcurrency$'`가 PASS해야 한다.

### [REVIEW_FILTER_OBSERVATION_CLOSURE-3] Integrated attempt handoff correlation

- 문제: `packages/go/streamgate/filter_observation_test.go:1990-2018`은 recovery kind와 순서만 검사해 dispatch 전 old attempt와 dispatch/후속 release·terminal의 new attempt correlation을 검증하지 않는다.
- 해결 방법: complete timeline의 각 recovery row를 exact kind로 찾고 selected/aborted/rebuilt는 `attempt.1`, dispatched 및 이후 attempt-2 evaluation/release/terminal은 `attempt.2`인지 assertion한다. continue-stream shared attempt는 `ObservationHooks`의 기존 exact assertion을 유지한다.

Before (`packages/go/streamgate/filter_observation_test.go:2009`):

```go
selectedIdx := slices.Index(kinds, ObservationKindRecoveryPlanSelected)
```

After:

```go
if obss[selectedIdx].AttemptID() != "attempt.1" || obss[dispatchedIdx].AttemptID() != "attempt.2" {
    t.Fatalf("recovery attempt handoff mismatch")
}
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/filter_observation_test.go`: complete timeline의 exact old/new attempt rows와 post-dispatch correlation을 추가한다.
- 테스트 작성: 작성. 기존 `TestRequestRuntime_CompleteTimelineOrder`를 확장하며 별도 synthetic fixture를 만들지 않는다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run '^TestRequestRuntime_CompleteTimelineOrder$'`가 PASS해야 한다.

## 의존 관계 및 구현 순서

선행 `22+21_filter_observation_runtime`은 위 archived `complete.log`로 충족됐다. failure helper 정리 → result matrix → actual concurrency → integrated handoff → fresh full/race 검증 순서로 수행한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/go/streamgate/recovery_coordinator.go` | REVIEW_FILTER_OBSERVATION_CLOSURE-1 |
| `packages/go/streamgate/filter_observation_test.go` | REVIEW_FILTER_OBSERVATION_CLOSURE-1, REVIEW_FILTER_OBSERVATION_CLOSURE-2, REVIEW_FILTER_OBSERVATION_CLOSURE-3 |

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`
- `rg --sort path -n '^func TestRecoveryCoordinator_Observation(ResultMatrix|Concurrency)\(' packages/go/streamgate/filter_observation_test.go`
- `go test -count=1 ./packages/go/streamgate -run '^(TestRecoveryCoordinator_ObservationResultMatrix|TestRequestRuntime_CompleteTimelineOrder)$'`
- `go test -race -count=1 ./packages/go/streamgate -run '^TestRecoveryCoordinator_ObservationConcurrency$'`
- `go test -count=1 ./packages/go/streamgate`
- `go test -race -count=1 ./packages/go/streamgate`
- `test -z "$(gofmt -l packages/go/streamgate/recovery_coordinator.go packages/go/streamgate/filter_observation_test.go)"`
- `go vet ./packages/go/streamgate/`
- `git diff --check`

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