<!-- task=m-stream-evidence-gate-core/01_event_contract_types plan=1 tag=REVIEW_API -->

# Code Review Reference - REVIEW_API

> **[IMPLEMENTATION WORKER]**
> Complete the implementation checklist, fill the implementation evidence below, report `review-ready`, and stop.
> Edit only completion checkboxes, `계획 대비 변경 사항`, `주요 설계 결정`, `사용자 리뷰 요청`, Agent UI implementation evidence when present, and command output blocks.
> Do not edit role identities, review, archive, create terminal files, or start another agent. Runtime and the later Review model own those actions.
> Do not ask the user directly. Use `사용자 리뷰 요청` only for a selected Milestone `구현 잠금 > 결정 필요`; record other blockers in the implementation evidence.

## 개요

date=2026-07-23
task=m-stream-evidence-gate-core/01_event_contract_types, plan=1, tag=REVIEW_API

## 역할 모델

- Plan: `GPT-5`
- Implementation: `Claude-opus-4-6`
- Review: `pending`

> Runtime-only ledger. Runtime requires three distinct model identities; the implementation worker does not edit it.


## Archive Evidence Snapshot

- 이전 pair:
  - `agent-task/m-stream-evidence-gate-core/01_event_contract_types/plan_local_G04_0.log`
  - `agent-task/m-stream-evidence-gate-core/01_event_contract_types/code_review_local_G05_0.log`
- 판정: `FAIL`
- Required:
  - event kind/base disposition/commit state 상수가 SDD와 다르고 unknown 및 kind-disposition 불일치를 거부하지 않는다.
  - response-start status와 kind별 typed payload가 없으며 공개 disposition과 header map이 계약을 우회하거나 변이시킨다.
  - `FilterDecision`/`FilterOutcome` 의미가 뒤바뀌고 필수 variant와 identity/evidence/intent 필드가 빠졌다.
  - recovery strategy/directive/reason이 임의 문자열이라 strategy별 typed directive와 raw-free evidence 경계가 없다.
  - `EvidenceBatch`가 response-start header를 얕게 복사하고 committed look-behind를 제공하지 않는다.
  - arbitrary-length/invalid cause와 공개 cause slice 때문에 최대 4단계 불변조건이 깨진다.
  - external descriptor와 terminal success/error 상호 배제가 SDD의 단일 안전 오류 직렬화 경계를 표현하지 못한다.
- Suggested: 없음
- Nit: 없음
- 영향 파일: `packages/go/streamgate/event.go`, `packages/go/streamgate/filter_contract.go`, `packages/go/streamgate/terminal.go`
- 기존 검증 evidence: formatter/package compile/Edge·Node transport 회귀는 PASS했으나 package에 test 파일이 없었고, untracked package에 대한 `git diff --check`는 whitespace 증거로 신뢰할 수 없었다.
- Roadmap carryover: 이 sibling은 `event-contract`의 부분 구현이다. 직접 단위 테스트는 `03+01_event_contract_unit_tests`, S09/S21 fake codec·host·import-boundary closure와 최종 Milestone Task 완료 근거는 `02+01+03_codec_contract_fixtures`가 소유한다.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| REVIEW_API-1 Exact event lifecycle과 typed payload | [x] |
| REVIEW_API-2 Filter result 모델과 immutable evidence | [x] |
| REVIEW_API-3 Bounded cause와 strict terminal result | [x] |

## 구현 체크리스트

- [x] `REVIEW_API-1` event kind/base disposition/commit state를 exact SDD 값으로 교정하고 response-start status 및 kind별 typed payload를 immutable constructor/accessor와 validation으로 고정한다.
- [x] `REVIEW_API-2` filter decision/outcome, typed recovery strategy/directive, bounded sanitized evidence, pending/look-behind `EvidenceBatch`를 SDD 모델로 교정하고 모든 mutable 경계를 deep-copy한다.
- [x] `REVIEW_API-3` failure cause chain과 external terminal descriptor/result를 private bounded storage, 모든 경계 validation, strict success/error 상호 배제로 교정한다.
- [x] 계획에 명시한 local 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 채우고 review-ready를 보고한 뒤 현재 실행을 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.


## 계획 대비 변경 사항

