<!-- task=m-stream-evidence-gate-core/13+11,12_recovery_plan_contract plan=1 tag=REVIEW_RECOVERY_PLAN_CONTRACT -->

# Plan - Recovery contract 독립성 및 managed post-rebuild 확정

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

이 plan의 구현·테스트·evidence만 작성한다. 모든 검증을 실행하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 stdout/stderr를 채운 뒤 active pair를 유지하고 리뷰 준비 완료를 보고한다. 막히면 정확한 blocker, 시도한 명령/출력, 재개 조건만 구현 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구·control-plane stop 파일을 사용하거나 다음 상태를 분류하지 않으며, log archive·`complete.log`·최종 판정은 code-review 전용이다.

## 배경

첫 리뷰에서 현재 checkout의 package/race 검증은 통과했지만, 선언된 split 선행 태스크만 재구성하면 recovery contract가 sibling snapshot 구현의 심볼을 직접 참조해 컴파일되지 않았다. 또한 managed allowance를 Rebuilder 조립 후 실제 prompt token으로 확정해야 하는 SDD 순서와 현재 final-plan-first API가 순환한다. 기존 task path 안에서 public contract와 최소 회귀만 보정한다.

## Archive Evidence Snapshot

- `agent-task/m-stream-evidence-gate-core/13+11,12_recovery_plan_contract/plan_cloud_G09_0.log`: 원 계획. `RecoveryPlan`, fault/trajectory budget, preparer/rebuilder/dispatcher host seam과 unit/race 검증 범위.
- `agent-task/m-stream-evidence-gate-core/13+11,12_recovery_plan_contract/code_review_cloud_G09_0.log`: FAIL. Required 2, Suggested 0, Nit 0.
  - `recovery_plan.go`가 선언되지 않은 sibling `IngressSnapshot`/`ErrIngressSnapshotLimitExceeded`에 의존해 선행 `11,12`만 적용한 격리 checkout에서 컴파일 실패.
  - managed plan이 Rebuilder 호출 전에 `rebuiltPromptTokens`와 allowance를 확정하지만 Rebuilder 반환에는 실제 prompt token 측정값이 없어 post-rebuild 확정 순서를 표현하지 못함.
- 영향 파일: `packages/go/streamgate/recovery_plan.go`, `packages/go/streamgate/recovery_plan_test.go`.
- 실제 검증: 현재 공유 checkout의 `go test -count=1 ./packages/go/streamgate`와 `go test -race -count=1 ./packages/go/streamgate`는 PASS. HEAD+현재 contract만 적용한 격리 checkout의 `GOWORK=off go test -count=1 ./packages/go/streamgate`는 `undefined: ErrIngressSnapshotLimitExceeded`, `undefined: IngressSnapshot`으로 FAIL.
- 선행 완료: `agent-task/archive/2026/07/m-stream-evidence-gate-core/11+09,10_policy_hook_contract/complete.log`, `agent-task/archive/2026/07/m-stream-evidence-gate-core/12+11_config_policy/complete.log`.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/recovery_plan.go` — 전체 public recovery/budget/host seam.
- `packages/go/streamgate/recovery_plan_test.go` — 전체 contract fixture.
- `packages/go/streamgate/ingress_snapshot.go`, `packages/go/streamgate/ingress_snapshot_test.go` — sibling이 소유하는 concrete snapshot과 현재 직접 의존 지점.
- `packages/go/streamgate/filter_contract.go` — recovery strategy/directive/intent와 immutable accessor 관련 구간.
- `packages/go/streamgate/event.go` — terminal reason metadata 관련 구간.
- `packages/go/streamgate/commit_boundary.go`, `packages/go/streamgate/commit_boundary_test.go` — strategy commit eligibility 관련 구간과 diff.
- `packages/go/streamgate/terminal.go` — `StableToken` validation 관련 구간.
- `packages/go/streamgate/validation_regression_test.go` — managed intent constructor 회귀 구간.

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, `[승인됨]`, 잠금 해제.
- 대상: S14 `recovery-coordinator`의 plan/idempotency/commit/cap contract, S15 `request-rebuilder`의 bounded snapshot/host admission seam, S23 `recovery-coordinator`의 preparation 후 success-only rebuild, S24 `recovery-coordinator`의 rebuilt prompt 기반 managed allowance.
- Evidence Map: S14의 one plan/dispatch 및 cap 우선, S15의 request snapshot/AttemptBinding, S23의 success-only rebuild/dispatch readiness, S24의 post-rebuild allowance와 fault ledger 분리를 회귀 항목으로 사용한다.
- 이 follow-up은 중간 contract closure이므로 Milestone Task 완료를 주장하지 않아 `Roadmap Targets`는 두지 않는다.

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`와 `agent-test/local/platform-common-smoke.md`를 읽었다. 호스트 `PATH`의 Go/GOROOT를 기록하고 fresh package/race 검증을 사용한다.
- protobuf/config/wire는 수정하지 않으므로 profile의 `make proto`, transport consumer, full-cycle은 적용하지 않는다. repo Edge-Node 진단·보조 E2E·외부 provider도 사용자 실행 경로를 바꾸지 않아 적용하지 않는다.
- fallback이 아니라 plan의 기존 `packages/go/streamgate` package/race 명령과 deterministic symbol isolation check를 사용한다. 테스트 룰 유지보수는 필요 없다.

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

