<!-- task=m-stream-evidence-gate-core/09_decision_arbiter plan=0 tag=API -->
# Stream Evidence Gate Core — Decision Arbiter 구현 계획

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

- 이 문서는 실행 계획이다. 아래 체크리스트 순서와 범위를 지키고, 범위를 넓혀야 하면 구현을 멈추고 근거를 남긴다.
- 구현 전 `agent-ops/rules/project/rules.md`, `agent-ops/rules/project/domain/platform-common/rules.md`, `agent-test/local/rules.md`를 다시 확인한다.
- 각 항목을 완료할 때 같은 디렉터리의 `CODE_REVIEW-cloud-G07.md`에서 구현 에이전트 소유 섹션을 실제 변경 파일, 판단, 명령, 출력으로 채운다.
- 검증이 모두 통과하기 전 Roadmap Task를 체크하지 않는다. 통과 후에만 `Roadmap Completion` 절차로 Task 상태와 근거를 갱신한다.
- 사용자 질문, commit/push, PR 생성, 배포 같은 제어면 작업은 이 계획의 범위가 아니다.

## 배경

현재 `GateCoordinator`는 모든 필터 결과를 안정 순서의 `EvaluationSet`으로 모은 뒤 `EpochArbiter.Arbitrate`를 정확히 한 번 호출하지만, arbiter가 반환하는 공통 실행 결정 타입이 없고 호출자에게 결정도 전달되지 않는다. 또한 `ResolvedFilter`의 유효 priority가 epoch outcome으로 보존되지 않아 recovery/replacement 충돌을 registry 정책과 stable filter id로 결정할 수 없다.

이 작업은 SDD S13의 혼합 판정 규칙을 공용 Core 계약으로 고정한다. 필터는 판정과 선택적 `RecoveryIntent`만 반환하고, 이 작업에서 만드는 arbiter는 단 하나의 불변 `GateAction` 후보를 선택한다. 실제 mutation, retry, rebuild, dispatch와 recovery budget/eligibility 집행은 후속 coordinator의 책임으로 남긴다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md)
- Task ids:
  - `decision-arbiter`: base disposition과 모든 filter outcome을 결정적으로 합성하고 단일 action을 반환한다.
- Completion mode: check-on-pass

## Analysis Result

### 읽은 파일

- `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`
- `packages/go/streamgate/filter_contract.go`
- `packages/go/streamgate/evaluation_contract.go`
- `packages/go/streamgate/parallel_evaluation.go`
- `packages/go/streamgate/filter_registry.go`
- `packages/go/streamgate/event.go`
- `packages/go/streamgate/evidence_tail.go`
- `packages/go/streamgate/evaluation_contract_test.go`
- `packages/go/streamgate/parallel_evaluation_test.go`
- `packages/go/streamgate/filter_registry_test.go`
- `packages/go/streamgate/consumer_contract_test.go`
- `go.mod`

### SDD 기준

- SDD 상태는 `[승인됨]`, 구현 잠금은 `해제`이며 별도 `USER_REVIEW.md`는 없다.
- SDD S13은 base disposition과 pass/not-applicable/deferred/violation/fatal/replacement를 한 번에 합성하고, unmatched provider error는 terminal을 유지하며 matched error의 eligible recovery만 복구 후보가 되는 경계를 요구한다.
- Evidence Map은 S13을 Roadmap `decision-arbiter` 완료 근거로 연결한다.
- `agent-spec/index.md`에는 이 내부 Core 동작과 매칭되는 현행 구현 스펙이 없어 코드와 승인된 SDD를 기준으로 삼는다.
- `agent-contract/index.md`에는 별도 외부 wire 계약이 없다. 이번 변경은 `packages/go/streamgate` 내부 공개 Go API이며 codec/config/wire schema를 변경하지 않는다.

### 심볼 참조

