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

# Plan - Edge FilterObservation compatibility, ordering, and evidence closure

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

구현·테스트·검증 evidence만 소유한다. 아래 명령을 실제로 실행하고 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션에 실제 변경과 stdout/stderr를 채운 뒤 active pair를 그대로 두고 리뷰 준비 완료를 보고한다. 막히면 정확한 blocker, 시도한 명령·출력, 재개 조건만 구현 evidence에 기록한다. 사용자 질문·user-input 도구·control-plane 파일·next-state 분류·log archive·`complete.log`·roadmap·dispatcher-owned `WORK_LOG.md`는 구현 에이전트가 다루지 않는다.

## 배경

두 번째 리뷰에서 observation target 검증이 기존 유효 식별자 계약보다 좁아 요청 의미를 바꿀 수 있고, Edge fixture가 lifecycle kind의 존재만 확인해 chronology를 증명하지 못하며, log-safety test가 실행하지 않은 `rg` 검증을 실행했다고 기록한 문제가 확인됐다. SDD S08의 raw-free chronological correlation을 기존 API/config 계약과 호환되게 닫고, 실행 evidence를 실제 명령과 일치시킨다.

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

- Latest plan: `agent-task/m-stream-evidence-gate-core/25+23,24_edge_observation_correlation/plan_cloud_G09_1.log`
- Latest review: `agent-task/m-stream-evidence-gate-core/25+23,24_edge_observation_correlation/code_review_cloud_G09_1.log`
- Verdict: `FAIL`; Required 3, Suggested 0, Nit 0.
- Required: 관측 target의 ASCII/256-byte 제한이 기존 유효 runtime 식별자를 거부한다. Edge integration fixture가 exact chronological order를 검사하지 않는다. 빈 `TestCompileTimeNoPreviewFields`가 실제로 실행하지 않은 `rg` 검증을 완료했다고 주장한다.
- Affected files: `packages/go/streamgate/filter_observation.go`, `packages/go/streamgate/filter_observation_test.go`, `apps/edge/internal/openai/stream_gate_vertical_slice_test.go`, `apps/edge/internal/openai/log_safety_test.go`.
- Fresh reviewer evidence: focused/package/race/fake full-cycle/gofmt/diff 검증은 PASS했고, 별도 reviewer `rg`에서는 production `_preview` zap field가 없었다. 다만 구현 review에 그 `rg` 명령이 없고 test log가 실행 사실과 모순되어 `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/skills/common/plan/templates/review-stub-template.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`.
- roadmap/설계/계약/spec: `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-contract/index.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-contract/outer/openai-compatible-api.md`, `agent-spec/index.md`, `agent-spec/runtime/provider-pool-config-refresh.md`, `agent-spec/input/openai-compatible-surface.md`.
- source/test: `packages/go/streamgate/filter_observation.go`, `packages/go/streamgate/filter_observation_test.go`, `packages/go/streamgate/runtime.go`, `packages/go/streamgate/recovery_plan.go`, `packages/go/config/provider_types.go`, `packages/go/config/validate.go`, `apps/edge/internal/openai/filter_observation_sink.go`, `apps/edge/internal/openai/filter_observation_sink_test.go`, `apps/edge/internal/openai/stream_gate_vertical_slice_test.go`, `apps/edge/internal/openai/log_safety_test.go`, `scripts/e2e-openai-vllm.sh`.
- 좁은 archive evidence: `agent-task/archive/2026/07/m-stream-evidence-gate-core/23+22_filter_observation_recovery/complete.log`, `agent-task/archive/2026/07/m-stream-evidence-gate-core/24+20,21_edge_observation_sink/complete.log`, latest plan/review log 두 개.

### SDD 기준

- `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`는 `[승인됨]`, SDD 잠금 해제이며 Milestone 구현 잠금도 해제되어 있다.
- 대상 Acceptance Scenario는 S08이고 Milestone Task id는 `filter-observation`이다. S08은 request/model과 actual run/provider/model/path를 연결하는 raw-free chronological record를 요구한다.
- Evidence Map의 `filter-observation` 행은 Core contract/unit, Edge sink/integration, log-safety/full-cycle 증거를 함께 요구한다. 따라서 checklist를 identifier-compatible Core contract, 실제 Edge ordered timeline, 신뢰 가능한 source/full-cycle oracle로 구성한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 매칭 profile `edge-smoke.md`, `platform-common-smoke.md`, `testing-smoke.md`를 모두 읽었다. Go fresh unit/package/race, Edge fake full-cycle, gofmt, `git diff --check`를 적용한다.
- 분석 환경은 repo root `/config/workspace/iop-s0`, branch `stream-evidence-gate-core`, 누적 milestone 변경으로 dirty다. `/config/.local/bin/go`는 `/config/opt/go/bin/go`, 버전은 `go1.26.2 linux/arm64`, GOROOT는 `/config/opt/go`다.
- `IOP_VLLM_MODE=fake`는 loopback fake provider와 현재 checkout의 Edge/Node를 사용하며 외부 secret/service가 필요 없다. Go test cache는 허용하지 않고 모두 `-count=1`을 사용한다.
- source oracle은 `rg --sort path`의 exact production glob/pattern을 PLAN에 고정한다. 출력이 없을 때만 PASS이며 review에는 실제 stdout/stderr를 기록한다. `<확인 필요>` 값은 없다.

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

