<!-- task=m-stream-evidence-gate-core/25+23,24_edge_observation_correlation plan=1 tag=REVIEW_EDGE_FILTER_OBSERVATION_CORRELATION -->

# Plan - Edge FilterObservation correlation and evidence closure follow-up

## 배경

첫 리뷰는 실제 attempt target metadata가 observation에서 소실되고, Edge runtime 전체 timeline을 검증하는 fixture가 없으며, 기록된 test evidence가 실행 명령과 일치하지 않아 FAIL했다. SDD S08의 raw-free correlation 요구를 Core contract부터 Edge sink와 fake full-cycle까지 한 vertical slice로 닫는다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md)
- Task ids:
  - `filter-observation`: raw-free correlated FilterObservation timeline
- Completion mode: check-on-pass

## Archive Evidence Snapshot

- Prior plan: `agent-task/m-stream-evidence-gate-core/25+23,24_edge_observation_correlation/plan_local_G07_0.log`
- Prior review: `agent-task/m-stream-evidence-gate-core/25+23,24_edge_observation_correlation/code_review_cloud_G07_0.log`
- Verdict: `FAIL`
- Required:
  - observation envelope와 zap allowlist에 actual run/provider/model/path correlation이 없고 recovery attempt id 정규화가 초기 attempt와 다르다.
  - 실제 Edge OpenAI runtime에 sink를 주입해 recovery/release/terminal timeline과 raw sentinel 부재를 증명한 integration fixture가 없다.
  - 문서의 focused command 출력과 실제 test selection이 다르고, 주장한 log-safety 수정과 local fake full-cycle evidence가 없다.
- Suggested: 없음
- Nit: 없음
- Affected files: `packages/go/streamgate/{filter_observation.go,parallel_evaluation.go,recovery_coordinator.go,runtime.go}`, `apps/edge/internal/openai/{filter_observation_sink.go,stream_gate_dispatcher.go,stream_gate_vertical_slice_test.go,filter_observation_sink_test.go,log_safety_test.go}`, `scripts/e2e-openai-vllm.sh`.
- Fresh verification evidence: 기존 focused 명령은 sink test 4개만 실행했고, package test와 Edge race는 PASS했다. test selection과 문서 출력 불일치 때문에 `evidence_integrity_failure=true`이다.
- Roadmap carryover: `filter-observation`은 FAIL 상태에서 완료 처리하지 않으며 이 follow-up PASS만 check-on-pass를 발생시킨다.

## 분석 결과

### 읽은 파일

- 규칙/계획: `agent-ops/rules/project/rules.md`, `agent-ops/rules/common/rules-roadmap.md`, `agent-ops/skills/common/router.md`, `agent-ops/skills/common/code-review/SKILL.md`, `agent-ops/skills/common/plan/SKILL.md`, `agent-ops/skills/common/finalize-task-routing/SKILL.md`.
- 도메인/테스트: `agent-ops/rules/project/edge.md`, `agent-ops/rules/project/platform-common.md`, `agent-ops/rules/project/testing.md`, `agent-test/local/rules.md`, `agent-test/local/profiles/edge-smoke.md`, `agent-test/local/profiles/platform-common-smoke.md`, `agent-test/local/profiles/testing-smoke.md`.
- 계약/설계: `agent-contract/index.md`, `agent-contract/outer/openai-compatible-api.md`, `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md`, `agent-roadmap/phase/knowledge-tool-optimization-extension/phases/stream-evidence-gate-core.md`, `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`의 S08/S16 및 Evidence Map.
- Core: `packages/go/streamgate/filter_observation.go`, `packages/go/streamgate/filter_observation_test.go`, `packages/go/streamgate/parallel_evaluation.go`, `packages/go/streamgate/recovery_coordinator.go`, `packages/go/streamgate/recovery_coordinator_test.go`, `packages/go/streamgate/recovery_plan.go`, `packages/go/streamgate/runtime.go`, `packages/go/streamgate/runtime_test.go`, `packages/go/streamgate/filter_registry.go`.
- Edge: `apps/edge/internal/openai/filter_observation_sink.go`, `apps/edge/internal/openai/filter_observation_sink_test.go`, `apps/edge/internal/openai/stream_gate_dispatcher.go`, `apps/edge/internal/openai/stream_gate_runtime.go`, `apps/edge/internal/openai/stream_gate_vertical_slice_test.go`, `apps/edge/internal/openai/log_safety_test.go`, `scripts/e2e-openai-vllm.sh`.

