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

# Stream Gate Evaluation 실행 계약 구현 계획

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

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

## 배경

`streamgate`에는 immutable `EvidenceBatch`와 네 가지 `FilterOutcome` 값만 있고, 병렬 evaluator가 소비할 active-filter binding과 결과 집합 계약은 없다. `parallel-evaluation`의 goroutine·queue 구현 전에 readiness, enforcement, deadline, stable filter identity를 검증 가능한 값으로 고정해야 동시성 코드가 Registry나 semantic filter 정책을 임의로 소유하지 않는다. 이 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

- 선행 task: `m-stream-evidence-gate-core/01_event_contract_types`
- 완료 근거: `agent-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`
- Verdict: `PASS`
- Carryover: immutable `EvidenceBatch`, evaluated/error/not-applicable/deferred `FilterOutcome`, sanitized `FilterDecision`과 recovery intent 계약이 fresh package/race-independent 검증을 통과했다.
- 현재 subtask는 위 타입을 변경하지 않고 병렬 실행기가 소비할 resolved epoch binding과 immutable outcome set을 추가한다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/filter_contract.go`
- `packages/go/streamgate/filter_contract_test.go`
- `packages/go/streamgate/validation_regression_test.go`
- `go.mod`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-spec/index.md`
- `agent-contract/index.md`
- `agent-roadmap/current.md`
- `agent-roadmap/priority-queue.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/m-stream-evidence-gate-core/03+01_event_contract_unit_tests/PLAN-local-G05.md`
- `agent-task/m-stream-evidence-gate-core/04+01,03_codec_contract_fixtures/PLAN-local-G04.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 잠금 `해제`, 사용자 리뷰 `없음`
- 대상 시나리오: S12 / `parallel-evaluation`
- Evidence Map S12는 evaluated/deferred/not-applicable mixed outcome, complete set, failure policy를 요구한다.
- 이 선행 plan은 S12의 실행 입력과 결과 표현만 고정한다. 실제 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 `go test -count=1` 대상 package 및 Edge/Node transport, `go vet`, `gofmt -d`, scoped `git diff --check`
- 현재 runner는 `go1.26.2 linux/arm64`, module 선언은 Go `1.24`다. baseline package, race, vet, transport 테스트가 모두 통과해 계획 작성 차단은 없다.
- `<확인 필요>`, 외부 runner, secret, service는 없다. wire/proto를 바꾸지 않으므로 full-cycle 외부 검증과 test-rule 갱신은 필요하지 않다.

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

- resolved filter identity/readiness/enforcement/deadline validation: 기존 타입과 테스트가 없다.
- event 미적용과 blocking trigger 미충족 정규화: 기존 outcome 값만 있고 실행 입력에서 이를 도출하는 테스트가 없다.
- filter id별 immutable outcome set, 중복 id 거부, completion order와 무관한 stable order: 기존 테스트가 없다.
- 기존 `EvidenceBatch` deep-copy와 네 `FilterOutcome` variant 자체는 기존 테스트가 커버한다.

### 심볼 참조

- rename/remove 없음.
- 새 third-party dependency 없음. `context`, `sort`, `time` 등 Go 표준 라이브러리만 사용한다.

### 분할 판단

- split decision policy를 plan 파일 선택 전에 적용했다.
- shared task group: `m-stream-evidence-gate-core`
- `02+01_evaluation_contract`: archived predecessor `01`의 event/filter 값 계약을 소비해 실행 binding과 immutable result set을 만든다.
- `05+02_parallel_evaluation_coordinator`: `02` 완료 뒤 fan-out, all-complete, single-flight, bounded backpressure를 구현한다.
- `02`의 predecessor index `01`은 `agent-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`로 충족됐다.
- active sibling `03`/`04`는 event contract 검증 파일을 소유하고, 이 plan은 새 파일 두 개만 소유하므로 파일 충돌이 없다.

### 범위 결정 근거

- 수정 허용: 새 `evaluation_contract.go`, `evaluation_contract_test.go`.
- 기존 `filter_contract.go`의 outcome/decision/batch 표현은 완료된 predecessor 계약이므로 수정하지 않는다.
- full `Filter` Registry의 model/provider applicability resolution, hold requirement 합성, config generation snapshot은 `filter-registry` Task에 남긴다.
- goroutine fan-out, queue, host shutdown, Arbiter callback은 dependent `05` 범위다.
- Edge/Node host, proto, config, agent-contract, agent-spec, roadmap 문서는 변경하지 않는다. 매칭 agent-spec/agent-contract 문서는 없고 외부 API/wire 계약도 바뀌지 않는다.

### 최종 라우팅

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

## 구현 체크리스트

- [ ] `API-1` resolved epoch filter binding이 event applicability와 blocking trigger readiness를 not-applicable/deferred/ready로 정규화하고 stable id, enforcement, positive timeout을 검증하도록 구현·테스트한다.
- [ ] `API-2` filter별 outcome과 blocking-fatal/observe-error 분류를 raw error 없이 보존하는 immutable stable-order evaluation set을 구현하고 duplicate/mismatch/defensive-copy 경계를 테스트한다.
- [ ] 계획의 전체 local 검증 명령을 fresh cache 조건으로 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Resolved epoch filter binding

- 문제:
  - `packages/go/streamgate/filter_contract.go:158-176`은 outcome 종류만 정의하며 어떤 filter가 ready/not-applicable/deferred인지 판정할 실행 입력이 없다.
  - full Registry를 먼저 구현하지 않고 coordinator에 raw bool과 callback을 흩뿌리면 stable id, timeout, enforcement 검증이 동시성 코드에 중복된다.
- 해결 방법:
  - 새 `FilterEvaluator`를 `ID() string`과 synchronous `Evaluate(context.Context, EvidenceBatch) (FilterDecision, error)`의 최소 실행 seam으로 둔다. full `Filter`/`FilterContext`/`Applies`/`HoldRequirement`는 `filter-registry` Task가 이 seam에 adapt한다.
  - `EpochFilter` 생성자는 evaluator, subscribed-event 존재, static trigger readiness, release blocking 여부, `blocking|observe_only` enforcement, positive evaluation timeout을 받는다.
  - Core normalization은 `subscribed=false → not_applicable_for_epoch`, `subscribed=true && trigger=false && blocks=true → deferred_by_requirement`, ready인 경우에만 evaluator 실행 대상으로 고정한다. nonblocking trigger 미충족은 not-applicable로 정규화한다.
- Before (`filter_contract.go:158-176`):

```go
FilterOutcomeKindNotApplicableForEpoch FilterOutcomeKind = "not_applicable_for_epoch"
FilterOutcomeKindDeferredByRequirement FilterOutcomeKind = "deferred_by_requirement"
```

- After (`evaluation_contract.go`):

```go
type FilterEvaluator interface {
	ID() string
	Evaluate(context.Context, EvidenceBatch) (FilterDecision, error)
}

