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

# Plan - Edge FilterObservation zap sink 복구

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

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

## 배경

이전 루프는 계획된 Edge observation sink와 테스트를 만들지 않아 FAIL했다. Core `FilterObservation`의 raw-free immutable envelope를 기존 zap 경계에 stable allowlist로 기록하는 stateless adapter와, 실제 테스트가 실행됐음을 증명하는 고유 test name을 이번 packet에서 완성한다. Runtime injection과 request/attempt rebinding timeline은 의존 child `25+23,24_edge_observation_correlation`이 소유한다.

## Archive Evidence Snapshot

- 현재 pair archive: `agent-task/m-stream-evidence-gate-core/24+20,21_edge_observation_sink/plan_local_G07_1.log`, `agent-task/m-stream-evidence-gate-core/24+20,21_edge_observation_sink/code_review_cloud_G07_1.log`
- 판정: FAIL; Required=2, Suggested=0, Nit=0.
- Required: 계획된 `apps/edge/internal/openai/filter_observation_sink.go`와 `filter_observation_sink_test.go` 및 관련 심볼이 모두 없었다.
- Required: 구현 체크·실제 검증 출력이 비었고 `go test -count=1 -v ./apps/edge/internal/openai -run 'FilterObservation|LogSafety'`는 `testing: warning: no tests to run`으로 종료됐다.
- 실제 reviewer evidence: host Go는 `/config/opt/go/bin/go`, `go1.26.2 linux/arm64`; baseline package tests는 PASS했지만 `TestFilterObservationSink_*` 목록은 0개였다.
- 선행 완료: `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 timeline sink mapping 일부를 구현하지만 이 child만으로 Milestone `filter-observation` Task 완료를 주장하지 않는다. Full correlation/timeline evidence와 Roadmap Completion은 child `25`가 소유한다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/filter_observation.go` — closed observation kinds, immutable accessors, `ObservationSink.Emit`, sequencer failure isolation.
- `packages/go/streamgate/filter_observation_test.go` — raw-free carrier, cause/evidence, lifecycle, sink failure fixtures.
- `packages/go/streamgate/filter_contract.go` — `FixedFingerprint`와 `SanitizedEvidence` public accessor 구간.
- `packages/go/streamgate/terminal.go` — bounded `FailureCauseChain` public accessor 구간.
- `packages/go/observability/observability.go` — existing zap production core construction.
- `apps/edge/internal/openai/log_safety_test.go` — existing raw/preview field safety assertions.
- `go.mod`, `go.sum` — `go.uber.org/zap v1.27.0` existing dependency.
- `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-spec/input/openai-compatible-surface.md`, `agent-contract/outer/openai-compatible-api.md` — public API/raw-secret boundary 불변 확인.

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `[승인됨]`, 잠금 `해제`.
- Target scenario: S08 / Milestone Task `filter-observation`.
- Evidence Map: multi-consumer observation timeline은 stable ids와 bounded cause/evidence만 기록하고 raw output/prompt/tool args/auth는 없어야 한다.
- 이 child는 S08의 zap allowlist adapter와 raw-absence/sink-failure unit evidence만 만든다. S08의 request/run/provider/model full timeline은 child `25`에 남으므로 `Roadmap Targets`를 두지 않는다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`를 읽었다.
- host `PATH`의 첫 Go와 `GOROOT` 일치 여부를 먼저 기록하고 모든 Go 검증은 fresh `-count=1`을 사용한다.
- 새 adapter는 아직 runtime/user path에 injection되지 않으므로 Edge-Node full-cycle·mock smoke·live provider는 이 child에 적용하지 않는다. child `25`의 integration 검증이 그 경계를 소유한다.
- proto/config 변경이 없으므로 `make proto`와 config refresh 검증은 적용하지 않는다.

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

- Core tests는 immutable envelope와 sequencer sink-failure isolation을 검증하지만 Edge zap field mapping을 검증하지 않는다.
- Edge에는 `TestFilterObservationSink_*`가 하나도 없어 기존 focused 명령이 zero-test PASS했다.
- 새 tests가 base/optional field allowlist, four-entry cause chain, evidence fingerprint/count/offset, forbidden key/value absence, zap core write failure와 sequencer isolation을 직접 검증해야 한다.

### 심볼 참조

- renamed/removed symbol: 없음.
- 신규 symbol: `zapFilterObservationSink`, `newZapFilterObservationSink`; runtime call site는 이 child에서 추가하지 않는다.

### 분할 판단

- 선택된 dependent subtask는 `24+20,21_edge_observation_sink`; predecessor `20`과 `21`은 위 archived `complete.log`로 모두 충족됐다.
- adapter+mapping tests는 stateless stable contract로 독립 PASS 가능하다. request/attempt rebinding과 full timeline은 `25+23,24_edge_observation_correlation`의 별도 correctness 경계다.

### 범위 결정 근거

- 포함: 신규 zap observation sink와 신규 focused test 두 파일.
- 제외: `server.go`, runtime factory, dispatch/rebinding callsite, `stream_gate_runtime.go`, public OpenAI response, metrics, storage/retention, config/proto, unrelated legacy log content policy. 이는 child `25` 또는 별도 기존 경계의 책임이다.

### 최종 라우팅

- 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=`boundary_contract,variant_product`(2).
- recovery signals: `review_rework_count=1`, `evidence_integrity_failure=true`; 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-1 `streamgate.ObservationSink`를 구현하는 zap adapter와 closed stable field allowlist를 추가한다.
- [ ] OBS-EDGE-3 base/optional/cause/evidence mapping, raw key/value 부재, zap write failure와 sequencer isolation을 검증하는 `TestFilterObservationSink_*` fixtures를 추가한다.
- [ ] OBS-EDGE-4 host Go 확인, fresh package/unit/race/test-list/log-safety/gofmt/diff 검증을 수행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [OBS-EDGE-1] zap ObservationSink adapter

문제: `packages/go/streamgate/filter_observation.go:896`은 `ObservationSink.Emit` 경계를 제공하지만, 이전 계획 `plan_local_G07_1.log:30`의 Edge 구현 파일이 생성되지 않았다.

해결 방법:

Before (`packages/go/streamgate/filter_observation.go:896`):

```go
type ObservationSink interface {
	Emit(ctx context.Context, obs FilterObservation) error
}
```

After (`apps/edge/internal/openai/filter_observation_sink.go`):

```go
type zapFilterObservationSink struct { core zapcore.Core }