- `TestObservationAttemptTarget_BoundedImmutableContract`는 공백을 invalid로 고정하고 Unicode/장문 유효 binding이 runtime 결과에 영향을 주지 않는지 검증하지 않는다.
- `assertProviderPoolObservationTimeline`은 sequence 숫자와 kind 존재만 보며 initial filter→recovery→new attempt filter→release→terminal의 ordered subsequence와 구간별 epoch/target을 검증하지 않는다.
- `TestCompileTimeNoPreviewFields`는 source를 검사하지 않는 빈 test이고 PLAN의 검증 명령에도 주장한 `rg`가 없다.
- 기존 package/race/fake full-cycle은 회귀 기반으로 유지되지만 위 세 oracle을 대신하지 못한다.

### 심볼 참조

- rename/remove 대상은 없다.
- `NewObservationAttemptTarget`의 production 호출은 `packages/go/streamgate/runtime.go:422,938`, 생성된 target 소비는 Core observation envelope와 Edge zap sink다. 새 dependency는 표준 라이브러리 `crypto/sha256`, `encoding/hex`뿐이며 manifest 변경은 없다.

### 분할 판단

- 단일 plan으로 유지한다. identifier normalization이 runtime observation construction을 안전하게 하고, 같은 target이 ordered Edge timeline과 log oracle에 나타나야 S08 correlation invariant가 독립적으로 PASS한다.
- 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 request/response API, config schema/validator, provider selection/recovery 정책, observation field allowlist는 변경하지 않는다. 기존 유효 식별자 계약을 좁히지 않는 observation-side normalization만 수정한다.
- runtime state machine과 sink production 코드는 현재 emission/allowlist가 맞으므로 변경하지 않는다. chronological 보강은 실제 production runtime을 구동하는 Edge fixture에 둔다.
- `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-policy.sh`, mode=`pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 true. 점수 `2,1,1,2,2`로 base `local-fit` G08이고, `review_rework_count=2` 또는 `evidence_integrity_failure=true`의 recovery boundary로 `cloud G08`, `PLAN-cloud-G08.md`다.
- review closures도 모두 true. 점수 `2,1,1,2,2`, route `official-review`, `cloud G08`, `CODE_REVIEW-cloud-G08.md`, adapter `codex`, model `gpt-5.6-sol`, reasoning `xhigh`다.
- `large_indivisible_context=false`; positive loop risks는 `temporal_state`, `boundary_contract` 2개이고 risk boundary는 false다. recovery boundary는 true다.
- capability-gap evidence는 없다.

## 구현 체크리스트

- [ ] CORR-COMPAT-1 기존 유효 model group/model/provider/path를 보존하거나 결정적으로 bounded-normalize하고 observation 때문에 request 결과가 바뀌지 않는 회귀 test를 추가한다.
- [ ] CORR-COMPAT-2 실제 Edge normalized→tunnel fixture에서 initial/recovery/release/terminal ordered subsequence와 구간별 attempt target/epoch을 검증한다.
- [ ] CORR-COMPAT-3 실행하지 않은 log-safety 주장을 제거하고 exact production `rg`, fresh unit/package/race/fake full-cycle evidence를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [CORR-COMPAT-1] Runtime-compatible bounded attempt target

- 문제: `packages/go/streamgate/filter_observation.go:33-47`은 ASCII allowlist와 256-byte 제한을 강제하지만 `packages/go/streamgate/recovery_plan.go:1278-1319`의 `AttemptBinding`과 `packages/go/config/provider_types.go:98-112,198-228`, `packages/go/config/validate.go:11-22`의 설정 계약은 non-empty만 요구한다. `packages/go/streamgate/runtime.go:938-940`은 이 관측 변환 오류를 request error로 반환한다.
- 해결 방법: 유효 UTF-8이며 256 bytes 이하인 non-empty 식별자는 공백·Unicode를 포함해 verbatim 보존한다. 장문 또는 invalid UTF-8은 `sha256:<64 lowercase hex>`로 결정적으로 normalize해 bounded valid string으로 만든다. 빈 값만 caller contract error로 유지하고, `Validate`는 constructor 결과의 non-empty/UTF-8/length invariant만 검사한다.

```go
// Before: packages/go/streamgate/filter_observation.go:33-47
func validateObservationAttemptTargetValue(name, value string) error {
    if value == "" { /* required error */ }
    if len(value) > maxObservationAttemptTargetValueBytes || !utf8.ValidString(value) { /* error */ }
    for _, r := range value {
        if /* ASCII identifier allowlist */ { continue }
        return errors.New("... unsupported character")
    }
    return nil
}

