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

# Stream Gate Event Contract 검증 폐쇄 후속 계획

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

- 아래 구현 체크리스트와 검증 명령을 완료하고 `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 공백은 일반 후속 이슈다.

## 배경

이전 구현은 fresh 명령을 모두 통과했지만 계획에 명시된 unsafe response-start header 경계를 실행하지 않았고, 일부 defensive-copy/cause-chain 테스트는 no-op 대입이나 append 재할당만 수행했다. 승인된 SDD S09/S21 evidence로 쓰려면 공개 생성자가 금지 transport metadata를 거부하고, snapshot·cause chain의 실제 element 변조에도 원본이 보존됨을 증명해야 한다. 이 후속은 같은 `streamgate` package 안에서 세 Required를 함께 폐쇄한다.

## 사용자 리뷰 요청 흐름

선택된 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_G04_0.log`
- Prior review: `agent-task/m-stream-evidence-gate-core/03+01_event_contract_unit_tests/code_review_local_G05_0.log`
- Verdict: `FAIL`
- Findings: Required 3, Suggested 0, Nit 0
- Required:
  - response-start 생성자와 테스트가 hop-by-hop/`Content-Length` 거부 계약을 구현·실행하지 않는다.
  - `EvidenceBatch` defensive-copy 테스트가 실제 element/nested payload를 변조하지 않는다.
  - cause-chain 테스트가 latest-four 순서, 원본 불변성, stage/code/id token 경계를 증명하지 않는다.
- Affected files: `packages/go/streamgate/event.go`, `packages/go/streamgate/event_test.go`, `packages/go/streamgate/filter_contract_test.go`, `packages/go/streamgate/terminal_test.go`
- Verification evidence: `make proto`, fresh streamgate/transport tests, `gofmt -d`, scoped `git diff --check`는 PASS했지만 누락된 assertion 때문에 계약 evidence로는 불충분했다.
- 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-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`: response-start typed boundary가 raw transport metadata를 Core에 남기지 않고 transport-agnostic contract를 유지해야 한다.
- S21 / `event-contract`: sanitized cause chain은 최신 원인 최대 4개로 제한되고 terminal result에 raw 값 없이 전달되어야 한다.
- Evidence Map S09/S21에 따라 금지 header 정상/경계 table, recursive snapshot mutation, distinct latest-four cause와 token validation을 구현 체크리스트 및 focused/fresh 검증에 포함했다.

### 테스트 환경 규칙

- `test_env`: local
- `agent-test/local/rules.md`: 존재하며 전체를 읽었다.
- 매칭 profile: `agent-test/local/platform-common-smoke.md`
- 적용 명령: `make proto`, fresh `go test -count=1` streamgate 및 Edge/Node transport, `gofmt -d`, `go vet`, deterministic `rg`, scoped `git diff --check`
- `<확인 필요>`와 외부 runner/secret/service는 없다. test-rule 갱신도 필요하지 않다.

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

- response-start forbidden header: 미커버이며 현재 생성자가 실제로 수용한다.
- `EvidenceBatch` input/accessor recursive copy: 구현은 deep copy하지만 기존 테스트의 no-op/append가 alias 결함을 검출하지 못한다.
- `FailureCauseChain` latest-four 및 immutability: cap 길이만 확인하고 retention 순서와 원본 보존을 검출하지 못한다.
- failure cause token: stage 한 변형 외 code/consumer/filter/rule id의 invalid constructor 경계가 없다.
- terminal mutual exclusion과 `ReleaseSink` compile contract는 기존 테스트가 유지한다.

### 심볼 참조

- rename/remove 없음.
- 새 third-party dependency 없음. header validation은 Go standard library만 사용한다.

### 분할 판단

- 기존 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`로 충족됐다.
- 후속 세 항목은 같은 공개 event contract와 같은 package test suite의 한 review verdict를 폐쇄하므로 추가 subtask로 나누면 현재 FAIL 상태와 evidence가 분산된다.
- 활성 sibling `04+01,03_codec_contract_fixtures`는 이 subtask index `03`에 의존하며 네 파일을 소유하지 않는다.