func newZapFilterObservationSink(logger *zap.Logger) streamgate.ObservationSink
func (s *zapFilterObservationSink) Emit(context.Context, streamgate.FilterObservation) error
```

- nil logger는 `zap.NewNop()` core로 정규화한다.
- `Emit`은 `obs.Validate()` 후 `zapcore.Entry{Level: InfoLevel, Time: obs.OccurredAt(), Message: "streamgate_filter_observation"}`를 기록한다. disabled core는 nil, enabled core는 `Core.Write` 오류를 그대로 반환해 sequencer가 격리할 수 있게 한다.
- base allowlist: `sequence`, `observation_kind`, `correlation_id`, `config_generation`, `attempt_id`, `epoch_id`, `commit_state`.
- optional allowlist: attribution(`consumer_id`, `filter_id`, `rule_id`), hold(`hold_mode`, `event_kind`, `effective_runes`, `pending_runes`), decision(`filter_outcome`, `decision_kind`, `enforcement`, `failure_disposition`), arbitration(`arbitration_action`, `base_disposition`), `terminal_reason`, recovery(`plan_id`, `recovery_strategy`, `resume_mode`, `shared_attempt_id`), preparer(`preparer_id`, `preparer_status`, `preparer_deadline_outcome`).
- cause는 `cause_count`와 순서가 같은 `cause_stages`, `cause_codes`, `cause_consumers`, `cause_filters`, `cause_rule_ids` 배열로 손실 없이 기록한다.
- evidence는 `evidence_event_kind`, `evidence_channel`, `evidence_filter_rule`, `evidence_outcome`, `evidence_descriptor_code`, lowercase hex `evidence_fingerprint`, `evidence_count`, `evidence_offset`, `evidence_timestamp`만 기록한다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/openai/filter_observation_sink.go` 신규 생성.
- [ ] interface compile assertion과 stable message constant를 둔다.
- [ ] raw prompt/output/tool args/result/auth/preparer input/output와 error text를 받는 field/helper를 만들지 않는다.

테스트 작성: OBS-EDGE-3에서 new adapter의 모든 optional branch와 failure boundary를 새 unit tests로 검증한다.

중간 검증:

- `go test -count=1 ./apps/edge/internal/openai -run '^TestFilterObservationSink_'` — 하나 이상의 named test가 실행되고 PASS해야 한다.