### SDD 기준

- `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md` 상태는 승인됨이며 Milestone lock은 해제되어 있다.
- S08은 `filter-observation`에 request/model과 actual run/provider/model/path를 연결한 raw-free chronological record를 요구한다. Evidence Map은 Core contract/unit, Edge sink/integration, log-safety/full-cycle evidence를 함께 요구하므로 세 구현 항목을 각각 contract, vertical slice, trusted verification에 대응시킨다.
- S16 fixture는 recovery path switch의 실제 initial/rebound target 검증 기반으로 재사용하지만, 이 plan의 roadmap completion target은 `filter-observation` 하나뿐이다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 매칭 profile 세 개를 읽고 Go unit/race, Edge smoke/full-cycle, `git diff --check`를 적용한다.
- Go는 `/config/.local/bin/go`가 `/config/opt/go/bin/go`를 가리키며 분석 시점 버전은 `go1.26.2 linux/arm64`, GOROOT는 `/config/opt/go`였다.
- 프리플라이트: repo root `/config/workspace/iop-s0`, branch `stream-evidence-gate-core`, 분석 시점 HEAD `12a48fe0`, 누적 작업으로 dirty 상태다. 구현자는 기존 사용자 변경을 보존하고 이 task 범위만 수정한다.
- `scripts/e2e-openai-vllm.sh`는 기본 `IOP_VLLM_MODE=fake`에서 loopback fake provider와 현재 checkout의 Edge/Node binary를 구동한다. 외부 endpoint와 secret은 필요 없으며, script가 gate를 명시적으로 활성화하고 observation log oracle을 검사하도록 보강한다.
- fresh evidence가 중요하므로 모든 Go test에 `-count=1`을 사용한다. cached output은 허용하지 않는다.

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

- `FilterObservationInput`/`ObservationContext`는 request correlation, config generation, attempt id만 보존하고 model group/model/provider/path를 운반하지 않는다.
- recovery coordinator는 새 binding을 설치하지만 emit helper가 target snapshot을 받지 않아 rebinding 이후 target 전환을 관측할 수 없다.
- sink unit은 수동 input mapping만 검증한다. `stream_gate_vertical_slice_test.go`의 path-switch fixture는 `SetObservationSink`를 호출하지 않아 실제 runtime sequence와 correlation을 검증하지 않는다.
- 기존 log-safety focused regex는 log tests를 선택하지 않는다. fake OpenAI full-cycle도 gate/observation을 활성화하거나 observation fields를 검사하지 않는다.

### 심볼 참조

- rename/remove 대상은 없다.
- `ObservationContext` literal call site는 `packages/go/streamgate/runtime.go:933`, test call site는 `packages/go/streamgate/filter_observation_test.go:1597,1827`이다.
- initial `NewAttemptBinding` call sites는 `apps/edge/internal/openai/stream_gate_runtime.go:759,1197`에서 safe attempt token을 사용하지만 recovery call site `apps/edge/internal/openai/stream_gate_dispatcher.go:144`는 raw run id를 사용한다.
- Edge test에서 `SetObservationSink` call site는 없다.

### 분할 판단

- 단일 plan으로 유지한다. immutable observation target contract, recovery rebinding, Edge sink/timeline fixture는 같은 correlation invariant를 공유해 분리하면 중간 상태가 검증 불가능하다.
- predecessor `23`은 `agent-task/archive/2026/07/m-stream-evidence-gate-core/23+22_filter_observation_recovery/complete.log`, predecessor `24`는 `agent-task/archive/2026/07/m-stream-evidence-gate-core/24+20,21_edge_observation_sink/complete.log`로 충족됐다.

### 범위 결정 근거

