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

# Stream Gate 입력 격리와 constructor matrix 회귀 완결 계획

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

- 아래 테스트 범위만 수정하고 모든 검증을 실제로 실행한다.
- 구현 마지막 단계에서 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션과 실제 stdout/stderr를 채운 뒤 active 파일을 그대로 두고 `review-ready`를 보고한다.
- 판정, log rename, `complete.log`, task archive는 code-review 전용이다.
- 선택된 Milestone `구현 잠금 > 결정 필요`가 실구현을 차단할 때만 review stub의 `사용자 리뷰 요청`에 정확한 연결 근거를 기록하고 중단한다.
- 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`를 만들지 않는다.
- 환경·secret·서비스 차단, 일반 범위 변경, 후속 에이전트가 닫을 수 있는 evidence 공백은 사용자 리뷰 사유가 아니다.

## 배경

직전 리뷰에서 production의 cause bound, defensive copy, constructor final `Validate` gate는 정상 동작하고 모든 명령도 통과했다. 그러나 입력 격리 테스트가 evaluated `FilterOutcome`까지 보호하지 않고, constructor matrix가 계획에 명시된 optional 정상 조합과 대상 constructor별 invalid 입력을 완결하지 않아 완료 evidence가 닫히지 않았다. 이번 후속은 production 동작을 바꾸지 않고 같은 regression 파일의 누락된 assertion만 보강한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone의 `구현 잠금 > 결정 필요`만 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 active review stub에 기록한다. 구현 중 직접 사용자 prompt는 금지하며, 요청의 유효성 판정과 실제 `USER_REVIEW.md` 작성은 code-review가 소유한다.

## Archive Evidence Snapshot

- 입력 evidence:
  - `agent-task/m-stream-evidence-gate-core/01_event_contract_types/plan_local_G05_6.log`
  - `agent-task/m-stream-evidence-gate-core/01_event_contract_types/code_review_local_G06_6.log`
- 판정: `FAIL`
- Required:
  - pointer isolation 회귀가 caller-owned intent의 public-style 재대입과 이를 미리 담은 evaluated `FilterOutcome` 보존을 검증하지 않는다.
  - constructor matrix가 `FailureCause`/`ExternalDescriptor` required-only 정상 조합과 대상 오류 반환 public constructor별 invalid 입력을 완결하지 않는다.
- 영향 파일:
  - `packages/go/streamgate/validation_regression_test.go`
- 검증 evidence:
  - formatter, proto generation, focused/package/transport tests, vet, import/public-symbol boundary, diff check는 통과했다.
  - production의 cause bound, defensive copy, constructor final `Validate` gate는 직접 소스 대조와 현재 회귀에서 확인됐다.
- Roadmap carryover: 이 pair는 `event-contract`의 부분 regression evidence만 닫으며 codec/host fixture까지 완료하지 않으므로 `Roadmap Targets`를 두지 않는다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/event.go`
- `packages/go/streamgate/filter_contract.go`
- `packages/go/streamgate/terminal.go`
- `packages/go/streamgate/validation_regression_test.go`
- `agent-task/m-stream-evidence-gate-core/01_event_contract_types/PLAN-local-G05.md` (현재 `plan_local_G05_6.log`)
- `agent-task/m-stream-evidence-gate-core/01_event_contract_types/CODE_REVIEW-local-G06.md` (현재 `code_review_local_G06_6.log`)
- `agent-task/m-stream-evidence-gate-core/01_event_contract_types/code_review_local_G05_5.log`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `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/outer/openai-compatible-api.md`
- `agent-spec/index.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`

