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

# Stream Gate Evaluation Set 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 공백은 사용자 리뷰 요청이 아니다.

## 배경

failure disposition derivation과 defensive validation 자체는 구현됐고 fresh package/transport 검증도 통과했다. 그러나 이전 loop의 package-local decision-id mismatch set 회귀가 새 disposition 테스트로 대체됐고, `go test -run`은 존재하지 않는 테스트 이름을 포함해도 성공했다. production 코드는 바꾸지 않고 누락된 set-boundary 회귀와 테스트 존재성 검증만 복원한다.

## 사용자 리뷰 요청 흐름

선택된 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_G01_2.log`
- 이전 review: `agent-task/m-stream-evidence-gate-core/02+01_evaluation_contract/code_review_local_G01_2.log`
- Verdict: `FAIL`
- Required:
  - 별도 `TestEvaluationSetRejectsForgedDecisionIDMismatch`가 삭제돼 forged wrapper/decision id 불일치의 `Validate()`/`NewEvaluationSet` 회귀가 사라졌다.
  - `evaluated_violation_with_observe_blocking_fatal`은 계획된 두 경계 중 `Validate()`만 확인한다.
  - focused `go test -run`이 누락된 테스트 이름을 숨기므로 테스트 존재성 검증이 필요하다.
- 영향 파일: `packages/go/streamgate/evaluation_contract_test.go`
- 실제 검증: Go `1.24.12` 명시 `GOROOT`에서 gofmt/focused/package/vet/proto/transport/import/diff 검증은 통과했다. `go test -list`는 요구된 다섯 이름 중 `TestEvaluationSetRejectsForgedDecisionIDMismatch`를 제외한 네 개만 출력했다.
- Roadmap carryover: SDD S12 complete outcome set의 identity/failure-policy set-boundary evidence를 복원하되 이 하위 계약 plan 자체는 `parallel-evaluation` 완료를 주장하지 않는다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/evaluation_contract.go`
- `packages/go/streamgate/evaluation_contract_test.go`
- `packages/go/streamgate/filter_contract.go`의 `FilterDecision`/`FilterOutcome` validation 정의
- `packages/go/streamgate/terminal.go`의 `StableToken` 정의
- `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/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_G01_2.log`
- `agent-task/m-stream-evidence-gate-core/02+01_evaluation_contract/code_review_local_G01_2.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`
- 상태: `[승인됨]`, SDD 잠금 `해제`, 사용자 리뷰 `없음`
- 대상: S12 / Milestone Task `parallel-evaluation`
- Evidence Map S12: evaluated/deferred/not-applicable mixed outcomes, complete outcome set, failure/cancel policy
- 이번 follow-up은 complete outcome set의 wrapper/decision identity와 disposition이 `EvaluationSet` 경계에서도 방어된다는 회귀만 복원한다. barrier/race/backpressure/cancel evidence는 `05+02_parallel_evaluation_coordinator` 소유다.

### 테스트 환경 규칙

- `test_env`: local
- `agent-test/local/rules.md`: 존재하며 읽었다.
- 매칭 profile: `agent-test/local/platform-common-smoke.md`
- 기본 `/config/opt/go`는 binary/source 불일치로 crypto 표준 라이브러리 consumer가 compile되지 않는다.
- 정상 cached toolchain `/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64`에 `GOROOT`와 `GOTOOLCHAIN=local`을 명시하면 streamgate 및 Edge/Node transport 검증이 통과한다.
- 적용 명령: toolchain 확인, test-name 존재성 diff, fresh focused/package test, vet, `make proto`, Edge/Node transport, stdlib import, gofmt/diff/proto status
- 외부 runner, secret, service, `<확인 필요>`는 없다.

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

- constructor wrapper/decision mismatch: `TestEpochFilterOutcomeRejectsDecisionIDMismatch`가 커버한다.
- package-local forged wrapper/decision mismatch: production `Validate()`는 거부하지만 독립 `Validate()`/`NewEvaluationSet` 회귀가 삭제됐다.
- forged disposition mismatch: 다섯 family가 `Validate()`를 확인하고 네 case만 `NewEvaluationSet`을 확인한다. evaluated violation의 set assertion이 빠졌다.
- 테스트 이름 존재성: 기존 `-run` 명령만으로는 누락을 실패로 만들지 못한다.

### 심볼 참조

- production rename/remove 없음.
- 누락된 test symbol `TestEvaluationSetRejectsForgedDecisionIDMismatch`는 이전 final verification 목록에 남아 있었으나 현재 Go test 목록에는 없다.

### 분할 판단

- split decision policy를 적용했다.
- shared task group: `m-stream-evidence-gate-core`
- 현재 subtask: `02+01_evaluation_contract`
- predecessor `01_event_contract_types`: `agent-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`로 충족됐다.
- 한 테스트 파일의 동일 `EpochFilterOutcome.Validate()` → `NewEvaluationSet()` 방어 경계를 복원하는 test-only unit이다. 별도 child로 나누면 fixture와 검증만 중복되므로 현재 subtask의 단일 follow-up이 안전하다.

