<!-- task=m-stream-evidence-gate-core/03+01_event_contract_unit_tests plan=2 tag=REVIEW_REVIEW_API -->

# Stream Gate 불변성 테스트 직접 변조 증거 후속 계획

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

- 아래 구현 체크리스트와 검증 명령을 완료하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 stdout/stderr를 기록한다.
- active PLAN/CODE_REVIEW 파일은 그대로 두고 review-ready만 보고한다. 판정, 로그 rename, `complete.log`, task archive는 code-review 책임이다.
- 선택된 Milestone `구현 잠금 > 결정 필요`가 구현을 막을 때만 review stub의 `사용자 리뷰 요청`에 정확한 연결 근거를 기록하고 중단한다.
- 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`를 만들지 않는다. 환경/secret/서비스, 일반 범위 조정, 재현 가능한 evidence 공백은 일반 후속 이슈다.

## 배경

현재 production copy 구현과 fresh 테스트는 통과하지만 일부 불변성 테스트가 실제 반환 storage가 아니라 `Headers()`/`All()`이 만든 추가 복사본만 변조한다. 따라서 nested header 또는 cause slice alias 결함이 생겨도 green이 될 수 있다. 같은-package 테스트의 private field 접근을 사용해 SDD S09/S21 evidence가 실제 alias 지점을 실행하도록 교정한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone `구현 잠금 > 결정 필요`만 active review stub의 `사용자 리뷰 요청`에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따르며, 구현 에이전트의 직접 사용자 질문은 금지된다. 요청 검증과 실제 `USER_REVIEW.md` 작성은 code-review가 소유한다.

## Archive Evidence Snapshot

- Task: `agent-task/m-stream-evidence-gate-core/03+01_event_contract_unit_tests`
- Prior plan: `agent-task/m-stream-evidence-gate-core/03+01_event_contract_unit_tests/plan_local_G05_1.log`
- Prior review: `agent-task/m-stream-evidence-gate-core/03+01_event_contract_unit_tests/code_review_local_G05_1.log`
- Verdict: `FAIL`
- Findings: Required 2, Suggested 0, Nit 0
- Required:
  - `EvidenceBatch` response-start 검증이 constructor/accessor event의 nested header storage가 아니라 추가 accessor copy만 변조한다.
  - cause-chain 검증이 retained constructor input과 Append/Copy/TerminalResult 반환 chain storage가 아니라 버려지는 element 또는 `All()` copy만 변조한다.
- Affected files: `packages/go/streamgate/filter_contract_test.go`, `packages/go/streamgate/terminal_test.go`
- Verification evidence: exact Go 1.24.12에서 focused/full streamgate, vet, Edge/Node transport, gofmt와 scoped diff check가 PASS했지만 잘못된 mutation target 때문에 recursive-copy 증거로는 불충분했다.
- Roadmap carryover: 승인된 SDD S09/S21의 `event-contract` evidence를 보강하지만 이 subtask 단독 PASS로 Milestone Task 완료를 체크하지 않는다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/event.go`
- `packages/go/streamgate/filter_contract.go`
- `packages/go/streamgate/terminal.go`
- `packages/go/streamgate/event_test.go`
- `packages/go/streamgate/filter_contract_test.go`
- `packages/go/streamgate/terminal_test.go`
- `packages/go/streamgate/validation_regression_test.go`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-contract/index.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-spec/index.md`
- `agent-spec/input/openai-compatible-surface.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-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`
- 상태: `[승인됨]`, SDD 잠금 `해제`, 사용자 리뷰 `없음`
- S09 / `event-contract`: codec-normalized response-start와 immutable typed event snapshot이 transport-agnostic Core 경계를 통과해야 한다.
- S21 / `event-contract`: failure cause는 최신 원인 최대 4개를 raw-free하게 보존하고 terminal result에 불변 snapshot으로 전달되어야 한다.
- Evidence Map S09/S21의 immutable batch와 bounded cause-chain proof에 맞춰 constructor input, accessor 반환 chain, nested header의 실제 storage 직접 변조를 구현 체크리스트와 focused 검증에 포함했다.

### 테스트 환경 규칙

- `test_env`: local
- `agent-test/local/rules.md`: 존재하며 전체를 읽었다.
- 매칭 profile: `agent-test/local/platform-common-smoke.md`
- 프로젝트 기준 runtime은 Go 1.24다. 현재 기본 `go version`은 불완전한 Go 1.26.2 설치를 가리켜 transport compile 시 `crypto/internal/fips140cache` 누락으로 실패했다.
- `GOTOOLCHAIN=go1.24.12 go version`으로 프로젝트 기준 toolchain을 고정한 뒤 focused/full streamgate, vet, Edge/Node transport가 모두 PASS했다. 모든 Go 검증 명령은 이 exact toolchain을 사용한다.
- 적용 명령: `make proto`, fresh `go test -count=1`, `go vet`, `gofmt -d`, deterministic `rg`, scoped `git diff --check`
- `<확인 필요>`, external runner, secret, service는 없다. test-rule 갱신은 필요하지 않다.

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

- `EvidenceBatch` events input/accessor nested header map: 미검증. 현재 테스트는 `AsResponseStart().Headers()`의 별도 copy만 바꾼다.
- `FailureCauseChain` constructor retained input: 미검증. 현재 테스트는 latest-four에서 탈락하는 `input[0]`만 바꾼다.
- Append/Copy/`TerminalResult.FailureCauses`: 미검증. 현재 테스트는 반환 chain의 `causes`가 아니라 `All()`의 별도 slice만 바꾼다.
- response-start forbidden header behavior, latest-four 순서, `All()` accessor copy, stable-token 경계는 기존 테스트가 유효하게 커버하며 유지한다.

### 심볼 참조

- rename/remove 없음.
- production symbol 변경 없음.
- 새 dependency 없음.

### 분할 판단

- 기존 split 경계 `03+01_event_contract_unit_tests`를 유지한다. predecessor index `01`은 `agent-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`로 충족됐다.
- 두 Required는 같은 package의 immutable event/cause evidence 신뢰를 닫는 test-only 후속이며 production/call-site ownership 경계가 없다. 추가 subtask로 나누면 같은 review verdict와 검증이 불필요하게 분산된다.
- dependent sibling과 dirty worktree의 다른 파일은 수정하지 않는다.

### 범위 결정 근거

- 수정 허용: `packages/go/streamgate/filter_contract_test.go`, `packages/go/streamgate/terminal_test.go`
- `event.go`, `filter_contract.go`, `terminal.go`의 production copy 동작은 정적 대조와 exact-toolchain 테스트에서 요구 계약을 충족하므로 수정하지 않는다.
- header behavior 테스트, proto/config, Edge/Node codec·host fixture, contract/spec/roadmap 문서는 변경하지 않는다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`
- build closures: scope/context/verification/evidence/ownership/decision 모두 `true`
  - scores: scope coupling 1, state/concurrency 1, blast/irreversibility 0, evidence diagnosis 0, verification complexity 1
  - result: `local`, `G03`, `PLAN-local-G03.md`