- `packages/go/streamgate/parallel_evaluation.go:13`의 `EpochArbiter`는 `error`만 반환한다.
- `packages/go/streamgate/parallel_evaluation.go:148`의 `Evaluate`와 `:263`의 `Submit`은 action을 호출자에게 노출하지 않는다.
- `packages/go/streamgate/parallel_evaluation.go:251`은 complete barrier 이후 arbiter를 정확히 한 번 호출한다. 이 보장은 유지한다.
- `packages/go/streamgate/evaluation_contract.go:262`의 `NewEpochFilterOutcome`은 filter id, outcome, enforcement만 보존한다.
- `packages/go/streamgate/filter_registry.go:640`의 `ResolvedFilter.Priority()`는 유효 priority를 제공하지만 `:674`의 `BindEpoch`는 이를 `EpochFilter`로 전달하지 않는다.
- `FilterDecision`은 pass/observe/violation/fatal/replacement를 표현하고 violation만 선택적 `RecoveryIntent`를 가질 수 있다.
- 생산 코드의 `Arbitrate` 호출 지점은 `parallel_evaluation.go` 하나이며, 테스트 seam은 `parallel_evaluation_test.go`와 `filter_registry_test.go`에 있다.

### 테스트 환경 규칙

- 환경 판정: local. `agent-test/local/rules.md`와 매칭 profile `agent-test/local/platform-common-smoke.md`가 존재해 모두 읽었고, 구조적 공백이나 `<확인 필요>` 항목은 없다.
- Go binary: `/config/.local/bin/go` -> `/config/opt/go/bin/go`, `go version go1.26.2 linux/arm64`, `GOROOT=/config/opt/go`.
- 사전 검증 결과:
  - `make proto` 통과.
  - `go test -count=1 ./packages/go/streamgate` 통과 (`ok iop/packages/go/streamgate 0.831s`).
  - `go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport` 통과.
- 외부 서비스나 secret이 필요한 preflight는 없다.
- Go test cache는 사용하지 않고 모든 검증에 `-count=1`을 사용한다.

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

- base disposition과 필터 outcome 조합을 단일 action으로 환원하는 생산 구현과 표 기반 경계 테스트가 없다.
- recovery/replacement가 여러 개인 경우 registry-effective priority 및 stable filter id tie-break를 증명하는 테스트가 없다.
- input 순서가 바뀌어도 동일 action이 나오는 결정성 테스트가 없다.
- complete barrier 이후 action이 `Evaluate`와 `Submit` 반환 경계까지 정확히 한 번 전달되는 테스트가 없다.
- observe-only fatal/violation/evaluation error가 blocking action을 만들지 않는 부정 테스트가 없다.

### 분할 판단

- 이 Milestone의 `decision-policy` epic은 두 개의 큰 Task로 분리한다.
- 이 Task는 공통 action 계약을 먼저 정의하는 독립 선행 작업이며 task index는 `09`다.
- 후속 `10+09_policy_hook`은 이 Task의 `complete.log`가 생성된 뒤에만 구현한다.
- 같은 task group의 archive sibling basename을 확인해 `09`와 `10`이 충돌하지 않음을 확인했으며 archive 내부 파일은 읽지 않았다.

### 범위 결정 근거

- recovery strategy 실행, budget/eligibility 판단, mutation, retry, rebuild, dispatch.
- Edge/Node host 연결, provider별 codec 변경, wire/config schema 추가.
- policy-hook의 hold requirement 조합 API와 소비자 semantic fixture 구현.
- 기존 `FilterDecision` 의미를 확장하거나 provider별 분기를 Core arbiter에 넣는 작업.

### 최종 라우팅

