# G4.4 Continuity Lift 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:** v18 system.md 의 3 continuity-related prose section (Section A `## 교차 샷 고정 요소 통합 규칙` 12L + Section B `## 앞쪽 참조 샷 처리 — ref_usage 유형별 지시` 35L + Section C `## 절대 규칙 — 단일 시점 (1인칭/3인칭 혼합 금지)` 19L = 66 lines) + `backend/app/core/steps/detail_steps.py:1555-1563` 안 9 줄 inline `if has_zoom_in_detail:` 블록 prose (Override O-3) = 합계 75 lines 을 `continuity_elements_used` 의 **3 new sibling policy dicts (`cross_shot_id_substitution_rule` / `ref_usage_constraints` / `view_consistency`)** + extended `constraints` list (G4.1 base 2 → G4.4 7 strings) 로 lift. v18 system.md 에서 3 prose 삭제 후 ≤ 32-line "Continuity" compact section 으로 교체. detail_steps.py 안 inline prose 9 줄 삭제. 5 canary scripts (4 metric + token). ~46 unit (table 46 enumerate, plan-R3+R4 audit 4+4 nested validator tests 추가) + ~17 integration test suite. 4-point version sync (prompt 디렉토리 + detail_steps.py:105 + version_registry.py:34 + version_registry.py:126). single commit + 듀얼 리뷰 3 iter fix loop.

**Architecture:**
- **render_prompt_card.py producer-side**: `build_continuity_elements_used()` (line 676) 3 new sibling policy dicts assemble + base 2 → 7 strings constraints 확장. 9 module-level constants (`_CONTINUITY_GENERIC_PERSON_NOUNS` / `_CONTINUITY_FURNITURE_LAYOUT_TOKENS` / `_CONTINUITY_FRAMING_SCOPE_OPTIONS` / `_CONTINUITY_REF_USAGE_VALUES` + 4 count constants + `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` Override O-7 paired-string single source) + cross-import of `_ID_BODY_PART_TRIGGERS` from G4.3. `_assert_continuity_elements_used_shape()` (line 1353-1385) 에 3 new sibling policy dicts strict 검증 추가 (Override O-5 strict required-keys subset). `compute_continuity_snapshot_hash(card)` helper 신설 (Override O-12). `build_render_prompt_card()` 가 `_card_metadata.lift_status` 에 4 신규 key (Override O-9 정확 명) + `rule_source` 3 신규 key 추가.
- **detail_steps.py consumer-side**: line 105 `SCENE_DETAIL_PROMPT_VERSION` v18 → `"19.<timestamp>"` bump. **line 1555-1563 inline `if has_zoom_in_detail:` 블록 9 줄 prose 삭제 (Override O-3)**. `_card_metadata` strip 로직 변경 0 (G4.2 14d14cb + G4.3 51d6c5e carry).
- **prompt-side**: `prompts/_base/scene_detail/19.<timestamp>/system.md` 신규 — v18 3 prose section 66 lines 삭제 + ≤ 32-line "Continuity" compact section. `detail_schema.json` / `user.md` verbatim copy.
- **canary scripts**: 5 종 (`g4_4_double_description.py` / `g4_4_zoom_in_detail_no_new_entity.py` / `g4_4_atmosphere_no_layout_import.py` / `g4_4_view_mixing.py` / `g4_4_token_count.py`). 모든 script 가 module-level constants import + ±50 chars window proximity (sentence regex 금지) + sentinel pre-pass + pinned-tuple post-loop validation + gate fail → exit 1. **Override O-11 multi-ref filter** (atmosphere check) + **Override O-17 regex brittleness** (LLM-validator 전환 P1 follow-up).
- **tests**: ~46 unit (table 46 enumerate, no buffer needed — plan-R4 4+4 nested validator tests 추가) + ~17 integration + alignment 4-point sync 갱신.

**Spec:** `docs/superpowers/specs/2026-05-05-g4.4-continuity-lift-design.md` (1396 lines, R1+R2 audit complete, 19 Override rows O-1~O-19). **commit gate** = G4.3 production canary 6 gate AND G4.4 self-canary 6 gate (R3-Entry-Note carry).

**Out of scope:**
- Background-binding lift (G4.2) — 이미 완료 (commit `14d14cb`). Rule A/C/E prose / `background_binding` shape / `_card_metadata` strip 로직 변경 0.
- ID-policy lift (G4.3) — 이미 완료 (commit `51d6c5e`). v18 의 `## ID Policy` compact section / `id_policy` sub-fields 변경 없음. line 313 의 cross-card reference (`id_policy.body_part_focus_rule`) 는 G4.4 view_consistency sub-field 와 1:1 보존만 (Override O-7 paired-string substring).
- Prompt slimming (G4.5) — heading count ≤ 10 enforcement 보류.
- **forward_zoom_targets prose lift 는 G4.4 scope 아님 (Override O-4)** — `detail_steps.py:1566-1620` forward zoom prose inject 는 별도 mechanism 으로 v19 LLM consumer 영향 0. lift_status 에 `continuity_forward_zoom_lifted` 키 추가 안 함.
- Producer side 변경 — `scene_consistency` / `shot_dependency_t2i` / `forward_zoom_targets` upstream step 변경 없음. **단 `build_continuity_elements_used()` 내부 로직 변경은 in-scope** (G4.2 R2-B2 carry).
- `_check_prompts()` / deterministic post-check 변경 없음.
- G3.2 owned sentinel 로직 변경 없음.
- `continuity_elements_used.fixed_elements[*]` 9 producer 필드 (G4.1 R1-I2 lossless adapter carry) shape 변경 없음 — sub-field 추가만.
- `continuity_elements_used.previous_shot_refs[*]` / `forward_zoom_targets[*]` shape 변경 없음.
- silent-fallback 잔존 carry (`backend/app/core/version_registry.py:148-155` — `MODULE_VERSIONS.get(..., "0.0.0")` 등) — Override O-8 G4.5 또는 별도 cleanup PR follow-up.
- canary DRY refactor (`scripts/canary/_canary_common.py` helper module) — G4.5 또는 P1 follow-up.

---

## Round Override (audit-driven, applied before Task body)

> **현황 (2026-05-05)**: spec R1+R2 audit 완료 (19 row O-1~O-19). plan R3 (Codex) + R4 (Claude) audit 미진행. Wave 5/6 fix loop 진행 시 plan R5 round 추가.

### Spec R1+R2 Override carry (본 plan body 에 inline 적용)

| ID | Spec source | plan 영향 |
|---|---|---|
| **O-1** | R1-B2 + R2-B1 | Task 4.5 token canary `g4_4_token_count.py` — **system-only `token_delta = candidate - baseline ≤ -540`** (1차 estimate-based binding gate). Wave 1-B tiktoken 실측이 더 강한 수치 (e.g. -480) 로 revise 가능 — Round 2 Override row 로만 변경, **upward revise 는 re-audit 필요**. system+inject 합산 (≤ -150) = secondary report-only metric (canary JSON 보조 기록 / no gate). exit_criteria_threshold field = `-540` (signed, system-only). Wave 1-B briefing / Verification Checklist / Wave 5 briefing 모두 통일. |
| **O-2** | R1-B1 + R2-I5 | Task 2.2 v19 `## Continuity` compact section line count gate — **`wc -l` ≤ 32 lines strict** (binding). 각 bullet 반드시 1 line — 80-col wrap 허용 안 됨 (wrap 줄도 line 1 로 카운트). Task 5.2 integration test §6.2 신규 row `test_v19_continuity_section_line_count_under_32` — `re.search(r"## Continuity.*?(?=\n## )", system_md, re.DOTALL)` content 추출 후 `len(content.split("\n")) <= 32` 강제 (R4-M4 carry from G4.3 — sed inclusive trap 회피). spec §3.3 본문 enumerate 14 lines — Wave 1-B draft `wc -l` 자체 점검 필수. |
| **O-3** | R1-B3 | Phase 3 Task 3.3 (Wave 2 — detail_steps.py:1555-1563 prose 삭제). `if has_zoom_in_detail:` 블록 9 줄 prose ("[중요 — 확대 샷 원칙 (zoom_in_detail)] / 새 인물·소품·자세 추가 금지 / 인체 부위 초점이면 보통명사 / 환경 동일") 전체 삭제. card 의 `ref_usage_constraints.zoom_in_detail` 5 required keys 가 동일 데이터 carry. integration test §6.2 `test_detail_steps_zoom_in_detail_inline_prose_absent_after_g4_4` 가 `"확대 샷 원칙"` substring 부재 + `"[중요 — 확대 샷 원칙 (zoom_in_detail)]"` substring 부재 강제. token reduction = system 외 별도 인접 계산 (~9 lines × 15 ≈ 135 tokens; Wave 1-B 실측 보정). |
| **O-4** | R2-B2 | Task 1.3 — `forward_zoom_targets` shape 변경 절대 금지. `cross_shot_id_substitution_rule` / `ref_usage_constraints` / `view_consistency` 3 new sibling policy dicts 만 추가. lift_status 에 `continuity_forward_zoom_lifted` 키 **추가 절대 금지**. detail_steps.py:1566-1620 forward zoom prose inject (별도 mechanism) 는 본 plan 변경 0. |
| **O-5** | R2-B3 | Task 1.4 — `_assert_continuity_elements_used_shape()` 신규 sub-field 검증 = **strict required-keys subset (set superset 검증)**: `EXPECTED_REQUIRED <= returned_keys` (set equality 가 아닌 superset). extra keys 허용 (G4.5 forward-compat). Task 5.1 모든 unit test 의 required_keys 검증 row 모두 `assert set(returned_keys) >= EXPECTED_SET` 으로 작성 (set equality `==` 사용 금지). _card_metadata 는 G4.3 R2-I5 carry 따라 free-form 유지. |
| **O-6** | R1-I1 | 본 plan body 전반 — "4 new top-level sub-fields" 표현 전부 **"3 new sibling policy dicts (`cross_shot_id_substitution_rule` / `ref_usage_constraints` / `view_consistency`) + extended `constraints` list (G4.1 base 2 → G4.4 7 strings)"** 로 정정. 기존 G4.1 sub-field (fixed_elements / previous_shot_refs / forward_zoom_targets) shape 변경 없음. Task 1.3 / 1.4 / 1.5 / 4.1-4.4 / 5.1 / 5.2 모든 row 정정. |
| **O-7** | R1-I2 + R2 | Task 1.3 — paired string substring 검증 (구조화 dict 미도입): `view_consistency.id_policy_cross_ref_for_body_part_focus` (string, literal substring `"id_policy.body_part_focus_rule"` 포함) + `ref_usage_constraints.zoom_in_detail.body_part_cross_ref` (string, 동일 substring 포함). Task 5.1 unit test +2 rows: `test_continuity_view_consistency_id_policy_cross_ref_string_present` + `test_continuity_zoom_in_detail_body_part_cross_ref_string_present` + `test_continuity_cross_card_pair_substrings_match` (paired drift 차단). Task 5.2 integration test +1 row `test_continuity_compact_section_references_id_policy_body_part_focus_rule`. Wave 1-A 코드에 maintainer 코멘트 ("rename 시 G4.4 spec patch 필요") 추가. `id_policy.py` builder source 코멘트 sync. |
| **O-8** | R1-I3 | silent-fallback 잔존 carry — `backend/app/core/version_registry.py:148-155` `MODULE_VERSIONS.get(..., "0.0.0")` / `_MODULE_INFO.get(..., {})` 패턴 = G4.1 inheritance, G4.4 scope 외. **G4.5 또는 별도 cleanup PR carry follow-up**. G4.4 신규 코드 (Wave 1-A producer + canary + tests) 는 새로운 silent-fallback 도입 절대 금지 (`(d or {})` / `(card or {})` / `(pair or {}).get(...)` 패턴 발견 시 BLOCKING). |
| **O-9** | R2-I3 verification | lift_status keyset 정확 명: G4.3 carry 4 lift_status keys = `id_policy_composite_lifted` / `id_policy_close_framing_face_lifted` / `id_policy_reproduction_surface_lifted` / `rule_h_lifted`. **G4.4 신규 4 lift_status 키**: `continuity_cross_shot_id_substitution_lifted` / `continuity_ref_usage_constraints_lifted` / `continuity_view_consistency_lifted` / `continuity_constraints_extended`. Task 1.5 / 5.1 test 44 / Wave 1-A briefing 모두 정확 명 사용 (abbrev 금지). |
| **O-10** | R2-I1 | Task 2.1 line range — Section A inclusive 행 = v18 lines 109-120 (12 lines: heading 109 / blank 110 / bullets 111-115 / blank 116 / ✓/✗ block 117-120). Section B = v18 lines 122-156 (35 lines: heading 122 / blank 123 / intro 124 / blank 125 / zoom_in_detail 126-141 / exact_background 143-149 / atmosphere_reference 151-156, blank gap 142/150). Section C = v18 lines 308-326 (19 lines). Blank lines 121 / 157 / 307 / 327 = 인접 섹션 보존. Wave 1-B v19 drafting 시 line-by-line diff 점검 필수. |
| **O-11** | R2-I2 | Task 4.3 `g4_4_atmosphere_no_layout_import.py` — shot-level filter = `any(ref.ref_usage == 'atmosphere_reference' for ref in previous_shot_refs)`. multi-ref 시 atmosphere check 만 적용. `exact_background` co-presence 시 false-positive baseline 비0 → P1 follow-up 추적. Task 5.1 unit test fixture +1 row `test_g4_4_view_mixing_canary_multi_ref_shot_filter`. |
| **O-12** | R2-I4 | Task 1.2 — `compute_continuity_snapshot_hash(card) = sha256(json.dumps(card['continuity_elements_used'], sort_keys=True, ensure_ascii=False))[:16]` 신설. `_card_metadata` 는 envelope sibling 이므로 자연 제외 — 추가 strip 불필요. G4.3 `compute_id_policy_snapshot_hash` 와 동일 패턴 (검증 완료). 5 canary script 모두 본 helper import. |
| **O-13** | R1-M1 | concrete wire 명시: `_collect_card_inputs(snapshot, scene_idx, shot_idx) → build_render_prompt_card(...) → _card_metadata strip → JSON prepend (line ~1500-1610 in detail_steps.py)`. `build_continuity_elements_used()` 위치 = `render_prompt_card.py:676`. consumer = scene_detail user_prompt JSON dict. 누락된 wire (Wave 2 삭제 대상) = detail_steps.py:1555-1563 prose 잔존 (O-3 carry). |
| **O-14** | R2-M2 | Task 5.2 line-count test 임계값 — "v18 - 50" → **"v18 baseline 587 lines - 32 (compact section gate Override O-2) - 보수 buffer 5 = 550 lines 미만"**. 도출식 명시 (임의 수치 대체). |
| **O-15** | R2-M1 | Task 2.2 ✓/✗ 예시 prose 삭제 footnote — G4.3 baseline 통과 패턴 carry (commit 51d6c5e). canary STRICT 0 검증이 LLM 행동 보증. v19 compact section 은 reasoning summary 만 carry. |
| **O-16** | R2-M2 | Task 5.1 unit test count — "총 unit tests: ~38" → **"총 unit tests: ~46"** (table 46 enumerate, plan-R3+R4 audit 4+4 nested shape validator tests 추가, no buffer needed). |
| **O-17** | R2-M3 | Task 4.3 + Task 4.4 + Wave 3 briefing — regex 1차 detector, baseline 비0 시 P1 follow-up 으로 LLM-validator (gpt-5.4-mini judge) 전환 검토. 의미 판단은 LLM-first (`feedback_no_regex_postprocessing.md`). brittle pattern: `g4_4_atmosphere_no_layout_import.py` (furniture token list) + `g4_4_view_mixing.py` (full-body + body-part close-up co-occurrence). false-positive (e.g. "C01O02 stands in profile, hand resting on the table" 자연스러운 부분 묘사) 발견 시 LLM-validator 전환. |
| **O-18** | R2-M4 | Wave numbering — Wave 1-A / 1-B / 2 / 3 / 4 / 5 / 6. **Wave 5 = production canary execution (사용자 실행). Wave 6 = single commit + push. test 파일 = Wave 4.** (G4.3 5 wave → G4.4 6 wave 로 production execution split). |
| **O-19** | R2-M5 | Glossary t2i_prompt 영어 가정 footnote — `feedback_t2i_storyboard.md` carry. Korean t2i prompt 도입 시 G4.5 에서 generic-noun 패턴 재검토. canary regex `_CONTINUITY_GENERIC_PERSON_NOUNS` 한국어 대응 P1 follow-up. |
| 메타룰 | (G4.2/G4.3 carry) | Round 1 Override 결정은 plan body 보다 우선. body 동기화는 Override 동기화 패치에서 inline 적용. body 와 Override 충돌 시 Override 우선. |

### Plan R3 (Codex) + R4 (Claude) Override stub — 미진행

> **상태 (2026-05-05)**: plan drafting 직후 Codex (R3) + Claude code-reviewer (R4) 듀얼 audit 진행 예정. 결정사항 row 추가 + body inline 동기화 (G4.2 + G4.3 패턴 답습). 본 stub 는 audit 결과 채울 자리.

| ID | Source | 결정 (Override) | 적용 위치 |
|---|---|---|---|
| (R3-B*) | (R3 audit pending) | (TBD) | (TBD) |
| (R4-B*) | (R4 audit pending) | (TBD) | (TBD) |

### 메타룰

- 본 표 가 plan body 보다 우선 적용.
- Body 동기화는 Override 동기화 패치에서 inline 적용 (G4.2 + G4.3 패턴 답습).
- Spec ↔ plan drift 발견 시 **즉시 plan R5 round** 진행.
- 절대 규칙 위반 (LLM 입력 truncation, 시나리오 의존 고유명사, silent fallback) 발견 시 BLOCKING 최우선.
- silent fallback 패턴 (`(d or {})` / `(card or {})` / `(pair or {}).get(...)`) 발견 시 BLOCKING. fail-fast (AppError) 강제 (`feedback_no_silent_fallback.md`).
- regex post-processing 의존 발견 시 IMPORTANT (`feedback_no_regex_postprocessing.md`). 의미 기반은 LLM validator 또는 prompt 로.

---

## Wave Decomposition Table

| Wave | inputs | outputs | blocking-on | approx LOC | approx test count |
|---|---|---|---|---|---|
| **Wave 1-A** | spec §2.2 / §4 / §7.2 step 1-5 / Override O-4 / O-5 / O-6 / O-7 / O-9 / O-12 | `render_prompt_card.py` modified (~320 LOC) — 9 module-level constants + cross-import `_ID_BODY_PART_TRIGGERS` + `compute_continuity_snapshot_hash()` + `build_continuity_elements_used()` 확장 (3 new sibling policy dicts + constraints 2→7) + `_assert_continuity_elements_used_shape()` 3 new sibling policy dicts strict 검증 (Override O-5 superset) + `build_render_prompt_card()` `_card_metadata` 4 신규 lift_status key (Override O-9) + 3 신규 rule_source key | 없음 (Wave 1-B 와 병렬) | ~320 | 0 (test 는 Wave 4) |
| **Wave 1-B** | spec §3.1 / §3.2 / §3.3 / §3.4 / Override O-2 / O-10 / O-15 | `prompts/_base/scene_detail/19.<timestamp>/system.md` (신규) + `detail_schema.json` (verbatim copy) + `user.md` (verbatim copy) — net -52 lines (66 system 삭제 + 14 추가) + Wave 2 의 detail_steps.py inline 9 lines 별도 saving | 없음 (Wave 1-A 와 병렬) | -52 net (system) -9 (detail_steps inline) | 0 |
| **Wave 2** | Wave 1-A + Wave 1-B 완료 (timestamp 결정) / Override O-3 | `detail_steps.py:105` `SCENE_DETAIL_PROMPT_VERSION` v18→`"19.<timestamp>"` bump + `detail_steps.py:1555-1563` inline 9 줄 prose 삭제 (Override O-3) + `version_registry.py:34` `MODULE_VERSIONS["scene_detail_composer"]` `1.18.0`→`1.19.0` + `version_registry.py:126` `_MODULE_INFO[...]["prompt_dependency"]` `"scene_detail/v18"`→`"scene_detail/v19"` (dict access — G4.3 R3R4-B1 carry). alignment test fixture 갱신 | Wave 1-B (timestamp pin) | ~16 (detail_steps inline 9 + bump 7) | 0 (alignment test 는 Wave 4) |
| **Wave 3** | spec §5.1 / §5.3 / §5.4 / Override O-1 / O-11 / O-12 / O-17 | 5 canary script (`scripts/canary/g4_4_*.py`) — 모든 script 가 module constants import + `compute_continuity_snapshot_hash` import + sentinel pre-pass + pinned-tuple post-loop + ±50 chars window | Wave 1-A (constants + hash helper import) | ~700 (5 script × ~140) | 0 (canary 자체 unit test 는 Wave 4 smoke) |
| **Wave 4** | spec §6.1 / §6.2 / §6.4 / Override O-2 / O-3 / O-5 / O-7 / O-9 / O-11 / O-14 | `backend/tests/unit/test_g4_4_continuity_lift.py` (~46 unit, table 46 enumerate) + `backend/tests/integration/test_g4_4_continuity_integration.py` (~17 integration) + alignment test 갱신 | Wave 1-A + 1-B + 2 (alignment test 의존) | ~900 | +63 (~46 unit + ~17 integration) |
| **Wave 5** | Wave 1-A~4 완료 + G4.3 production canary 6 gate green (R3-Entry-Note carry) | baseline canary capture (v18) + candidate canary capture (v19) + 4 metric + token + line count = 6 gate 비교 + 듀얼 리뷰 3 iter fix loop | G4.3 production canary (commit gate) | n/a (실행 단계) | 0 (수정만) |
| **Wave 6** | Wave 5 통과 | single commit (4 영역 + canary 5 + tests 3) + push + Codex/Claude 듀얼 리뷰 fix → 재push | Wave 5 (canary green) | n/a | 0 (수정만) |

총 expected delta: **+~320 producer LOC + -52 prompt LOC (system.md net) + -9 LOC (detail_steps inline) + ~700 canary LOC + ~900 test LOC + ~49~55 신규 test**.

---

## Phase 1 (Wave 1-A): render_prompt_card.py 변경

**Phase 목표:** 9 module-level constants + cross-import + `compute_continuity_snapshot_hash()` helper 신설. `build_continuity_elements_used()` 에 3 new sibling policy dicts assemble 로직 + constraints 2 → 7 strings 확장. `_assert_continuity_elements_used_shape()` 3 new sibling policy dicts strict 검증 (Override O-5 superset). `build_render_prompt_card()` `_card_metadata.lift_status` 4 신규 key + `rule_source` 3 신규 key 추가.

**Phase 1 expected test pass delta:** 0 (test 는 Wave 4 에서 추가).

**Wave subagent 함정 경고 (G4.1 + G4.2 + G4.3 carry):**
- **절대 금지**: `fixed_elements=[]` / `previous_shot_refs=[]` / `forward_zoom_targets=[]` 류의 hardcoded empty inputs 도입. fixture default 가 production failure 를 가릴 위험. 본 Wave 구현자 는 helper 함수 / fixture 의 default 인자 추가 시 반드시 `_DEFAULT_SENTINEL = object()` pattern 사용 (None / [] / default 셋 명시 구분). G4.1 R3-B2 / G4.2 plan-R1-I4 / G4.3 plan-R1-I4 / R4-M6 carry.
- **절대 금지**: `or [] / or {}` silent absorb pattern. None 흡수 → builder fail-fast 무력화 → production drift cascade. G4.2 iter1 C3 / G4.3 carry / Override O-8.
- **절대 금지**: 신규 3 new sibling policy dicts 를 `if previous_shot_refs:` / `if fixed_elements:` 류 truthy guard 안 배치. **builder-static** — 입력 무관 항상 동일 dict (R2-I4 carry from G4.3 / Override O-9 invariant). Test `test_continuity_4_subfields_independent_of_input_lists` 가 강제 검증.
- **절대 금지**: lift_status 에 abbrev 키 (`continuity_cross_shot_id_lifted` / `continuity_ref_usage_lifted`) 사용. **정확 명** = `continuity_cross_shot_id_substitution_lifted` / `continuity_ref_usage_constraints_lifted` / `continuity_view_consistency_lifted` / `continuity_constraints_extended` (Override O-9). drift 시 G4.5 lift status enumerate test fail.
- **반드시**: `import hashlib` + `import json` 존재 확인 self-check (G4.1 carry — 미존재 시 add). G4.3 R4-M1 carry.

---

### Task 1.1 — module-level constants 도입 + cross-import

- [ ] **File:** `backend/app/core/steps/render_prompt_card.py`
- [ ] **Insert location:** module top — G4.3 7 constants (`_ID_BODY_PART_TRIGGERS` 등 line 56-209 추정) 직후 일괄 추가
- [ ] **Dependency:** 없음 (G4.3 base 위에서 직접 수정). G4.3 의 `_ID_BODY_PART_TRIGGERS` 는 **cross-import** (rename 시 G4.4 spec patch 필요 maintainer 코멘트 추가 — Override O-7)
- [ ] **Spec cross-reference:** spec §2.2 / §7.2 step 5 / §9 Glossary / Override O-6 / O-7
- [ ] **Expected diff size:** ~70 lines added

**변경 내용:**

G4.3 constants 직후 다음 9 constant 일괄 도입:

```python
# G4.4 ground-truth: generic person nouns for cross-shot double-description detection (R1+R2 audit
# 시 ground-truth list 확정). canary `g4_4_double_description.py` ±50 chars window
# proximity source. id_policy 등록된 character 의 보통명사 동시 등장 검출.
# regional_consistency carry from G4.3 — 한 작품 region 일관 가정 위 generic Asian-default
# placeholders (`feedback_t2i_storyboard.md` carry — 영어 t2i_prompt 가정).
# Override O-19 follow-up: Korean t2i prompt 도입 시 G4.5 에서 generic-noun 패턴 재검토.
_CONTINUITY_GENERIC_PERSON_NOUNS: Tuple[str, ...] = (
    "an Asian man",
    "a young woman",
    "a figure",
    "a man",
    "a woman",
    "an elderly figure",
    "a child",
)

# G4.4 ground-truth: furniture / wall / layout import tokens for atmosphere_reference
# violation detection. canary `g4_4_atmosphere_no_layout_import.py` 검출 source.
# Override O-17 — regex 1차 detector, baseline 비0 시 P1 follow-up 으로 LLM-validator
# (gpt-5.4-mini judge) 전환 검토 (`feedback_no_regex_postprocessing.md`).
_CONTINUITY_FURNITURE_LAYOUT_TOKENS: Tuple[str, ...] = (
    "furniture",
    "wall position",
    "room layout",
    "same chair",
    "same table",
    "same wall",
    "same window",
)

# G4.4 ground-truth: framing scope options (view_consistency.framing_scope_options).
# 정확 2 entries — `_assert_continuity_elements_used_shape()` 가 list equality 강제.
_CONTINUITY_FRAMING_SCOPE_OPTIONS: Tuple[str, ...] = (
    "third_person",
    "close_up",
)

# G4.4 ground-truth: ref_usage values (previous_shot_refs[i].ref_usage).
# 정확 3 entries — `ref_usage_constraints` 의 3 sub-key 와 1:1.
_CONTINUITY_REF_USAGE_VALUES: Tuple[str, ...] = (
    "zoom_in_detail",
    "exact_background",
    "atmosphere_reference",
)

# G4.4 count constants — sub-field list-len 검증용 single source.
# spec §2.2 ground-truth list/literal counts row carry.
_CONTINUITY_ZOOM_FORBIDDEN_ADDITIONS_COUNT: int = 5
_CONTINUITY_ZOOM_REQUIRED_PHRASINGS_COUNT: int = 3
_CONTINUITY_EXACT_PERMITTED_ADDITIONS_COUNT: int = 2
_CONTINUITY_ATMOSPHERE_FORBIDDEN_IMPORTS_COUNT: int = 3

# G4.4 Override O-7 paired-string substring single source (cross-card reference literal).
# Wave 4 briefing line 2818 + 5 canary scripts + unit test 17 / 18 / 19 + integration
# test 17 모두 본 constant 를 import. G4.3 `id_policy.py` builder sub-field rename 시
# 본 literal 도 동시 patch 필요 (paired drift 차단 — Override O-7).
_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL: str = "id_policy.body_part_focus_rule"
```

**Cross-import maintainer 코멘트 (Override O-7 carry):**

`build_continuity_elements_used()` body 안 cross-card reference string 작성 위치 직전에 다음 코멘트:

```python
# G4.4 Override O-7 carry: paired-string substring `"id_policy.body_part_focus_rule"` 는
# G4.3 `id_policy.body_part_focus_rule` sub-field 명을 literal 로 참조한다.
# G4.3 `id_policy.py` builder 안 sub-field rename 시 본 spec / plan / canary / unit test
# 모두 동시 patch 필요 (paired drift 차단 — `test_continuity_cross_card_pair_substrings_match`).
```

**주의:**
- 9 constant 중 4 tuple constants (GENERIC_PERSON_NOUNS / FURNITURE_LAYOUT_TOKENS / FRAMING_SCOPE_OPTIONS / REF_USAGE_VALUES) 는 **tuple of str** (immutable) — list 사용 금지. 4 count constants 는 `int`. 1 cross-ref literal 은 `str` (Override O-7 paired-string single source).
- 모든 constant 가 module-level (Class 안에 넣지 말 것) — `from app.core.steps.render_prompt_card import _CONTINUITY_GENERIC_PERSON_NOUNS` 형태로 canary script 와 unit test 가 직접 import.
- 시나리오 의존 0: 9 constant 안에 작품 고유명사 (인물 이름, 지명) 절대 포함 금지.
- 본 step 의 정확한 entry 개수는 R2-B4 carry 로 unit test 가 `len() == N` + `set(...) == EXACT_GROUND_TRUTH_SET` 강제 검증.
- G4.3 의 `_ID_BODY_PART_TRIGGERS` (4 trigger) 는 cross-import — `g4_4_view_mixing.py` 에서 body-part close-up trigger 검출에 재사용. import 부분에 maintainer 코멘트 추가.

**Wave subagent fixture trap 회피 (G4.1 carry)**: helper 추가 시 `bg_owned=_DEFAULT_SENTINEL` 류 pattern 사용. constant tuple 자체는 hardcoded literal 이므로 trap 무관 — 하지만 본 constant 를 사용하는 helper 가 default arg 가지면 sentinel pattern 적용.

---

### Task 1.2 — `compute_continuity_snapshot_hash()` helper 신설

- [ ] **File:** `backend/app/core/steps/render_prompt_card.py`
- [ ] **Insert location:** `compute_id_policy_snapshot_hash()` (G4.3 carry) 직후 — hash helper 영역 logical grouping
- [ ] **Dependency:** Task 1.1 (constants 가 본 helper 하부 호출 대상이 아니지만, 동일 모듈에서 logical grouping)
- [ ] **Spec cross-reference:** spec §5.2 / §5.3 / Override O-12
- [ ] **Expected diff size:** ~30 lines added

**변경 내용:**