### [OBS-EDGE-3] allowlist와 failure fixtures

문제: 이전 계획 `plan_local_G07_1.log:34`가 요구한 test 파일이 없고, reviewer의 focused 실행은 `[no tests to run]`이었다.

해결 방법:

Before (`code_review_cloud_G07_1.log:86`):

```text
go test -count=1 -v ./apps/edge/internal/openai -run 'FilterObservation|LogSafety'
testing: warning: no tests to run
```

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

```go
func TestFilterObservationSink_MapsStableAllowlist(t *testing.T)
func TestFilterObservationSink_OmitsRawFieldsAndSentinels(t *testing.T)
func TestFilterObservationSink_PropagatesCoreWriteFailure(t *testing.T)
func TestFilterObservationSink_SequencerIsolatesCoreWriteFailure(t *testing.T)
```

- public streamgate constructors와 capture sink로 valid all-fields observation을 만든다.
- observed context key set을 exact allowlist와 비교하고 cause array 순서·4개 bound, evidence hex/count/offset/timestamp를 확인한다.
- JSON/observer output에서 forbidden key fragments와 raw prompt/output/tool/auth/preparer sentinel 문자열이 없음을 확인한다.
- failing `zapcore.WriteSyncer`로 direct adapter `Emit` 오류와 `ObservationSequencer.Emit` 성공/sequence 진행을 각각 검증한다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/openai/filter_observation_sink_test.go` 신규 생성.
- [ ] test names를 `^TestFilterObservationSink_` prefix로 고정해 zero-test PASS를 방지한다.
- [ ] production field list와 test expected allowlist가 하나의 closed mapping을 검증하도록 한다.

테스트 작성: 필수. 위 네 regression tests와 필요한 local fixture만 새 파일에 둔다.

중간 검증:

- `go test ./apps/edge/internal/openai -list '^TestFilterObservationSink_'` — 네 test name이 출력돼야 한다.
- `go test -race -count=1 ./apps/edge/internal/openai -run '^TestFilterObservationSink_'` — race detector에서 PASS해야 한다.

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

문제: `code_review_cloud_G07_1.log:83`~`89`의 구현 검증 출력이 비어 있어 artifact와 실행 증거가 일치하지 않았다.

해결 방법: 아래 최종 검증을 정확히 실행하고 stdout/stderr를 `CODE_REVIEW-cloud-G03.md`에 원문으로 기록한다. 명령 변경 시 대체 명령과 이유를 `계획 대비 변경 사항`에 먼저 남긴다.

수정 파일 및 체크리스트:

- [ ] `CODE_REVIEW-cloud-G03.md`의 구현 완료 표·체크리스트·설계 결정·검증 출력을 실제 결과로 채운다.

테스트 작성: 추가 test item 없음. OBS-EDGE-3 fixtures를 fresh unit/race와 full affected-package 회귀로 실행한다.

중간 검증:

- `test -z "$(gofmt -l apps/edge/internal/openai/filter_observation_sink.go 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-1 → OBS-EDGE-3 → OBS-EDGE-4. Directory dependency에 없는 `23`/`25`를 이 child의 선행 조건으로 추가하지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `apps/edge/internal/openai/filter_observation_sink.go` | OBS-EDGE-1 |
| `apps/edge/internal/openai/filter_observation_sink_test.go` | OBS-EDGE-3 |
| `agent-task/m-stream-evidence-gate-core/24+20,21_edge_observation_sink/CODE_REVIEW-cloud-G03.md` | OBS-EDGE-4 evidence |

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT` — host Go path/source가 일치해야 한다.
- `go test ./apps/edge/internal/openai -list '^TestFilterObservationSink_'` — 네 named test가 출력돼야 한다.
- `go test -count=1 -v ./apps/edge/internal/openai -run '^TestFilterObservationSink_'` — adapter fixtures가 실제 실행돼 PASS해야 한다.
- `go test -count=1 ./packages/go/streamgate ./packages/go/observability ./apps/edge/internal/openai` — affected package 회귀가 PASS해야 한다.
- `go test -race -count=1 ./apps/edge/internal/openai -run '^TestFilterObservationSink_'` — focused race 검증이 PASS해야 한다.
- `test -z "$(gofmt -l apps/edge/internal/openai/filter_observation_sink.go apps/edge/internal/openai/filter_observation_sink_test.go)"` — gofmt drift가 없어야 한다.
- `git diff --check` — whitespace 오류 없이 성공해야 한다.

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