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

# Stream Gate Evidence Tail 계약 및 상태기 구현 계획

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

- 구현 체크리스트와 검증 명령을 완료하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 stdout/stderr를 기록한다.
- active PLAN/CODE_REVIEW 파일은 유지하고 review-ready만 보고한다. 판정, 로그 rename, `complete.log`, task archive는 code-review 책임이다.
- 막히면 정확한 차단 원인, 시도한 명령과 출력, 재개 조건만 구현 소유 evidence에 기록한다.
- 사용자에게 질문하거나 선택지를 제시하거나 user-input 도구·control-plane stop 파일을 만들거나 다음 상태를 분류하지 않는다.

## 배경

재검토에서 filter hold 계약과 release cursor의 sink 성공 경계를 명확히 했다. 이 계획은 SDD의 `FilterHoldRequirement`를 그대로 정의하고 evidence release를 prepare/confirm 두 단계로 분리한다. overflow의 실제 attempt cancel/terminal 통합은 dependent commit-boundary plan에서 검증한 뒤 `evidence-tail` Roadmap Task를 함께 완료하므로, 이 foundation plan 자체는 Roadmap Task를 조기 체크하지 않는다.

## Archive Evidence Snapshot

- 선행 완료: `agent-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`
- 선행 결과: normalized event, immutable `EvidenceBatch`, release/terminal 계약이 PASS로 종결됐다.
- 구현 기준: SDD 명칭/필드 일치, UTF-8 입력 경계, tool fragment keying, sink 성공 뒤 cursor/look-behind 확정.
- Roadmap carryover: S01/S02 core evidence는 이 plan에서 만들고, S19 cancel→terminal/no-release evidence와 `evidence-tail` 완료 이벤트는 `07+01,06_commit_boundary`에서 닫는다.
- 구현자는 위 경로 외의 `agent-task/archive/**`를 다시 탐색할 필요가 없다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/event.go`
- `packages/go/streamgate/filter_contract.go`
- `packages/go/streamgate/terminal.go`
- `packages/go/streamgate/evaluation_contract.go`
- `packages/go/streamgate/parallel_evaluation.go`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-spec/index.md`
- `agent-contract/index.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`
- `.gitignore`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`
- 상태: `[승인됨]`, 구현 잠금 `해제`, 사용자 리뷰 `없음`
- 대상: S01, S02의 core state와 S19의 overflow signal / Milestone Task `evidence-tail`
- `FilterHoldRequirement`는 channel, `none|rolling_window|terminal_gate|fragment_gate`, subscribed event kinds, `evidence_runes`, terminal/fragment trigger, `max_buffer_runes`를 가진다.
- S01/S02의 threshold/look-behind/recovery/Unicode evidence는 이 plan이 검증한다. S19가 요구하는 attempt cancel과 terminal commit은 `07+01,06_commit_boundary`가 이 plan의 overflow signal을 실제 seam에 연결해 검증한다.
- 따라서 이 plan에는 `Roadmap Targets`를 두지 않으며 dependent plan PASS 때 S01/S02 predecessor evidence와 S19 integration evidence를 함께 확인한다.

### 테스트 환경 규칙

- `test_env`: local
- `agent-test/local/rules.md`와 `agent-test/local/platform-common-smoke.md`를 읽었다.
- 검증은 `PATH`에서 처음 해석되는 호스트 설치 `go`만 사용하며 별도 `GOTOOLCHAIN`이나 고정 버전 경로를 쓰지 않는다.
- 프리플라이트에서 host `go`의 streamgate fresh/race/vet와 `make proto`는 PASS했다.
- Edge/Node transport unit은 host GOROOT의 `crypto/internal/fips140cache` 누락으로 setup failure다. 구현자는 우회 toolchain을 쓰지 않고 재실행하며, 계속 실패하면 실제 오류와 host Go 설치 복구를 재개 조건으로 기록한다.
- 이 package foundation은 proto/wire나 사용자 실행 경로를 바꾸지 않으므로 full-cycle은 적용하지 않는다.

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

- SDD 이름/필드를 그대로 가진 immutable `FilterHoldRequirement`와 enforcement-aware channel requirement composition이 없다.
- 200/500 rolling threshold, bounded pending/look-behind와 cross-boundary repeat snapshot이 없다.
- valid UTF-8 chunk 단위의 Korean rune counting과 invalid UTF-8 fail-closed 검증이 없다.
- channel+tool-call ID별 fragment completion/bound가 없다.
- sink 전송 전 prepared release와 성공한 event 수만 반영하는 partial confirm 상태 전이가 없다.
- replace reset과 continuation look-behind/cursor preserve 검증이 없다.

### 심볼 참조

- rename/remove 없음.
- 새 public symbol: `FilterHoldMode`, `FilterHoldRequirement`, `FilterHoldBinding`, `EvidencePlan`, `EvidenceTail`, `EvidenceEpoch`, `EvidenceTailSignal`, `PreparedRelease`, `ReleaseConfirmation`, `NewEvidenceTail`.
- 현재 call site는 없으며 dependent commit/registry plan이 이 계약을 소비한다.
- 새 third-party dependency는 없고 `unicode/utf8` 등 Go 표준 라이브러리만 사용한다.
- 매칭되는 active agent-spec/agent-contract 문서는 없다. 외부 API·wire·config schema는 변경하지 않는다.

### 분할 판단

- split decision policy를 plan 파일 선택 전에 적용했다.
- shared task group: `m-stream-evidence-gate-core`
- siblings:
  - `06+01_evidence_tail`: hold 계약, rune state, prepared release와 overflow signal.
  - `07+01,06_commit_boundary`: `06` 완료 뒤 sink progress/attempt cancel/terminal과 통합하고 `evidence-tail`, `commit-boundary`를 함께 완료.
  - `08+02,05,06_filter_registry`: archived `02`, `05`와 `06` 완료 뒤 Filter adapter/registry를 구현.
- S19 side effect와 sink cursor 확정은 별도 ownership boundary이므로 `07`에 분리했다. `06` 내부의 requirement/rune/prepared-release는 같은 state invariant를 공유하므로 더 쪼개지 않는다.

### 범위 결정 근거

- 새 `packages/go/streamgate/evidence_tail.go`, `packages/go/streamgate/evidence_tail_test.go`만 수정한다.
- `ReleaseSink` 호출, attempt cancel, terminal commit, concrete Arbiter, Edge/Node adoption은 이 plan에서 구현하지 않는다.
- `none`/mixed filter의 strongest channel composition policy는 `policy-hook`에서 최종 검증하되, 이 plan은 단일 channel에 전달된 requirement set을 deterministic하게 합성할 수 있는 validation/helper까지만 제공한다.
- raw output은 request-local pending/look-behind에만 두고 observation/log 또는 cross-request state로 복제하지 않는다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`
- build closures: scope/context/verification/evidence/ownership/decision 모두 `true`
  - 근거: 두 신규 파일과 S01/S02 core state, release transaction boundary가 닫히고 S19의 dependent 책임도 명시됐다.
  - scores: scope coupling 1, state/concurrency 2, blast/irreversibility 1, evidence diagnosis 0, verification complexity 1
  - result: `local`, `G05`, `PLAN-local-G05.md`