```python
def compute_continuity_snapshot_hash(card: Dict[str, Any]) -> str:
    """G4.4 Override O-12 — continuity_elements_used partial canonicalize hash.

    canary pinning block 의 `continuity_card_snapshot_hash` 산출용.
    baseline / candidate continuity_elements_used shape 동일 검증. 변동 source 차단.

    산출법:
      sha256(json.dumps(card["continuity_elements_used"], sort_keys=True, ensure_ascii=False))[:16]

    Override O-12 결정 근거: G4.2 의 `compute_card_hash()` 가 envelope 전체 hash —
    G4.4 canary 는 continuity_elements_used 변경에만 sensitive 해야 하므로 partial hash 별도.
    `_card_metadata` 는 envelope sibling 이므로 자연 제외 — 추가 strip 불필요. G4.3
    `compute_id_policy_snapshot_hash` 와 동일 패턴 carry (검증 완료).

    fail-fast: card 가 dict 가 아니거나 `continuity_elements_used` key 부재 시 AppError raise
    (silent fallback 금지 — `feedback_no_silent_fallback.md` carry / Override O-8).
    """
    if not isinstance(card, dict):
        raise AppError(
            code="step.contract_violation",
            message=(
                f"compute_continuity_snapshot_hash: card must be dict "
                f"(got {type(card).__name__})"
            ),
        )
    if "continuity_elements_used" not in card:
        raise AppError(
            code="step.contract_violation",
            message="compute_continuity_snapshot_hash: card missing 'continuity_elements_used'",
        )
    payload = json.dumps(
        card["continuity_elements_used"],
        sort_keys=True,
        ensure_ascii=False,
    )
    return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
```

**주의:**
- `json` / `hashlib` 모두 module top 의 import 영역에 이미 존재 (G4.1 carry / G4.3 carry) — 신규 import 불필요. **R4-M1 carry from G4.3**: Wave 1-A 진입 시 self-check (미존재 시 add — 그러나 G4.3 이 추가했으므로 정상 존재해야 함).
- 본 helper 는 5 canary script 모두 import 사용. canary 추가 시 동일 helper 재사용.
- silent fallback 금지: `card.get("continuity_elements_used", {})` 패턴 절대 금지 — None / 빈 dict 흡수 차단.
- G4.3 의 `compute_id_policy_snapshot_hash` 와 sibling — 동일 모듈에서 두 helper 가 logical grouping (continuity_card_snapshot_hash + id_policy_card_snapshot_hash 둘 다 canary pinning 7 field 안 등록).

---

### Task 1.3 — `build_continuity_elements_used()` 확장: 3 new sibling policy dicts assemble + constraints 2 → 7 strings

- [ ] **File:** `backend/app/core/steps/render_prompt_card.py:676-744`
- [ ] **Dependency:** Task 1.1 (6 constants 사용)
- [ ] **Spec cross-reference:** spec §2.2 / §3.2 / §4 / §7.2 step 1-2 / Override O-4 / O-6 / O-7 / O-15
- [ ] **Expected diff size:** ~210 lines (3 new sibling policy dicts ~150 + constraints 확장 ~60)

**변경 내용:**

`build_continuity_elements_used()` 의 현재 return 직전에 다음 dict 3개 assemble 추가. constraints list 도 base 2 → 7 strings 으로 확장.

```python
def build_continuity_elements_used(
    *,
    fixed_elements: List[Dict[str, Any]],
    previous_shot_refs: List[Dict[str, Any]],
    forward_zoom_targets: List[Dict[str, Any]],
) -> Dict[str, Any]:
    """spec §2.2 — continuity_elements_used field.

    G4.4 (2026-05-05) — Override O-6 명명 정정 (3 new sibling policy dicts):
      - cross_shot_id_substitution_rule (6 required keys) 신규 sibling policy dict —
        fixed_element 의 character_name → C##/C##O## 매핑 시 보통명사 인물 묘사 치환
        + 이중 묘사 금지 + 자세 보존 + 반복 금지.
      - ref_usage_constraints (3 sub-key: zoom_in_detail / exact_background /
        atmosphere_reference, 각 5/5/4 required keys) 신규 sibling policy dict —
        ref_usage 별 forbidden/required wording.
      - view_consistency (9 required keys) 신규 sibling policy dict — single-camera
        rule + Focus-on 의미 + prop orientation + body-part Focus cross-ref to
        id_policy.body_part_focus_rule.
      - constraints G4.1 base 2 → G4.4 7 strings.

    Override O-4 carry: forward_zoom_targets shape 변경 절대 금지 (G4.4 lift 대상 아님).
      detail_steps.py:1566-1620 forward zoom prose inject 별도 mechanism, v19 LLM
      consumer 영향 0.

    G4.1 R2-B4 carry: 입력 list 가 None 이면 producer missing 으로 간주 → AppError.
      명시적 [] 만 valid (silent absorb 차단 — Override O-8).
    """
    if fixed_elements is None:
        raise AppError(
            code="step.contract_violation",
            message="build_continuity_elements_used: fixed_elements must not be None",
        )
    if previous_shot_refs is None:
        raise AppError(
            code="step.contract_violation",
            message="build_continuity_elements_used: previous_shot_refs must not be None",
        )
    if forward_zoom_targets is None:
        raise AppError(
            code="step.contract_violation",
            message="build_continuity_elements_used: forward_zoom_targets must not be None",
        )

    # G4.4 신규 sibling policy dict 1: cross_shot_id_substitution_rule (Override O-6)
    cross_shot_id_substitution_rule = {
        "applies_when": (
            "fixed_elements[i].character_name maps to a C## allowed in "
            "id_policy.allowed_base_entity_ids OR a C##O## in "
            "id_policy.allowed_outlook_pairs"
        ),
        "substitution": (
            "replace the common-noun person reference inside "
            "fixed_elements[i].description (e.g. 'An Asian man' / 'a woman' / "
            "'a figure') with the matched C## or C##O##"
        ),
        "double_description_forbidden": (
            "do not describe the same character with both a C##/C##O## and a "
            "separate common-noun person reference in the same t2i_prompt — "
            "produces phantom extra figures"
        ),
        "preserve_pose_unchanged": (
            "fixed_elements[i].description 의 자세/위치/상태 keyword 는 그대로 유지 — "
            "인물 참조 토큰만 교체"
        ),
        "no_repeat_after_pose": (
            "이미 t2i_prompt 안에서 같은 인물의 자세를 묘사했다면, 같은 fixed_element "
            "description 을 별도 문장으로 반복하지 마라"
        ),
        "rationale_summary": (
            "fixed_element description 이 보통명사로 작성되었고 같은 인물의 C## 묘사가 "
            "t2i_prompt 에 따로 있으면 모델이 두 인물로 인식해 인원 수가 부풀려진다"
        ),
    }

    # G4.4 신규 sibling policy dict 2: ref_usage_constraints (Override O-6)
    # — 3 sub-key: zoom_in_detail / exact_background / atmosphere_reference
    ref_usage_constraints = {
        "zoom_in_detail": {
            "scope_summary": (
                "same moment, same space, same primary subject; camera "
                "moved/zoomed onto a sub-region only"
            ),
            "forbidden_additions": [
                "new persons not in the prior shot",
                "new props not in the prior shot",
                "new background elements not in the prior shot",
                "new poses, gestures, or expressions not in the prior shot",
                "different moments of action",
            ],  # exactly 5 entries — _CONTINUITY_ZOOM_FORBIDDEN_ADDITIONS_COUNT
            "required_phrasings": [
                "describe only the focused sub-region in concrete terms",
                "explicitly state the same environment (lighting, background) is preserved",
                "use continuity tokens such as 'the same figure', 'the same room', "
                "'the same surface'",
            ],  # exactly 3 entries — _CONTINUITY_ZOOM_REQUIRED_PHRASINGS_COUNT
            # Override O-7 paired-string substring — G4.3 cross-card paired
            "body_part_cross_ref": (
                "id_policy.body_part_focus_rule applies — when the focused "
                "sub-region is a body part, use a common noun + demographic "
                "descriptor (no C##/C##O##)"
            ),
            "rationale_summary": (
                "zoom_in_detail = 앞 샷의 특정 영역을 확대한 '같은 사진'. 새 정보 추가 "
                "금지 — 현재 프레임이 보여주는 부분만 정확히 묘사"
            ),
        },
        "exact_background": {
            "scope_summary": (
                "same room, possibly different moment or angle; background "
                "carries through, persons/actions are described fresh for this shot"
            ),
            "permitted_additions": [
                "new persons, poses, expressions, actions for this shot's moment",
                "different camera angle within the same room",
            ],  # exactly 2 entries — _CONTINUITY_EXACT_PERMITTED_ADDITIONS_COUNT
            "background_consistency_rule": (
                "background elements established in the prior shot must remain "
                "consistent (no contradictory walls, doors, furniture)"
            ),
            "ignore_keep_handled_elsewhere": (
                "ignore/keep directives are auto-applied via reference image "
                "processing — do not echo them inside t2i_prompt"
            ),
            "rationale_summary": (
                "같은 공간, 새 순간 — 인물·동작은 이 샷의 instantaneous moment 로 "
                "묘사하되 배경은 prior shot 과 일관"
            ),
        },
        "atmosphere_reference": {
            "scope_summary": (
                "different room/floor/angle entirely — only mood/tone is shared "
                "with the prior shot"
            ),
            "forbidden_imports": [
                "furniture layout from the prior shot",
                "wall positions from the prior shot",
                "specific architectural features from the prior shot",
            ],  # exactly 3 entries — _CONTINUITY_ATMOSPHERE_FORBIDDEN_IMPORTS_COUNT
            "required_handling": (
                "describe the new space's background directly in this shot; carry "
                "through only color tone or lighting mood (e.g. 'cool gray ambient' / "
                "'dim warm pool')"
            ),
            "rationale_summary": (
                "atmosphere_reference = 분위기 reference 만, 레이아웃 reference 아님. "
                "가구·벽 배치를 끌어오면 모델이 두 공간을 한 공간으로 합성"
            ),
        },
    }

    # G4.4 신규 sibling policy dict 3: view_consistency (Override O-6)
    view_consistency = {
        "framing_scope_options": list(_CONTINUITY_FRAMING_SCOPE_OPTIONS),  # 2 entries
        "single_camera_rule": (
            "one t2i_prompt = one camera position. do not combine a third-person "
            "full-body description and a close-up of the same character's body part."
        ),
        "third_person_handling": (
            "describe persons with full or partial body in frame; their hands/arms "
            "remain attached to their bodies (no isolated hand close-up alongside "
            "the full body)"
        ),
        "close_up_handling": (
            "isolated body part (hand, wrist, fingers, brow) — do not include the "
            "same character's full body in the same prompt"
        ),
        "focus_on_is_focus_area_not_view_switch": (
            "the phrasing 'Focus on ...' specifies the area of focus while the "
            "camera position remains the same; it is NOT a view switch"
        ),
        # Override O-7 paired-string substring — G4.3 cross-card paired
        "id_policy_cross_ref_for_body_part_focus": (
            "id_policy.body_part_focus_rule applies — body-part Focus 시 C##O## "
            "금지 (얼굴 ref 합성 차단)"
        ),
        "prop_orientation_rule": (
            "props the character holds must face one direction only — toward "
            "camera OR toward character, not both. forbidden contradiction "
            "example: 'photo angled partly toward the camera' + 'eyes fixed on "
            "the photograph' (양방향 모순)"
        ),
        "mixing_forbidden": True,  # bool, NOT bool int subclass — G3.2/G4.3 trap carry
        "rationale_summary": (
            "single t2i_prompt = single camera shutter at 1/1000s. 시점 혼합은 "
            "모델이 두 카메라 합성으로 해석해 인물이 두 번 등장 또는 부위 분리"
        ),
    }

    # G4.4 constraints: G4.1 base 2 → G4.4 7 strings
    constraints = [
        # constraints[0] — G4.1 carry: fixed_elements binding (verbatim string from G4.1)
        (
            "fixed_elements are continuity inputs derived from prior shots — "
            "treat them as binding context, not as source facts to be invented"
        ),
        # constraints[1] — cross_shot_id_substitution_rule inline
        (
            "when fixed_elements[i].character_name maps to a C## in "
            "id_policy.allowed_base_entity_ids or a C##O## in "
            "id_policy.allowed_outlook_pairs, replace the common-noun person "
            "reference inside fixed_elements[i].description with that C##/C##O## "
            "and do not also write a separate common-noun reference for the same "
            "character in the same t2i_prompt"
        ),
        # constraints[2] — zoom_in_detail inline
        (
            "previous_shot_refs with ref_usage='zoom_in_detail' must preserve the "
            "same moment, the same space, and the same lighting as the prior "
            "shot — no new persons, props, background elements, poses, or moments"
        ),
        # constraints[3] — exact_background inline
        (
            "previous_shot_refs with ref_usage='exact_background' may freely "
            "describe this shot's persons, poses, and moment, but must keep "
            "background elements consistent with the prior shot's established room"
        ),
        # constraints[4] — atmosphere_reference inline
        (
            "previous_shot_refs with ref_usage='atmosphere_reference' must "
            "describe a new space directly — do not import furniture layout or "
            "wall positions from the prior shot, only mood/tone"
        ),
        # constraints[5] — single_camera + body-part cross-ref to id_policy (Override O-7)
        (
            "one t2i_prompt = one camera position — do not combine a third-person "
            "full-body description and a close-up of the same character's body "
            "part; 'Focus on' specifies the area of focus, not a view switch "
            "(id_policy.body_part_focus_rule applies for any body-part Focus)"
        ),
        # constraints[6] — prop orientation
        (
            "props held by characters face one direction only — toward camera OR "
            "toward character, never both"
        ),
    ]

    return {
        "fixed_elements": list(fixed_elements),  # G4.1 carry — lossless adapter shape
        "previous_shot_refs": list(previous_shot_refs),  # G4.1 carry
        "forward_zoom_targets": list(forward_zoom_targets),  # G4.1 carry — Override O-4

        # G4.4 신규 3 new sibling policy dicts (Override O-6)
        "cross_shot_id_substitution_rule": cross_shot_id_substitution_rule,
        "ref_usage_constraints": ref_usage_constraints,
        "view_consistency": view_consistency,

        # G4.4 확장 constraints (G4.1 base 2 → G4.4 7 strings)
        "constraints": constraints,
    }
```

**주의 (Wave 1-A 함정 회피 — G4.1 / G4.2 / G4.3 trap carry):**
- `list(...)` wrapping 필수 — caller mutate 방지 + JSON serializable. 단 신규 3 dict 안 list (forbidden_additions 등) 는 직접 list literal — module constant 의 변환은 view_consistency.framing_scope_options 만.
- 신규 3 sibling policy dict 모두 **builder-static** (입력 무관 — fixed_elements=[] / previous_shot_refs=[] / forward_zoom_targets=[] 상태에서도 항상 동일 dict). `if previous_shot_refs:` 류 truthy guard 안 배치 절대 금지 (Override O-9 invariant). test `test_continuity_4_subfields_independent_of_input_lists` 가 강제.
- `view_consistency.mixing_forbidden = True` — bool literal `True` (NOT `1`, NOT `True if ... else False` 회피 패턴). bool int subclass trap carry from G4.3 / G3.2 — `assert mixing_forbidden is True and type(mixing_forbidden) is bool` 검증.
- 신규 3 dict 안 한국어 prose (rationale_summary 등) 는 시나리오 의존 0 — 작품 고유명사 (인물 이름, 지명) 절대 포함 금지.
- Override O-7 paired-string substring `"id_policy.body_part_focus_rule"` 는 정확 literal — G4.3 sub-field 명 rename 시 본 plan / spec / canary / unit test 모두 동시 patch (paired drift 차단).

---

### Task 1.4 — `_assert_continuity_elements_used_shape()` 3 new sibling policy dicts strict 검증 추가

- [ ] **File:** `backend/app/core/steps/render_prompt_card.py:1353-1385`
- [ ] **Dependency:** Task 1.3 (3 new sibling policy dicts 가 builder 출력에 존재)
- [ ] **Spec cross-reference:** spec §2.2 (assert_card_shape strict) / Override O-5 / O-6
- [ ] **Expected diff size:** ~120 lines (3 new sibling policy dicts 검증 추가)

**변경 내용:**

기존 `_assert_continuity_elements_used_shape()` (line 1353-1385) 가 `fixed_elements` / `previous_shot_refs` / `forward_zoom_targets` (G4.1 base 3 field) 만 검증 — G4.4 3 new sibling policy dicts 검증 추가 (Override O-5 strict required-keys subset).

각 sub-field 의 required_keys (Override O-5 superset):

```python
def _assert_continuity_elements_used_shape(
    ce: Dict[str, Any], where: str
) -> None:
    """G4.1 carry: fixed_elements (list[dict] with 9 producer 필드) +
    previous_shot_refs (list[dict] with scene_index/shot_index/ref_usage) +
    forward_zoom_targets (list[dict] with scene_index/shot_index/description).

    G4.4 (Override O-5 + O-6 carry): 3 new sibling policy dicts strict 검증 추가.
    각 sub-field 의 required_keys 는 본 함수에서 single source.

    Override O-5 — strict required-keys subset (set superset):
      `EXPECTED_REQUIRED <= returned_keys` (set equality 가 아닌 superset).
      extra keys 허용 (G4.5 forward-compat).
    """
    # G4.1 carry — fixed_elements / previous_shot_refs / forward_zoom_targets 검증
    if "fixed_elements" not in ce:
        raise AppError(...)  # 기존 코드 그대로
    _assert_dict_list_with_keys(...)
    if "previous_shot_refs" not in ce:
        raise AppError(...)
    _assert_dict_list_with_keys(...)
    if "forward_zoom_targets" not in ce:
        raise AppError(...)
    _assert_dict_list_with_keys(...)

    # G4.4 신규 3 new sibling policy dicts 검증 (Override O-5 superset)
    _CONTINUITY_SUB_FIELD_REQUIRED_KEYS = {
        "cross_shot_id_substitution_rule": frozenset({
            "applies_when",
            "substitution",
            "double_description_forbidden",
            "preserve_pose_unchanged",
            "no_repeat_after_pose",
            "rationale_summary",
        }),
        "view_consistency": frozenset({
            "framing_scope_options",
            "single_camera_rule",
            "third_person_handling",
            "close_up_handling",
            "focus_on_is_focus_area_not_view_switch",
            "id_policy_cross_ref_for_body_part_focus",
            "prop_orientation_rule",
            "mixing_forbidden",
            "rationale_summary",
        }),
    }
    _CONTINUITY_REF_USAGE_SUB_KEYS = frozenset({
        "zoom_in_detail",
        "exact_background",
        "atmosphere_reference",
    })
    _CONTINUITY_REF_USAGE_NESTED_REQUIRED_KEYS = {
        "zoom_in_detail": frozenset({
            "scope_summary",
            "forbidden_additions",
            "required_phrasings",
            "body_part_cross_ref",
            "rationale_summary",
        }),
        "exact_background": frozenset({
            "scope_summary",
            "permitted_additions",
            "background_consistency_rule",
            "ignore_keep_handled_elsewhere",
            "rationale_summary",
        }),
        "atmosphere_reference": frozenset({
            "scope_summary",
            "forbidden_imports",
            "required_handling",
            "rationale_summary",
        }),
    }

    # 1. cross_shot_id_substitution_rule + view_consistency: top-level 2 dict
    for sub_field_name, required_keys in _CONTINUITY_SUB_FIELD_REQUIRED_KEYS.items():
        if sub_field_name not in ce:
            raise AppError(
                code="step.contract_violation",
                message=(
                    f"render_prompt_card.continuity_elements_used missing "
                    f"{sub_field_name!r} (G4.4 Override O-6) {where}"
                ),
            )
        sub_field = ce[sub_field_name]
        if not isinstance(sub_field, dict):
            raise AppError(
                code="step.contract_violation",
                message=(
                    f"render_prompt_card.continuity_elements_used.{sub_field_name} "
                    f"must be dict (got {type(sub_field).__name__}) {where}"
                ),
            )
        # Override O-5 superset 검증
        returned_keys = set(sub_field.keys())
        if not (required_keys <= returned_keys):
            missing = sorted(required_keys - returned_keys)
            raise AppError(
                code="step.contract_violation",
                message=(
                    f"render_prompt_card.continuity_elements_used.{sub_field_name} "
                    f"missing required keys {missing} (have keys "
                    f"{sorted(returned_keys)}) {where}"
                ),
            )

    # 2. ref_usage_constraints: top-level 3 sub-key + nested required_keys
    if "ref_usage_constraints" not in ce:
        raise AppError(
            code="step.contract_violation",
            message=(
                "render_prompt_card.continuity_elements_used missing "
                f"'ref_usage_constraints' (G4.4 Override O-6) {where}"
            ),
        )
    ruc = ce["ref_usage_constraints"]
    if not isinstance(ruc, dict):
        raise AppError(
            code="step.contract_violation",
            message=(
                "render_prompt_card.continuity_elements_used.ref_usage_constraints "
                f"must be dict (got {type(ruc).__name__}) {where}"
            ),
        )
    # 3 sub-key superset
    ruc_returned = set(ruc.keys())
    if not (_CONTINUITY_REF_USAGE_SUB_KEYS <= ruc_returned):
        missing = sorted(_CONTINUITY_REF_USAGE_SUB_KEYS - ruc_returned)
        raise AppError(
            code="step.contract_violation",
            message=(
                "ref_usage_constraints missing required sub-keys "
                f"{missing} (have {sorted(ruc_returned)}) {where}"
            ),
        )
    # nested per-sub-key required_keys superset
    for sub_key, nested_required in _CONTINUITY_REF_USAGE_NESTED_REQUIRED_KEYS.items():
        nested = ruc[sub_key]
        if not isinstance(nested, dict):
            raise AppError(
                code="step.contract_violation",
                message=(
                    f"ref_usage_constraints.{sub_key} must be dict "
                    f"(got {type(nested).__name__}) {where}"
                ),
            )
        nested_returned = set(nested.keys())
        if not (nested_required <= nested_returned):
            missing = sorted(nested_required - nested_returned)
            raise AppError(
                code="step.contract_violation",
                message=(
                    f"ref_usage_constraints.{sub_key} missing required keys "
                    f"{missing} (have {sorted(nested_returned)}) {where}"
                ),
            )

    # 3. view_consistency.framing_scope_options 정확 list (extra-strict — list equality)
    vc = ce["view_consistency"]
    fso = vc.get("framing_scope_options")
    if fso != list(_CONTINUITY_FRAMING_SCOPE_OPTIONS):
        raise AppError(
            code="step.contract_violation",
            message=(
                f"view_consistency.framing_scope_options must be exactly "
                f"{list(_CONTINUITY_FRAMING_SCOPE_OPTIONS)} (got {fso!r}) {where}"
            ),
        )
    # mixing_forbidden bool (NOT bool int subclass) trap carry
    mf = vc.get("mixing_forbidden")
    if mf is not True or type(mf) is not bool:
        raise AppError(
            code="step.contract_violation",
            message=(
                f"view_consistency.mixing_forbidden must be bool True "
                f"(got {type(mf).__name__}={mf!r}) {where}"
            ),
        )
```

**주의:**
- Override O-5 strict superset — `EXPECTED_REQUIRED <= returned_keys`. set equality (`==`) 사용 금지. extra keys 허용 (G4.5 forward-compat).
- `framing_scope_options` 만 list equality (`fso == [...]`) — view_consistency 의 다른 free-form key 와 차별. 정확 2 entry sequence 강제.
- `mixing_forbidden is True and type(mf) is bool` — `True == 1` Python 동등 함정 회피. bool int subclass trap carry from G4.3 / G3.2.
- `frozenset` 사용 — module-level data 가 정의된 후 mutate 방지.

---

### Task 1.5 — `build_render_prompt_card()` `_card_metadata` 4 신규 lift_status key + 3 신규 rule_source key

- [ ] **File:** `backend/app/core/steps/render_prompt_card.py` — `build_render_prompt_card()` 위치
- [ ] **Dependency:** Task 1.3
- [ ] **Spec cross-reference:** spec §2.2 _card_metadata matrix / §7.2 step 4 / G4.2 R1-B1 carry / Override O-9
- [ ] **Expected diff size:** ~15 lines

**변경 내용:**

기존 G4.3 `_card_metadata` assembly (commit `51d6c5e` 도입 — `lift_status` 7 key + `rule_source` 7 key) 에 G4.4 신규 4 lift_status key + 3 rule_source key 추가.

```python
# G4.4 Override O-9 carry: continuity lift 완료 — 정확 명 사용 (abbrev 금지)
card["_card_metadata"] = {
    "lift_status": {
        # G4.2 carry (commit 14d14cb)
        "rule_a_lifted": True,
        "rule_c_lifted": True,
        # G4.2 carry — rule_e_lifted 표현은 G4.2 14d14cb commit 의 build_render_prompt_card
        # 안 기존 분기 표현식 그대로 보존 (render_prompt_card.py:1487-1500 참조):
        #   bb_mode = card["background_binding"]["mode"]
        #   if bb_mode in (BG_MODE_OFF, BG_MODE_SKIPPED_CLOSE): rule_e_lifted = True
        #   elif bb_mode in (BG_MODE_NOT_APPLICABLE, BG_MODE_REF_ATTACHED): rule_e_lifted = False
        #   else: raise AppError(...)
        # 본 plan 코드 블록은 G4.4 신규 4 lift_status key 추가 위치만 표시하며, rule_e_lifted
        # 표현식 자체는 G4.2 carry 그대로 유지 — 아래 placeholder 자리에 위 분기 결과 binding.
        "rule_e_lifted": rule_e_lifted,
        # G4.3 carry (commit 51d6c5e)
        "rule_h_lifted": True,
        "id_policy_composite_lifted": True,
        "id_policy_close_framing_face_lifted": True,
        "id_policy_reproduction_surface_lifted": True,
        # G4.4 신규 4 키 (Override O-9 정확 명)
        "continuity_cross_shot_id_substitution_lifted": True,
        "continuity_ref_usage_constraints_lifted": True,
        "continuity_view_consistency_lifted": True,
        "continuity_constraints_extended": True,
    },
    "rule_source": {
        # G4.2 carry
        "camera_rule": "A",
        "owned_rule": "C",
        "close_skip_rule": "E",
        # G4.3 carry
        "demographic_rule": "H",
        "composite_id_rule": "C## 사용 규칙 — 참조 이미지 연동 (v17:32-64)",
        "close_framing_face_rule": "극단 클로즈업 표현 (v17:66-85)",
        "reproduction_surface_rule": "사진·포스터·화면·거울 속 인물 규칙 (v17:133-146)",
        # G4.4 신규 3 키
        "cross_shot_id_substitution_rule": "교차 샷 고정 요소 통합 규칙 (v18:109-120)",
        "ref_usage_rule": "앞쪽 참조 샷 처리 — ref_usage 유형별 지시 (v18:122-156)",
        "view_consistency_rule": "절대 규칙 — 단일 시점 (1인칭/3인칭 혼합 금지) (v18:308-326)",
    },
}
```

**주의 (G4.2 + G4.3 carry):**
- `_card_metadata` 는 `canonicalize_render_prompt_card()` 가 top-level pop (G4.1/G4.2/G4.3 carry) — hash 영향 0.
- `_card_metadata` 는 inject 직전 `detail_steps.py:1937-1942` 에서 strip — LLM 입력 영향 0.
- 4 신규 lift_status key 는 **모든 ref_usage 에서 `True`** (continuity_elements_used 는 ref_usage 무관 동일 적용 — spec §2.2 matrix 단일 row).
- `rule_source` 의 v18 line range 표기는 hardcoded — 시나리오 의존 0 (작품 고유명사 없음).
- **Override O-9 정확 명 사용** — abbrev 형태 (`continuity_cross_shot_id_lifted` / `continuity_ref_usage_lifted`) 절대 금지. drift 시 G4.5 lift_status enumerate test fail.

---

### Task 1.6 — docstring 갱신

- [ ] **File:** `backend/app/core/steps/render_prompt_card.py`
- [ ] **Dependency:** Task 1.1 / 1.2 / 1.3 / 1.4 / 1.5
- [ ] **Spec cross-reference:** spec §7.2 step 1-5
- [ ] **Expected diff size:** ~25 lines (docstring only)

**변경 내용:**

1. Module docstring 상단 `G4.1/G4.2/G4.3 RenderPromptCard helper` → `G4.1/G4.2/G4.3/G4.4 RenderPromptCard helper`.
2. `build_continuity_elements_used()` docstring 에 G4.4 변경 기록 추가:
   ```
   G4.4 (2026-05-05): 3 new sibling policy dicts 추가 (cross_shot_id_substitution_rule
   / ref_usage_constraints / view_consistency — Override O-6 명명). constraints
   2 → 7 strings 확장. ground-truth data 는 module-level constants (Override O-7
   cross-card paired). _card_metadata.lift_status 4 신규 key (Override O-9 정확 명)
   는 build_render_prompt_card wrapper 책임. forward_zoom_targets shape 변경 0
   (Override O-4).
   ```
3. `_assert_continuity_elements_used_shape()` docstring 에 G4.4 3 new sibling policy dicts strict 검증 명시 (Override O-5 superset).
4. `compute_continuity_snapshot_hash()` docstring 에 Override O-12 결정 근거 명시.

---

## Phase 2 (Wave 1-B parallel): v19 prompt 신규 디렉토리

**Phase 목표:** `prompts/_base/scene_detail/19.<timestamp>/` 신규 디렉토리 생성. v18 system.md 복사 후 3 continuity prose section 66 lines 삭제 + ≤ 32-line "Continuity" compact section 신설. `detail_schema.json` / `user.md` 는 v18 verbatim copy. tiktoken cl100k_base 실측으로 §3.4 estimate vs 실측 비교 + Override O-1 token gate threshold 보정 (Wave 5 fix loop 진입 전).

**Phase 2 expected test pass delta:** 0 (test 는 Phase 5 에서).

**중요 — 덮어쓰기 금지:** CLAUDE.md `feedback_prompt_versioning.md` 규칙. `18.<timestamp>/system.md` 수정 금지. 반드시 새 버전 디렉토리 (`19.<timestamp>/`) 생성.

**Wave 1-B → Wave 2 timestamp lockstep**: scratch file `.wave_1b_timestamp.txt` 에 timestamp 기록 후 Wave 2 dispatcher 가 read (G4.3 R4-I3 carry). alignment test fail = lockstep 누락 신호.

---

### Task 2.1 — v19 디렉토리 생성 + system.md 3 prose 삭제

- [ ] **File:** `prompts/_base/scene_detail/19.<timestamp>/system.md` (신규 생성)
- [ ] **Dependency:** 없음 (Wave 1-A 와 병렬 가능)
- [ ] **Spec cross-reference:** spec §3.1 / §3.2 / Override O-10
- [ ] **Expected diff size:** -66 lines (3 prose 삭제) — Override O-2 line count 확인은 Task 2.2 에서

**변경 내용:**

1. **타임스탬프 결정**: 작업 시점의 `YYYYMMDDHHmm` 형식 (예: `19.202605051600`). Wave 2 의 detail_steps.py / version_registry.py 와 lockstep 동기화. **scratch file `.wave_1b_timestamp.txt` 에 timestamp 기록**.
2. **디렉토리 복사**:
   ```bash
   cp -r prompts/_base/scene_detail/18.202605041549 prompts/_base/scene_detail/19.<timestamp>
   ```