- review closures: scope/context/verification/evidence/ownership/decision 모두 `true`
  - scores: scope coupling 1, state/concurrency 1, blast/irreversibility 0, evidence diagnosis 2, verification complexity 1
  - result: `local`, `G05`, `CODE_REVIEW-local-G05.md`

## 구현 체크리스트

- [ ] `REVIEW_REVIEW_API-1` `EvidenceBatch` constructor input과 `Events()` 반환 event의 nested header storage를 직접 변조하고 원본 snapshot 보존을 focused test로 검증한다.
- [ ] `REVIEW_REVIEW_API-2` retained constructor input과 Append/Copy/`TerminalResult.FailureCauses` 반환 chain의 `causes` element를 직접 변조하고 원본 순서·값 보존을 focused test로 검증한다.
- [ ] exact Go 1.24.12에서 계획의 전체 local 검증 명령을 fresh 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_API-1] EvidenceBatch nested header 직접 변조

- 문제:
  - `packages/go/streamgate/filter_contract_test.go:320-352`는 `Events()` 반환 event에 `AsResponseStart()`를 호출하고 다시 `Headers()`를 호출한 뒤 세 번째 copy만 바꾼다.
  - constructor input인 `rsEvent.headers`도 batch 생성 뒤 직접 변조하지 않아 `cloneNormalizedEvent`의 nested map copy가 제거돼도 테스트가 통과한다.
- 해결 방법:
  - response-start event를 constructor input으로 넘긴 뒤 caller-owned `inputEvent.headers`를 직접 바꾼다.
  - `got := b.Events()` 뒤 `got[0].headers`를 직접 바꾸고 재조회한 event의 status/header가 원래 값인지 확인한다.
  - 기존 text event slice element replacement와 staged response-start 검증은 유지한다.
- Before (`filter_contract_test.go:332-339`):

```go
got := b.Events()
rs, err := got[0].AsResponseStart()
headers := rs.Headers()
headers["x-custom"] = "mutated"
```

- After:

```go
inputEvent.headers["x-custom"] = "input-mutated"
got := b.Events()
got[0].headers["x-custom"] = "accessor-mutated"
got2 := b.Events()
rs2, err := got2[0].AsResponseStart()
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/filter_contract_test.go`: constructor input/accessor event nested header direct mutation과 re-query assertion
- 테스트 작성:
  - 기존 `TestEvidenceBatchDefensivelyCopiesInputsAndAccessors`를 수정한다.
  - input/accessor 각각의 direct header mutation이 stored `"x-custom": "val"`과 status `200`을 바꾸지 못함을 검증한다.
- 중간 검증:
  - `GOTOOLCHAIN=go1.24.12 go test -count=1 ./packages/go/streamgate -run '^TestEvidenceBatchDefensivelyCopiesInputsAndAccessors$'`
  - 기대 결과: PASS

### [REVIEW_REVIEW_API-2] FailureCauseChain 반환 storage 직접 변조

- 문제:
  - `packages/go/streamgate/terminal_test.go:83-119`는 latest-four에서 버려지는 `input[0]`만 바꾼다.
  - `terminal_test.go:145-177`, `234-255`, `499-528`은 Append/Copy/FailureCauses 반환 chain에 `All()`을 한 번 더 호출한 slice만 바꿔 chain storage alias를 검출하지 못한다.
- 해결 방법:
  - constructor 뒤 retained `input[1]` 또는 `input[4]`를 다른 cause로 교체하고 chain이 `code1..code4`를 유지하는지 확인한다.
  - `appended.causes[0]`, `copied.causes[0]`, `returned.causes[0]`을 직접 교체한 뒤 원본 chain/terminal result를 재조회한다.
  - 기존 `All()` 반환 slice direct element mutation 검증은 별도 public accessor 증거로 유지한다.
- Before (`terminal_test.go:246-254`):

```go
cp2 := chain4.Copy()
cp2Causes := cp2.All()
cp2Causes[0] = mutated
c, err = chain4.At(0)
```

- After:

```go
cp2 := chain4.Copy()
cp2.causes[0] = mutated
c, err = chain4.At(0)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/terminal_test.go`: retained input, Append, Copy, FailureCauses 반환 chain direct element mutation
- 테스트 작성:
  - 기존 `TestFailureCauseChainCapsAtFour`와 `TestTerminalResultValidation`을 수정한다.
  - 각 mutation 뒤 원본의 distinct code 순서와 length가 유지됨을 검증한다.
- 중간 검증:
  - `GOTOOLCHAIN=go1.24.12 go test -count=1 ./packages/go/streamgate -run '^Test(FailureCauseChainCapsAtFour|TerminalResultValidation)$'`
  - 기대 결과: PASS

## 의존 관계 및 구현 순서

1. predecessor `01_event_contract_types` 완료 근거: `agent-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`
2. `REVIEW_REVIEW_API-1`, `REVIEW_REVIEW_API-2`는 서로 다른 테스트 파일을 수정하므로 순서는 자유지만 최종 fresh 검증은 두 변경을 함께 실행한다.
3. dependent sibling의 runtime gate는 이 subtask의 `complete.log`가 생길 때까지 충족되지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/streamgate/filter_contract_test.go` | REVIEW_REVIEW_API-1 |
| `packages/go/streamgate/terminal_test.go` | REVIEW_REVIEW_API-2 |

## 최종 검증

1. `GOTOOLCHAIN=go1.24.12 go version`
   - 기대 결과: `go version go1.24.12`
2. `gofmt -d packages/go/streamgate/filter_contract_test.go packages/go/streamgate/terminal_test.go`
   - 기대 결과: stdout 없음
3. `make proto`
   - 기대 결과: 성공
4. `GOTOOLCHAIN=go1.24.12 go test -count=1 ./packages/go/streamgate -run '^Test(EvidenceBatchDefensivelyCopiesInputsAndAccessors|FailureCauseChainCapsAtFour|TerminalResultValidation)$'`
   - 기대 결과: PASS
5. `GOTOOLCHAIN=go1.24.12 go test -count=1 ./packages/go/streamgate`
   - 기대 결과: PASS
6. `GOTOOLCHAIN=go1.24.12 go vet ./packages/go/streamgate`
   - 기대 결과: PASS, stdout 없음
7. `GOTOOLCHAIN=go1.24.12 go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport`
   - 기대 결과: PASS
8. `rg --sort path -n 'inputEvent\.headers|got\[0\]\.headers|\.causes\[0\]' packages/go/streamgate/filter_contract_test.go packages/go/streamgate/terminal_test.go`
   - 기대 결과: constructor/accessor event header와 returned chain storage의 direct mutation assertion이 출력됨
9. `git diff --check -- packages/go/streamgate proto/gen/iop`
   - 기대 결과: stdout 없음

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