### 범위 결정 근거

- 수정 허용: `packages/go/streamgate/evaluation_contract_test.go`
- `evaluation_contract.go`, `filter_contract.go`, `terminal.go`, proto/config/Edge/Node/contract/spec/roadmap 문서는 변경하지 않는다.
- production validation은 fresh 검토에서 올바르므로 API나 오류 문구를 바꾸지 않는다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`
- build closures: scope/context/verification/evidence/ownership/decision 모두 `true`
  - scores: scope coupling 1, state/concurrency 0, blast/irreversibility 0, evidence diagnosis 1, 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 0, blast/irreversibility 0, evidence diagnosis 2, verification complexity 1
  - result: `local`, `G04`, `CODE_REVIEW-local-G04.md`

## 구현 체크리스트

- [ ] `REVIEW_API-4` forged decision-id mismatch의 독립 `Validate()`/`NewEvaluationSet` 회귀를 복원하고 disposition 대표 case의 누락된 set assertion과 테스트 이름 존재성 검증을 추가한다.
- [ ] 계획의 전체 local 검증 명령을 정상 Go 1.24.12와 명시적 `GOROOT`에서 fresh 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_API-4] Evaluation-set boundary regression restoration

- 문제:
  - `packages/go/streamgate/evaluation_contract_test.go:851-870`은 constructor mismatch만 검증하고 package-local forged entry의 set boundary 회귀가 없다.
  - `packages/go/streamgate/evaluation_contract_test.go:906-921`은 evaluated violation disposition mismatch에 `NewEvaluationSet` assertion이 없다.
  - 이전 final `go test -run`은 삭제된 테스트 이름이 없어도 exit 0이었다.
- 해결 방법:
  - `TestEvaluationSetRejectsForgedDecisionIDMismatch`를 별도 테스트로 복원한다. wrapper token은 `wrapper.id`, decision filter id는 `other.id`, pass+blocking의 canonical disposition은 `none`으로 구성해 identity mismatch만 검증한다.
  - 같은 forged entry에 `Validate()`와 `NewEvaluationSet([]EpochFilterOutcome{forged})`를 각각 호출하고 둘 다 `filter id mismatch`를 포함한 오류로 거부되는지 확인한다.
  - `evaluated_violation_with_observe_blocking_fatal`에도 `NewEvaluationSet` rejection assertion을 추가한다.
  - focused test 전 `go test -list` 결과를 기대한 두 forged regression 이름과 `diff -u`로 비교해 누락을 실패로 만든다.
- Before (`evaluation_contract_test.go:851-877`, `evaluation_contract_test.go:916-921`):

```go
func TestEpochFilterOutcomeRejectsDecisionIDMismatch(t *testing.T) {
	// constructor boundary only
}

func TestEvaluationSetRejectsForgedFailureDispositionMismatch(t *testing.T) {
	// replacement test; the distinct decision-id set regression is absent
}

// In existing TestEvaluationSetRejectsForgedFailureDispositionMismatch, within evaluated_violation_with_observe_blocking_fatal case:
//   forged := ... // constructed inside the case block
//   if err := forged.Validate(); err == nil {
//       t.Error("Validate() should reject ...")
//   }
//   // no NewEvaluationSet assertion
```

- After:

```go
func TestEvaluationSetRejectsForgedDecisionIDMismatch(t *testing.T) {
	forged := EpochFilterOutcome{
		filterID:           mustToken("wrapper.id"),
		outcome:            mustEvalOutcome(t, newPassDecision(t, "other.id")),
		enforcement:        FilterEnforcementBlocking,
		failureDisposition: EvaluationFailureDispositionNone,
	}
	if err := forged.Validate(); err == nil {
		t.Fatal("Validate() should reject forged decision id mismatch")
	}
	if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
		t.Fatal("NewEvaluationSet should reject forged decision id mismatch")
	}
}