3. **v19 system.md 에서 다음 3 sections 삭제** (v18 line 기준 — Override O-10 inclusive 행):

| section | v18 lines (inclusive) | 라인수 | 삭제 이유 |
|---|---|---|---|
| `## 교차 샷 고정 요소 통합 규칙` | 109-120 | 12 | card `cross_shot_id_substitution_rule` (6 required keys) + constraints[1] 가 동일 데이터 carry |
| `## 앞쪽 참조 샷 처리 — ref_usage 유형별 지시` | 122-156 | 35 | card `ref_usage_constraints` (3 sub-key, 5/5/4 required keys) + constraints[2..4] 가 동일 데이터 carry |
| `## 절대 규칙 — 단일 시점 (1인칭/3인칭 혼합 금지)` | 308-326 | 19 | card `view_consistency` (9 required keys) + constraints[5..6] 가 동일 데이터 carry. line 313 의 cross-card reference (`id_policy.body_part_focus_rule`) 는 v19 compact section 안 1 line reminder 로 보존 (Override O-7 paired) |

총 삭제: 12 + 35 + 19 = **66 lines** (system.md). detail_steps.py inline 9 lines 별도 (Phase 3 Task 3.3 (Wave 2) — Override O-3).

**Blank lines preservation (Override O-10)**: blank lines 121 / 157 / 307 / 327 = 인접 섹션 보존. 트레일링 blank 한 개씩 carry 또는 drop 결정은 v19 final draft 시 line-by-line diff 점검.

**삭제 자동화 검증 (`grep -n` post-delete check):**
```bash
grep -n "교차 샷 고정 요소 통합 규칙" prompts/_base/scene_detail/19.<timestamp>/system.md
# → (no output)
grep -n "앞쪽 참조 샷 처리 — ref_usage 유형별 지시" prompts/_base/scene_detail/19.<timestamp>/system.md
# → (no output)
grep -n "절대 규칙 — 단일 시점" prompts/_base/scene_detail/19.<timestamp>/system.md
# → (no output)
```

**보존 검증** (G4.3 / G4.2 영역 변경 없음):
```bash
grep -n "## ID Policy" prompts/_base/scene_detail/19.<timestamp>/system.md
# → <line>:## ID Policy ...  (G4.3 carry 보존)
grep -n "## Background Binding" prompts/_base/scene_detail/19.<timestamp>/system.md
# → <line>:## Background Binding ...  (G4.2 carry 보존)
```

---

### Task 2.2 — v19 system.md ≤ 32-line "Continuity" compact section 삽입

- [ ] **File:** `prompts/_base/scene_detail/19.<timestamp>/system.md`
- [ ] **Dependency:** Task 2.1 완료
- [ ] **Spec cross-reference:** spec §3.3 / Override O-2 / O-15
- [ ] **Expected diff size:** +14 lines (spec §3.3 enumerate, gate ≤ 32 strict)

**변경 내용:**

삭제된 `## 교차 샷 고정 요소 통합 규칙` (line 109) 위치에 다음 ≤ 32-line compact section 삽입 (`## 앞쪽 참조 샷 처리` / `## 절대 규칙 — 단일 시점` 3 section 의 위치 모두 본 단일 section 으로 통합):

```markdown
## Continuity (cross-shot substitution + ref_usage + single-view)

`[RenderPromptCard v1]` 의 `continuity_elements_used` field 가 이 shot 의 연속성 정책을 결정한다 (card 가 primary contract — 충돌 시 card 우선).

**핵심 원칙:**

- `fixed_elements[i].description` 안 보통명사 인물 ("An Asian man" / "a woman" 류) 이 `cross_shot_id_substitution_rule.applies_when` 조건 (character_name → C## 또는 C##O## 매핑이 id_policy 안에 존재) 만족 시 해당 보통명사를 C##/C##O## 로 치환. 같은 인물 C##/C##O## + 보통명사 이중 묘사 금지 (모델이 두 인물로 인식해 인원 부풀림). 자세/위치/상태 keyword 는 보존.
- `previous_shot_refs[i].ref_usage='zoom_in_detail'` 이면 같은 시공간·같은 주요 피사체·카메라 sub-region 확대만 — `ref_usage_constraints.zoom_in_detail.forbidden_additions` (새 인물·소품·배경·자세·다른 순간) 추가 금지. `required_phrasings` (the same figure / the same room / the same surface 류) 사용. body-part Focus 시 `id_policy.body_part_focus_rule` 적용 (보통명사 + demographic descriptor).
- `previous_shot_refs[i].ref_usage='exact_background'` 이면 같은 방 다른 순간/앵글 — 인물·동작은 이 샷의 instantaneous moment 로 새로 묘사, 배경은 prior shot 과 일관 유지. ignore/keep 자동 반영.
- `previous_shot_refs[i].ref_usage='atmosphere_reference'` 이면 다른 공간 — 새 공간 배경 직접 묘사하고 색감·조명 톤만 prior shot 에서 carry. `ref_usage_constraints.atmosphere_reference.forbidden_imports` (가구 배치·벽 위치·architectural feature) 끌어오기 금지.
- `view_consistency.single_camera_rule`: 한 t2i_prompt = 한 카메라 위치. 3인칭 전신 묘사 + 같은 인물의 body-part close-up 한 프롬프트 결합 금지. "Focus on" = 초점 영역 (시점 전환 X). body-part Focus 시 `id_policy.body_part_focus_rule` 참조 — body-part Focus 시 C##O## 금지 (얼굴 ref 합성 차단).
- `view_consistency.prop_orientation_rule`: 인물이 잡은 소품은 한 방향만 — 카메라 향함 OR 인물 향함, 양방향 모순 금지.

**충돌 해결**: 본 섹션 prose 는 reasoning summary; 구체 token list (forbidden_additions / required_phrasings / forbidden_imports) 는 모두 `continuity_elements_used` sub-field 가 single source. prose 가 sub-field 와 충돌하면 sub-field 우선.
```

**주의:**
- 정확히 **14 lines** spec §3.3 enumerate (heading 1 + blank 1 + intro 1 + blank 1 + "**핵심 원칙:**" 1 + blank 1 + 6 bullet × 1 line + blank 1 + "**충돌 해결**" 1).
- **Override O-2 binding gate `wc -l` ≤ 32 lines strict** — wrap 줄도 line 1 로 카운트. 80-col wrap 발생 시 violation. Wave 1-B draft 작성 시 자체 점검 필수.
- ✓/✗ 예시 inline 미포함 (Override O-15 결정 — G4.3 baseline 통과 패턴 carry, canary STRICT 0 검증이 LLM 행동 보증).
- Override O-7 cross-card paired-string substring `"id_policy.body_part_focus_rule"` 1+ 등장 (line 313 carry — 통합 테스트 §6.2 강제).
- perception_mode 우선 명시는 G4.3 ID Policy 영역 — G4.4 변경 0.

**삽입 후 검증 (R4-M4 carry from G4.3 — sed inclusive trap 회피):**
```bash
grep -n "## Continuity" prompts/_base/scene_detail/19.<timestamp>/system.md
# → <line>:## Continuity (cross-shot substitution + ref_usage + single-view)
```

```python
# Override O-2 line count gate
import re
with open("prompts/_base/scene_detail/19.<timestamp>/system.md") as f:
    text = f.read()
m = re.search(r"## Continuity.*?(?=\n## )", text, re.DOTALL)
assert m is not None, "## Continuity section not found"
content_lines = m.group().split("\n")
assert len(content_lines) <= 32, f"expected ≤ 32 lines, got {len(content_lines)}"
```

---

### Task 2.3 — `detail_schema.json` / `user.md` verbatim copy + 자체 line count + tiktoken 실측

- [ ] **File:** `prompts/_base/scene_detail/19.<timestamp>/detail_schema.json` + `user.md` (verbatim copy)
- [ ] **Dependency:** Task 2.1 / 2.2 완료
- [ ] **Spec cross-reference:** spec §7.2 step 5-8 / Override O-1
- [ ] **Expected diff size:** 0 (verbatim) + tiktoken 실측 결과 별도 캡쳐

**변경 내용:**

1. **verbatim copy** (cp_r 시 이미 복사됨 — 변경 없음 확인):
   ```bash
   diff prompts/_base/scene_detail/18.202605041549/detail_schema.json prompts/_base/scene_detail/19.<timestamp>/detail_schema.json
   # → (no output — 동일)

   diff prompts/_base/scene_detail/18.202605041549/user.md prompts/_base/scene_detail/19.<timestamp>/user.md
   # → (no output — 동일)
   ```

2. **자체 line count + tiktoken 실측 (Override O-1 carry)**:
   ```bash
   wc -l prompts/_base/scene_detail/18.202605041549/system.md
   # → 587 (baseline)

   wc -l prompts/_base/scene_detail/19.<timestamp>/system.md
   # → 587 - 66 + 14 = 535 부근 (3 prose 삭제 + Continuity compact)

   python -c "
   import tiktoken
   enc = tiktoken.get_encoding('cl100k_base')
   with open('prompts/_base/scene_detail/18.202605041549/system.md') as f:
       v18_tokens = len(enc.encode(f.read()))
   with open('prompts/_base/scene_detail/19.<timestamp>/system.md') as f:
       v19_tokens = len(enc.encode(f.read()))
   print(f'v18: {v18_tokens} tokens')
   print(f'v19: {v19_tokens} tokens')
   print(f'token_delta: {v19_tokens - v18_tokens}')
   print(f'gate -540 pass: {v19_tokens - v18_tokens <= -540}')
   "
   ```

3. **gate margin 실측 후 estimate 보정 (Override O-1)**: estimate `-780` 와 actual delta 비교 → spec §3.4 inline note 갱신 (선택 — Wave 5 fix loop 진입 시). Round 2 Override row 로 downward (e.g. -700) revise 가능, **upward revise (e.g. -300) 는 re-audit 필요**.

---

### Task 2.4 — Wave 1-B → Wave 2 timestamp lockstep 기록

- [ ] **File:** `.wave_1b_timestamp.txt` (scratch — gitignored)
- [ ] **Dependency:** Task 2.1 완료
- [ ] **Spec cross-reference:** Override O-18 / G4.3 R4-I3 carry
- [ ] **Expected diff size:** 1 line (timestamp value)

**변경 내용:**

```bash
echo "19.<timestamp>" > .wave_1b_timestamp.txt
```

Wave 2 dispatcher (Task 3.1 + 3.2 + 3.3) 가 본 file 을 read 후 detail_steps.py:105 + version_registry.py:34 + version_registry.py:126 + alignment test fixture 4-point sync 적용. lockstep drift 시 alignment test fail.

**주의:**
- `.wave_1b_timestamp.txt` 는 .gitignore 에 추가 (git status `?? .wave_1b_timestamp.txt` 확인) — single commit 시점에 삭제 또는 commit 제외.
- timestamp 형식 `YYYYMMDDHHmm` (12자리) — `feedback_version_format.md` carry.

---

## Phase 3 (Wave 2): detail_steps.py + version_registry 4-point sync + Override O-3 inline prose 삭제

**Phase 목표:** `SCENE_DETAIL_PROMPT_VERSION` v18 → `"19.<timestamp>"` bump (디렉토리 매칭). `MODULE_VERSIONS["scene_detail_composer"]` `1.18.0` → `1.19.0`. `_MODULE_INFO["scene_detail_composer"]["prompt_dependency"]` `"scene_detail/v18"` → `"scene_detail/v19"` (dict access — major suffix only, G4.3 R3R4-B1 carry). **detail_steps.py:1555-1563 inline `if has_zoom_in_detail:` 9 줄 prose 삭제 (Override O-3)**. alignment test 가 4-point sync 강제 검증.

**Phase 3 expected test pass delta:** 0 (alignment test pass 는 Wave 4 에서 검증).

---

### Task 3.1 — `SCENE_DETAIL_PROMPT_VERSION` 상수 bump

- [ ] **File:** `backend/app/core/steps/detail_steps.py:105`
- [ ] **Dependency:** Wave 1-B Task 2.1 완료 (v19 `<timestamp>` 결정 *후*) + scratch `.wave_1b_timestamp.txt` read
- [ ] **Spec cross-reference:** spec §6.4 4-point sync / G4.3 plan-R1-I3 carry
- [ ] **Expected diff size:** ~1 line

**변경 내용:**

line 105 의 정확한 변경:
```python
# Before (G4.3):
SCENE_DETAIL_PROMPT_VERSION = "18.202605041549"  # G4.3: ID-policy lift — 4 prose sections (C##/close-framing/reproduction/Rule H) → id_policy 5 sub-fields + 6-string constraints. v18 system.md = v17 - 109 lines + 30-line compact section + line 395 인플레이스.

# After (G4.4):
SCENE_DETAIL_PROMPT_VERSION = "19.<timestamp>"  # G4.4: Continuity lift — 3 prose sections (cross-shot subst./ref_usage/single-view) → continuity_elements_used 3 new sibling policy dicts + 7-string constraints. v19 system.md = v18 - 66 lines + ≤ 32-line Continuity compact section. detail_steps.py:1555-1563 inline prose 9 줄 별도 삭제 (Override O-3).
```

`<timestamp>` 는 Phase 2 Task 2.1 에서 결정된 실제 `YYYYMMDDHHmm` 값 — Wave 1-B / Wave 2 lockstep (`.wave_1b_timestamp.txt` read).

line 713 의 사용처 (`"prompt_version": SCENE_DETAIL_PROMPT_VERSION`) 는 자동 반영 — 별도 변경 불필요. 본 변경은 cp `config_hash` 자동 변경 → resume 시 v18 cp 가 force escalate path 로 진입 (G4.1 R1-B1 / G4.2 / G4.3 carry pattern).

**주의 (G4.2 / G4.3 plan carry — SCHEMA alignment):**
- `SCENE_DETAIL_SCHEMA_VERSION` (= 7) 은 **변경 없음**. cp shape 변경 아님 (continuity_elements_used 3 new sibling policy dicts 추가는 cp 안 dict key 추가만).
- `backend/app/core/step_manifest.py:632` 의 `scene_detail.schema_version = 7` 도 변경 없음.
- `CARD_SCHEMA_VERSION` (= 1) 은 변경 없음 — continuity_elements_used 신규 sub-field 는 envelope 외 dict 확장이며 7 → 동일.

---

### Task 3.2 — `version_registry.py` 4-point sync 동시 변경

- [ ] **File:** `backend/app/core/version_registry.py:34` + `:126`
- [ ] **Dependency:** Task 3.1 완료
- [ ] **Spec cross-reference:** spec §6.4 / G4.3 R3R4-B1 carry
- [ ] **Expected diff size:** ~6 lines

**변경 내용:**

```python
# backend/app/core/version_registry.py:34 — MODULE_VERSIONS dict
# Before (G4.3):
MODULE_VERSIONS = {
    ...
    "scene_detail_composer": "1.18.0",  # G4.3 ID-policy lift
    ...
}

# After (G4.4):
MODULE_VERSIONS = {
    ...
    "scene_detail_composer": "1.19.0",  # G4.4 continuity lift (3 prose → 3 new sibling policy dicts + 7 constraints)
    ...
}
```

```python
# backend/app/core/version_registry.py:125-128 — _MODULE_INFO dict entry
# (G4.3 R3R4-B1 carry: 실제 shape 는 dict, NOT ModuleInfo 객체. value 도 nested dict.)
# Before (G4.3):
"scene_detail_composer": {
    "prompt_dependency": "scene_detail/v18",
    "updated_at": "2026-05-04",
},

# After (G4.4):
"scene_detail_composer": {
    "prompt_dependency": "scene_detail/v19",  # major suffix only — NOT "scene_detail/19.<timestamp>"
    "updated_at": "2026-05-05",
},
```

**주의 (G4.3 R3R4-B1 carry):**
- `_MODULE_INFO` 는 plain dict 리터럴 (`backend/app/core/version_registry.py:43-145`). `ModuleInfo(...)` constructor 는 존재하지 않음 — attribute access (`.prompt_dependency`) 사용 시 NameError. 반드시 dict access (`["prompt_dependency"]`) 사용.
- `prompt_dependency` 값 = `"scene_detail/v19"` (major suffix only). 기존 alignment test 가 `pdep.endswith(f"v{latest_major}")` 검증하므로 timestamp 형식 (`"scene_detail/19.<timestamp>"`) 사용 시 alignment fail. `version_registry.py:106 "shot_extract/v11"` / `:110 "shot_validator/v3"` 등 기존 entry 모두 major-only suffix.
- 본 변경 누락 시 `test_scene_detail_continuity_alignment.py` alignment test 4-point sync row 가 fail.
- Task 5.3 alignment test 가 본 변경 검증 (Wave 4 에서).
- `SCENE_DETAIL_PROMPT_VERSION` (Task 3.1) 의 timestamp 부분과 본 `prompt_dependency` 값은 **별도 source** — 전자는 `"19.<timestamp>"` (디렉토리 매칭), 후자는 `"scene_detail/v19"` (major suffix). 혼동 금지.

---

### Task 3.3 — `detail_steps.py:1555-1563` inline prose 9 줄 삭제 (Override O-3)

- [ ] **File:** `backend/app/core/steps/detail_steps.py:1555-1563`
- [ ] **Dependency:** Task 3.1 완료 (v19 prompt active)
- [ ] **Spec cross-reference:** spec §2.3 / §3.2 / §4 / §7.2 step 11 / Override O-3
- [ ] **Expected diff size:** -9 lines (inline prose 9 줄 + trailing `\n\n`)

**변경 내용 (2 sub-step — R3-M1 carry, 명시 분리):**

card 의 `ref_usage_constraints.zoom_in_detail` sub-dict (5 required keys) 가 inline prose 9 줄 데이터 lossless carry.

**Sub-step 1 (Mandatory):** `if has_zoom_in_detail:` 블록 안 9 줄 inline prose 삭제 (line 1555-1563).

```python
# Before (G4.3 carry):
if has_zoom_in_detail:
    user_prompt += (
        "\n\n[중요 — 확대 샷 원칙 (zoom_in_detail)]\n"
        "- 새 인물 추가 금지\n"
        "- 새 소품 추가 금지\n"
        "- 새 자세 추가 금지\n"
        "- 인체 부위 초점이면 보통명사 사용\n"
        "- 환경(조명·배경) 동일 유지\n"
        "- 다른 순간 묘사 금지\n"
        "- 'the same figure' / 'the same room' 류 연속 표현 사용\n"
    )

# After (G4.4 — 9 줄 inline prose 전체 + `if has_zoom_in_detail:` 분기 자체 삭제):
# (block 전체 제거)
```

**Sub-step 2 (Conditional — grep 후 결정):** `has_zoom_in_detail` 변수 정의 처리.

```bash
grep -n "has_zoom_in_detail" backend/app/core/steps/detail_steps.py
```

분기:
- **grep 결과 0 사용처** (= sub-step 1 으로 마지막 사용처 삭제됨): 변수 정의 (line 1540-1554 부근) 도 dead code 로 함께 삭제. integration test 13 (`test_detail_steps_has_zoom_in_detail_variable_no_dead_uses`) 가 강제.
- **grep 결과 ≥1 사용처** (sub-step 1 외 다른 코드 경로 사용): 변수 정의 그대로 유지 (오삭제 방지). 9 줄 prose 만 삭제 완료.

Wave 2 implementer 는 grep 명시 후 분기 결정 (M1 R3 audit carry — 우연한 잔존 / 우연한 오삭제 차단).

**삭제 후 검증 (Wave 4 integration test §6.2 가 강제):**
```bash
# v18-style substring 부재 확인
grep -n "확대 샷 원칙" backend/app/core/steps/detail_steps.py
# → (no output)

grep -n "\\[중요 — 확대 샷 원칙 (zoom_in_detail)\\]" backend/app/core/steps/detail_steps.py
# → (no output)
```

**주의 (Override O-3 carry):**
- 삭제 대상 = `if has_zoom_in_detail:` 블록 내부 prose **만**. forward_zoom_targets prose inject (line 1566-1620) 는 G4.4 scope 외 (Override O-4) — 보존.
- card 의 `ref_usage_constraints.zoom_in_detail` 5 required keys (`scope_summary` / `forbidden_additions` 5 entries / `required_phrasings` 3 entries / `body_part_cross_ref` / `rationale_summary`) 가 prose 9 줄 데이터 lossless carry.
- v19 system.md compact section (Task 2.2) 의 zoom_in_detail bullet 도 보조 reasoning carry.
- token reduction = system 외 별도 인접 계산 (~9 lines × 15 ≈ 135 tokens; per-shot prepend 시 zoom_in_detail shot 만 — Wave 1-B 실측 보정).

---

## Phase 4 (Wave 3): canary scripts 5종

**Phase 목표:** `scripts/canary/` 에 5개 측정 스크립트 작성 (4 metric + 1 token). 4 metric scripts (Task 4.1-4.4) 는 `compute_continuity_snapshot_hash` import 후 CP-pinning JSON 에 hash 기록. **Task 4.5 token script 는 CP-free** — system.md 토큰만 측정하므로 hash helper 미import (M2 carry — R3-M2). 각 스크립트는 module-level constants import + sentinel pre-pass + pinned-tuple post-loop validation + ±50 chars window proximity (sentence regex 금지) + gate fail → exit 1.

**스크립트 5종**:
- Task 4.1 `g4_4_double_description.py` — fixed_element id-mapping shot only, ±50 chars window double-description detection
- Task 4.2 `g4_4_zoom_in_detail_no_new_entity.py` — ref_usage='zoom_in_detail' shot only, prior shot diff
- Task 4.3 `g4_4_atmosphere_no_layout_import.py` — Override O-11 multi-ref filter (any atmosphere ref), furniture token detection
- Task 4.4 `g4_4_view_mixing.py` — 모든 shot scope, full-body + body-part close-up co-occurrence (cross-import G4.3 `_ID_BODY_PART_TRIGGERS`)
- Task 4.5 `g4_4_token_count.py` — system.md tiktoken cl100k_base v18 vs v19 delta + system+inject 합산 (Override O-1 secondary report-only)

**공통 패턴 (G4.2 + G4.3 carry — silent-absorb 차단)**:
- **CP shape**: `scene["render_prompt_card"]["continuity_elements_used"]` (NOT 평면 `scene["continuity_elements_used"]`). G4.2 함정 carry — `detail_steps.py:2161` authoritative (G4.3 R3-M1 carry).
- **silent absorb 금지**: `or [] / or {}` 패턴 제거. `data.scenes` / `t2i_variations` / `t2i_prompt` 누락은 `measurement_failures` list 로 기록 후 exit 1.
- **pinned tuple seen_in_cp post-loop validation**: pinning 의 `(si, shi)` 가 CP 에 실제 존재했는지 enumerate 후 검증. 없으면 false STRICT pass 차단.
- **t2i_variations + per-variation t2i_prompt validation**: variation 누락 / variation 내부 t2i_prompt 누락 모두 measurement_failures.
- **module-level constants import**: 각 script 가 `from app.core.steps.render_prompt_card import _CONTINUITY_GENERIC_PERSON_NOUNS, _CONTINUITY_FURNITURE_LAYOUT_TOKENS, _CONTINUITY_REF_USAGE_VALUES, _ID_BODY_PART_TRIGGERS, compute_continuity_snapshot_hash`.
- **±50 chars window proximity (sentence regex 사용 금지)** — `feedback_no_regex_postprocessing.md` carry. id 등장 위치를 기준으로 [start-50, end+50] window 안에서 keyword/component substring presence 검사.
- **Override O-17 carry** — regex 1차 detector. baseline 비0 시 P1 follow-up 으로 LLM-validator (gpt-5.4-mini judge) 전환 검토.

**공통 preflight helper (G4.3 R3R4-B2 carry — 4 metric script 모두 적용)**:

```python
# 모든 4 metric script 가 metric 합산 전 본 helper 호출. silent absorb 차단.

def _load_scenes_or_fail(cp: dict, measurement_failures: list) -> list | None:
    # silent-fallback ban (Trap #1, R3-B1) — explicit key existence + type validation.
    if "data" not in cp:
        measurement_failures.append("cp.data key missing")
        return None
    data = cp["data"]
    if not isinstance(data, dict):
        measurement_failures.append("cp.data not dict")
        return None
    if "scenes" not in data:
        measurement_failures.append("cp.data.scenes key missing")
        return None
    scenes = data["scenes"]
    if not isinstance(scenes, list):
        measurement_failures.append("cp.data.scenes not list")
        return None
    return scenes


def _validate_scene_preflight(
    scene: dict, si: int, shi: int, measurement_failures: list
) -> list | None:
    # silent-fallback ban (Trap #1, R3-B1) — explicit key existence + type validation.
    if "render_prompt_card" not in scene:
        measurement_failures.append(
            f"s{si}_sh{shi}: render_prompt_card key missing"
        )
        return None
    rpc = scene["render_prompt_card"]
    if not isinstance(rpc, dict):
        measurement_failures.append(
            f"s{si}_sh{shi}: render_prompt_card not dict"
        )
        return None
    if "continuity_elements_used" not in rpc:
        measurement_failures.append(
            f"s{si}_sh{shi}: render_prompt_card.continuity_elements_used key missing"
        )
        return None
    ce = rpc["continuity_elements_used"]
    if not isinstance(ce, dict):
        measurement_failures.append(
            f"s{si}_sh{shi}: render_prompt_card.continuity_elements_used not dict"
        )
        return None
    # snapshot hash 산출 — 실패 시 measurement_failures (silent skip 금지)
    try:
        compute_continuity_snapshot_hash(rpc)
    except Exception as e:
        measurement_failures.append(f"s{si}_sh{shi}: continuity hash failed: {e}")
        return None
    # silent-fallback ban (Trap #1, R3-B1) — explicit key existence + type +
    # non-empty validation (missing OR empty must fail, never silently absorbed).
    if "t2i_variations" not in scene:
        measurement_failures.append(
            f"s{si}_sh{shi}: t2i_variations key missing"
        )
        return None
    variations = scene["t2i_variations"]
    if not isinstance(variations, list):
        measurement_failures.append(
            f"s{si}_sh{shi}: t2i_variations not list"
        )
        return None
    if not variations:
        measurement_failures.append(
            f"s{si}_sh{shi}: t2i_variations empty"
        )
        return None
    return variations


# script 본문 — 사용 예시:
# scenes = _load_scenes_or_fail(cp, measurement_failures)
# if scenes is None:
#     sys.exit(1)
# for scene in scenes:
#     si = scene.get("scene_index")
#     shi = scene.get("_shot_index")
#     variations = _validate_scene_preflight(scene, si, shi, measurement_failures)
#     if variations is None:
#         continue
#     for vi, var in enumerate(variations):
#         prompt = var.get("t2i_prompt")
#         if not isinstance(prompt, str):
#             measurement_failures.append(f"s{si}_sh{shi}_v{vi}: t2i_prompt missing")
#             continue
#         # ... metric 측정 ...
# if measurement_failures:
#     print(json.dumps({"measurement_failures": measurement_failures}))
#     sys.exit(1)
```

**주의 (G4.3 R3R4-B2 carry + R3-B1 strict):**
- silent-absorb 패턴 (`scene.get("t2i_variations", [])` / `ce.get("fixed_elements", [])` / `rpc.get("id_policy", {})` / `ce.get("previous_shot_refs", [])` 등) **절대 금지** — Trap #1 violation. `_validate_scene_preflight()` helper + per-Task explicit key existence + type + (where applicable) non-empty validation 으로 fail-fast.
- `t2i_variations` 누락 OR 빈 list 모두 `measurement_failures` 누적 (R3-B1 carry — empty must fail).
- token script (Task 4.5) 는 CP 가 아니라 prompt 파일을 읽으므로 본 helper 미적용 (R3-M2 carry — CP-free script). prompt 파일 존재 / version 매칭 / pinning validation 별도 적용. `compute_continuity_snapshot_hash` 미import.
- gate fail 시 모든 4 metric script + token script 명시적 `sys.exit(1)`.
- 모든 5 canary script 의 출력 JSON schema 에 `measurement_failures` field (배열) 필수 — silent absorb 차단 (R3-B1 carry).
- canary DRY refactor (`scripts/canary/_canary_common.py` helper module 신설) = G4.5 또는 P1 follow-up (Override O-8 carry).

---

### Task 4.1 — `g4_4_double_description.py` (fixed_element id-mapping shot only)

- [ ] **File:** `scripts/canary/g4_4_double_description.py` (신규 생성)
- [ ] **Dependency:** Task 1.1 (`_CONTINUITY_GENERIC_PERSON_NOUNS` 사용) + Task 1.2 (`compute_continuity_snapshot_hash`)
- [ ] **Spec cross-reference:** spec §5.1 (cross-shot row) / §5.3 / Override O-7 / O-12
- [ ] **Expected diff size:** ~150 lines (신규)

**스크립트 역할:**

주어진 PID + scene_index_list 의 scene_detail CP 를 읽어, **fixed_elements[i].character_name → C##/C##O## 매핑이 1+ 존재하는 shot 만** scan, 매핑된 character ID + ±50 chars window 안의 generic person noun (`_CONTINUITY_GENERIC_PERSON_NOUNS`) 동시 등장 검출.

**regex (Override O-7 carry — paired-string substring):**
```python
import re
from app.core.steps.render_prompt_card import (
    _CONTINUITY_GENERIC_PERSON_NOUNS,
    compute_continuity_snapshot_hash,
)

# bare C## OR composite C##O##
ID_REGEX = re.compile(r"\bC\d{2}(?:O\d{2})?\b")
WINDOW_CHARS = 50
```