func NewEpochFilter(
	evaluator FilterEvaluator,
	subscribedEventPresent, triggerReady, blocksRelease bool,
	enforcement FilterEnforcement,
	timeout time.Duration,
) (EpochFilter, error)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/evaluation_contract.go`: evaluator seam, closed enums, validated immutable binding/accessors
  - [ ] `packages/go/streamgate/evaluation_contract_test.go`: normal/invalid constructor matrix와 readiness normalization table
- 테스트 작성:
  - `TestEpochFilterNormalizesApplicabilityAndRequirement`: ready/not-applicable/deferred/nonblocking-unready table을 검증한다.
  - `TestEpochFilterRejectsInvalidExecutionContract`: nil evaluator, invalid/duplicate-style id, unknown enforcement, ready filter의 non-positive timeout을 거부한다.
- 중간 검증:
  - `go test -count=1 ./packages/go/streamgate -run '^TestEpochFilter(NormalizesApplicabilityAndRequirement|RejectsInvalidExecutionContract)$'`
  - 기대 결과: PASS

### [API-2] Immutable evaluation outcome set

- 문제:
  - `packages/go/streamgate/filter_contract.go:197-203`의 `FilterOutcome`은 filter identity와 enforcement를 포함하지 않고, `EvidenceBatch` 주석(`836-840`)은 결과가 외부 집합에 유지된다고만 규정한다.
  - all-complete 결과가 completion order나 raw error 객체를 보존하면 Arbiter 입력과 observation이 비결정적이거나 민감 원인을 노출할 수 있다.
- 해결 방법:
  - `EpochFilterOutcome`에 stable filter id, 기존 `FilterOutcome`, enforcement, `none|fatal|observe_error` failure disposition만 둔다.
  - failure disposition은 `NewEpochFilterOutcome` 시그니처에서 파라미터로 받지 않고, 입력 `FilterOutcome`의 `Kind`와 `FilterEnforcement` 조합에서 파생한다.
    - `FilterDecisionKind` pass/observe/replacement 이고 `FilterOutcomeKind` evaluation_error가 아니면 `none`
    - `FilterDecisionKind` violation 이고 `FilterEnforcement` blocking 이면 `blocking_fatal`
    - `FilterDecisionKind` violation 이고 `FilterEnforcement` observe_only 이면 `observe_error`
    - `FilterOutcomeKind` evaluation_error 이고 blocking 이면 `blocking_fatal`, observe_only 이면 `observe_error`
    - not_applicable/deferred 조합은 `none`
  - error 객체나 message는 저장하지 않고, `FilterOutcome`에 인코딩된 code로만 표현한다.
  - `EvaluationSet` 생성자는 모든 outcome을 검증하고 duplicate id를 거부한 뒤 filter id 오름차순으로 복사한다. accessor는 새 slice를 반환한다.
- Before (`filter_contract.go:836-840`):

```go
// Filter results are
// maintained outside the batch as a separate set of FilterOutcome values.
```

- After (`evaluation_contract.go`):

```go
type EpochFilterOutcome struct {
	filterID           StableToken  // filter_contract.go 정의
	outcome            FilterOutcome  // filter_contract.go 정의
	enforcement        FilterEnforcement  // filter_contract.go 정의
	failureDisposition EvaluationFailureDisposition  // new
}

