<!-- task=m-stream-evidence-gate-core/18+14,16_runtime_contract plan=2 tag=REVIEW_CORE_RUNTIME_CONTRACT -->

# Plan - Runtime contract validation 보완

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

구현과 테스트, 실제 검증 evidence만 소유한다. 아래 체크리스트를 완료한 뒤 활성 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 변경 내용과 명령의 실제 stdout/stderr를 채우고 active pair를 그대로 둔 채 리뷰 준비 완료를 보고한다. 차단되면 구현 에이전트 소유 evidence 필드에 정확한 blocker, 시도한 명령/출력, 재개 조건만 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하거나 control-plane stop 파일을 만들거나 다음 상태를 분류하지 않는다. verdict, log archive, `complete.log`, task archive는 code-review 전용이다.

## 배경

첫 리뷰에서 `RuntimeOptions`가 16 MiB 절대 상한을 강제하지 않고 `RequestRuntimeSnapshot`이 registry generation 불일치를 허용하는 두 public contract 결함이 재현됐다. 후속 구현은 두 validation과 직접 회귀 테스트만 보완해 다음 owner loop가 invalid request-local snapshot을 받을 수 없도록 한다.

## Archive Evidence Snapshot

- 이전 pair:
  - Plan: `agent-task/m-stream-evidence-gate-core/18+14,16_runtime_contract/plan_local_G08_1.log`
  - Review: `agent-task/m-stream-evidence-gate-core/18+14,16_runtime_contract/code_review_cloud_G08_1.log`
- Verdict: `FAIL`
- Required:
  - `packages/go/streamgate/runtime.go:58`: `DefaultMaxIngressSnapshotBytes+1`이 `NewRuntimeOptions`를 통과한다.
  - `packages/go/streamgate/runtime.go:154`: request `configGen`과 `FilterRegistrySnapshot.Generation()` 불일치가 `NewRequestRuntimeSnapshot`을 통과한다.
- Suggested/Nit: 없음
- 영향 파일: `packages/go/streamgate/runtime.go`, `packages/go/streamgate/runtime_contract_test.go`
- 검증 evidence:
  - 기존 `go test -count=1 ./packages/go/streamgate`, `go test -race -count=1 ./packages/go/streamgate`, `git diff --check`는 PASS했다.
  - 리뷰 전용 임시 `TestReview*` fixture는 위 두 invalid input이 모두 accepted되어 FAIL했고, 임시 파일은 제거됐다.
- Roadmap carryover: 특정 Milestone Task 완료를 주장하지 않는 하위 contract 보완이므로 `Roadmap Targets` 없음.
- 상세 재읽기가 필요하면 위 두 archive log만 읽는다. `agent-task/archive/**`를 탐색하지 않는다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/runtime.go` — 전체 public runtime option/request snapshot contract.
- `packages/go/streamgate/runtime_contract_test.go` — 전체 Noop/options/snapshot contract tests.
- `packages/go/streamgate/filter_registry.go` — registry generation match와 request snapshot 경계.
- `packages/go/streamgate/ingress_snapshot.go` — ingress snapshot max-byte lifecycle 경계.
- `packages/go/streamgate/recovery_coordinator.go`, `packages/go/streamgate/recovery_plan.go`, `packages/go/streamgate/parallel_evaluation.go` — runtime options가 묶는 coordinator/host seam 경계.
- `packages/go/streamgate/filter_contract.go`, `packages/go/streamgate/evidence_tail.go`, `packages/go/streamgate/terminal.go` — Noop/evidence/stable token 관련 계약.
- `packages/go/config/edge_types.go`, `packages/go/config/stream_evidence_gate_config_test.go` — 16 MiB config absolute cap과 boundary fixture.
- `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.md` — local Go/runtime 검증 규칙과 platform-common profile.
- `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` — 활성 범위, 잠금, acceptance/evidence 기준.
- `agent-task/archive/2026/07/m-stream-evidence-gate-core/14+13_recovery_execution/complete.log`, `agent-task/archive/2026/07/m-stream-evidence-gate-core/16+15_ingress_snapshot_allocation/complete.log` — split predecessor 완료 근거.
- `agent-task/m-stream-evidence-gate-core/18+14,16_runtime_contract/plan_local_G08_1.log`, `agent-task/m-stream-evidence-gate-core/18+14,16_runtime_contract/code_review_cloud_G08_1.log` — 현재 follow-up의 원본 범위와 raw findings.

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `[승인됨]`, 잠금 `해제`, 사용자 리뷰 없음.
- S11 / Milestone Task `filter-registry`: request와 registry는 같은 config generation을 유지해야 한다. 이 기준이 generation match validation과 match/mismatch fixture를 요구한다.
- S22 / Milestone Task `request-rebuilder`: `max_ingress_snapshot_bytes`는 limit-1/limit/limit+1을 구분하고 16 MiB 초과를 거부해야 한다. 이 기준이 `RuntimeOptions` 상한과 경계 table fixture를 요구한다.
- 이 child는 위 Milestone Task 전체 완료가 아니라 owner loop 이전 contract 보완만 수행하므로 `Roadmap Targets`를 두지 않는다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`를 읽었고 PATH 첫 Go(`/config/.local/bin/go` → `/config/opt/go/bin/go`)와 `GOROOT=/config/opt/go`를 유지한다.
- `agent-test/local/platform-common-smoke.md`를 확인했다. 이 follow-up은 proto/config/wire를 바꾸지 않으므로 profile의 `make proto`와 Edge-Node transport smoke는 적용 대상이 아니다.
- 변경 패키지의 기존 plan과 Go test layout을 fallback source로 사용해 focused unit, fresh package unit, race, gofmt diff, `git diff --check`를 실행한다. 외부 runner/service/secret은 없다.

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