**핵심 로직 (silent-absorb 차단 패턴):**
```python
measurement_failures: list = []
total_shots_with_id_mapping = 0
double_description_count = 0
double_description_per_shot = {}
seen_in_cp: set = set()

cp = json.loads(cp_path.read_text())
scenes = _load_scenes_or_fail(cp, measurement_failures)
if scenes is None:
    print(json.dumps({"measurement_failures": measurement_failures}))
    sys.exit(1)

for scene in scenes:
    si = scene.get("scene_index")
    shi = scene.get("_shot_index")
    if (si, shi) not in pinning_scene_set:
        continue
    seen_in_cp.add((si, shi))
    variations = _validate_scene_preflight(scene, si, shi, measurement_failures)
    if variations is None:
        continue
    # fixed_elements id-mapping check (분모 — id_mapping 1+ shot 만)
    # silent-fallback ban (Trap #1, R3-B1) — explicit key existence + type validation,
    # NEVER `.get(key, default)` patterns.
    rpc = scene["render_prompt_card"]
    ce = rpc["continuity_elements_used"]
    if "fixed_elements" not in ce:
        measurement_failures.append(
            f"s{si}_sh{shi}: continuity_elements_used.fixed_elements key missing"
        )
        continue
    fixed_elements = ce["fixed_elements"]
    if not isinstance(fixed_elements, list):
        measurement_failures.append(
            f"s{si}_sh{shi}: continuity_elements_used.fixed_elements not list"
        )
        continue
    if "id_policy" not in rpc:
        measurement_failures.append(
            f"s{si}_sh{shi}: render_prompt_card.id_policy key missing"
        )
        continue
    id_policy = rpc["id_policy"]
    if not isinstance(id_policy, dict):
        measurement_failures.append(
            f"s{si}_sh{shi}: render_prompt_card.id_policy not dict"
        )
        continue
    if "allowed_outlook_pairs" not in id_policy:
        measurement_failures.append(
            f"s{si}_sh{shi}: id_policy.allowed_outlook_pairs key missing"
        )
        continue
    allowed_pairs = id_policy["allowed_outlook_pairs"]
    if not isinstance(allowed_pairs, list):
        measurement_failures.append(
            f"s{si}_sh{shi}: id_policy.allowed_outlook_pairs not list"
        )
        continue
    if not any(
        fe.get("character_name") and any(
            fe["character_name"] == op.get("character_id") or
            fe["character_name"] == f'{op.get("character_id")}{op.get("outlook_id")}'
            for op in allowed_pairs
        )
        for fe in fixed_elements
    ):
        continue  # id_mapping 0 — scope 외
    total_shots_with_id_mapping += 1
    shot_count = 0
    for vi, var in enumerate(variations):
        prompt = var.get("t2i_prompt")
        if not isinstance(prompt, str):
            measurement_failures.append(
                f"s{si}_sh{shi}_v{vi}: t2i_prompt missing"
            )
            continue
        # ID positions
        prompt_lower = prompt.lower()
        for match in ID_REGEX.finditer(prompt):
            id_start = match.start()
            id_end = match.end()
            id_token = match.group()
            # ±50 chars window 안 generic noun 검출
            win_start = max(0, id_start - WINDOW_CHARS)
            win_end = min(len(prompt), id_end + WINDOW_CHARS)
            window = prompt_lower[win_start:win_end]
            for noun in _CONTINUITY_GENERIC_PERSON_NOUNS:
                if noun.lower() in window:
                    shot_count += 1
                    break  # one violation per ID per window
    double_description_count += shot_count
    double_description_per_shot[f"{si}_{shi}"] = shot_count

# pinned tuple seen_in_cp post-loop validation
missing_in_cp = pinning_scene_set - seen_in_cp
if missing_in_cp:
    measurement_failures.append(
        f"pinning scene_set NOT in CP: {sorted(missing_in_cp)}"
    )

if measurement_failures:
    print(json.dumps({"measurement_failures": measurement_failures}))
    sys.exit(1)
```

**출력 JSON schema:**
```json
{
  "timestamp": "ISO8601",
  "prompt_version": "18.202605041549",
  "pinning": {
    "pid": "...",
    "scene_index_list": [],
    "shot_index_list_per_scene": {},
    "model_routing": "gemini-3.1-pro-preview",
    "prompt_source_mode": "file",
    "card_commit_hash": "51d6c5e",
    "continuity_card_snapshot_hash": "..."
  },
  "scene_set": [
    {"scene_index": 3, "shot_index": 1, "has_fixed_element_id_mapping": true, "ref_usage": null}
  ],
  "metrics": {
    "total_shots_with_id_mapping": 0,
    "double_description_count": 0,
    "double_description_per_shot": {}
  },
  "measurement_failures": [],
  "measurement_scripts": {
    "double_description": "scripts/canary/g4_4_double_description.py (id-mapping shots only)"
  }
}
```

**exit criteria threshold (spec §5.1 strict):**
- **STRICT 0**: `candidate.metrics.double_description_count == 0` (baseline 비0 이어도 candidate 0 강제).
- baseline 비0 이면 P1 follow-up 등록 (G4.4 scope 밖, 별도 추적).

---

### Task 4.2 — `g4_4_zoom_in_detail_no_new_entity.py` (zoom_in_detail shot only)

- [ ] **File:** `scripts/canary/g4_4_zoom_in_detail_no_new_entity.py` (신규 생성)
- [ ] **Dependency:** Task 1.1 / 1.2
- [ ] **Spec cross-reference:** spec §5.1 (zoom_in_detail row) / §5.3
- [ ] **Expected diff size:** ~155 lines (신규)

**스크립트 역할:**

ref_usage='zoom_in_detail' shot 만 scan. prior shot 의 t2i_prompt 의 C##/C##O## set + prop short_id (P##) set 와 current t2i_prompt 의 동일 set diff 검출. diff > 0 = entity 추가 violation.

**핵심 로직 — prior shot 식별 + diff:**

```python
import re
from app.core.steps.render_prompt_card import compute_continuity_snapshot_hash

# bare C## / composite C##O## / prop P##
CHAR_ID_REGEX = re.compile(r"\bC\d{2}(?:O\d{2})?\b")
PROP_ID_REGEX = re.compile(r"\bP\d{2}\b")


def extract_entity_set(prompt: str) -> set:
    """t2i_prompt 안 모든 C##/C##O##/P## entity ID set 반환."""
    chars = set(CHAR_ID_REGEX.findall(prompt))
    props = set(PROP_ID_REGEX.findall(prompt))
    return chars | props


# scene-shot lookup dict 사전 구축
scene_shot_lookup = {}
for scene in scenes:
    si = scene["scene_index"]
    shi = scene["_shot_index"]
    scene_shot_lookup[(si, shi)] = scene


# zoom_in_detail shot 만 measurement
for scene in scenes:
    si = scene.get("scene_index")
    shi = scene.get("_shot_index")
    if (si, shi) not in pinning_scene_set:
        continue
    seen_in_cp.add((si, shi))
    variations = _validate_scene_preflight(scene, si, shi, measurement_failures)
    if variations is None:
        continue
    rpc = scene["render_prompt_card"]
    ce = rpc["continuity_elements_used"]
    # silent-fallback ban (Trap #1, R3-B1) — explicit key existence + type validation.
    if "previous_shot_refs" not in ce:
        measurement_failures.append(
            f"s{si}_sh{shi}: continuity_elements_used.previous_shot_refs key missing"
        )
        continue
    refs = ce["previous_shot_refs"]
    if not isinstance(refs, list):
        measurement_failures.append(
            f"s{si}_sh{shi}: continuity_elements_used.previous_shot_refs not list"
        )
        continue
    zoom_refs = [r for r in refs if r.get("ref_usage") == "zoom_in_detail"]
    if not zoom_refs:
        continue  # zoom_in_detail 아님 — scope 외
    total_zoom_shots += 1
    # prior shot 의 entity set 합산
    prior_entities = set()
    for ref in zoom_refs:
        prior_si = ref.get("scene_index")
        prior_shi = ref.get("shot_index")
        prior_scene = scene_shot_lookup.get((prior_si, prior_shi))
        if prior_scene is None:
            measurement_failures.append(
                f"s{si}_sh{shi}: zoom_in_detail prior s{prior_si}_sh{prior_shi} not found in CP"
            )
            continue
        # explicit existence + type + non-empty (Trap #1) — NEVER `.get(default)`.
        if "t2i_variations" not in prior_scene:
            measurement_failures.append(
                f"s{prior_si}_sh{prior_shi}: t2i_variations key missing"
            )
            continue
        prior_variations = prior_scene["t2i_variations"]
        if not isinstance(prior_variations, list):
            measurement_failures.append(
                f"s{prior_si}_sh{prior_shi}: t2i_variations not list"
            )
            continue
        if not prior_variations:
            measurement_failures.append(
                f"s{prior_si}_sh{prior_shi}: t2i_variations empty"
            )
            continue
        for pvar in prior_variations:
            pprompt = pvar.get("t2i_prompt")
            if isinstance(pprompt, str):
                prior_entities |= extract_entity_set(pprompt)
    # current shot 의 entity set
    current_entities = set()
    for var in variations:
        prompt = var.get("t2i_prompt")
        if not isinstance(prompt, str):
            measurement_failures.append(f"s{si}_sh{shi}: t2i_prompt missing")
            continue
        current_entities |= extract_entity_set(prompt)
    # new entities (current - prior)
    new_entities = current_entities - prior_entities
    if new_entities:
        zoom_new_entity_count += len(new_entities)
        zoom_new_entity_per_shot[f"{si}_{shi}"] = sorted(new_entities)
```

**출력 JSON schema (`ref_usage` field 포함):**
```json
{
  "timestamp": "ISO8601",
  "prompt_version": "...",
  "pinning": {...},
  "scene_set": [...],
  "metrics": {
    "total_zoom_in_detail_shots": 0,
    "zoom_in_detail_new_entity_count": 0,
    "zoom_in_detail_new_entity_per_shot": {}
  },
  "measurement_failures": [],
  "measurement_scripts": {
    "zoom_in_detail_no_new_entity": "scripts/canary/g4_4_zoom_in_detail_no_new_entity.py (zoom_in_detail shots only)"
  }
}
```

**exit criteria threshold (spec §5.1 strict):**
- **STRICT 0**: `candidate.metrics.zoom_in_detail_new_entity_count == 0`.

---

### Task 4.3 — `g4_4_atmosphere_no_layout_import.py` (Override O-11 multi-ref filter)

- [ ] **File:** `scripts/canary/g4_4_atmosphere_no_layout_import.py` (신규 생성)
- [ ] **Dependency:** Task 1.1 / 1.2
- [ ] **Spec cross-reference:** spec §5.1 (atmosphere row) / §5.3 / Override O-11 / O-17
- [ ] **Expected diff size:** ~145 lines (신규)

**스크립트 역할:**

Override O-11 multi-ref filter — `any(ref.ref_usage == 'atmosphere_reference' for ref in previous_shot_refs)` 인 shot scope. multi-ref 시 atmosphere check 만 적용 (`exact_background` co-presence 시 false-positive baseline 비0 → P1 follow-up 추적). `_CONTINUITY_FURNITURE_LAYOUT_TOKENS` 검출.

**module constants + 검출:**
```python
import re
from app.core.steps.render_prompt_card import (
    _CONTINUITY_FURNITURE_LAYOUT_TOKENS,
    compute_continuity_snapshot_hash,
)


def find_layout_imports(prompt: str) -> list:
    """7 furniture/layout token 중 prompt 안 등장 list 반환."""
    found = []
    prompt_lower = prompt.lower()
    for token in _CONTINUITY_FURNITURE_LAYOUT_TOKENS:
        if token.lower() in prompt_lower:
            found.append(token)
    return found
```

**Override O-11 multi-ref filter:**
```python
for scene in scenes:
    si = scene.get("scene_index")
    shi = scene.get("_shot_index")
    if (si, shi) not in pinning_scene_set:
        continue
    variations = _validate_scene_preflight(scene, si, shi, measurement_failures)
    if variations is None:
        continue
    rpc = scene["render_prompt_card"]
    ce = rpc["continuity_elements_used"]
    # silent-fallback ban (Trap #1, R3-B1) — explicit key existence + type validation.
    if "previous_shot_refs" not in ce:
        measurement_failures.append(
            f"s{si}_sh{shi}: continuity_elements_used.previous_shot_refs key missing"
        )
        continue
    refs = ce["previous_shot_refs"]
    if not isinstance(refs, list):
        measurement_failures.append(
            f"s{si}_sh{shi}: continuity_elements_used.previous_shot_refs not list"
        )
        continue
    # Override O-11 — multi-ref filter
    if not any(r.get("ref_usage") == "atmosphere_reference" for r in refs):
        continue  # atmosphere_reference 부재 — scope 외
    has_exact_bg_co_presence = any(
        r.get("ref_usage") == "exact_background" for r in refs
    )
    total_atmosphere_shots += 1
    if has_exact_bg_co_presence:
        atmosphere_multi_ref_shots += 1
        # P1 follow-up 추적 — false-positive 후보
    shot_count = 0
    for vi, var in enumerate(variations):
        prompt = var.get("t2i_prompt")
        if not isinstance(prompt, str):
            measurement_failures.append(f"s{si}_sh{shi}_v{vi}: t2i_prompt missing")
            continue
        layouts = find_layout_imports(prompt)
        shot_count += len(layouts)
    atmosphere_layout_import_count += shot_count
    atmosphere_layout_import_per_shot[f"{si}_{shi}"] = {
        "count": shot_count,
        "exact_bg_co_present": has_exact_bg_co_presence,
    }
```

**출력 JSON schema:**
```json
{
  "timestamp": "ISO8601",
  "prompt_version": "...",
  "pinning": {...},
  "scene_set": [...],
  "metrics": {
    "total_atmosphere_reference_shots": 0,
    "atmosphere_multi_ref_shots": 0,
    "atmosphere_layout_import_count": 0,
    "atmosphere_layout_import_per_shot": {}
  },
  "measurement_failures": [],
  "measurement_scripts": {
    "atmosphere_no_layout_import": "scripts/canary/g4_4_atmosphere_no_layout_import.py (atmosphere_reference shots only — Override O-11 multi-ref filter)"
  }
}
```

**exit criteria threshold (spec §5.1 strict):**
- **STRICT 0**: `candidate.metrics.atmosphere_layout_import_count == 0`.
- multi-ref shot 의 false-positive 는 P1 follow-up 추적 (`atmosphere_multi_ref_shots > 0` 시 별도 issue).

**Override O-17 carry — regex brittleness:**
- furniture token list (`_CONTINUITY_FURNITURE_LAYOUT_TOKENS`) 는 1차 detector. baseline 비0 시 P1 follow-up 으로 LLM-validator (gpt-5.4-mini judge) 전환 검토.
- false-positive 예: "the wall position is preserved" 자연스러운 묘사 — LLM-validator 가 의미 판단 가능.

---

### Task 4.4 — `g4_4_view_mixing.py` (모든 shot scope, cross-import G4.3 _ID_BODY_PART_TRIGGERS)

- [ ] **File:** `scripts/canary/g4_4_view_mixing.py` (신규 생성)
- [ ] **Dependency:** Task 1.1 / 1.2 + G4.3 `_ID_BODY_PART_TRIGGERS` cross-import
- [ ] **Spec cross-reference:** spec §5.1 (view-mixing row) / §5.3 / Override O-17
- [ ] **Expected diff size:** ~165 lines (신규)

**스크립트 역할:**

모든 shot scan. 같은 인물 ID 의 전신/상반신 묘사 (`stands` / `seated` / `leaning` 류) + 같은 인물의 body-part close-up (`focus on` / `close on` / `tight on` / `detail on` + body-part) 동시 등장 검출.

**module constants + cross-import:**
```python
import re
from app.core.steps.render_prompt_card import (
    _ID_BODY_PART_TRIGGERS,  # G4.3 cross-import — 4 trigger alternation
    compute_continuity_snapshot_hash,
)

# G4.4 ground-truth: full-body / partial-body person verbs (R1+R2 audit 시 ground-truth)
_VIEW_MIXING_FULL_BODY_VERBS: tuple = (
    "stands",
    "seated",
    "leaning",
    "walking",
    "running",
    "kneeling",
    "lying",
)

# 4 trigger alternation pattern (G4.3 R1R2-B2 carry)
_TRIGGER_ALT = "|".join(re.escape(t) for t in _ID_BODY_PART_TRIGGERS)
BODY_PART_FOCUS_PATTERN = re.compile(
    r"\b(?:" + _TRIGGER_ALT + r")\s+C\d{2}(?:O\d{2})?'s\s+\w+",
    re.IGNORECASE,
)
ID_REGEX = re.compile(r"\bC\d{2}(?:O\d{2})?\b")
WINDOW_CHARS = 50
```

**핵심 로직 — same-character full + close-up co-occurrence:**

```python
def detect_view_mixing(prompt: str) -> list:
    """returns list of violations.

    각 violation = {id_token, full_body_position, body_part_position}
    same id 의 full-body verb + body-part trigger 동시 등장 시 violation.
    """
    violations = []
    prompt_lower = prompt.lower()

    # ID positions
    id_positions = {}  # id_token → list of (start, end)
    for match in ID_REGEX.finditer(prompt):
        id_token = match.group()
        id_positions.setdefault(id_token, []).append((match.start(), match.end()))

    # body-part focus matches
    bp_focus_matches = []
    for match in BODY_PART_FOCUS_PATTERN.finditer(prompt):
        # extract id token from match
        id_match = ID_REGEX.search(match.group())
        if id_match:
            bp_focus_matches.append({
                "id_token": id_match.group(),
                "start": match.start(),
                "end": match.end(),
            })

    # full-body verb matches — id 의 ±50 chars window 안 verb
    for id_token, positions in id_positions.items():
        for id_start, id_end in positions:
            win_start = max(0, id_start - WINDOW_CHARS)
            win_end = min(len(prompt), id_end + WINDOW_CHARS)
            window = prompt_lower[win_start:win_end]
            has_full_body = any(v in window for v in _VIEW_MIXING_FULL_BODY_VERBS)
            if has_full_body:
                # body-part close-up of same id token elsewhere?
                for bp in bp_focus_matches:
                    if bp["id_token"] == id_token and not (
                        bp["start"] >= id_start - WINDOW_CHARS
                        and bp["end"] <= id_end + WINDOW_CHARS
                    ):
                        violations.append({
                            "id_token": id_token,
                            "full_body_position": [id_start, id_end],
                            "body_part_position": [bp["start"], bp["end"]],
                        })
    return violations
```

**중요 — sentence boundary regex 사용 금지 (`feedback_no_regex_postprocessing.md` carry):**
- `WINDOW_CHARS = 50` char-level proximity — proximity 기반 결합 판정.
- LLM 의 punctuation 가변성 — sentence split 으로 결합 검사 시 false-negative.

**출력 JSON schema:**
```json
{
  "timestamp": "ISO8601",
  "prompt_version": "...",
  "pinning": {...},
  "scene_set": [...],
  "metrics": {
    "total_shots": 0,
    "view_mixing_count": 0,
    "view_mixing_per_shot": {},
    "violations_detail": []
  },
  "measurement_failures": [],
  "measurement_scripts": {
    "view_mixing": "scripts/canary/g4_4_view_mixing.py (all shots, cross-import G4.3 _ID_BODY_PART_TRIGGERS)"
  }
}
```

**exit criteria threshold (spec §5.1 strict):**
- **STRICT 0**: `candidate.metrics.view_mixing_count == 0`.

**Override O-17 carry — regex brittleness:**
- false-positive 예: "C01O02 stands in profile, hand resting on the table" — full-body + body-part 자연스러운 부분 묘사 (close-up 아님).
- baseline 비0 시 LLM-validator (gpt-5.4-mini judge) 전환 P1 follow-up.

---

### Task 4.5 — `g4_4_token_count.py` (tiktoken cl100k_base + system+inject 합산)

- [ ] **File:** `scripts/canary/g4_4_token_count.py` (신규 생성)
- [ ] **Dependency:** Task 2.1 / 2.2 (v19 prompt 존재해야 candidate 측정)
- [ ] **Spec cross-reference:** spec §5.1 token row / Override O-1
- [ ] **Expected diff size:** ~110 lines (신규)

**스크립트 역할:**

`prompts/_base/scene_detail/<version>/system.md` 를 `tiktoken cl100k_base` 로 인코딩, token 수 실측. v18 vs v19 delta 계산. Override O-1 binding gate `≤ -540 system-only` 판정. system+inject 합산 (≤ -150) = secondary report-only metric.

**출력 JSON schema (G4.2 plan-R2-I4 + G4.3 carry — pinning 7 field 포함):**
```json
{
  "timestamp": "ISO8601",
  "pinning": {
    "pid": "...",
    "scene_index_list": [],
    "shot_index_list_per_scene": {},
    "model_routing": "...",
    "prompt_source_mode": "file",
    "card_commit_hash": "...",
    "continuity_card_snapshot_hash": "..."
  },
  "baseline_prompt_version": "18.202605041549",
  "candidate_prompt_version": "19.<timestamp>",
  "baseline_token_count": 0,
  "candidate_token_count": 0,
  "token_delta": 0,
  "exit_criteria_threshold": -540,
  "exit_criteria_pass": false,
  "system_plus_inject_baseline": 0,
  "system_plus_inject_candidate": 0,
  "system_plus_inject_delta": 0,
  "system_plus_inject_secondary_threshold": -150,
  "system_plus_inject_secondary_pass": false,
  "measurement_tool": "tiktoken cl100k_base",
  "measurement_failures": []
}
```

**measurement_failures (R3-B1 carry):** prompt 파일 부재 / version mismatch / encoding 실패 시 list 에 누적 후 `sys.exit(1)`. silent skip 금지 — 모든 실패 사례 명시 기록.

**CLI args:**
```bash
python scripts/canary/g4_4_token_count.py \
  --baseline-version 18.202605041549 \
  --candidate-version 19.<timestamp> \
  --pid <PID> \
  --scene-index-list "[3,7,12,18,24]" \
  --shot-index-list-per-scene '{"3":[1,2],"7":[4],"12":[4],"18":[3],"24":[1,5]}' \
  --model-routing gemini-3.1-pro-preview \
  --prompt-source-mode file \
  --card-commit-hash 51d6c5e \
  --continuity-card-snapshot-hash <hash> \
  --output docs/canary/g4_4_token_count_$(date +%Y%m%d%H%M).json
```

또는 `--config <json-file>` 로 한 번에 받음 (G4.2 + G4.3 carry).

**exit criteria threshold (Override O-1):**
- `token_delta = candidate_token_count - baseline_token_count` (음수 expected).
- `token_delta <= -540` → pass (system-only binding gate).
- `system_plus_inject_delta <= -150` → secondary report-only (no gate, JSON 보조 기록만).

---

## Phase 5 (Wave 4): unit + integration + alignment tests

**Phase 목표:** `backend/tests/unit/test_g4_4_continuity_lift.py` (~46 unit, table 46 enumerate) + `backend/tests/integration/test_g4_4_continuity_integration.py` (~17 integration) + alignment test 4-point sync 갱신. G4.3 baseline 대비 0 regressions.

**Phase 5 expected total test count delta:** **+~63 minimum** (46 unit + 17 integration). regression suite 가 위에 alignment test pass 도 보존.

**Wave subagent 함정 경고 (G4.1 / G4.2 / G4.3 carry):**
- **절대 금지**: fixture 의 hardcoded empty inputs (`fixed_elements=[]`, `previous_shot_refs=[]`) 도입 — production drift cascade. 반드시 `_DEFAULT_SENTINEL = object()` pattern + `is _DEFAULT_SENTINEL` 비교.
- **반드시**: 각 unit test 가 실제 `build_continuity_elements_used()` / `build_render_prompt_card()` 를 call (mock 금지). hardcoded result dict 사용 금지.
- **반드시**: required_keys 검증은 Override O-5 superset (`assert set(returned) >= EXPECTED_SET`). set equality (`==`) 사용 금지.

---

### Task 5.1 — unit tests (~46 tests)

- [ ] **File:** `backend/tests/unit/test_g4_4_continuity_lift.py` (신규 생성)
- [ ] **Dependency:** Phase 1 완료 (Task 1.1 ~ 1.5)
- [ ] **Spec cross-reference:** spec §6.1 / Override O-5 / O-6 / O-7 / O-9 / O-11 / O-16
- [ ] **Expected diff size:** ~720 lines (신규 — 4+4 shape validator tests R4-I3 carry)
- [ ] **Expected test count:** **~46** (table 46 enumerate, no buffer needed — Override O-16 갱신: plan-R3+R4 audit I3 시 4+4 nested shape validator tests 명시 enumerate. 38-46 range 도 ambiguity acceptable 시 OK)

**test list (spec §6.1 + Override carry — content selector pattern, set superset):**

| # | test name | assert |
|---|---|---|
| 1 | `test_continuity_has_cross_shot_id_substitution_rule` | `cross_shot_id_substitution_rule` dict 존재 + 6 required key superset |
| 2 | `test_continuity_cross_shot_id_substitution_rule_required_keys_superset` | `assert set(returned_keys) >= {"applies_when", "substitution", "double_description_forbidden", "preserve_pose_unchanged", "no_repeat_after_pose", "rationale_summary"}` (Override O-5 superset, NOT set equality) |
| 3 | `test_continuity_cross_shot_id_substitution_rule_substitution_substring` | `substitution` 안 `"replace the common-noun person reference"` substring |
| 4 | `test_continuity_cross_shot_id_substitution_rule_no_repeat_after_pose_substring` | `no_repeat_after_pose` 안 `"별도 문장으로 반복하지 마라"` substring |
| 5 | `test_continuity_has_ref_usage_constraints` | `ref_usage_constraints` dict + 3 sub-key superset (`zoom_in_detail` / `exact_background` / `atmosphere_reference`) |
| 6 | `test_continuity_ref_usage_zoom_in_detail_required_keys_superset` | `assert set(returned_keys) >= {"scope_summary", "forbidden_additions", "required_phrasings", "body_part_cross_ref", "rationale_summary"}` |
| 7 | `test_continuity_ref_usage_zoom_in_detail_forbidden_additions_count` | `len(forbidden_additions) == _CONTINUITY_ZOOM_FORBIDDEN_ADDITIONS_COUNT` (= 5) |
| 8 | `test_continuity_ref_usage_zoom_in_detail_required_phrasings_count` | `len(required_phrasings) == _CONTINUITY_ZOOM_REQUIRED_PHRASINGS_COUNT` (= 3) |
| 9 | `test_continuity_ref_usage_zoom_in_detail_body_part_cross_ref_string_present` | Override O-7 carry — `body_part_cross_ref` 안 `"id_policy.body_part_focus_rule"` substring |
| 10 | `test_continuity_ref_usage_exact_background_required_keys_superset` | `assert set(returned_keys) >= {"scope_summary", "permitted_additions", "background_consistency_rule", "ignore_keep_handled_elsewhere", "rationale_summary"}` |
| 11 | `test_continuity_ref_usage_exact_background_permitted_additions_count` | `len(permitted_additions) == _CONTINUITY_EXACT_PERMITTED_ADDITIONS_COUNT` (= 2) |
| 12 | `test_continuity_ref_usage_atmosphere_reference_required_keys_superset` | `assert set(returned_keys) >= {"scope_summary", "forbidden_imports", "required_handling", "rationale_summary"}` |
| 13 | `test_continuity_ref_usage_atmosphere_reference_forbidden_imports_count` | `len(forbidden_imports) == _CONTINUITY_ATMOSPHERE_FORBIDDEN_IMPORTS_COUNT` (= 3) |
| 14 | `test_continuity_has_view_consistency` | `view_consistency` dict + 9 required key superset |
| 15 | `test_continuity_view_consistency_required_keys_superset` | `assert set(returned_keys) >= {"framing_scope_options", "single_camera_rule", "third_person_handling", "close_up_handling", "focus_on_is_focus_area_not_view_switch", "id_policy_cross_ref_for_body_part_focus", "prop_orientation_rule", "mixing_forbidden", "rationale_summary"}` |
| 16 | `test_continuity_view_consistency_framing_scope_options_exact` | `view_consistency.framing_scope_options == list(_CONTINUITY_FRAMING_SCOPE_OPTIONS)` (정확 2 entry list equality — 신규 sub-field 중 유일하게 list equality 강제) |
| 17 | `test_continuity_view_consistency_mixing_forbidden_is_bool_true` | `mixing_forbidden is True and type(mixing_forbidden) is bool` (G3.2/G4.3 bool int subclass trap carry) |
| 18 | `test_continuity_view_consistency_id_policy_cross_ref_string_present` | Override O-7 carry — `id_policy_cross_ref_for_body_part_focus` 안 `"id_policy.body_part_focus_rule"` substring |
| 19 | `test_continuity_cross_card_pair_substrings_match` | Override O-7 paired drift 차단 — view_consistency 의 `id_policy_cross_ref_for_body_part_focus` AND ref_usage_constraints.zoom_in_detail 의 `body_part_cross_ref` 모두 동일 literal `"id_policy.body_part_focus_rule"` substring 보유 |
| 20 | `test_continuity_view_consistency_focus_on_is_focus_area_not_view_switch_string_present` | `"area of focus"` + `"NOT a view switch"` substring |
| 21 | `test_continuity_view_consistency_prop_orientation_rule_string_present` | `prop_orientation_rule` 안 `"one direction only"` substring |
| 22 | `test_continuity_constraints_grew_to_seven_strings` | `len(constraints) == 7` (G4.1 base 2 → G4.4 7) |
| 23 | `test_continuity_constraints_string_1_fixed_binding_carry` | `constraints[0] == "fixed_elements are continuity inputs derived from prior shots — treat them as binding context, not as source facts to be invented"` (G4.1 carry verbatim string equality) |
| 24 | `test_continuity_constraints_string_2_cross_shot_substitution_inline` | constraints[1] 안 `"replace the common-noun person reference"` + `"do not also write a separate common-noun"` substring |
| 25 | `test_continuity_constraints_string_3_zoom_in_detail_inline` | constraints[2] 안 `"ref_usage='zoom_in_detail'"` + `"no new persons, props, background"` substring |
| 26 | `test_continuity_constraints_string_4_exact_background_inline` | constraints[3] 안 `"ref_usage='exact_background'"` + `"keep background elements consistent"` substring |
| 27 | `test_continuity_constraints_string_5_atmosphere_reference_inline` | constraints[4] 안 `"ref_usage='atmosphere_reference'"` + `"do not import furniture layout"` substring |
| 28 | `test_continuity_constraints_string_6_single_camera_inline` | constraints[5] 안 `"one t2i_prompt = one camera position"` + `"id_policy.body_part_focus_rule"` substring (Override O-7 cross-card paired) |
| 29 | `test_continuity_constraints_string_7_prop_orientation_inline` | constraints[6] 안 `"one direction only"` substring |
| 30 | `test_continuity_fixed_elements_none_raises` | `fixed_elements=None` → pytest.raises(AppError) (G4.1/G4.2/G4.3 silent-absorb ban carry, Override O-8) |
| 31 | `test_continuity_previous_shot_refs_none_raises` | `previous_shot_refs=None` → AppError |
| 32 | `test_continuity_forward_zoom_targets_none_raises` | `forward_zoom_targets=None` → AppError |
| 33 | `test_continuity_fixed_elements_empty_list_valid_with_static_subfields` | `fixed_elements=[]` + `previous_shot_refs=[]` + `forward_zoom_targets=[]` → no raise + `assert "cross_shot_id_substitution_rule" in result and "ref_usage_constraints" in result and "view_consistency" in result and len(result["constraints"]) == 7` (R2-I4 from G4.3 carry — builder-static 확보) |
| 34 | `test_continuity_4_subfields_independent_of_input_lists` | hardcoded `[]` 입력 (Wave subagent fixture trap 회피 carry) → cross_shot_id_substitution_rule / ref_usage_constraints / view_consistency / constraints 4 모두 builder-static 출력 (Override O-9 invariant — `if previous_shot_refs:` 류 truthy guard 안 배치 절대 금지). 모든 sub-field deep equality vs ground-truth |
| 35 | `test_g4_4_view_mixing_canary_multi_ref_shot_filter` | Override O-11 carry — atmosphere_reference + exact_background co-presence multi-ref shot fixture 가 `any(r.ref_usage == 'atmosphere_reference' for r in previous_shot_refs)` filter 에 포함되는지 검증 |
| 36 | `test_card_metadata_lift_status_includes_continuity_keys` | `_card_metadata.lift_status` 4 신규 key 정확 명 (Override O-9): `continuity_cross_shot_id_substitution_lifted=True` + `continuity_ref_usage_constraints_lifted=True` + `continuity_view_consistency_lifted=True` + `continuity_constraints_extended=True` 모두 present |
| 37 | `test_card_metadata_rule_source_includes_continuity_keys` | `_card_metadata.rule_source` 3 신규 key (`cross_shot_id_substitution_rule` / `ref_usage_rule` / `view_consistency_rule`) 모두 present |
| 38 | `test_card_metadata_excluded_from_hash_after_g4_4_keys_added` | G4.2 R1-B1 + G4.3 R1-I11 carry — 4 신규 lift_status key 추가 후에도 `compute_card_hash()` 결과 변경 0. canonical name (longer descriptive form) 고정 — content selector pattern, index 의존 금지 |
| 39 | `test_assert_continuity_shape_validates_cross_shot_id_substitution_rule_shape` | `_assert_continuity_elements_used_shape()` 가 cross_shot_id_substitution_rule dict 누락 시 `AppError(code="step.contract_violation")` raise + 6 required keys (`applies_when` / `substitution` / `double_description_forbidden` / `preserve_pose_unchanged` / `no_repeat_after_pose` / `rationale_summary`) 중 1개라도 missing 시 raise (spec §6.1 carry — Override O-5 superset 차단 검증) |
| 40 | `test_assert_continuity_shape_validates_ref_usage_constraints_shape` | `_assert_continuity_elements_used_shape()` 가 ref_usage_constraints dict 누락 / 3 sub-key (zoom_in_detail / exact_background / atmosphere_reference) 중 1개 missing / nested required keys missing 시 모두 AppError raise (Risk Register R3-I3 nested validator carry) |
| 41 | `test_assert_continuity_shape_validates_view_consistency_shape` | `_assert_continuity_elements_used_shape()` 가 view_consistency dict 누락 시 AppError raise (Override O-5 superset 차단 검증 — top-level dict presence) |
| 42 | `test_assert_continuity_shape_validates_view_consistency_required_keys` | `_assert_continuity_elements_used_shape()` 가 view_consistency 9 required keys (`framing_scope_options` / `single_camera_rule` / `third_person_handling` / `close_up_handling` / `focus_on_is_focus_area_not_view_switch` / `id_policy_cross_ref_for_body_part_focus` / `prop_orientation_rule` / `mixing_forbidden` / `rationale_summary`) 중 1개라도 missing 시 raise (Override O-5 superset) |
| 43 | `test_assert_ref_usage_constraints_shape_zoom_in_detail_required_keys` | `_assert_ref_usage_constraints_shape()` nested helper 가 zoom_in_detail 5 required keys (`scope_summary` / `forbidden_additions` / `required_phrasings` / `body_part_cross_ref` / `rationale_summary`) 중 1개 missing 시 AppError raise (Risk Register R3-I3 nested validator) |
| 44 | `test_assert_ref_usage_constraints_shape_exact_background_required_keys` | `_assert_ref_usage_constraints_shape()` nested helper 가 exact_background 5 required keys (`scope_summary` / `permitted_additions` / `background_consistency_rule` / `ignore_keep_handled_elsewhere` / `rationale_summary`) 중 1개 missing 시 AppError raise |
| 45 | `test_assert_ref_usage_constraints_shape_atmosphere_reference_required_keys` | `_assert_ref_usage_constraints_shape()` nested helper 가 atmosphere_reference 4 required keys (`scope_summary` / `forbidden_imports` / `required_handling` / `rationale_summary`) 중 1개 missing 시 AppError raise |
| 46 | `test_assert_ref_usage_constraints_shape_invalid_sub_key_raises` | `_assert_ref_usage_constraints_shape()` 가 ref_usage_constraints sub-key 가 dict 아닐 때 (예: `ref_usage_constraints["zoom_in_detail"] = None` / `[]` / `"string"`) AppError raise — silent absorb 차단 (`feedback_no_silent_fallback.md` carry) |