- review closures: scope/context/verification/evidence/ownership/decision 모두 `true`
  - 근거: requirement validation, rune fixture, prepared/partial confirmation과 recovery transition을 focused/race test로 판정할 수 있다.
  - scores: scope coupling 1, state/concurrency 2, blast/irreversibility 1, evidence diagnosis 1, verification complexity 1
  - result: `local`, `G06`, `CODE_REVIEW-local-G06.md`

## 구현 체크리스트

- [ ] `API-1` SDD와 동일한 `FilterHoldRequirement`, enforcement-aware channel plan 및 tool-fragment keyed Unicode rune evidence state를 구현하고 200/500 threshold, Korean chunk, terminal/fragment trigger를 테스트한다.
- [ ] `API-2` prepared release/partial confirmation, overflow no-release signal, terminal pending discard, replace reset/continuation preserve를 구현하고 stale·중복 transition을 테스트한다.
- [ ] 호스트 `go` 기준 focused/fresh/race/vet 및 공통 profile 명령을 실행하고 blocker를 포함한 실제 출력을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] FilterHoldRequirement와 keyed Unicode evidence state

- 문제:
  - `packages/go/streamgate/filter_contract.go:836-849`는 pending/look-behind snapshot만 정의하며 SDD `FilterHoldRequirement`나 누적 상태가 없다.
  - `packages/go/streamgate/event.go:279-360`은 string payload를 허용하지만 tail 경계에서 valid UTF-8와 rune count를 강제하지 않는다.
