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

# Stream Gate Evaluation Stable Identity 보완 계획

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

- 아래 구현 체크리스트와 검증 명령을 완료하고 `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 공백은 사용자 리뷰 요청이 아니다.

## 배경

첫 리뷰에서 resolved `EpochFilter`의 id가 evaluator mutation을 따라 바뀌고, evaluated outcome의 wrapper id와 decision id가 달라도 생성되는 문제가 재현됐다. 두 경로 모두 filter-id별 complete outcome set이라는 SDD S12 계약을 깨뜨리므로, 생성 시 identity snapshot과 outcome identity 일치 검증을 같은 실행 계약 경계에서 보완한다. 이 subtask는 계속 Milestone Task 완료를 주장하지 않는다.

## 사용자 리뷰 요청 흐름

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

## Archive Evidence Snapshot

- 이전 plan: `agent-task/m-stream-evidence-gate-core/02+01_evaluation_contract/plan_local_G03_0.log`
- 이전 review: `agent-task/m-stream-evidence-gate-core/02+01_evaluation_contract/code_review_local_G03_0.log`
- Verdict: `FAIL`
- Required:
  - `EpochFilter.ID()`가 생성 시 검증한 id를 snapshot하지 않아 mutable evaluator id를 따라 바뀐다.
  - `NewEpochFilterOutcome`이 wrapper filter id와 evaluated decision filter id 불일치를 허용한다.
  - failure-policy test의 not-applicable/deferred case가 실제로는 evaluated pass outcome을 만든다.
- 영향 파일: `packages/go/streamgate/evaluation_contract.go`, `packages/go/streamgate/evaluation_contract_test.go`
- 실제 검증: 계획의 gofmt/focused/full package/vet/proto/transport/import/diff 명령은 통과했지만 공개 API 집중 재현은 `stored_id_after_mutation=mutated.id`, `mismatch_error=<nil> wrapper_id=claimed.id decision_id=actual.id`를 출력했다.
- Roadmap carryover: SDD S12의 stable filter identity와 complete outcome set을 보완하되 이 하위 계약 plan 자체는 `parallel-evaluation` 완료를 주장하지 않는다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/evaluation_contract.go`
- `packages/go/streamgate/evaluation_contract_test.go`
- `packages/go/streamgate/filter_contract.go`
- `packages/go/streamgate/filter_contract_test.go`
- `packages/go/streamgate/validation_regression_test.go`
- `packages/go/streamgate/terminal.go`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-spec/index.md`
- `agent-contract/index.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-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`
- `agent-task/m-stream-evidence-gate-core/02+01_evaluation_contract/plan_local_G03_0.log`
- `agent-task/m-stream-evidence-gate-core/02+01_evaluation_contract/code_review_local_G03_0.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`
- 상태: `[승인됨]`, SDD 잠금 `해제`, 사용자 리뷰 `없음`
- 대상 시나리오: S12 / `parallel-evaluation`
- Evidence Map S12는 evaluated/deferred/not-applicable mixed outcome, complete outcome set, failure/cancel policy와 stable filter identity를 요구한다.
- 이번 후속은 S12의 실행 입력/결과 identity 불변조건만 복구한다. fan-out, barrier, backpressure, cancel/race evidence는 dependent `05+02_parallel_evaluation_coordinator`가 소유한다.

### 테스트 환경 규칙

- `test_env`: local
- `agent-test/local/rules.md`: 존재하며 읽었다.
- 매칭 profile: `agent-test/local/platform-common-smoke.md`
- 적용 명령: `make proto`, fresh 대상 package 테스트, Edge/Node transport 테스트, `go vet`, `gofmt -d`, scoped `git diff --check`
- 외부 runner, secret, service, `<확인 필요>`는 없다.
- proto/wire를 바꾸지 않지만 기존 package 소비 회귀를 확인하기 위해 profile의 proto/transport 명령을 유지한다.

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

- mutable evaluator id: 기존 테스트는 evaluator가 고정 id를 반환하는지만 확인하며 생성 후 mutation을 검증하지 않는다.
- evaluated wrapper/decision id mismatch: constructor와 forged entry/set validation 회귀가 없다.
- not-applicable/deferred failure disposition: 이름만 해당 case이고 실제 입력은 evaluated pass여서 해당 outcome kind를 검증하지 않는다.
- stable order, duplicate id, accessor slice 교체, raw error code 보존은 기존 테스트가 커버한다.

### 심볼 참조

- rename/remove 없음.
- `EpochFilter`, `NewEpochFilterOutcome`, `EvaluationSet`의 production caller는 아직 없고 dependent coordinator plan만 이 계약을 소비할 예정이다.
- 새 dependency 없음.

### 분할 판단

- split decision policy를 후속 plan 파일 선택 전에 적용했다.
- shared task group: `m-stream-evidence-gate-core`
- 현재 subtask: `02+01_evaluation_contract`
- predecessor `01`은 `agent-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`로 충족됐다.
- stable binding id와 evaluated outcome id는 같은 filter identity 불변조건이며 동일 production/test 두 파일과 같은 focused 검증을 공유한다. 별도 subtask로 나누면 생성자/validation 회귀가 분산되므로 기존 subtask의 단일 follow-up이 더 안전하다.
- active sibling `03`, `04`, `05`는 다른 파일과 후속 coordinator 범위를 소유하므로 파일 충돌이 없다.

### 범위 결정 근거

- 수정 허용: `packages/go/streamgate/evaluation_contract.go`, `packages/go/streamgate/evaluation_contract_test.go`
- 기존 `filter_contract.go`, event/terminal 타입, Edge/Node/proto/config/contract/spec/roadmap 문서는 변경하지 않는다.
- failure disposition 의미, Registry, goroutine fan-out, Arbiter, queue/backpressure는 이번 Required와 무관하므로 확장하지 않는다.

### 최종 라우팅

- 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 1, verification complexity 1
  - result: `local`, `G04`, `CODE_REVIEW-local-G04.md`

## 구현 체크리스트

- [ ] `REVIEW_API-1` `EpochFilter`가 생성 시 검증한 evaluator id를 immutable snapshot으로 보존하고 evaluator mutation 뒤에도 같은 id를 반환하도록 구현·테스트한다.
- [ ] `REVIEW_API-2` evaluated outcome의 wrapper/decision filter id 일치를 constructor와 validation/set 경계에서 강제하고 실제 not-applicable/deferred failure-policy 입력을 검증하도록 구현·테스트한다.
- [ ] 계획의 전체 local 검증 명령을 fresh cache 조건으로 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_API-1] Resolved binding id snapshot

- 문제:
  - `packages/go/streamgate/evaluation_contract.go:85-90`은 evaluator id를 검증하지만 `EpochFilter`에 저장하지 않는다.
  - `packages/go/streamgate/evaluation_contract.go:109`의 `ID()`가 evaluator를 다시 호출하므로 생성 후 evaluator state가 바뀌면 epoch identity도 바뀐다.
- 해결 방법:
  - `EpochFilter`에 검증된 `StableToken` id를 저장한다.
  - 생성자는 `NewStableTokenRequired("filterID", evaluator.ID())` 결과를 snapshot하고, `ID()`는 evaluator를 다시 호출하지 않고 snapshot string을 반환한다.
  - `Evaluator()` seam과 평가 동작은 유지한다.
- Before (`evaluation_contract.go:85-109`):

```go
id := evaluator.ID()
if err := validateStableTokenRequired("filterID", id); err != nil {
	return EpochFilter{}, err
}