총 unit tests: **~46** (table 46 enumerate, no buffer needed — Override O-16 갱신. Risk Register R3-I3 nested validator + Override O-5 superset 차단 4+4 shape validator tests 명시 enumerate. plan-R3+R4 audit 결정. 38-46 range 도 buffer ambiguity acceptable 시 OK).

**Fixture helper section (M4 carry — `_make_full_card()` definition):**

Wave 4 Task 5.1 의 모든 unit test 가 `_make_full_card()` helper 를 통해 G4.4 신규 sub-field 가 모두 채워진 fully-populated card 를 생성한다. 본 helper 는 G4.3 test file (`backend/tests/unit/test_g4_3_id_policy_lift.py`) 에 carry 패턴이 이미 존재할 수 있으므로 **재사용 우선** — carry 못 하면 inline 정의.

```python
# backend/tests/unit/test_g4_4_continuity_lift.py top
# Override O-2 / O-17 carry from G4.2 — _DEFAULT_SENTINEL pattern.
# G4.1 R3-B2 / G4.2 plan-R1-I4 / G4.3 plan-R1-I4 / R4-M6 carry — fixture trap 회피.

_DEFAULT_SENTINEL = object()


def _make_full_card(
    *,
    fixed_elements=_DEFAULT_SENTINEL,
    previous_shot_refs=_DEFAULT_SENTINEL,
    forward_zoom_targets=_DEFAULT_SENTINEL,
    # ... 7 envelope fields with G4.4-aware sentinel-aware defaults
) -> Dict[str, Any]:
    """Returns fully-populated render_prompt_card with all G4.1+G4.2+G4.3+G4.4
    sub-fields present.

    sentinel-aware defaults: 각 인자 default = `_DEFAULT_SENTINEL`. caller 가
    explicit None / [] 전달 시 None / [] valid (test trap 차단). caller 가 인자
    생략 시 sentinel → realistic non-empty default 로 치환 (production-shaped
    fixture 보장 — Wave subagent fixture trap #14 차단).

    Reuse priority:
      1. G4.3 test file (`test_g4_3_id_policy_lift.py`) 에 동일 helper 가 carry
         되어 있으면 그대로 import 후 G4.4 sub-field 가 추가된 확장 helper 작성
         (carry preferred).
      2. G4.3 helper 부재 시 본 file 안 inline 정의 — 7 envelope fields 모두
         sentinel-aware defaults 로 채움.
    """
```

**중요 — G4.3 helper carry**:
- 작업 시작 전 `grep -n "_make_full_card\|def _build_full" backend/tests/unit/test_g4_3_id_policy_lift.py` 실행 → carry pattern 확인.
- carry 가능: G4.3 helper import 후 G4.4 신규 sub-field (cross_shot_id_substitution_rule / ref_usage_constraints / view_consistency / extended constraints) 만 G4.4 test file 에서 inject.
- carry 불가: 본 G4.4 test file 안 inline 정의. G4.1+G4.2+G4.3+G4.4 모든 sub-field 가 realistic non-empty default 로 채워지도록 sentinel-aware default args 명시.

**Override O-17 carry (`_DEFAULT_SENTINEL` pattern)**:
- default arg + None / [] / default 셋 명시 구분 — `_DEFAULT_SENTINEL = object()` + `is _DEFAULT_SENTINEL` 비교.
- G4.2 plan-R1-I4 / G4.1 R3-B2 / G4.3 plan-R1-I4 / R4-M6 carry — Wave 4 fixture trap 차단.

---

### Task 5.2 — integration tests (~17 tests)

- [ ] **File:** `backend/tests/integration/test_g4_4_continuity_integration.py` (신규 생성)
- [ ] **Dependency:** Phase 1 + Phase 2 + Phase 3 완료 (Wave 1-A producer + Wave 1-B v19 prompt + Wave 2 wiring)
- [ ] **Spec cross-reference:** spec §6.2 / Override O-2 / O-3 / O-7 / O-14 / G4.3 plan-R1R2-Q1 carry
- [ ] **Expected diff size:** ~480 lines (신규)
- [ ] **Expected test count:** **~17**

**test list (spec §6.2 + Override carry — substring presence/absence + JSON inject path):**

| # | test name | assert |
|---|---|---|
| 1 | `test_v18_system_prompt_has_section_a_cross_shot_heading` | v18 system.md (`prompts/_base/scene_detail/18.202605041549/system.md`) 에 `"## 교차 샷 고정 요소 통합 규칙"` heading present (baseline 보장 — v19 가 실제 삭제하는지 검증 가능) |
| 2 | `test_v18_system_prompt_has_section_b_ref_usage_heading` | v18 system.md 에 `"## 앞쪽 참조 샷 처리 — ref_usage 유형별 지시"` heading present |
| 3 | `test_v18_system_prompt_has_section_c_single_view_heading` | v18 system.md 에 `"## 절대 규칙 — 단일 시점"` heading present |
| 4 | `test_v19_system_prompt_missing_section_a` | v19 system.md 에 `"## 교차 샷 고정 요소 통합 규칙"` heading absent (Section A 삭제 검증 — Override O-10 inclusive 행) |
| 5 | `test_v19_system_prompt_missing_section_b` | v19 system.md 에 `"## 앞쪽 참조 샷 처리 — ref_usage 유형별 지시"` heading absent (Section B 삭제 검증) |
| 6 | `test_v19_system_prompt_missing_section_c` | v19 system.md 에 `"## 절대 규칙 — 단일 시점"` heading absent (Section C 삭제 검증) |
| 7 | `test_v19_system_prompt_has_continuity_compact_heading` | v19 system.md 에 `"## Continuity (cross-shot substitution + ref_usage + single-view)"` heading present |
| 8 | `test_v19_continuity_section_line_count_under_32` | Override O-2 binding gate — `## Continuity` heading 부터 next `## ` heading 직전까지 `wc -l` ≤ **32** lines strict (wrap 줄도 line 1 카운트 — 80-col wrap 발생 시 violation) |
| 9 | `test_v19_system_md_total_lines_under_550` | Override O-14 도출식 — v18 baseline 587 lines − 32 (compact gate O-2) − 보수 buffer 5 = **550 lines 미만** strict (`wc -l prompts/_base/scene_detail/19.<timestamp>/system.md` < 550) |
| 10 | `test_v19_id_policy_section_preserved_unchanged` | v19 system.md 에 `"## ID Policy"` heading present (G4.3 carry — G4.4 가 변경 0 보장) |
| 11 | `test_v19_background_binding_section_preserved_unchanged` | v19 system.md 에 `"## Background Binding"` heading present (G4.2 carry — G4.4 가 변경 0 보장) |
| 12 | `test_detail_steps_zoom_in_detail_inline_prose_absent_after_g4_4` | Override O-3 — `backend/app/core/steps/detail_steps.py` 안 `"[중요 — 확대 샷 원칙 (zoom_in_detail)]"` substring **absent** + `"새 인물·소품·자세 추가 금지"` substring **absent** + `"if has_zoom_in_detail:"` 9 줄 prepend 블록 절대 부재 (Wave 2 Task 3.3 deletion 검증) |
| 13 | `test_detail_steps_has_zoom_in_detail_variable_no_dead_uses` | Override O-3 maintainer carry — `grep -n "has_zoom_in_detail" backend/app/core/steps/detail_steps.py` 결과가 line 1555-1563 외 사용처 0 + 변수 정의 함께 정리 (dead code 0) |
| 14 | `test_continuity_card_to_scene_detail_user_prompt_inject_3_sibling_dicts` | end-to-end — `_collect_card_inputs()` + `build_render_prompt_card()` + `_card_metadata` strip + JSON prepend (`detail_steps.py:1500-1610`) 경로 통해 user_prompt JSON 안 `continuity_elements_used.cross_shot_id_substitution_rule` + `continuity_elements_used.ref_usage_constraints` + `continuity_elements_used.view_consistency` 3 sibling policy dict 모두 present |
| 15 | `test_continuity_card_to_scene_detail_user_prompt_inject_constraints_7_strings` | 동일 경로 통해 user_prompt JSON 안 `continuity_elements_used.constraints` len == 7 + constraint[0] G4.1 verbatim string equality + constraint[1..6] substring 검증 (Wave 1-A Task 1.3 출력과 1:1) |
| 16 | `test_continuity_card_to_scene_detail_user_prompt_inject_card_metadata_stripped` | G4.2 14d14cb + G4.3 51d6c5e carry — `_card_metadata` 는 envelope **top-level sibling** (NOT nested under `continuity_elements_used`). user_prompt JSON 안 top-level `_card_metadata` key **absent**: `parsed_card = json.loads(injected_user_prompt_json_block); assert "_card_metadata" not in parsed_card` (`canonicalize_render_prompt_card()` top-level pop 검증 — Override O-10 carry). |
| 17 | `test_cross_card_paired_substring_in_v19_system_md_and_card_inject` | Override O-7 cross-card paired ref drift 차단 — v19 `## Continuity` section 안 `"id_policy.body_part_focus_rule"` substring 1+ 등장 AND user_prompt JSON 안 `continuity_elements_used.view_consistency.id_policy_cross_ref_for_body_part_focus` substring + `continuity_elements_used.ref_usage_constraints.zoom_in_detail.body_part_cross_ref` substring 모두 동일 literal 보유 |
| 18 | `test_token_delta_v18_to_v19_system_md_under_neg_540` | Override O-1 binding gate fixture-based — tiktoken cl100k_base 실측 v19 system.md token count − v18 system.md token count `<= -540` strict (signed delta convention — `feedback_no_silent_fallback.md` 류 silent absorb 0). `pytest.fixture(scope="module")` 로 한 번만 측정 |
| 19 | `test_v18_checkpoint_resume_on_v19_force_escalates` | G4.1 R1-B1 + G4.2 + G4.3 carry pattern — v18 cp (schema_version=7, prompt_version=18.202605041549) 를 v19 consumer 로 resume → `_config_hash` mismatch 발생 → `mode="force"` escalate (force escalate path 가 v18→v19 에 동일 적용) |
| 20 | `test_g3_2_sentinel_g4_2_g4_3_coexist_with_g4_4_continuity` | 임의 shot → card 안 G3.2 variation-level `owned_validation` sentinel + G4.2 `background_binding.constraints` + G4.3 `id_policy` 5 sub-field + G4.4 `continuity_elements_used` 3 sibling policy dicts + 7 constraints 모두 CP 에 존재 (전체 G3.2~G4.4 carry 회귀 0 확인) |

총 integration tests: **~17** (위 20 row 중 일부 (1/2/3 baseline 보장 류) 통합 가능 — 최소 17 strict).

**중요 (G4.3 plan carry — set equality + len equality):**

- 모든 ground-truth 검증은 `set equality + len equality` (G4.2 R2-B4 carry — substring 검증 절대 금지). 단, v18/v19 prompt presence/absence 류는 substring 허용 (test 1-7).
- test 14 / 15 의 user_prompt 검증은 실제 `_collect_card_inputs()` + `build_render_prompt_card()` + JSON prepend pipeline 을 call (mock 금지 — G4.1 / G4.2 / G4.3 trap carry).
- test 17 cross-card paired substring 은 `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` 모듈 상수 (Wave 1-A Task 1.1) single source — drift 시 unit test 19 + integration test 17 모두 fail.

---

### Task 5.3 — alignment test 4-point sync 갱신

- [ ] **File:** alignment test 파일 (find via `grep -rn "test_scene_detail.*alignment\|SCENE_DETAIL_PROMPT_VERSION" backend/tests/`) — G4.3 commit `51d6c5e` 가 추가/갱신한 `backend/tests/prompts/test_scene_detail_id_policy_alignment.py` 또는 `backend/tests/prompts/test_scene_detail_alignment.py` 류
- [ ] **Dependency:** Phase 3 완료 (Task 3.1 + 3.2 + 3.3 — 4-point sync 적용 후)
- [ ] **Spec cross-reference:** spec §6.4 4-way sync / Override O-9 / G4.3 plan R3R4-B1 + R2-I7 carry / G4.2 plan-R2-I6 carry
- [ ] **Expected diff size:** ~14 lines (fixture / row 갱신 + lift_status 4 신규 key)

**변경 내용:**

기존 alignment test 의 latest version row 갱신 (G4.3 → G4.4 transition):

```python
# Before (G4.3 carry):
def test_scene_detail_prompt_version_constant_matches_latest():
    assert SCENE_DETAIL_PROMPT_VERSION == "18.202605041549"


def test_scene_detail_version_registry_aligned():
    assert MODULE_VERSIONS["scene_detail_composer"] == "1.18.0"
    # _MODULE_INFO 는 dict — attribute access 아님 (G4.3 R3R4-B1 carry)
    assert _MODULE_INFO["scene_detail_composer"]["prompt_dependency"] == "scene_detail/v18"


def test_scene_detail_schema_version_synced_with_manifest():
    # G4.3 carry — schema_version 변경 없음 (= 7)
    assert SCENE_DETAIL_SCHEMA_VERSION == 7


def test_card_metadata_lift_status_g4_keys():
    # G4.2 + G4.3 carry — lift_status 4 keys (id_policy_composite_lifted /
    # id_policy_close_framing_face_lifted / id_policy_reproduction_surface_lifted /
    # rule_h_lifted)
    card = build_render_prompt_card(...)  # 임의 fixture
    lift_status = card["_card_metadata"]["lift_status"]
    assert lift_status["id_policy_composite_lifted"] is True
    # ... 4 keys ...


# After (G4.4):
def test_scene_detail_prompt_version_constant_matches_latest():
    assert SCENE_DETAIL_PROMPT_VERSION == "19.<timestamp>"  # 디렉토리 timestamp 매칭 lockstep — `.wave_1b_timestamp.txt` read


def test_scene_detail_version_registry_aligned():
    assert MODULE_VERSIONS["scene_detail_composer"] == "1.19.0"  # G4.3 R2-I7 → G4.4 carry
    # G4.3 R3R4-B1 carry: dict access (NOT attribute) + major suffix only ("scene_detail/v19", NOT timestamp)
    assert _MODULE_INFO["scene_detail_composer"]["prompt_dependency"] == "scene_detail/v19"


def test_scene_detail_schema_version_synced_with_manifest():
    # G4.4 carry — schema_version 변경 없음 (= 7) — continuity_elements_used sub-field
    # 확장은 dict key 추가만, envelope shape 변경 0
    assert SCENE_DETAIL_SCHEMA_VERSION == 7


def test_card_metadata_lift_status_g4_keys_with_continuity():
    # G4.4 신규 4 keys (Override O-9 carry):
    # continuity_cross_shot_id_substitution_lifted / continuity_ref_usage_constraints_lifted /
    # continuity_view_consistency_lifted / continuity_constraints_extended
    card = build_render_prompt_card(...)  # 임의 fixture
    lift_status = card["_card_metadata"]["lift_status"]
    # G4.2 / G4.3 carry 4 keys 모두 보존
    assert lift_status["id_policy_composite_lifted"] is True
    assert lift_status["id_policy_close_framing_face_lifted"] is True
    assert lift_status["id_policy_reproduction_surface_lifted"] is True
    assert lift_status["rule_h_lifted"] is True
    # G4.4 신규 4 keys
    assert lift_status["continuity_cross_shot_id_substitution_lifted"] is True
    assert lift_status["continuity_ref_usage_constraints_lifted"] is True
    assert lift_status["continuity_view_consistency_lifted"] is True
    assert lift_status["continuity_constraints_extended"] is True
```

