# Area #5 — Reference Phrase / Phantom Guard Replacement Implementation Plan

> **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:** `ref_contract_validator.py` step 6 phantom guard 의 open-world semantic regex / lexicon (3 noun list + `classify_from_the_reference` 30/60자 window nearest-token classifier) 폐기 + LLM emit per-variation structured `reference_phrase_kinds` (array enum [character|background|prop]) sidecar 로 대체. step 1-5 closed-world identity contract 보존. 4 prompt surface + 2 code surface closed-list literal 폐기.

**Architecture:** scene_detail v25→v26 producer schema 에 `t2i_variations[].reference_phrase_kinds` required field 추가. `build_scene_attached_refs` 3-tuple → 4-tuple return. `validate_attached_refs` keyword-only required `reference_phrase_kinds` arg + step 6 rewrite (strict exact compare, legacy close-framing skip 폐기). t2i_review issue category `close_framing_existing_ref` prompt section + scene_schema.json enum 동시 삭제. scene_image translate_prompt + render_prompt_card + prompt_service phantom phrase prohibition closed-list 폐기, principle-only rewrite.

**Tech Stack:** Python 3.11+ / FastAPI / SQLAlchemy / pytest / pytest-asyncio / JSON schema validation / Gemini + GPT prompt packs (timestamped directory pattern)

**Spec:** `docs/superpowers/specs/2026-05-18-area-5-reference-phrase-phantom-guard-replacement-design.md` (commit `eab4032`, Codex iter 3 APPROVED_FOR_PLAN)

