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

# Plan - Dispatchable request 생성 권한과 capability binding

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

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

## 배경

measured draft와 allowance-bound final request의 타입 분리는 도입됐지만 final request 생성자가 공개되어 Core finalizer를 우회할 수 있다. 또한 finalizer가 plan의 required capabilities와 draft를 대조하지 않아 admission 제약이 빠진 dispatchable request를 승인한다. 같은 task path에서 final request authenticity 불변식과 최소 회귀를 닫는다.

## Archive Evidence Snapshot

- `agent-task/m-stream-evidence-gate-core/13+11,12_recovery_plan_contract/plan_local_G07_2.log`: measured draft → plan finalization → allowance-bound request 순서를 도입한 직전 계획.
- `agent-task/m-stream-evidence-gate-core/13+11,12_recovery_plan_contract/code_review_cloud_G07_2.log`: FAIL. Required 1, Suggested 0, Nit 0.
  - exported `NewRebuiltRequest`가 임의 allowance로 finalizer를 우회하고, `FinalizeRebuiltRequest`는 plan의 required capabilities와 다른 draft를 승인한다.
- 영향 파일: `packages/go/streamgate/recovery_plan.go`, `packages/go/streamgate/recovery_plan_test.go`.
- 실제 검증: host Go identity, gofmt, focused/package/race, stale/dependency symbol search와 `git diff --check`는 PASS했다. `go doc`와 source/test 대조에서 공개 bypass와 capability mismatch가 확인됐다.
- SDD carryover: S24 post-rebuild allowance와 SDD `AttemptDispatcher` required-capability admission 계약을 적용한다. 이 하위 contract closure는 Milestone `recovery-coordinator` 전체 완료를 주장하지 않아 `Roadmap Targets`는 두지 않는다.
- 선행 완료: `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` — RecoveryPlan, draft/final request, Rebuilder/Dispatcher public seam 전체.
- `packages/go/streamgate/recovery_plan_test.go` — lifecycle, allowance, preparation, host seam fixtures 전체.
- `agent-task/m-stream-evidence-gate-core/13+11,12_recovery_plan_contract/plan_local_G07_2.log` — 직전 구현 범위와 one-shot finalizer/required capability 기준.
- `agent-task/m-stream-evidence-gate-core/13+11,12_recovery_plan_contract/code_review_cloud_G07_2.log` — 현재 FAIL finding과 재현 근거.
- `agent-task/m-stream-evidence-gate-core/13+11,12_recovery_plan_contract/plan_local_G08_1.log`, `code_review_cloud_G08_1.log` — 직전 post-rebuild lifecycle과 inherited finding.
- `agent-roadmap/phase/knowledge-tool-optimization-extension/PHASE.md`, `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md` — Phase 경계, Milestone 범위와 잠금.
- `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md` — 승인된 S24 lifecycle, AttemptDispatcher capability 경계와 Evidence Map.
- `agent-ops/rules/project/domain/platform-common/rules.md` — 공통 Go 계약 경계.
- `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.md` — local Go identity와 platform-common 검증 기준.
- `agent-spec/index.md`, `agent-contract/index.md` — 직접 매칭되는 recovery living spec/계약 문서가 없음을 확인했다.
- `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` — split 선행 11/12 완료 근거.

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `[승인됨]`, SDD 잠금 `해제`, 사용자 리뷰 없음.
- S24 / `recovery-coordinator`: rebuilt prompt 측정 뒤 Core가 allowance를 계산해 다음 managed dispatch에 적용하며 fault ledger와 분리한다.
- `AttemptDispatcher` 계약: rebuilt request와 required capability를 기존 admission에 전달한다. `RecoveryPlan`에 고정된 capability가 final request에서 누락되면 계약을 충족하지 못한다.
- Evidence Map S24의 post-rebuild allowance와 single downstream continuation 증거에서 sole finalization path, allowance/capability binding, mismatch no-dispatch 회귀를 역산했다.
- 이 follow-up은 중간 public contract만 닫으므로 Milestone `recovery-coordinator` Task 완료를 주장하지 않는다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/platform-common-smoke.md`를 읽었다.
- 호스트 `PATH`의 Go/GOROOT identity를 먼저 기록하고 fresh `go test -count=1`, package race, gofmt, deterministic symbol search와 `git diff --check`를 적용한다.
- proto/config/wire를 수정하지 않으므로 profile의 `make proto`, transport consumer와 full-cycle은 적용하지 않는다. task-specific verification은 `packages/go/streamgate` focused/package/race suite다.
- 외부 runner, provider, secret, port, artifact가 필요하지 않으며 테스트 룰 유지보수도 필요 없다.

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

- allowance variant와 recording dispatcher는 finalizer가 반환한 정상 request의 숫자만 검증한다.
- `TestRebuiltRequestDraftLifecycleAndIdentity`가 공개 `NewRebuiltRequest(draft, 300)`를 직접 호출해 임의 allowance bypass를 정상 API로 고정한다.
- managed lifecycle fixtures의 plan에는 `stream.normalized`, `assistant.prefill` capability가 있지만 draft에는 `nil`을 넣고 finalizer 성공을 기대해 capability 누락을 탐지하지 못한다.
- capability mismatch no-final-request/no-dispatch와 successful dispatcher identity/allowance/capability 동등성 회귀가 없다.

### 심볼 참조

- 제거 대상 exported `NewRebuiltRequest`: production 정의 `recovery_plan.go:1210`, finalizer 호출 `recovery_plan.go:962,969`, test 호출 `recovery_plan_test.go:710,723`뿐이다. package 밖 production caller는 없다.
- 유지 대상 `FinalizeRebuiltRequest`: production 정의와 `recovery_plan_test.go` lifecycle fixture에서만 사용한다.
- `RebuiltRequestDraft`, `RebuiltRequest`, `RequestRebuilder`, `AttemptDispatcher`의 package 밖 production 구현은 아직 없다. 후속 active sibling은 index 13 완료에 의존하며 이번 public seam을 소비할 예정이므로 별도 call-site rollout을 선행하지 않는다.

### 분할 판단

- split 정책을 재평가했고 기존 `m-stream-evidence-gate-core/13+11,12_recovery_plan_contract`를 유지한다. 선행 index 11/12는 위 archived `complete.log`로 충족됐다.
- exported constructor 제거와 capability binding은 같은 final-request authenticity 불변식이며 동일 finalizer와 lifecycle fixtures를 함께 바꾼다.
- API-vs-call-site rollout은 production caller가 없어 존재하지 않고, 도메인·외부 검증·독립 완료 가능한 slice도 없다. 추가 split은 중간에 우회 가능한 final request 계약을 남긴다.

### 범위 결정 근거

- 수정 포함: `recovery_plan.go`, `recovery_plan_test.go`의 final request 생성 권한, capability binding과 회귀.
- 수정 제외: RecoveryCoordinator 실행, Edge/OpenAI Rebuilder/Dispatcher, ingress snapshot sibling, config/proto, roadmap/SDD/spec/contract 문서. 직접 매칭 문서가 없고 이 follow-up은 transport-agnostic seam만 닫는다.
- public compatibility shim은 남기지 않는다. production caller가 없고 shim이 바로 bypass를 유지하기 때문이다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`; finalizer: `finalize-task-policy.sh`; finalizer_mode: `pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 true. 두 파일과 SDD criterion, 공개 constructor 및 capability mismatch의 직접 source/test 근거, fresh package/race PASS, no production caller, 사용자 결정 없음으로 닫혔다.
- build route_basis: `local-fit`; capability_gap: none; scores=`scope_coupling:2,state_concurrency:1,blast_irreversibility:1,evidence_diagnosis:2,verification_complexity:1`; lane=`local`; grade=`G07`; filename=`PLAN-local-G07.md`.
- build loop-risk: ordered transitions 4개(provisional/prepared/measured/dispatchable), adverse path는 constructor bypass/capability mismatch/identity mismatch/exhaustion이다. concurrency actor는 1개이고 immutable value 외 공유 상태는 없다. boundary component 4개(Plan/Draft/Final Request/Dispatcher), consumer 2개(Rebuilder/Dispatcher), identity·allowance·capability binding 제약이 있다. structured interpretation은 없다. strategy×preparation×allowance/capability variant 조합 검증이 필요하다. matched=`temporal_state,boundary_contract,variant_product`; triggered=true; G floor=none.
- review closures: scope/context/verification/evidence/ownership/decision 모두 true. public API visibility, SDD binding과 actual dispatcher evidence를 독립 재검증할 수 있다.
- review route_basis: `official-review`; capability_gap: none; scores=`2/1/1/2/1`; lane=`cloud`; grade=`G07`; filename=`CODE_REVIEW-cloud-G07.md`; official target=`Codex gpt-5.6-sol xhigh`; G floor=none.

## 구현 체크리스트

- [ ] REVIEW_FINAL_REQUEST_AUTHENTICITY-1 dispatchable `RebuiltRequest`를 `RecoveryPlan.FinalizeRebuiltRequest`만 생성하고 plan의 required capabilities를 bind하도록 한다.
- [ ] REVIEW_FINAL_REQUEST_AUTHENTICITY-2 constructor bypass, capability mismatch no-dispatch와 dispatcher-observed identity/allowance/capability 회귀를 추가한다.
- [ ] REVIEW_FINAL_REQUEST_AUTHENTICITY-3 focused/package/race/gofmt/symbol/diff 검증을 수행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_FINAL_REQUEST_AUTHENTICITY-1] Final request authority와 capability binding

- 문제: `recovery_plan.go:1210-1224`의 exported `NewRebuiltRequest`가 임의 allowance를 받아 `recovery_plan.go:917-973`의 plan finalizer를 우회한다. 또한 `FinalizeRebuiltRequest`는 `recovery_plan.go:927-929`에서 plan/idempotency만 비교하고 `recovery_plan.go:1247-1249`의 capability를 draft에서 그대로 노출해 plan admission 제약과 달라도 승인한다.
- 해결 방법: exported constructor를 unexported `newRebuiltRequest`로 바꾸고 `FinalizeRebuiltRequest` 내부에서만 호출한다. `slices.Equal(draft.RequiredCapabilities(), p.RequiredCapabilities())` 같은 exact immutable snapshot 비교를 allowance 계산 전에 수행해 mismatch는 빈 final plan/request와 error로 끝낸다. 성공한 final request만 plan과 동일한 identity, required capabilities와 Core 계산 allowance를 가진다.

Before (`recovery_plan.go:927-969`, `recovery_plan.go:1208-1210`):

```go
if draft.PlanID() != p.PlanID() || draft.IdempotencyKey() != p.IdempotencyKey() {
    return RecoveryPlan{}, RebuiltRequest{}, errors.New("streamgate: rebuilt request plan or idempotency key mismatch")
}
request, err := NewRebuiltRequest(draft, allowance)