**주의 (G4.3 R3R4-B1 carry + Trap #13 silent fallback carry):**

- `_MODULE_INFO` 는 plain dict literal — attribute access (`.prompt_dependency`) 사용 시 NameError. 반드시 `["prompt_dependency"]` 사용 (dict access strict).
- **Trap #13 (silent fallback) carry**: `lift_status.get("continuity_*", "")` 류 fallback 절대 금지. 반드시 explicit dict access (`lift_status["continuity_*"]`) — key 부재 시 KeyError 로 fail-fast (`feedback_no_silent_fallback.md` carry). G3.2 / G4.2 / G4.3 패턴 답습.
- `prompt_dependency` 값 = `"scene_detail/v19"` (major suffix only — G4.3 R3R4-B1 carry). 기존 alignment infrastructure (`pdep.endswith(f"v{latest_major}")`) 가 `"v19"` suffix 검증.
- `SCENE_DETAIL_PROMPT_VERSION` 만 `"19.<timestamp>"` 형식 (Task 3.1 — 디렉토리 매칭). `prompt_dependency` 와 별도 source.
- `<timestamp>` 는 Wave 1-B → Wave 2 lockstep (`.wave_1b_timestamp.txt`). 1 char drift 시 prompt loader fail (alignment 와 별개).
- 4-point sync row: (1) v19 prompt directory + (2) `SCENE_DETAIL_PROMPT_VERSION` + (3) `MODULE_VERSIONS["scene_detail_composer"] == "1.19.0"` + (4) dict access `prompt_dependency == "scene_detail/v19"` 모두 alignment test 가 강제.

---

### Task 5.4 — regression 확인

- [ ] **Dependency:** Task 5.1 + 5.2 + 5.3 모두 완료
- [ ] **Spec cross-reference:** spec §6.3 / G4.2 + G4.3 plan-Task-5.4 carry

**실행:**

```bash
cd backend && pytest tests/ -x --timeout=120
```

기준: G4.3 baseline (2025 + 105/105 = ~2130 passed estimated post-G4.3) 대비 **0 regressions**. G4.4 신규 test 추가 후 total count ≥ **G4.3 baseline + 63 (~46 unit + ~17 integration) ~= 2193+ passed**.

G3.2 sentinel test (`test_g3_2_*.py`) + G4.1 (`test_g4_1_*.py`) + G4.2 (`test_g4_2_*.py`) + G4.3 (`test_g4_3_*.py`) 가 G4.4 이후에도 모두 pass 해야 한다 (continuity_elements_used 확장 외 영역 변경 없음).

```bash
# G3.2 sentinel test
pytest backend/tests/ -k "g3_2" -v

# G4.1 test
pytest backend/tests/ -k "g4_1" -v

# G4.2 test (background-binding lift carry)
pytest backend/tests/ -k "g4_2" -v

# G4.3 test (id-policy lift carry)
pytest backend/tests/ -k "g4_3" -v

# G4.4 신규 test
pytest backend/tests/ -k "g4_4" -v

# alignment test (4-point sync — version_registry / step_manifest / prompt 일관 — Task 5.3)
pytest backend/tests/prompts/ -v -k "alignment"
```

**기존 verification bullet 흡수:**

- [ ] G3.2 + G4.1 + G4.2 + G4.3 test suite 모두 green (regression 0)
- [ ] alignment test green (Task 5.3 4-point sync 강제)
- [ ] total count ≥ G4.3 baseline + 63 (~46 unit + ~17 integration)
- [ ] 어느 한 test 실패 시 Phase 5 미완료 — Wave 4 fix 후 재실행. Wave 1-A / 1-B / 2 부분 재구현 가능성 검토.

---

## Phase 6 (Wave 5): canary execution + exit criteria 판정

**Phase 목표:** Phase 4 스크립트 5종으로 baseline (v18) + candidate (v19) capture. exit criteria 7개 항목 판정. 통과 시 단일 commit 진행 (Phase 7).

**Phase 6 entry condition (G4.3 R3-Entry-Note carry):**

- G4.3 production canary 6-gate 실행 완료 (사용자 confirm — Override O-22 carry from G4.3). G4.4 spec / plan / Wave 1-4 진행은 G4.3 production canary 무관 — 본 Phase 6 만이 production canary 결과 의존.
- Wave 5 진입 전 사용자 confirm: G4.3 production canary 결과 (token + body-part focus + close-framing forbidden + reproduction surface + demographic descriptor 5 metric) 모두 pass 됐는지.
- 진입 시 사용자 confirm: PID 결정 + scene/shot set pinning. plan 본문에 specific PID 박지 않음 (시나리오 의존 차단 — `feedback_no_scenario_names.md` carry).

---

### Task 6.1 — baseline canary capture (v18)

- [ ] **File:** N/A (실행 단계 — JSON 출력 `docs/canary/g4_4_*_baseline_<timestamp>.json`)
- [ ] **Dependency:** Phase 4 스크립트 5종 완성 (Task 4.1 ~ 4.5) + G4.3 production canary 6-gate 통과 + PID 결정 (사용자 confirm)
- [ ] **Spec cross-reference:** spec §5.2 / §5.3 Step 1 / Override O-12 (`compute_continuity_snapshot_hash`) / Override O-11 (multi-ref filter) / G4.3 plan R2-M6 carry
- [ ] **Expected gate values:** baseline 비0 허용 (strict 0 은 candidate 전용)

**실행 순서:**

1. **PID 결정 (G4.3 plan R1-I9 carry — 사용자 confirm 단계)**: 본 task 진입 시 사용자 확인 필요. plan 본문에 specific PID 박지 않음 (시나리오 의존 차단). PID 결정 기준:
   - `fixed_elements[i].character_name → C##/C##O##` 매핑이 1+ 존재하는 shot 최소 **5개** (double_description denominator 안정성 — spec §5.2).
   - 그 중 `previous_shot_refs[i].ref_usage='zoom_in_detail'` shot 최소 **2개** (zoom_in_detail_no_new_entity denominator).
   - 그 중 `previous_shot_refs[i].ref_usage='atmosphere_reference'` shot 최소 **1개** (atmosphere_no_layout_import denominator). multi-ref shot (atmosphere + exact_background co-presence) 포함 시 Override O-11 filter 검증 가능.
   - **R2-M6 carry**: full-body + body-part close-up 동시 등장 가능한 shot 최소 **3개** (view_mixing denominator).
   - G3.x / G4.1 / G4.2 / G4.3 검증된 PID (memory `next_session_*` 계열 후보) 또는 신규 PID. **사용자 confirm 필수**.

2. 선택 scene/shot set 을 `pinning.scene_index_list` + `pinning.shot_index_list_per_scene` 에 명시 (G4.3 plan-R2-I4 carry — pinning 7 field).

3. **v18 prompt (G4.3 commit `51d6c5e` baseline) 로 force scene_detail 실행** (명시된 scene/shot 만 — 시나리오 무관 절차):

```bash
# 선택 scene/shot 만 force 실행 (`feedback_only_modified_steps.md` carry — run-all 금지)
curl -X POST http://<LAN_IP>:8000/projects/<PID>/pipeline/scene_detail/force \
  -H "Cookie: ..." \
  -d '{"scene_index_list": [3,7,12,18,24], "shot_index_list_per_scene": {"3":[1,2],"7":[4],"12":[4],"18":[3],"24":[1,5]}, "prompt_version": "18.202605041549"}'
```

4. canary scripts 실행 (5종):

```bash
# 1. double-description (fixed_element id-mapping shot only — Override O-12 hash pinning)
python scripts/canary/g4_4_double_description.py \
  --pid <PID> --role baseline --prompt-version 18.202605041549 \
  --output docs/canary/g4_4_baseline_double_desc_$(date +%Y%m%d%H%M).json

# 2. zoom_in_detail no new entity (zoom_in_detail shot only)
python scripts/canary/g4_4_zoom_in_detail_no_new_entity.py \
  --pid <PID> --role baseline --prompt-version 18.202605041549 \
  --output docs/canary/g4_4_baseline_zoom_$(date +%Y%m%d%H%M).json

# 3. atmosphere no layout import (Override O-11 multi-ref filter)
python scripts/canary/g4_4_atmosphere_no_layout_import.py \
  --pid <PID> --role baseline --prompt-version 18.202605041549 \
  --output docs/canary/g4_4_baseline_atmosphere_$(date +%Y%m%d%H%M).json

# 4. view-mixing (모든 shot scope, cross-import G4.3 _ID_BODY_PART_TRIGGERS — Override O-17)
python scripts/canary/g4_4_view_mixing.py \
  --pid <PID> --role baseline --prompt-version 18.202605041549 \
  --output docs/canary/g4_4_baseline_view_mixing_$(date +%Y%m%d%H%M).json

# 5. token count (system-only delta, system+inject 합산 secondary — Override O-1)
python scripts/canary/g4_4_token_count.py \
  --baseline-version 18.202605041549 --candidate-version 19.<timestamp> \
  --pid <PID> ... \
  --output docs/canary/g4_4_token_count_$(date +%Y%m%d%H%M).json
```

5. **pinning 7 field 검증 (Override O-12 carry — `compute_continuity_snapshot_hash`)**:
   각 baseline JSON 의 `pinning` block 7 field 모두 채워지는지 확인 — `pid` / `scene_index_list` / `shot_index_list_per_scene` / `model_routing` / `prompt_source_mode` / `card_commit_hash` / **`continuity_card_snapshot_hash`** (Override O-12 — `sha256(json.dumps(card['continuity_elements_used'], sort_keys=True, ensure_ascii=False))[:16]`).

6. baseline JSON 5종 저장 — Task 6.3 가 통합 비교.

---

### Task 6.2 — candidate canary capture (v19)

- [ ] **File:** N/A (실행 단계 — JSON 출력 `docs/canary/g4_4_*_candidate_<timestamp>.json`)
- [ ] **Dependency:** Task 6.1 완료 + Phase 2 (v19 prompt 존재) + Phase 3 (Task 3.1 / 3.2 / 3.3 — 4-point sync + Override O-3 inline prose 9 줄 삭제 검증)
- [ ] **Spec cross-reference:** spec §5.3 Step 2 / Override O-11 (multi-ref filter) / O-12 (hash pinning)
- [ ] **Expected gate values:** **STRICT 0** (4 metric — view_mixing / atmosphere_layout_import / zoom_in_detail_new_entity / double_description)

**실행 순서:**

1. **Task 6.1 과 동일 PID / 동일 scene/shot set force 재실행** (v19 prompt 적용 상태):

```bash
# v19 prompt 로 force 재실행 (4-point sync 적용된 후)
curl -X POST http://<LAN_IP>:8000/projects/<PID>/pipeline/scene_detail/force \
  -H "Cookie: ..." \
  -d '{"scene_index_list": [3,7,12,18,24], "shot_index_list_per_scene": {...}, "prompt_version": "19.<timestamp>"}'
```

2. canary scripts 실행 (4 metric script — token script 는 baseline/candidate 동시 측정이므로 Task 6.1 에서 1회 실행 완료):

```bash
python scripts/canary/g4_4_double_description.py \
  --pid <PID> --role candidate --prompt-version 19.<timestamp> \
  --output docs/canary/g4_4_candidate_double_desc_$(date +%Y%m%d%H%M).json

python scripts/canary/g4_4_zoom_in_detail_no_new_entity.py \
  --pid <PID> --role candidate --prompt-version 19.<timestamp> \
  --output docs/canary/g4_4_candidate_zoom_$(date +%Y%m%d%H%M).json

# Override O-11 multi-ref filter — `any(r.ref_usage == 'atmosphere_reference' for r in previous_shot_refs)`
python scripts/canary/g4_4_atmosphere_no_layout_import.py \
  --pid <PID> --role candidate --prompt-version 19.<timestamp> \
  --output docs/canary/g4_4_candidate_atmosphere_$(date +%Y%m%d%H%M).json

python scripts/canary/g4_4_view_mixing.py \
  --pid <PID> --role candidate --prompt-version 19.<timestamp> \
  --output docs/canary/g4_4_candidate_view_mixing_$(date +%Y%m%d%H%M).json
```

3. **pinning block 7 field 가 baseline 과 일치하는지 확인** (`pid`, `scene_index_list`, `shot_index_list_per_scene`, `model_routing`, `prompt_source_mode`, `card_commit_hash`, **`continuity_card_snapshot_hash`** — Override O-12). 불일치 시 baseline 재측정 (Task 6.1 재실행).

4. **Override O-11 multi-ref filter 검증**: atmosphere_reference + exact_background co-presence shot 에서 atmosphere_no_layout_import script 가 atmosphere check 만 적용 (exact_background 의 `same wall` 표현이 false-positive 발생 가능 → P1 follow-up 추적 — 별 issue).

---

### Task 6.3 — exit criteria 판정 (7 gates)

- [ ] **File:** N/A (실행 단계 — 통합 결과 JSON `docs/canary/g4_4_continuity_summary_<timestamp>.json` 선택사항)
- [ ] **Dependency:** Task 6.1 + Task 6.2 모두 완료
- [ ] **Spec cross-reference:** spec §5.1 / Override O-1 (token gate) / O-2 (line count gate) / O-17 (regex brittleness P1 follow-up) / G4.3 plan R3-I1 carry
- [ ] **Expected gate values:** 7 gates 모두 pass

**7가지 exit criteria 체크리스트:**

- [ ] **view_mixing** (`g4_4_view_mixing.py` 출력, **STRICT**): `candidate.metrics.view_mixing_count == 0` (full-body + body-part close-up 동일 prompt 결합 0 — canary-output key, Task 4.4 line 1677 일치)
- [ ] **atmosphere_layout_import** (`g4_4_atmosphere_no_layout_import.py` 출력, **STRICT**): `candidate.metrics.atmosphere_layout_import_count == 0` (atmosphere_reference shot 안 furniture/wall/architectural import 0 — Override O-11 multi-ref filter 적용 후)
- [ ] **zoom_in_detail_new_entity** (`g4_4_zoom_in_detail_no_new_entity.py` 출력, **STRICT**): `candidate.metrics.zoom_in_detail_new_entity_count == 0` (zoom_in_detail shot 안 prior shot 에 없던 person/prop/background 추가 0)
- [ ] **double_description** (`g4_4_double_description.py` 출력, **STRICT**): `candidate.metrics.double_description_count == 0` (fixed_element id-mapping shot 안 C##/C##O## + generic person noun 동시 등장 0 — canary-output key, Task 4.1 line 1344 일치)
- [ ] **token_delta_system** (`g4_4_token_count.py` 출력, Override O-1 binding gate): `token_delta = candidate_token_count - baseline_token_count` 가 `<= -540` strict (tiktoken cl100k_base 실측 — system.md only). 이는 `baseline - candidate >= 540` 과 수학적으로 동등. system+inject 합산 (`<= -150`) = secondary report-only (no gate).
- [ ] **continuity_section_line_count** (Override O-2 binding gate, integration test 8 강제): v19 system.md 의 `## Continuity` heading 부터 next `## ` heading 직전까지 `wc -l` `<= 32` lines strict (wrap 줄도 line 1 카운트).
- [ ] **pytest G4.4 suite:** `pytest backend/tests/ -k "g4_4" → 0 failures` (63+ 신규 test all green — ~46 unit + ~17 integration) + Phase 5 Task 5.4 regression green (G3.2 + G4.1 + G4.2 + G4.3 carry suite 모두 green).

모두 통과 → Task 7.1 commit 진행.
실패 항목 있음 → 해당 sibling policy dict / compact section 문구 / canary script / Override O-3 inline 삭제 검증 수정 후 재canary.

**비교 절차 (Task 6.3 inline)**:

1. baseline JSON 4개 + candidate JSON 4개 + token JSON 1개 모두 load.
2. 각 JSON 의 `pinning` block 동일 비교 (G4.3 R2-B6 carry — dict equality + 7 field 정확).
3. 위 5 metric gate (4 STRICT + 1 token) + 1 line count gate + 1 pytest gate = 총 **7 gate** 각각 비교 + 통과 여부 기록.
4. 통과 시 통합 결과 JSON 저장: `docs/canary/g4_4_continuity_summary_<timestamp>.json` (선택사항).

**STRICT 0 metric (4종)** — baseline 비0 이어도 candidate 0 강제. baseline 비0 면 P1 follow-up 등록 (G4.4 scope 밖, 별도 production fix 추적 — G4.2 R1-I4 / R2-B5 + G4.3 strict 패턴 carry / Override O-16 carry).

**Override O-17 carry (regex brittleness P1 follow-up)** — view_mixing canary 가 자연스러운 부분 묘사 ("C01O02 stands in profile, hand resting on the table") 류 false-positive 발생 시 baseline 비0 → **P1 follow-up issue 등록**: LLM-validator (gpt-5.4-mini judge) 전환 검토. `feedback_no_regex_postprocessing.md` 의 의미 판단 LLM-first 정책 carry — G4.4 scope 외 (별 issue).

**기존 verification bullet 흡수:**

- [ ] G4.3 production canary 6-gate 통과 confirm (Phase 6 entry condition — Override O-22 carry from G4.3).
- [ ] baseline (v18) + candidate (v19) capture pinning 7 field 일치 (`continuity_card_snapshot_hash` 포함 — Override O-12).
- [ ] 7 exit criteria 모두 pass (위 체크리스트).
- [ ] baseline 비0 metric 발견 시 P1 follow-up issue 등록 (G4.4 scope 밖 — Override O-16 / O-17 carry).

---

## Phase 7 (Wave 6): commit + dual review fix loop

**Phase 목표:** 단일 commit push (Phase 5 + Phase 6 산출물 모두 squash). Codex (R5) + Claude (R6) 듀얼 리뷰. 3 iter fix loop (G4.2 14d14cb + G4.3 51d6c5e 패턴 carry — `--amend` post-push 절대 금지, fix iter 마다 신규 commit). BLOCKING 모두 fix 후 final merge.

---

### Task 7.1 — commit 준비 + single commit push

- [ ] **File:** N/A (commit 단계 — `git add` 대상 파일 enumerate)
- [ ] **Dependency:** Phase 6 exit criteria 7 gate 모두 통과 + Phase 5 regression green
- [ ] **Spec cross-reference:** spec §7.2 step 17 / G4.3 commit `51d6c5e` 단일 commit pattern carry / Override O-3 prose deletion carry
- [ ] **Expected diff size:** N/A (squash 합산)

**commit 대상 파일:**

| 파일 | 변경 유형 | Wave |
|---|---|---|
| `backend/app/core/steps/render_prompt_card.py` | Modified (~330 LOC: 9 module-level constants + helper + 3 sibling policy dicts + extended constraints + shape validator + 4 lift_status + 3 rule_source) | Wave 1-A |
| `backend/app/core/steps/detail_steps.py` | Modified (~10 lines — line 105 version bump + line 1555-1563 inline 9 줄 prose 삭제 + has_zoom_in_detail 변수 정리 — Override O-3) | Wave 2 |
| `backend/app/core/version_registry.py` | Modified (4-point sync — line 34 + line 126) | Wave 2 |
| `prompts/_base/scene_detail/19.<timestamp>/system.md` | New (-66 + 14 = -52 net lines vs v18 — 3 prose 삭제 + ≤ 32-line Continuity compact section) | Wave 1-B |
| `prompts/_base/scene_detail/19.<timestamp>/detail_schema.json` | New (verbatim copy from v18) | Wave 1-B |
| `prompts/_base/scene_detail/19.<timestamp>/user.md` | New (verbatim copy from v18) | Wave 1-B |
| `scripts/canary/g4_4_double_description.py` | New (~150 lines) | Wave 3 |
| `scripts/canary/g4_4_zoom_in_detail_no_new_entity.py` | New (~140 lines) | Wave 3 |
| `scripts/canary/g4_4_atmosphere_no_layout_import.py` | New (~145 lines) | Wave 3 |
| `scripts/canary/g4_4_view_mixing.py` | New (~155 lines) | Wave 3 |
| `scripts/canary/g4_4_token_count.py` | New (~110 lines) | Wave 3 |
| `backend/tests/unit/test_g4_4_continuity_lift.py` | New (~46 tests, table 46 enumerate) | Wave 4 |
| `backend/tests/integration/test_g4_4_continuity_integration.py` | New (~17 tests) | Wave 4 |
| `backend/tests/prompts/test_scene_detail_*_alignment.py` | Modified (4-point sync row 갱신 + lift_status 4 신규 key) | Wave 4 |
| `docs/canary/g4_4_baseline_*.json` (4종) | New | Wave 5 |
| `docs/canary/g4_4_candidate_*.json` (4종) | New | Wave 5 |
| `docs/canary/g4_4_token_count_*.json` | New | Wave 5 |

**.wave_1b_timestamp.txt** scratch file 은 commit 제외 (gitignore — Task 2.4).

**commit message format (G4.3 `51d6c5e` 패턴 carry — Override O-3 prose deletion 명시):**

```
feat(g4.4): continuity lift — 3 prose 66 lines → card 3 sibling dicts + extended constraints + v19 prompt

- build_continuity_elements_used(): 3 신규 builder-static sibling policy dicts
  (cross_shot_id_substitution_rule / ref_usage_constraints / view_consistency)
  + extended constraints (G4.1 base 2 → G4.4 7 strings)
- 9 module-level constants (_CONTINUITY_GENERIC_PERSON_NOUNS /
  _CONTINUITY_FURNITURE_LAYOUT_TOKENS / _CONTINUITY_REF_USAGE_VALUES /
  _CONTINUITY_FRAMING_SCOPE_OPTIONS / _CONTINUITY_ZOOM_FORBIDDEN_ADDITIONS_COUNT
  / _CONTINUITY_ZOOM_REQUIRED_PHRASINGS_COUNT /
  _CONTINUITY_EXACT_PERMITTED_ADDITIONS_COUNT /
  _CONTINUITY_ATMOSPHERE_FORBIDDEN_IMPORTS_COUNT /
  _CONTINUITY_ID_POLICY_CROSS_REF_LITERAL — Override O-7 paired drift 차단)
- compute_continuity_snapshot_hash() helper (Override O-12)
- _assert_continuity_elements_used_shape() 3 신규 sibling policy dicts +
  extended constraints superset 검증 (Override O-5)
- _card_metadata.lift_status: 4 신규 key (continuity_cross_shot_id_substitution_lifted
  / continuity_ref_usage_constraints_lifted / continuity_view_consistency_lifted
  / continuity_constraints_extended) + rule_source 3 신규 key
  (cross_shot_id_substitution_rule / ref_usage_rule / view_consistency_rule)
- detail_steps.py:105: SCENE_DETAIL_PROMPT_VERSION 18→19.<timestamp> bump
- detail_steps.py:1555-1563: inline `if has_zoom_in_detail:` 9 줄 prose 삭제
  (Override O-3) — card.continuity_elements_used.ref_usage_constraints.zoom_in_detail
  가 동일 데이터 carry. has_zoom_in_detail 변수 dead code 정리.
- version_registry.py: MODULE_VERSIONS 1.18.0→1.19.0 + prompt_dependency v19
  (G4.3 R3R4-B1 carry — dict access + major suffix only)
- v19 system.md: 3 prose sections removed (-66 lines: Section A 109-120 / Section
  B 122-156 / Section C 308-326) → ≤ 32-line Continuity compact section
  (+14 lines). net -52 lines / actual <-540 token gate (Override O-1).
- 5 canary scripts (g4_4_double_description / zoom_in_detail_no_new_entity /
  atmosphere_no_layout_import / view_mixing / token_count) — module constants
  import + ±50 chars window proximity (sentence regex 금지 — Override O-17
  P1 carry) + sentinel pre-pass + pinned-tuple post-loop validation
- +46 unit + +17 integration tests (Override O-5 superset + cross-card paired
  substring + line count gate + token delta gate)
- alignment test 4-point sync 갱신 (Override O-9 — lift_status 4 신규 key)
- CARD_SCHEMA_VERSION=1, SCENE_DETAIL_SCHEMA_VERSION=7 (no bump —
  continuity_elements_used sub-field expansion is dict key addition only,
  not envelope shape change)

R1+R2 audit: 19 Override rows applied (O-1 token gate / O-2 line count gate /
O-3 detail_steps inline prose / O-5 superset / O-6 명명 / O-7 paired ref /
O-9 lift_status keys / O-11 multi-ref filter / O-12 snapshot hash /
O-14 line count math / O-17 regex brittleness P1 carry).
```

**push 절차 (G4.3 carry):**

```bash
# 모든 wave commits squash → single commit
git checkout main
git log --oneline | head -20  # wave commits enumerate

# squash (G4.3 51d6c5e 패턴):
# wave 1-A / 1-B / 2 / 3 / 4 / 5 commit 모두 1 단일 commit 으로 통합
# 이후 Task 7.4 fix loop 는 신규 commit (--amend post-push 절대 금지)

git push origin main
```

---

### Task 7.2 — Codex 리뷰 (R5 / iter 1)

- [ ] **File:** N/A (리뷰 단계 — codex-companion 호출)
- [ ] **Dependency:** Task 7.1 commit + push 완료
- [ ] **Spec cross-reference:** CLAUDE.md `feedback_dual_code_review.md` / G4.3 `51d6c5e` Codex 리뷰 패턴 carry

**Codex 리뷰 dispatch 포인트 (audit dimensions):**

1. **silent fallback 검출** (`feedback_no_silent_fallback.md` carry — Trap #13):
   - `lift_status.get(...)` / `card.get("continuity_elements_used", {})` / `(ce or {})` 류 fallback 신규 도입 0.
   - `_assert_continuity_elements_used_shape()` 가 missing key 시 AppError fail-fast (silent absorb 0).
2. **14 carry trap 회귀** (G4.3 carry):
   - bool int subclass trap (`mixing_forbidden is True and type() is bool`).
   - hardcoded fixture `[]` 입력 → builder-static sub-field 누락 trap (test 33 / 34 강제).
   - `_card_metadata` hash exclusion (test 38 강제).
   - `_MODULE_INFO` dict access (G4.3 R3R4-B1 — alignment test 5.3 강제).
   - prompt_dependency major suffix only (`"scene_detail/v19"` not timestamp).
3. **cross-card paired ref drift** (Override O-7):
   - `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` 단일 source — view_consistency + ref_usage_constraints.zoom_in_detail 양쪽 동일 substring (unit test 19 + integration test 17 강제).
   - Wave 1-A 코드 maintainer 코멘트 ("rename 시 G4.4 spec patch 필요") present.
4. **Override O-3 prose deletion 정확** (detail_steps.py:1555-1563):
   - `grep -n "has_zoom_in_detail" backend/app/core/steps/detail_steps.py` 결과 0 사용처.
   - `[중요 — 확대 샷 원칙 (zoom_in_detail)]` substring absent (integration test 12 강제).
   - 변수 정의 dead code 함께 정리.
5. **spec drift** (spec §1-§9 본문 vs Override 표 충돌):
   - 19 Override row 모두 본문 inline 동기화 (또는 Round 2 Override row 명시).
6. **canary script 정확** (Override O-17 carry):
   - module constants import (`_CONTINUITY_GENERIC_PERSON_NOUNS` 등 9개) + `compute_continuity_snapshot_hash` import.
   - ±50 chars window proximity (sentence regex 금지).
   - sentinel pre-pass + pinned-tuple post-loop validation + gate fail → exit 1.
7. **prompt-side 정확** (v19 system.md):
   - 3 prose section 모두 삭제 + ≤ 32-line Continuity compact section + line 313 cross-card reference 보존 (Override O-7 carry).
   - tiktoken 실측 token_delta `<= -540` 보존.

BLOCKING 발견 시 → fix → 신규 commit (Task 7.4). IMPORTANT / MINOR 는 동일 iter 안 통합 fix.

---

### Task 7.3 — Claude 리뷰 (R6 / iter 1)

- [ ] **File:** N/A (리뷰 단계 — superpowers:code-reviewer subagent 호출)
- [ ] **Dependency:** Task 7.1 commit + push 완료 (Task 7.2 와 병렬 가능 — independent audit)
- [ ] **Spec cross-reference:** CLAUDE.md `feedback_dual_code_review.md` / G4.3 `51d6c5e` Claude 리뷰 패턴 carry

**Claude 리뷰 focus (audit dimensions — Codex R5 와 independent):**

1. **build_continuity_elements_used() shape 정확** (spec §2.2 1:1):
   - 3 신규 sibling policy dicts (cross_shot_id_substitution_rule / ref_usage_constraints / view_consistency) 의 required_keys + ground-truth content 가 spec §2.2 와 deep equality.
   - extended constraints 7 strings 의 substring + verbatim string equality (constraints[0] G4.1 carry).
2. **v19 system.md ≤ 32-line Continuity compact section 정확** (spec §3.3):
   - Override O-2 binding gate `wc -l <= 32` 실측.
   - Override O-7 paired-string substring `"id_policy.body_part_focus_rule"` 1+ 등장.
   - perception_mode 우선 명시는 G4.3 ID Policy 영역 — G4.4 변경 0 (regression 차단).
3. **detail_steps.py:1555-1563 inline 9 줄 prose 삭제 정확** (Override O-3):
   - "[중요 — 확대 샷 원칙 (zoom_in_detail)]" substring absent.
   - `if has_zoom_in_detail:` 블록 자체 absent + has_zoom_in_detail 변수 dead code 정리.
4. **integration test substring presence/absence 정확** (Task 5.2 17 tests):
   - test 4-7 v19 heading absent/present 검증.
   - test 14-17 user_prompt JSON inject path 검증 (mock 금지 — 실제 pipeline call).
5. **4-point sync alignment test 정확** (Task 5.3):
   - `MODULE_VERSIONS["scene_detail_composer"] == "1.19.0"` row 가 G4.3 R2-I7 → G4.4 carry.
   - `_MODULE_INFO[...]["prompt_dependency"] == "scene_detail/v19"` row 가 G4.3 R3R4-B1 carry (dict access + major suffix only).
   - lift_status 4 신규 key 모두 explicit dict access (`feedback_no_silent_fallback.md` Trap #13 carry).
6. **G3.2 / G4.1 / G4.2 / G4.3 carry 회귀 0 확인**:
   - id_policy / background_binding / owned_validation sentinel sub-field 변경 0.
   - line 313 의 `id_policy.body_part_focus_rule` cross-card reference 가 G4.4 view_consistency sub-field 와 1:1 보존만 (Override O-7).

---

### Task 7.4 — Dual review fix loop (iter 2 / iter 3)

- [ ] **File:** N/A (fix loop — 신규 commit 단계)
- [ ] **Dependency:** Task 7.2 + Task 7.3 모두 완료 (양쪽 독립 audit 결과)
- [ ] **Spec cross-reference:** G4.2 commit `14d14cb` + G4.3 commit `51d6c5e` 3-iter pattern carry / Override O-22 carry from G4.3

**iter 2 / iter 3 fix loop (max 3 iter — G4.3 carry):**

1. **Codex (R5) + Claude (R6) 리뷰 결과 통합** → Round 2 Override rows 정리 (spec Round Override 표에 row 추가).
2. **BLOCKING 모두 fix** → 신규 commit (G4.3 `51d6c5e` 패턴: 신규 commit; **`--amend` post-push 절대 금지** — `feedback_review_before_commit.md` carry / Git Safety Protocol).
3. **Task 7.2 (Codex R5) / 7.3 (Claude R6) 재실행** → 양쪽 APPROVED 시 final.
4. **3 iter 통과 못 하면** Wave 1-A (producer) 또는 Wave 1-B (prompt) 부분 재구현 검토 — fundamental design issue 시 spec Round 3 audit 진입 (사용자 confirm).

**iter 별 fix → commit 패턴:**

```bash
# iter 2 fix 후 신규 commit (G4.3 패턴):
git add <fixed files>
git commit -m "$(cat <<'EOF'
fix(g4.4): R5+R6 iter 2 — <BLOCKING list 요약>

- <Codex R5 BLOCKING fix>
- <Claude R6 BLOCKING fix>
- spec Round Override 표 row 추가 (Round 2 Override)

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

# Task 7.2 / 7.3 재실행 — 양쪽 APPROVED 시 final.
```

**Override 정리 표 (예시 — Round 2 audit 후 채움):**

| Round | Codex (R5) 발견 | Claude (R6) 발견 | 결정 (Override 행 ID) |
|---|---|---|---|
| iter 2 | `?` | `?` | (실행 후 채움 — 예: O-20 / O-21) |
| iter 3 | `?` | `?` | (실행 후 채움 — 예: O-22 / O-23) |

**3 iter 통과 후 final**: spec / plan / 코드 / 테스트 / canary JSON 모두 main HEAD 에 단일 squash 산출물 + iter 별 신규 commit (G4.3 카운트 1 squash + 1-2 fix 패턴 carry).

---

## Risk Register / Rollback

### Risk Register (G4.4 spec §8 carry + plan-specific)

#### G4.4-unique risks (spec §8.1 carry)

| risk | mitigation | measurable threshold |
|---|---|---|
| v19 compact section 이 fixed_element substitution 룰 (자세 보존 + 반복 금지) 을 충분히 carry 하지 않아 LLM 이 보통명사 + C## 이중 묘사 출력 | constraints[1] + `cross_shot_id_substitution_rule` 6 sub-key 명시 + canary STRICT 0 | candidate count == 0 |
| zoom_in_detail prose 삭제 후 LLM 이 prior shot 에 없던 entity 추가 출력 | card `ref_usage_constraints.zoom_in_detail.forbidden_additions` 5 entry + constraints[2] + canary STRICT 0 | candidate count == 0 |
| atmosphere_reference prose 삭제 후 LLM 이 prior shot 의 가구 layout import | card `ref_usage_constraints.atmosphere_reference.forbidden_imports` 3 entry + constraints[4] + canary STRICT 0 | candidate count == 0 |
| 시점 혼합 prose 삭제 후 LLM 이 전신 + 같은 인물 body-part close-up 한 prompt 결합 | card `view_consistency.{single_camera_rule,mixing_forbidden,third_person_handling,close_up_handling}` + constraints[5] + canary STRICT 0 | candidate count == 0 |
| `_CONTINUITY_GENERIC_PERSON_NOUNS` ground-truth list 가 actual production prompt 의 generic person noun 다양성을 cover 하지 않음 | R1+R2 audit 에서 ground-truth list 확정 (7 entry) + Wave 5 production canary 에서 baseline scan time 에 missed phrase 자동 발견 시 list 확장 (P1 follow-up 별도 추적) | actual production canary baseline 에 등장한 generic noun 의 ≥ 90% list cover |
| `_CONTINUITY_FURNITURE_LAYOUT_TOKENS` 가 너무 broad 해 atmosphere_reference shot 가 "the dim ambient pool" 같은 ambient mood 표현을 false-positive 검출 | R1+R2 audit + R3+R4 plan audit 에서 token list 정정. `furniture` / `wall position` / `same chair` 류 layout-specific 만 포함, ambient/lighting tone 표현 제외 (7 entry 확정) | unit test fixture 가 ambient mood 표현 false-positive 0 검증 |
| view-mixing canary 가 "C01O02 stands in profile, hand resting on the table" 류 자연스러운 부분 묘사를 false-positive 검출 | regex 가 same-character ID + body-part close-up trigger phrase (focus on / close on / tight on / detail on) 둘 다 동일 prompt 안 동시 등장 시에만 trigger. 단순 hand 언급은 trigger 아님. Override O-17 carry — baseline 비0 시 P1 follow-up 으로 LLM-validator (gpt-5.4-mini judge) 전환 검토 | unit test fixture 가 자연스러운 부분 묘사 false-positive 0 검증 |
| `_card_metadata.lift_status` 신규 4 key (Override O-9) 가 hash 에 의도치 않게 포함되어 hash 가 매 build 마다 변경 | G4.2 R1-B1 + G4.3 carry: `_card_metadata` 전체 가 `canonicalize_render_prompt_card()` 에서 strip. unit test `test_card_metadata_excluded_from_hash_after_g4_4_keys_added` (test 38 — content selector pattern, longer descriptive form) 가 강제 | `_card_metadata` not in canonicalized output (post G4.4 key 추가) |
| canary PID 의 fixed_element id-mapping shot / ref_usage shot 부족 시 metric 분모 0 | baseline / candidate scene set 선정 시 fixed_element id-mapping shot 최소 5개 + zoom_in_detail shot 최소 2개 + atmosphere_reference shot 최소 1개 보장 (spec §5.2) | scene_set in canary JSON ≥ 5+2+1 분리 |
| canary baseline double-description / zoom_in_detail / atmosphere / view-mixing count 가 비0 인 경우 | strict: candidate == 0 강제. baseline 비0 이면 P1 follow-up 으로 별 추적 (G4.4 scope 밖이지만 production 결함 시그널) | candidate count == 0 mandatory; baseline 비0 면 별 issue 등록 |
| Wave subagent fixture 가 신규 3 sibling policy dicts (Override O-6 명명) 누락한 partial card 사용 → production drift cascade | `_DEFAULT_SENTINEL = object()` fixture pattern + `_make_full_card()` helper 가 항상 3 sibling policy dicts + extended constraints 채움 + `_assert_continuity_elements_used_shape()` strict (Override O-5 superset) | unit tests rows 39-42 (`test_assert_continuity_shape_validates_cross_shot_id_substitution_rule_shape` / `*_ref_usage_constraints_shape` / `*_view_consistency_shape` / `*_view_consistency_required_keys`) — 4종 (R4-I3 enumerate) |
| view_consistency 의 cross-card reference (`id_policy.body_part_focus_rule`) 가 G4.3 의 sub-field 와 sync 안 맞아 inconsistent | **Override O-7 paired-string substring**: `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` constant 가 single source. unit test 17 / 18 + `test_continuity_cross_card_pair_substrings_match` 3 곳 검증. Wave 1-A 코드에 maintainer 코멘트 ("rename 시 G4.4 spec patch 필요") | all 3 tests pass with same substring |
| ref_usage_constraints 의 nested dict 깊이 (3 sub-key × 4-5 sub-sub-key) 가 `_assert_continuity_elements_used_shape()` 검증 누락 위험 | nested validator helper `_assert_ref_usage_constraints_shape()` 신설 (R3-I3) + 3 sub-key 각각 required key set 검증 (Override O-5 superset) + 4 unit tests rows 43-46 (`test_assert_ref_usage_constraints_shape_zoom_in_detail_required_keys` / `*_exact_background_required_keys` / `*_atmosphere_reference_required_keys` / `*_invalid_sub_key_raises`) — R4-I3 enumerate | nested shape AppError on missing sub-key |
| **multi-ref shot atmosphere_reference filter false-positive** (Override O-11) | atmosphere_reference + exact_background co-presence shot 에서 atmosphere check 만 적용 — `g4_4_atmosphere_no_layout_import.py` shot-level filter `any(ref.ref_usage == 'atmosphere_reference' for ref in previous_shot_refs)`. exact_background 의 `same wall` 표현이 false-positive 발생 가능 → P1 follow-up 추적 (별 issue) | unit test `test_g4_4_view_mixing_canary_multi_ref_shot_filter` (test 19) |
| **regex view-mixing canary brittleness** (Override O-17) | regex 는 1차 detector 만, baseline 비0 시 P1 follow-up 으로 LLM-validator (gpt-5.4-mini judge) 전환 검토. 의미 판단은 LLM-first (`feedback_no_regex_postprocessing.md`) | false-positive 발견 시 LLM-validator follow-up issue 등록 |
| **bool int subclass trap** — `view_consistency.mixing_forbidden = True` 가 `1` int 로 직렬화되어 test 가 misleading pass | `assert mixing_forbidden is True and type(mixing_forbidden) is bool` strict (G3.2 / G4.3 carry) — test 16 강제 | test 16 green |
| **Wave subagent hardcoded `[]` fixture trap** (G4.1 origin / G4.3 R4-M6 / O-26 carry) | 신규 3 sibling policy dicts 는 builder-static — `if visible_entities:` 또는 `if fixed_elements:` 류 truthy guard 안 배치 절대 금지. test 33 / 34 강제 + Wave 1-A briefing 명시 | test 33 / 34 green |
| **Override O-3 inline prose 삭제 시 has_zoom_in_detail 변수 정의 오삭제 위험** | Wave 2 dispatcher 가 `has_zoom_in_detail` 변수 다른 사용처 검사 후 분리 삭제. 현재 본 변수가 line 1555 분기에만 사용되면 변수 정의도 dead code 로 함께 삭제 가능. 검증: `grep -n "has_zoom_in_detail" backend/app/core/steps/detail_steps.py` 실행 후 본 9 줄 외 사용처 0 확인 → 변수 정의도 정리 | grep 결과 0 사용처 + integration test 14 green |

#### G4.1+G4.2+G4.3 carry / inherited checks

| inherited risk | carry source | G4.4 enforcement |
|---|---|---|
| v19 로 resume 시 old v18 cp 가 자동 force escalate | G4.1 R1-B1 (`_config_hash` mismatch 검증) | integration test 16 `test_v18_checkpoint_resume_on_v19_escalates_force` |
| `_user_edited` reuse path stored card 와 recompute card hash drift | G4.1 Wave 4 R4 B1/B2/B3 (ctx-driven single source) | G4.1 패턴 그대로 — G4.4 변경 없음 |
| schema_version != 7 result 에서 card check bypass warning step 당 1회 emit | G4.1 cleanup I5 (commit c656ce3) | G4.4 변경 없음 — 동일 동작 |
| `or [] / or {}` silent absorb pattern 차단 | G4.2 iter1 C3 / O-6 carry + G4.3 R5-B1 | G4.4 canary scripts 5종 + builder 모두 동일 pattern carry. 신규 producer/canary 코드 review 시 `grep -n "or \[\]\|or {}"` 검사 (Trap #1) |
| `_DEFAULT_SENTINEL = object()` fixture pattern 필수 | G4.1 R3-B2 / G4.2 plan-R1-I4 / G4.3 plan-R1-I4 | G4.4 fixture (None / [] / default 셋 명시 구분) — Trap #2 |
| canary CP shape `scene["render_prompt_card"]["continuity_elements_used"]` (NOT 평면) | G4.2 함정 + G4.3 `_validate_scene_preflight()` helper carry | G4.4 4 metric scripts 모두 동일 helper 사용 — Trap #3 |
| pinned tuple seen_in_cp post-loop validation + sentinel pre-pass + gate fail → exit 1 | G4.2 iter2 B1 + G4.3 carry | G4.4 canary scripts 5종 동일 pattern — Trap #4 |
| `_MODULE_INFO` dict access (NOT attribute) + `prompt_dependency = "scene_detail/v19"` (major suffix only) | G4.3 R3R4-B1 / Override O-23 carry | G4.4 alignment test 동일 — Trap #5 |
| token gate signed convention `token_delta = candidate - baseline`, gate `<= -<value>` | G4.3 R4-B4 / Override O-25 carry | Phase 4 Task 4.5 + Phase 6 Task 6.3 + Verification — Trap #6 |
| compact section line count 정확 (multi-line wrap trap) — bullet 1줄 강제 | G4.3 R3R4-B3 / Override O-24 carry | Phase 2 Task 2.3 + Override O-2 binding gate — Trap #7 |
| module-level constants single source — builder + canary + unit test 동일 import. drift 차단 | G4.3 R1-I2/R2-I6 / Override O-20 carry | Phase 1 Task 1.1 + 5 canary scripts + unit test fixture imports — Trap #8 |
| 시나리오 의존 0 — 작품 고유명사 절대 금지. 예시 prose 도 generic Asian-default placeholder | CLAUDE.md 절대 규칙 + `feedback_no_scenario_names.md` | Phase 1 Task 1.3 sub-shape examples (`"An Asian man"` / `"a hand"` / `"a young woman"` 만) + Wave 1-A briefing 명시 — Trap #9 |
| prompt versioning — NEVER overwrite v18, 새 dir `prompts/_base/scene_detail/19.<TIMESTAMP>/` | CLAUDE.md `feedback_prompt_versioning.md` | Phase 2 Task 2.1 — Trap #10 |
| 4-point sync lockstep — prompt dir + detail_steps.py:105 + version_registry.py:34 + version_registry.py:126 | G4.3 R4-I6 / Override O-21 carry | Phase 3 Task 3.1 / 3.2 + alignment test — Trap #11 |
| `_card_metadata.rule_source` 키 명세 — namespace 분리되어 top-level field 와 동명 OK | G4.3 R5-I1 / Override O-27 carry | Phase 1 Task 1.5 (각 신규 sub-field 마다 명시 키명: `cross_shot_id_substitution_rule` / `ref_usage_rule` / `view_consistency_rule`) — Trap #12 |
| alignment test silent fallback 제거 — `MODULE_VERSIONS.get(..., "")` / `_MODULE_INFO.get(..., {})` 금지 — 명시적 키 존재 + 타입 검증 | G4.3 R5-I2 / Override O-28 carry | Phase 5 Task 5.3 silent fallback 제거 패턴 명시 — Trap #13 |
| Wave subagent hardcoded `[]` fixture trap — 신규 sub-field 는 builder-static, `if visible_entities:` truthy guard 안 배치 절대 금지 | G4.1 origin / G4.3 R4-M6 / Override O-26 carry | Phase 1 Task 1.3 + Phase 5 Task 5.1 test 33 / 34 + Wave 1-A briefing — Trap #14 |
| `t2i_variations` + per-variation `t2i_prompt` validation | G4.2 iter2 B2 + iter3 B3 / Override O-4 / O-5 carry | G4.4 4 metric scripts 모두 검증 |
| LLM 입력 truncation 절대 금지 (`[:400]`, `[:500]` 등) | CLAUDE.md 절대 규칙 | G4.4 canary script 가 t2i_prompt 전문 scan, deepcopy lossless |
| regex post-processing 의존 금지 (`feedback_no_regex_postprocessing.md`) | CLAUDE.md 메모리 / Override O-17 carry | G4.4 canary 의 regex 는 ID/trigger 검출용 deterministic pattern 만, 의미 판단은 module-level constant token list (LLM validator 또는 prompt 우선) |
| **silent-fallback 잔존 carry (Override O-8)** | `backend/app/core/version_registry.py:148-155` 의 `MODULE_VERSIONS.get(..., "0.0.0")` / `_MODULE_INFO.get(..., {})` 잔존 = G4.1 inheritance, G4.4 scope 외 | **G4.5 또는 별도 cleanup PR carry follow-up**. G4.4 신규 코드 (Wave 1-A producer + canary + tests) 는 새로운 silent-fallback 도입 절대 금지 — Trap #15 |

### Rollback (Phase 6 또는 Phase 7 실패 시)

| 상황 | rollback 절차 |
|---|---|
| Wave 5 canary 1 metric 실패 (예: view_mixing 비0) | 1) 실패 metric 의 source sub-field 확인 (e.g. `view_consistency.mixing_forbidden` / `single_camera_rule` 문구) → 2) Wave 1-A 부분 재구현 → Wave 4-5 재실행. spec drift 면 spec back-propagate. |
| Wave 5 canary 모든 metric 실패 → 시스템적 결함 | 1) v19 prompt compact section 문구 검토 (constraint summary 가 reasoning 만 carry — token list 부재) → 2) compact section ≤ 32 line 한도 안 추가 명시 (perception_mode 류 추가 bullet) → 3) Wave 1-B 재구현 → Wave 4-5 재실행. |
| alignment test 실패 (Phase 5 Task 5.3) | 4-point sync 1 곳 누락 — 4 곳 (prompt 디렉토리 / detail_steps.py:105 / version_registry.py:34 / version_registry.py:126) 모두 lockstep 확인. timestamp 1 char drift 가능 — re-run pytest. `.wave_1b_timestamp.txt` scratch file 일치 검증. |
| Override O-3 inline prose 삭제 후 production zoom_in_detail shot 에서 entity 추가 발생 | card `ref_usage_constraints.zoom_in_detail.forbidden_additions` 5 entry 가 LLM 에 충분히 inject 안 된 신호 — 1) Phase 5 integration test 11 / 14 검토. 2) v19 compact section bullet 안 zoom_in_detail rule 보강. 3) detail_steps.py:1555-1563 prose 부분 복원 검토 (rollback). 4) baseline 으로 되돌려 G4.5 redesign 검토. |
| Phase 7 dual review iter 3 도 fix 못 함 | 단일 commit revert (`git revert HEAD`). **prompt 디렉토리 artifact 도 함께 사라짐** (commit 이 새 dir 추가 — G4.3 R3-M3 carry 명시). artifact 보존 목적이면 별도 cherry-pick 필요 (spec / plan / canary script). render_prompt_card.py 의 G4.4 추가 부분만 revert. main HEAD 복구. |