- 해결 방법:
  - closed `FilterHoldMode`와 immutable `FilterHoldRequirement`를 SDD 필드명 그대로 추가한다. mode별 required/forbidden field, subscribed kind, channel, positive rune/bound를 constructor와 `Validate`에서 검사한다.
  - `FilterHoldBinding`은 stable filter ID, requirement와 caller-resolved `BlocksRelease` boolean을 묶는다. `NewEvidencePlan`은 blocking binding만 channel hold에 합성하고 non-blocking/`none`은 release를 막지 않게 한다. 구체 enforcement enum→boolean 변환은 registry/policy owner가 담당하므로 이 foundation은 evaluation-contract dependency를 새로 만들지 않는다.
  - `NewEvidenceTail(EvidencePlan)`은 compiled channel plan만 받아 channel별 pending/look-behind와 tool-call ID별 fragment state를 소유한다.
  - `Append(NormalizedEvent)`는 text/reasoning/tool argument가 `utf8.ValidString`인지 확인하고 valid string chunk 사이에서만 `utf8.RuneCountInString`/`[]rune` 절단을 사용한다. invalid UTF-8은 release 없이 stable error로 거부한다.
  - rolling은 effective rune threshold, terminal gate는 terminal event, fragment gate는 `CompleteFragment(channel, toolCallID)`에서 immutable `EvidenceEpoch`를 만든다.
- Before (`filter_contract.go:841-849`):

```go
type EvidenceBatch struct {
	events              []NormalizedEvent
	channelPending      map[string][]NormalizedEvent
	committedLookBehind map[string][]NormalizedEvent
	// ...
}
```

- After (`evidence_tail.go`):

```go
type FilterHoldRequirement struct {
	// private immutable fields exposed by defensive accessors
}

func NewEvidencePlan(bindings []FilterHoldBinding) (EvidencePlan, error)
func NewEvidenceTail(plan EvidencePlan) (*EvidenceTail, error)
func (t *EvidenceTail) Append(event NormalizedEvent) (EvidenceEpoch, EvidenceTailSignal, error)
func (t *EvidenceTail) CompleteFragment(channel, toolCallID string) (EvidenceEpoch, bool, error)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/evidence_tail.go`: mode/requirement constructors, enforcement-aware channel plan, UTF-8/rune helpers, channel/tool key state, trigger snapshot
  - [ ] `packages/go/streamgate/evidence_tail_test.go`: field matrix, blocking/observe composition, 200/500 threshold, cross-boundary look-behind, valid Korean chunks, invalid UTF-8, interleaved tool IDs
- 테스트 작성:
  - `TestFilterHoldRequirementValidatesModeMatrix`: mode별 field와 hard bound를 검증한다.
  - `TestEvidencePlanComposesOnlyBlockingRequirements`: observe-only/none이 hold를 만들지 않고 blocking requirement만 channel plan에 반영됨을 검증한다.
  - `TestEvidenceTailRollingRuneThresholdAndLookBehind`: 200/500 threshold와 bounded cross-boundary snapshot을 검증한다.
  - `TestEvidenceTailPreservesKoreanRuneChunks`: 여러 valid UTF-8 Korean chunk를 rune 기준으로 합치고 invalid string은 fail-closed함을 검증한다.
  - `TestEvidenceTailKeysFragmentsByToolCall`: 같은 channel의 interleaved tool ID가 서로의 completion을 trigger하지 않음을 검증한다.
- 중간 검증:
  - `go test -count=1 ./packages/go/streamgate -run '^Test(FilterHoldRequirementValidatesModeMatrix|EvidencePlanComposesOnlyBlockingRequirements|EvidenceTail(RollingRuneThresholdAndLookBehind|PreservesKoreanRuneChunks|KeysFragmentsByToolCall))$'`
  - 기대 결과: PASS

### [API-2] Transactional release confirmation과 recovery state

- 문제:
  - filter PASS 뒤 release event를 반환하는 동시에 committed cursor를 갱신하면 downstream partial failure 때 실제 전송 상태와 tail state가 달라진다.
  - terminal/fragment hard bound overflow, replace/continuation, stale epoch를 표현하는 concrete state가 없다.