- 기존 package/race suite는 sibling snapshot 파일이 함께 있는 공유 checkout만 검증해 declared dependency isolation을 가린다.
- `TestManagedTrajectoryAllowanceAndFaultLedgerIsolation`은 `rebuiltPromptTokens`를 plan 생성 전에 주입하므로 Rebuilder 측정 뒤 finalization을 검증하지 않는다.
- preparer-required plan의 rebuild readiness는 검증하지만 managed provisional plan의 rebuild-ready/dispatch-not-ready 구분, RebuiltRequest identity/token 측정 결합, zero allowance no-dispatch 경계가 없다.

### 심볼 참조

- `IngressSnapshot`: `recovery_plan.go`의 `RequestRebuilder` 인자와 `recovery_plan_test.go` fake 구현에서만 직접 참조한다.
- `ErrIngressSnapshotLimitExceeded`: `RebuiltRequest.Validate`와 overflow fixture에서 직접 참조한다.
- `ManagedTrajectoryBudget`, `NewRecoveryPlan`, `RebuiltRequest`, `RequestRebuilder`: 현재 production 참조는 `recovery_plan.go` 내부뿐이고 package 외 caller는 아직 없다. 테스트 참조는 `recovery_plan_test.go`에 집중돼 있다.
- 제거/변경 시 위 참조를 모두 갱신한다.

### 분할 판단

- 기존 split task path `m-stream-evidence-gate-core/13+11,12_recovery_plan_contract`를 그대로 유지한다. index 11은 `11+09,10_policy_hook_contract/complete.log`, index 12는 `12+11_config_policy/complete.log`로 충족됐다.
- 두 Required는 동일한 `recovery_plan.go` public lifecycle을 닫는 한 contract slice이며 같은 fixtures로 API 순서를 증명해야 한다. 별도 sibling으로 나누면 provisional/final plan과 RequestRebuilder 반환 계약을 중복 조정하므로 추가 split gate를 적용하지 않는다.
- sibling `15/16`의 `ingress_snapshot.go`와 lifecycle 구현은 수정하지 않는다.

### 범위 결정 근거

- 수정 포함: `recovery_plan.go`, `recovery_plan_test.go`의 contract와 tests.
- 수정 제외: `ingress_snapshot.go`, config, Edge/OpenAI rebuilder, RecoveryCoordinator, roadmap/spec/contract 문서. 이들은 각각 sibling `15/16/17/14` 또는 runtime 소유이며 이 follow-up은 의존 방향과 public seam만 고친다.
- public API 변경은 아직 production caller가 없으므로 compatibility shim을 남기지 않고 단일 안전 계약으로 정리한다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`; finalizer: `finalize-task-policy.sh`; finalizer_mode: `pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 true. 근거는 두 Required의 파일·SDD 기준, 격리 compile 실패, current unit/race PASS, sibling 소유권 제외, 사용자 결정 불필요다.
- build route_basis: `local-fit`; capability_gap: none; scores=`scope_coupling:2,state_concurrency:1,blast_irreversibility:2,evidence_diagnosis:2,verification_complexity:1`; lane=`local`; grade=`G08`; filename=`PLAN-local-G08.md`.
- build loop-risk: ordered transitions 4개(provisional/prepared/rebuild-measured/dispatch-ready)와 exhaustion 경로, component 4개(Plan/Budget/Rebuilder/RebuiltRequest)·consumer 2개, strategy×preparation×measurement variant 조합. matched=`temporal_state,boundary_contract,variant_product`; triggered=true; structured interpretation 없음; G floor 없음.
- review closures: 모두 true. public contract와 상충 증거를 함께 재검증할 수 있다.
- review route_basis: `official-review`; capability_gap: none; scores=`2/1/2/2/1`; lane=`cloud`; grade=`G08`; filename=`CODE_REVIEW-cloud-G08.md`; official target=`Codex gpt-5.6-sol xhigh`; G floor 없음.

## 구현 체크리스트