---

## Override Table (G4.2 + G4.3 carry + G4.4-new)

본 표는 G4.2 + G4.3 audit 결과 중 G4.4 에 동일 적용해야 하는 결정사항 + G4.4 spec R1+R2 audit 의 신규 row. plan body 보다 우선 적용.

### G4.2 carry (Override O-1 ~ O-19 / immutable)

| ID | Source | Override | G4.4 plan 적용 위치 |
|---|---|---|---|
| **O-1** | G4.2 R2-B4 | builder None vs [] fail-fast (`build_continuity_elements_used()` `fixed_elements=None` / `previous_shot_refs=None` / `forward_zoom_targets=None` → AppError) | Phase 1 Task 1.3 (G4.1 carry 보존) + Phase 5 Task 5.1 unit test 30/31/32 |
| **O-2** | G4.2 R1-I12 / G4.3 R3-B2 fixture trap | `_DEFAULT_SENTINEL = object()` fixture pattern (test 측) | Phase 5 Task 5.1 fixture helper |
| **O-3** | G4.2 R2-I7 / iter2 B1 | canary pinned-tuple seen_in_cp post-loop 검증 | Phase 4 Task 4.1 / 4.2 / 4.3 / 4.4 (4 metric canary scripts; Task 4.5 token script 별도) |
| **O-4** | G4.2 iter2 B2 | `t2i_variations` 누락 silent absorb 차단 — `t2i_variations=[]` / missing → measurement_failures | Phase 4 Task 4.1 ~ 4.5 |
| **O-5** | G4.2 iter3 B3 | variation 내부 `t2i_prompt` 누락 silent absorb 차단 — `var.get("t2i_prompt") or ""` 패턴 금지 | Phase 4 Task 4.1 ~ 4.5 |
| **O-6** | G4.2 iter1 C3 / G4.3 R5-B1 carry | `or [] / or {}` 패턴 전반 차단 — `(d or {})` / `(card or {})` / `(pair or {}).get(...)` 모두 fail-fast (AppError) 강제 | Wave 1-A 전체 + 5 canary scripts |
| **O-7** | G4.2 iter1 C2 | gate fail 시 exit 1 강제 (return 0 금지) | 4 metric canary scripts |
| **O-8** | G4.2 iter1 I1 | producer-side `_g41_bg_owned = ... or []` 패턴 금지 — None 흡수 차단, builder fail-fast 보장 | Phase 1 Task 1.3 (G4.1 carry 보존) |
| **O-9** | G4.2 iter1 I2 | hash 회귀 test gap 방지 — `_card_metadata` 변경 / `continuity_elements_used.constraints` 변경 drift test 의무 | Phase 5 Task 5.1 test 37 / 38 / 39 |
| **O-10** | G4.2 R1-B1 | `_card_metadata` envelope-sibling top-level only — nested 구조 도입 금지. `canonicalize_render_prompt_card()` top-level pop. | Phase 1 Task 1.5 |
| **O-11** | G4.2 R2-I5 | `_card_metadata` free-form (extra-key 허용, schema 검증 없음). assert_card_shape envelope 7 field 만 strict. | Phase 1 Task 1.4 (G4.1 carry 보존) |
| **O-12** | G4.2 R2-I4 | `detail_steps.py` inject 직전 `_card_metadata` strip 보존 | Phase 3 (변경 0 — G4.2 commit 14d14cb 그대로) |
| **O-13** | G4.2 R1-I6 | `CARD_SCHEMA_VERSION` 유지 (= 1) — continuity_elements_used 신규 sub-field 는 envelope shape 변경 아님. `SCENE_DETAIL_SCHEMA_VERSION` 유지 (= 7) — cp shape 변경 아님. | Phase 3 Task 3.1 (변경 없음 확인) |
| **O-14** | G4.2 R2-B6 | canary JSON `pid` 가 `pinning` block 안 (top-level redundant 제거) | Phase 4 Task 4.1 ~ 4.5 |
| **O-15** | G4.2 R1-M1 | token measurement = `tiktoken cl100k_base` 실측 (line × 15 estimate 는 spec 참조용) | Phase 4 Task 4.5 + Phase 6 Task 6.1 / 6.2 / 6.3 |
| **O-16** | G4.2 R1-I4 / R2-B5 | strict 0 metric 은 baseline 비0 이어도 candidate 0 강제 | Phase 6 Task 6.3 (4 STRICT 0 metric — view_mixing / atmosphere_layout / zoom_in_detail / double_description) |
| **O-17** | G4.2 plan-R1-I4 | unit test fixture `_make_full_card()` 가 `_DEFAULT_SENTINEL = object()` 적용 — None / [] / default 구분 | Phase 5 Task 5.1 fixture helper |
| **O-18** | G4.2 plan-R2-I5 | canary owned/sentinel scripts 가 sentinel 합산 전 `assert_owned_sentinel_shape()` 호출 — G4.4 의 4 metric scripts 도 sentinel 검증 시 동일 pattern | Phase 4 Task 4.1 ~ 4.5 (sentinel 사용처 사전 검증) |
| **O-19** | G4.2 plan-R2-I6 | commit target 에 `version_registry.py` (4-way sync 일부) 의무 포함 | Phase 7 Task 7.1 commit 대상 |

### G4.3 carry (R1+R2 spec audit 결정 / Override O-20 ~ O-28)

| ID | Source | Override | G4.4 plan 적용 위치 |
|---|---|---|---|
| **O-20** | G4.3 R3-B1 ground-truth | ground-truth lists single source — Glossary 항목이 §2.2 / §3.3 / §6.1 unit test / canary detector 모두 reference 하는 single source | Phase 1 Task 1.1 (9 module constants) + Trap #8 |
| **O-21** | G4.3 R3-I2 4-way sync | v19 push lockstep 4 곳 (prompt 디렉토리 + detail_steps.py:105 + version_registry.py:34 + version_registry.py:126) — alignment test 가 강제 검증 | Phase 3 Task 3.1 / 3.2 + Phase 5 Task 5.3 + Trap #11 |
| **O-22** | G4.3 R3-I3 entry gate split | spec drafting / plan / Wave 1-5 = canary 무관 진행 가능. **commit gate** = G4.3 production canary 6 gate AND G4.4 self-canary 6 gate 둘 다 필수 | Phase 6 entry condition + Phase 7 commit gate |
| **O-23** | G4.3 R3R4-B1 | `_MODULE_INFO` dict access (NOT attribute) + `prompt_dependency` 값 = `"scene_detail/v19"` (major suffix only) | Phase 3 Task 3.2 + Phase 5 Task 5.3 + Trap #5 |
| **O-24** | G4.3 R3R4-B3 | compact section line count 정확 (multi-line wrap trap) — bullet 1줄 강제 | Phase 2 Task 2.2 + Task 2.3 + Override O-2 binding + Trap #7 |
| **O-25** | G4.3 R4-B4 | token gate signed convention `token_delta = candidate - baseline`, gate `<= -<value>` | Phase 4 Task 4.5 + Phase 6 Task 6.3 + Verification + Trap #6 |
| **O-26** | G4.3 R4-M6 / R5-I3 | Wave subagent hardcoded `[]` fixture trap — 신규 sub-field 는 builder-static, `if visible_entities:` 또는 `if fixed_elements:` truthy guard 안 배치 절대 금지 | Phase 1 Task 1.3 + Phase 5 Task 5.1 test 33 / 34 + Wave 1-A briefing + Trap #14 |
| **O-27** | G4.3 R5-I1 | `_card_metadata.rule_source` 키 명세 — namespace 분리되어 top-level field 와 동명 OK (e.g. `cross_shot_id_substitution_rule` 가 continuity sub-field 명 + `_card_metadata.rule_source` 키 모두 OK) | Phase 1 Task 1.5 + Trap #12 |
| **O-28** | G4.3 R5-I2 | alignment test silent fallback 제거 — `MODULE_VERSIONS.get(..., "")` / `_MODULE_INFO.get(..., {})` 금지 — 명시적 키 존재 + 타입 검증 | Phase 5 Task 5.3 + Trap #13 |

### G4.4-new (R1+R2 spec audit, 19 rows — verbatim copy from G4.4 spec §0)

| ID | Source | Override | G4.4 plan 적용 위치 |
|---|---|---|---|
| **O-1** | spec R1-B2 + R2-B1 | Token gate threshold = system-only `token_delta = candidate - baseline ≤ -540` (1차 estimate-based binding gate). system+inject 합산 (≤ -150) = secondary report-only metric. upward revise (e.g. -300) 는 re-audit 필요. | Phase 4 Task 4.5 + Phase 6 Task 6.3 + Verification |
| **O-2** | spec R1-B1 + R2-I5 | `## Continuity` compact section line count gate: `wc -l` ≤ 32 lines strict (binding). 각 bullet 반드시 1 line — 80-col wrap 허용 안 됨. integration test §6.2 `test_v19_continuity_section_line_count_under_32`. | Phase 2 Task 2.2 + Task 2.3 + Phase 5 Task 5.2 test 8 |
| **O-3** | spec R1-B3 | `detail_steps.py:1555-1563` `if has_zoom_in_detail:` 블록 안 9 줄 inline prose 검증 완료 — Wave 2 wiring scope 에 prose 9 줄 삭제 task 추가. card.continuity_elements_used.ref_usage_constraints.zoom_in_detail 가 데이터 carry. canonical test name = `test_detail_steps_zoom_in_detail_inline_prose_absent_after_g4_4` (M1 — index 가 row 12). | Phase 3 Task 3.3 (신규) + Phase 5 Task 5.2 test 12 + Wave 2 briefing |
| **O-4** | spec R2-B2 | forward_zoom_targets G4.4 lift 대상 아님 (Option A 채택). lift_status 에 `continuity_forward_zoom_lifted` 키 추가 안 함. G4.5 prompt slimming pass 에서 G4.1 lossless carry status 재검토. | Phase 1 Task 1.5 (4 lift_status keys only) |
| **O-5** | spec R2-B3 | `assert_continuity` 확장 = strict required-keys subset (set superset 검증). 신규 sub-field required keys = `EXPECTED_REQUIRED <= returned_keys` (set equality 가 아닌 superset). extra keys 허용 (G4.5 forward-compat). | Phase 1 Task 1.4 superset 검증 + Phase 5 Task 5.1 unit test |
| **O-6** | spec R1-I1 | Sub-shape 명명 정정: "4 new top-level sub-fields" → "**3 new sibling policy dicts (`cross_shot_id_substitution_rule` / `ref_usage_constraints` / `view_consistency`) + extended `constraints` list (G4.1 base 2 → G4.4 7 strings)**". 기존 G4.1 sub-field shape 변경 없음. | Phase 1 Task 1.3 + 모든 docstring + Wave Briefing Templates |
| **O-7** | spec R1-I2 + R2 | Cross-card reference = paired string substring (구조화 dict 미도입). `view_consistency.id_policy_cross_ref_for_body_part_focus` (string, 리터럴 substring `"id_policy.body_part_focus_rule"` 포함) + `ref_usage_constraints.zoom_in_detail.body_part_cross_ref` (string, 동일 substring 포함). Wave 1-A 코드에 maintainer 코멘트. | Phase 1 Task 1.3 (`_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` constant) + Phase 5 Task 5.1 test 17 / 18 + Phase 5 Task 5.2 test 9 / 13 |
| **O-8** | spec R1-I3 | silent-fallback 잔존 carry: `version_registry.py:148-155` 의 `MODULE_VERSIONS.get(..., "0.0.0")` / `_MODULE_INFO.get(..., {})` = G4.1 inheritance, G4.4 scope 외. **G4.5 또는 별도 cleanup PR carry follow-up**. | Risk Register §8.2 carry + Trap #15 |
| **O-9** | spec R2-I3 verification | lift_status keyset 정정 — G4.4 신규 4 key: `continuity_cross_shot_id_substitution_lifted` / `continuity_ref_usage_constraints_lifted` / `continuity_view_consistency_lifted` / `continuity_constraints_extended`. | Phase 1 Task 1.5 + Phase 5 Task 5.1 test 35 |
| **O-10** | spec R2-I1 | Lift mapping line range footnote — Section A 109-120 / Section B 122-156 / Section C 308-326. Blank lines 121/157/307/327 = 인접 섹션 보존. Wave 1-B v19 drafting 에서 line-by-line diff 로 결정. | Phase 2 Task 2.1 line range 명시 |
| **O-11** | spec R2-I2 | Canary multi-ref shot atmosphere_reference 필터: shot-level filter `any(ref.ref_usage == 'atmosphere_reference' for ref in previous_shot_refs)`. multi-ref 시 atmosphere check 만 적용. exact_background co-presence false-positive baseline 비0 → P1 follow-up. | Phase 4 Task 4.3 + Phase 5 Task 5.1 test 19 |
| **O-12** | spec R2-I4 | `compute_continuity_snapshot_hash(card)` 동작 명시: `sha256(json.dumps(card['continuity_elements_used'], sort_keys=True, ensure_ascii=False))[:16]`. `_card_metadata` 는 envelope sibling 이므로 자연 제외. G4.3 `compute_id_policy_snapshot_hash` 패턴. | Phase 1 Task 1.2 + 5 canary scripts pinning |
| **O-13** | spec R1-M1 | Producer/Consumer matrix 구체 wire 명시: `_collect_card_inputs() → build_render_prompt_card() → _card_metadata strip → JSON prepend (line ~1500-1610)`. `build_continuity_elements_used()` 위치 = `render_prompt_card.py:676`. | Data Flow 섹션 + Phase 3 Task 3.3 |
| **O-14** | spec R1-M2 | §6.2 line-count test 임계 도출 — "v18 baseline 587 lines - 32 - 5 = 550 lines 미만". | Phase 5 Task 5.2 test 7 |
| **O-15** | spec R2-M1 | §3.3 ✓/✗ 예시 prose 삭제 footnote — G4.3 baseline 통과 패턴 carry. canary STRICT 0 검증이 LLM 행동 보증. | Phase 2 Task 2.2 inline note |
| **O-16** | spec R2-M2 | §6.1 unit test count 정합 — "총 unit tests: ~46 (table 46 enumerate, no buffer needed — plan-R3+R4 audit 4+4 nested validator tests 추가)". | Phase 5 Task 5.1 enumerated 46 + Wave 4 briefing |
| **O-17** | spec R2-M3 | regex view-mixing canary brittleness footnote — regex 는 1차 detector 만, baseline 비0 시 P1 follow-up 으로 LLM-validator (gpt-5.4-mini judge) 전환 검토. 의미 판단은 LLM-first. | Phase 4 Task 4.3 + Task 4.4 inline footnote + Risk Register |
| **O-18** | spec R2-M4 | Wave 5/6 numbering — Wave 1-A / 1-B / 2 / 3 / 4 / 5 / 6. **Wave 5 = production canary execution. Wave 6 = single commit + push. test 파일 = Wave 4.** | Wave Decomposition Table + Phase 6 / Phase 7 / Implementation Checklist Summary |
| **O-19** | spec R2-M5 | Glossary t2i_prompt 영어 가정 footnote — `_CONTINUITY_GENERIC_PERSON_NOUNS` / `_CONTINUITY_FURNITURE_LAYOUT_TOKENS` 영어 substring 기반 = `feedback_t2i_storyboard.md` carry. Korean t2i prompt 도입 시 G4.5 에서 generic-noun 패턴 재검토. | Phase 1 Task 1.1 maintainer 코멘트 + Risk Register |

---

## Implementation Checklist Summary

### Phase 1 (Wave 1-A) — render_prompt_card.py
- [ ] Task 1.1: 9 module-level constants 도입 (~70 LOC)
- [ ] Task 1.2: `compute_continuity_snapshot_hash()` helper 신설 (~28 LOC)
- [ ] Task 1.3: `build_continuity_elements_used()` 3 신규 sibling policy dicts assemble + 7 constraints 확장 (~180 LOC)
- [ ] Task 1.4: `_assert_continuity_elements_used_shape()` 3 신규 sub-field strict superset 검증 + `_assert_ref_usage_constraints_shape()` nested helper (~110 LOC)
- [ ] Task 1.5: `build_render_prompt_card()` `_card_metadata` 4 신규 lift_status key + 3 신규 rule_source key (~12 LOC)
- [ ] Task 1.6: docstring 갱신 (~25 LOC)

### Phase 2 (Wave 1-B) — v19 prompt
- [ ] Task 2.1: v19 디렉토리 생성 + 3 prose section 66 lines 삭제
- [ ] Task 2.2: ≤32-line `## Continuity` compact section 삽입 (Override O-2 binding)
- [ ] Task 2.3: `detail_schema.json` + `user.md` verbatim copy + 자체 line count + tiktoken 실측 (Override O-1 system-only ≤ -540 + Override O-2 line ≤ 32 strict)
- [ ] Task 2.4: timestamp lockstep scratch file (`.wave_1b_timestamp.txt` — Wave 2 dispatcher 가 read)