**Roadmap:** Track B `docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md` §5.5 (Tier 2 #2)

---

## File Structure

### W1 Producer Schema (Wave 1)

- Create: `prompts/_base/scene_detail/26.20260518HHMM/detail_schema.json` (v25 superset + `t2i_variations[].reference_phrase_kinds: array<enum>` required)
- Create: `prompts/_base/scene_detail/26.20260518HHMM/system.md` (v25 system instruction + `REFERENCE PHRASE KIND DECLARATION` principle-only section)
- Modify: `backend/app/core/steps/detail_steps.py:105-106` (`SCENE_DETAIL_SCHEMA_VERSION = 11`, `SCENE_DETAIL_PROMPT_VERSION = "26.20260518HHMM"`)
- Modify: existing test fixtures with scene_detail v25 shape → v26 shape (cascade)

### W2 Validator step 6 Rewrite + Legacy delete (Wave 2)

- Modify: `backend/app/core/ref_contract_validator.py:47-190` (atomic delete 13 identifiers: `_CHARACTER_TOKENS` / `_BACKGROUND_TOKENS` / `_OBJECT_TOKENS` / `_FROM_THE_REFERENCE_RE` / `_PLURAL_REFERENCE_IMAGES_RE` / `_PHRASE_LOCAL_WINDOW` / `_PHRASE_FALLBACK_WINDOW` / `_GENERIC_VERB_TOKENS` / `_GENERIC_NOUN_TOKENS` / `_classify_window` / `_is_plural_reference_images` / `_has_generic_instruction_signal` / `classify_from_the_reference`)
- Modify: `backend/app/core/ref_contract_validator.py:283-309` (`validate_attached_refs` signature + docstring 확장 — keyword-only required `reference_phrase_kinds`)
- Modify: `backend/app/core/ref_contract_validator.py:425-457` (step 6 rewrite — sidecar exact compare, legacy close-framing background skip 폐기)
- Rewrite: `backend/tests/services/test_ref_contract_validator.py` (phantom guard test class 전체 sidecar 기반 cascade)

### W3 Caller Cascade + Prompt + Code Sync (Wave 3)

- Modify: `backend/app/services/scene_generation_coordinator.py:209-343` (`build_scene_attached_refs` definition — 3-tuple → 4-tuple return, `reference_phrase_kinds` extract from selected variation)
- Modify: `backend/app/services/scene_generation_coordinator.py:1237` (single path unpack — 3-tuple → 4-tuple)
- Modify: `backend/app/services/scene_generation_coordinator.py:1287-1291` (single path `validate_attached_refs` call — `reference_phrase_kinds=` keyword arg 추가)
- Modify: `backend/app/services/scene_generation_coordinator.py:1339-1410` (variation path `_generate_variation_in_loop` — `var["reference_phrase_kinds"]` direct read + `validate_attached_refs` call signature 확장 + Fix C typed failure dict 보존)
- Modify: `backend/app/core/steps/render_prompt_card.py:1245-1314` (close framing forbidden phrase list + non-close owned positive phrase `'the door from the reference'` literal 폐기, principle-only rewrite)
- Modify: `backend/app/services/prompt_service.py:365-374` (translate system_prompt phantom phrase prohibition literal 폐기, translation preservation principle rewrite)
- Create: `prompts/_base/scene_image/4.20260518HHMM/translate_prompt.md` (v3 base + PHANTOM PHRASE PROHIBITION closed enumeration 폐기, principle-only)
- Create: `prompts/_base/t2i_review/5.20260518HHMM/scene_system.md` (v4 base + issue category `close_framing_existing_ref` section delete)
- Create: `prompts/_base/t2i_review/5.20260518HHMM/scene_schema.json` (v4 base + enum value `"close_framing_existing_ref"` 항목 삭제)
- Cascade: `backend/tests/services/test_build_scene_attached_refs.py` (3-tuple → 4-tuple unpack), `backend/tests/services/test_single_scene_uses_helper.py` (helper mock 4-tuple return), `backend/tests/services/test_single_scene_rpc_lookup.py` (helper return cascade if applicable). Direct `validate_attached_refs` tests including `test_d5_canary_s8.py` are W2 Task 2.5b (signature-change wave).

### W4 Closure (Wave 4)

- Modify: `docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md` §5.5 + §11 + §14 (Area #5 ✓ closed C-5 marker + closed area 추가 + 다음 진입점 update)
- Create: `backend/tests/integration/test_area_5_canary.py` (canary 5 scenario deterministic — 3 kind scenarios [character/background/prop] + empty array + close-framing strict violation)
- Memory: closure memo `~/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/session_20260518_area_5_reference_phrase_phantom_guard_sot_v1_closure.md`

---

## BLOCKING Pre-Steps (Tier 2 Pre-Step Discipline, 사용자 명시)

다음 5 pre-step 은 plan 본문 task 진행 전 의무 — 위반 시 W2/W3 cascade undercount 재발 (Tier 1 함정 #1 carry):

### Pre-Step 1: Cascade grep enumeration (이미 완료, 본 plan inline)

```
Production:
- ref_contract_validator.py:47-457 (전체 — 13 identifier + step 6 + signature)
- scene_generation_coordinator.py — build_scene_attached_refs definition (line 209-343) + single path unpack (line 1237) + 2 validate_attached_refs call sites (1287, 1393) + import (1266, 1378)
- render_prompt_card.py:1245-1314 (close framing + non-close owned constraints)
- prompt_service.py:365-374 (translate system_prompt phantom 부분)

Test (cascade scope, `rg -l "validate_attached_refs\(" backend/tests backend/app --glob '*.py'` 전수):
- backend/tests/services/test_ref_contract_validator.py (phantom guard direct tests 대량, 13 identifier 호출 site 8+, `validate_attached_refs` 직접 호출 다수) — W2 cascade
- backend/tests/services/test_d6_validator_stale_upstream.py (validate_attached_refs 직접 호출 9 site: line 36, 57, 77, 94, 130, 154, 170, 290, 315) — W2 cascade (signature 변경 시점). **정적 line list 만 의존 금지** — edits 후 `rg -n "validate_attached_refs\(" backend/tests/services/test_d6_validator_stale_upstream.py` 로 모든 direct call 에 `reference_phrase_kinds=` 동반 의무
- backend/tests/services/test_c01_zero_gate_regression.py (validate_attached_refs 직접 호출 2 site: line 76, 89) — W2 cascade (signature 변경 시점)
- backend/tests/integration/test_d5_canary_s8.py (validate_attached_refs call 3 site: line 60, 111, 140) — W2 cascade (signature 변경 시점, W3 으로 미루지 말 것)
- backend/tests/services/test_single_scene_rpc_lookup.py (validate_attached_refs 호출 — wording 확인 + 직접 call 시 cascade) — W2 또는 W3 cascade (확인 후)
- backend/tests/services/test_build_scene_attached_refs.py (3-tuple unpack — 4-tuple cascade 의무) — W3 cascade
- backend/tests/services/test_single_scene_uses_helper.py (helper mock return — 4-tuple cascade) — W3 cascade

Active prompt:
- prompts/_base/scene_image/3.202605101200/translate_prompt.md → new 4.20260518HHMM/
- prompts/_base/t2i_review/4.202605150957/{scene_system.md, scene_schema.json} → new 5.20260518HHMM/
- prompts/_base/scene_detail/25.202605161320/ → new 26.20260518HHMM/

Out-of-cascade (보존):
- visible_entities_validator.py (comment reference만, validate_attached_refs 책임 분리)
- scene_reference_service.py (comment reference만, line 488)
- scene_image_service.py (comment reference만, line 687)
- prompts/_base/scene_detail_owned_judge/3.202605051746/ (Area #7 carry, A045)
- prompts/_base/t2i_review/4.202605150957/system.md 의 다른 issue type (Area #6 carry)
```

### Pre-Step 2: Canonical principle wording mapping table (spec §5 reference)

Spec §5 canonical principle mapping table 의 5 row + A089 schema cascade row = single source. 본 plan 의 wave task wording 은 spec §5 row 와 1:1 매핑. wording drift 차단.

### Pre-Step 3: Residue gate scope definition (spec §9 reference)

Spec §9 의 4-항목 split (production code 4 group + active prompt + docs/architecture + out-of-residue) = single source. 본 plan 의 W4 residue gate task = spec §9 wording 그대로. Codex iter 2 I-1 pattern (`hits=0; for ...; if rg ...; then hits=1; fi; done; test "$hits" -eq 0`) form 의무.

### Pre-Step 4: NO VLM (사용자 hard rule)

LLM emit producer-side `reference_phrase_kinds` 만 sidecar source — VLM (Vision Language Model) 검증 / 의존 0. canary 도 mock LLM emit 만, real LLM call X. 사용자 memo `feedback_no_vlm_dependency.md` 일관.

### Pre-Step 5: tmux trigger checklist (양방향)

각 wave 완료 시 (W1/W2/W3/W4) Codex MCP pane (%0) 에 자동 알림 의무:
- `mcp__tmux-bridge__tmux_read %0` → `tmux_type` + `tmux_keys Enter`
- Codex 의논 prompt 끝에 verbatim `"완료되면 끝났다고 claude mcp 호출해줘!"`
- Codex → Claude pane `tmux_message` + Enter 의무 (양방향 trigger)

---

## Wave Sequence Overview

| Wave | Scope | Atomic commit count | Codex review |
|---|---|---|---|
| W1 | scene_detail v26 producer schema + prompt + cp invalidation | 1-2 commit | per-wave review (NEEDS_REVISION_MINOR → APPROVED) |
| W2 | `validate_attached_refs` signature 확장 + step 6 rewrite + legacy 13 identifier atomic delete | 1-2 commit | per-wave |
| W3 | caller cascade (single path 4-tuple unpack + variation path direct read) + 4 prompt sync + 2 code-side constraints rewrite + test cascade | 2-3 commit | per-wave |
| W4 | residue gate + canary 5 (3 kind + empty + strict violation) + roadmap §5.5/§11/§14 update + closure memo | 1-2 commit | range review (W1-W4) → APPROVED_FOR_PUSH |

각 wave 진입 전 Codex 의논 의무. 각 commit 은 atomic + path-limited add.

---

## Wave 1: scene_detail v26 producer schema + prompt

**Wave 목적**: producer side LLM 이 per-variation `reference_phrase_kinds` enum array emit 하도록 schema v26 추가. prompt instruction = closed-list 가르치지 말고 principle-only. cp invalidation 강제 (schema_version 10→11 + prompt_version v25→v26).

**Pre-task verify** (먼저 Codex 의논): W1 시점 timestamp 채택 (예: `26.20260518HHMM` → 실제 `26.202605181500` 등). prompt pack convention 확인 — Area #4 pattern (`scene_consistency` 도 schema_version + prompt_version 동시 bump).

### Task 1.1: Verify scene_detail v25 schema + system.md base 읽기

**Files:**
- Read: `prompts/_base/scene_detail/25.202605161320/detail_schema.json`
- Read: `prompts/_base/scene_detail/25.202605161320/system.md`
- Read: `backend/app/core/steps/detail_steps.py:105-110, 987-989, 1184-1185, 1448-1450, 1600-1620`

- [ ] **Step 1: v25 schema + system.md 본문 read 후 v25 t2i_variations 구조 파악**

목적 = v26 = v25 superset 의무 (Area #1/#2/#4 field 보존). 다른 required field (subject_reference_policy / gaze_direction_kind / gaze_target_id / subject_state / element_scope) 그대로 보존. 새 field `reference_phrase_kinds` 만 t2i_variations[] items 안에 추가.

- [ ] **Step 2: detail_steps.py 의 schema_version / prompt_version usage 확인**

`SCENE_DETAIL_SCHEMA_VERSION` 사용 사이트 4곳 (line 987 cp config_hash 계산, 1448 manifest, 1602 cp 유효성 check). cp invalidation 동작 트리거 확인.

### Task 1.2: scene_detail v26 detail_schema.json 작성

**Files:**
- Create: `prompts/_base/scene_detail/26.<W1_TIMESTAMP>/detail_schema.json`

`<W1_TIMESTAMP>` = wave 진입 시 timestamp (예: `202605181500`). 모든 신규 prompt 동일 timestamp 채택.

- [ ] **Step 1: v25 detail_schema.json 복사 → v26 base**

```bash
mkdir -p prompts/_base/scene_detail/26.<W1_TIMESTAMP>
cp prompts/_base/scene_detail/25.202605161320/detail_schema.json \
   prompts/_base/scene_detail/26.<W1_TIMESTAMP>/detail_schema.json
```

- [ ] **Step 2: `t2i_variations[].items` 안 `reference_phrase_kinds` required field 추가**

`t2i_variations` items schema 안 `properties` 에 다음 추가 (정확한 위치 — `t2i_prompt` 와 `evidence` 사이):

```json
"reference_phrase_kinds": {
  "type": "array",
  "items": {
    "type": "string",
    "enum": ["character", "background", "prop"]
  },
  "uniqueItems": true,
  "description": "Reference-kind sidecar for this variation's t2i_prompt. Empty array when the prompt body contains no reference-image attribution phrase. Producer SOT for Area #5 phantom guard — validator compares this set against attached_meta kinds. No closed phrase enumeration; structured declaration only."
}
```

같은 items 의 `required` 배열에 `"reference_phrase_kinds"` 추가 (기존 `t2i_prompt` 등 require 와 같은 level).

- [ ] **Step 3: jsonschema 자체 validation**

Run: `python -c "import json; json.load(open('prompts/_base/scene_detail/26.<W1_TIMESTAMP>/detail_schema.json'))"`
Expected: no SyntaxError.

Run: `python -c "import jsonschema; jsonschema.Draft7Validator.check_schema(__import__('json').load(open('prompts/_base/scene_detail/26.<W1_TIMESTAMP>/detail_schema.json')))"`
Expected: no exception.

### Task 1.3: scene_detail v26 system.md 작성

**Files:**
- Create: `prompts/_base/scene_detail/26.<W1_TIMESTAMP>/system.md`

- [ ] **Step 1: v25 system.md 복사**

```bash
cp prompts/_base/scene_detail/25.202605161320/system.md \
   prompts/_base/scene_detail/26.<W1_TIMESTAMP>/system.md
```

- [ ] **Step 2: principle-only `REFERENCE PHRASE KIND DECLARATION` section 추가**

system.md 의 적절한 위치 (t2i_variations 관련 instruction block 안 또는 직후) 에 다음 section 추가:

```
## REFERENCE PHRASE KIND DECLARATION (per-variation)

For each `t2i_variations[i]`, emit `reference_phrase_kinds`: a set of kind labels indicating which kinds of reference image content the prompt body relies on.

- Allowed values: "character", "background", "prop".
- Set membership criteria: declare a kind if the prompt body relies on character/background/prop appearance supplied by numbered image mappings in the ID → REFERENCE IMAGE MAPPING.
- Empty array = the prompt body does not use any reference-image attribution.
- This is a producer contract — do not enumerate forbidden phrases. The downstream validator compares the declared set against attached metadata kinds; declare truthfully what the prompt body relies on.
- No phrase examples are needed; this is a structural declaration, not a phrase enumeration task.
```

closed-list 가르치는 표현 (예: "use 'shown in image N' instead of 'from the reference'") 절대 추가 금지. 추상 원칙만.

- [ ] **Step 3: Inherited v25 Background Binding block rewrite (forbidden literal 제거 의무)**

`v25 system.md` line 131-148 (`## Background Binding (Rules A / C / E)` 본문) 에 다음 forbidden literal 잔존:
- line 139: `집중하려면 "from the reference" 앵커 사용`
- line 144-146: `the existing X / from the reference / use the X from the reference / preserving the same room perspective / maintaining the reference's framing / do not generate a new X`

W1 simple copy + new section 추가 strategy 만으로는 W1 residue gate fail. v26 system.md 의 Background Binding block 본문을 다음 principle-only wording 으로 rewrite 의무:

```markdown
## Background Binding (Rules A / C / E)

`[RenderPromptCard v1]` 의 `background_binding` field 가 이 shot 의 배경 binding 을
결정한다 (card 가 primary contract — 이 섹션 prose 와 충돌 시 card 우선).

**mode 별 동작:**

- `background_ref_attached`: `owned_objects` 의 항목은 배경 이미지에 이미 그려져 있다.
  새로 그리도록 지시 금지 — 이중 합성 발생. 참조 이미지의 내용에 의존하는 경우, 의존하는
  종류 (character / background / prop) 를 본 variation 의 `reference_phrase_kinds` set
  으로 declare. `camera_reference` 가 지정한 카메라 설정에 맞춰 t2i_prompt 를 작성;
  deviation 은 명시적으로 기술 (합성 scale 불일치 방지). owned/camera 둘 중 하나라도
  부재면 card constraints 가 conditional 로 표현하므로 prompt 도 그 분기 따른다.
- `skipped_close_framing`: 배경 reference 가 합성에서 제외된다. close framing 의 close
  subject 와 그 주변만 묘사; 부재한 background reference 에 의존하는 표현 금지. Rule A
  의 일치형/deviation형 표현도 close framing 에서는 무효 (reference 자체 없음).
  `[L##: ...]` block 으로 frame edge surface 직접 묘사.
- `background_mode_off` / `not_applicable`: reference 자체 없음 — 공간을 직접 묘사,
  reference-image 종류 declare 안 한다 (`reference_phrase_kinds = []`).
```

closed-list (`the existing X`, `from the reference`, `use the X from the reference`, `preserving the same room perspective`, `maintaining the reference's framing`, `do not generate a new X`) 모두 제거. closed-list 가르치는 표현 (예: "use 'shown in image N' instead of 'from the reference'") 절대 추가 금지. 추상 원칙만.

이외 v25 system.md 의 다른 section (line 1-130, 150-end) 변경 0 — Area #1/#2/#3/#4 보존.

- [ ] **Step 4: section 추가 + Background Binding rewrite 정확성 verify**

Run: `grep -c "REFERENCE PHRASE KIND DECLARATION" prompts/_base/scene_detail/26.<W1_TIMESTAMP>/system.md`
Expected: `1`

Run: `grep -E "from the reference|the existing X|use the X from the reference|preserving the same room perspective|maintaining the reference's framing|do not generate a new X" prompts/_base/scene_detail/26.<W1_TIMESTAMP>/system.md`
Expected: 0 hits (closed-list / phrase example 0, Background Binding rewrite 후).

Run: `grep -E "reference_phrase_kinds" prompts/_base/scene_detail/26.<W1_TIMESTAMP>/system.md`
Expected: ≥3 hits (REFERENCE PHRASE KIND DECLARATION section + Background Binding block 안 declare wording).

### Task 1.4: SCENE_DETAIL_SCHEMA_VERSION + SCENE_DETAIL_PROMPT_VERSION bump

**Files:**
- Modify: `backend/app/core/steps/detail_steps.py:105-106`

- [ ] **Step 1: Test 먼저 — schema_version bump verify**

Create or modify `backend/tests/services/test_detail_steps.py` (or existing equivalent):

```python
def test_scene_detail_schema_version_is_11():
    from backend.app.core.steps.detail_steps import SCENE_DETAIL_SCHEMA_VERSION
    assert SCENE_DETAIL_SCHEMA_VERSION == 11

def test_scene_detail_prompt_version_is_v26():
    from backend.app.core.steps.detail_steps import SCENE_DETAIL_PROMPT_VERSION
    assert SCENE_DETAIL_PROMPT_VERSION.startswith("26.")
```

- [ ] **Step 2: Run test — verify FAIL**

Run: `pytest backend/tests/services/test_detail_steps.py::test_scene_detail_schema_version_is_11 backend/tests/services/test_detail_steps.py::test_scene_detail_prompt_version_is_v26 -v`
Expected: 2 FAIL (current SCENE_DETAIL_SCHEMA_VERSION=10, PROMPT_VERSION="25.202605161320")

- [ ] **Step 3: Bump SCENE_DETAIL_SCHEMA_VERSION + PROMPT_VERSION**

Edit `backend/app/core/steps/detail_steps.py:105-106`:

```python
SCENE_DETAIL_SCHEMA_VERSION = 11  # 2026-05-18 (Area #5): t2i_variations[].reference_phrase_kinds 추가 (per-variation sidecar). 이전: 10 (Area #1 id_policy.subject_reference_policy).
SCENE_DETAIL_PROMPT_VERSION = "26.<W1_TIMESTAMP>"  # 2026-05-18 (Area #5) — v26 prompt: REFERENCE PHRASE KIND DECLARATION principle-only section 추가.
```

- [ ] **Step 4: Run test — verify PASS**

Run: `pytest backend/tests/services/test_detail_steps.py::test_scene_detail_schema_version_is_11 backend/tests/services/test_detail_steps.py::test_scene_detail_prompt_version_is_v26 -v`
Expected: 2 PASS

### Task 1.5: Existing scene_detail v25 fixture cascade verify (regression baseline)

**Files:**
- Read: `backend/tests/services/test_scene_detail_*.py` (모든 scene_detail 의존 test)
- Read: `backend/tests/fixtures/scene_detail/` (if applicable)

- [ ] **Step 1: scene_detail v25 fixture usage grep**

Run: `grep -rn "schema_version.*10\|prompt_version.*25.202605161320\|SCENE_DETAIL_SCHEMA_VERSION.*10" backend/tests/ --include="*.py" 2>/dev/null | head -20`
List: cascade target file names + line numbers.

- [ ] **Step 2: 각 cascade target 에 `reference_phrase_kinds: []` (empty default) 추가 — TDD baseline**

각 affected fixture 파일의 t2i_variations 안 dict 에 `"reference_phrase_kinds": []` 추가 (empty array — v26 required field 만족, validator 행동은 step 1-5 baseline 그대로). 의도: regression 0 (validator 변경 X, schema 만 superset).

- [ ] **Step 3: Run scene_detail-related tests — baseline regression check**

Run: `pytest backend/tests/services/test_scene_detail*.py -v 2>&1 | tail -30`
Expected: existing tests PASS (cascade 후), 모든 test 가 schema_version=11 + reference_phrase_kinds=[] shape 통과.

### Task 1.6: W1 atomic commit + Codex per-wave review

- [ ] **Step 1: W1 changes verify (path-limited)**

Run: `git status --short`
Expected stage candidates:
- `prompts/_base/scene_detail/26.<W1_TIMESTAMP>/detail_schema.json` (Create)
- `prompts/_base/scene_detail/26.<W1_TIMESTAMP>/system.md` (Create)
- `backend/app/core/steps/detail_steps.py` (Modify, line 105-106)
- `backend/tests/services/test_detail_steps.py` (Modify or Create)
- 기타 fixture cascade files (Task 1.5)

Run: `git diff --stat`
Confirm: 변경 file 수 정확히 W1 scope (cascade 외 file 변경 0).

- [ ] **Step 2: W1 atomic commit (path-limited add)**

```bash
git add prompts/_base/scene_detail/26.<W1_TIMESTAMP>/ \
        backend/app/core/steps/detail_steps.py \
        backend/tests/services/test_detail_steps.py \
        <cascade fixture files from Task 1.5>
git commit -m "$(cat <<'EOF'
feat(area-5-reference-phrase-phantom-guard-replacement-v1): W1 — scene_detail v26 producer schema + prompt + cp invalidation

t2i_variations[].reference_phrase_kinds: array<enum [character|background|prop]> required field 추가. principle-only system.md section (closed-list 가르치지 X). SCENE_DETAIL_SCHEMA_VERSION 10→11 + PROMPT_VERSION v25→v26 강제 v26 rerun.

spec §3 Producer layer + §3.4.1 prompt principle wording + §8 W1 deliverable.

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

- [ ] **Step 3: Codex per-wave review 의논 (rule 7a)**

tmux %0 에 W1 commit hash + diff summary + 4 gate matrix (Producer side) + 다음 W2 진입 confirm ask 송신. APPROVED_FOR_W2 받으면 W2 진입.

---

## Wave 2: validate_attached_refs signature 확장 + step 6 rewrite + legacy delete

**Wave 목적**: validator step 6 phantom guard 의 open-world semantic regex / lexicon 완전 폐기. `validate_attached_refs` signature 에 keyword-only required `reference_phrase_kinds` arg 추가. step 6 = sidecar exact compare (close-framing background skip 없음, strict). 13 legacy identifier atomic delete. step 1-5 변경 0.

**Pre-task verify** (Codex 의논): step 1-5 (line 199-417) 변경 0 보존 의무. signature 확장 시 default 값 없이 keyword-only required — caller 가 강제 누락 시 TypeError. Test cascade scope = `test_ref_contract_validator.py` 의 phantom guard test 대량 (W2 안에서 cascade).

### Task 2.1: Test 먼저 — sidecar phantom guard new behavior tests

**Files:**
- Create: `backend/tests/services/test_ref_contract_validator_sidecar.py`

- [ ] **Step 1: 신규 test file 작성 (sidecar phantom guard new behavior 검증)**

```python
"""Area #5 v1 — sidecar phantom guard tests.

scene_detail v26 producer 가 emit 한 `reference_phrase_kinds` 가
validate_attached_refs step 6 의 새 sidecar exact compare 진입점.
legacy classify_from_the_reference 폐기 후 test class.
"""
import pytest
from app.core.ref_contract_validator import validate_attached_refs, RefContractError


def _make_rpc(required_refs=None, readiness=None):
    """sidecar tests 의 _make_rpc default — readiness=None 으로 step 5 (readiness_policy
    consistency) 우회. step 6 sidecar 만 검사. step 5 readiness behavior 검사 test 는
    explicit non-empty required_refs + readiness="block_if_missing" 전달.
    """
    return {
        "asset_requirements": {
            "required_refs": required_refs or [],
            "readiness_policy": readiness,
        }
    }


def test_sidecar_character_declared_with_meta_passes():
    """character kind declared + attached character_outlook 1+ — PASS"""
    validate_attached_refs(
        _make_rpc(),
        labeled_refs=[("Reference image 1 — character C01: outlook O01", b"\x89PNG"*10)],
        attached_meta=[("character_outlook", "C01O01")],
        prompt="the character shown in image 1 holds an item",
        is_close_framing=False,
        reference_phrase_kinds=["character"],
    )  # no exception = PASS


def test_sidecar_character_declared_without_meta_fails():
    """character kind declared + attached_meta has no character — RefContractError"""
    with pytest.raises(RefContractError, match="reference_phrase_kinds declares kind='character'"):
        validate_attached_refs(
            _make_rpc(),
            labeled_refs=[("Reference image 1 — background L01", b"\x89PNG"*10)],
            attached_meta=[("background", "L01")],
            prompt="some prompt",
            is_close_framing=False,
            reference_phrase_kinds=["character"],
        )


def test_sidecar_background_declared_close_framing_with_meta_passes():
    """background kind declared + close framing + attached background meta — PASS (strict exact compare)"""
    validate_attached_refs(
        _make_rpc(),
        labeled_refs=[("Reference image 1 — background L01", b"\x89PNG"*10)],
        attached_meta=[("background", "L01")],
        prompt="some prompt",
        is_close_framing=True,
        reference_phrase_kinds=["background"],
    )  # no exception — C2 fix: step 6 ignores close-framing skip


def test_sidecar_background_declared_close_framing_without_meta_fails():
    """background kind declared + close framing + no background meta — RefContractError (C2 fix: legacy skip 폐기)"""
    with pytest.raises(RefContractError, match="reference_phrase_kinds declares kind='background'"):
        validate_attached_refs(
            _make_rpc(),
            labeled_refs=[("Reference image 1 — character C01: outlook O01", b"\x89PNG"*10)],
            attached_meta=[("character_outlook", "C01O01")],
            prompt="close-up of subject's hand",
            is_close_framing=True,  # legacy: skipped. C2 fix: still fails.
            reference_phrase_kinds=["background"],
        )


def test_sidecar_prop_declared_with_meta_passes():
    """prop kind declared + attached prop meta — PASS"""
    validate_attached_refs(
        _make_rpc(),
        labeled_refs=[("Reference image 1 — prop P01", b"\x89PNG"*10)],
        attached_meta=[("prop", "P01")],
        prompt="the object shown in image 1",
        is_close_framing=False,
        reference_phrase_kinds=["prop"],
    )  # PASS


def test_sidecar_empty_array_passes():
    """empty reference_phrase_kinds array — no phantom check, step 6 passes"""
    validate_attached_refs(
        _make_rpc(),
        labeled_refs=[],
        attached_meta=[],
        prompt="generic prompt body without reference attribution",
        is_close_framing=False,
        reference_phrase_kinds=[],
    )  # PASS


def test_sidecar_missing_kwarg_raises_typeerror():
    """reference_phrase_kinds keyword arg 누락 — TypeError (keyword-only required, no default)"""
    with pytest.raises(TypeError, match="reference_phrase_kinds"):
        validate_attached_refs(
            _make_rpc(),
            labeled_refs=[],
            attached_meta=[],
            prompt="x",
            is_close_framing=False,
            # reference_phrase_kinds 누락
        )


def test_sidecar_none_raises_refcontracterror():
    """reference_phrase_kinds=None — RefContractError (No Silent Fallback)"""
    with pytest.raises(RefContractError, match="reference_phrase_kinds missing"):
        validate_attached_refs(
            _make_rpc(),
            labeled_refs=[],
            attached_meta=[],
            prompt="x",
            is_close_framing=False,
            reference_phrase_kinds=None,
        )


def test_sidecar_invalid_enum_value_raises():
    """reference_phrase_kinds 안 invalid enum value — RefContractError"""
    with pytest.raises(RefContractError, match="reference_phrase_kinds invalid enum value"):
        validate_attached_refs(
            _make_rpc(),
            labeled_refs=[],
            attached_meta=[],
            prompt="x",
            is_close_framing=False,
            reference_phrase_kinds=["face"],  # invalid (allowed: character/background/prop)
        )


def test_sidecar_non_list_type_raises():
    """reference_phrase_kinds non-list type — RefContractError"""
    with pytest.raises(RefContractError, match="reference_phrase_kinds malformed"):
        validate_attached_refs(
            _make_rpc(),
            labeled_refs=[],
            attached_meta=[],
            prompt="x",
            is_close_framing=False,
            reference_phrase_kinds="character",  # str, not list
        )


def test_sidecar_multiple_kinds_all_validated():
    """multiple kinds declared — 모든 kind 가 attached meta 와 strict compare"""
    # character + background 둘 다 declared, 둘 다 attached
    validate_attached_refs(
        _make_rpc(),
        labeled_refs=[
            ("Reference image 1 — character C01: outlook O01", b"\x89PNG"*10),
            ("Reference image 2 — background L01", b"\x89PNG"*10),
        ],
        attached_meta=[("character_outlook", "C01O01"), ("background", "L01")],
        prompt="x",
        is_close_framing=False,
        reference_phrase_kinds=["character", "background"],
    )  # PASS

    # character + background declared, only character attached
    with pytest.raises(RefContractError, match="kind='background'"):
        validate_attached_refs(
            _make_rpc(),
            labeled_refs=[("Reference image 1 — character C01: outlook O01", b"\x89PNG"*10)],
            attached_meta=[("character_outlook", "C01O01")],
            prompt="x",
            is_close_framing=False,
            reference_phrase_kinds=["character", "background"],
        )
```

- [ ] **Step 2: Run new tests — verify FAIL (validator 미수정 상태)**

Run: `pytest backend/tests/services/test_ref_contract_validator_sidecar.py -v 2>&1 | tail -30`
Expected: 모든 test FAIL (TypeError on signature, current validator does not accept `reference_phrase_kinds`).

### Task 2.2: validate_attached_refs signature 확장

**Files:**
- Modify: `backend/app/core/ref_contract_validator.py:283-309` (signature + docstring)

- [ ] **Step 1: signature 확장 — keyword-only required `reference_phrase_kinds`**

`ref_contract_validator.py` 의 `validate_attached_refs` def 수정 (line 283-291):

```python
def validate_attached_refs(
    rpc: Optional[Dict[str, Any]],
    labeled_refs: List[Tuple[str, bytes]],
    attached_meta: List[Tuple[str, str]],
    prompt: str,
    is_close_framing: bool,
    *,
    chain_bg_lookup: Optional[Callable[[str], Optional[str]]] = None,
    reference_phrase_kinds: List[str],  # keyword-only required (Area #5 v1, per-variation sidecar)
) -> None:
```

`reference_phrase_kinds` 가 keyword-only required — default 값 없음. 누락 시 TypeError. None / non-list / invalid enum value 는 step 6 안에서 RefContractError fail-fast.

- [ ] **Step 2: docstring 확장 (line 292-309)**

기존 docstring 의 step 6 항목 변경:

```python
    """spec D5 §4.4 + Area #5 v1 — identity-level required_refs vs attached_meta + prev_shot
    lineage + readiness_policy + meta length invariant + sidecar phantom guard.

    위반 시 RefContractError raise (fail-fast). HTTP 422.

    검사 순서:
      1. invariant 1 — len(labeled_refs) == len(attached_meta).
      2. rpc shape strict (D2 보존, 사용자 binding G1 — `or {}` 금지).
      3. character_outlook strict — ("character_outlook", cid) exact match.
         close_framing 분기 없음 (사용자 binding G2).
      4. background — NOT close framing 시 ("background", bg_id) exact 또는
         chain_bg_lookup(bg_id) 가 loc_id 반환 시 ("background_prev_shot", loc_id)
         lineage 일치. chain_bg_lookup=None / lookup 결과 None 이면 lineage
         확인 불가 = missing (사용자 binding G3).
      5. readiness_policy=block_if_missing consistency (required_refs empty 시 drift).
      6. Sidecar phantom guard (Area #5 v1) — `reference_phrase_kinds` (producer
         emit) 의 각 kind 가 `attached_meta` 에 해당 kind 1+ 있는지 strict exact
         compare. close-framing 무관 (legacy 30/60자 window classifier 폐기,
         strict exact compare).
    """
```

- [ ] **Step 3: Run tests — Task 2.1 일부 PASS verify**

Run: `pytest backend/tests/services/test_ref_contract_validator_sidecar.py::test_sidecar_missing_kwarg_raises_typeerror -v`
Expected: PASS (signature 확장 후 keyword arg 누락 → TypeError raised)

다른 test 는 아직 FAIL — step 6 본문 미수정.

### Task 2.3: step 6 rewrite — sidecar exact compare

**Files:**
- Modify: `backend/app/core/ref_contract_validator.py:425-457`

- [ ] **Step 1: step 6 본문 rewrite (line 425-457 replace)**

기존 line 425-457 의 phantom guard 로직 (classifier_matches loop + `_meta_has_kind` calls) 모두 제거 후 다음으로 교체:

```python
    # 6. Sidecar phantom guard (Area #5 v1) — reference_phrase_kinds vs attached_meta
    #    kind set exact 비교. legacy classify_from_the_reference + 30/60자 window
    #    classifier 폐기, strict exact compare. close-framing 무관 (step 6 = strict,
    #    legacy skip 보존 X).
    if reference_phrase_kinds is None:
        raise RefContractError(
            "reference_phrase_kinds missing — producer (scene_detail v26) emit "
            "required, no silent fallback (Area #5 v1)"
        )
    if not isinstance(reference_phrase_kinds, list):
        raise RefContractError(
            f"reference_phrase_kinds malformed: must be list, "
            f"got {type(reference_phrase_kinds).__name__}={reference_phrase_kinds!r}"
        )
    _ALLOWED_KINDS = ("character", "background", "prop")
    for kind in reference_phrase_kinds:
        if kind not in _ALLOWED_KINDS:
            raise RefContractError(
                f"reference_phrase_kinds invalid enum value: {kind!r} "
                f"(allowed: {_ALLOWED_KINDS})"
            )
        if kind == "character" and not _meta_has_kind(
            attached_set, ("character", "character_outlook", "character_state")
        ):
            raise RefContractError(
                f"reference_phrase_kinds declares kind={kind!r} but no character "
                f"meta attached — attached={sorted(attached_set)}"
            )
        if kind == "background" and not _meta_has_kind(
            attached_set, ("background", "background_prev_shot")
        ):
            raise RefContractError(
                f"reference_phrase_kinds declares kind={kind!r} but no background "
                f"meta — attached={sorted(attached_set)}"
            )
        if kind == "prop" and not _meta_has_kind(attached_set, ("prop",)):
            raise RefContractError(
                f"reference_phrase_kinds declares kind={kind!r} but no prop meta — "
                f"attached={sorted(attached_set)}"
            )
```

Notes (코드 안 주석으로 추가하지 말고, 본 task 의 verbal 설명만):
- `prompt` arg 보존 (signature 안), step 6 안에서 prompt body read 0. 본문 prose parse 안 함.
- `_meta_has_kind` (line 193-197) 기존 helper 재사용.
- step 4 의 `is_close_framing` 분기 (line 380-417) 변경 0. step 4 = required_refs background close skip 보존, step 6 = strict (axis 분리).

- [ ] **Step 2: Run new sidecar tests — verify PASS**

Run: `pytest backend/tests/services/test_ref_contract_validator_sidecar.py -v 2>&1 | tail -30`
Expected: 모든 sidecar test PASS (10+ test).

### Task 2.4: legacy 13 identifier atomic delete

**Files:**
- Modify: `backend/app/core/ref_contract_validator.py:47-190`

- [ ] **Step 1: Delete legacy identifier (line 47-190 전체)**

다음 코드 블록 전체 삭제 (도구 line 47-190):

```python
# classifier 토큰 list — conservative (false negative 보다 false positive 회피)
# spec §4.2 Non-goals: classify_from_the_reference 알고리즘 재설계 X — 토큰 그대로.
_CHARACTER_TOKENS = (...)
_BACKGROUND_TOKENS = (...)
_OBJECT_TOKENS = (...)

_FROM_THE_REFERENCE_RE = re.compile(...)

# 2026-05-10 D2 phantom guard 보정 (review NEEDS_REVISION_MINOR 1차+2차 반영): ...
_PLURAL_REFERENCE_IMAGES_RE = re.compile(...)
_PHRASE_LOCAL_WINDOW = 30
_PHRASE_FALLBACK_WINDOW = 60

# generic-instruction line signal ...
_GENERIC_VERB_TOKENS = (...)
_GENERIC_NOUN_TOKENS = (...)


def _classify_window(window: str) -> str: ...
def _is_plural_reference_images(prompt: str, end_pos: int) -> bool: ...
def _has_generic_instruction_signal(prompt: str, phrase_start: int) -> bool: ...
def classify_from_the_reference(prompt: str) -> List[Dict[str, Any]]: ...
```

→ 모두 삭제. `import re` 도 ref_contract_validator.py 안 다른 site (line 387 BG_ID_RE) 에서 사용 안 하면 같이 정리 (verify with `grep -n "re\." backend/app/core/ref_contract_validator.py`).

- [ ] **Step 2: docstring 안 stale wording 정리 (line 1-20)**

ref_contract_validator.py 의 module docstring (line 1-20) 안 phantom guard 설명 wording 정리:

```python
"""Reference attachment contract validator — D5 identity-level + Area #5 sidecar edition.

spec: docs/superpowers/specs/2026-05-09-attached-reference-identity-contract-design.md §4.4
      docs/superpowers/specs/2026-05-18-area-5-reference-phrase-phantom-guard-replacement-design.md (Area #5 v1)
plan: docs/superpowers/plans/2026-05-09-attached-reference-identity-contract-implementation.md Task 3
      docs/superpowers/plans/2026-05-18-area-5-reference-phrase-phantom-guard-replacement-implementation.md

required_refs 의 (kind, id) 가 attached_meta 에 exact match 로 존재해야 통과.
substring/text 매칭 폐기. P1 (label 추론 X) + P2 (fallback meta 위조 X).

핵심 contract (D5):
- character_outlook required → attached_meta 에 ("character_outlook", id) exact 존재.
  base "character" / "character_state" 는 만족 X (close_framing 무관).
- background required + NOT close → attached_meta 에 ("background", bg_id) exact
  존재 또는 ("background_prev_shot", chain_bg_lookup(bg_id)) lineage 일치.
  chain_bg_lookup(bg_id)=None 이면 lineage 확인 불가 = missing.
- readiness_policy=block_if_missing + required_refs empty → drift fail-fast.
- meta length mismatch → 즉시 fail-fast (invariant 1).
- sidecar phantom guard (Area #5 v1): reference_phrase_kinds (per-variation, producer
  emit) 의 각 kind 가 attached_meta 의 kind set 과 strict exact compare. legacy
  classify_from_the_reference + 30/60자 window classifier 폐기. step 4 의 close-framing
  background skip 은 보존하지만 step 6 은 strict (axis 분리).

D2 strict extractor 그대로 보존 (사용자 binding G1) — `or {}` silent fallback 금지.
"""
```

- [ ] **Step 3: Run legacy delete verify**

Run: `grep -nE "_CHARACTER_TOKENS|_BACKGROUND_TOKENS|_OBJECT_TOKENS|_FROM_THE_REFERENCE_RE|_PLURAL_REFERENCE_IMAGES_RE|_PHRASE_LOCAL_WINDOW|_PHRASE_FALLBACK_WINDOW|_GENERIC_VERB_TOKENS|_GENERIC_NOUN_TOKENS|_classify_window|_is_plural_reference_images|_has_generic_instruction_signal|classify_from_the_reference" backend/app/core/ref_contract_validator.py`
Expected: 0 hits (모두 atomic delete).

- [ ] **Step 4: Run all ref_contract_validator tests — regression check**

Run: `pytest backend/tests/services/test_ref_contract_validator*.py -v 2>&1 | tail -30`
Expected: legacy `test_ref_contract_validator.py` 의 `classify_from_the_reference` 직접 test 8+개는 ImportError (function 폐기됨). 다음 task 2.5 에서 cascade 정리.

### Task 2.5: Legacy test cascade — phantom guard direct tests 삭제 / migrate

**Files:**
- Modify: `backend/tests/services/test_ref_contract_validator.py`

- [ ] **Step 1: classify_from_the_reference 직접 test 삭제**

`test_ref_contract_validator.py` 안 다음 test 함수 모두 삭제 (line 20-110 area):

- `test_classify_basic_*`
- `test_classify_window_*`
- `test_classify_plural_images_*`
- `test_classify_generic_instruction_*`

import 도 `from app.core.ref_contract_validator import classify_from_the_reference` 모든 line 삭제.

- [ ] **Step 2: validate_attached_refs phantom guard 간접 test 의 cascade — reference_phrase_kinds 추가**

기존 `validate_attached_refs` 호출 test 의 모든 call site 에 `reference_phrase_kinds=` keyword arg 추가. legacy phantom guard 의도가 character → `["character"]` 등 추정해 set. baseline regression 의도 = `[]` default.

```python
# Before (legacy phantom guard via prompt prose):
validate_attached_refs(
    rpc, labeled_refs, attached_meta,
    prompt="the character from the reference holds the object from the reference",
    is_close_framing=False,
)

# After (Area #5 sidecar):
validate_attached_refs(
    rpc, labeled_refs, attached_meta,
    prompt="the character from the reference holds the object from the reference",  # prose preserved for error context
    is_close_framing=False,
    reference_phrase_kinds=["character", "prop"],  # explicit sidecar
)
```

각 affected test 의 의도 따라 set 채택.

- [ ] **Step 3: Run all tests — verify cascade pass**

Run: `pytest backend/tests/services/test_ref_contract_validator*.py -v 2>&1 | tail -30`
Expected: 모든 test PASS (sidecar new tests + cascaded existing tests).

### Task 2.5b: Additional direct validate_attached_refs caller cascade (signature 변경 시점)

**Files:**
- Modify: `backend/tests/services/test_d6_validator_stale_upstream.py` (line 36, 57, 77, 94, 130, 154, 170, 290, 315 — 9 direct call sites)
- Modify: `backend/tests/services/test_c01_zero_gate_regression.py` (line 76, 89 — 2 direct call sites)
- Modify: `backend/tests/integration/test_d5_canary_s8.py` (line 60, 111, 140 — 3 direct call sites)
- Verify (and modify if needed): `backend/tests/services/test_single_scene_rpc_lookup.py` (direct call vs comment only — verify after read)

**Rationale**: validate_attached_refs signature 가 W2 Task 2.2 에서 keyword-only required `reference_phrase_kinds` 추가됨. broader regression 시 (또는 다른 wave 시) 이 test 들이 fail 하면 W2 시점 cascade 누락. Codex iter 1 C4 finding 흡수 의무 — W3 으로 미루지 말고 signature 변경과 같이 cascade.

- [ ] **Step 1: 각 file 의 direct call site read 후 `reference_phrase_kinds=<intent>` keyword arg 추가**

각 test 의 의도 (어떤 phantom guard scenario 였는지) 분석 후 set 채택:
- D6 stale upstream tests = stale upstream 검사 위주, phantom guard scope 무관 → `reference_phrase_kinds=[]` (empty) baseline
- C01 zero gate regression tests = zero gate 검사 위주 → `reference_phrase_kinds=[]` (empty) baseline
- D5 canary S8 tests = 각 canary scenario 의 의도 (character / background / prop) 따라 set (legacy object wording 금지; Area #5 enum value = prop)
- single_scene_rpc_lookup = read 후 결정 (direct call 시 baseline `[]`)

- [ ] **Step 2: Run cascade tests — verify PASS**

Run: `pytest backend/tests/services/test_d6_validator_stale_upstream.py backend/tests/services/test_c01_zero_gate_regression.py backend/tests/integration/test_d5_canary_s8.py backend/tests/services/test_single_scene_rpc_lookup.py -v 2>&1 | tail -30`
Expected: 모든 cascade test PASS (signature 새 arg 동반).

- [ ] **Step 3: Run broader regression — verify validate_attached_refs caller signature 변경 0 누락**

Run: `rg -l "validate_attached_refs\(" backend/tests --glob '*.py'` → all listed files PASS verify.
Expected: cascade scope 전수 (Pre-Step 1 grep 결과 와 일치).

### Task 2.6: W2 atomic commit + Codex per-wave review

- [ ] **Step 1: W2 changes verify**

Run: `git status --short`
Expected stage:
- `backend/app/core/ref_contract_validator.py` (Modify — line 47-190 delete + 283-309 signature + 425-457 step 6 rewrite + docstring update)
- `backend/tests/services/test_ref_contract_validator_sidecar.py` (Create)
- `backend/tests/services/test_ref_contract_validator.py` (Modify — cascade)
- `backend/tests/services/test_d6_validator_stale_upstream.py` (Modify — Task 2.5b cascade)
- `backend/tests/services/test_c01_zero_gate_regression.py` (Modify — Task 2.5b cascade)
- `backend/tests/integration/test_d5_canary_s8.py` (Modify — Task 2.5b cascade)
- `backend/tests/services/test_single_scene_rpc_lookup.py` (Modify if direct call site — Task 2.5b)

- [ ] **Step 2: W2 atomic commit**

```bash
git add backend/app/core/ref_contract_validator.py \
        backend/tests/services/test_ref_contract_validator_sidecar.py \
        backend/tests/services/test_ref_contract_validator.py \
        backend/tests/services/test_d6_validator_stale_upstream.py \
        backend/tests/services/test_c01_zero_gate_regression.py \
        backend/tests/integration/test_d5_canary_s8.py \
        backend/tests/services/test_single_scene_rpc_lookup.py
git commit -m "$(cat <<'EOF'
refactor(area-5-reference-phrase-phantom-guard-replacement-v1): W2 — validate_attached_refs sidecar arg + step 6 rewrite + legacy 13 identifier atomic delete

ref_contract_validator.py:
- signature 확장: keyword-only required `reference_phrase_kinds: List[str]`
- step 6 rewrite: sidecar `reference_phrase_kinds` vs attached_meta kind exact compare. legacy close-framing background skip 폐기 (strict, step 4 vs step 6 axis 분리).
- legacy 13 identifier atomic delete: _CHARACTER_TOKENS / _BACKGROUND_TOKENS / _OBJECT_TOKENS / _FROM_THE_REFERENCE_RE / _PLURAL_REFERENCE_IMAGES_RE / _PHRASE_LOCAL_WINDOW / _PHRASE_FALLBACK_WINDOW / _GENERIC_VERB_TOKENS / _GENERIC_NOUN_TOKENS / _classify_window / _is_plural_reference_images / _has_generic_instruction_signal / classify_from_the_reference

test cascade:
- test_ref_contract_validator_sidecar.py 신규 (10+ tests)
- test_ref_contract_validator.py classify_from_the_reference 직접 test 삭제 + validate_attached_refs call site `reference_phrase_kinds=` keyword arg cascade

step 1-5 변경 0 (Area B-min identity contract boundary 보존).

spec §3.1 + §3.2 + §3 4-layer Consumer row + §4 closed area boundary + §13 trap 27/28/29/34.

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

- [ ] **Step 3: Codex per-wave review 의논**

tmux %0 에 W2 commit hash + W2 4-gate matrix (Consumer + Helper + Legacy delete) + 다음 W3 진입 confirm ask. APPROVED_FOR_W3 받으면 W3 진입.

---

## Wave 3: caller cascade + 4 prompt sync + code-side constraints rewrite

**Wave 목적**: scene_generation_coordinator 의 helper 4-tuple cascade + 2 validate_attached_refs call site signature 확장. 4 prompt surface (scene_image translate / t2i_review scene_system + scene_schema / render_prompt_card / prompt_service) closed-list literal 폐기 / principle-only rewrite. test cascade.

**Pre-task verify** (Codex 의논): Fix C typed failure dict 보존 의무 (variation path). caller behavior 변경 0 (single propagate / variation typed dict). build_scene_attached_refs 의 selected variation logic 변경 0 (return shape만 확장).

### Task 3.1: build_scene_attached_refs — 4-tuple return signature

**Files:**
- Modify: `backend/app/services/scene_generation_coordinator.py:209-343`

- [ ] **Step 1: Test 먼저 — 4-tuple return test**

`backend/tests/services/test_build_scene_attached_refs.py` 안에 신규 test 추가:

```python
def test_build_scene_attached_refs_returns_4_tuple_with_reference_phrase_kinds(coord, scene_detail_fixture_v26):
    """Area #5 v1: helper returns (full_prompt, labeled_refs, attached_meta, reference_phrase_kinds)"""
    from app.services.scene_generation_coordinator import build_scene_attached_refs
    result = build_scene_attached_refs(**inputs)  # use existing fixture pattern
    assert len(result) == 4, f"Expected 4-tuple, got {len(result)}: {result}"
    full_prompt, labeled_refs, attached_meta, reference_phrase_kinds = result
    assert isinstance(reference_phrase_kinds, list)
    # reference_phrase_kinds from selected variation in scene_detail v26 fixture
    assert reference_phrase_kinds == scene_detail_fixture_v26["t2i_variations"][selected_index]["reference_phrase_kinds"]


def test_build_scene_attached_refs_missing_reference_phrase_kinds_raises(coord, scene_detail_fixture_v25):
    """Area #5 v1: scene_detail v25 (no reference_phrase_kinds field) → stale producer error"""
    from app.services.scene_generation_coordinator import build_scene_attached_refs
    from app.core.ref_contract_validator import RefContractError
    # v25 fixture has no reference_phrase_kinds field on t2i_variations items
    with pytest.raises((KeyError, RefContractError)):
        build_scene_attached_refs(**v25_inputs)
```

- [ ] **Step 2: Run test — verify FAIL**

Run: `pytest backend/tests/services/test_build_scene_attached_refs.py::test_build_scene_attached_refs_returns_4_tuple_with_reference_phrase_kinds -v`
Expected: FAIL (current helper returns 3-tuple)

- [ ] **Step 3: build_scene_attached_refs signature + selected variation logic 확장**

`scene_generation_coordinator.py:209-343` 의 `build_scene_attached_refs` 함수 수정:

(a) 함수 signature return annotation 3-tuple → 4-tuple:

```python
def build_scene_attached_refs(
    *,
    still: SceneStill,
    still_data: Dict[str, Any],
    visible_entities: List[Dict[str, Any]],
    # ... (other args 동일)
) -> Tuple[str, List[Tuple[str, bytes]], List[Tuple[str, str]], List[str]]:
```

(b) 함수 본문 안 selected variation pick 위치에서 `reference_phrase_kinds` extract:

```python
    # ... existing helper logic ...
    # selected_variation 은 기존 logic 으로 결정. Area #5 v1: selected variation 의
    # reference_phrase_kinds 도 같이 추출.
    reference_phrase_kinds = selected_variation.get("reference_phrase_kinds")
    if reference_phrase_kinds is None:
        # scene_detail v25 stale checkpoint signal — fail-fast (no silent fallback)
        raise RefContractError(
            f"selected_variation missing 'reference_phrase_kinds' — "
            f"stale scene_detail v25 checkpoint, rerun required "
            f"(Area #5 v1)"
        )
    return full_prompt, labeled_refs, attached_meta, reference_phrase_kinds
```

- [ ] **Step 4: Run test — verify PASS**

Run: `pytest backend/tests/services/test_build_scene_attached_refs.py -v 2>&1 | tail -20`
Expected: 4-tuple test PASS, missing field test PASS.

### Task 3.2: Single path unpack site cascade

**Files:**
- Modify: `backend/app/services/scene_generation_coordinator.py:1237`
- Modify: `backend/app/services/scene_generation_coordinator.py:1287-1291`

- [ ] **Step 1: Test 먼저 — single path validate_attached_refs call signature**

`backend/tests/services/test_single_scene_uses_helper.py` 안 기존 test 의 helper mock return 4-tuple cascade + `validate_attached_refs` call assertion 에 `reference_phrase_kinds=` kwarg verify:

```python
def test_single_path_passes_reference_phrase_kinds_to_validator(coord, tmp_path, mock_helper):
    """Area #5 v1: single path forwards helper's 4th tuple element to validator"""
    mock_helper.return_value = (
        "full_prompt_text",
        [("Reference image 1 — character C01: outlook O01", b"\x89PNG"*10)],
        [("character_outlook", "C01O01")],
        ["character"],  # 4th element: reference_phrase_kinds
    )
    # ... existing call setup ...
    with mock.patch(
        "app.services.scene_generation_coordinator.validate_attached_refs"
    ) as mock_validator:
        coord._build_single_scene_prompt_and_refs(still=..., still_data=..., visible_entities=...)

    # validator must be called with reference_phrase_kinds=["character"]
    _, kwargs = mock_validator.call_args
    assert kwargs.get("reference_phrase_kinds") == ["character"]
```

- [ ] **Step 2: Run test — verify FAIL**

Run: `pytest backend/tests/services/test_single_scene_uses_helper.py::test_single_path_passes_reference_phrase_kinds_to_validator -v`
Expected: FAIL (current single path call has no reference_phrase_kinds)

- [ ] **Step 3: Modify single path unpack — 3-tuple → 4-tuple**

`scene_generation_coordinator.py:1237` 변경:

```python
# Before:
full_prompt, labeled_refs, attached_meta = build_scene_attached_refs(...)

# After:
full_prompt, labeled_refs, attached_meta, reference_phrase_kinds = build_scene_attached_refs(...)
```

`scene_generation_coordinator.py:1287-1291` 의 `validate_attached_refs` call site 수정:

```python
validate_attached_refs(
    _rpc, labeled_refs, attached_meta, full_prompt,
    is_close_framing=_is_close_framing,
    chain_bg_lookup=_chain_bg_lookup,
    reference_phrase_kinds=reference_phrase_kinds,  # Area #5 v1
)
```

- [ ] **Step 4: Run test — verify PASS**

Run: `pytest backend/tests/services/test_single_scene_uses_helper.py -v 2>&1 | tail -20`
Expected: 모든 single path test PASS.

### Task 3.3: Variation path cascade

**Files:**
- Modify: `backend/app/services/scene_generation_coordinator.py:1339-1410`

- [ ] **Step 1: Test 먼저 — variation path direct var read**

`backend/tests/services/test_scene_generation_coordinator.py` 또는 variation path test 안 신규 test:

```python
def test_variation_path_reads_reference_phrase_kinds_from_var_dict(coord):
    """Area #5 v1: variation path reads var['reference_phrase_kinds'] directly"""
    var = {
        "var_t2i": "variation prompt body",
        "theme": "test_theme",
        "theme_label": "Test",
        "reference_phrase_kinds": ["background"],  # variation-level sidecar
    }
    # ... call _generate_variation_in_loop with var, attached_meta=[("background", "L01")] ...
    # validator must be called with reference_phrase_kinds=["background"]


def test_variation_path_missing_reference_phrase_kinds_typed_failure(coord):
    """Area #5 v1: variation dict missing reference_phrase_kinds → typed failure dict (Fix C 보존)"""
    var = {
        "var_t2i": "x",
        # reference_phrase_kinds 누락 — scene_detail v25 stale signal
    }
    result = coord._generate_variation_in_loop(var=var, ...)
    assert result["_failure_reason"] == "REF_CONTRACT_VIOLATION"
    assert "reference_phrase_kinds" in result["_failure_detail"]
```

- [ ] **Step 2: Run test — verify FAIL**

Run: `pytest backend/tests/services/test_scene_generation_coordinator.py::test_variation_path_reads_reference_phrase_kinds_from_var_dict backend/tests/services/test_scene_generation_coordinator.py::test_variation_path_missing_reference_phrase_kinds_typed_failure -v`
Expected: FAIL (variation path 현재 reference_phrase_kinds 미사용)

- [ ] **Step 3: Modify variation path — direct var dict read + validate call signature 확장**

`scene_generation_coordinator.py:1393-1410` 의 variation path 수정 — `var["reference_phrase_kinds"]` direct read 추가, `validate_attached_refs` call 에 keyword arg 추가:

```python
# variation path 안 _full_prompt build 직후, validate_attached_refs 호출 직전:
try:
    reference_phrase_kinds = var["reference_phrase_kinds"]  # scene_detail v26 required (KeyError = stale producer)
except KeyError:
    logger.error(
        "Scene %d Shot %s variation=%s: var dict missing 'reference_phrase_kinds' "
        "— stale scene_detail v25 checkpoint",
        still_data.get("scene_index", 0),
        still_data.get("shot_index") or still_data.get("_shot_index"),
        var_theme,
    )
    return {
        "_failure_reason": "REF_CONTRACT_VIOLATION",
        "_failure_detail": f"variation '{var_theme}' missing 'reference_phrase_kinds' — stale scene_detail v25 checkpoint, rerun required",
    }

try:
    validate_attached_refs(
        rpc, labeled_refs, attached_meta, _full_prompt,
        is_close_framing=is_close_framing,
        chain_bg_lookup=chain_bg_lookup,
        reference_phrase_kinds=reference_phrase_kinds,  # Area #5 v1
    )
except RefContractError as _ref_exc:
    logger.error(...)
    return {
        "_failure_reason": "REF_CONTRACT_VIOLATION",
        "_failure_detail": str(_ref_exc)[:500],
    }
```

`_generate_variation_in_loop` signature 에 `var` 가 이미 dict 로 들어옴 — 변경 0. 추가 arg 없음.

- [ ] **Step 4: Run test — verify PASS**

Run: `pytest backend/tests/services/test_scene_generation_coordinator.py -v 2>&1 | tail -20`
Expected: 모든 variation path test PASS, Fix C typed failure dict 보존.

### Task 3.4: scene_image v4 translate_prompt 작성

**Files:**
- Create: `prompts/_base/scene_image/4.<W3_TIMESTAMP>/translate_prompt.md`

- [ ] **Step 1: v3 translate_prompt.md base copy**

```bash
mkdir -p prompts/_base/scene_image/4.<W3_TIMESTAMP>
cp prompts/_base/scene_image/3.202605101200/translate_prompt.md \
   prompts/_base/scene_image/4.<W3_TIMESTAMP>/translate_prompt.md
```

- [ ] **Step 2: PHANTOM PHRASE PROHIBITION closed enumeration 폐기 + principle-only rewrite**

`translate_prompt.md` 의 lines 23-27 (PHANTOM PHRASE PROHIBITION block) 전체 삭제, 다음으로 교체:

```markdown
REFERENCE PRESERVATION:
- Preserve numbered image reference semantics ("the character/object shown in image N") exactly. Do not introduce or invent new reference-kind attribution that the original Korean prompt did not contain.
- Translation must remain prose-only — do not alter the reference structure of the prompt.
- The producer (scene_detail v26) declares `reference_phrase_kinds` per variation; this translation step does not modify that declaration.
```

closed-list (`"from the reference"`, `"from Reference image N"`, `"from a reference image"`, `"from … reference"`) 모두 제거. principle-only.

- [ ] **Step 3: 적용 정확성 verify**

Run: `grep -nE "PHANTOM PHRASE PROHIBITION|from the reference|from Reference image N|from a reference image" prompts/_base/scene_image/4.<W3_TIMESTAMP>/translate_prompt.md`
Expected: 0 hits

Run: `grep -nE "REFERENCE PRESERVATION|preserves numbered image reference|reference_phrase_kinds" prompts/_base/scene_image/4.<W3_TIMESTAMP>/translate_prompt.md`
Expected: 3 hits

### Task 3.5: t2i_review v5 scene_system.md + scene_schema.json 작성

**Files:**
- Create: `prompts/_base/t2i_review/5.<W3_TIMESTAMP>/scene_system.md`
- Create: `prompts/_base/t2i_review/5.<W3_TIMESTAMP>/scene_schema.json`

- [ ] **Step 1: v4 dir 두 파일 copy**

```bash
mkdir -p prompts/_base/t2i_review/5.<W3_TIMESTAMP>
cp prompts/_base/t2i_review/4.202605150957/scene_system.md \
   prompts/_base/t2i_review/5.<W3_TIMESTAMP>/scene_system.md
cp prompts/_base/t2i_review/4.202605150957/scene_schema.json \
   prompts/_base/t2i_review/5.<W3_TIMESTAMP>/scene_schema.json
```

- [ ] **Step 2: scene_system.md — `### 4. close_framing_existing_ref` section 전체 삭제 + 번호 재정렬**

`scene_system.md:31-49` 의 `### 4. close_framing_existing_ref ★ 신규 — close framing 시 reference 가정 표현 누출` section 전체 삭제 (heading + 검출 패턴 + target/suggestion 작성 방식 + 예시 모두). 다음 section (`### 5. physical_inconsistency`) 가 `### 4. physical_inconsistency` 로 번호 재정렬. `### 6. unshared_fg_bg_actors` 가 `### 5.` 로.

scene_system.md 의 상단 issue type 개요 (목차) 도 `5 → 6 → 5` 형식으로 update (전체 issue type 수 5).

- [ ] **Step 3: scene_schema.json — `"close_framing_existing_ref"` enum 항목 삭제**

`scene_schema.json:27-36` 의 `issue_code` enum 안 `"close_framing_existing_ref"` 항목만 제거. 다른 5 issue type (`"missing_ethnicity"`, `"proper_noun_translated"`, `"awkward_translation"`, `"physical_inconsistency"`, `"unshared_fg_bg_actors"`) 보존.

- [ ] **Step 4: 적용 정확성 verify**

Run: `grep -nE "close_framing_existing_ref" prompts/_base/t2i_review/5.<W3_TIMESTAMP>/`
Expected: 0 hits (scene_system.md + scene_schema.json 모두)

Run: `python -c "import json; data=json.load(open('prompts/_base/t2i_review/5.<W3_TIMESTAMP>/scene_schema.json')); print([x for x in data.get('properties',{}).get('issues',{}).get('items',{}).get('properties',{}).get('issue_code',{}).get('enum',[])])"`
Expected: 5 enum values, `"close_framing_existing_ref"` not in list.

### Task 3.6: render_prompt_card.py constraints rewrite

**Files:**
- Modify: `backend/app/core/steps/render_prompt_card.py:1245-1314`

- [ ] **Step 1: Test 먼저 — closed-list literal 0 verify**

`backend/tests/core/steps/test_render_prompt_card.py` 안 신규 test:

```python
def test_render_prompt_card_no_phantom_phrase_closed_list():
    """Area #5 v1: close framing constraints + non-close owned constraints 안
    closed-list phantom phrase literal 0"""
    import inspect
    from app.core.steps import render_prompt_card
    src = inspect.getsource(render_prompt_card)
    forbidden = [
        "the existing X",
        "use the X from the reference",
        "matching the reference camera",
        "the door from the reference",
        "preserving the same room perspective",
        "maintaining the reference's framing",
    ]
    for lit in forbidden:
        assert lit not in src, f"closed-list literal '{lit}' present in render_prompt_card.py"
```

- [ ] **Step 2: Run test — verify FAIL**

Run: `pytest backend/tests/core/steps/test_render_prompt_card.py::test_render_prompt_card_no_phantom_phrase_closed_list -v`
Expected: FAIL (현재 모든 literal 존재)

- [ ] **Step 3: Rewrite close framing forbidden block (line 1245-1264)**

`render_prompt_card.py:1249-1264` 의 `"close_framing_skips_background_ref": True` block 의 `constraints` list 수정:

```python
"constraints": [
    "this shot uses close framing — the background reference is intentionally omitted at composition time",
    "describe the close subject and its immediate surroundings without relying on the absent background reference",
    "use [L##: ...] block for frame-edge surface description",
],
```

기존 closed-list 2개 (`"do not use any of: 'the existing X', 'from the reference', ..."` 와 `"do not use any of: 'matching the reference camera', ..."`) 모두 제거.

- [ ] **Step 4: Rewrite non-close owned constraints (line 1290-1306)**

`render_prompt_card.py:1287-1313` 의 owned + camera sub-cases (A/B/C/D) constraints 수정 — `'the door from the reference'` positive literal recommendation 모두 제거:

```python
constraints: List[str] = []
if bg_owned and cam_ref:
    # sub-case A: owned + camera 둘 다
    constraints.extend([
        "owned_objects lists all environment objects already drawn in the background reference — do not create new ones",
        "if focusing on an owned object, declare its reference-kind via the reference_phrase_kinds sidecar (producer contract); do not enumerate forbidden phrasings here",
        "camera_reference specifies the framing under which the background was painted — match camera_position, camera_height, and framing",
        "if deviating from the reference framing (push-in, angle shift, ECU), state the deviation explicitly so the compositor can account for the scale difference",
    ])
elif bg_owned and not cam_ref:
    # sub-case B: owned only
    constraints.extend([
        "owned_objects lists all environment objects already drawn in the background reference — do not create new ones",
        "if focusing on an owned object, declare its reference-kind via the reference_phrase_kinds sidecar (producer contract)",
    ])
elif not bg_owned and cam_ref:
    # sub-case C: camera only
    constraints.extend([
        "camera_reference specifies the framing under which the background was painted — match camera_position, camera_height, and framing",
        "if deviating from the reference framing (push-in, angle shift, ECU), state the deviation explicitly so the compositor can account for the scale difference",
    ])
else:
    # sub-case D: neither — fallback (R1-I1)
    constraints.append(
        "the chain_bg reference is attached but carries no owned-object list and no camera metadata "
        "— describe the scene freely while staying consistent with the reference image overall mood"
    )
```

- [ ] **Step 5: Rewrite no chain_bg block (line 1266-1278)**

`render_prompt_card.py:1273-1277` 의 `bg_id == None` block 의 `constraints` 수정:

```python
"constraints": [
    "no chain_bg is bound to this shot — describe the location freely without referencing a background image",
],
```

기존 `"do not use 'from the reference' / 'the existing room' / 'preserving the room' style phrasing — there is no reference image to refer to"` literal 제거.

- [ ] **Step 6: Run test — verify PASS**

Run: `pytest backend/tests/core/steps/test_render_prompt_card.py -v 2>&1 | tail -20`
Expected: closed-list literal 0 test PASS. 기존 다른 render_prompt_card test 도 PASS (logic 변경 없음, wording 만 변경).

### Task 3.7: prompt_service.py translate_prompt 부분 rewrite

**Files:**
- Modify: `backend/app/services/prompt_service.py:365-374`

- [ ] **Step 1: Test 먼저**

`backend/tests/services/test_prompt_service.py` 안 신규 test:

```python
def test_prompt_service_translate_no_phantom_closed_list():
    """Area #5 v1: translate system_prompt 안 phantom phrase closed-list literal 0"""
    import inspect
    from app.services import prompt_service
    src = inspect.getsource(prompt_service.translate_if_korean)  # actual function (line 309)
    forbidden = ["from the reference", "from Reference image N", "phantom guard"]
    for lit in forbidden:
        assert lit not in src, f"closed-list literal '{lit}' present in translate"
```

- [ ] **Step 2: Run test — verify FAIL**

Run: `pytest backend/tests/services/test_prompt_service.py::test_prompt_service_translate_no_phantom_closed_list -v`
Expected: FAIL

- [ ] **Step 3: system_prompt wording rewrite (line 365-374)**

`prompt_service.py:365-374` 의 `call_text` system_prompt 수정:

```python
system_prompt=(
    "T2I 프롬프트 번역 전문가. 한국어를 영어로 변환하세요. "
    "'the character/object shown in image N' 표현은 정확히 보존하세요. "
    "원문에 없던 reference attribution 표현을 새로 추가하지 마세요 "
    "(producer 가 declare 한 reference_phrase_kinds sidecar 와 mismatch 차단)."
),
```

기존 `"'from the reference', 'from Reference image N' 같은 표현은 절대 새로 만들지 마세요 (phantom guard 충돌)."` 폐기.

- [ ] **Step 4: Run test — verify PASS**

Run: `pytest backend/tests/services/test_prompt_service.py::test_prompt_service_translate_no_phantom_closed_list -v`
Expected: PASS.

Run: `pytest backend/tests/services/test_prompt_service.py -v 2>&1 | tail -10`
Expected: 모든 prompt_service test PASS (regression 0).

### Task 3.8: prompt_service.py:227-230 obsolete docstring cleanup (Codex iter 1 I2)

**Files:**
- Modify: `backend/app/services/prompt_service.py:227-230`

**Rationale**: `replace_entity_ids` 의 docstring/comment 가 `ref_contract_validator._FROM_THE_REFERENCE_RE` 를 reference. W2 에서 이 regex 가 atomic delete 되므로 stale reference. production stale comment 정리.

- [ ] **Step 1: line 227-230 docstring read 후 wording rewrite**

`prompt_service.py:227-230` 의 `_FROM_THE_REFERENCE_RE` reference + 폐기된 regex 표현을 다음으로 교체:

```python
# Area #5 v1: numbered image wording (e.g. "the character shown in image N") is preserved for
# reference mapping. phantom validation is handled by structured `reference_phrase_kinds` sidecar
# (scene_detail v26 producer + ref_contract_validator step 6), not prompt-prose regex.
```

기존 `_FROM_THE_REFERENCE_RE` mention 및 regex 표기 모두 제거.

- [ ] **Step 2: Run grep — verify stale ref 제거**

Run: `grep -nE "_FROM_THE_REFERENCE_RE|classify_from_the_reference" backend/app/services/prompt_service.py`
Expected: 0 hits

### Task 3.9: W3 atomic commit + Codex per-wave review

- [ ] **Step 1: W3 changes verify**

Run: `git status --short`
Expected stage (W3 scope):
- `backend/app/services/scene_generation_coordinator.py` (Modify — helper signature + single path + variation path)
- `backend/app/core/steps/render_prompt_card.py` (Modify — close framing + non-close + no_bg constraints)
- `backend/app/services/prompt_service.py` (Modify — translate system_prompt)
- `prompts/_base/scene_image/4.<W3_TIMESTAMP>/translate_prompt.md` (Create)
- `prompts/_base/t2i_review/5.<W3_TIMESTAMP>/scene_system.md` (Create)
- `prompts/_base/t2i_review/5.<W3_TIMESTAMP>/scene_schema.json` (Create)
- `backend/tests/services/test_build_scene_attached_refs.py` (Modify — cascade + new test)
- `backend/tests/services/test_single_scene_uses_helper.py` (Modify — cascade + new test)
- `backend/tests/services/test_scene_generation_coordinator.py` (Modify — new variation tests)
- `backend/tests/core/steps/test_render_prompt_card.py` (Modify — new closed-list test)
- `backend/tests/services/test_prompt_service.py` (Modify — new translate test)
- Note: `test_d5_canary_s8.py` cascade 는 W2 Task 2.5b 로 이동 (signature 변경 시점 cascade 의무)

- [ ] **Step 2: W3 atomic commit (또는 2-3 sub-commit 으로 분할)**

```bash
git add backend/app/services/scene_generation_coordinator.py \
        backend/app/core/steps/render_prompt_card.py \
        backend/app/services/prompt_service.py \
        prompts/_base/scene_image/4.<W3_TIMESTAMP>/ \
        prompts/_base/t2i_review/5.<W3_TIMESTAMP>/ \
        backend/tests/services/test_build_scene_attached_refs.py \
        backend/tests/services/test_single_scene_uses_helper.py \
        backend/tests/services/test_scene_generation_coordinator.py \
        backend/tests/core/steps/test_render_prompt_card.py \
        backend/tests/services/test_prompt_service.py \
        backend/app/services/prompt_service.py  # docstring cleanup (Task 3.8) 포함
git commit -m "$(cat <<'EOF'
feat(area-5-reference-phrase-phantom-guard-replacement-v1): W3 — caller cascade + 4 prompt sync + 2 code constraints rewrite

scene_generation_coordinator.py:
- build_scene_attached_refs: 3-tuple → 4-tuple return (selected variation reference_phrase_kinds extract + missing field fail-fast)
- single path (line 1237 + 1287-1291): 4-tuple unpack + validate_attached_refs reference_phrase_kinds= kwarg cascade
- variation path (_generate_variation_in_loop): var["reference_phrase_kinds"] direct read + Fix C typed failure dict 보존 (missing field signal)

Prompt sync (4 surface):
- prompts/_base/scene_image/4.<W3_TS>/translate_prompt.md 신규: PHANTOM PHRASE PROHIBITION closed enumeration 폐기, REFERENCE PRESERVATION principle-only
- prompts/_base/t2i_review/5.<W3_TS>/scene_system.md + scene_schema.json 신규: close_framing_existing_ref issue category section + enum 동시 삭제 (6→5 issue type)
- render_prompt_card.py:1245-1314: close framing forbidden phrase list + non-close owned positive phrase 'the door from the reference' literal 폐기, principle-only rewrite (sidecar contract reference)
- prompt_service.py:365-374: phantom phrase prohibition literal 폐기, translation preservation principle (reference_phrase_kinds sidecar 보존)

Test cascade:
- test_build_scene_attached_refs.py: 4-tuple test 신규 + cascade
- test_single_scene_uses_helper.py: helper mock 4-tuple cascade + reference_phrase_kinds keyword verify
- test_scene_generation_coordinator.py: variation path direct read + Fix C typed failure dict 보존
- test_render_prompt_card.py: closed-list literal 0 verify
- test_prompt_service.py: translate system_prompt literal 0 verify
- test_d5_canary_s8.py cascade completed in W2 Task 2.5b (signature-change wave); W3 changes none.

step 1-5 변경 0 (Area B-min identity contract boundary 보존). Caller behavior 변경 0 (Fix C typed failure dict 보존).

spec §3.3 caller cascade + §3.4.2 + §3.4.3 + §3.4.4 + §3.4.5 + §8 W3.

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

- [ ] **Step 3: Codex per-wave review 의논**

tmux %0 에 W3 commit hash + diff stat + 4-gate matrix (Prompt + Code Constraints + Caller) + residue gate 임시 verify (W3 시점 in-scope file literal 0) + 다음 W4 진입 confirm ask. APPROVED_FOR_W4 받으면 W4 진입.

---

## Wave 4: residue gate + canary 5 + roadmap update + closure memo

**Wave 목적**: Area #5 v1 closure verdict 4-gate compliance verify (production residue split + active prompt residue + canary 5 PASS + roadmap update + closure memo). canary 5 = 3 kind scenarios (character / background / prop) + empty array + close-framing strict violation. Codex range review (W1-W4) → APPROVED_FOR_PUSH 받으면 push 결정.

### Task 4.1: Production residue gate (spec §9, 4-항목 split)

**Files:**
- Create: `backend/tests/integration/test_area_5_residue_gate.py`

- [ ] **Step 1: 4-항목 residue gate test 작성 (deterministic)**

```python
"""Area #5 v1 — production residue gate (spec §9, 4-항목 split).

Codex iter 2 I-1 pattern (Area #4 lesson 24) 의 strict form:
`hits=0; for ...; if rg ...; then hits=1; fi; done; test "$hits" -eq 0`
"""
import subprocess
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[3]  # /Users/manta/Documents/Projects/TheRoad-I1


def _rg_count(pattern: str, path: str) -> int:
    """rg count — exit 0 = found, 1 = not found, 2 = error"""
    result = subprocess.run(
        ["rg", "-c", "--no-heading", pattern, str(REPO_ROOT / path)],
        capture_output=True, text=True, check=False,
    )
    if result.returncode == 1:  # no match
        return 0
    if result.returncode != 0:
        raise RuntimeError(f"rg error: {result.stderr}")
    return sum(int(line.split(":")[-1]) for line in result.stdout.strip().split("\n") if line)


def test_residue_gate_1_ref_contract_validator_legacy_identifiers_zero():
    """Item 1: ref_contract_validator.py — 13 legacy identifier 0 hits"""
    identifiers = [
        "_CHARACTER_TOKENS", "_BACKGROUND_TOKENS", "_OBJECT_TOKENS",
        "_FROM_THE_REFERENCE_RE", "_PLURAL_REFERENCE_IMAGES_RE",
        "_PHRASE_LOCAL_WINDOW", "_PHRASE_FALLBACK_WINDOW",
        "_GENERIC_VERB_TOKENS", "_GENERIC_NOUN_TOKENS",
        "_classify_window", "_is_plural_reference_images",
        "_has_generic_instruction_signal", "classify_from_the_reference",
    ]
    hits = 0
    for ident in identifiers:
        if _rg_count(ident, "backend/app/core/ref_contract_validator.py") > 0:
            hits = 1
            print(f"FOUND: {ident}")
    assert hits == 0


def test_residue_gate_2_scene_generation_coordinator_validator_kwarg():
    """Item 2: scene_generation_coordinator.py — validate_attached_refs call 모두 reference_phrase_kinds= keyword 동반.

    Codex iter 1 M1 fix: regex-fragile `re.findall(r"validate_attached_refs\\([^)]*\\)", ...)` 폐기
    (nested call 시 truncate 위험). line-window scan 채택 — 각 `validate_attached_refs(` 발견 line
    부터 닫는 `)` 까지 다음 10 line 안에 `reference_phrase_kinds=` 가 있는지 확인.
    """
    path = REPO_ROOT / "backend/app/services/scene_generation_coordinator.py"
    lines = path.read_text().splitlines()
    hits = 0
    i = 0
    while i < len(lines):
        if "validate_attached_refs(" in lines[i] and "def validate_attached_refs" not in lines[i]:
            # find closing `)` within next 12 lines (call block)
            block_end = min(len(lines), i + 12)
            block_text = "\n".join(lines[i:block_end])
            if "reference_phrase_kinds=" not in block_text:
                hits = 1
                print(f"MISSING kwarg at line {i+1}: {lines[i].strip()}")
        i += 1
    assert hits == 0


def test_residue_gate_3_render_prompt_card_closed_list_literals_zero():
    """Item 3: render_prompt_card.py:1240-1320 — closed-list literal 0"""
    literals = [
        "the existing X",
        "use the X from the reference",
        "matching the reference camera",
        "the door from the reference",
        "preserving the same room perspective",
        "maintaining the reference's framing",
    ]
    hits = 0
    path = REPO_ROOT / "backend/app/core/steps/render_prompt_card.py"
    src = path.read_text()
    for lit in literals:
        if lit in src:
            hits = 1
            print(f"FOUND: {lit}")
    assert hits == 0


def test_residue_gate_4_prompt_service_translate_phantom_closed_list_zero():
    """Item 4: prompt_service.py:365-374 — phantom prohibition closed-list literal 0"""
    literals = [
        "'from the reference'",
        "'from Reference image N'",
        "phantom guard",
    ]
    hits = 0
    path = REPO_ROOT / "backend/app/services/prompt_service.py"
    src = path.read_text()
    for lit in literals:
        if lit in src:
            hits = 1
            print(f"FOUND: {lit}")
    assert hits == 0
```

- [ ] **Step 2: Run residue gate — verify all 4 PASS**

Run: `pytest backend/tests/integration/test_area_5_residue_gate.py -v 2>&1 | tail -20`
Expected: 4 PASS

### Task 4.2: Active prompt residue gate

**Files:**
- Create test or extend `test_area_5_residue_gate.py`:

- [ ] **Step 1: 4 prompt-residue check 추가**

```python
def test_active_prompt_residue_scene_image_v4_no_phantom_phrase_prohibition():
    """active prompt: scene_image v4 안 PHANTOM PHRASE PROHIBITION 0"""
    # find latest version dir
    import glob
    dirs = sorted(glob.glob(str(REPO_ROOT / "prompts/_base/scene_image/4.*/")))
    assert dirs, "No scene_image v4 dir found"
    latest = dirs[-1]
    hits = _rg_count("PHANTOM PHRASE PROHIBITION", latest.replace(str(REPO_ROOT)+"/", ""))
    assert hits == 0


def test_active_prompt_residue_t2i_review_v5_no_close_framing_existing_ref():
    """active prompt: t2i_review v5 안 close_framing_existing_ref 0"""
    import glob
    dirs = sorted(glob.glob(str(REPO_ROOT / "prompts/_base/t2i_review/5.*/")))
    assert dirs
    latest = dirs[-1]
    hits = _rg_count("close_framing_existing_ref", latest.replace(str(REPO_ROOT)+"/", ""))
    assert hits == 0


def test_active_prompt_residue_scene_detail_v26_no_closed_phrase_examples():
    """active prompt: scene_detail v26 안 closed-list phrase example 0"""
    import glob
    dirs = sorted(glob.glob(str(REPO_ROOT / "prompts/_base/scene_detail/26.*/")))
    assert dirs
    latest = dirs[-1]
    forbidden = ["from the reference", "the existing X", "shown in image N"]
    hits = 0
    for lit in forbidden:
        if _rg_count(lit, latest.replace(str(REPO_ROOT)+"/", "")) > 0:
            hits = 1
            print(f"FOUND: {lit}")
    assert hits == 0
```

- [ ] **Step 2: Run active prompt residue — verify all 3 PASS**

Run: `pytest backend/tests/integration/test_area_5_residue_gate.py -v 2>&1 | tail -20`
Expected: 7 PASS (production 4 + active prompt 3).

### Task 4.3: Canary 5 scenario deterministic (3 kind + empty + strict violation)

**Files:**
- Create: `backend/tests/integration/test_area_5_canary.py`

- [ ] **Step 1: Canary 5 fixture + test (3 kind + empty + strict violation)**

```python
"""Area #5 v1 — canary 5 scenario deterministic (3 kind + empty + strict violation).

producer mock LLM emit fixture (real LLM call X, VLM 미사용).
"""
from unittest import mock
import pytest
from app.core.ref_contract_validator import validate_attached_refs, RefContractError


def test_canary_1_character_kind_sidecar():
    """Canary 1: scene_detail v26 emit reference_phrase_kinds=['character'], attached_meta 에 character_outlook 1+"""
    validate_attached_refs(
        rpc={"asset_requirements": {"required_refs": [{"kind": "character_outlook", "id": "C01O01", "policy": "required"}], "readiness_policy": "block_if_missing"}},
        labeled_refs=[("Reference image 1 — character C01: outlook O01", b"\x89PNG\r\n\x1a\n" + b"\x00"*100)],
        attached_meta=[("character_outlook", "C01O01")],
        prompt="the character shown in image 1 stands at the threshold (canary 1)",
        is_close_framing=False,
        reference_phrase_kinds=["character"],
    )  # PASS — character declared + character_outlook attached


def test_canary_2_background_kind_sidecar_close_framing_strict():
    """Canary 2: close framing + reference_phrase_kinds=['background'] + attached background — PASS (strict, C2 fix)"""
    validate_attached_refs(
        rpc={"asset_requirements": {"required_refs": [{"kind": "background", "id": "L01", "policy": "required"}], "readiness_policy": "block_if_missing"}},
        labeled_refs=[("Reference image 1 — background L01", b"\x89PNG\r\n\x1a\n" + b"\x00"*100)],
        attached_meta=[("background", "L01")],
        prompt="the doorway from image 1 fills the close-up frame (canary 2)",
        is_close_framing=True,
        reference_phrase_kinds=["background"],
    )  # PASS — background declared + background attached (close framing 무관, C2 fix)


def test_canary_3_prop_kind_sidecar_with_meta():
    """Canary 3: reference_phrase_kinds=['prop'] + attached prop — PASS"""
    validate_attached_refs(
        rpc={"asset_requirements": {"required_refs": [{"kind": "prop", "id": "P01", "policy": "required"}], "readiness_policy": "block_if_missing"}},
        labeled_refs=[("Reference image 1 — prop P01 photograph", b"\x89PNG\r\n\x1a\n" + b"\x00"*100)],
        attached_meta=[("prop", "P01")],
        prompt="the photograph shown in image 1 sits on the desk (canary 3)",
        is_close_framing=False,
        reference_phrase_kinds=["prop"],
    )  # PASS


def test_canary_4_empty_array_no_attribution():
    """Canary 4: reference_phrase_kinds=[] (empty) — step 6 passes, no kind compare"""
    validate_attached_refs(
        rpc={"asset_requirements": {"required_refs": [], "readiness_policy": None}},
        labeled_refs=[],
        attached_meta=[],
        prompt="a generic environmental description without reference attribution (canary 4)",
        is_close_framing=False,
        reference_phrase_kinds=[],
    )  # PASS — empty array, no kind to verify


def test_canary_5_close_framing_background_declared_no_meta_fails():
    """Canary 5: close framing + background declared + no background meta — RefContractError (C2 strict, NOT skipped)"""
    with pytest.raises(RefContractError, match="reference_phrase_kinds declares kind='background'"):
        validate_attached_refs(
            rpc={"asset_requirements": {"required_refs": [{"kind": "character_outlook", "id": "C01O01", "policy": "required"}], "readiness_policy": "block_if_missing"}},
            labeled_refs=[("Reference image 1 — character C01: outlook O01", b"\x89PNG\r\n\x1a\n" + b"\x00"*100)],
            attached_meta=[("character_outlook", "C01O01")],
            prompt="close-up of subject's face (canary 5 — producer contract violation)",
            is_close_framing=True,
            reference_phrase_kinds=["background"],  # declared but no background meta
        )
```

- [ ] **Step 2: Run canary — verify all 5 PASS**

Run: `pytest backend/tests/integration/test_area_5_canary.py -v 2>&1 | tail -20`
Expected: 5 PASS (deterministic, no real LLM call, no VLM)

### Task 4.4: roadmap §5.5 / §11 / §14 update

**Files:**
- Modify: `docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md`

- [ ] **Step 1: §5.5 ✓ closed marker + 본문 wording update**

`§5.5 Area #5` heading 에 `✓ closed C-5 (Tier 2 #2, 2026-05-18+)` marker 추가. 본문의 Fix scope / Cost / Risk 부분 그대로 두되, "v1 closure complete" 명시.

- [ ] **Step 2: §11 closed area 추가**

`§11 Closed Area 재분류` 표에 다음 row 추가:

```
| Area #5 (Reference phrase / phantom guard) | open | ✓ closed C-5 (commit <W4_COMMIT_HASH>) | Tier 2 #2 v1 closure |
```

- [ ] **Step 3: §14 다음 진입점 update**

`§14 다음 진입점` 본문에서 "Tier 2 #2 Area #5" 제거, "Tier 2 #3 — Area #11 (prompt_service _classify_label) 또는 Tier 2 #4 — Area #6 (T2I review mutation redesign)" 등 명시 (사용자/Codex 의논 따라).

### Task 4.5: closure memo 작성

**Files:**
- Create: `~/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/session_20260518_area_5_reference_phrase_phantom_guard_sot_v1_closure.md`

- [ ] **Step 1: closure memo 작성 (Area #4 closure memo pattern donor 일관)**

내용:
- Status, push commit range, Codex iter loop 종합, 4 gate compliance matrix, closure criteria, non-claim, 함정 35 (carry 26 + Area #5 신규 10), 사용자 hard rule 준수, 관련 메모 link, 다음 area 진입점

- [ ] **Step 2: MEMORY.md index update**

`MEMORY.md` 첫 줄 위치에 다음 entry 추가:

```
- [session_20260518_area_5_reference_phrase_phantom_guard_sot_v1_closure.md](session_20260518_area_5_reference_phrase_phantom_guard_sot_v1_closure.md) - **2026-05-18+ Area #5 ... v1 closure 종합**
```

기존 `[next_session_area_4_scene_consistency_element_scope_brainstorm.md]` 의 status 를 `(해소됨, 결과 session_20260518_area_4_scene_consistency_element_scope_sot_v1_closure.md 참조)` 같은 패턴으로 update.

새 `next_session_area_X_brainstorm.md` 작성 (다음 area 진입점 — Tier 2 #3 후보).

### Task 4.6: Full regression test run

- [ ] **Step 1: 모든 area #5 in-scope test 전체 run**

Run: `pytest backend/tests/services/test_ref_contract_validator*.py backend/tests/services/test_build_scene_attached_refs.py backend/tests/services/test_single_scene_uses_helper.py backend/tests/services/test_scene_generation_coordinator.py backend/tests/services/test_prompt_service.py backend/tests/core/steps/test_render_prompt_card.py backend/tests/integration/test_d5_canary_s8.py backend/tests/integration/test_area_5_canary.py backend/tests/integration/test_area_5_residue_gate.py -v 2>&1 | tail -40`
Expected: 모든 area #5 in-scope test PASS, regression 0.

- [ ] **Step 2: 더 광범위 regression (선택 — 의심되는 cascade 확장)**

Run: `pytest backend/ -v 2>&1 | tail -20`
Expected: 사전 baseline (W0 시점 PASS count) 와 동일 또는 superset (unrelated regression 0).

### Task 4.7: W4 atomic commit + Codex range review

- [ ] **Step 1: W4 changes verify**

Run: `git status --short`
Expected:
- `backend/tests/integration/test_area_5_residue_gate.py` (Create)
- `backend/tests/integration/test_area_5_canary.py` (Create)
- `docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md` (Modify — §5.5/§11/§14)
- 외부 memory file (untracked, commit 외)

- [ ] **Step 2: W4 atomic commit**

```bash
git add backend/tests/integration/test_area_5_canary.py \
        backend/tests/integration/test_area_5_residue_gate.py \
        docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md
git commit -m "$(cat <<'EOF'
docs(area-5-reference-phrase-phantom-guard-replacement-v1): W4 — residue gate + canary 5 + roadmap §5.5/§11/§14 update + closure

Area #5 v1 closure:
- residue gate: production 4-항목 split (ref_contract_validator.py 13 identifier 0 / scene_generation_coordinator.py validate_attached_refs reference_phrase_kinds= kwarg 동반 / render_prompt_card.py closed-list literal 0 / prompt_service.py phantom literal 0). active prompt 3 surface (scene_image v4 / t2i_review v5 / scene_detail v26).
- canary 5 scenario deterministic (mock LLM, no real call, no VLM): character / background (close framing strict) / prop / empty array / background close-framing strict violation.
- roadmap §5.5 ✓ closed C-5 marker + §11 closed area 추가 + §14 다음 진입점 update.

closure memo: ~/.claude/projects/.../memory/session_20260518_area_5_reference_phrase_phantom_guard_sot_v1_closure.md (외부 memory).

4 Gate Compliance (모두 ✓):
- G1 Semantic Regex Ban: 13 legacy identifier atomic delete, step 6 enum literal compare 만
- G2 Prompt Closed-List Ban: A058 PHANTOM PHRASE PROHIBITION / A089 close_framing_existing_ref / P024 closed-list / P087 phantom literal 모두 폐기
- G3 Structured SOT Required: scene_detail v26 t2i_variations[].reference_phrase_kinds required field
- G4 No Silent Fallback: None/missing/invalid enum 모두 fail-fast, cp invalidation schema_version 10→11 강제 rerun

Non-claim (Area #11 / #7 / #6 / Area B-min identity contract change / global from-the-reference 0 — 모두 carry).

spec §6 closure verdict + §8 W4 + §9 residue + §13 trap 35.

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

- [ ] **Step 3: Codex range review (W1-W4 종합)**

tmux %0 에 W1-W4 commit hash range + 4 gate matrix + closure criteria 4 PASS + non-claim + 사용자 hard rule 준수 + push gate ask (Codex 결정). APPROVED_FOR_PUSH 받으면 push 결정 (사용자 ask 0, 둘이 결정).

### Task 4.8: push 결정 + 실행 (Codex 의논 후)

- [ ] **Step 1: Codex push 결정 의논**

Area #4 pattern donor — Codex range review APPROVED 후 push 자체 결정 (사용자 ask 0). push command:

```bash
git push origin main
```

전 후 verify:

```bash
git log --oneline -10  # W1-W4 commit 확인
git status --short  # uncommitted residue 0
```

- [ ] **Step 2: push 후 closure verify**

```bash
git log origin/main --oneline -5
```

Expected: W1-W4 atomic commit 모두 origin/main 에 반영.

- [ ] **Step 3: closure memo 의 push commit range 채우기**

`session_20260518_area_5_reference_phrase_phantom_guard_sot_v1_closure.md` 안 push commit range (`<W4_COMMIT_HASH>`) 실제 hash 채워서 update.

---

## Self-Review

### Spec coverage check

spec §1.2 in-scope:
- ✓ Validator step 6 본체 (`ref_contract_validator.py:47-190` + `:283-309` + `:425-457`) — W2 Task 2.2/2.3/2.4
- ✓ Single caller (`scene_generation_coordinator.py:1287-1291`) — W3 Task 3.2
- ✓ Variation caller (`:1393-1410`) — W3 Task 3.3
- ✓ `render_prompt_card` close framing forbidden (`:1253-1261, 1276`) + owned positive (`:1290-1306`) — W3 Task 3.6
- ✓ `prompt_service` translation guard (`:365-374` phantom 부분) — W3 Task 3.7
- ✓ `scene_detail` v26 producer schema + system.md — W1 Task 1.2 + 1.3
- ✓ `scene_image` translate_prompt v4 — W3 Task 3.4
- ✓ `t2i_review` scene_system + scene_schema v5 — W3 Task 3.5
- ✓ Version bump (SCHEMA_VERSION 10→11, PROMPT_VERSION) — W1 Task 1.4

spec §6 closure verdict 4 criteria:
- ✓ scene_detail v26 producer per-variation `reference_phrase_kinds` required — W1
- ✓ ref_contract_validator no longer classifies prompt prose — W2
- ✓ step 6 = sidecar exact compare, step 1-5 보존 — W2
- ✓ active Area #5 surfaces no longer teach closed phrase enumerations — W3
- ✓ cp invalidation 강제 v26 rerun — W1
- ✓ residue gates + canary fixtures PASS — W4

spec §9 residue gate (production 4 split + active prompt 3) — W4 Task 4.1 + 4.2

spec §11 Codex iter loop expectation — wave 진입 마다 Codex per-wave + W4 range review

### Placeholder scan

- `<W1_TIMESTAMP>` / `<W3_TIMESTAMP>` = wave 진입 시 실제 timestamp 채우는 명시적 placeholder (W1 진입 시 W0 의논 후 결정)
- `<W4_COMMIT_HASH>` = W4 commit 후 채우는 closure memo placeholder
- 다른 TBD/TODO/"implement later"/"add appropriate error handling" 없음 — 모두 step 별 정확한 코드/명령

### Type consistency

- `reference_phrase_kinds: List[str]` (validator signature) ↔ `array<enum>` (schema) — Python List[str] = JSON array<str>, runtime check enum membership
- `build_scene_attached_refs` 4-tuple = `(str, List[Tuple[str, bytes]], List[Tuple[str, str]], List[str])` — single path + variation path unpack 일관
- `RefContractError` signature 동일 (변경 0)
- `_meta_has_kind` helper 재사용 (W2 보존)

### Type/method consistency check

- `validate_attached_refs(..., reference_phrase_kinds=)` — keyword-only required, no default. 모든 caller (single + variation + test cascade) keyword 동반.
- `var["reference_phrase_kinds"]` (variation) vs helper return 4th element (single) — 같은 type, 같은 의미.

All checks PASS. Plan ready for Codex review.

---

## 함정 35 (Tier 1/2 carry 26 + Area #5 신규 10) — wave 진행 시 회피 의무

전체 함정 list = spec §13. wave 진입 전 의무 확인 항목 요약:

1. **Cascade enumeration**: W1 fixture cascade + W2 test cascade + W3 caller cascade — Pre-Step 1 cascade grep 결과 본 plan 안에 inline 명시
2. **Wording iteration**: spec §5 canonical principle mapping table = single source
3. **tmux trigger**: 각 wave 완료 시 양방향 trigger 의무
4. **사용자 ask 0**: rule 7a maximally strict — push 도 Codex+Claude 둘이 결정
5. **NO VLM**: canary 도 mock LLM, real LLM call X
6. **closure wording MAY/MUST NOT**: spec §6 Codex draft + §6.1 Non-claim 명시
7. **push gate strict**: range review APPROVED 받은 후만
8. **Roadmap §5.X update**: W4 Task 4.4
9-26. Area #1/#2/#3/#4 carry — closure memo reference 보존
27. t2i_review mutation staleness — issue category 4 폐기 (Area #5 v1 채택)
28. variation-level sidecar 의무 — shot-level union 금지
29. None silent fallback 차단 — keyword-only required + None fail-fast
30. `build_scene_attached_refs` 4-tuple cascade undercount — Pre-Step 1 grep 명시
31. closed-list 폐기 후 LLM alternative phrasing — sidecar ground-truth로 phrase-blind step 6
32. scene_detail v26 = v25 superset (subject_reference_policy / gaze fields / element_scope 보존)
33. prompt arg 보존 vs step 6 prose parse 안 함
34. is_close_framing axis 분리 (step 4 보존, step 6 strict)
35. Codex iter finding 흡수 시 cross-document grep 의무
36. roadmap §5.5/§11/§14 update 의무 (W4 Task 4.4)

---

**Status**: plan design closure. Codex plan iter 1 review 진입 의무 (사용자 ask 0, rule 7a maximally strict).