func (f EpochFilter) ID() string { return f.evaluator.ID() }
```

- After:

```go
id, err := NewStableTokenRequired("filterID", evaluator.ID())
if err != nil {
	return EpochFilter{}, err
}

func (f EpochFilter) ID() string { return f.id.String() }
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/evaluation_contract.go`: validated id snapshot과 accessor
  - [ ] `packages/go/streamgate/evaluation_contract_test.go`: mutable evaluator id 회귀
- 테스트 작성:
  - `TestEpochFilterSnapshotsEvaluatorID`: 생성 뒤 stub evaluator의 id를 바꿔도 `EpochFilter.ID()`는 원래 id이고 `Evaluator().ID()`만 변경된 값을 보이는지 검증한다.
- 중간 검증:
  - `go test -count=1 ./packages/go/streamgate -run '^TestEpochFilter(SnapshotsEvaluatorID|NormalizesApplicabilityAndRequirement|RejectsInvalidExecutionContract)$'`
  - 기대 결과: PASS

### [REVIEW_API-2] Evaluated outcome identity consistency

- 문제:
  - `packages/go/streamgate/evaluation_contract.go:263-319`은 wrapper `filterID`와 evaluated `FilterDecision.FilterID()`를 비교하지 않는다.
  - `packages/go/streamgate/evaluation_contract.go:425-456`의 `Validate()`도 이 불일치를 거부하지 않아 forged entry가 `NewEvaluationSet`을 통과할 수 있다.
  - `packages/go/streamgate/evaluation_contract_test.go:431-442`의 not-applicable/deferred case는 실제로 evaluated pass outcome을 만든다.
- 해결 방법:
  - evaluated outcome이면 constructor와 `EpochFilterOutcome.Validate()`에서 decision 존재 확인 뒤 `decision.FilterID() == filterID`를 강제한다.
  - mismatch 오류에는 raw 값 대신 안정적인 계약 오류만 반환한다.
  - public constructor mismatch와 package-local forged entry의 `NewEvaluationSet` rejection을 모두 회귀 테스트한다.
  - failure-policy table은 not-applicable/deferred case에서 실제 해당 constructor를 사용하고 outcome kind와 disposition을 함께 검증한다.
- Before (`evaluation_contract.go:283-300`):

```go
case FilterOutcomeKindEvaluated:
	decision := outcome.Decision()
	if decision == nil {
		return EpochFilterOutcome{}, errors.New("streamgate: evaluated outcome must carry a decision")
	}