- 해결 방법:
  - `PrepareRelease(epochID)`는 validated `PreparedRelease` token과 ordered `ReleaseEvent` copy를 만들되 pending/look-behind/cursor를 변경하지 않는다.
  - `ConfirmRelease(token, ReleaseConfirmation{ReleasedEvents:n})`만 실제 전송된 event prefix를 committed look-behind/cursor에 반영하고 나머지는 pending으로 유지한다. 0/full/partial, stale token, duplicate confirm을 모두 검증한다.
  - overflow는 release event가 없는 typed `EvidenceTailSignalOverflow`와 channel/bound/cause code만 반환한다. attempt cancel/terminal 적용은 dependent `07`이 소유한다.
  - `ResetForReplace`는 모든 attempt-local state를 초기화하고 `PrepareContinuation`은 committed look-behind/cursor만 보존하며 pending/prepared token을 폐기한다.
  - `DiscardPendingForTerminal`은 idle/overflow terminal 전에 pending과 prepared token을 release 없이 폐기한다.
- Before (`filter_contract.go:902-910`):

```go
func validateEvidenceTail(channel string, events []NormalizedEvent) error {
	if channel == "" {
		return errors.New("streamgate: evidence tail channel key must not be empty")
	}
```

- After (`evidence_tail.go`):

```go
func (t *EvidenceTail) PrepareRelease(epochID uint64) (PreparedRelease, error)
func (t *EvidenceTail) ConfirmRelease(token string, confirmation ReleaseConfirmation) error
func (t *EvidenceTail) ResetForReplace()
func (t *EvidenceTail) PrepareContinuation()
func (t *EvidenceTail) DiscardPendingForTerminal()
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/evidence_tail.go`: opaque prepared token, zero/partial/full confirm, overflow signal, terminal discard, reset/continuation, defensive copies
  - [ ] `packages/go/streamgate/evidence_tail_test.go`: no mutation before confirm, partial progress, overflow no-release, terminal discard, stale/duplicate token, recovery transitions
- 테스트 작성:
  - `TestEvidenceTailConfirmsOnlyReleasedPrefix`: prepare 전후 state와 partial/full confirm cursor를 검증한다.
  - `TestEvidenceTailOverflowSignalsNoRelease`: limit+1이 release 없이 bounded overflow signal만 만드는지 검증한다.
  - `TestEvidenceTailRecoveryTransitionsInvalidatePreparedRelease`: replace/continuation/terminal discard가 prepared token을 무효화하고 보존·폐기 범위가 다른지 검증한다.
- 중간 검증:
  - `go test -race -count=1 ./packages/go/streamgate -run '^TestEvidenceTail(ConfirmsOnlyReleasedPrefix|OverflowSignalsNoRelease|RecoveryTransitionsInvalidatePreparedRelease)$'`
  - 기대 결과: PASS, race 없음

## 의존 관계 및 구현 순서

1. `01_event_contract_types`는 `agent-task/archive/2026/07/m-stream-evidence-gate-core/01_event_contract_types/complete.log`로 충족됐다.
2. `API-1`의 immutable requirement/keyed rune state를 먼저 구현하고 `API-2`가 transactional release와 recovery transition을 추가한다.
3. `07+01,06_commit_boundary`와 `08+02,05,06_filter_registry`는 이 subtask의 `complete.log`가 생긴 뒤 시작한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/streamgate/evidence_tail.go` | API-1, API-2 |
| `packages/go/streamgate/evidence_tail_test.go` | API-1, API-2 |

## 최종 검증

1. `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`
   - 기대 결과: PATH의 host `go`와 실제 경로/GOROOT가 기록됨
2. `gofmt -d packages/go/streamgate/evidence_tail.go packages/go/streamgate/evidence_tail_test.go`
   - 기대 결과: stdout 없음
3. `go test -count=1 ./packages/go/streamgate -run '^Test(FilterHoldRequirement|EvidencePlan|EvidenceTail)'`
   - 기대 결과: PASS
4. `go test -race -count=1 ./packages/go/streamgate`
   - 기대 결과: PASS, race 없음
5. `go vet ./packages/go/streamgate`
   - 기대 결과: PASS, stdout 없음
6. `make proto`
   - 기대 결과: 성공, proto 생성물 추가 diff 없음
7. `go test -count=1 ./apps/edge/internal/transport ./apps/node/internal/transport`
   - 기대 결과: PASS. host GOROOT mismatch가 계속되면 우회하지 말고 실제 오류와 host Go 설치 복구 조건을 기록
8. `git diff --check -- packages/go/streamgate proto/gen/iop`
   - 기대 결과: stdout 없음
9. `git status --short -- proto/gen/iop`
   - 기대 결과: stdout 없음

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