### 범위 결정 근거

- 수정 허용: `event.go`와 세 기존 test 파일.
- `filter_contract.go`와 `terminal.go`는 정적 대조상 요구 deep copy/cap 동작을 이미 구현하므로 수정하지 않는다.
- proto/config, Edge/Node codec·host fixture, contract/spec/roadmap 문서는 변경하지 않는다. codec/host single-terminal evidence는 dependent sibling 범위다.

### 최종 라우팅

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

## 구현 체크리스트

- [ ] `REVIEW_API-1` response-start header validation을 두 공개 생성자와 value validation 경로에 공통 적용하고 정상/금지 header focused test를 fresh 실행한다.
- [ ] `REVIEW_API-2` `EvidenceBatch` input/accessor/staged-start 테스트를 실제 element와 nested header 변조로 바꾸고 focused test를 fresh 실행한다.
- [ ] `REVIEW_API-3` distinct latest-four cause, constructor/Append/accessor 불변성, stage/code/id invalid token table을 작성하고 focused test를 fresh 실행한다.
- [ ] 계획의 전체 local 검증 명령을 fresh cache 조건으로 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_API-1] Response-start unsafe header 경계

- 문제:
  - `packages/go/streamgate/event.go:122-140`, `211-236`은 모든 header를 검증 없이 복사한다.
  - `packages/go/streamgate/event_test.go:193-316`은 함수명과 달리 hop-by-hop/`Content-Length`를 한 번도 전달하지 않는다.
- 해결 방법:
  - case-insensitive forbidden-name helper를 추가한다. 최소 `Connection`, `Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization`, `Proxy-Connection`, `TE`, `Trailer`, `Transfer-Encoding`, `Upgrade`, `Content-Length`를 거부한다.
  - `NewResponseStart`, `NewResponseStartEvent`, `ResponseStart.Validate`, response-start `NormalizedEvent.Validate`에 같은 helper를 적용한다.
  - header value는 오류에 포함하지 않고 정상 extension/end-to-end header는 그대로 보존한다.
- Before (`event.go:132-135`):

```go
cp := make(map[string]string, len(headers))
for k, v := range headers {
	cp[k] = v
}
```

- After:

```go
import (
	"errors"
	"strconv"
	"strings"
	"time"
)

if err := validateResponseStartHeaders(headers); err != nil {
	return ResponseStart{}, err
}
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/event.go`: 공통 forbidden header validation과 네 진입점 적용
  - [ ] `packages/go/streamgate/event_test.go`: allow header 및 mixed-case forbidden header table
- 테스트 작성:
  - `TestResponseStartRejectsUnsafeHeaders`: 정상 header 보존, 각 금지 header가 두 생성자와 직접 value validation에서 거부됨을 검증한다.
- 중간 검증:
  - `go test -count=1 ./packages/go/streamgate -run '^TestResponseStartRejectsUnsafeHeaders$'`
  - 기대 결과: PASS

### [REVIEW_API-2] EvidenceBatch recursive defensive-copy proof

- 문제:
  - `filter_contract_test.go:35-36`, `162-163`은 같은 event를 재대입하고 append만 한다.
  - `filter_contract_test.go:59`, `94`, `185`, `215`의 append는 cap-one slice를 재할당할 수 있어 nested backing 공유를 검출하지 못한다.
  - `StagedResponseStart()` 반환 포인터를 변조한 뒤 재조회하는 검증이 없다.
- 해결 방법:
  - 서로 다른 payload의 valid event로 slice index를 직접 교체한다.
  - response-start event의 nested header map을 constructor input과 accessor 반환값 양쪽에서 직접 바꾼 뒤 원본 batch를 재조회한다.
  - pending/look-behind의 element를 직접 교체하고 staged-start 반환 포인터의 status/header를 직접 바꿔 snapshot을 다시 확인한다.
- Before (`filter_contract_test.go:34-36`):

```go
inputEvents[0] = textEv
inputEvents = append(inputEvents, textEv)
```