- `event.go`: `EventKindToolResult`, `EventKindTerminalSuccess`, `EventKindTerminalHostError` 제거. `EventKindResponseStart`, `EventKindTextDelta`, `EventKindReasoningDelta`, `EventKindToolCallFragment`, `EventKindTerminal`, `EventKindProviderError`로 SDD exact 값 사용. `BaseDispositionStaged/Committed/Abandoned` 제거하고 `BaseDispositionReleaseCandidate/TerminalSuccessCandidate/TerminalErrorCandidate`로 교체. `BaseDispositionOf` 반환값을 `(BaseEventDisposition, error)`으로 변경하여 unknown kind 거부. `ResponseStart`에 status code 추가, headers를 private storage로 변경. `NormalizedEvent`에서 generic `Payload string` 제거하고 kind별 typed 필드를 private으로 추가. `Disposition`을 public 필드에서 constructor 내부 설정으로 변경. `ReleaseEvent`도 kind별 typed payload로 교체.
- `filter_contract.go`: `FilterDecision`/`FilterOutcome`을 string alias에서 struct로 변경. `FilterDecisionKind`(pass/observe/violation/fatal/replacement)와 `FilterOutcomeKind`(evaluated/evaluation_error/not_applicable_for_epoch/deferred_by_requirement) 추가. `RecoveryIntent`에 임의 문자열 대신 `RecoveryStrategy` enum과 `RecoveryDirective` typed 구조체 사용. `SanitizedEvidence`에서 임의 `Reason string` 제거하고 descriptorCode/fingerprint/count/offset 등 제한된 scalar만 남김. `EvidenceBatch`에서 decisions/outcomes/recoveries 필드 제거하고 committedLookBehind 추가.
- `terminal.go`: `CommitStateHold/Committed/Abandoned` 제거하고 `CommitStateTransportUncommitted/StreamOpen/TerminalCommitted`로 SDD exact 값 사용. `FailureCauseChain`을 slice 타입에서 struct+private slice로 변경하여 bounded 불변조건 강제. `ExternalDescriptor`에서 `Stage` 필드 제거하고 `Type`(필수)/`Code`(선택)/`Message`(필수)/`Param`(선택)으로 교체. `TerminalResult`에서 공개 Success/Error bool 필드를 private으로 변경하고 success 시 descriptor/cause 금지 validation 추가.

## 주요 설계 결정

- NormalizedEvent와 ReleaseEvent 모두 private 필드 + typed constructor + typed accessor 패턴 사용. caller가 임의 필드를 설정하거나 mutable backing storage에 접근하지 못하도록 함.
- BaseDispositionOf가 unknown kind에 오류를 반환하도록 하여 kind-disposition 불일치를 compile 시점이 아닌 runtime에 검출. Validate()에서 kind별 disposition 매칭을 재확인.
- RecoveryDirective의 kind가 RecoveryStrategy와 반드시 일치해야 하는 validation을 NewRecoveryIntent에서 수행. MatchStrategy() 메서드로 추가 검증 가능.
- FailureCauseChain을 struct로 변경하여 Append 시에도 입력 validation과 max cap을 강제. Copy()가 항상 defensive copy를 반환.
- ExternalDescriptor를 private 필드로 변경하고 NewExternalDescriptor에서만 생성 가능. Type은 필수, Code/Param은 선택.
- TerminalResult에서 success 결과에 descriptor/cause가 함께 있으면 Validate()가 거부. error 결과에 descriptor가 없으면 거부. 상호 배제를 constructor와 Validate() 양쪽에서 강제.
- EvidenceBatch에서 filter 결과(decisions/outcomes/recoveries)를 batch 밖으로 분리. batch는 events/pending/committed-look-behind/staged-start/terminal-flag/commit-state/captured-time만 보유.

## 사용자 리뷰 요청

- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: gofmt, go vet, go build, go test (streamgate/transport 회귀), make proto, go list imports
- 후속으로 해결할 수 없는 이유: 없음
- 재개 조건: 없음

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

- exact event kind/base disposition/commit-state 상수와 table이 SDD S09를 따르고 unknown/mismatch를 거부하는지 확인한다.
- response-start status와 kind별 typed payload가 존재하고 caller가 public field로 disposition 또는 mutable header backing을 우회할 수 없는지 확인한다.
- decision `pass|observe|violation|fatal|replacement`와 outcome `evaluated|evaluation_error|not_applicable_for_epoch|deferred_by_requirement`의 variant별 필수/금지 필드를 확인한다.
- recovery strategy와 typed directive가 정확히 일치하고 sanitized evidence/descriptor에 raw prompt/output/provider body/auth 자유 문자열 경로가 없는지 확인한다.
- `EvidenceBatch`가 pending과 committed look-behind를 분리하고 event/response-start를 constructor/accessor 양쪽에서 재귀 deep-copy하는지 확인한다.
- arbitrary-length cause 입력이 항상 valid 최신 4개로 bounded되고 terminal success/error와 descriptor/cause 상호 배제가 모든 constructor/`Validate` 경계에서 유지되는지 확인한다.
- 세 production 파일 외 source/test가 수정되지 않았고 Roadmap Targets가 없는 partial sibling인지 확인한다.