`agent-spec/index.md`에는 `streamgate`와 직접 매칭되는 living spec이 없어 코드, 계약, SDD, 테스트를 현재 기준으로 사용한다.

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`
- 상태: `[승인됨]`, SDD 잠금 `해제`, 사용자 리뷰 없음
- S09 / `event-contract`: transport-agnostic typed value가 caller-owned input 재대입 뒤에도 immutable하도록 `REVIEW_API-1`의 decision/outcome assertion으로 보강한다.
- S21 / `event-contract`: bounded raw-free failure descriptor/cause 생성 계약을 `REVIEW_API-2`의 required-only/optional 정상 조합과 invalid constructor assertion으로 보강한다.
- Evidence Map의 codec/host fixture는 dependent sibling 범위이므로 이번 PASS만으로 S09/S21 전체 완료를 주장하지 않는다.

### 테스트 환경 규칙

- `test_env=local`
- `agent-test/local/rules.md`: 존재하며 읽음
- 매칭 profile: `agent-test/local/platform-common-smoke.md`
- 적용: `make proto`, 대상 package fresh test, Edge/Node transport fresh test, `go vet`, deterministic symbol/import 검색, `git diff --check`
- full-cycle 실제 연결은 proto/wire/host 호출부를 바꾸지 않는 test-only 후속에는 적용하지 않는다.
- `<확인 필요>` 값과 외부 runner/service/secret 전제는 없다.
- Go test cache는 bug-fix 회귀를 가리지 않도록 `-count=1`로 비활성화한다.

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

- caller-owned `RecoveryIntent` 재대입 뒤 `FilterDecision` 보존: 현재 test는 private field mutation으로 decision만 확인한다.
- caller-owned `RecoveryIntent` 재대입 뒤 사전에 생성한 evaluated `FilterOutcome` 보존: 기존 test 없음.
- `FailureCause`/`ExternalDescriptor` required-only와 optional-full 정상 조합: optional-full만 존재한다.
- 대상 filter/terminal 오류 반환 public constructor별 대표 invalid 입력: 세 constructor만 존재하고 outcome/directive/intent/evidence가 빠져 있다.

### 심볼 참조

- rename/remove 대상 없음.
- repo Go source에서 `iop/packages/go/streamgate` importer와 `streamgate.` 호출부는 없다.
- forbidden public constructor `NewReleaseResponseStartEvent`, `NewReleaseTerminalEvent`, `NewTerminalResult`는 계속 없어야 한다.

### 분할 판단

- split decision policy를 재평가했다.
- 이번 후속은 기존 `01_event_contract_types` 안의 한 regression 파일과 두 기존 test 함수만 보강하는 단일 테스트 단위다.
- API/call-site, ownership, dependency, 검증 전략 경계가 없고 둘을 나누면 같은 constructor fixture를 중복 조립해야 하므로 single plan이 더 안전하다.
- 디렉터리 `01_event_contract_types`에는 encoded predecessor가 없다.

### 범위 결정 근거

- 포함: `packages/go/streamgate/validation_regression_test.go`의 두 기존 test와 필요한 test helper.
- 제외: `packages/go/streamgate/*.go` production 변경. 새 회귀가 실제 production 결함을 드러내면 구현을 확장하지 말고 현재 routing을 무효화해 다시 계획한다.
- 제외: `apps/**`, `proto/**`, `packages/go/config/**`, roadmap/SDD/contract/spec 문서, sibling task 파일.
- 기존 dirty worktree의 다른 변경은 사용자 소유이므로 수정하거나 정리하지 않는다.
- 새 dependency와 import는 필요하지 않아 `go.mod`와 import block을 변경하지 않는다.

### 최종 라우팅

- `evaluation_mode: isolated-reassessment`
- build:
  - closures: scope/context/verification/evidence/ownership/decision 모두 `true`
  - 근거: 단일 Go test 파일의 두 명시 회귀와 deterministic local 검증으로 구현 범위가 폐쇄됨
  - scores: `scope_coupling=0`, `state_concurrency=0`, `blast_irreversibility=0`, `evidence_diagnosis=0`, `verification_complexity=1`
  - 결과: `local`, `G01`, `PLAN-local-G01.md`
- review:
  - closures: scope/context/verification/evidence/ownership/decision 모두 `true`
  - 근거: exact test source와 focused/package 명령으로 완료 여부를 판정할 수 있음
  - scores: `scope_coupling=0`, `state_concurrency=0`, `blast_irreversibility=0`, `evidence_diagnosis=0`, `verification_complexity=1`
  - 결과: `local`, `G01`, `CODE_REVIEW-local-G01.md`

## 구현 체크리스트

- [ ] `REVIEW_API-1` caller-owned `RecoveryIntent` 변수를 public-style로 재대입해도 기존 `FilterDecision`과 사전에 생성한 evaluated `FilterOutcome`이 원래 nested intent를 보존하는 회귀를 완성한다.
- [ ] `REVIEW_API-2` 대상 filter/terminal constructor의 required-only/optional 정상 조합과 constructor별 대표 invalid 입력 matrix를 완성한다.
- [ ] 계획의 local 최종 검증 명령을 `-count=1` 기준으로 모두 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_API-1] Decision과 evaluated outcome 입력 격리 회귀

- 문제: `packages/go/streamgate/validation_regression_test.go:604-643`은 원본 intent의 private `priority`를 바꾼 뒤 decision만 확인한다. 외부 caller가 할 수 있는 변수 재대입과 재대입 전에 생성된 evaluated outcome의 nested decision/intent 보존은 검증하지 않는다.
- 해결 방법:

Before (`packages/go/streamgate/validation_regression_test.go:627-642`):

```go
// Mutate the original intent variable.
intent.priority = 999

// The decision's intent must retain the original priority (0).
d := decision.Intent()
// ...
if err := decision.Validate(); err != nil {
	t.Fatalf("decision should still validate after intent mutation: %v", err)
}
```

After:

```go
outcome, err := NewFilterOutcomeEvaluated(decision)
if err != nil {
	t.Fatalf("NewFilterOutcomeEvaluated: %v", err)
}

intent = RecoveryIntent{}

if err := decision.Validate(); err != nil {
	t.Fatalf("decision should survive caller intent reassignment: %v", err)
}
if err := outcome.Validate(); err != nil {
	t.Fatalf("outcome should survive caller intent reassignment: %v", err)
}
// decision.Intent()와 outcome.Decision().Intent()가 원래 strategy/reason/priority를 유지하는지 확인한다.
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/validation_regression_test.go`: outcome을 caller intent 재대입 전에 생성
  - [ ] `packages/go/streamgate/validation_regression_test.go`: zero-value 또는 다른 정상 intent 재대입 뒤 decision/outcome `Validate`와 nested intent 원값 assertion
- 테스트 작성:
  - 기존 `TestFilterDecisionConstructorCopiesRecoveryIntent`를 확장한다.
  - private field 직접 mutation은 제거하고 외부 package에서도 가능한 변수 재대입을 사용한다.
  - decision과 outcome 양쪽의 strategy, reason, priority 중 원본을 구분할 수 있는 값을 확인한다.
- 중간 검증:
  - `go test -count=1 ./packages/go/streamgate -run '^TestFilterDecisionConstructorCopiesRecoveryIntent$'`
  - 기대: PASS.

### [REVIEW_API-2] Constructor 정상/오류 matrix 완결

- 문제: `packages/go/streamgate/validation_regression_test.go:865-1043`은 normal matrix를 선언하지만 `FailureCause`와 `ExternalDescriptor`의 optional-empty 정상 variant가 없고, invalid section은 대상 열 개 constructor 중 세 개만 다룬다.
- 해결 방법:

Before (`packages/go/streamgate/validation_regression_test.go:1003-1043`):

```go
t.Run("FailureCause", func(t *testing.T) {
	fc, err := NewFailureCause("stage", "code", "consumer", "filter", "rule")
	// ...
})
t.Run("ExternalDescriptor", func(t *testing.T) {
	ed, err := NewExternalDescriptor("error.type", "code", "message", "param")
	// ...
})

// Invalid inputs: FilterDecision, FailureCause, ExternalDescriptor only.
```

After:

```go
// FailureCause와 ExternalDescriptor는 required-only와 optional-full을 각각
// 독립 subtest로 만들고 반환 값의 Validate를 확인한다.
// 대상 오류 반환 constructor마다 대표 malformed input을 하나씩 호출해
// err != nil을 확인한다.
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/validation_regression_test.go`: `FailureCause` required-only/optional-full 정상 subtest
  - [ ] `packages/go/streamgate/validation_regression_test.go`: `ExternalDescriptor` required-only/optional-full 정상 subtest
  - [ ] `packages/go/streamgate/validation_regression_test.go`: `NewFilterDecision`, 두 `NewFilterOutcome*`, 세 `NewRecoveryDirective*`, `NewRecoveryIntent`, `NewSanitizedEvidence`, `NewFailureCause`, `NewExternalDescriptor` 각각의 독립 invalid subtest
- 테스트 작성:
  - 기존 `TestFilterAndTerminalConstructorsProduceValidValues`를 확장한다.
  - 각 invalid subtest는 정확히 한 constructor를 호출하고 `err == nil`이면 실패한다.
  - invalid fixture는 unknown enum, 빈/잘못된 stable token, strategy/directive mismatch, 음수 cursor/priority, zero fingerprint 중 해당 constructor의 public 입력으로 재현 가능한 값을 사용한다.
- 중간 검증:
  - `go test -count=1 ./packages/go/streamgate -run '^TestFilterAndTerminalConstructorsProduceValidValues$'`
  - 기대: PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/streamgate/validation_regression_test.go` | `REVIEW_API-1`, `REVIEW_API-2` |

## 최종 검증

환경: `local`, repo root `/config/workspace/iop`. Go test cache는 허용하지 않는다.

1. `gofmt -d packages/go/streamgate/validation_regression_test.go`
   - 기대: 출력 없음.
2. `make proto`
   - 기대: exit 0, `proto/**` 추가 변경 없음.
3. `go test -count=1 ./packages/go/streamgate -run 'Test(FilterDecisionConstructorCopiesRecoveryIntent|FilterAndTerminalConstructorsProduceValidValues)$'`
   - 기대: 두 후속 회귀 PASS.
4. `go test -count=1 ./packages/go/streamgate`
   - 기대: package 전체 PASS.
5. `go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport`
   - 기대: 두 package PASS.
6. `go vet ./packages/go/streamgate`
   - 기대: 출력 없이 exit 0.
7. `go list -f '{{range .Imports}}{{println .}}{{end}}' ./packages/go/streamgate | LC_ALL=C sort`
   - 기대: `context`, `errors`, `strconv`, `time`.
8. `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`
   - 기대: `PASS: no forbidden imports`.
9. `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`
   - 기대: `PASS: forbidden public constructors absent`.
10. `git diff --check -- packages/go/streamgate`
    - 기대: 출력 없음.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