- [ ] REVIEW_RECOVERY_PLAN_CONTRACT-1 recovery contract 소유의 opaque bounded snapshot reference와 limit error로 sibling concrete 타입 의존성을 제거한다.
- [ ] REVIEW_RECOVERY_PLAN_CONTRACT-2 managed plan을 rebuild-eligible provisional 상태와 post-rebuild dispatch-ready final 상태로 분리한다.
- [ ] REVIEW_RECOVERY_PLAN_CONTRACT-3 dependency isolation과 managed post-rebuild allowance/identity/exhaustion 회귀 fixtures를 추가한다.
- [ ] REVIEW_RECOVERY_PLAN_CONTRACT-4 focused/package/race/gofmt/symbol/diff 검증을 수행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [REVIEW_RECOVERY_PLAN_CONTRACT-1] Task-independent snapshot seam

- 문제: `recovery_plan.go:1039`가 sibling error를 반환하고 `recovery_plan.go:1161-1162`가 더 늦은 sibling의 concrete `*IngressSnapshot`을 public interface에 넣어 task dependency graph와 독립 compile을 깨뜨린다.

Before (`recovery_plan.go:1038-1039`, `recovery_plan.go:1161-1162`):

```go
if r.retainedBytes > r.peakBytes || r.peakBytes > r.maxBytes {
    return ErrIngressSnapshotLimitExceeded
}
type RequestRebuilder interface {
    RebuildRequest(ctx context.Context, snapshot *IngressSnapshot, plan RecoveryPlan) (RebuiltRequest, error)
}
```

- 해결 방법: recovery contract가 소유하는 stable opaque snapshot reference를 새 value type으로 정의한다. reference에는 raw body/auth accessor를 넣지 않고 stable ref와 `retained <= peak <= max`, positive max의 bounded accounting만 보존한다. rebuilt overflow는 recovery contract 소유 error로 반환한다. `RequestRebuilder`는 이 reference를 받고 host가 request-local registry에서 concrete snapshot을 해석하도록 둔다.

After:

```go
type RecoveryRequestSnapshotRef struct { /* private stable ref + bounded accounting */ }

var ErrRecoverySnapshotLimitExceeded = errors.New("streamgate: recovery snapshot limit exceeded")

type RequestRebuilder interface {
    RebuildRequest(ctx context.Context, snapshot RecoveryRequestSnapshotRef, plan RecoveryPlan) (RebuiltRequest, error)
}
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/recovery_plan.go`: opaque constructor/Validate/accessors, contract-owned error, interface signature.
  - [ ] `packages/go/streamgate/recovery_plan_test.go`: fake rebuilder와 overflow expectation 갱신.
- 테스트 작성: 작성. `TestRecoveryRequestSnapshotRefBounds`에서 정상/zero max/retained-peak-max overflow/defensive stable ref를, host seam test에서 concrete `IngressSnapshot` 없이 interface 구현 가능함을 검증한다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run 'TestRecovery(RequestSnapshotRefBounds|HostSeamsAndRawFreeReferences)'`가 PASS한다.

### [REVIEW_RECOVERY_PLAN_CONTRACT-2] Managed post-rebuild finalization

- 문제: `recovery_plan.go:647-662`는 plan 생성 시 실제 rebuilt prompt 측정값과 양수 allowance를 요구하지만, `recovery_plan.go:959-968`의 RebuiltRequest는 그 측정값을 반환하지 않고 `recovery_plan.go:1161-1162`의 Rebuilder는 이미 final plan을 입력받는다. SDD S24 순서를 구현할 수 없다.

Before (`recovery_plan.go:647-662`):

```go
if strategy == RecoveryStrategyManagedContinuation {
    budgetCopy := copyManagedTrajectoryBudget(*input.ManagedTrajectory)
    allowance = budgetCopy.NextAllowance()
    if allowance <= 0 {
        return RecoveryPlan{}, ErrManagedTrajectoryExhausted
    }
}
```

- 해결 방법: `ManagedTrajectoryBudget` 생성 시 request-start base 값만 고정하고 rebuilt prompt 측정 상태를 별도로 둔다. managed `NewRecoveryPlan`은 preparer 완료 후 rebuild 가능한 provisional plan을 만들되 dispatch-ready는 false다. `RebuiltRequest`가 non-negative `rebuiltPromptTokens`를 반환하고, plan/idempotency identity가 일치하는 `WithManagedRebuiltRequest` 같은 immutable finalizer가 allowance를 계산한다. allowance 0이면 `ErrManagedTrajectoryExhausted`를 반환하고 final dispatch plan을 만들지 않는다. `UsageAfterDispatch`는 preparation과 managed measurement가 모두 끝난 dispatch-ready plan에서만 성공한다.

After:

```go
provisional, _ := NewRecoveryPlan(input)
rebuilt, _ := rebuilder.RebuildRequest(ctx, snapshotRef, provisional)
final, _ := provisional.WithManagedRebuiltRequest(rebuilt)
if !final.ReadyForDispatch() { /* reject */ }
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/recovery_plan.go`: base trajectory/measurement state, RebuiltRequest prompt-token field, immutable finalizer, rebuild/dispatch readiness validation.
  - [ ] `packages/go/streamgate/recovery_plan_test.go`: constructor와 fake rebuilt request 갱신.
- 테스트 작성: 작성. `TestManagedTrajectoryPostRebuildFinalization`에서 provisional plan의 rebuild 가능/dispatch 불가, actual prompt tokens 기반 min allowance, identity mismatch, input plan immutability, zero caller/context allowance error, fault ledger 미소비를 검증한다. preparer-required managed plan은 prepared 전 rebuild 불가, prepared 뒤 provisional, measurement 뒤 dispatch-ready 순서를 검증한다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run 'Test(ManagedTrajectoryPostRebuildFinalization|RecoveryPlanDefensiveCopiesAndPreparationContract)'`가 PASS한다.