## 검증 결과

> 구현 에이전트는 각 명령의 실제 stdout/stderr를 아래에 원문으로 기록한다. 명령을 바꾸면 `계획 대비 변경 사항`에 대체 명령과 이유를 적는다.

### `gofmt -d packages/go/streamgate/event.go packages/go/streamgate/filter_contract.go packages/go/streamgate/terminal.go`

```text
(no output)
```

### `make proto`

```text
protoc \
	--go_out=. \
	--go_opt=module=iop \
	--proto_path=. \
	proto/iop/runtime.proto \
	proto/iop/node.proto \
	proto/iop/control.proto \
	proto/iop/job.proto
```

### `go test -count=1 ./packages/go/streamgate`

```text
?   	iop/packages/go/streamgate	[no test files]
```

의도: 직접 test는 dependent sibling(`03+01_event_contract_unit_tests`) 소유이므로 현재 `[no test files]` 허용.

### `go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport`

```text
ok  	iop/apps/edge/internal/transport	4.746s
ok  	iop/apps/node/internal/transport	5.563s
```

### `go list -f '{{range .Imports}}{{println .}}{{end}}' ./packages/go/streamgate | LC_ALL=C sort`

```text
context
errors
time
```

---

> Before saving, fill every implementation-owned field and command output. Then report `review-ready` and stop.

## 코드리뷰 결과

- 종합 판정: FAIL
- 리뷰 시작 예외: 사용자가 `Review: pending` identity preflight를 무시하고 진행하라고 명시했다. runtime 소유 identity 값은 수정하지 않았으며, 아래 판정은 구현·계약·검증 증거만을 기준으로 한다.
- 차원별 평가:
  - Correctness: Fail
  - Completeness: Fail
  - Test coverage: Pass — 직접 단위 테스트는 dependent sibling `03+01_event_contract_unit_tests`의 명시 범위다.
  - API contract: Fail
  - Code quality: Fail
  - Implementation deviation: Fail
  - Verification trust: Pass — review stub의 formatter/proto/package/transport/import 출력은 fresh 재실행 결과와 일치한다.
  - Spec conformance: Fail