- 기본/0/음수 recovery와 ingress 0 검증은 존재한다.
- `RuntimeOptions`의 ingress limit-1/limit/limit+1 및 direct `Validate` upper bound coverage가 없다.
- `RequestRuntimeSnapshot`의 generation match/mismatch 및 direct `Validate` mismatch coverage가 없다.
- 이번 follow-up이 두 공백을 같은 contract test 파일에 추가한다.

### 심볼 참조

- rename/remove 없음. 새 dependency/package 없음.
- public constructor signatures는 유지하고 validation만 강화한다.

### 분할 판단

- split decision policy를 먼저 적용했다. 현재 subtask `18+14,16_runtime_contract`의 predecessor 14와 16은 각각 `agent-task/archive/2026/07/m-stream-evidence-gate-core/14+13_recovery_execution/complete.log`, `agent-task/archive/2026/07/m-stream-evidence-gate-core/16+15_ingress_snapshot_allocation/complete.log`로 충족된다.
- 두 finding은 같은 `runtime.go` constructor/`Validate` family와 같은 contract test 파일을 수정한다. API-vs-call-site rollout, 별도 domain, 외부 검증, 독립 배포 경계가 없으므로 추가 split은 인위적이다. 기존 subtask 경로의 단일 follow-up을 유지한다.

### 범위 결정 근거

- 포함: `packages/go/streamgate/runtime.go`, `packages/go/streamgate/runtime_contract_test.go`.
- 제외: config/registry/ingress 구현, coordinator owner loop, Edge host integration, SDD/roadmap 상태. 기존 source of truth는 이미 상한·generation invariant를 정의하므로 call-site나 문서 변경이 필요 없다.

### 최종 라우팅

- evaluation mode: `isolated-reassessment`; 이전 lane/G/점수/파일명은 중립 입력에서 제외했다.
- finalizer: `finalize-task-policy.sh`; mode: `pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 `true`. 두 invalid input, 두 수정 파일, focused/full/race 검증, shared dirty worktree 경계와 승인된 SDD로 닫혔다.
- build scores: scope coupling `1`, state/concurrency `0`, blast/irreversibility `1`, evidence/diagnosis `1`, verification complexity `1`; route basis `local-fit`, capability gap `none`, lane/G `local/G04`, filename `PLAN-local-G04.md`.
- build loop-risk: ordered transitions `state_count=0`, concurrency `actor_count=0`, boundary contract `component_count=3`/`consumer_count=1`/validation·immutability constraints, structured interpretation 없음, variant axes `2`지만 조합 검증 불필요. matched signature=`boundary_contract`, triggered=`true`; route/G floor=`none`.
- review closures: 여섯 closure 모두 `true`; scores `1/0/1/1/1`; route basis `official-review`, capability gap `none`, lane/G `cloud/G04`, filename `CODE_REVIEW-cloud-G04.md`, target `gpt-5.6-sol` xhigh.

## 구현 체크리스트

- [x] REVIEW_CORE_RUNTIME_CONTRACT-1 `RuntimeOptions`가 ingress snapshot byte limit `1..16 MiB`만 허용하고 limit-1/limit/limit+1 및 direct `Validate` 회귀 테스트를 통과하게 한다.
- [x] REVIEW_CORE_RUNTIME_CONTRACT-2 `RequestRuntimeSnapshot`이 request config generation과 registry generation 일치를 강제하고 match/mismatch 및 direct `Validate` 회귀 테스트를 통과하게 한다.
- [x] 계획의 focused unit, fresh package unit, race, gofmt diff, `git diff --check` 검증을 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_CORE_RUNTIME_CONTRACT-1] Ingress snapshot 절대 상한

- 문제: `packages/go/streamgate/runtime.go:58-62,86-90`은 양수만 확인해 SDD S22의 16 MiB 절대 상한보다 큰 값을 validated options로 만든다.
- 해결 방법: constructor가 최종 호출하는 `RuntimeOptions.Validate`의 범위를 `1..DefaultMaxIngressSnapshotBytes`로 강화하고 기존 오류 의미를 유지한다.

```go
// Before: packages/go/streamgate/runtime.go:86
if o.maxIngressSnapshotBytes <= 0 {
    return errors.New("streamgate: runtime options max ingress snapshot bytes must be positive")
}