- evaluation_mode: `first-pass`.
- finalizer: `agent-ops/skills/common/finalize-task-routing/scripts/finalize-task-policy.sh`; finalizer_mode: `pair`.
- Build closure: local-fit=true, boundary=2, concurrency=2, fixture=1, external=0, tool=1. Base route `local-fit -> local`, grade score 6, final `local G06`; capability-gap floor `none`, loop-risk floor `none`.
- Review closure: official-review, boundary=2, concurrency=2, fixture=1, external=1, tool=1. Base route `official-review -> cloud`, grade score 7, final `cloud G07`; capability-gap floor `none`, loop-risk floor `none`.
- Official review adapter: `codex`; model: `gpt-5.6-sol`; reasoning effort: `xhigh`.
- 산출물: `PLAN-local-G06.md`, `CODE_REVIEW-cloud-G07.md`.
- Loop-risk 원시 관측: hold/release/terminal/recovery/replacement의 순서 전이, complete barrier, 단일 action exact-once, outcome variant 조합이 존재한다. unknown 신호는 없고 capability gap도 없다.
- Loop-risk는 route closure를 바꾸지 않으며 구현 시 결정성/경계 테스트 강도를 높이는 근거로만 사용한다.

## 구현 체크리스트

- [ ] [API-1] 공용 DecisionArbiter와 불변 GateAction 계약을 추가하고 S13 우선순위를 구현한다.
- [ ] [API-2] registry-effective priority를 epoch outcome까지 보존하고 GateCoordinator가 action을 정확히 한 번 반환하도록 연결한다.
- [ ] [TEST-3] S13 조합표·충돌·결정성·observe-only·coordinator 전달 경계를 자동화한다.
- [ ] [VERIFY-4] local 필수 검증과 race 검증을 실행하고 Roadmap Completion 근거를 정리한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [API-1] 공용 DecisionArbiter와 불변 GateAction 계약을 추가한다

문제:

`EpochArbiter`는 결과를 반환하지 않으므로 SDD가 요구하는 단일 공통 action을 표현할 타입과 우선순위가 없다.

해결 방법:

현재 형태:

```go
// packages/go/streamgate/parallel_evaluation.go:13
 type EpochArbiter interface {
     Arbitrate(context.Context, EvidenceBatch, EvaluationSet) error
 }
```

목표 형태:

```go
// 개념 예시 — 실제 이름과 validation은 기존 immutable contract 관례를 따른다.
type GateActionKind string

const (
    GateActionHold            GateActionKind = "hold"
    GateActionRelease         GateActionKind = "release"
    GateActionTerminalSuccess GateActionKind = "terminal_success"
    GateActionTerminalError   GateActionKind = "terminal_error"
    GateActionRecovery        GateActionKind = "recovery"
    GateActionReplacement     GateActionKind = "replacement"
)

type DecisionArbiter struct{}

func (DecisionArbiter) Arbitrate(
    context.Context,
    EvidenceBatch,
    EvaluationSet,
) (GateAction, error)
```

세부 기준:

- `packages/go/streamgate/decision_arbiter.go`를 추가해 `GateActionKind`, 불변 `GateAction`, constructor/validator/accessor와 기본 `DecisionArbiter`를 둔다.
- base disposition은 `EvidenceBatch`의 정규화된 disposition만 사용하고 임의 provider 조건을 추가하지 않는다.
- blocking enforcement의 fatal 및 evaluation error는 terminal error 후보가 된다.
- blocking deferred는 상위 fatal/recovery/replacement가 없을 때 hold가 된다. not-applicable은 nonblocking이다.
- eligible recovery intent가 있는 blocking violation은 base release 또는 terminal-error 후보를 recovery 후보로 대체할 수 있다. 여기서 arbiter는 intent의 구조 유효성과 후보 선택만 담당하며 실제 eligibility/budget은 후속 coordinator가 집행한다.
- replacement는 replacement 후보로 보존한다. 여러 recovery/replacement 후보의 순서는 registry-effective priority 내림차순, stable filter id 오름차순으로 고정한다.
- 필터가 제공한 `RecoveryIntent.Priority`가 registry-effective priority를 덮어쓰지 못하게 한다. 둘의 불일치를 허용할지 오류로 거절할지는 기존 constructor 계약과 호환되는 한 가지 규칙으로 문서화하고 테스트한다.
- observe-only outcome은 관찰 정보로 남되 hold/terminal/recovery/replacement 후보가 되지 않는다.
- 어떤 override도 없으면 base disposition을 그대로 action으로 변환한다. unmatched provider error와 기존 terminal error가 release로 약화되어서는 안 된다.
- action은 선택된 filter id와 decision/intent의 방어적 복사본을 제공하고 zero/unknown/불일치 상태를 validation에서 거절한다.
- 패키지 외부 mutation/retry/dispatch capability를 action이나 arbiter에 노출하지 않는다.