- 발견된 문제:
  - Required — `packages/go/streamgate/event.go:290`, `packages/go/streamgate/event.go:443`, `packages/go/streamgate/event.go:556`, `packages/go/streamgate/event.go:579`, `packages/go/streamgate/terminal.go:246`: success terminal과 release payload variant가 서로 모순된다. `NewTerminalEvent`는 success event를 만들면서 error descriptor를 필수로 받고 cause도 보존하고, `AsTerminal`은 그 descriptor/cause를 success `TerminalResult`로 넘긴다. 그런데 `TerminalResult.Validate`는 success의 descriptor/cause를 금지하며 `NewTerminalResult` 자체는 이 불변조건과 descriptor/cause validation을 실행하지 않아 constructor가 invalid result를 성공으로 반환한다. 또한 release tool fragment constructor는 arguments를 받거나 보존하지 않고, terminal release constructor는 `terminal` success-candidate kind에 `success=false`를 허용한다. success terminal은 descriptor/cause가 없는 전용 constructor로, provider error는 validated descriptor/bounded cause를 요구하는 전용 constructor로 분리하고 모든 constructor가 공통 validation을 통과하게 해야 한다. `ReleaseEvent`도 허용 kind와 variant payload를 고정하고 tool arguments를 손실 없이 보존해야 한다.
  - Required — `packages/go/streamgate/filter_contract.go:46`, `packages/go/streamgate/filter_contract.go:82`, `packages/go/streamgate/filter_contract.go:154`, `packages/go/streamgate/filter_contract.go:194`, `packages/go/streamgate/filter_contract.go:442`: filter decision/outcome/evidence variant validation이 닫혀 있지 않다. `NewFilterDecision`은 임의의 non-empty `FilterDecisionKind`를 수용하고 `Validate`는 known kind, evidence, intent 또는 kind별 필수/금지 조합을 확인하지 않는다. `FilterOutcome.Validate`도 evaluated/error/not-applicable/deferred variant의 반대쪽 payload 금지를 검사하지 않는다. `NewSanitizedEvidence`와 `Validate`는 unknown event kind, 빈/unknown outcome과 길이 제한 없는 descriptor/fingerprint를 허용한다. closed enum 검증, decision별 intent 규칙, outcome payload 상호 배제, stable token/fingerprint 길이 상한을 constructor와 `Validate` 양쪽에 적용해야 한다.
  - Required — `packages/go/streamgate/filter_contract.go:270`, `packages/go/streamgate/filter_contract.go:291`, `packages/go/streamgate/filter_contract.go:302`, `packages/go/streamgate/filter_contract.go:358`, `packages/go/streamgate/terminal.go:58`, `packages/go/streamgate/terminal.go:193`: raw-free typed boundary가 실제 타입으로 강제되지 않는다. continuation directive의 `safePrefix`, schema directive의 `schemaPatch`, recovery intent의 `reason`, failure cause identity와 external descriptor message/param이 모두 길이·형식 제한 없는 자유 문자열이라 raw output, prompt, provider body 또는 auth를 그대로 담을 수 있다. continuation은 cursor/snapshot reference 같은 bounded typed scalar만, schema repair는 stable schema/patch descriptor만 갖게 하고, reason/cause/error descriptor는 허용된 stable code와 bounded safe template/param 타입으로 제한해야 한다.
  - Required — `packages/go/streamgate/filter_contract.go:547`, `packages/go/streamgate/filter_contract.go:559`, `packages/go/streamgate/filter_contract.go:602`, `packages/go/streamgate/filter_contract.go:610`, `packages/go/streamgate/filter_contract.go:620`, `packages/go/streamgate/filter_contract.go:634`, `packages/go/streamgate/filter_contract.go:645`: `EvidenceBatch`가 SDD의 channel별 immutable snapshot 계약을 충족하지 않는다. committed look-behind가 channel별 map이 아닌 단일 slice이고, normalized event 내부 header/descriptor/cause와 staged response-start header는 constructor/accessor에서 struct만 복사해 재귀 deep-copy되지 않는다. constructor와 `Validate`는 event·pending·look-behind element, channel key, staged start, terminal flag 조합과 commit state를 전혀 검증하지 않는다. pending/look-behind를 모두 channel별로 표현하고 각 nested value를 전용 copy helper로 복제하며 모든 element와 commit/terminal invariant를 입력과 `Validate` 경계에서 확인해야 한다.
  - Required — `packages/go/streamgate/event.go:105`, `packages/go/streamgate/event.go:124`, `packages/go/streamgate/event.go:188`, `packages/go/streamgate/event.go:353`, `packages/go/streamgate/event.go:594`, `packages/go/streamgate/terminal.go:29`: event/commit lifecycle validation을 우회할 공개 경계가 남아 있다. response-start는 status `0`만 일부 거부하고 음수나 HTTP 범위 밖 값을 허용하며, `NormalizedEvent.Validate`와 `ReleaseEvent.Validate`는 kind별 반대 payload 금지와 nested descriptor/cause validation을 하지 않는다. `ValidCommitStates`는 공개 mutable map이고 `knownCommitStates`가 같은 map을 alias하므로 caller가 exact commit-state 집합을 변경할 수 있으며, `EvidenceBatch`는 이 집합조차 검사하지 않는다. status와 kind-payload exclusivity를 완전 검증하고 commit-state lookup을 immutable switch 또는 defensive-copy accessor로 바꾼 뒤 모든 소비 경계에서 unknown state를 거부해야 한다.
  - Nit — `packages/go/streamgate/terminal.go:119`: invalid cause index를 `string(rune('0'+i))`로 만들면 index 10부터 숫자가 아닌 문자가 된다. `strconv.Itoa(i)`로 정확한 진단 위치를 출력해야 한다.
- 검증 재실행:
  - `gofmt -d packages/go/streamgate/event.go packages/go/streamgate/filter_contract.go packages/go/streamgate/terminal.go`: PASS, 출력 없음
  - `make proto`: PASS, 생성물 추가 변경 없음
  - `go test -count=1 ./packages/go/streamgate`: PASS, `[no test files]`
  - `go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport`: PASS
  - `go list -f '{{range .Imports}}{{println .}}{{end}}' ./packages/go/streamgate | LC_ALL=C sort`: `context`, `errors`, `time`
  - `go vet ./packages/go/streamgate`: PASS, 출력 없음
  - forbidden production import search: PASS, `apps/*/internal`, `proto/gen/iop`, `packages/go/config` import 없음
- 다음 단계: active pair를 archive한 뒤 다른 Plan 역할이 같은 task path에 위 Required 항목을 해결하는 후속 PLAN/CODE_REVIEW pair를 작성한다.