### Phase 3 (Wave 2) — wiring + Override O-3 inline prose 삭제
- [ ] Task 3.1: `SCENE_DETAIL_PROMPT_VERSION` v18 → v19 bump (`detail_steps.py:105`)
- [ ] Task 3.2: `version_registry.py` 4-point sync (`MODULE_VERSIONS` 1.18.0 → 1.19.0 + `_MODULE_INFO[...]["prompt_dependency"]` v19 — Trap #5)
- [ ] Task 3.3: **`detail_steps.py:1555-1563` zoom_in_detail inline prose 9 줄 삭제 (Override O-3 신규 task)**

### Phase 4 (Wave 3) — canary scripts (5종)
- [ ] Task 4.1: `g4_4_double_description.py` (id-mapping shots only, ±50 chars window dedup)
- [ ] Task 4.2: `g4_4_zoom_in_detail_no_new_entity.py` (zoom_in_detail shot only, prior shot ID set diff)
- [ ] Task 4.3: `g4_4_atmosphere_no_layout_import.py` (atmosphere_reference shot only, multi-ref filter Override O-11)
- [ ] Task 4.4: `g4_4_view_mixing.py` (모든 shot scope, Override O-17 regex brittleness)
- [ ] Task 4.5: `g4_4_token_count.py` (tiktoken cl100k_base, Override O-1 binding gate)

### Phase 5 (Wave 4) — tests
- [ ] Task 5.1: ~38-46 unit tests (table 46 enumerate per R4-I3 — 4+4 shape validator tests 추가, set superset Override O-5)
- [ ] Task 5.2: ~17 integration tests (Override O-2 line count + O-3 detail_steps prose absent + O-7 cross-card paired + O-14 line count math)
- [ ] Task 5.3: alignment test 4-point sync 갱신 (silent fallback 제거 Trap #13)
- [ ] Task 5.4: regression 확인 (G3.2 + G4.1 + G4.2 + G4.3 baseline 보존)

### Phase 6 (Wave 5) — canary execution
- [ ] G4.3 production canary 6-gate confirm (entry condition Override O-22)
- [ ] Task 6.1: baseline canary capture (v18)
- [ ] Task 6.2: candidate canary capture (v19)
- [ ] Task 6.3: 7 exit criteria 판정 (4 STRICT 0 + 1 token + 1 line count + 1 pytest)

### Phase 7 (Wave 6) — commit + dual review
- [ ] Task 7.1: single commit + push
- [ ] Task 7.2: Codex 리뷰 (R5 / iter 1)
- [ ] Task 7.3: Claude 리뷰 (R6 / iter 1)
- [ ] Task 7.4: dual review fix loop (iter 2 / iter 3)

---

## Wave Briefing Templates (subagent dispatch)

각 Wave 의 implementer subagent (모두 opus 4.7) 가 받을 briefing 의 minimum context.

### Wave 1-A briefing

```
Wave 1-A — render_prompt_card.py producer expansion (G4.4 continuity lift).

Inputs:
  - spec §2.2 (continuity_elements_used 3 신규 sibling policy dicts shape)
  - spec §4 (Lift Mapping Table — Section A/B/C → 3 sibling policy dicts)
  - plan Phase 1 Task 1.1 ~ 1.6
  - 14 carry traps (특히 Trap #1, #8, #9, #14)

Outputs:
  - 9 module-level constants (Task 1.1 — 정확한 tuple 값 spec §2.2 ground-truth 일치):
    * _CONTINUITY_GENERIC_PERSON_NOUNS (7 entry — generic Asian-default placeholders)
    * _CONTINUITY_FURNITURE_LAYOUT_TOKENS (7 entry — layout-specific only, ambient mood 제외)
    * _CONTINUITY_FRAMING_SCOPE_OPTIONS (2 entry: third_person / close_up)
    * _CONTINUITY_REF_USAGE_VALUES (3 entry: zoom_in_detail / exact_background / atmosphere_reference)
    * _CONTINUITY_ZOOM_FORBIDDEN_ADDITIONS_COUNT = 5
    * _CONTINUITY_ZOOM_REQUIRED_PHRASINGS_COUNT = 3
    * _CONTINUITY_EXACT_PERMITTED_ADDITIONS_COUNT = 2
    * _CONTINUITY_ATMOSPHERE_FORBIDDEN_IMPORTS_COUNT = 3
    * _CONTINUITY_ID_POLICY_CROSS_REF_LITERAL = "id_policy.body_part_focus_rule" (paired-string)
  - 6 required-keys tuples (cross_shot_id_sub 6 / ref_usage_top 3 / zoom 5 / exact 5 / atmosphere 4 / view_consistency 9)
  - compute_continuity_snapshot_hash() helper (Task 1.2 — sha256(json.dumps continuity_elements_used)[:16])
  - build_continuity_elements_used() 3 신규 sibling policy dicts (Task 1.3 — cross_shot_id_substitution_rule / ref_usage_constraints / view_consistency)
  - constraints G4.1 base 2 → G4.4 7 strings 확장 (Task 1.3)
  - _assert_continuity_elements_used_shape() 3 신규 sub-field strict superset 검증 (Task 1.4)
  - _assert_ref_usage_constraints_shape() nested helper (Task 1.4 R3-I3)
  - build_render_prompt_card() _card_metadata 4 신규 lift_status key (Override O-9) + 3 신규 rule_source key (Task 1.5)
  - module + builder docstring 갱신 (Task 1.6)

Pre-flight (R4-M1 carry from G4.3):
  - 작업 시작 전 `render_prompt_card.py` top 에 `import hashlib` + `import json` 존재 확인. 미존재 시 add (G4.3 carry 가정 verify).

함정 회피 (14 carry traps):
  - hardcoded `[]` / `or [] / or {}` 절대 금지 (Trap #1 / #6 — production drift cascade)
  - 시나리오 의존 0 (Trap #9 — 작품 고유명사 ban — sub-shape examples generic placeholders only: "An Asian man" / "a hand" / "a young woman")
  - default arg + None / [] 셋 명시 구분 시 _DEFAULT_SENTINEL = object() pattern (Trap #2)
  - module-level constant tuple 사용 (list 금지 — immutable, Trap #8)
  - **3 신규 sibling policy dicts 는 builder-static (Trap #14 / R4-M6 carry)** — `if visible_entities:` 또는 `if fixed_elements:` 같은 truthy guard 안 배치 절대 금지. fixed_elements=[] / previous_shot_refs=[] / forward_zoom_targets=[] (Test 33 / 34) 에서도 3 sibling policy dicts 모두 present 보장.
  - silent fallback 금지 (`feedback_no_silent_fallback.md` — Trap #1) — `card.get("continuity_elements_used", {})` 패턴 절대 금지

Constraints:
  - Override O-7 cross-card paired-string substring: _CONTINUITY_ID_POLICY_CROSS_REF_LITERAL 가 single source.
    view_consistency.id_policy_cross_ref_for_body_part_focus + ref_usage_constraints.zoom_in_detail.body_part_cross_ref 둘 다 동일 literal 사용.
    Wave 1-A 코드에 maintainer 코멘트 ("rename 시 G4.4 spec patch + id_policy 동시 rename 필요")
  - Override O-5 strict superset: 3 신규 sub-field required keys = `EXPECTED <= returned_keys`. extra keys 허용 (G4.5 forward-compat).
  - Override O-9 lift_status 4 신규 key 정확 명: continuity_cross_shot_id_substitution_lifted / continuity_ref_usage_constraints_lifted / continuity_view_consistency_lifted / continuity_constraints_extended.
  - rule_source 3 신규 key 정확 명 (Trap #12 / namespace 분리): cross_shot_id_substitution_rule / ref_usage_rule / view_consistency_rule.

Verification before return:
  - len + content 정확: _CONTINUITY_GENERIC_PERSON_NOUNS=7 / _CONTINUITY_FURNITURE_LAYOUT_TOKENS=7 / _CONTINUITY_FRAMING_SCOPE_OPTIONS=2 / _CONTINUITY_REF_USAGE_VALUES=3 / 4 count constants
  - build_continuity_elements_used(fixed_elements=[], previous_shot_refs=[], forward_zoom_targets=[]) 빈 입력에서 3 sibling policy dicts 모두 present + constraints == 7 (R2-I4 / O-5 carry)
  - view_consistency.mixing_forbidden = True (bool literal — `assert mixing_forbidden is True and type(mixing_forbidden) is bool` test 16 강제)
  - _CONTINUITY_ID_POLICY_CROSS_REF_LITERAL 가 view_consistency + ref_usage_constraints.zoom_in_detail 두 곳에 substring 포함
  - 4 lift_status key + 3 rule_source key 모두 _card_metadata 에 추가 (Override O-9)
```

### Wave 1-B briefing

```
Wave 1-B — v19 prompt 신규 디렉토리.

Inputs:
  - spec §3.1 / §3.2 / §3.3 / §3.4 (v18 3 prose 66 lines 위치 + ≤32-line compact section)
  - plan Phase 2 Task 2.1 ~ 2.4
  - 14 carry traps (특히 Trap #7, #10)

Outputs:
  - prompts/_base/scene_detail/19.<TIMESTAMP>/system.md (v18 - 66 lines + ≤14 lines compact section)
  - prompts/_base/scene_detail/19.<TIMESTAMP>/detail_schema.json (verbatim copy)
  - prompts/_base/scene_detail/19.<TIMESTAMP>/user.md (verbatim copy)
  - .wave_1b_timestamp.txt scratch file 에 timestamp 기록 (Wave 2 dispatcher 가 read)

함정 회피:
  - 덮어쓰기 금지 (`feedback_prompt_versioning.md` carry — Trap #10). v18 sealed.
  - line count 정확 — ≤ 32 lines strict (Override O-2 binding gate, Trap #7 — bullet 1 line, wrap 줄도 카운트)
  - Override O-7 cross-card paired-string substring 보존 — compact section 안 `id_policy.body_part_focus_rule` substring 1+ 등장
  - ✓/✗ 예시 미포함 (Override O-15 — token gate tight)
  - **timestamp lockstep (R4-I3 carry)**: scratch file `.wave_1b_timestamp.txt` 에 timestamp 값 기록 후 Wave 2 dispatcher 가 read. 1 char drift 시 4-point sync alignment fail.

Sections to delete (Override O-10 line range 정확):
  - Section A: lines 109-120 (heading 109 / blank 110 / bullets 111-115 / blank 116 / ✓/✗ 117-120) = 12 lines. Blank line 121 = 인접 섹션 보존.
  - Section B: lines 122-156 (heading 122 / blank 123 / intro 124 / blank 125 / zoom 126-141 / exact 143-149 / atmosphere 151-156, blank gap 142/150) = 35 lines. Blank line 157 = 인접 섹션 보존.
  - Section C: lines 308-326 = 19 lines. Blank line 307 / 327 = 인접 섹션 보존.

Compact section 본문 (spec §3.3 verbatim):
  ## Continuity (cross-shot substitution + ref_usage + single-view)

  `[RenderPromptCard v1]` 의 `continuity_elements_used` field 가 이 shot 의 연속성 정책을 결정한다 (card 가 primary contract — 충돌 시 card 우선).

  **핵심 원칙:**
  - 6 bullets (각 1 line — 80-col wrap 금지)
  - bullet 1: cross_shot_id_substitution_rule
  - bullet 2: zoom_in_detail
  - bullet 3: exact_background
  - bullet 4: atmosphere_reference
  - bullet 5: single_camera_rule (id_policy.body_part_focus_rule cross-card paired-string 포함)
  - bullet 6: prop_orientation_rule

  **충돌 해결**: ...

Verification before return:
  - 3 deletion target heading 모두 absent (`grep -n` == 0)
  - `## Continuity` heading present + ≤ 32 lines (`re.search(r"## Continuity.*?(?=\n## )", text, re.DOTALL)` content 만 추출 후 line count)
  - `id_policy.body_part_focus_rule` substring 1+ 등장
  - tiktoken cl100k_base 실측 token_delta = candidate - baseline `<= -540` (Override O-1 binding system-only, signed convention — Trap #6)
  - .wave_1b_timestamp.txt 작성 완료
```

### Wave 2 briefing

```
Wave 2 — detail_steps.py + version_registry 4-point sync + Override O-3 inline prose 삭제.

Inputs:
  - Wave 1-B 완료 (.wave_1b_timestamp.txt scratch file 존재)
  - plan Phase 3 Task 3.1 ~ 3.3
  - 14 carry traps (특히 Trap #5, #11)

Outputs:
  - `detail_steps.py:105` SCENE_DETAIL_PROMPT_VERSION v18 → "19.<TIMESTAMP>" (Wave 1-B timestamp 일치)
  - **`detail_steps.py:1555-1563` zoom_in_detail inline prose 9 줄 삭제 (Override O-3)**
  - `version_registry.py:34` MODULE_VERSIONS["scene_detail_composer"] 1.18.0 → 1.19.0
  - `version_registry.py:126` _MODULE_INFO["scene_detail_composer"]["prompt_dependency"] "scene_detail/v18" → "scene_detail/v19"

Pre-flight:
  - .wave_1b_timestamp.txt scratch file read (timestamp lockstep — Trap #11)
  - `has_zoom_in_detail` 변수 다른 사용처 검사: `grep -n "has_zoom_in_detail" backend/app/core/steps/detail_steps.py`
    * 본 9 줄 외 사용처 0 → 변수 정의도 정리 가능 (선택)
    * 본 9 줄 외 사용처 존재 → 변수 정의 보존, prose inject 만 삭제

함정 회피:
  - prompt_dependency 값 = "scene_detail/v19" (major suffix only — Trap #5 / R3R4-B1 carry)
  - _MODULE_INFO 는 dict access (NOT attribute — Trap #5 carry)
  - 4-point sync lockstep (Trap #11 / Override O-21 carry) — 4 곳 모두 갱신
  - SCENE_DETAIL_SCHEMA_VERSION (= 7) 변경 없음 (Override O-13 carry — cp shape 변경 0)

Override O-3 삭제 대상 (정확 line range):
  - `detail_steps.py:1555-1563` `if has_zoom_in_detail:` 블록 + 9 줄 prose + trailing `\n\n`:
    ```
            if has_zoom_in_detail:
                user_prompt += (
                    "[중요 — 확대 샷 원칙 (zoom_in_detail)]\n"
                    "이 샷은 앞 샷과 같은 순간·같은 공간·같은 피사체를 유지한 채 카메라가 확대/이동한 것입니다.\n"
                    "- 새 인물·새 소품·새 배경 요소를 추가하지 마세요.\n"
                    "- 앞 샷의 프레임에 없던 자세·표정·행동을 추가하지 마세요.\n"
                    "- t2i_prompt는 앞 샷이 담은 프레임 중 '확대된 영역'만 구체화하세요.\n"
                    "- 인체 부위 초점이면 보통명사로만 묘사(C##O## 금지 — 얼굴 참조 중첩 방지).\n"
                    "- 환경(조명·배경)이 동일하게 유지됨을 암시적으로 반영하세요.\n\n"
                )
    ```
  - 인접 forward_zoom_targets 블록 (line 1566+) 보존 (Override O-4 carry — G4.4 lift 대상 아님)

Verification before return:
  - SCENE_DETAIL_PROMPT_VERSION 가 .wave_1b_timestamp.txt timestamp 와 정확 일치
  - `grep -n "확대 샷 원칙" backend/app/core/steps/detail_steps.py` → (no output)
  - `grep -n "[중요 — 확대 샷 원칙 (zoom_in_detail)]" backend/app/core/steps/detail_steps.py` → (no output)
  - MODULE_VERSIONS["scene_detail_composer"] == "1.19.0"
  - _MODULE_INFO["scene_detail_composer"]["prompt_dependency"] == "scene_detail/v19"
```

### Wave 3 briefing

```
Wave 3 — canary scripts 5종.

Inputs:
  - spec §5.1 / §5.3 (4 metric + 1 token)
  - plan Phase 4 Task 4.1 ~ 4.5
  - 14 carry traps (특히 Trap #1, #3, #4, #6, #8)

Outputs:
  - scripts/canary/g4_4_view_mixing.py (~150 LOC, 모든 shot scope)
  - scripts/canary/g4_4_atmosphere_no_layout_import.py (~140 LOC, atmosphere shot only + multi-ref filter)
  - scripts/canary/g4_4_zoom_in_detail_no_new_entity.py (~155 LOC, zoom_in_detail shot only)
  - scripts/canary/g4_4_token_count.py (~95 LOC, tiktoken cl100k_base)
  - scripts/canary/g4_4_double_description.py (~145 LOC, id-mapping shot only)

공통 패턴:
  - module-level constants import (Trap #8): _CONTINUITY_GENERIC_PERSON_NOUNS / _CONTINUITY_FURNITURE_LAYOUT_TOKENS / _CONTINUITY_FRAMING_SCOPE_OPTIONS / _CONTINUITY_REF_USAGE_VALUES / _CONTINUITY_ID_POLICY_CROSS_REF_LITERAL / _ID_BODY_PART_TRIGGERS (G4.3 re-import) / compute_continuity_snapshot_hash
  - `_load_scenes_or_fail()` + `_validate_scene_preflight()` helper (R3R4-B2 carry)
  - CP shape: scene["render_prompt_card"]["continuity_elements_used"] (Trap #3)
  - silent absorb 금지 (Trap #1) — `or [] / or {}` 패턴 제거
  - pinned tuple seen_in_cp post-loop validation (Trap #4)
  - t2i_variations + per-variation t2i_prompt validation (Override O-4 / O-5 carry)
  - ±50 chars window proximity (sentence regex 사용 금지 — `feedback_no_regex_postprocessing.md` / Override O-17 carry)
  - gate fail → exit 1 (Override O-7 carry)

pinning 7 fields (Override O-12):
  - pid / scene_index_list / shot_index_list_per_scene / model_routing / prompt_source_mode / card_commit_hash / continuity_card_snapshot_hash

Override O-11 multi-ref filter (g4_4_atmosphere_no_layout_import.py):
  shot-level filter: `any(ref.ref_usage == 'atmosphere_reference' for ref in previous_shot_refs)`. multi-ref 시 atmosphere check 만 적용. exact_background co-presence false-positive baseline 비0 → P1 follow-up.

Override O-17 regex brittleness (g4_4_view_mixing.py):
  regex 는 1차 detector 만, baseline 비0 시 P1 follow-up 으로 LLM-validator (gpt-5.4-mini judge) 전환 검토.

함정 회피:
  - silent fallback 금지 (Trap #1) — measurement_failures 누적 후 exit 1
  - module-level constants 직접 import (Trap #8) — 7 entry generic noun list / 7 entry furniture token / etc. 모두 hardcoded 작성 금지
  - 시나리오 의존 0 (Trap #9) — 작품 고유명사 절대 금지
  - LLM 입력 truncation 금지 — t2i_prompt 전문 scan, deepcopy lossless

Verification before return:
  - 5 scripts 모두 module-level constants import (drift 차단)
  - sentinel pre-pass + pinned-tuple post-loop validation 5 scripts 모두 적용
  - JSON output schema spec §5.3 일치 (pinning 7 field — continuity_card_snapshot_hash 포함)
  - gate fail → exit 1 (return 0 금지)
```

### Wave 4 briefing

```
Wave 4 — unit + integration + alignment tests.

Inputs:
  - spec §6.1 / §6.2 / §6.4
  - plan Phase 5 Task 5.1 ~ 5.4
  - Wave 1-A / 1-B / 2 / 3 모두 완료
  - 14 carry traps (특히 Trap #2, #13, #14)

Outputs:
  - backend/tests/unit/test_g4_4_continuity_lift.py (~46 unit tests, table 46 enumerate per Override O-16, no buffer needed — plan-R3+R4 4+4 nested validator tests 추가)
  - backend/tests/integration/test_g4_4_continuity_integration.py (~17 integration tests)
  - backend/tests/prompts/test_scene_detail_continuity_alignment.py (4-point sync alignment, 신규 또는 갱신)

pytest selector:
  - `pytest backend/tests/ -k "g3_2 or g4_1 or g4_2 or g4_3 or g4_4" -v`
  - alignment test 명시 path: `backend/tests/prompts/test_scene_detail_continuity_alignment.py`

함정 회피:
  - hardcoded `[]` fixture 절대 금지 (Trap #14 — production drift cascade)
  - `_DEFAULT_SENTINEL = object()` pattern (Trap #2 carry)
  - 각 unit test 가 실제 build_continuity_elements_used() / build_render_prompt_card() call (mock 금지, hardcoded result dict 금지)
  - alignment test silent fallback 제거 (Trap #13) — `MODULE_VERSIONS.get(..., "")` / `_MODULE_INFO.get(..., {})` 금지

Mandatory tests (Override O-5 / O-7 / O-11 / O-16 / Trap #14 / R4-I3):
  - Test 16: `test_continuity_view_consistency_mixing_forbidden_is_bool_true` (bool int subclass trap carry)
  - Test 9 / 18: `test_continuity_ref_usage_zoom_in_detail_body_part_cross_ref_string_present` + `test_continuity_view_consistency_id_policy_cross_ref_string_present` (Override O-7 paired substrings)
  - Test 19: `test_continuity_cross_card_pair_substrings_match` (Override O-7 paired drift 차단)
  - Test 33: `test_continuity_fixed_elements_empty_list_valid_with_static_subfields` (Override O-5 / R2-I4)
  - Test 34: `test_continuity_4_subfields_independent_of_input_lists` (Trap #14)
  - Test 35: `test_g4_4_view_mixing_canary_multi_ref_shot_filter` (Override O-11)
  - Test 36: `test_card_metadata_lift_status_includes_continuity_keys` (Override O-9 — 4 정확 key명)
  - Test 38: `test_card_metadata_excluded_from_hash_after_g4_4_keys_added` (canonical name — content selector pattern, 인덱스 의존 금지)
  - Test 39-42: `test_assert_continuity_shape_validates_*` (4 shape validator tests — R4-I3 enumerate)
  - Test 43-46: `test_assert_ref_usage_constraints_shape_*` (4 nested validator tests — R4-I3 enumerate)

Integration mandatory:
  - test 8: `test_v19_continuity_section_line_count_under_32` (Override O-2 binding — `re.search(...).group()` 후 split lines `<= 32`)
  - test 12: `test_detail_steps_zoom_in_detail_inline_prose_absent_after_g4_4` (Override O-3 — canonical name from M1, substring 부재)
  - test 17: `test_cross_card_paired_substring_in_v19_system_md_and_card_inject` (Override O-7 cross-card paired)

Alignment test (silent fallback 제거 — Trap #13 carry):
  - `assert "scene_detail_composer" in MODULE_VERSIONS` (키 존재 강제)
  - `assert MODULE_VERSIONS["scene_detail_composer"] == "1.19.0"` (silent fallback 제거)
  - `assert _MODULE_INFO["scene_detail_composer"]["prompt_dependency"] == "scene_detail/v19"` (Trap #5 — major suffix only)

Verification before return:
  - 38-46 unit + ~17 integration test count 일치 (R4-I3 enumerate)
  - test 16 bool literal strict 검증
  - test 9 / 18 / 19 + integration test 17 모두 _CONTINUITY_ID_POLICY_CROSS_REF_LITERAL substring 검증
  - test 33 / 34 builder-static 빈 입력 검증
  - test 39-42 + 43-46 shape validator 8 tests 강제 (R4-I3)
  - alignment test silent fallback 제거
  - regression: G3.2 + G4.1 + G4.2 + G4.3 baseline 보존 (0 regressions)
```

### Wave 5 briefing

```
Wave 5 — production canary execution (사용자 실행, Override O-18).

Inputs:
  - Wave 1-A ~ 4 완료
  - G4.3 production canary 6-gate 통과 (entry condition Override O-22)
  - plan Phase 6 Task 6.1 ~ 6.3

Pre-flight:
  - G4.3 production canary 6-gate confirm — 미실행 시 G4.4 commit gate 차단
  - PID 결정 (baseline scene set 요건 만족: id-mapping ≥ 5 + zoom_in_detail ≥ 2 + atmosphere_reference ≥ 1)

Outputs:
  - docs/canary/g4_4_view_mixing_baseline_*.json + candidate_*.json
  - docs/canary/g4_4_atmosphere_baseline_*.json + candidate_*.json
  - docs/canary/g4_4_zoom_baseline_*.json + candidate_*.json
  - docs/canary/g4_4_double_baseline_*.json + candidate_*.json
  - docs/canary/g4_4_token_baseline_*.json + candidate_*.json
  - docs/canary/g4_4_continuity_summary_*.json (통합 결과)

Steps:
  1. Baseline (v18) capture — pinning 7 field 명시 + 5 canary scripts 실행
  2. Candidate (v19) capture — 동일 pinning + 동일 scene_set force 재실행
  3. Comparison: pinning block dict equality 한 번 + 7 gate 판정
     - Gate 1: view_mixing_count == 0 strict (Override R3-I1)
     - Gate 2: atmosphere_layout_import_count == 0 strict (Override O-11 multi-ref filter)
     - Gate 3: zoom_in_detail_new_entity_count == 0 strict
     - Gate 4: double_description_count == 0 strict
     - Gate 5: token_delta_system <= -540 (Override O-1 binding system-only, signed)
     - Gate 6: continuity_section_line_count <= 32 (Override O-2 binding)
     - Gate 7: pytest G4.4 suite 0 failures

Override O-1 binding gate revise 정책:
  - Wave 1-B 실측이 더 강한 수치 (e.g. -480) 로 revise 가능 — Round 2 Override row 로만 변경
  - upward revise (e.g. -300) 는 re-audit 필요

Override O-16 carry: STRICT 0 metric 4종 — baseline 비0 이어도 candidate 0 강제. baseline 비0 면 P1 follow-up 등록 (G4.4 scope 밖, 별도 production fix 추적).

Verification before return:
  - 7 gate 모두 pass
  - pinning block 7 field baseline / candidate 일치
  - JSON output schema spec §5.3 일치
```

### Wave 6 briefing

```
Wave 6 — single commit + push + dual review fix loop (Override O-18).

Inputs:
  - Wave 5 통과 (7 gate green)
  - plan Phase 7 Task 7.1 ~ 7.4

Outputs:
  - single squash commit (commit message format spec §7.1)
  - main branch push
  - Codex 리뷰 (R5 / iter 1) 결과
  - Claude 리뷰 (R6 / iter 1) 결과
  - 3 iter dual review fix loop 완료

commit 대상 파일 (Phase 7 Task 7.1 표 carry):
  - render_prompt_card.py (~310 LOC)
  - detail_steps.py (1 line + 9 lines deletion)
  - version_registry.py (~6 LOC)
  - prompts/_base/scene_detail/19.<TIMESTAMP>/ (3 files)
  - scripts/canary/g4_4_*.py (5 scripts)
  - backend/tests/unit/test_g4_4_continuity_lift.py
  - backend/tests/integration/test_g4_4_continuity_integration.py
  - backend/tests/prompts/test_scene_detail_continuity_alignment.py
  - docs/canary/g4_4_*.json (Wave 5 결과)

3 iter fix loop:
  1. Codex + Claude 리뷰 결과 통합 → R5+R6 round Override 표 추가
  2. BLOCKING 모두 fix → Task 7.2 / 7.3 재실행
  3. 통과 시 follow-up commit only after push (NEVER --amend post-push — G4.3 R3-M3 carry / `feedback_review_before_commit.md` / Git Safety Protocol)
  4. 3 iter 통과 못 하면 Wave 1-A 또는 Wave 1-B 부분 재구현 검토

Codex review focus (Task 7.2):
  - 9 module-level constants tuple 값 정확 (set equality + len ground-truth)
  - 3 sibling policy dicts assemble 로직 + sub-field shape contract (Override O-5 superset)
  - _assert_ref_usage_constraints_shape() nested validator 검증 로직 누락 없음 (R3-I3)
  - compute_continuity_snapshot_hash() deterministic + None handling
  - SCENE_DETAIL_PROMPT_VERSION timestamp lockstep
  - Override O-3 inline prose 9 줄 정확 삭제
  - 4-point sync (MODULE_VERSIONS 1.19.0 + prompt_dependency v19 — Trap #5)
  - tests 가 실제 build_continuity_elements_used() call (hardcoded result dict 금지 — Trap #14)
  - canary scripts module constants import + ±50 chars window + sentinel + pinned-tuple + exit 1

Claude review focus (Task 7.3):
  - 3 sibling policy dicts shape spec §2.2 1:1 일치 + builder-static
  - v19 system.md ≤32-line compact section spec §3.3 동일 + line count 정확
  - detail_steps.py:1555-1563 prose 정확 삭제 (Override O-3)
  - integration test substring presence/absence 정확 (test 1-9 + test 14)
  - 4-point sync alignment test silent fallback 제거 (Trap #13)
  - cross-card paired-string substring drift 차단 (Override O-7)
  - Trap #14 (builder-static) test 33 / 34 강제

Verification before push:
  - Phase 6 7 gate 모두 pass
  - regression: G3.2 + G4.1 + G4.2 + G4.3 baseline 0 regressions
  - alignment test green
  - commit message format spec §7.1 따라 작성
```

---

## Critical Details

### Error Handling (silent fallback fail-fast — Trap #1)

- Producer (`build_continuity_elements_used()`): None 입력 → AppError. silent fallback 절대 금지.
- Helper (`compute_continuity_snapshot_hash()`): card 가 dict 아니거나 `continuity_elements_used` key 부재 → AppError.
- Validator (`_assert_continuity_elements_used_shape()` / `_assert_ref_usage_constraints_shape()`): 3 신규 sub-field / nested sub-key / required key 누락 시 AppError.
- Canary scripts: `data.scenes` / `t2i_variations` / `t2i_prompt` 누락 → measurement_failures 누적 후 exit 1.
- Tests: hardcoded `[]` fixture 절대 금지 — `_DEFAULT_SENTINEL = object()` pattern.
- Alignment test: `MODULE_VERSIONS.get(..., "")` / `_MODULE_INFO.get(..., {})` 금지 — 명시적 키 존재 + 타입 검증 (Trap #13).

### State Management

- `_card_metadata` 는 envelope sibling top-level. `canonicalize_render_prompt_card()` 가 hash 입력에서 strip. `detail_steps.py` inject 직전 strip.
- `compute_continuity_snapshot_hash()` 는 partial canonicalize hash (continuity_elements_used 만 — `_card_metadata` 자연 제외).
- 4 신규 lift_status key + 3 신규 rule_source key 는 모든 ref_usage 에서 동일 (continuity_elements_used 는 ref_usage 무관 동일 적용 — spec §2.2 matrix single row).
- `SCENE_DETAIL_SCHEMA_VERSION` (= 7) 변경 없음 (cp shape 변경 0).
- `CARD_SCHEMA_VERSION` (= 1) 변경 없음 (envelope shape 변경 0).
- `SCENE_DETAIL_PROMPT_VERSION` v18 → v19 bump 가 cp `_config_hash` 변경 → resume 시 force escalate.

### Testing

- ~46 unit tests + ~17 integration tests + alignment test 4-point sync 신규.
- Override O-5 superset 검증 (`set(returned) >= EXPECTED_SET`) — extra keys 허용 (G4.5 forward-compat).
- Override O-7 cross-card paired-string substring (test 17 / 18 / 9 + integration test 9 / 13 — 4 곳 검증).
- Override O-11 multi-ref filter (test 19).
- Override O-9 lift_status 4 신규 key 정확 명 (test 35).
- Trap #14 builder-static (test 33 / 34) — 빈 입력에서도 3 sibling policy dicts 모두 present.
- Trap #13 alignment test silent fallback 제거.
- bool int subclass trap (test 16) — `mixing_forbidden is True and type(...) is bool` strict.
- regression: G3.2 + G4.1 + G4.2 + G4.3 baseline 0 regressions.

### Performance

- `build_continuity_elements_used()` 추가 LOC ~180 (3 sibling policy dicts + 7 constraints) — single shot build 시간 영향 미미 (~1ms 미만 추정).
- v19 prompt token reduction (~ -540 system-only binding gate) — LLM call latency / cost 감소.
- 5 canary scripts 실행 시간 ~5분 (5 scenes × 3 shots × 2 variations × 4 metrics × ~5s each).
- `compute_continuity_snapshot_hash()` 호출 비용 무시 가능 (sha256 single dict).

### Security / Correctness

- LLM 입력 truncation 절대 금지 (CLAUDE.md 절대 규칙) — t2i_prompt 전문 scan, deepcopy lossless.
- 시나리오 의존 0 (Trap #9 / `feedback_no_scenario_names.md`) — sub-shape examples generic placeholders only.
- `feedback_no_silent_fallback.md` — Producer / canary / tests 모두 fail-fast.
- `feedback_no_regex_postprocessing.md` (Override O-17) — regex 1차 detector 만, 의미 판단은 LLM-first.
- `feedback_prompt_versioning.md` (Trap #10) — v18 sealed, 새 dir `19.<TIMESTAMP>/`.
- 4-point sync lockstep (Trap #11 / Override O-21) — 4 곳 lockstep 갱신.
- silent-fallback 잔존 carry (Override O-8) — version_registry.py:148-155 = G4.5 또는 별도 cleanup PR follow-up. G4.4 신규 코드 새 silent-fallback 도입 절대 금지.

### G4.5 Entry Condition (spec §7.3 carry)

G4.5 (final prompt slimming pass) 는 다음 모두 충족 후 진입:

- G4.4 4 metric STRICT (double-description / zoom_in_detail entity 추가 / atmosphere_reference layout import / view-mixing) 통과
- G4.4 token delta gate `≤ -540` system-only 통과 (Override O-1 binding)
- G4.4 v19 compact section line count gate `≤ 32 lines` 통과 (Override O-2 binding)
- G4.4 unit + integration test suite green
- v19 prompt push + 4-point sync 완료

G4.5 가 다루는 scope: final prompt slimming pass — G4.2-4 lift 후 잔여 prose heading count ≤ 10 enforcement + 추가 token 절감 + 미 lift 된 reasoning prose 통합. G4.4 와 별개 phase.

---

## Data Flow (signature-level: build_continuity_elements_used → _card_metadata strip → JSON prepend)

**Override O-13 carry — concrete wire 명시**:

```
Step 1 — Producer (render_prompt_card.py:676):
  build_continuity_elements_used(
      fixed_elements: List[Dict[str, Any]],     # scene_consistency CP (G4.1 carry)
      previous_shot_refs: List[Dict[str, Any]], # shot_dependency_t2i CP (G4.1 carry)
      forward_zoom_targets: List[Dict[str, Any]], # G4.1 carry
  ) -> Dict[str, Any]
  # returns:
  #   {
  #     "fixed_elements": [...],                 # G4.1 carry
  #     "previous_shot_refs": [...],             # G4.1 carry
  #     "forward_zoom_targets": [...],           # G4.1 carry
  #     "cross_shot_id_substitution_rule": {...}, # G4.4 신규 (6 keys)
  #     "ref_usage_constraints": {                # G4.4 신규 (3 sub-key dict)
  #       "zoom_in_detail": {...},                # 5 keys
  #       "exact_background": {...},              # 5 keys
  #       "atmosphere_reference": {...},          # 4 keys
  #     },
  #     "view_consistency": {...},                # G4.4 신규 (9 keys)
  #     "constraints": [..., ..., ..., ..., ..., ..., ...],  # G4.1 base 2 → G4.4 7
  #   }

Step 2 — Wrapper (render_prompt_card.py build_render_prompt_card):
  build_render_prompt_card(...) -> Dict[str, Any]
  # returns full card with 7 envelope fields + _card_metadata sibling.
  # _card_metadata.lift_status 에 4 G4.4 신규 key 추가 (Override O-9).
  # _card_metadata.rule_source 에 3 G4.4 신규 key 추가.
  # _assert_continuity_elements_used_shape() 가 strict superset 검증 (Override O-5).

Step 3 — Hash (render_prompt_card.py canonicalize):
  canonicalize_render_prompt_card(card) -> Dict
  # _card_metadata 를 top-level pop (G4.1/G4.2 carry — hash 영향 0).
  compute_card_hash(card) -> str  # envelope 7 field hash
  compute_continuity_snapshot_hash(card) -> str  # G4.4 partial hash, Override O-12

Step 4 — Inject (detail_steps.py:~1500-1610):
  user_prompt = "..."  # base prepend blocks
  # ... [중요 — 순간 고정] block (line ~1549-1554, G4.4 보존)
  # OVERRIDE O-3: detail_steps.py:1555-1563 zoom_in_detail inline prose 9 줄 삭제
  # ... forward_zoom_targets prose (line 1566-1607, Override O-4 G4.4 carry)
  # ... fixed_elements prose (line ~1609-)
  # _card_metadata strip 직전:
  card_for_inject = deepcopy(card)
  card_for_inject.pop("_card_metadata", None)  # G4.2 carry
  user_prompt += json.dumps(card_for_inject, ...)

Step 5 — LLM consumer (scene_detail step):
  scene_detail step calls LLM with user_prompt.
  v19 system.md `## Continuity` compact section 가 reasoning summary carry.
  card 의 3 sibling policy dicts + 7 constraints 가 single source of truth.

Step 6 — Post-parse (detail_steps verify):
  parse LLM output → t2i_prompt + outlook + ...
  card hash → cp.render_prompt_card_hash 비교 (drift 차단)
```

**누락된 wire (Wave 2 삭제 대상 — Override O-3)**: `detail_steps.py:1555-1563` `if has_zoom_in_detail:` 블록 9 줄 inline prose. card `ref_usage_constraints.zoom_in_detail` 5 required keys (forbidden_additions / required_phrasings / body_part_cross_ref / scope_summary / rationale_summary) 가 동일 데이터 carry — prose 9 줄 삭제 후 LLM 행동 fallback 검증은 Wave 5 production canary `g4_4_zoom_in_detail_no_new_entity.py` (STRICT 0).

---

## Spec ↔ plan drift quick-check (commit gate)

Phase 7 Task 7.1 commit 직전 다음 cross-reference 검증:

| Spec 위치 | Plan 위치 | 일치 검증 |
|---|---|---|
| spec §0 Round Override 표 (O-1 ~ O-19) | plan §"Spec R1+R2 Override carry" 표 | 19 row 모두 inline 적용 |
| spec §2.2 sub-shape contract (3 sibling policy dicts + 6 required-keys tuples) | Phase 1 Task 1.3 + 1.4 + Task 5.1 unit test | required_keys 정합 |
| spec §3.2 deletion target line range (109-120 / 122-156 / 308-326) + Override O-10 footnote | Phase 2 Task 2.1 | line range 정확 |
| spec §3.3 compact section ≤ 32 lines (Override O-2) | Phase 2 Task 2.2 + Task 2.3 | bullet 1 line + 14 lines target |
| spec §5.1 4 metric STRICT 0 + token gate ≤ -540 system-only | Phase 4 Task 4.1 ~ 4.5 + Phase 6 Task 6.3 | 7 gate 명시 |
| spec §6.1 46 enumerate (Override O-16, plan-R3+R4 audit — 4+4 nested shape validator) | Phase 5 Task 5.1 | ~46 unit count (or 38-46 if buffer ambiguity acceptable) |
| spec §6.2 17 integration tests | Phase 5 Task 5.2 | 17 enumerate |
| spec §6.4 4-point sync alignment | Phase 3 + Phase 5 Task 5.3 + Trap #11 | 4 곳 lockstep |
| spec §7.2 Wave decomposition (1-A / 1-B / 2 / 3 / 4 / 5 / 6) | plan §Wave Decomposition Table | 7 Wave 일치 |
| spec §8 Risk Register | plan §Risk Register / Rollback | G4.4-unique + G4.1+G4.2+G4.3 carry 모두 inline |
| spec §12 Override Table (G4.2 + G4.3 carry + G4.4-new) | plan §Override Table | 3 section 모두 verbatim copy |

drift 발견 시 **즉시 plan R5 round** 진행. body 동기화 패치 후 commit.

---

## Notes for Implementer

본 plan 은 dual-audited (Codex R3 + Claude R4) 후 Wave 1-A / 1-B / 2 / 3 / 4 sequential subagent dispatch. 각 Wave subagent (모두 opus 4.7) 가 받을 briefing 은 위 Wave Briefing Templates 섹션 참조 — self-contained.

Phase 6 (Wave 5) production canary execution 은 사용자 실행. Phase 7 (Wave 6) commit + dual review fix loop 은 commit gate 통과 후.

14 carry traps 모두 본 plan body 안 relevant Phase 에 명시. G4.2 + G4.3 패턴 답습 — 새 silent-fallback 도입 / 시나리오 의존 / regex post-processing / hardcoded `[]` fixture 발견 시 BLOCKING.

main HEAD = `51d6c5e` (G4.3 push). G4.4 commit 은 main 에 single squash commit + push.

---