func NewRebuiltRequest(draft RebuiltRequestDraft, managedAllowance int) (RebuiltRequest, error)
```

After:

```go
import "slices"

if draft.PlanID() != p.PlanID() || draft.IdempotencyKey() != p.IdempotencyKey() {
    return RecoveryPlan{}, RebuiltRequest{}, errors.New("streamgate: rebuilt request plan or idempotency key mismatch")
}
if !slices.Equal(draft.RequiredCapabilities(), p.RequiredCapabilities()) {
    return RecoveryPlan{}, RebuiltRequest{}, errors.New("streamgate: rebuilt request required capabilities mismatch")
}
request, err := newRebuiltRequest(draft, allowance)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/recovery_plan.go`: final constructor 비공개화, capability exact binding validation, managed/fault finalizer call 갱신.
  - [ ] `packages/go/streamgate/recovery_plan_test.go`: direct constructor call 제거와 모든 성공 draft에 plan capability snapshot 전달.
- 테스트 작성: 작성. `TestRebuiltRequestFinalizationAuthorityAndCapabilities`에서 success와 missing/reordered/mutated capability mismatch, 빈 final result와 dispatcher no-call을 검증한다.
- 중간 검증: `go test -count=1 ./packages/go/streamgate -run 'Test(RebuiltRequestFinalizationAuthorityAndCapabilities|ManagedTrajectoryPostRebuildFinalization|RebuiltRequestDraftLifecycleAndIdentity)'`가 PASS한다.

### [REVIEW_FINAL_REQUEST_AUTHENTICITY-2] Dispatcher regression closure

- 문제: `recovery_plan_test.go:375-415`의 managed fixture는 plan capability를 draft에 싣지 않고 allowance만 관찰하며, `recovery_plan_test.go:710-724`는 public bypass를 정상 동작으로 검증한다.
- 해결 방법: managed/fault/prepared fixtures가 plan의 capability snapshot으로 draft를 만들게 하고 recording dispatcher가 plan/idempotency, allowance, capabilities를 함께 캡처한다. capability mismatch와 exhaustion에서는 final request가 zero value이고 dispatcher call이 0인지 검증한다. `NewRebuiltRequest` exported symbol과 직접 call site가 없는지 deterministic search로 고정한다.
- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/recovery_plan_test.go`: authority/capability table, recording dispatcher 동등성, mismatch/exhaustion no-dispatch assertions.
  - [ ] `packages/go/streamgate/recovery_plan.go`: package-private final request factory만 유지.