### [REVIEW_RECOVERY_PLAN_CONTRACT-3] Contract regression closure

- 문제: 기존 tests는 shared checkout의 sibling 심볼과 plan 생성 전 주입한 prompt token만 검증해 두 Required를 재발 방지하지 못한다.
- 해결 방법: task-owned fake snapshot ref/rebuilder/dispatcher만으로 public seam compile과 identity/accounting을 검증하고, managed lifecycle variant table을 추가한다. existing fault total-first, commit eligibility, defensive-copy assertions는 유지한다.
- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/recovery_plan_test.go`: dependency-free compile fixtures, provisional/final variant table, zero/no-dispatch assertions.
  - [ ] stale `IngressSnapshot`/`ErrIngressSnapshotLimitExceeded` 직접 참조가 recovery contract files에 없는지 deterministic search로 고정한다.
- 테스트 작성: 작성. 위 focused tests가 regression test다.
- 중간 검증: `if rg --sort path -n '\b(IngressSnapshot|ErrIngressSnapshotLimitExceeded)\b' packages/go/streamgate/recovery_plan.go packages/go/streamgate/recovery_plan_test.go; then exit 1; fi`가 출력 없이 exit 0이다.

### [REVIEW_RECOVERY_PLAN_CONTRACT-4] Verification closure

- 문제: 첫 검증은 공유 sibling 구현이 존재하는 상태만 검사해 verification trust가 부족했다.
- 해결 방법: host Go identity, gofmt, focused tests, full package, race, forbidden-symbol search, diff check를 fresh 실행한다. Go test cache는 허용하지 않는다.
- 수정 파일 및 체크리스트:
  - [ ] 구현 파일 gofmt 결과가 깨끗하다.
  - [ ] focused/full/race와 symbol isolation이 통과한다.
  - [ ] 실제 stdout/stderr를 review stub에 기록한다.
- 테스트 작성: 별도 테스트 파일은 만들지 않는다. 기존 contract test에 회귀를 결합한다.
- 중간 검증: 아래 최종 검증 전체가 PASS한다.

## 의존 관계 및 구현 순서

`11`, `12` complete evidence 유지 → CONTRACT-1 sibling decoupling → CONTRACT-2 managed lifecycle → CONTRACT-3 fixtures → CONTRACT-4. `ingress_snapshot.go`를 수정하거나 sibling 15/16의 미완료 구현을 성공 조건으로 사용하지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/go/streamgate/recovery_plan.go` | REVIEW_RECOVERY_PLAN_CONTRACT-1/2 |
| `packages/go/streamgate/recovery_plan_test.go` | REVIEW_RECOVERY_PLAN_CONTRACT-1/2/3/4 |

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT` — 호스트 Go identity가 local rule과 일치한다.
- `gofmt -l packages/go/streamgate/recovery_plan.go packages/go/streamgate/recovery_plan_test.go` — 출력 없음.
- `go test -count=1 ./packages/go/streamgate -run 'Test(RecoveryRequestSnapshotRefBounds|ManagedTrajectoryPostRebuildFinalization|RecoveryPlanDefensiveCopiesAndPreparationContract|RecoveryHostSeamsAndRawFreeReferences)'` — PASS.
- `go test -count=1 ./packages/go/streamgate` — PASS.
- `go test -race -count=1 ./packages/go/streamgate` — PASS.
- `if rg --sort path -n '\b(IngressSnapshot|ErrIngressSnapshotLimitExceeded)\b' packages/go/streamgate/recovery_plan.go packages/go/streamgate/recovery_plan_test.go; then exit 1; fi` — 출력 없이 exit 0.
- `git diff --check` — 출력 없음.

repo Edge-Node 진단, 보조 E2E smoke, full-cycle 실제 구동은 적용하지 않는다. 이 follow-up은 아직 production caller가 없는 transport-agnostic contract와 unit fixture만 변경한다.

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