# FINDING 9 — Image-phase Ref-contract Violations Implementation Plan (v1)

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** scene_image_pipeline E2E partial(44/64, 20 fail)을 일으킨 4종 image-phase ref-contract 위반을 카테고리별 wave로 해소한다.

**Architecture:** image-generation consumer-layer 가 처음 E2E 도달하며 표면화된 4종 producer↔consumer 불일치를 단일 FINDING 9 umbrella + 카테고리별 wave 로 분리 수정. 코드 fix 3 + producer prompt-pack fix 1. 각 wave 는 독립 commit, W5 에서 통합 E2E 재검증.

**Tech Stack:** Python 3.12 / FastAPI backend, PostgreSQL, pytest, scene image pipeline (Gemini image gen).

---

## Status

- **W0 (read-only triage + root-cause): 완료.** Codex `FINDING9_W0_VERDICT: APPROVED_FOR_PLAN_DRAFTING_WITH_ORDER_ADJUSTMENT`.
- **plan v1: `APPROVED_FOR_EXECUTION`** — Codex `FINDING9_PLAN_AMEND1_REVIEW: APPROVED_FOR_EXECUTION` (amend 1 = 3 BLOCKING NARROW 반영).
- **실행은 fresh-session 에서 시작** (Codex SESSION VERDICT). W1 Cat3 부터: entry W0 → TDD failing test → 구현 → gate → atomic commit → Codex per-wave review. W2/W3/W4 동일 commit 금지. DB/checkpoint 수동 수정 금지, partial checkpoint + 138 png evidence 보존.
- 대상: project `be446d9c-90bf-429e-84f7-b8e549a1c734` / episode `35f49275-7cf2-4592-9fac-216c6fdadefd`. origin/main `d8ac895` (FINDING 8 포함).
- partial checkpoint + disk asset 138개 = 증거 보존 (삭제 금지).

## Context

FINDING 5/6/7/8 해소 후 파이프라인이 처음으로 image phase 끝(`scene_image_pipeline`)까지 도달.
`scene_image_pipeline` step `a7890a01` = **partial, 64 applicable / 44 ok / 20 fail**.
실패 20건은 `scene_checkpoint.json` `failed` dict (still_id keyed, 20 entries) 원문으로 전수 확보.
메모리 핸드오프 수치(12/8/4/4)는 오류 — 실제 분포 = **12 / 4 / 2 / 2**.

## W0 Evidence Table — 20 실패 / 4 카테고리