// After: compatible values stay exact; unsafe-to-log byte strings become a stable bounded join key.
func normalizeObservationAttemptTargetValue(name, value string) (string, error) {
    if value == "" { return "", errors.New("... required") }
    if utf8.ValidString(value) && len(value) <= maxObservationAttemptTargetValueBytes { return value, nil }
    sum := sha256.Sum256([]byte(value))
    return "sha256:" + hex.EncodeToString(sum[:]), nil
}
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/filter_observation.go`: compatible normalization과 validation을 구현하고 `crypto/sha256`, `encoding/hex` import를 추가한다.
  - [ ] `packages/go/streamgate/filter_observation_test.go`: 공백/Unicode verbatim, 장문/invalid UTF-8 deterministic bounded hash, empty rejection, immutability를 검증한다.
  - [ ] `packages/go/streamgate/filter_observation_test.go`: `TestRuntimeObservation_IdentifierCompatibilityDoesNotAffectRequestOutcome`에서 실제 `RequestRuntime.Run` 성공 terminal과 모든 observation의 normalized target correlation을 검증한다.
- 테스트 작성: 위 두 regression test를 작성한다. 특히 runtime test는 allowlist 밖이지만 `AttemptBinding`에 유효한 model/provider/path와 model group을 사용해 evaluation 경로까지 실행한다.
- 중간 검증: `go test -count=1 -v ./packages/go/streamgate -run '^(TestObservationAttemptTarget_.*|TestRuntimeObservation_IdentifierCompatibilityDoesNotAffectRequestOutcome)$'`가 대상 test를 정확히 실행하고 PASS해야 한다.

### [CORR-COMPAT-2] Exact Edge recovery timeline order

- 문제: `apps/edge/internal/openai/stream_gate_vertical_slice_test.go:897-963`의 map 기반 assertion은 각 kind가 한 번 존재하는지만 확인한다. recovery와 terminal의 상대 순서가 뒤집혀도 통과하며 S08 chronology를 증명하지 못한다.
- 해결 방법: 실제 normalized→tunnel fixture의 관측에서 `response_staged → filter_evaluation_started → filter_evaluated → arbitration_decided → recovery_plan_selected → recovery_attempt_aborted → recovery_rebuilt → recovery_dispatched → response_staged → filter_evaluation_started → filter_evaluated → arbitration_decided → release_committed → terminal_committed` ordered subsequence를 앞에서부터 소비한다. 각 matched row의 sequence, initial/rebound attempt id/target, recovery 전후 epoch consistency, terminal commit state를 함께 검사한다. 추가 filter row가 있더라도 요구 subsequence의 상대 순서는 반드시 유지한다.

```go
// Before: apps/edge/internal/openai/stream_gate_vertical_slice_test.go:911-962
required := map[streamgate.ObservationKind]bool{/* kinds */}
for _, obs := range observations { required[obs.Kind()] = true }
for kind, seen := range required { if !seen { t.Errorf(...) } }

// After: 같은 kind의 두 attempt 구간을 구분하는 ordered row oracle.
for _, want := range expectedTimelineRows {
    idx = nextMatchingObservation(observations, idx, want)
    assertAttemptTargetEpoch(t, observations[idx], want)
    idx++
}
```

- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/stream_gate_vertical_slice_test.go`: map assertion을 ordered subsequence row oracle로 교체한다.
  - [ ] 동일 actual runtime fixture에서 pre-dispatch는 initial target, `recovery_dispatched`부터 rebound target이며 두 evaluation/arbitration epoch group이 내부적으로 일치함을 검증한다.
  - [ ] sink failure isolation test도 같은 ordered oracle을 재사용해 실패 sink에서도 chronology와 response/terminal 결과가 유지됨을 고정한다.