```

- After:

```go
case FilterOutcomeKindEvaluated:
	decision := outcome.Decision()
	if decision == nil {
		return EpochFilterOutcome{}, errors.New("streamgate: evaluated outcome must carry a decision")
	}
	if decision.FilterID() != fid.String() {
		return EpochFilterOutcome{}, errors.New("streamgate: evaluated outcome filter id mismatch")
	}
```

- Before (`evaluation_contract.go:428-440`):

```go
case FilterOutcomeKindEvaluated:
	if o.outcome.Decision() == nil {
		return errors.New("streamgate: evaluated outcome must carry a decision")
	}
```

- After (`evaluation_contract.go:428-440`):

```go
case FilterOutcomeKindEvaluated:
	if o.outcome.Decision() == nil {
		return errors.New("streamgate: evaluated outcome must carry a decision")
	}
	if o.outcome.Decision().FilterID() != o.filterID.value {
		return errors.New("streamgate: evaluated outcome filter id mismatch")
	}
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/evaluation_contract.go`: constructor/Validate identity consistency gate
  - [ ] `packages/go/streamgate/evaluation_contract_test.go`: constructor mismatch, forged set entry, actual not-applicable/deferred cases
- 테스트 작성:
  - `TestEpochFilterOutcomeRejectsDecisionIDMismatch`: public constructor가 mismatched evaluated decision을 거부한다.
  - `TestEvaluationSetRejectsForgedDecisionIDMismatch`: package-local forged entry가 `Validate()`/`NewEvaluationSet`을 통과하지 못한다.
  - `TestEpochFilterOutcomeValidatesFailurePolicy`: not-applicable/deferred 입력을 실제 outcome kind로 고치고 `none` disposition을 검증한다.
- 중간 검증:
  - `go test -count=1 ./packages/go/streamgate -run '^Test(EpochFilterOutcomeRejectsDecisionIDMismatch|EvaluationSetRejectsForgedDecisionIDMismatch|EpochFilterOutcomeValidatesFailurePolicy)$'`
  - 기대 결과: 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`에서 epoch filter id snapshot을 고정한다.
3. `REVIEW_API-2`에서 같은 stable identity를 evaluated outcome/set 경계까지 강제한다.
4. 이 subtask가 PASS하고 `complete.log`를 만들기 전에는 `05+02_parallel_evaluation_coordinator` 구현을 시작하지 않는다.

## 수정 파일 요약

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

## 최종 검증

1. `gofmt -d packages/go/streamgate/evaluation_contract.go packages/go/streamgate/evaluation_contract_test.go`
   - 기대 결과: stdout 없음
2. `go test -count=1 ./packages/go/streamgate -run '^Test(EpochFilterSnapshotsEvaluatorID|EpochFilterOutcomeRejectsDecisionIDMismatch|EvaluationSetRejectsForgedDecisionIDMismatch|EpochFilterNormalizesApplicabilityAndRequirement|EpochFilterRejectsInvalidExecutionContract|EvaluationSetIsStableAndImmutable|EpochFilterOutcomeValidatesFailurePolicy)$'`
   - 기대 결과: PASS
3. `go test -count=1 ./packages/go/streamgate`
   - 기대 결과: PASS
4. `go vet ./packages/go/streamgate`
   - 기대 결과: PASS, stdout 없음
5. `make proto`
   - 기대 결과: 성공, proto 생성물 추가 diff 없음
6. `go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport`
   - 기대 결과: PASS
7. `go list -f '{{range .Imports}}{{println .}}{{end}}' ./packages/go/streamgate | LC_ALL=C sort`
   - 기대 결과: Go 표준 라이브러리 import만 있고 `iop/apps/*/internal` 또는 `iop/proto/gen/iop`가 없음
8. `git diff --check -- packages/go/streamgate proto/gen/iop`
   - 기대 결과: stdout 없음

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