- After:

```go
inputEvents[0] = otherEvent
inputEvents[0].headers["x-custom"] = "mutated"
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/filter_contract_test.go`: events/pending/look-behind/staged-start input 및 accessor direct mutation
- 테스트 작성:
  - `TestEvidenceBatchDefensivelyCopiesInputsAndAccessors`: mutation이 stored kind/payload/header/status를 바꾸지 않음을 검증한다.
- 중간 검증:
  - `go test -count=1 ./packages/go/streamgate -run '^TestEvidenceBatchDefensivelyCopiesInputsAndAccessors$'`
  - 기대 결과: PASS

### [REVIEW_API-3] FailureCause retention·token·immutability proof

- 문제:
  - `terminal_test.go:42-57`은 5개 cause가 동일해 어떤 항목이 탈락했는지 알 수 없다.
  - `terminal_test.go:64-90`, `110-128`, `328-336`은 원본/반환 slice element를 직접 바꾸지 않는다.
  - `terminal_test.go:131-143`은 malformed stage만 검사해 code와 optional ids 경계가 빠졌다.
- 해결 방법:
  - 서로 다른 code의 5개 cause로 최신 4개와 순서를 assert한다.
  - constructor input, Append 반환 chain, `All`, `Copy`, `TerminalResult.FailureCauses`의 element를 직접 바꾸고 원본 chain/result를 재조회한다.
  - required stage/code의 empty/invalid와 optional consumer/filter/rule id의 invalid stable-token table을 추가한다.
- Before (`terminal_test.go:43-49`):

```go
for i := range causes5 {
	c, _ := NewFailureCause("stage", "code", "", "", "")
	causes5[i] = c
}
```

- After:

```go
// codes code0..code4
// NewFailureCauseChain retains code1..code4 in order.
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/terminal_test.go`: distinct retention, direct mutation isolation, invalid token table
- 테스트 작성:
  - `TestFailureCauseChainCapsAtFour`: cap, order, Append, constructor/accessor/copy isolation, token rejection을 검증한다.
  - `TestTerminalResultValidation`: cause accessor element mutation이 stored result에 영향 없음을 검증한다.
- 중간 검증:
  - `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_API-1`의 behavior/test를 먼저 맞춘 뒤 test-only `REVIEW_API-2`, `REVIEW_API-3`을 보강한다.
3. 이 subtask가 PASS/complete 되기 전 dependent sibling `04+01,03_codec_contract_fixtures`를 구현하거나 리뷰하지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/streamgate/event.go` | REVIEW_API-1 |
| `packages/go/streamgate/event_test.go` | REVIEW_API-1 |
| `packages/go/streamgate/filter_contract_test.go` | REVIEW_API-2 |
| `packages/go/streamgate/terminal_test.go` | REVIEW_API-3 |

## 최종 검증

1. `gofmt -d packages/go/streamgate/event.go packages/go/streamgate/event_test.go packages/go/streamgate/filter_contract_test.go packages/go/streamgate/terminal_test.go`
   - 기대 결과: stdout 없음
2. `make proto`
   - 기대 결과: 성공
3. `go test -count=1 ./packages/go/streamgate -run '^Test(ResponseStartRejectsUnsafeHeaders|EvidenceBatchDefensivelyCopiesInputsAndAccessors|FailureCauseChainCapsAtFour|TerminalResultValidation|ReleaseSinkContractCompiles)$'`
   - 기대 결과: PASS
4. `go test -count=1 ./packages/go/streamgate`
   - 기대 결과: PASS
5. `go vet ./packages/go/streamgate`
   - 기대 결과: PASS, stdout 없음
6. `go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport`
   - 기대 결과: PASS
7. `rg --sort path -n -i 'content-length|transfer-encoding|connection' packages/go/streamgate/event.go packages/go/streamgate/event_test.go`
   - 기대 결과: forbidden header validation과 regression case가 모두 출력됨
8. `git diff --check -- packages/go/streamgate proto/gen/iop`
   - 기대 결과: stdout 없음

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