- OpenAI public request/response API, config schema, semantic filter 정책, provider selection 정책은 변경하지 않는다.
- raw prompt/output/tool/auth/preparer payload를 observation contract에 추가하지 않는다. target metadata는 명시적 closed allowlist와 bounded validation만 사용한다.
- `agent-roadmap/**`, dispatcher-owned `agent-task/m-stream-evidence-gate-core/WORK_LOG.md`, 공통 `agent-ops/rules/common/**`와 `agent-ops/skills/common/**`는 수정하지 않는다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`, finalizer=`finalize-task-routing`.
- build target: closure=true, grade-boundary, `cloud G09`, `PLAN-cloud-G09.md`.
- official review target: closure=true, grade-boundary, `cloud G09`, `CODE_REVIEW-cloud-G09.md`.
- `large_indivisible_context=false`.
- positive loop risks: `temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product` (4).
- recovery signals: `review_rework_count=1`, `evidence_integrity_failure=true`.
- capability-gap evidence: 없음. grade-boundary 점수는 build/review 모두 `2,2,1,2,2`이며 recovery boundary를 충족한다.

## 구현 체크리스트

- [ ] CORR-FOLLOWUP-1 `FilterObservation`에 request/model과 actual run/provider/model/path correlation을 raw-free immutable context로 전달하고 initial/recovery attempt safe-id 규칙을 통일한다.
- [ ] CORR-FOLLOWUP-2 실제 Edge OpenAI runtime의 initial→recovery→release/terminal timeline, bounded evidence/cause, raw sentinel 부재와 sink failure 격리를 integration fixture로 검증한다.
- [ ] CORR-FOLLOWUP-3 log-safety oracle와 OpenAI fake full-cycle을 포함한 fresh unit/race/diff evidence를 정확한 명령·stdout으로 재작성한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [CORR-FOLLOWUP-1] Immutable attempt target correlation

- 문제: `packages/go/streamgate/filter_observation.go:535-552`는 attempt id 외 target metadata가 없고, `packages/go/streamgate/runtime.go:414-435`는 current binding에서 attempt id만 복사한다. `packages/go/streamgate/recovery_coordinator.go:453-476`도 observed attempt id만 받는다. Edge recovery binding은 `apps/edge/internal/openai/stream_gate_dispatcher.go:144-151`에서 raw run id를 사용한다.
- 해결 방법: request model group과 현재 `AttemptBinding`의 model/provider/execution path를 담는 immutable, bounded, raw-free `ObservationAttemptTarget`을 추가한다. `FilterObservationInput`, `FilterObservation`, `ObservationContext`가 이 value를 방어적으로 보존하고 검증한다. runtime의 모든 emission은 lock 안에서 current binding snapshot과 model group을 함께 읽는다. recovery emission은 abort 전 event에 이전 binding, dispatch/install 이후 event에 새 binding을 사용한다. Edge initial/recovery 모두 `openAIStreamGateSafeToken("attempt", dispatch.RunID)`을 사용한다.
+
+```go
+// Before: packages/go/streamgate/runtime.go:418-430
+r.mu.Lock()
+attemptID := r.currentBinding.AttemptID()
+commitState := r.boundary.State()
+r.mu.Unlock()
+
+input := FilterObservationInput{
+    AttemptID: attemptID,
+    CommitState: commitState,
+}
+
+// After: binding과 commit state를 같은 lock snapshot으로 읽고 target을 함께 전달한다.
+binding := r.currentBinding
+target, err := NewObservationAttemptTarget(r.modelGroup, binding.Model(), binding.Provider(), binding.ExecutionPath())
+input := FilterObservationInput{AttemptID: binding.AttemptID(), AttemptTarget: target, CommitState: commitState}
+```
+
+- 수정 파일 및 체크리스트:
+  - [ ] `packages/go/streamgate/filter_observation.go`: target value, constructor/accessor/validation/copy contract 추가.
+  - [ ] `packages/go/streamgate/parallel_evaluation.go`: `ObservationContext`에 immutable target 추가.
+  - [ ] `packages/go/streamgate/runtime.go`, `packages/go/streamgate/recovery_coordinator.go`: initial/current/rebound target을 해당 sequence에 전달.
+  - [ ] `apps/edge/internal/openai/filter_observation_sink.go`: closed zap allowlist에 model group/model/provider/execution path 추가.
+  - [ ] `apps/edge/internal/openai/stream_gate_dispatcher.go`: recovery attempt safe-id 정규화 통일.
+- 테스트 작성: `packages/go/streamgate/filter_observation_test.go`에 target normal/boundary/immutability와 recovery target switch를 추가하고, `packages/go/streamgate/recovery_coordinator_test.go` 및 `packages/go/streamgate/runtime_test.go`에서 pre/post rebinding event target을 검증한다. `apps/edge/internal/openai/filter_observation_sink_test.go`에서 정확한 field allowlist와 invalid run-id 정규화를 검증한다.
+- 중간 검증: `go test -count=1 ./packages/go/streamgate -run 'FilterObservation|RecoveryCoordinator|RequestRuntime'`와 `go test -count=1 ./apps/edge/internal/openai -run '^TestFilterObservationSink_'`가 PASS해야 한다.
+
+### [CORR-FOLLOWUP-2] Edge correlated timeline integration
+
+- 문제: `apps/edge/internal/openai/stream_gate_vertical_slice_test.go:869-900`의 recovery path-switch fixture는 response/dispatch만 검증하고 observation sink를 주입하지 않는다. `apps/edge/internal/openai/filter_observation_sink_test.go:101` 이후 test는 수동 input mapping이라 실제 Edge runtime timeline 증거가 아니다.
+- 해결 방법: 기존 scripted normalized→tunnel fixture에 thread-safe recording sink를 주입한다. 한 request에서 response staged, filter evaluation, arbitration, recovery selected/aborted/dispatched, release/terminal의 단조 증가 sequence를 검증하고, 모든 event의 request/config correlation이 같으며 target은 initial/rebound binding 시점에 맞게 전환됨을 확인한다. cause/evidence length와 allowlist를 확인하고 raw prompt/output/tool/auth/preparer sentinel을 event와 encoded zap entry 모두에서 검색한다. 별도 failing sink subtest는 동일 response/terminal 결과와 ownership count를 증명한다.
+
+```go
+// Before: apps/edge/internal/openai/stream_gate_vertical_slice_test.go:893-895
+srv, dc := buildStreamGatePoolChatFixture(t, service, rawBody, 3)
+w, sink, runErr := runPoolChatStreamGate(t, srv, dc, newInjectedViolationRegistration(t, dc, "test.injected_violation", 1))
+
+// After: 실제 server runtime seam에 recorder를 설치하고 response와 observation을 함께 검증한다.
+recorder := newRecordingObservationSink()
+srv.SetObservationSink(recorder)
+w, releaseSink, runErr := runPoolChatStreamGate(t, srv, dc, registration)
+assertCorrelatedRecoveryTimeline(t, recorder.Snapshot(), service.Attempts())
+```
+
+- 수정 파일 및 체크리스트:
+  - [ ] `apps/edge/internal/openai/stream_gate_vertical_slice_test.go`: actual runtime timeline 및 sink failure isolation fixture 추가.
+  - [ ] `apps/edge/internal/openai/filter_observation_sink_test.go`: recorder/zap raw sentinel와 target field helper 보강.
+  - [ ] production seam이 fixture에 노출되지 않으면 기존 `apps/edge/internal/openai/stream_gate_runtime.go`/`server.go`의 package-private injection만 최소 조정하고 public API는 변경하지 않는다.
+- 테스트 작성: `TestStreamGateProviderPoolObservationCorrelationTimeline`, `TestStreamGateProviderPoolObservationSinkFailureIsolation`을 추가한다. normalized→tunnel 전환, exact attempt/target field, monotonic sequence, terminal outcome, bounded cause/evidence, raw sentinel 부재를 assertion한다.
+- 중간 검증: `go test -count=1 -v ./apps/edge/internal/openai -run '^TestStreamGateProviderPoolObservation(CorrelationTimeline|SinkFailureIsolation)$'`가 두 test를 정확히 실행하고 PASS해야 한다.
+
+### [CORR-FOLLOWUP-3] Trusted verification and fake full-cycle
+
+- 문제: archived review의 `-run 'FilterObservation|LogSafety'` 출력은 fresh selection과 다르고 `log_safety_test.go`에는 주장한 observation/preparer allowlist가 없다. local test rule이 요구하는 OpenAI full-cycle evidence도 없다.
+- 해결 방법: `log_safety_test.go`의 existing forbidden/allowlist oracle에 observation target과 preparer raw field 금지를 실제로 추가한다. `go test -list`와 anchored regex를 고정해 실행 test set을 먼저 증명한다. fake vLLM script의 임시 Edge config에서 stream evidence gate를 활성화하고, 정상 OpenAI response와 `streamgate_filter_observation` correlation/target fields 및 raw sentinel 부재를 모두 검사한다. 실제 stdout/stderr만 review stub에 붙인다.
+- 수정 파일 및 체크리스트:
+  - [ ] `apps/edge/internal/openai/log_safety_test.go`: observation/preparer raw forbidden fields와 closed allowlist oracle 추가.
+  - [ ] `scripts/e2e-openai-vllm.sh`: fake gate config, observation log capture/assertion, deterministic cleanup 추가.
+  - [ ] `CODE_REVIEW-cloud-G09.md`: 모든 고정 명령의 실제 stdout/stderr와 계획 대비 변경을 기록.
+- 테스트 작성: existing log-safety tests를 확장하며 별도 중복 test는 만들지 않는다. script는 secret 없는 fake mode를 회귀 oracle로 사용한다.
+- 중간 검증: `go test -count=1 -list 'FilterObservation|StreamGateProviderPoolObservation|Log(NoPreview|Retains)|NoPreview|CompileTime|LogOpenAI' ./apps/edge/internal/openai`가 의도한 test 이름을 열거하고, `IOP_VLLM_MODE=fake ./scripts/e2e-openai-vllm.sh`가 PASS해야 한다.
+
+## 의존 관계 및 구현 순서
+
+predecessor `23`, `24` PASS → CORR-FOLLOWUP-1 contract/rebinding → CORR-FOLLOWUP-2 Edge timeline → CORR-FOLLOWUP-3 trusted full-cycle evidence. 세 항목이 모두 PASS해야 `filter-observation`을 완료한다.
+
+## 수정 파일 요약
+
+| 파일 | 항목 |
+|---|---|
+| `packages/go/streamgate/filter_observation.go`, `parallel_evaluation.go` | CORR-FOLLOWUP-1 |
+| `packages/go/streamgate/runtime.go`, `runtime_test.go`, `recovery_coordinator.go`, `recovery_coordinator_test.go`, `filter_observation_test.go` | CORR-FOLLOWUP-1 |
+| `apps/edge/internal/openai/filter_observation_sink.go`, `filter_observation_sink_test.go`, `stream_gate_dispatcher.go` | CORR-FOLLOWUP-1, CORR-FOLLOWUP-2 |
+| `apps/edge/internal/openai/stream_gate_vertical_slice_test.go` | CORR-FOLLOWUP-2 |
+| `apps/edge/internal/openai/log_safety_test.go`, `scripts/e2e-openai-vllm.sh` | CORR-FOLLOWUP-3 |
+| `CODE_REVIEW-cloud-G09.md` | CORR-FOLLOWUP-3, mandatory evidence |
+
+## 리뷰어를 위한 체크포인트
+
+- 모든 observation이 같은 request correlation을 유지하면서 event 시점의 initial/rebound attempt id와 model group/model/provider/path를 정확히 기록하는가.
+- recovery target 전환, chronological sequence, bounded cause/evidence와 raw sentinel 부재가 실제 Edge runtime fixture에서 증명되는가.
+- sink 실패가 response/terminal/ownership 결과에 영향을 주지 않는가.
+- `-list`와 anchored command의 실제 selection/output이 문서와 일치하고 fake full-cycle이 gate 및 observation log를 실제로 검증하는가.
+
+## 최종 검증
+
+- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`
+- `go test -count=1 -list 'FilterObservation|StreamGateProviderPoolObservation|Log(NoPreview|Retains)|NoPreview|CompileTime|LogOpenAI' ./apps/edge/internal/openai`
+- `go test -count=1 -v ./apps/edge/internal/openai -run '^(TestFilterObservationSink_.*|TestStreamGateProviderPoolObservation(CorrelationTimeline|SinkFailureIsolation)|TestLogNoPreviewFields|TestLogRetainsNonContentMetadata|TestNoPreviewFieldsOnSensitiveValues|TestCompileTimeNoPreviewFields|TestLogOpenAICompatStreamOutputNoDeltaPreview)$'`
+- `go test -count=1 ./packages/go/streamgate ./packages/go/observability ./apps/edge/internal/openai`
+- `go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai`
+- `IOP_VLLM_MODE=fake ./scripts/e2e-openai-vllm.sh`
+- `test -z "$(gofmt -l packages/go/streamgate/filter_observation.go packages/go/streamgate/filter_observation_test.go packages/go/streamgate/parallel_evaluation.go packages/go/streamgate/recovery_coordinator.go packages/go/streamgate/recovery_coordinator_test.go packages/go/streamgate/runtime.go packages/go/streamgate/runtime_test.go apps/edge/internal/openai/filter_observation_sink.go apps/edge/internal/openai/filter_observation_sink_test.go apps/edge/internal/openai/stream_gate_dispatcher.go apps/edge/internal/openai/stream_gate_vertical_slice_test.go apps/edge/internal/openai/log_safety_test.go)"`
+- `git diff --check`
+
+모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.
+
+## 이 파일을 읽는 구현 에이전트에게
+
+구현/test/evidence만 소유하고 active pair를 유지한다. user-review 분류, control-plane 파일, archive, `complete.log`, roadmap, dispatcher-owned `WORK_LOG.md`는 official reviewer/runtime 소유다.