// NewEpochFilterOutcome is defensive-by-construction. It derives failureDisposition
// from the input FilterOutcome Kind and FilterEnforcement, producing stable ordering.
type NewEpochFilterOutcome func(fId StableToken, fo FilterOutcome, fe FilterEnforcement) (EpochFilterOutcome, error)

type EvaluationSet struct {
	outcomes []EpochFilterOutcome
}

// NewEvaluationSet validates each entry via EpochFilterOutcome.Validate() and
// rejects duplicate filter IDs before sorting by id ascending.
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/evaluation_contract.go`: outcome wrapper, failure disposition, stable-order immutable set
  - [ ] `packages/go/streamgate/evaluation_contract_test.go`: duplicate id, evaluated id mismatch, stable sort, accessor mutation, raw-error absence
- 테스트 작성:
  - `TestEvaluationSetIsStableAndImmutable`: reverse input order를 stable id 순으로 반환하고 accessor mutation이 원본을 바꾸지 않음을 검증한다.
  - `TestEpochFilterOutcomeValidatesFailurePolicy`: evaluated/non-error, blocking error/fatal, observe-only error/observe-error 조합과 invalid 조합을 검증한다.
- 중간 검증:
  - `go test -count=1 ./packages/go/streamgate -run '^Test(EvaluationSetIsStableAndImmutable|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. API-1과 API-2는 공통으로 `filter_contract.go`의 `StableToken`, `FilterOutcome`, `FilterEnforcement` 타입을 소비한다.
3. API-1의 validated binding(ready/not-applicable/deferred 정규화)은 API-2가 소비하는 `FilterOutcome`의 유도 기반이 된다.
4. API-2의 `EvaluationSet`은 API-1에서 유도된 `FilterOutcome`과 `FilterEnforcement`를 `EpochFilterOutcome`으로 감싸고, duplicate id와 invalid 조합을 검증한다.
5. 이 subtask가 PASS하고 `complete.log`를 만들기 전에는 `05+02_parallel_evaluation_coordinator`를 구현하지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/streamgate/evaluation_contract.go` | API-1, API-2 |
| `packages/go/streamgate/evaluation_contract_test.go` | API-1, 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(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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