// After
if o.maxIngressSnapshotBytes <= 0 || o.maxIngressSnapshotBytes > DefaultMaxIngressSnapshotBytes {
    return errors.New("streamgate: runtime options max ingress snapshot bytes must be between 1 and 16777216")
}
```

- 수정 파일 및 체크리스트:
  - [x] `packages/go/streamgate/runtime.go`: upper bound validation을 추가한다.
  - [x] `packages/go/streamgate/runtime_contract_test.go`: limit-1/limit/limit+1 constructor table과 direct `Validate` rejection을 추가한다.
- 테스트 작성: 작성. `TestRuntimeOptionsValidation` 하위에 `IngressAbsoluteCapBoundaries`, `DirectValidateRejectsIngressLimitPlusOne`을 추가해 16 MiB-1/16 MiB 성공, +1 실패를 단언한다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run '^TestRuntimeOptionsValidation$'` —
```
ok  	iop/packages/go/streamgate	0.002s
```

### [REVIEW_CORE_RUNTIME_CONTRACT-2] Registry generation 일치

- 문제: `packages/go/streamgate/runtime.go:154-156,203-205`은 registry generation의 존재만 확인해 request generation과 불일치한 snapshot을 허용한다.
- 해결 방법: `RequestRuntimeSnapshot.Validate`에서 non-empty 확인 직후 `registrySnapshot.Generation() == configGen.String()`을 강제한다. constructor는 조립 후 같은 `Validate`를 호출하므로 construction/revalidation이 동일 invariant를 사용한다.

```go
// Before: packages/go/streamgate/runtime.go:203
if r.registrySnapshot.Generation() == "" {
    return errors.New("streamgate: request runtime snapshot registry snapshot generation is required")
}

// After
registryGeneration := r.registrySnapshot.Generation()
if registryGeneration == "" {
    return errors.New("streamgate: request runtime snapshot registry snapshot generation is required")
}
if registryGeneration != r.configGen.String() {
    return errors.New("streamgate: request runtime snapshot config and registry generations must match")
}
```

- 수정 파일 및 체크리스트:
  - [x] `packages/go/streamgate/runtime.go`: generation equality invariant를 추가한다.
  - [x] `packages/go/streamgate/runtime_contract_test.go`: matching constructor success, mismatching constructor failure, direct `Validate` mismatch failure를 추가한다.
- 테스트 작성: 작성. `TestRequestRuntimeSnapshotContract` 하위에 `GenerationMismatch`와 `DirectValidateRejectsGenerationMismatch`를 추가하고 기존 `ValidConstruction`의 matching path를 유지한다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run '^TestRequestRuntimeSnapshotContract$'` —
```
ok  	iop/packages/go/streamgate	0.003s
```

## 의존 관계 및 구현 순서

1. REVIEW_CORE_RUNTIME_CONTRACT-1과 -2는 서로 독립 validation이지만 같은 파일을 수정하므로 함께 적용한다.
2. 두 focused test를 통과한 뒤 전체 package/race 검증을 실행한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/go/streamgate/runtime.go` | REVIEW_CORE_RUNTIME_CONTRACT-1, REVIEW_CORE_RUNTIME_CONTRACT-2 |
| `packages/go/streamgate/runtime_contract_test.go` | 두 public contract regression tests |

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`

_실제 출력:_
```
/config/.local/bin/go
/config/opt/go/bin/go
go version go1.26.2 linux/arm64
/config/opt/go
```

- `go test -count=1 ./packages/go/streamgate -run '^TestRuntimeOptionsValidation$'`

_실제 출력:_
```
ok  	iop/packages/go/streamgate	0.002s
```

- `go test -count=1 ./packages/go/streamgate -run '^TestRequestRuntimeSnapshotContract$'`

_실제 출력:_
```
ok  	iop/packages/go/streamgate	0.003s
```

- `go test -count=1 ./packages/go/streamgate -run '^(TestRuntimeOptionsValidation|TestRequestRuntimeSnapshotContract)$'`
- `go test -count=1 ./packages/go/streamgate`
- `go test -race -count=1 ./packages/go/streamgate`
- `gofmt -d packages/go/streamgate/runtime.go packages/go/streamgate/runtime_contract_test.go`
- `git diff --check`

_실제 출력:_
```
ok  	iop/packages/go/streamgate	0.006s
ok  	iop/packages/go/streamgate	0.869s
ok  	iop/packages/go/streamgate	1.913s
(no output from gofmt)
(no output from git diff --check)
```

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.
