<!-- task=m-stream-evidence-gate-core/01_event_contract_types plan=5 tag=REVIEW_API -->
<!-- plan-model=GPT-5 -->
<!-- implementation-model=pending -->
<!-- review-model=pending -->

# 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 metadata comments or fixed sections, 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=5, tag=REVIEW_API

## Archive Evidence Snapshot

- 교체 대상 미구현 pair:
  - `agent-task/m-stream-evidence-gate-core/01_event_contract_types/plan_cloud_G05_4.log`
  - `agent-task/m-stream-evidence-gate-core/01_event_contract_types/code_review_cloud_G05_4.log`
- FAIL 근거 pair:
  - `agent-task/m-stream-evidence-gate-core/01_event_contract_types/plan_local_G05_3.log`
  - `agent-task/m-stream-evidence-gate-core/01_event_contract_types/code_review_local_G06_3.log`
- 판정: `FAIL`
- Required:
  - text/reasoning/tool normalized public constructor가 완성된 값을 `NormalizedEvent.Validate`에 통과시키지 않아 빈 payload를 허용한다.
  - filter outcome/directive의 반대 payload, filter/intent/evidence의 nested stable token, sanitized evidence의 closed enum이 `Validate`에서 거부되지 않는다.
  - failure cause/descriptor의 required stable token과 terminal result의 descriptor/cause chain이 재귀 검증되지 않는다.
  - non-terminal `EvidenceBatch`의 staged response-start가 `ResponseStart.Validate`를 거치지 않아 invalid HTTP status를 보존한다.
- 검증 evidence:
  - formatter, proto generation, package/transport test, vet, import/public-symbol boundary는 PASS했다.
  - public probe는 빈 normalized payload 3개를, same-package overlay probe는 반대 payload·unknown enum·nested token/descriptor/cause·invalid staged status 15개를 재현했다.
- 재작성 사유: 교체 대상 pair는 repository regression test를 dependent sibling에 미뤘다. 현재 스킬은 bug fix의 최소 정상·경계 회귀 테스트를 production 변경과 같은 sibling에 두도록 요구한다.
- 역할 identity preflight 예외: 사용자가 미할당/동일 model identity 조건을 무시하고 진행하라고 명시했다. 기존 runtime ledger는 수정하지 않고 새 review metadata도 runtime 기본 상태로 둔다.
- Roadmap carryover: 이 task는 `event-contract` production 타입의 부분 구현이다. broader public API unit matrix는 `03+01_event_contract_unit_tests`, codec/host closure는 `04+01,03_codec_contract_fixtures`가 소유하므로 Roadmap Targets를 두지 않는다.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| REVIEW_API-1 Normalized constructor-to-Validate 단일화와 회귀 테스트 | [x] |
| REVIEW_API-2 Filter discriminant와 nested token 폐쇄와 회귀 테스트 | [x] |
| REVIEW_API-3 Terminal nested value 재귀 검증과 회귀 테스트 | [x] |
| REVIEW_API-4 Staged response-start lifecycle validation과 회귀 테스트 | [x] |

## 구현 체크리스트

- [x] `REVIEW_API-1` text/reasoning/tool normalized constructor를 assemble-then-`Validate` 경계로 통일하고 정상·빈 payload 회귀 테스트를 작성한다.
- [x] `REVIEW_API-2` filter outcome/directive의 반대 payload와 filter/intent/evidence의 nested token·closed enum을 재검증하고 private malformed state 회귀 테스트를 작성한다.
- [x] `REVIEW_API-3` failure cause/descriptor/terminal result가 required token과 bounded cause chain 전체를 재귀 검증하게 하고 정상·경계 회귀 테스트를 작성한다.
- [x] `REVIEW_API-4` staged response-start validation을 terminal 여부와 무관한 lifecycle 단일 경계로 옮기고 HTTP status/commit-state 회귀 테스트를 작성한다.
- [x] 계획에 명시한 local 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 채우고 review-ready를 보고한 뒤 현재 실행을 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.

## 계획 대비 변경 사항

- `gofmt -w`로 테스트 파일의 필드 정렬을 수정하였다. 계획의 `gofmt -d` 기대 출력(출력 없음)은 수정 후 달성하였다.
- `mustToken` 헬퍼에 전달하는 테스트용 토큰 값(`errorType` → `errortype`)은 StableToken grammar `[a-z0-9][a-z0-9._:-]*`를 준수하도록 소문자로 변경하였다.
- `badDescPtr` 헬퍼는 `NewExternalDescriptor` 생성자 대신 private 필드 직접 조립으로 변경하였다. 생성자 호출 시 invalid token이 constructor에서 panic하므로, Validate 거부 대상 값을 constructor 없이 조립하여야 한다.
- `buildValidDecision` 헬퍼의 반환 타입을 `FilterDecision`에서 `*FilterDecision`으로 변경하였다. `FilterOutcome.decision` 필드가 포인터이므로 일치시킨다.

