<!-- task=m-stream-evidence-gate-core/24+20,21_edge_observation_sink plan=3 tag=REVIEW_EDGE_FILTER_OBSERVATION_SINK -->

# Plan - Edge FilterObservation allowlist 회귀 검증 보강

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

이 계획은 구현·테스트·증거 작성만 소유한다. 아래 검증을 실행하고 실제 stdout/stderr, 설계 결정, 계획 대비 변경을 `CODE_REVIEW-cloud-G03.md`의 구현 에이전트 소유 섹션에 채운 뒤 active 파일을 그대로 두고 review ready를 보고한다. 차단되면 정확한 차단 원인, 시도한 명령과 출력, 재개 조건만 구현 증거에 기록한다. 사용자에게 질문하거나 user-input 도구·control-plane stop 파일을 사용하거나 다음 상태를 분류하지 않는다. 판정, log archive, `complete.log`, task archive는 code-review 스킬 전용이다.

## 배경

zap sink production mapping과 fresh 명령 실행은 정상이나 기존 fixture가 closed allowlist의 exact key/value를 고정하지 않아 잘못된 추가 field나 cause 배열 오매핑을 놓친다. raw sentinel 부재 검사도 sentinel을 입력에 넣지 않아 회귀 증거가 되지 않는다. Production 코드는 바꾸지 않고 test oracle과 실행 evidence만 보강한다.

## Archive Evidence Snapshot

- 현재 pair archive: `agent-task/m-stream-evidence-gate-core/24+20,21_edge_observation_sink/plan_cloud_G03_2.log`, `agent-task/m-stream-evidence-gate-core/24+20,21_edge_observation_sink/code_review_cloud_G03_2.log`.
- 판정: FAIL; Required=1, Suggested=0, Nit=0.
- Required: `TestFilterObservationSink_MapsStableAllowlist`가 expected key 존재만 확인하고 대부분의 value, 4-entry cause order, `terminal_reason`을 검증하지 않으며 `OmitsRawFieldsAndSentinels`는 sentinel을 fixture에 주입하지 않는다.
- 실제 reviewer evidence: host Go `/config/opt/go/bin/go`, `go1.26.2 linux/arm64`; 네 named test, affected package, focused race, gofmt, `git diff --check`는 fresh PASS했다. 실패 원인은 실행 여부가 아니라 필수 assertion 공백이다.
- 선행 완료: `agent-task/archive/2026/07/m-stream-evidence-gate-core/20+17,19_edge_vertical_slice/complete.log`, `agent-task/archive/2026/07/m-stream-evidence-gate-core/21+14,19_filter_observation_contract/complete.log`.
- Roadmap carryover: SDD S08의 raw-free sink mapping 일부만 보강하므로 이 child는 Milestone `filter-observation` Task 완료를 주장하지 않는다. Full request timeline과 Roadmap Completion은 child `25`가 소유한다.

## 분석 결과

### 읽은 파일

- `apps/edge/internal/openai/filter_observation_sink.go` — production zap field mapping과 write-error 경계.
- `apps/edge/internal/openai/filter_observation_sink_test.go` — 현재 존재-only/value 부분 검증과 vacuous sentinel fixture.
- `apps/edge/internal/openai/log_safety_test.go` — 기존 OpenAI log raw/preview safety assertion.
- `packages/go/streamgate/filter_observation.go` — immutable observation accessor, validation, sink/sequencer failure isolation.
- `packages/go/streamgate/filter_contract.go` — `FixedFingerprint`, `SanitizedEvidence` accessor/validation.
- `packages/go/streamgate/terminal.go` — bounded `FailureCauseChain` 순서와 최대 4개 계약.
- `packages/go/observability/observability.go` — production zap core construction.
- `agent-ops/rules/project/domain/edge/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/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`.
- `agent-spec/input/openai-compatible-surface.md`, `agent-contract/outer/openai-compatible-api.md` — public API와 raw-secret boundary 불변.
- `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`.

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`; 상태 `[승인됨]`, 잠금 `해제`.
- Target: S08 / Milestone Task `filter-observation`.
- Evidence Map: stable correlation/config/attempt/epoch/lifecycle fields와 bounded cause/evidence만 기록하고 raw prompt/output/tool args/auth는 없어야 한다.
- 이번 follow-up은 S08 sink 단위 증거의 exact key/value, bounded cause order, raw-context 비관찰을 고정한다. Full timeline은 범위 밖이므로 `Roadmap Targets`를 두지 않는다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`를 읽고 host Go path/source를 먼저 확인한다.
- `edge-smoke.md`, `platform-common-smoke.md`를 읽었으나 이번 변경은 test-only이고 registry/transport/config/proto/runtime injection을 바꾸지 않아 smoke/full-cycle/make proto는 적용하지 않는다.
- fresh `-count=1` focused/package/race, test-list, gofmt, diff를 사용한다. 외부 runner/service/device preflight는 적용하지 않는다.

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

- actual all-fields key set을 expected set과 exact 비교하지 않아 새 unexpected key를 탐지하지 못한다.
- sequence/fingerprint 외 base/optional/cause/evidence 값, cause 4개 순서, terminal reason branch를 검증하지 않는다.
- raw sentinel 문자열이 observation context 어디에도 주입되지 않아 현재 부재 검사는 항상 통과한다.
- direct zap write failure와 sequencer isolation은 기존 두 fixture가 충분히 검증하므로 유지한다.