수정 파일 및 체크리스트:

- [ ] `packages/go/streamgate/decision_arbiter.go` — 공용 action 타입, validator/accessor, deterministic arbitration.
- [ ] `packages/go/streamgate/decision_arbiter_test.go` — constructor/immutability와 표 기반 arbitration.
- [ ] `packages/go/streamgate/parallel_evaluation.go` — `EpochArbiter` 반환 계약 정합화.
- [ ] 다른 provider/transport/config 파일은 수정하지 않는다.

테스트 작성:

- `packages/go/streamgate/decision_arbiter_test.go`에 `TestGateAction*`, `TestDecisionArbiter*` 표 기반 정상/경계/불변성 테스트를 새로 작성한다.
- fixture는 S13의 base disposition/outcome matrix와 priority/id conflict를 직접 생성하며 실제 recovery 실행은 하지 않는다.

중간 검증:

```bash
gofmt -w packages/go/streamgate/decision_arbiter.go packages/go/streamgate/decision_arbiter_test.go
go test -count=1 ./packages/go/streamgate -run 'Test(GateAction|DecisionArbiter)'
```

예상 결과: action constructor/validation과 S13 arbitration 집중 테스트가 통과한다.

### [API-2] effective priority와 action을 coordinator 경계까지 전달한다

문제:

registry가 계산한 priority는 `ResolvedFilter`에만 있고, `EpochFilterOutcome`과 arbiter에는 전달되지 않는다. coordinator도 arbiter action을 버린다.

해결 방법:

현재 형태:

```go
// packages/go/streamgate/filter_registry.go:674
return NewEpochFilter(
    adapter,
    subscribedEventPresent,
    triggerReady,
    blocksRelease,
    r.enforcement,
    r.timeout,
)

// packages/go/streamgate/parallel_evaluation.go:251
if arbErr := c.arbiter.Arbitrate(ctx, batch, set); arbErr != nil {
    return set, errors.New("streamgate: evaluate arbiter: " + arbErr.Error())
}
return set, nil
```

목표 형태:

```go
// 개념 예시
return NewEpochFilter(
    adapter,
    subscribedEventPresent,
    triggerReady,
    blocksRelease,
    r.enforcement,
    r.timeout,
    r.priority,
)

action, err := c.arbiter.Arbitrate(ctx, batch, set)
if err != nil {
    return set, GateAction{}, fmt.Errorf("streamgate: evaluate arbiter: %w", err)
}
return set, action, nil
```

세부 기준:

- `EpochFilter`와 `EpochFilterOutcome`에 nonnegative effective priority를 immutable field로 추가하고 constructor, `Validate`, accessor에 반영한다.
- `ResolvedFilter.BindEpoch`가 registry snapshot의 priority를 전달한다. filter 구현이 runtime에 값을 바꿔도 epoch snapshot은 변하지 않아야 한다.
- ready와 non-ready outcome 모두 동일 priority를 보존한다.
- `EpochArbiter.Arbitrate`를 `(GateAction, error)`로 변경한다.
- `GateCoordinator.Evaluate`, worker `epochResult`, `Submit`이 action을 호출자에게 전달한다. cancellation/validation/evaluator 오류 경로에서는 유효 action을 가장하지 않는다.
- all-complete barrier와 arbiter at-most-once 보장을 유지한다. submit queue, close, backpressure 동작을 바꾸지 않는다.
- production call site와 두 테스트 seam을 전부 컴파일 가능하게 갱신한다.