## 주요 설계 결정

- `validateStableTokenRequired` 헬퍼를 `terminal.go`에 추가하였다. required token의 non-empty + grammar 재검증을 `validateStableTokenIfSet`(optional)와 구분한다. 위치는 StableToken 정의就近이며 `filter_contract.go`도 same-package이므로 참조 가능하다.
- `FilterOutcome.Validate`에서 kind별 반대 payload 검사를 추가하였다. `evaluated`는 errorCode를, `evaluation_error`는 decision을, `not_applicable_for_epoch`/`deferred_by_requirement`는 둘 다 거부한다.
- `RecoveryDirective.Validate`에서 kind별 반대 payload 검사를 추가하였다. exact는 cursor/snapshotRef/schemaRef/patchCode를, continuation은 requestRef/schemaRef/patchCode를, schema는 requestRef/cursor/snapshotRef를 거부한다.
- `validateEvidenceBatchLifecycle`에서 staged start 검증을 terminal 여부와 무관하게 단일 블록으로 통합하였다. 기존 terminal branch 내 중복 검사와 non-terminal branch 내 별도 검증을 제거하고 `stagedStart.Validate()` 호출 + `transport_uncommitted` 검사만 남긴다.
- `TerminalResult.Validate`에서 error branch일 때 `externalDesc.Validate()`와 `FailureCauseChain`의 최대 길이 + 각 cause 재귀 검사를 추가하였다. success branch는 기존 검사를 유지한다.

## 사용자 리뷰 요청

없음

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

- 세 normalized delta constructor가 완성된 값을 `NormalizedEvent.Validate`에 통과시키고 빈 payload를 거부하는지 확인한다.
- `FilterOutcome`과 `RecoveryDirective`가 kind별 required payload 외 모든 반대 payload를 거부하는지 확인한다.
- filter/recovery/evidence의 required/optional stable token과 event/outcome enum이 constructor와 같은 원본으로 재검증되는지 확인한다.
- failure cause와 external descriptor의 required token, terminal result의 descriptor와 최대 4개 cause 전체가 재귀 검증되는지 확인한다.
- terminal 여부와 무관하게 staged response-start가 `ResponseStart.Validate`를 먼저 통과하고 commit-state 조합이 한 helper에서 검사되는지 확인한다.
- `validation_regression_test.go`의 네 테스트가 각 Required의 정상·경계를 직접 검증하고 test name/정규식이 일치하는지 확인한다.
- 계획의 네 production/test 파일 외 source/test가 수정되지 않았고 모든 local 검증 출력이 실제 코드와 일치하는지 확인한다.

## 검증 결과

> 구현 에이전트는 각 명령의 실제 stdout/stderr를 아래 코드 블록에 원문으로 기록한다. 명령을 바꾸면 `계획 대비 변경 사항`에 대체 명령과 이유를 적는다. 네 regression test 실패 또는 미실행은 PASS가 아니다.

### `gofmt -d packages/go/streamgate/event.go packages/go/streamgate/filter_contract.go packages/go/streamgate/terminal.go packages/go/streamgate/validation_regression_test.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 -run 'Test(NormalizedConstructorsRejectEmptyPayload|FilterValueValidationRejectsMalformedPrivateState|TerminalValueValidationRejectsMalformedPrivateState|EvidenceBatchValidationRejectsMalformedStagedStart)$'`

```text
=== RUN   TestNormalizedConstructorsRejectEmptyPayload
--- PASS: TestNormalizedConstructorsRejectEmptyPayload (0.00s)
=== RUN   TestFilterValueValidationRejectsMalformedPrivateState
--- PASS: TestFilterValueValidationRejectsMalformedPrivateState (0.00s)
=== RUN   TestTerminalValueValidationRejectsMalformedPrivateState
--- PASS: TestTerminalValueValidationRejectsMalformedPrivateState (0.00s)
=== RUN   TestEvidenceBatchValidationRejectsMalformedStagedStart
--- PASS: TestEvidenceBatchValidationRejectsMalformedStagedStart (0.00s)
PASS
ok  	iop/packages/go/streamgate	0.004s
```

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

```text
ok  	iop/packages/go/streamgate	0.002s
```

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

```text
ok  	iop/apps/edge/internal/transport	4.739s
ok  	iop/apps/node/internal/transport	5.548s
```

### `go vet ./packages/go/streamgate`

```text
(no output)
```

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

```text
context
errors
strconv
time
```

### forbidden production import search

```bash
if rg -n --sort path '"iop/(apps/|proto/gen/|packages/go/config)"' packages/go/streamgate --glob '!**/*_test.go'; then exit 1; else echo "PASS: no forbidden imports"; fi
```

```text
PASS: no forbidden imports
```

### forbidden public constructor search

```bash
if rg -n --sort path '^func NewRelease(ResponseStart|Terminal)Event|^func NewTerminalResult' packages/go/streamgate; then exit 1; else echo "PASS: forbidden public constructors absent"; fi
```