### 심볼 참조

- renamed/removed symbol 없음. Production symbol과 call site는 변경하지 않는다.

### 분할 판단

- 기존 dependent subtask `24+20,21_edge_observation_sink`를 유지한다. predecessor `20`, `21`은 위 archived `complete.log`로 충족됐다.
- 한 test file의 closed allowlist oracle 보강은 독립 PASS 가능한 compact test-only boundary이므로 추가 split하지 않는다.

### 범위 결정 근거

- 포함: `filter_observation_sink_test.go`의 exact key/value, terminal/cause bound, context sentinel assertion과 review evidence.
- 제외: `filter_observation_sink.go`, runtime injection/correlation, public OpenAI field, metric, retention/storage, config/proto, unrelated log policy. Reviewer 확인상 production mapping 결함은 없다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`; finalizer=`finalize-task-policy.sh`, mode=`pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 true; capability gap 없음.
- build scores=`1/0/0/1/1` → G03, base=`local-fit`; `large_indivisible_context=false`; positive loop-risk=`variant_product`(1).
- recovery signals: `review_rework_count=2`, `evidence_integrity_failure=false`; recovery boundary matched.
- build route=`recovery-boundary`, cloud, G03, `PLAN-cloud-G03.md`.
- review closures 모두 true; scores=`1/0/0/1/1` → cloud G03, `official-review`, `CODE_REVIEW-cloud-G03.md`.

## 구현 체크리스트

- [ ] OBS-EDGE-3 all-fields/base/terminal fixture가 exact key set과 모든 mapped value, 4-entry cause order/bound, evidence 값, raw-context sentinel 부재를 검증하도록 보강한다.
- [ ] OBS-EDGE-4 host Go 확인과 fresh test-list/unit/package/race/gofmt/diff 검증을 수행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [OBS-EDGE-3] closed allowlist test oracle 보강

문제: `apps/edge/internal/openai/filter_observation_sink_test.go:125-219`는 expected key 존재만 검사하고 `:265-337`은 sentinel을 입력하지 않아 잘못된 추가 field/value mapping도 PASS한다.

해결 방법:

Before (`apps/edge/internal/openai/filter_observation_sink_test.go:205`):

```go
for _, k := range expectedKeys {
	if _, ok := fieldMap[k]; !ok {
		t.Errorf("missing expected field key: %s", k)
	}
}
```

After:

```go
assertExactObservationFields(t, entry, expectedFields)
```

- all-fields fixture는 cause 4개를 서로 다른 stage/code/consumer/filter/rule 값으로 만들고 base/attribution/hold/decision/arbitration/recovery/preparer/evidence의 모든 key와 value를 exact 비교한다.
- terminal fixture를 별도로 emit해 `terminal_reason` 존재/value와 causes 부재를 확인한다. base fixture는 base key만 exact 비교한다.
- custom typed context에 prompt/output/tool/auth/preparer sentinel을 넣어 `Emit`에 전달하고 JSON 및 exact key set에 sentinel/forbidden key가 없음을 확인한다.
- 기존 direct core write failure와 sequencer isolation fixture는 유지한다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/openai/filter_observation_sink_test.go`에 deterministic exact-map helper와 필요한 표준 라이브러리 import를 추가한다.
- [ ] all-fields/base/terminal key set과 모든 value를 비교하고 cause arrays 4개 입력 순서를 고정한다.
- [ ] raw context sentinel을 실제 `context.Context`에 주입하고 log JSON에 forbidden key/value가 없음을 확인한다.
- [ ] production `filter_observation_sink.go`는 변경하지 않는다.

테스트 작성: 기존 `TestFilterObservationSink_MapsStableAllowlist`와 `TestFilterObservationSink_OmitsRawFieldsAndSentinels`를 강화한다. 새 top-level test name은 필요 없으며 anchored 네-test 목록을 유지한다.

중간 검증:

- `go test -count=1 -v ./apps/edge/internal/openai -run '^TestFilterObservationSink_(MapsStableAllowlist|OmitsRawFieldsAndSentinels)$'` — exact mapping과 raw-context fixture가 실행돼 PASS해야 한다.

### [OBS-EDGE-4] fresh verification evidence

문제: assertion 변경 뒤 기존 성공 출력은 새 test oracle의 실행 근거가 아니다.

해결 방법: 아래 최종 검증을 현재 checkout에서 다시 실행하고 실제 stdout/stderr를 review stub에 기록한다. test cache는 허용하지 않는다.

수정 파일 및 체크리스트:

- [ ] `CODE_REVIEW-cloud-G03.md`의 완료 표, 체크리스트, 계획 대비 변경, 설계 결정, 검증 출력을 실제 결과로 채운다.

테스트 작성: 추가 test item 없음. OBS-EDGE-3 강화 fixture와 affected package/race를 fresh 실행한다.

중간 검증:

- `test -z "$(gofmt -l apps/edge/internal/openai/filter_observation_sink_test.go)"` — 출력 없이 성공해야 한다.

## 의존 관계 및 구현 순서

`20+17,19_edge_vertical_slice` PASS와 `21+14,19_filter_observation_contract` PASS → OBS-EDGE-3 → OBS-EDGE-4. Directory dependency에 없는 `23`/`25`를 선행 조건으로 추가하지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