수정 파일 및 체크리스트:

- [ ] `packages/go/streamgate/evaluation_contract.go` — effective priority field/validation/accessor.
- [ ] `packages/go/streamgate/filter_registry.go` — BindEpoch priority snapshot 전달.
- [ ] `packages/go/streamgate/parallel_evaluation.go` — action 반환과 worker reply 전파.
- [ ] `packages/go/streamgate/evaluation_contract_test.go` — priority validation/immutability.
- [ ] `packages/go/streamgate/filter_registry_test.go` — resolved priority propagation.
- [ ] `packages/go/streamgate/parallel_evaluation_test.go` — signature와 action exact-once 전파.

테스트 작성:

- `evaluation_contract_test.go`에 negative/zero/positive priority validation과 defensive snapshot을 추가한다.
- `filter_registry_test.go`에 resolved priority 전달을, `parallel_evaluation_test.go`에 action 반환·오류·cancellation·at-most-once를 추가한다.

중간 검증:

```bash
gofmt -w packages/go/streamgate/evaluation_contract.go packages/go/streamgate/filter_registry.go packages/go/streamgate/parallel_evaluation.go
go test -count=1 ./packages/go/streamgate -run 'Test(EpochFilter|EpochFilterOutcome|ResolvedFilter|GateCoordinator)'
```

예상 결과: priority snapshot과 coordinator action 전파 집중 테스트가 통과한다.

### [TEST-3] S13 결정표와 부정 경계를 자동화한다

문제:

현재 테스트는 outcome 정규화와 coordinator barrier를 개별 검증하지만 S13의 혼합 action 우선순위, 입력 순서 독립성, observe-only 부정 경계를 한 표로 고정하지 않는다.

해결 방법:

`decision_arbiter_test.go`의 표 fixture와 기존 contract/coordinator 테스트를 조합해 action 선택과 전달 경계를 분리 검증한다.

현재 형태:

```go
// 혼합 action 결정표가 없다.
```

목표 형태:

```go
tests := []struct {
    name       string
    base       BaseDisposition
    outcomes   []EpochFilterOutcome
    wantKind   GateActionKind
    wantFilter string
}{
    // S13 normal, boundary, conflict cases
}
```

테스트 작성:

- fixture 추가: 예. release+pass, hold+not-applicable, pass+blocking-deferred, unmatched error+pass, matched error+recovery, deferred+violation, fatal+retry, replacement conflict.
- boundary 추가: empty set, unknown action kind, invalid/negative priority, intent/filter identity mismatch, observe-only violation/fatal/evaluation-error.
- determinism 추가: 같은 후보를 여러 입력 순서로 만들고 동일 kind/filter id/intent가 선택되는지 비교한다.
- conflict 추가: priority가 다른 후보와 같은 priority 후보를 각각 만들고 priority 내림차순 및 stable id 오름차순을 증명한다.
- concurrency 추가: coordinator가 complete set 전에는 arbiter를 호출하지 않고, 성공 epoch마다 action을 한 번만 만들고 `Evaluate`/`Submit` 반환값까지 그대로 전달하는지 확인한다.
- failure 추가: arbiter 오류, caller cancellation, filter deadline에서 action이 노출되지 않고 기존 sanitized error/at-most-once 규칙이 유지되는지 확인한다.
- 제외: 실제 retry/mutation/rebuild/dispatch가 발생하는 통합 테스트는 후속 recovery-coordinator 작업으로 남긴다.

수정 파일 및 체크리스트:

- [ ] `packages/go/streamgate/decision_arbiter_test.go` — S13 matrix, conflict, determinism, immutability.
- [ ] `packages/go/streamgate/parallel_evaluation_test.go` — exact-once/action propagation/cancellation.
- [ ] `packages/go/streamgate/evaluation_contract_test.go` — priority 경계.
- [ ] `packages/go/streamgate/filter_registry_test.go` — resolved priority snapshot.

중간 검증:

```bash
go test -count=1 ./packages/go/streamgate -run 'Test(DecisionArbiter|GateCoordinator|EvaluationSet|ResolvedFilter)'
go test -race -count=1 ./packages/go/streamgate -run 'TestGateCoordinator'
```

예상 결과: S13 matrix와 coordinator ordering/race 집중 테스트가 통과한다.

### [VERIFY-4] 최종 검증과 Roadmap Completion 근거를 준비한다

문제:

공용 API와 동시성 경계 변경은 집중 테스트만으로 완료할 수 없으며 local profile 회귀, race, 생성물, diff 근거가 함께 필요하다.

해결 방법:

아래 최종 명령을 fresh 실행하고 실제 stdout/stderr와 exit status를 `CODE_REVIEW-cloud-G07.md`에 기록한다. 모든 검증 통과 후에만 SDD S13/Evidence Map 근거로 `decision-arbiter` Task 완료 이벤트를 준비한다.

수정 파일 및 체크리스트:

- [ ] `agent-task/m-stream-evidence-gate-core/09_decision_arbiter/CODE_REVIEW-cloud-G07.md` — 실제 구현 판단과 원문 검증 출력.
- [ ] Roadmap Milestone 문서는 review PASS 후 runtime `update-roadmap`이 갱신하며 구현 에이전트가 직접 수정하지 않는다.

테스트 작성:

- 이 항목은 새 테스트 파일을 추가하지 않는다. API-1~TEST-3에서 작성한 테스트와 local profile 전체 회귀를 fresh 실행한다.

중간 검증:

```bash
go test -count=1 ./packages/go/streamgate
go test -race -count=1 ./packages/go/streamgate
git diff --check
```

예상 결과: package/race/diff 검증이 모두 통과한다.

## 수정 파일 요약

| 파일 | 항목 | 변경 |
|---|---|---|
| `packages/go/streamgate/decision_arbiter.go` | API-1 | 공용 action 및 deterministic arbiter 추가 |
| `packages/go/streamgate/decision_arbiter_test.go` | API-1, TEST-3 | action 불변성과 S13 matrix 검증 |
| `packages/go/streamgate/evaluation_contract.go` | API-2 | effective priority를 outcome에 보존 |
| `packages/go/streamgate/evaluation_contract_test.go` | API-2, TEST-3 | priority validation/immutability 검증 |
| `packages/go/streamgate/filter_registry.go` | API-2 | resolved priority를 epoch에 전달 |
| `packages/go/streamgate/filter_registry_test.go` | API-2, TEST-3 | registry snapshot 전파 검증 |
| `packages/go/streamgate/parallel_evaluation.go` | API-1, API-2 | action을 exact-once 생성·반환 |
| `packages/go/streamgate/parallel_evaluation_test.go` | API-2, TEST-3 | coordinator action 전파와 동시성 경계 검증 |
| `CODE_REVIEW-cloud-G07.md` | VERIFY-4 | 구현 판단과 원문 검증 증거 기록 |

## 최종 검증

```bash
command -v go
readlink -f "$(command -v go)"
go version
go env GOROOT
gofmt -w packages/go/streamgate/decision_arbiter.go packages/go/streamgate/decision_arbiter_test.go packages/go/streamgate/evaluation_contract.go packages/go/streamgate/evaluation_contract_test.go packages/go/streamgate/filter_registry.go packages/go/streamgate/filter_registry_test.go packages/go/streamgate/parallel_evaluation.go packages/go/streamgate/parallel_evaluation_test.go
go test -count=1 ./packages/go/streamgate -run 'Test(GateAction|DecisionArbiter|EpochFilter|EpochFilterOutcome|EvaluationSet|ResolvedFilter|GateCoordinator)'
go test -count=1 ./packages/go/streamgate
go test -race -count=1 ./packages/go/streamgate
make proto
go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport
git diff --check
```

예상 결과: 모든 명령이 exit 0이고, race report와 생성물 diff가 없으며, `git diff --check` 출력이 비어 있다.

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