- 테스트 작성: 작성. 위 신규 test와 기존 `TestManagedTrajectoryPostRebuildFinalization`, `TestRebuiltRequestDraftLifecycleAndIdentity`, `TestRecoveryHostSeamsAndRawFreeReferences`가 회귀 fixture다.
- 중간 검증: `if rg --sort path -n '\bNewRebuiltRequest\b' packages/go/streamgate/recovery_plan.go packages/go/streamgate/recovery_plan_test.go; then exit 1; fi`가 출력 없이 exit 0이다.

### [REVIEW_FINAL_REQUEST_AUTHENTICITY-3] Verification closure

- 문제: 현재 focused/package/race PASS는 public factory bypass와 plan capability drift를 성공 경로로 포함해 final request authenticity를 증명하지 못한다.
- 해결 방법: host Go identity, gofmt, focused authority/lifecycle tests, fresh package/race, exported/stale/sibling symbol search와 diff check를 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/recovery_plan.go`, `packages/go/streamgate/recovery_plan_test.go`: gofmt와 forbidden symbol 없음.
  - [ ] `CODE_REVIEW-*-G??.md`: 모든 명령의 실제 출력을 기록한다.
- 테스트 작성: 별도 test file은 만들지 않는다. AUTHENTICITY-1/2 회귀를 기존 contract test 파일에 둔다.
- 중간 검증: 아래 최종 검증 전체가 PASS한다.

## 의존 관계 및 구현 순서

선행 index 11/12 complete 유지 → AUTHENTICITY-1 sole factory/capability validation → AUTHENTICITY-2 regression → AUTHENTICITY-3. 디렉터리명에 없는 추가 runtime 의존성은 없다.

## 수정 파일 요약

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

## 최종 검증

- `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(ManagedTrajectoryPostRebuildFinalization|RebuiltRequestDraftLifecycleAndIdentity|RebuiltRequestFinalizationAuthorityAndCapabilities|RecoveryPlanDefensiveCopiesAndPreparationContract|RecoveryRequestSnapshotRefBounds|RecoveryHostSeamsAndRawFreeReferences)'` — PASS.
- `go test -count=1 ./packages/go/streamgate` — fresh package PASS; test cache를 사용하지 않는다.
- `go test -race -count=1 ./packages/go/streamgate` — fresh race PASS.
- `if rg --sort path -n '\bNewRebuiltRequest\b' packages/go/streamgate/recovery_plan.go packages/go/streamgate/recovery_plan_test.go; then exit 1; fi` — 출력 없이 exit 0.
- `if rg --sort path -n '\bWithManagedRebuiltRequest\b' packages/go/streamgate/recovery_plan.go packages/go/streamgate/recovery_plan_test.go; then exit 1; fi` — 출력 없이 exit 0.
- `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` — 출력 없음.

proto/config/wire, Edge-Node 진단, 보조 E2E와 외부 provider는 적용하지 않는다. 이 follow-up은 아직 production caller가 없는 transport-agnostic contract와 unit fixture만 변경한다.

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