| Cat | 위반 | 건수 | still_id (S#.shot) | root cause | 분류 |
|-----|------|------|--------------------|-----------|------|
| 1 | `close_ref_usage_violation` | 12 | 45c8b9de(S7.2) 7c07f2fd(S12.5) 7a237ee0(S8.6) 16036e83(S5.7) 18852c67(S9.1) ddaca45f(S20.5) 2d601a4a(S28.3) f8685e50(S26.6) 6704b108(S14.9) a5ff35a3(S18.2) 107742ef(S25.4) 63349e9d(S27.2) | shot_staging `framing_scale='close'` ↔ shot_dependency_t2i `ref_usage` ∈ {exact_background×8, atmosphere_reference×2, ''×2}. consumer `scene_reference_service.py:988` close×ref_usage matrix fail-fast. 두 producer cross-step 불일치, 상호검증 0. | pre-existing latent |
| 2 | `required character_outlook missing` | 4 | f9478ab3(S1.3 C07O09) 0357a014(S15.2 C04O06) b0a64260(S16.3 C04O06) 04e1a30a(S5.10 C02O03) | 4건 전부 `render_prompt_card.id_policy.subject_reference_policy` 에서 해당 character = `policy:base_id_required`. t2i_prompt bare `C##` 토큰 = W4b canonicalization 기준 정상. 그러나 동일 rpc 의 `asset_requirements.required_refs` 는 `character_outlook`(policy=required) 선언 + resolver `resolve_refs_for_prompt_set:438` 은 composite regex `(C\d{2,3})(O\d{2,3})` 로만 attach → bare `C##` 0 attach. | **post-W4b surfaced compatibility gap** |
| 3 | `required prop missing` | 2 | 1b7bc30b(S18.15 P11) 1217a370(S25.7 P10) | batch-path ordering bug. `generate_scene_in_loop` 의 `resolve_refs_for_prompt_set` 호출(:694)이 `required_refs` 를 `still_data.get("render_prompt_card")`(:689)에서 추출하나 render_prompt_card 는 `:836` 에서야 still_data 에 inject → resolver 가 `required_refs=[]` 수신. prop attach 는 Area D-min 이후 required_refs 단일 SOT → batch-path prop attach 100% broken (required-prop still 2/64, 둘 다 실패). | pre-existing latent |
| 4 | `reference_phrase_kinds prop-without-meta` | 2 | e4d27b1f(S7.5) d2d0e2f1(S12.17) | scene_detail 이 per-variation sidecar `reference_phrase_kinds` 에 `'prop'` emit (e4d27b1f=P05 스마트폰, d2d0e2f1=P09 벽 위 원형 표식 — 둘 다 descriptive P## 언급, prop ref image 미매핑). scene_detail `system.md:134` producer contract = "declare a kind ONLY IF prompt relies on appearance supplied by numbered image mappings". `required_refs` 는 id_policy 기준 정상 제외. validator `ref_contract_validator.py:330` step6 phantom guard false-positive. | pre-existing latent (Area#5 era) |

**보강 증거:**
- close_ref 12건: `shot_staging/manifest.json` 12 shot 전부 `framing_scale='close'`; `shot_dependency_t2i/manifest.json` 동일 shot `location_refs[].ref_usage` 확인.
- Cat2 reference image: C07O09/C04O06/C02O03 outlook ref image_asset 전부 `generated` 존재 — 미첨부는 attachment-layer 문제.
- Cat3: P10/P11 = active prop, `is_primary=1` ref image_asset, disk file 존재, episode-linked, `visible_entities_json` 존재 — 전부 정상. 순수 ordering bug.
- contract validator (ref_contract_validator Patch A `5dba607` / Area#5 sidecar `71f253a` / framing_scale SOT `58ced51` / close×ref_usage matrix) 전부 기존 commit — image phase 가 첫 E2E 도달로 latent 표면화.
- **FINDING 5-8 직접 regression 없음.** Cat2 만 FINDING 6 W4b(`872ab4a`, base_id_required canonicalization) 와 직접 맞물림.

## Wave 구조 + 실행 순서 (Codex W0 verdict)

umbrella = FINDING 9, 카테고리별 wave 분리. 한 commit 으로 4종 동시 수정 금지.
실행 순서 = **W1 Cat3 → W2 Cat2 → W3 Cat1 → W4 Cat4 → W5**. 근거:
- W1 Cat3 가 가장 작음(단일 ordering fix), prop missing 2건 즉시 제거, 후속 wave 의 evidence/fixture 안정화.
- W2 Cat2 = W4b 와 직접 맞물림 → 단독 wave. required_refs 생성 vs resolver attachment 중 single-SOT 선정 필수.
- W3 Cat1 = 12건 최대지만 producer 간 정책 조정 → blast radius 큼.
- W4 Cat4 = scene_detail sidecar contract 조정.
- W5 = 통합 E2E 재검증 + closure/push.

각 wave 는 Codex per-wave review (memory [[feedback_codex_mcp_discussion_workflow]]). push 정책 = W5 range review `APPROVED_FOR_PUSH` 후 일괄.

---

## Task W1: Cat3 — batch-path render_prompt_card ordering fix

**근본 원인:** `generate_scene_in_loop` 에서 `resolve_refs_for_prompt_set`(`:694`) 가 `_patch_a_required_refs_batch`(`:689`, `still_data.get("render_prompt_card")` 기반) 를 인자로 받지만, render_prompt_card 의 still_data inject 는 `:836` 에서 일어난다. scene_still DB dict 자체엔 render_prompt_card 필드가 없으므로 resolver 는 항상 `required_refs=[]` 수신 → prop 영원히 미첨부.

**Fix:** render_prompt_card resolution 블록(현 `:828-836`: `_scene_index`/`_shot_index` 계산 + `rpc` lookup + `still_data["render_prompt_card"]` inject)을 `_patch_a_required_refs_batch` 계산(`:689`) **이전**으로 이동. 단일 SOT — render_prompt_card 는 resolver·validator 양쪽에서 동일 still_data 필드를 소비.

**Files:**
- Modify: `backend/app/services/scene_generation_coordinator.py` (`generate_scene_in_loop` — rpc resolution 블록 relocate)
- Test: `backend/tests/services/test_scene_generation_coordinator.py` (기존 파일, 신규 test 추가)

- [ ] **Step 1: W1.0 baseline — broad test-pin**

Run: `cd backend && PYTHONPATH=. .venv/bin/python -m pytest tests/services/test_scene_generation_coordinator.py tests/services/test_build_scene_attached_refs.py tests/services/test_ref_contract_validator.py -q`
Expected: 현 PASS 수 기록 (baseline). 이후 step 에서 regression 0 확인용.

- [ ] **Step 2: Write the failing test**

`backend/tests/services/test_scene_generation_coordinator.py` 에 추가. batch path 가 render_prompt_card.required_refs(kind=prop)를 resolver 호출 시점에 전달하는지 검증. test 는 `generate_scene_in_loop` 의 `resolve_refs_for_prompt_set` 인자 `required_refs` 가 비어있지 않음을 assert (mock 으로 resolver 호출 capture, render_prompt_card 가 prop required_ref 포함하는 still fixture).

```python
def test_batch_path_passes_required_refs_to_resolver(monkeypatch, ...):
    # still_data = scene_still dict WITHOUT 'render_prompt_card' (DB shape)
    # scene_detail manifest stub returns rpc with required_refs=[{"kind":"prop","id":"P10","policy":"required"}]
    captured = {}
    def _capture_resolver(*a, **kw):
        captured["required_refs"] = kw.get("required_refs")
        return _empty_payload()
    monkeypatch.setattr(reference_svc, "resolve_refs_for_prompt_set", _capture_resolver)
    coordinator.generate_scene_in_loop(si=..., ...)
    assert captured["required_refs"] == [{"kind": "prop", "id": "P10", "policy": "required"}]
```

(정확한 fixture/mock 구성은 기존 `test_scene_generation_coordinator.py` 패턴 따름 — 실행 시 기존 test 의 coordinator 생성/mock 헬퍼 재사용.)

- [ ] **Step 3: Run test to verify it fails**

Run: `cd backend && PYTHONPATH=. .venv/bin/python -m pytest tests/services/test_scene_generation_coordinator.py::test_batch_path_passes_required_refs_to_resolver -v`
Expected: FAIL — `captured["required_refs"] == []` (현 ordering bug).

- [ ] **Step 4: Relocate rpc resolution block**

`generate_scene_in_loop` 내 현 `:828-836` 블록 (`_scene_index` / `_shot_index` / `rpc = still_data.get("render_prompt_card")` / `if rpc is None: lookup_render_prompt_card(...) → still_data["render_prompt_card"]=rpc`) 을 `_patch_a_required_refs_batch` 계산(`:689`) 직전으로 이동. 원위치(`:828-836`)의 중복 블록 제거. `chain_bg_lookup` 빌드(`:837`)는 원위치 유지(rpc 무관). relocate 후 `:689` 의 `still_data.get("render_prompt_card")` 가 inject 된 rpc 를 읽음.

- [ ] **Step 5: Run new test + W1.0 baseline regression**

Run: `cd backend && PYTHONPATH=. .venv/bin/python -m pytest tests/services/test_scene_generation_coordinator.py::test_batch_path_passes_required_refs_to_resolver tests/services/test_scene_generation_coordinator.py tests/services/test_build_scene_attached_refs.py tests/services/test_ref_contract_validator.py -q`
Expected: 신규 test PASS + Step 1 baseline 수 ≥ 유지 (regression 0).

- [ ] **Step 6: Commit**

```bash
git add backend/app/services/scene_generation_coordinator.py backend/tests/services/test_scene_generation_coordinator.py
git commit -m "$(cat <<'EOF'
W1(e2e-bughunt-v1): scene_image batch-path render_prompt_card ordering fix — FINDING 9 Cat3

resolve_refs_for_prompt_set received required_refs=[] because render_prompt_card
was injected into still_data after the resolver call. Relocate rpc resolution
before _patch_a_required_refs_batch so prop attach (Area D-min required_refs SOT)
works in the batch path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task W2: Cat2 — base_id_required character_outlook compatibility

**근본 원인:** `subject_reference_policy.policy == "base_id_required"` subject 는 t2i_prompt 에 bare `C##` 토큰을 쓴다(W4b canonicalization, 정상). 그러나 (a) 동일 render_prompt_card 의 `required_refs` 는 그 subject 를 `character_outlook`(C##O##) 으로 required 선언하고, (b) resolver 는 composite 토큰만 인식해 bare `C##` 에 대해 base ref 조차 attach 하지 않는다.

**Design decision — CONFIRMED D1+D2+D3 (Codex plan v1 review BLOCKING NARROW 1).**
single SOT = `required_refs`. D1 단독은 위반만 제거하고 base ref attachment 미보장 → 불충분 (Codex 확정). 구현 전제: `render_prompt_card.build_asset_requirements` (또는 그 호출부) 가 현재 `subject_reference_policy` 를 인자로 받지 않음 → **입력 배선 추가 필요**.

- (D1) `render_prompt_card.build_asset_requirements` 또는 호출부가 normalized `subject_reference_policy` / policy_map 을 입력으로 받는다. visible subject 별 policy 분기:
  - `policy == base_id_required` → `{kind:"character", id:"C##", policy:"required"}` emit.
  - `policy == id_and_outlook_required` → 기존 `{kind:"character_outlook", id:"C##O##", policy:"required"}` 유지.
  - `policy == generic_descriptor_allowed` → character / character_outlook required_ref 둘 다 emit 안 함.
- (D2) resolver `resolve_refs_for_prompt_set`: prop branch(`:571-608`) 와 대칭으로 `required_refs(kind="character")` 기반 base-ref force-attach branch 추가 — base ref → `attached_meta.append(("character", sid))`.
- (D3) `ref_contract_validator.py`: step 3b 신설 — `required.get("character", [])` 의 각 id 가 `("character", id)` 로 attached_meta 에 exact 존재해야 통과 (step 3 character_outlook / step 3a prop 와 동일 패턴).

**W4b rollback 금지/불필요** — W4b(`872ab4a`) base_id_required canonicalization 은 정상 동작. W2 = downstream required_refs/resolver/validator 가 그 출력을 honor 하도록 정합하는 것.

**Files (실행 시 정확한 함수 위치 pin):**
- Modify: `backend/app/core/steps/render_prompt_card.py` (required_refs / asset_requirements 생성 — base_id_required 분기)
- Modify: `backend/app/services/scene_reference_service.py` (`resolve_refs_for_prompt_set` — character base branch)
- Modify: `backend/app/core/ref_contract_validator.py` (step 3b)
- Test: `backend/tests/services/test_ref_contract_validator.py`, `backend/tests/core/steps/test_render_prompt_card_helper.py`, `backend/tests/services/test_scene_reference_service.py`

**Gates (test 작성 기준 — 3 policy 전부 포함):**
- G1: `base_id_required` subject → required_refs 에 `kind="character"` (not `character_outlook`).
- G2: `id_and_outlook_required` subject → required_refs 에 `character_outlook` 유지 (회귀 0).
- G3: `generic_descriptor_allowed` subject → required_refs 에 character / character_outlook 둘 다 없음.
- G4: resolver 가 required `character` base ref 를 attached_meta `("character", sid)` 로 첨부.
- G5: validator step 3b — required character 미첨부 시 `RefContractError`, 첨부 시 통과.
- G6: 4 실패 still (f9478ab3/0357a014/b0a64260/04e1a30a) 재현 fixture → 위반 0.

**W2 entry W0 pin (의무):** `render_prompt_card.py` 의 required_refs / asset_requirements 생성 함수(`build_asset_requirements` 추정) + 그 호출부 + `subject_reference_policy` 정규화/소비 지점 식별. 입력 배선이 어디서 끊겨 있는지 확정 후 D1 구현.

> **Cat2 분류 표기:** plan/closure 문서에서 Cat2 는 pre-existing 아님 — `post-W4b surfaced compatibility gap` 으로 별도 표기. W4b rollback 대상 아님.

---

## Task W3: Cat1 — close_ref_usage cross-step guard/fix

**근본 원인:** shot_staging `framing_scale='close'` 와 shot_dependency_t2i `location_refs[].ref_usage` (zoom_in_detail 이어야 함) 가 cross-step 불일치. 두 producer 가 서로의 출력을 검증하지 않음. consumer `scene_reference_service.py:988` close×ref_usage matrix 가 image-gen 직전 fail-fast.

**Design decision — CONFIRMED producer-side 정합 (Codex plan v1 review BLOCKING NARROW 2).**
shot_dependency_t2i 가 shot_staging.framing_scale 을 SOT 로 참조 (prompt-pack 또는 deterministic post-processing). consumer(scene_image_pipeline) 완화는 **last resort** — close×ref_usage matrix invariant 보존. location_refs=0 close shot 분기를 아래로 **고정**:
1. close shot 에 **valid previous location_ref 가 존재**하면 — shot_dependency_t2i producer/postprocess 가 `ref_usage="zoom_in_detail"` 를 deterministic 하게 보장.
2. **valid previous ref 가 없으면** — `location_refs=[]` 가 정답. 임의 ref 합성 금지.
3. `location_refs=[]` 와 "location_ref object 는 존재하나 `ref_usage` 가 빈 문자열" 은 **다른 상태**. 후자 = malformed / ref_usage drift → fail-fast 또는 normalize 대상. 전자 = 정상 no-ref 가능성.
4. **W3 entry W0 pin (의무):** 현 2건(S9.1/S27.2)의 `''` 가 실제 empty list 인지 malformed object with empty `ref_usage` 인지 재확인. (현 W0 evidence: `shot_dependency_t2i/manifest.json` 의 두 shot `location_refs` = 길이 0 list 로 관측 + 그 shot 에 `best_prev_bytes` 존재 → "empty list 가 정답인지 under-declaration 인지" 를 W3 entry 에서 확정.)

**Files (실행 시 pin):**
- 검토: `backend/app/core/steps/shot_dependency_t2i_step.py`, `backend/app/modules/pipeline/` shot_dependency_t2i producer, 해당 prompt-pack `prompts/_base/...`
- 검토: shot_staging.framing_scale → shot_dependency_t2i 입력 배선
- Test: shot_dependency_t2i producer/통합 test + close×ref_usage 재현 12건

**Gates:**
- G1: close shot + valid prior location_ref → `ref_usage="zoom_in_detail"`.
- G2: close shot + valid prior ref 없음 → `location_refs=[]` (임의 ref 합성 0).
- G3: malformed "location_ref object 존재 + `ref_usage` 빈 문자열" → fail-fast 또는 normalize (분기 명시).
- G4: non-close shot ref_usage 회귀 0.
- G5: 12 실패 shot 재현 → close×ref_usage matrix 위반 0.

> **blast radius 주의:** producer 정책 조정 = 12건 영향. close framing 판정 SOT = shot_staging.framing_scale (enum SOT, `58ced51`). W3 진입 시 shot_dependency_t2i producer 의 ref_usage 결정 로직 W0-extension pin 필요.

---

## Task W4: Cat4 — reference_phrase_kinds prop over-declaration

**근본 원인:** scene_detail 이 per-variation sidecar `reference_phrase_kinds` 에 `'prop'` 을 over-declare. producer contract (`scene_detail` 최신 `system.md:129-137`) = "declare a kind ONLY IF the prompt body relies on appearance supplied by numbered image mappings in ID→REFERENCE IMAGE MAPPING". P05(스마트폰)/P09(벽 위 원형 표식) 는 descriptive P## 언급일 뿐 prop ref image 미매핑 → `'prop'` emit 금지 대상. `required_refs` 는 id_policy 기준 정상 제외.

**Design decision — CONFIRMED deterministic producer-side normalization (Codex plan v1 review BLOCKING NARROW 3).**
prompt-only fix 는 불충분 — LLM sidecar over-declaration 은 prompt clarification 만으로 재발 가능. **primary fix = scene_detail producer/postprocess 에서 `reference_phrase_kinds` 를 render_prompt_card / required_refs SOT 와 정합시키는 deterministic normalization.**
- primary: scene_detail 저장 전 producer-side guard — `required_refs` 에 `kind="prop"` 가 없으면 `reference_phrase_kinds` 에서 `'prop'` 제거 (또는 동등 효과의 explicit producer-side guard). self-consistent sidecar 를 저장 전 보장.
- validator `ref_contract_validator.py:295-334` step6 = **약화 금지** — downstream fail-fast 로 유지.
- 보조 (optional): scene_detail prompt-pack v34 clarification 으로 LLM 출력 품질 보강 가능 — **primary 아님**. v34 생성 시 version cascade + test-pin 필수. output schema 변경 없으면 `schema_version` 유지.

**Files (실행 시 pin):**
- Modify: scene_detail producer/postprocess (`reference_phrase_kinds` emit/저장 경로 — W4 entry 에서 정확한 함수 pin) — deterministic normalization guard 추가
- (보조) `prompts/_base/scene_detail/` 최신 → 신규 버전 디렉토리 (덮어쓰기 금지, v34 생성 시)
- Test: `backend/tests/` scene_detail producer test + `backend/tests/prompts/` alignment + Cat4 재현 2건
- **무변경 확인:** `ref_contract_validator.py:295-334` step6 (약화 금지)

**Gates:**
- G1: required_refs(kind=prop) 없음 + t2i_prompt 에 P## descriptive mention → `reference_phrase_kinds` 에 `'prop'` 없음 (producer normalization).
- G2: required_refs(kind=prop) 있음 → `reference_phrase_kinds` `'prop'` 유지.
- G3: 2 실패 still (e4d27b1f/d2d0e2f1) 재현 fixture → phantom guard 위반 0.
- G4: validator step6 무변경 (약화 0) 확인.
- G5: prompt-pack v34 생성 시 version cascade (version_registry / config_hash) + test-pin 정합.

> **prompt-pack 규칙 (CLAUDE.md):** 프롬프트 파일 덮어쓰기 금지 — 새 버전 디렉토리. 버전 형식 `버전.YYYYMMDDHHmm`.

**W4 entry W0 pin (의무):** scene_detail 의 `reference_phrase_kinds` emit/저장 경로 (producer 또는 postprocess 함수) + required_refs 와의 정합 지점 식별.

---

## Task W5: 통합 E2E 재검증 + closure/push

- [ ] **Step 1: 전체 backend test gate**

Run: `cd backend && PYTHONPATH=. .venv/bin/python -m pytest -q`
Expected: W1-W4 신규 test 전부 PASS, pre-existing 실패는 [[next_session_pre_existing_10_test_cleanup]] 범위만 (FINDING 9 무관).

- [ ] **Step 2: scene_image_pipeline E2E 재실행 (force, 동일 episode)**

project `be446d9c` / episode `35f49275` 의 image phase 재실행. 20 실패 still 이 0 으로 수렴하는지 확인. partial→완료 (64/64) 검증.
**수동 DB 수정 / 실패 silent accept 금지.** 재실행 전 Codex 에 force 재실행 승인 확인.

- [ ] **Step 3: Codex W1-W5 range review**

range diff + E2E 결과를 Codex 에 제출. `APPROVED_FOR_PUSH` verdict 시 push.

- [ ] **Step 4: closure 문서 + memory closure**

closure 문서 작성 (docs/superpowers 패턴), MEMORY.md 갱신, [[next_session_finding9_scene_image_pipeline_partial]] 해소 표기.

- [ ] **Step 5: push**

Codex `APPROVED_FOR_PUSH` 후 W1-W5 일괄 push (memory [[feedback_push_no_user_ask]]).

---

## Resolved decisions (Codex plan v1 review — amend 1)

1. **W2 single-SOT**: CONFIRMED D1+D2+D3 (required_refs SOT + resolver base branch + validator step 3b). D1 단독 불충분. `build_asset_requirements` 입력 배선(`subject_reference_policy`) 추가 + `generic_descriptor_allowed` 분기 명시. → W2 task 반영.
2. **W3 fix layer**: CONFIRMED producer-side 정합. `location_refs=[]` vs malformed empty `ref_usage` 분기 4-point 고정, W3 entry W0 re-pin 의무. consumer 완화 = last resort. → W3 task 반영.
3. **W4 fix layer**: CONFIRMED deterministic producer-side normalization 이 **primary** (prompt-pack v34 = 보조). validator step6 약화 금지. → W4 task 반영.
4. **wave 병합**: 4 wave 독립 유지 — 같은 root 로 합쳐질 근거 없음.
5. **세션 (Codex SESSION VERDICT)**: 현 세션 = narrow amend + re-review 까지만. plan 이 `APPROVED_FOR_EXECUTION` 되어도 **W1 실행은 fresh-session handoff 후 시작**. 현 세션에서 W1 코드 실행 진입 금지.

## Self-review

- W0 evidence 4 카테고리 20건 전수 — table 에 still_id 매핑 완비.
- W1 = 유일하게 fully-pinned (line-level), TDD step-complete. W2/W3/W4 = 설계+single-SOT 결정 확정+파일영역+gate level — 정확한 producer 함수 위치만 wave-entry W0 pin (각 wave 의 "entry W0 pin (의무)" 항목).
- amend 1 (Codex plan v1 review NEEDS_REVISION_NARROW_3): W2/W3/W4 의 3개 single-SOT 결정을 plan 안에 확정 lock — 설계 선택지 열림 0. line-level code-complete 는 W2/W3/W4 에 불요 (Codex IMPORTANT), 단 결정 + mandatory W-entry W0 pin 은 박음.
- placeholder 없음 — W2/W3/W4 의 "entry W0 pin" 은 plan 결함이 아니라 Codex 가 승인한 wave-진입-시 narrow scope (large-context producer 파일 전수 trace 를 W0 에 포함하지 않음).
- Cat2 분류 = `post-W4b surfaced compatibility gap` 일관 표기. W4b rollback 금지/불필요 W2 task 명시.