// 기존 TestEvaluationSetRejectsForgedFailureDispositionMismatch 내 evaluated_violation_with_observe_blocking_fatal case 블록에 추가:
//   forged := ... // case 블록 내부에서 이미 constructed
//   if _, err := NewEvaluationSet([]EpochFilterOutcome{forged}); err == nil {
//       t.Error("NewEvaluationSet should reject forged evaluated+violation disposition")
//   }
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/evaluation_contract_test.go`: 독립 decision-id set regression 복원
  - [ ] `packages/go/streamgate/evaluation_contract_test.go`: evaluated violation disposition case의 set assertion 추가
- 테스트 작성:
  - `TestEvaluationSetRejectsForgedDecisionIDMismatch`: package-local forged entry가 `Validate()`와 `NewEvaluationSet`에서 같은 identity invariant로 거부됨을 검증한다.
  - `TestEvaluationSetRejectsForgedFailureDispositionMismatch` 내 `evaluated_violation_with_observe_blocking_fatal` case에 `NewEvaluationSet` rejection assertion을 inline로 추가한다.
- 중간 검증:
  - `review_go_root=/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64; diff -u <(printf '%s\n' 'TestEvaluationSetRejectsForgedDecisionIDMismatch' 'TestEvaluationSetRejectsForgedFailureDispositionMismatch') <(env GOROOT="$review_go_root" GOTOOLCHAIN=local "$review_go_root/bin/go" test ./packages/go/streamgate -list '^TestEvaluationSetRejectsForged(DecisionID|FailureDisposition)Mismatch$' | sed -n '/^Test/p' | LC_ALL=C sort)`
  - `review_go_root=/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64; env GOROOT="$review_go_root" GOTOOLCHAIN=local "$review_go_root/bin/go" test -count=1 ./packages/go/streamgate -run '^Test(EpochFilterOutcomeRejectsDecisionIDMismatch|EvaluationSetRejectsForgedDecisionIDMismatch|EvaluationSetRejectsForgedFailureDispositionMismatch)$'`
  - 기대 결과: 두 regression 이름이 정확히 존재하고 focused test PASS

## 의존 관계 및 구현 순서

1. predecessor `01_event_contract_types` 완료 근거: `agent-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`
2. 독립 decision-id set regression을 복원하고 disposition case의 set assertion을 추가한다.
3. test-name 존재성 검증 뒤 focused/full 검증을 실행한다.
4. 이 subtask가 PASS하고 `complete.log`를 만들기 전에는 `05+02_parallel_evaluation_coordinator` 구현을 시작하지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/streamgate/evaluation_contract_test.go` | REVIEW_API-4 |

## 최종 검증

1. `review_go_root=/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64; env GOROOT="$review_go_root" GOTOOLCHAIN=local "$review_go_root/bin/go" version && env GOROOT="$review_go_root" GOTOOLCHAIN=local "$review_go_root/bin/go" env GOROOT`
   - 기대 결과: Go `1.24.12`, `GOROOT`가 `$review_go_root`
2. `review_go_root=/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64; env GOROOT="$review_go_root" "$review_go_root/bin/gofmt" -d packages/go/streamgate/evaluation_contract_test.go`
   - 기대 결과: stdout 없음
3. `review_go_root=/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64; diff -u <(printf '%s\n' 'TestEvaluationSetRejectsForgedDecisionIDMismatch' 'TestEvaluationSetRejectsForgedFailureDispositionMismatch') <(env GOROOT="$review_go_root" GOTOOLCHAIN=local "$review_go_root/bin/go" test ./packages/go/streamgate -list '^TestEvaluationSetRejectsForged(DecisionID|FailureDisposition)Mismatch$' | sed -n '/^Test/p' | LC_ALL=C sort)`
   - 기대 결과: stdout 없음
4. `review_go_root=/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64; env GOROOT="$review_go_root" GOTOOLCHAIN=local "$review_go_root/bin/go" test -count=1 ./packages/go/streamgate -run '^Test(EpochFilterOutcomeValidatesFailurePolicy|EpochFilterOutcomeRejectsDecisionIDMismatch|EvaluationSetRejectsForgedDecisionIDMismatch|EvaluationSetRejectsForgedFailureDispositionMismatch|EpochFilterSnapshotsEvaluatorID)$'`
   - 기대 결과: PASS
5. `review_go_root=/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64; env GOROOT="$review_go_root" GOTOOLCHAIN=local "$review_go_root/bin/go" test -count=1 ./packages/go/streamgate`
   - 기대 결과: PASS
6. `review_go_root=/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64; env GOROOT="$review_go_root" GOTOOLCHAIN=local "$review_go_root/bin/go" vet ./packages/go/streamgate`
   - 기대 결과: PASS, stdout 없음
7. `make proto`
   - 기대 결과: 성공, proto 생성물 추가 diff 없음
8. `review_go_root=/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64; env GOROOT="$review_go_root" GOTOOLCHAIN=local "$review_go_root/bin/go" test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport`
   - 기대 결과: PASS
9. `review_go_root=/config/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.12.linux-arm64; env GOROOT="$review_go_root" GOTOOLCHAIN=local "$review_go_root/bin/go" list -f '{{range .Imports}}{{println .}}{{end}}' ./packages/go/streamgate | LC_ALL=C sort`
   - 기대 결과: `context`, `errors`, `sort`, `strconv`, `strings`, `time`만 출력
10. `git diff --check -- packages/go/streamgate proto/gen/iop`
    - 기대 결과: stdout 없음
11. `git status --short -- proto/gen/iop`
    - 기대 결과: stdout 없음

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