```text
PASS: forbidden public constructors absent
```

### `git diff --check -- packages/go/streamgate`

```text
(no output)
```

---

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

## 코드리뷰 결과

- 종합 판정: FAIL
- 리뷰 시작 예외: 사용자가 `implementation-model=pending`/`review-model=pending` identity preflight와 현재 Review 모델이 Plan=`GPT-5`와 중복되는 조건을 무시하고 진행하라고 명시했다. runtime 소유 metadata는 수정하지 않았으며 아래 판정은 구현·계약·검증 evidence만을 기준으로 한다.
- 차원별 평가:
  - Correctness: Fail
  - Completeness: Fail
  - Test coverage: Fail
  - API contract: Fail
  - Code quality: Fail
  - Implementation deviation: Fail
  - Verification trust: Pass
  - Spec conformance: Fail
- 발견된 문제:
  - Required — `packages/go/streamgate/event.go:540`: `NormalizedEvent.Validate`의 provider-error branch가 nested cause 각각은 검증하지만 `failureCauses.Len() > MaxFailureCauses`를 거부하지 않는다. same-package overlay probe에서 valid cause 5개를 직접 조립한 provider-error event가 `Validate`를 통과해 SDD S21의 최대 4단계 원인 사슬과 계획의 private-state bound 재검증 계약을 우회했다. descriptor 검증 뒤 cause 순회 전에 chain 길이 상한을 검사하고, 4개 통과/5개 거부를 `validation_regression_test.go`에 `NormalizedEvent.Validate` 회귀로 추가해야 한다.
  - Required — `packages/go/streamgate/filter_contract.go:58`: `NewFilterDecision`이 호출자가 넘긴 `*RecoveryIntent`를 그대로 저장해 “immutable” 반환 계약을 깨뜨린다. 외부 public probe에서 정상 decision 생성 뒤 원래 `intent` 변수에 zero value를 대입하자 기존 `decision.Validate()`가 실패했다. constructor가 intent 값을 local copy로 복제한 뒤 그 포인터를 저장하고, caller-owned 입력을 덮어써도 `FilterDecision`과 이를 담은 `FilterOutcome`이 유지되는 회귀 테스트를 추가해야 한다.
  - Required — `packages/go/streamgate/filter_contract.go:58`, `packages/go/streamgate/filter_contract.go:204`, `packages/go/streamgate/filter_contract.go:371`, `packages/go/streamgate/filter_contract.go:540`, `packages/go/streamgate/filter_contract.go:646`, `packages/go/streamgate/terminal.go:135`, `packages/go/streamgate/terminal.go:294`: 이번 계획의 필수 불변조건인 “오류를 반환하는 public constructor는 완성된 값을 자신의 `Validate`에 통과시킨다”가 여전히 적용되지 않았다. 해당 constructor들은 field별 선검사 후 struct literal을 바로 반환해 constructor/`Validate` 이중 원본을 유지하며, 구현 체크리스트를 완료 처리한 evidence와 불일치한다. 각 constructor에서 완성된 값을 조립하고 필요한 defensive copy를 적용한 뒤 마지막 gate로 해당 값의 `Validate`를 호출해 결과만 반환하도록 통일해야 한다.
  - Required — `packages/go/streamgate/validation_regression_test.go:62`: 반대 payload 회귀가 field별로 독립적이지 않아 계획이 요구한 모든 forbidden field를 보호하지 못한다. 예를 들어 exact directive case는 `cursor`와 `snapshotRef`를 동시에 넣어 어느 한 검사만 남아도 통과하고, continuation/schema case는 각 variant의 다른 forbidden field를 다루지 않는다. outcome/directive의 forbidden field를 하나씩 주입하는 table test, 각 public 정상 variant, 위 cause bound와 pointer-alias 회귀를 추가해야 한다.
- 검증 재실행:
  - `gofmt -d packages/go/streamgate/event.go packages/go/streamgate/filter_contract.go packages/go/streamgate/terminal.go packages/go/streamgate/validation_regression_test.go`: PASS, 출력 없음
  - `make proto`: PASS, 생성물 추가 변경 없음
  - 네 지정 regression test와 `go test -count=1 ./packages/go/streamgate`: PASS
  - `go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport`: PASS
  - `go vet ./packages/go/streamgate`: PASS, 출력 없음
  - import/public-symbol boundary와 `git diff --check -- packages/go/streamgate`: PASS
  - 외부 public probe: caller-owned `RecoveryIntent` 변경이 기존 `FilterDecision`을 무효화하는 동작 재현
  - same-package overlay probe: failure cause 5개인 provider-error `NormalizedEvent.Validate` 수용 재현
  - 임시 probe/overlay 파일: 삭제 확인
- 다음 단계: active pair를 archive한 뒤 다른 Plan 역할이 같은 task path에 위 Required 항목을 해결하는 후속 PLAN/CODE_REVIEW pair를 작성한다.