- 테스트 작성: 기존 `TestStreamGateProviderPoolObservationCorrelationTimeline`, `TestStreamGateProviderPoolObservationSinkFailureIsolation`을 보강하며 synthetic observation-only test는 추가하지 않는다.
- 중간 검증: `go test -count=1 -v ./apps/edge/internal/openai -run '^TestStreamGateProviderPoolObservation(CorrelationTimeline|SinkFailureIsolation)$'`가 두 actual runtime fixture를 실행하고 PASS해야 한다.

### [CORR-COMPAT-3] Executed log-safety and full-cycle evidence

- 문제: `apps/edge/internal/openai/log_safety_test.go:337-343`은 source 검사를 하지 않으면서 `rg` 검증 완료를 log하고, archived plan/review에는 그 명령이 없다.
- 해결 방법: misleading empty `TestCompileTimeNoPreviewFields`와 실행하지 않은 claim을 제거한다. production Go source의 `_preview` zap field를 검사하는 exact `rg --sort path` 명령을 중간/최종 검증에 포함한다. 기존 runtime log observer tests와 fake OpenAI full-cycle을 fresh 실행하고 실제 출력만 review에 기록한다.
- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/log_safety_test.go`: 빈 compile-time test/comment를 제거하고 existing behavioral log tests는 유지한다.
  - [ ] `CODE_REVIEW-cloud-G08.md`: source oracle을 포함한 모든 고정 명령의 실제 stdout/stderr를 기록한다.
- 테스트 작성: 새 중복 Go test는 작성하지 않는다. production source를 직접 검사하는 아래 `rg`가 static regression oracle이고 기존 observer tests가 runtime field oracle이다.
- 중간 검증: `test -z "$(rg --sort path -n 'zap\.(String|ByteString|Reflect|Any)\("[^"[:space:]]*_preview"' apps/edge/internal/openai --glob '*.go' --glob '!*_test.go')"`가 exit 0/no output이고, `IOP_VLLM_MODE=fake ./scripts/e2e-openai-vllm.sh`가 PASS해야 한다.

## 의존 관계 및 구현 순서

predecessor `23`, `24` PASS → CORR-COMPAT-1 compatibility contract → CORR-COMPAT-2 actual ordered timeline → CORR-COMPAT-3 trusted evidence. 세 항목이 모두 PASS해야 `filter-observation`을 완료한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/go/streamgate/filter_observation.go` | CORR-COMPAT-1 |
| `packages/go/streamgate/filter_observation_test.go` | CORR-COMPAT-1 |
| `apps/edge/internal/openai/stream_gate_vertical_slice_test.go` | CORR-COMPAT-2 |
| `apps/edge/internal/openai/log_safety_test.go` | CORR-COMPAT-3 |
| `CODE_REVIEW-cloud-G08.md` | mandatory evidence |

## 리뷰어를 위한 체크포인트

- 기존 계약상 유효한 공백/Unicode/장문 식별자가 observation 때문에 request를 실패시키지 않고 bounded deterministic target correlation으로 남는가.
- actual Edge normalized→tunnel recovery의 두 attempt 구간, recovery lifecycle, release/terminal이 ordered subsequence로 검증되는가.
- raw-free allowlist와 sink failure isolation이 유지되는가.
- review의 source oracle과 test stdout이 실제 실행 명령·selection과 정확히 일치하는가.

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`
- `go test -count=1 -list 'ObservationAttemptTarget|RuntimeObservation_IdentifierCompatibility|StreamGateProviderPoolObservation|Log(NoPreview|Retains)|NoPreview|LogOpenAI' ./packages/go/streamgate ./apps/edge/internal/openai`
- `test -z "$(rg --sort path -n 'zap\.(String|ByteString|Reflect|Any)\("[^"[:space:]]*_preview"' apps/edge/internal/openai --glob '*.go' --glob '!*_test.go')"`
- `go test -count=1 -v ./packages/go/streamgate -run '^(TestObservationAttemptTarget_.*|TestRuntimeObservation_IdentifierCompatibilityDoesNotAffectRequestOutcome)$'`
- `go test -count=1 -v ./apps/edge/internal/openai -run '^(TestFilterObservationSink_.*|TestStreamGateProviderPoolObservation(CorrelationTimeline|SinkFailureIsolation)|TestLogNoPreviewFields|TestLogRetainsNonContentMetadata|TestNoPreviewFieldsOnSensitiveValues|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 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`의 구현 에이전트 소유 섹션을 채운다.
