# G4.5a Spatial Rules Lift Design

본 문서는 G4 spec (`docs/superpowers/specs/2026-05-04-g4-render-prompt-card-strategy.md`)
§7 G4.5 섹션의 **첫 단계 (G4.5a)** detail sub-spec 이다. G4.4 continuity lift
(commit `b1c5032`) 의 자연 후속.

- 작성일: 2026-05-05
- 상태: initial draft (R1+R2 dual audit 미진행)
- 선행 완료:
  - G4.1 RenderPromptCard bootstrap (5d045f3 + c656ce3 + 86473df)
  - G4.2 background-binding lift (14d14cb — Rule A/C/E → `background_binding`,
    v17 compact section, 4 canary, 25+10 tests)
  - G4.3 ID-policy lift (51d6c5e — 109 prose lines → `id_policy` 4 sub-fields,
    v18 compact section, 5 canary, 50+18 tests, tiktoken Δ-2073)
  - G4.4 continuity lift (b1c5032 — 75 prose lines → 3 sibling policy dicts +
    extended constraints, v19 compact section, 5 canary, 76 tests, tiktoken
    Δ-1010)
- 목적: v19 system.md 의 3 spatial-rules prose section (약 124 lines —
  Rule F line 144-176 (33L) + Rule G line 177-217 (41L) + Rule J line
  218-267 (50L)) 을 **기존 5-field envelope contract 안의 sub-shape** 로
  lift, system.md 에서 해당 long-form sections 제거 후 compact "Spatial
  consistency" reference section 으로 교체. **새 top-level field 도입 절대
  금지** (사용자 binding) — `render_strategy` 내부 nested dict
  `spatial_consistency` 1 sibling 으로 흡수. 3 metric STRICT 0 검증 +
  tiktoken 실측 token delta gate ≤ -<value> (R1+R2 audit 에서 정정).

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

본문보다 우선 적용. Codex (R1) + Claude (R2) 라운드 감사 + lead/user 결정 결과를
본문 반영 전 fast-path 로 박는다. 본문은 정식 패치에서 동기화하되, 그 전까지
본 표가 진실의 source 다.

### Round 1 — Codex (R1) + Claude (R2) dual audit 결과 (2026-05-05)

R1 verdict: `NEEDS_REVISION` (R1-B1 + R1-I1 + R1-I2 + R1-M1). R2 verdict:
`NEEDS_REVISION` (R2-B1 + R2-B2 + R2-B3 + R2-I1 + R2-I2 + R2-I3 + R2-I4 + R2-I5
+ R2-M1 + R2-M2 + R2-M3 + R2-M4).

**Override numbering convention (RO-7 binding)**: Round 1 Override IDs prefix
**RO-N** (RO-1, RO-2, ...). §12 carry IDs prefix **O-N** (G4.2/G4.3/G4.4
carry). body 본문 references must prefix `RO-` 또는 `O-` to disambiguate.

| ID | Source | 결정 (Override) | 적용 위치 |
|---|---|---|---|
| **RO-1** | R1-I2 + R2-I1 disagreement | **Token gate threshold = system-only `token_delta = candidate - baseline ≤ -1000`** (binding 1차 estimate-based gate). R2 분석: G4.4 estimate→actual realization ≈83%, G4.5a estimate -1665 → actual ~-1380 → **28% margin over -1000 gate, sufficient**. Wave 1-B 실측 후 Round 2 Override 로 tighten 가능 (e.g. -1300). secondary report-only `system+inject ≤ -800` (no gate). §3.4 margin claim "65%" → "28%" 정정. | §3.4 + §5.1 + §5.3 + §8.1 |
| **RO-2** | G4.4 Override O-2 carry | **§3.3 v20 `## Spatial consistency` compact section `wc -l` ≤ 32 lines strict** (binding). 각 bullet 1줄 강제 — 80-col wrap 허용 안 됨 (wrap 줄도 line 1 로 카운트). 통합 테스트 §6.2 `test_v20_spatial_consistency_section_line_count_under_32` 강제. | §3.3 + §6.2 |
| **RO-3** | R1-B1 | **Rule F core_principles 누락 fix**: §2.2 `camera_frame_rule` 에 explicit `core_principles` list (4 entries) 추가 — v19 line 170-175 의 4 핵심 원칙 (camera 바닥 룰 / 머리 위 룰 / 허리 룰 / full body shot 룰) 모두 verbatim carry. 단위 테스트 4 entries count strict (`test_camera_frame_rule_core_principles_count == 4`) + 각 entry substring assertion 4종 (`test_camera_frame_rule_core_principles_substring_*` × 4). §4 Lift Mapping Table line 170-175 row 변경: `consistency_check_summary` (single string) → `core_principles` (4-entry list). required keys 6 → 7. | §2.2 + §4 + §6.1 + §8.1 |
| **RO-4** | R1-I1 + R2-B1 | **framing_scale enum drift fix — card-only classification 채택**: `_derive_framing_scale()` producer 는 G4.5a 변경 없음 (close/medium/insert 유지 — wide 미생성). `primary_framing_rule.applies_when` 텍스트 = "framing_scale ∈ {close, medium, insert} from `_derive_framing_scale()`. wide-class shots are mapped to medium and use `wide_medium_rules`. The LLM may *additionally* infer wide from prose keywords (`wide shot`/`전경`/...) for stricter checking — these are LLM-side reasoning hints, not the production framing_scale enum." §2.2 line 202 (envelope shape) + line 256 (applies_when) + §5.1 line 599 + Glossary `Framing scale keywords` 모두 동기화. Producer 변경 = G4.5c 또는 follow-up. | §1.2 + §2.2 + §2.3 + §5.1 + §9 |
| **RO-5** | R2-B2 | **Korean close-framing keyword conflict — 분리 채택**: card `framing_scale_keywords.close` LLM-facing 11-entry list 그대로 carry (LLM reasoning hint). 단 canary `g4_5a_primary_framing.py` 의 close-framing shot scope filter 는 production `_CLOSE_FRAMING_RE` 의 9-entry alignment 재사용 (`close-up`, `CU`, `MCU`, `ECU`, `XCU`, `extreme close-up`, `medium close-up`, `클로즈업`, `손가락이`). §2.2 + §5.1 + §10 Q5 모두 명시 — LLM hint vs canary detector 분리 strict. unit test `test_primary_framing_rule_close_keywords_count == 11` (LLM-facing 11-entry list) + canary smoke fixture `test_canary_close_framing_filter_uses_production_close_framing_re` (9-entry production-aligned). | §2.2 + §5.1 + §6.1 + §10 |
| **RO-6** | R2-B3 + Override O-31 carry | **Cross-card reference — Wired 채택 (option 1)**: `primary_framing_rule.close_framing_rules.body_part_focus_cross_ref` + `primary_framing_rule.wide_medium_rules.body_part_focus_cross_ref` 둘 다 paired-string `"see id_policy.body_part_focus_rule"` 포함. `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` constant 재사용 (rename G4.5c 또는 follow-up — Override O-31 footnote). 2 unit tests (`test_primary_framing_rule_close_body_part_cross_ref_paired` + `test_primary_framing_rule_wide_medium_body_part_cross_ref_paired`) + 1 integration test (`test_cross_card_paired_substring_in_v20_system_md_and_card_inject`). | §2.2 + §3.3 + §6.1 + §6.2 |
| **RO-7** | R2-I2 | **Override numbering scheme**: §0 Round Override = `RO-N` (RO-1 ~ RO-14). §12 carry = `O-N` (O-1 ~ O-38 G4.2/G3.3/G4.4 carry). body 본문 references — 기존 G4.4 carry "Override O-1 (token gate)" / "Override O-2 (line count)" / "Override O-14 (line-count derivation)" 등은 G4.4 spec §12 carry 의 동일 의미 row 가리키므로 G4.4 Override O-N 으로 carry-prefix 명시. **G4.5a Round 1 결정사항은 RO-N**. 메타룰 §0 추가 (위 convention block). | §0 + §3.4 + §5.1 + §6.2 |
| **RO-8** | R2-I3 | **same-character-ID detection algorithm**: §5.1 `g4_5a_primary_framing.py` canary 명시 — per-shot scan, build dict `{character_id: [token_positions]}`. For each `body_part_close_up_trigger` token position, find nearest C##/C##O## within ±N=20 tokens — `cid_trigger`. For each `full_body_description_keyword` (`stands / seated / leaning / 전신`) position, find nearest character ID — `cid_body`. If `cid_trigger == cid_body` AND both nearest-character-distance ≤ 20: increment violation count. unit test fixture covers 3 cases: (a) same-id co-occurrence → violation, (b) different-id → no violation, (c) > 20 tokens distance → no violation. Wave 3 implementer follows this algorithm verbatim. | §5.1 + §6.1 + §7.2 |
| **RO-9** | R2-I4 (Q9 commit) | **`_assert_primary_framing_rule_shape()` separate helper 채택**: nested helper as sibling to `_assert_spatial_consistency_shape()`. §7.2 Wave 1-A Step 3 명시 + §6.1 unit tests `test_assert_primary_framing_rule_shape_validates_*` 4 rows 추가 (framing_scale_keywords / close_framing_rules / wide_medium_rules / cross-ref strings). §8.1 risk row 추가. G4.4 `_assert_ref_usage_constraints_shape` nested helper 패턴 carry. | §6.1 + §7.2 + §8.1 |
| **RO-10** | R2-I5 | **Section boundary trailing-blank policy**: §3.2 명시 — "G4.5a policy: 삭제 섹션의 trailing blank 한 개 (line 142, 217, 268 트레일링 blank — Rule F end / Rule G end / Rule J end) 모두 drop. Inserted compact section starts on its own blank line. Net: 인접 섹션 사이 1 blank line 유지 (v19 §1-§2-§3 convention 일관)." Wave 1-B mechanical 적용. | §3.2 |
| **RO-11** | R2-M1 | **`_SPATIAL_BODY_PART_TRIGGERS` cross-import naming 정정**: §5.4 명시 — "G4.5a 는 G4.3 의 `_ID_BODY_PART_TRIGGERS` 를 alias-import 로 직접 재사용 (single source — drift 차단). G4.5a 내부에서 `_SPATIAL_BODY_PART_TRIGGERS` 별칭 도입 금지 — 모든 import 는 `from backend.app.core.steps.render_prompt_card import _ID_BODY_PART_TRIGGERS as _ID_BODY_PART_TRIGGERS_G4_3_CARRY` 또는 직접 사용." 별칭 도입 X. | §5.4 + §7.2 |
| **RO-12** | R1-M1 + R2-M2 | **Unit test count alignment**: §6.1 line 823 "총 unit tests: ~30-35" → "~38 (table 38 enumerate, +5 fix-loop buffer for plan-R3+R4 audit additions = ~38-43 final)". §10 Q7 동기화. test count prose 가 enumerate count 와 일치. | §6.1 + §10 |
| **RO-13** | R2-M3 | **`_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` namespace footnote**: §12 Override O-31 row 명시 — "G4.5a reuses `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` directly (G4.4 carry, not renaming). Future cleanup: rename to `_ID_POLICY_BODY_PART_FOCUS_CROSS_REF` in G4.5c or separate cleanup PR." follow-up note. | §12 (O-31) |
| **RO-14** | R2-M4 | **Test count baseline disambiguation**: §6.3 line 870 "G4.4 끝 약 2179+ pass" → "G4.4 끝 cumulative backend pytest count ≈ 2179. G4.5a adds ~38 unit + ~13 integration → ≈2230 expected". §7.1 76/76 G4.4-specific 그대로 유지. | §6.3 |
| 메타룰 | (G4.2/G4.3/G4.4 carry) | — | Round 1 Override 결정은 body 본문보다 우선. Round 2 (예정) 추가 row 가 없으면 Round 1 결정 유지. body 와 Override 충돌 시 Override 우선 — body 동기화 필수. |

### Round 2 — 예정 (R3+R4 plan audit 또는 R5+R6 fix loop 후)

### 메타룰 (G4.2 + G4.3 + G4.4 carry)

- Round Override 표 가 §1-§9 본문보다 우선.
- **Override numbering convention (RO-7 binding)**: Round 1 Override IDs
  prefix `RO-N`. §12 carry IDs prefix `O-N`. body 본문 references must prefix
  `RO-` 또는 `O-` to disambiguate.
- 본 spec drafting 직후 Codex (R1) + Claude code-reviewer (R2) 듀얼 audit
  진행. 결정사항은 본 표에 row 추가 + 본문 inline 동기화 (G4.2 + G4.3 + G4.4
  패턴 답습).
- 절대 규칙 위반 (LLM 입력 truncation, 시나리오 의존 고유명사, silent fallback,
  새 top-level field 도입) 발견 시 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 로.
- **신규 top-level field 도입 발견 시 BLOCKING** — 사용자 binding ("기존
  5-field contract 안 흡수").

---

## 1. Overview

### 1.1 Why now

G4.1~G4.4 가 v19 system.md 를 587 lines (G4.3 baseline) 에서 534 lines (G4.4
post) 로 줄였고, 5-field envelope contract (`render_strategy` / `id_policy` /
`background_binding` / `continuity_elements_used` / `asset_requirements`) 가
G4.2~G4.4 lift 를 통해 충분히 안정화되었다. 그러나 v19 system.md 에는 여전히
3 long-form prose section (★ 신규 표시) 이 남아 있다:

- **Rule F** (line 144-176, 33 lines): `## 카메라 위치와 frame edge의 물리
  일관성` — camera_direction 의 vertical 높이/위치와 frame 안 요소들의
  vertical/depth 위치 모순 검출. 4 prohibited pattern + 4 recommended +
  self-check 4-step + 4 핵심 원칙.
- **Rule G** (line 177-217, 41 lines): `## fg/bg 인물 분리 시 공유 공간 명시`
  — 같은 인터랙션 두 인물을 fg/bg 분리 묘사 시 공유 surface/공간 anchor 명시.
  3 적용 조건 + 3 prohibited + 3 recommended + 5 표현 키워드 + self-check
  4-step.
- **Rule J** (line 218-267, 50 lines): `## Primary Subject Framing — 한
  shot = 한 framing scale` — close/medium/wide framing 판정 + close framing
  룰 (primary subject only / partial / soft bg / absent) + wide/medium 룰 +
  4 self-check + 4 ✓/✗ 예시.

이 세 section 의 역할은 "카메라/구도 메타가 frame 안 spatial 요소들과 일관
되게 묘사되도록 LLM 을 강제" 하는 것이다. 모두 visual quality critical (★
신규 표시) — lift 후 LLM 행동 변화 가능성이 있어 production canary 로 별도
검증 필수.

#### 사용자 binding 핵심 원칙

> **(사용자 directive — 본 spec 의 invariant binding)**:
>
> 1. **새 top-level field 금지** — 5-field envelope contract (`render_strategy`
>    / `id_policy` / `background_binding` / `continuity_elements_used` /
>    `asset_requirements`) 그대로 유지. G4.2/G4.3/G4.4 의 contract 안정성을
>    유지하면서도 실제로 250 lines 에 접근하기 위해, **기존 5-field contract
>    안 흡수** (Option A' aggressive lift, staged execution).
> 2. **Sub-field 신설 OK** — `render_strategy` 안에 신규 nested dict
>    `spatial_consistency` 1 sibling 추가 + 3 sub-key (camera_frame_consistency
>    / fg_bg_shared_anchor / primary_framing) 형태로 흡수.
> 3. **"compact constraints"** — G4.4 의 sibling policy dicts 방식 carry.
> 4. **Behavior canary 별도 단계 검증** — 3 metric STRICT 0 + token delta +
>    line count + integration alignment.

#### G4.5 staged execution (Option A')

- **G4.5a (본 spec)** — Rule F/G/J lift (124 lines), `render_strategy.spatial_consistency`
  신규. 목표: system.md 534 → ~410 lines, token delta ≤ -1000 (system-only).
- **G4.5b** — Rule B/D/outfit/evidence lift (잔여 prose), 5-field 안 흡수 유지.
- **G4.5c** — dense compression / final 250-line target / 잔여 reasoning prose
  consolidation.

> **Override numbering convention (RO-7 binding)**: Round 1 Override IDs prefix
> `RO-N` (RO-1 ~ RO-14). §12 carry IDs prefix `O-N` (G4.2/G4.3/G4.4 carry).
> body references must prefix `RO-` 또는 `O-` to disambiguate.

### 1.2 Non-goals (명시)

G4.5a 는 다음을 **하지 않는다**:

- background-binding lift (G4.2) — 이미 완료. Rule A/C/E prose 변경 없음.
- ID-policy lift (G4.3) — 이미 완료. v19 의 `## ID Policy` compact section
  은 G4.5a 가 변경하지 않음.
- continuity lift (G4.4) — 이미 완료. v19 의 `## Continuity` compact section
  은 G4.5a 가 변경하지 않음.
- **Rule B (조명·색조가 신체를 변형하지 않게) lift = G4.5b carry**: line 63-90
  prose 28 lines. G4.5a scope 아님.
- **Rule D (큰 prop은 본체가 프레임에 보이게) lift = G4.5b carry**: v19 line
  386-408 prose 23 lines. G4.5a scope 아님.
- **outfit 묘사 룰 lift = G4.5b carry**: v19 line 418-441 prose 24 lines.
- **evidence and inference disclosure lift = G4.5b carry**: v19 line 494-533
  prose 40 lines.
- **dense compression / final 250-line target = G4.5c carry**.
- **새 top-level field 도입 = 영구 금지** (사용자 binding) — G4.5a 뿐 아니라
  G4.5b/c 모든 단계에서 invariant.
- **production canary 사용자 단계** — Wave 5 production canary 실행 자체는
  사용자 단계 (G4.4 carry). G4.5a Wave 1-4 작성 자체는 production canary 미실행
  이어도 진행 가능.
- Producer side 변경 — `_collect_card_inputs()` / `shot_dependency_t2i` /
  shot_extract / shot_staging upstream step 변경 없음. 그러나
  `build_render_strategy()` 내부 로직 변경은 G4.5a 자연 부산물 in-scope.
- **`_derive_framing_scale()` producer 변경 = G4.5c 또는 follow-up carry
  (Override RO-4)**: G4.5a 는 producer side `_derive_framing_scale()` 의 enum
  (close/medium/insert) 을 변경하지 않음. wide-class shots 는 medium 으로
  매핑되어 `wide_medium_rules` 적용. LLM 은 prose keyword (`wide shot`/`전경`
  등) 로 stricter checking 을 위해 wide 를 추가 추론할 수 있으나, 이는 LLM-side
  reasoning hint 일 뿐 production framing_scale enum 이 아님. card-only
  classification = `_derive_framing_scale()` enum / LLM hint 분리.
- `_check_prompts()` / deterministic post-check 변경 없음.
- G3.2 owned sentinel 의 로직 변경 없음.
- `id_policy` / `background_binding` / `continuity_elements_used` /
  `asset_requirements` field 의 sub-field 또는 constraint list 변경 없음.
- scene_image_pipeline 또는 합성 로직 변경 없음.

---

## 2. Card Envelope Sub-shape: render_strategy 확장

### 2.1 G4.4 현재 5-field shape (그대로 유지 — invariant)

`build_render_prompt_card()` 가 반환하는 envelope (G4.4 post — commit
`b1c5032`) 의 5 semantic field:

```json
{
  "schema_version": 1,
  "shot_key": {"scene_index": 12, "shot_index": 4},
  "render_strategy": {
    "mode": "direct",
    "primary_subject": "<text>",
    "framing_scale": "close|medium|insert",
    "moment_lock": "single still moment from the selected shot only",
    "camera_direction": "<text>",
    "lighting_mood": "<text>",
    "perception_mode": "direct|reflection|mirror|through_device|projection",
    "constraints": [
      "one prompt captures one still moment",
      "do not combine full-body framing and body-part close-up in one frame"
    ]
  },
  "id_policy": {"...": "..."},
  "background_binding": {"...": "..."},
  "continuity_elements_used": {"...": "..."},
  "asset_requirements": {"...": "..."}
}
```

이 envelope contract (envelope-level 7 fields: `schema_version` / `shot_key` /
5 semantic) 는 G4.5a 가 절대 변경하지 않는다 (사용자 binding invariant).

### 2.2 G4.5a sub-shape 확정 (Option 3 — sibling nested dict)

#### 1차 proposal (Option 1/2/3 비교)

| Option | 매핑 | Pro | Con |
|---|---|---|---|
| **1** (3 룰 모두 render_strategy top-level sibling 흡수) | `render_strategy.camera_frame_consistency` + `render_strategy.fg_bg_shared_anchor` + `render_strategy.primary_framing` | render_strategy builder 한 곳 변경 / 3 룰 모두 camera/구도 의미 통합 | render_strategy top 비대 (G4.5b 추가 lift 시 더 비대) |
| **2** (Rule G 만 background_binding 으로) | `render_strategy.camera_frame_consistency` + `background_binding.shared_space_anchor` (Rule G — 배경 anchor 공유 측면) + `render_strategy.primary_framing` | Rule G 의 "공유 공간 anchor" 가 배경과 의미 연결 | 2 builder 분산 / Rule G 가 "배경" 보다 "두 인물 spatial relationship" 임 — semantic stretch |
| **3 ★ 권장** (3 룰 통합 sibling nested dict) | `render_strategy.spatial_consistency` = `{"camera_frame_rule": {...}, "fg_bg_shared_anchor_rule": {...}, "primary_framing_rule": {...}}` | G4.4 sibling dict 패턴 carry / render_strategy top 비대 방지 / 3 룰 의미 그룹화 ("spatial consistency") 명확 / G4.5b/c 시 추가 spatial constraint 동일 nested dict 안 추가 가능 (forward-compat) | nested dict 깊이 증가 (1→2 levels) / 별도 nested validator helper 필요 |

**Option 3 채택 (R1+R2 audit 가 final 결정)** — G4.4 의 `view_consistency`
sibling dict 패턴 (9 required keys nested) 과 1:1 동일 구조. nested 깊이
증가 (1→2) 는 nested validator helper (`_assert_spatial_consistency_shape()`)
신설로 mitigate — G4.4 의 `_assert_ref_usage_constraints_shape()` 패턴 carry.

#### G4.5a 신규 sub-shape (Option 3)

```json
{
  "render_strategy": {
    "mode": "direct|not_applicable",
    "primary_subject": "<text>",
    "framing_scale": "close|medium|insert",
    "moment_lock": "single still moment from the selected shot only",
    "camera_direction": "<text>",
    "lighting_mood": "<text>",
    "perception_mode": "direct|reflection|mirror|through_device|projection",

    "spatial_consistency": {
      "camera_frame_rule": {
        "applies_when": "camera_direction specifies a vertical height/position (low / hip-height / overhead / ground level / quay level / floor level / from above) and the scene description includes frame-edge surfaces",
        "consistency_check_summary": "elements described inside the frame must be at vertical/depth positions consistent with the camera's height — low cameras place feet/lower-body in the lower frame, overhead cameras place head/shoulders in upper frame, hip-height cameras place torso in frame with head/feet implied beyond edges",
        "core_principles": [
          "low cameras at ground/quay/floor level place feet and lower legs in the lower frame; chest-height frame-edge surfaces behind the subject are geometrically forbidden",
          "overhead high angles from above place the top of head and shoulders in the upper-center frame; floor surfaces visible below feet at the lower frame edge are forbidden",
          "hip-height side cameras place torso and arms in frame; head and feet are implied beyond frame edges, not both fully visible together",
          "full body shots require either eye-level or high-angle framing where head and feet both fit; low cameras + full silhouette in one frame is forbidden"
        ],
        "forbidden_combinations": [
          "low camera at ground/quay/floor level + frame-edge surface described at chest/waist height behind the subject",
          "overhead high angle + feet visible at the lower frame edge with floor visible below them",
          "hip-height side camera + full silhouette including head and feet visible in same frame",
          "low angle looking up + ceiling and floor both fully visible in same frame"
        ],
        "recommended_phrasings": [
          "low observer at ground level, shoes and lower legs occupying the lower frame, upper body extending toward the upper edge",
          "overhead high angle from above, the top of head and shoulders fill the upper-center frame, the floor visible around them",
          "at hip height beside the subject, torso and arms in frame, head and lower legs implied beyond frame edges",
          "low angle looking up at the figure, ceiling lamps in the upper frame, the wall behind extending downward"
        ],
        "self_check_steps": [
          "identify the camera position phrasing (height + angle)",
          "list elements described inside the frame (foreground prop, body parts, background surfaces)",
          "verify each element's vertical/depth placement is geometrically consistent with that camera position",
          "if mismatch found, adjust frame-edge description to match camera position"
        ],
        "rationale_summary": "T2I 모델은 카메라 위치와 frame 안 요소 vertical 좌표 모순 시 절충 합성 — 인물이 가구에 박히거나 신체 일부가 잘리거나 prop 이 떠 있는 부자연스러운 결과"
      },
      "fg_bg_shared_anchor_rule": {
        "applies_when": "two characters share an interaction (dialogue / handing object / facing / co-watching) AND are described as foreground/background separated AND a shared surface or spatial anchor exists between them (bench / table / door / room)",
        "shared_anchor_required": "describe the shared surface or spatial anchor explicitly so both figures occupy the same physical space",
        "forbidden_phrasings": [
          "fg/bg separation without naming the shared surface/space",
          "isolated foreground hand reaching with another figure 'in the rear background' (no shared anchor named)",
          "one figure 'in the foreground' and another 'in the back' without naming the room/surface they share"
        ],
        "recommended_keywords": [
          "shared bench / shared table / shared surface",
          "the same X they both occupy (X = bench, sofa, table, doorway, room)",
          "across the X from each other",
          "<prop> crosses the air between them (interaction prop visually links the two)",
          "the <surface> sharing a single line of sight from the camera"
        ],
        "self_check_steps": [
          "do the two characters share a single interaction (dialogue, handing, facing, co-watching)?",
          "is one foreground and the other background-separated?",
          "is the shared surface/space anchor explicitly named?",
          "if not, add one of the recommended keywords"
        ],
        "rationale_summary": "fg/bg 분리만 묘사하면 모델이 두 인물을 다른 공간에 배치하거나 인터랙션이 끊긴 어색한 합성 — 공유 anchor 명시가 같은 물리적 공간 강제"
      },
      "primary_framing_rule": {
        "applies_when": "framing_scale ∈ {close, medium, insert} from `_derive_framing_scale()` (render_strategy.framing_scale, production enum). wide-class shots are mapped to medium and use `wide_medium_rules`. The LLM may *additionally* infer wide from prose keywords (`wide shot` / `전경` / `establishing` / `aerial`) for stricter checking — these are LLM-side reasoning hints, not the production framing_scale enum (RO-4 binding — card-only classification).",
        "framing_scale_keywords": {
          "_comment": "LLM-facing reasoning hints — not the production framing_scale enum. card framing_scale is close/medium/insert from _derive_framing_scale(); wide is LLM hint only (RO-4 / RO-5 binding). Canary close-framing shot scope filter uses production _CLOSE_FRAMING_RE (9-entry alignment) — separate from this 11-entry LLM list.",
          "close": [
            "close-up", "CU", "MCU", "ECU", "XCU", "extreme close-up",
            "클로즈업", "손가락이", "손이", "눈이", "얼굴이"
          ],
          "wide": [
            "wide shot", "establishing", "aerial",
            "전경", "전신"
          ],
          "medium_default": "medium / mid / two-shot — anything not matching close/wide keywords"
        },
        "close_framing_rules": {
          "primary_subject_only_fully_visible": "the named primary subject (hand / face / object) is the only fully visible entity in the frame",
          "other_entities_appearance_options": [
            "partial visibility — shoulder slice / arm slice / sleeve / fingertips entering the frame edge",
            "soft background — out-of-focus silhouette via shallow depth of field",
            "absent — not rendered at all even if listed in visible_entities"
          ],
          "forbidden_combinations": [
            "same character's full body and body-part close-up in the same frame (view mixing)",
            "two characters both rendered as sharp close-up faces in the same frame"
          ],
          "body_part_focus_cross_ref": "see id_policy.body_part_focus_rule (RO-6 paired-string substring; G4.4 _CONTINUITY_ID_POLICY_CROSS_REF_LITERAL reuse — Override O-31 carry)"
        },
        "wide_medium_rules": {
          "primary_and_secondary_both_visible": "primary subject + secondary entities all renderable",
          "body_part_close_up_forbidden": "body-part close-up of any character is forbidden in wide/medium framing — close-up belongs in a separate shot",
          "fg_bg_separation_requires_shared_anchor": "if two characters are rendered fg/bg separated, also apply fg_bg_shared_anchor_rule",
          "body_part_focus_cross_ref": "see id_policy.body_part_focus_rule (RO-6 paired-string substring; G4.4 _CONTINUITY_ID_POLICY_CROSS_REF_LITERAL reuse — Override O-31 carry)"
        },
        "self_check_steps": [
          "identify framing_scale from camera_direction",
          "if close: verify only primary subject is fully visible; other entities are partial / soft bg / absent",
          "verify same character's full body + body-part close-up are not combined",
          "if close: verify two faces are not both rendered sharp"
        ],
        "rationale_summary": "한 shot 안 framing scale 가 일관되어야 모델이 단일 카메라 셔터로 해석. close/wide/medium 혼합은 multi-camera 합성으로 인식되어 인물 분열 / 부위 분리 / 시점 혼합 합성"
      },
      "rationale_summary": "Rule F/G/J 의 spatial consistency 룰 통합 — camera 위치 vs frame edge 모순, fg/bg 분리 시 공유 anchor 누락, framing scale 혼합. 3 sub-rule 모두 single t2i_prompt = single camera shutter 원칙의 spatial 측면."
    },

    "constraints": [
      "one prompt captures one still moment",
      "do not combine full-body framing and body-part close-up in one frame",
      "camera position and frame-edge elements must be geometrically consistent — see render_strategy.spatial_consistency.camera_frame_rule",
      "two characters in fg/bg separation must share a named surface/space anchor — see render_strategy.spatial_consistency.fg_bg_shared_anchor_rule",
      "framing_scale binds entity rendering — close = primary subject only fully visible, wide/medium = no body-part close-up — see render_strategy.spatial_consistency.primary_framing_rule"
    ]
  },
  ...
  "_card_metadata": {
    "lift_status": {
      "rule_a_lifted": true,
      "rule_c_lifted": true,
      "rule_e_lifted": true,
      "rule_h_lifted": true,
      "id_policy_composite_lifted": true,
      "id_policy_close_framing_face_lifted": true,
      "id_policy_reproduction_surface_lifted": true,
      "continuity_cross_shot_id_substitution_lifted": true,
      "continuity_ref_usage_constraints_lifted": true,
      "continuity_view_consistency_lifted": true,
      "continuity_constraints_extended": true,
      "spatial_camera_frame_lifted": true,
      "spatial_fg_bg_shared_anchor_lifted": true,
      "spatial_primary_framing_lifted": true,
      "spatial_constraints_extended": true
    },
    "rule_source": {
      "...G4.2/G4.3/G4.4 carry...": "...",
      "camera_frame_rule": "카메라 위치와 frame edge의 물리 일관성 (Rule F) (v19:144-176)",
      "fg_bg_shared_anchor_rule": "fg/bg 인물 분리 시 공유 공간 명시 (Rule G) (v19:177-217)",
      "primary_framing_rule": "Primary Subject Framing — 한 shot = 한 framing scale (Rule J) (v19:218-267)"
    }
  }
}
```

#### Required-key contract per G4.5a sub-shape (strict superset 검증)

| sub-field | required_keys |
|---|---|
| `render_strategy.spatial_consistency` | `("camera_frame_rule", "fg_bg_shared_anchor_rule", "primary_framing_rule", "rationale_summary")` (4 top-level keys) |
| `render_strategy.spatial_consistency.camera_frame_rule` | `("applies_when", "consistency_check_summary", "core_principles", "forbidden_combinations", "recommended_phrasings", "self_check_steps", "rationale_summary")` (**7 keys** — `core_principles` 추가, RO-3 binding) |
| `render_strategy.spatial_consistency.fg_bg_shared_anchor_rule` | `("applies_when", "shared_anchor_required", "forbidden_phrasings", "recommended_keywords", "self_check_steps", "rationale_summary")` (6 keys) |
| `render_strategy.spatial_consistency.primary_framing_rule` | `("applies_when", "framing_scale_keywords", "close_framing_rules", "wide_medium_rules", "self_check_steps", "rationale_summary")` (6 keys) |
| `render_strategy.spatial_consistency.primary_framing_rule.framing_scale_keywords` | `("close", "wide", "medium_default")` (3 keys; `_comment` 무시 — superset 검증 시 extra) |
| `render_strategy.spatial_consistency.primary_framing_rule.close_framing_rules` | `("primary_subject_only_fully_visible", "other_entities_appearance_options", "forbidden_combinations", "body_part_focus_cross_ref")` (**4 keys** — `body_part_focus_cross_ref` 추가, RO-6 binding) |
| `render_strategy.spatial_consistency.primary_framing_rule.wide_medium_rules` | `("primary_and_secondary_both_visible", "body_part_close_up_forbidden", "fg_bg_separation_requires_shared_anchor", "body_part_focus_cross_ref")` (**4 keys** — `body_part_focus_cross_ref` 추가, RO-6 binding) |

ground-truth list/literal counts (R1+R2 audit 에서 final 검증):

- `camera_frame_rule.core_principles` = **4 entries** (RO-3 binding — v19 line
  170-175 4 핵심 원칙 verbatim carry)
- `camera_frame_rule.forbidden_combinations` = 4 entries
- `camera_frame_rule.recommended_phrasings` = 4 entries
- `camera_frame_rule.self_check_steps` = 4 entries
- `fg_bg_shared_anchor_rule.forbidden_phrasings` = 3 entries
- `fg_bg_shared_anchor_rule.recommended_keywords` = 5 entries
- `fg_bg_shared_anchor_rule.self_check_steps` = 4 entries
- `primary_framing_rule.framing_scale_keywords.close` = **11 entries
  LLM-facing** (영어 6 + 한국어 5) — RO-5 binding — LLM reasoning hint. Canary
  `g4_5a_primary_framing.py` 의 close-framing shot scope filter 는 production
  `_CLOSE_FRAMING_RE` **9-entry alignment** (`close-up`, `CU`, `MCU`, `ECU`,
  `XCU`, `extreme close-up`, `medium close-up`, `클로즈업`, `손가락이`) 재사용 —
  LLM hint vs canary detector 분리 strict.
- `primary_framing_rule.framing_scale_keywords.wide` = 5 entries (영어 3 +
  한국어 2) — LLM-facing reasoning hint only (RO-4 binding — production
  framing_scale enum 은 close/medium/insert)
- `primary_framing_rule.close_framing_rules.other_entities_appearance_options`
  = 3 entries
- `primary_framing_rule.close_framing_rules.forbidden_combinations` = 2 entries
- `primary_framing_rule.close_framing_rules.body_part_focus_cross_ref` = string
  paired-substring `"see id_policy.body_part_focus_rule"` 포함 (RO-6 binding)
- `primary_framing_rule.wide_medium_rules` = **4 sub-keys** (
  primary_and_secondary_both_visible / body_part_close_up_forbidden /
  fg_bg_separation_requires_shared_anchor / body_part_focus_cross_ref — RO-6
  binding)
- `primary_framing_rule.wide_medium_rules.body_part_focus_cross_ref` = string
  paired-substring `"see id_policy.body_part_focus_rule"` 포함 (RO-6 binding)
- `primary_framing_rule.self_check_steps` = 4 entries

constraints: G4.4 base 2 → G4.5a 5 strings (G4.4 의 2 carry + 3 신규 spatial
strings).

> **Strict required-keys subset (set superset 검증) — G4.4 carry**: 신규
> sub-field required keys 는 `EXPECTED_REQUIRED <= returned_keys` (set
> equality 가 아닌 superset). extra keys 허용 — G4.5b/c forward-compat. 단위
> 테스트도 `assert set(returned.keys()) >= EXPECTED_SET` 으로 통일.

`_card_metadata` 는 G4.2/G4.3/G4.4 와 동일 free-form (extra-key 허용, schema
검증 없음).

### 2.3 Producer / consumer matrix

| field | producer | consumer (LLM) | consumer (post-parse judge) |
|---|---|---|---|
| `render_strategy.mode` ~ `perception_mode` | `_collect_card_inputs()` ← shot_staging CP (G4.1 carry) | 한 컷의 구도와 시점 결정 | (없음) |
| `render_strategy.spatial_consistency.camera_frame_rule` | builder static (`build_render_strategy()` @ render_prompt_card.py:379) | camera_direction vs frame-edge surface vertical/depth 일관성 검증 입력 | canary `g4_5a_camera_frame_consistency.py` (camera height phrasing + frame-edge surface position 모순 검출 == 0) |
| `render_strategy.spatial_consistency.fg_bg_shared_anchor_rule` | builder static (`build_render_strategy()`) | fg/bg 분리 시 공유 anchor 명시 강제 | canary `g4_5a_fg_bg_shared_anchor.py` (fg/bg 분리 + 인터랙션 동시 등장 + 공유 anchor 키워드 부재 검출 == 0) |
| `render_strategy.spatial_consistency.primary_framing_rule` | builder static (`build_render_strategy()`) | framing_scale 별 entity 등장 형태 제한 | canary `g4_5a_primary_framing.py` (close framing + 같은 인물 전신/부위 close-up 동시 등장 검출 == 0; wide/medium + body-part close-up 검출 == 0) |
| `render_strategy.constraints` | builder static (G4.4 base 2 → G4.5a 5 strings) | t2i_prompt 작성 시 constraint list 준수 | 위 3 canary 통합 |

`build_render_strategy()` 위치 = `render_prompt_card.py:379-446` (G4.1 carry).
G4.5a Wave 1-A 는 이 builder 안에 `spatial_consistency` 신규 dict 작성 + base
2 → 5 strings constraints 확장. `_card_metadata` 는 envelope sibling 으로
hash 자동 strip (G4.2 carry). `_derive_framing_scale()` producer enum
(close/medium/insert) 은 G4.5a 가 변경하지 않음 — wide-class shots 는 medium 으로
매핑되어 `wide_medium_rules` 적용 (RO-4 binding — card-only classification).
producer 변경 = G4.5c 또는 follow-up.

> **RO-9 nested helper carry**: `_assert_render_strategy_shape()` 안에서
> `_assert_spatial_consistency_shape()` nested helper 가 `spatial_consistency`
> top-level 4 sub-key 검증, 그 안에서 `_assert_primary_framing_rule_shape()`
> further nested helper 가 primary_framing_rule 의 6 sub-key + framing_scale_keywords
> 3 / close_framing_rules 4 / wide_medium_rules 4 sub-sub-key 모두 검증. G4.4
> `_assert_ref_usage_constraints_shape` nested helper 패턴 carry.

### 2.4 정보 분리 원칙

Card 가 carry 하는 것 vs prompt 에 남는 것의 경계:

| 정보 | G4.5a 이후 위치 | 근거 |
|---|---|---|
| Camera vertical/depth 모순 forbidden 4 + recommended 4 | card `camera_frame_rule.forbidden_combinations` + `recommended_phrasings` | deterministic enumeration → card 로 |
| Camera consistency self-check 4-step | card `camera_frame_rule.self_check_steps` | deterministic → card 로 |
| fg/bg 분리 적용 조건 + 공유 anchor 키워드 5 | card `fg_bg_shared_anchor_rule.applies_when` + `recommended_keywords` | deterministic → card 로 |
| fg/bg forbidden 3 (공유 anchor 누락 패턴) | card `fg_bg_shared_anchor_rule.forbidden_phrasings` | deterministic → card 로 |
| framing scale 판정 키워드 (close 11 + wide 5 + medium default) | card `primary_framing_rule.framing_scale_keywords` | deterministic → card 로 |
| close framing 룰 (primary only / partial / soft bg / absent) | card `primary_framing_rule.close_framing_rules` | deterministic → card 로 |
| wide/medium 룰 (body-part close-up 금지 + fg_bg cross-ref) | card `primary_framing_rule.wide_medium_rules` | deterministic → card 로 |
| "왜 camera 모순이 인체 비례 위반 합성으로 가는가" | system.md "Spatial consistency" 한 단락 reminder | reasoning prose — card 로 표현 불가 |
| "왜 fg/bg 분리 시 공유 anchor 가 필요한가" | system.md "Spatial consistency" 한 단락 reminder | reasoning prose — card 로 표현 불가 |
| "왜 framing scale 혼합이 multi-camera 합성으로 가는가" | system.md "Spatial consistency" 한 단락 reminder | reasoning prose — card 로 표현 불가 |
| Rule F ✓/✗ 8 예시 (line 150-160) | 삭제 (v20) | card constraints 가 대체 |
| Rule G ✓/✗ 6 예시 (line 190-200) | 삭제 (v20) | card constraints 가 대체 |
| Rule J ✓/✗ 4 예시 (line 256-266) | 삭제 (v20) | card constraints 가 대체 |

> **G4.4 carry footnote — ✓/✗ 예시 prose 삭제 안전성**: G4.2/G4.3/G4.4 baseline
> 통과 패턴 carry. canary STRICT 0 검증이 LLM 행동 보증 — 예시 prose 의 추가
> reasoning 효과 없음. v20 compact section 은 reasoning summary 만 carry,
> sub-field 가 single source of truth.

---

## 3. Prompt Change Spec

### 3.1 대상 버전

- **Before**: `prompts/_base/scene_detail/19.202605050814/system.md` (v19,
  534 lines, 3 spatial-rules prose section 포함 + G4.2/G4.3/G4.4 compact
  sections 모두 완료)
- **After**: `prompts/_base/scene_detail/20.<TIMESTAMP>/system.md` (v20, 3
  spatial-rules prose section removed, "Spatial consistency" compact section
  1 단락 대체)

새 버전 디렉토리 생성 필수 — 덮어쓰기 금지 (CLAUDE.md
`feedback_prompt_versioning.md`). v19 sealed.

`<TIMESTAMP>` format = `YYYYMMDDHHmm` (e.g. `20.202605051600`) per
`feedback_version_format.md`.

### 3.2 삭제 대상 lines (v19 기준)

| section | lines (v19) | 내용 | 삭제 이유 |
|---|---|---|---|
| `## 카메라 위치와 frame edge의 물리 일관성 (Rule F) ★ 신규` | 144-176 | 4 prohibited + 4 recommended + self-check 4-step + 4 핵심 원칙 = **33 lines** | card `render_strategy.spatial_consistency.camera_frame_rule` (6 required keys) + constraints[2] 가 동일 데이터 carry |
| `## fg/bg 인물 분리 시 공유 공간 명시 (Rule G) ★ 신규` | 177-217 | 3 적용 조건 + 3 prohibited + 3 recommended + 5 표현 키워드 + self-check 4-step = **41 lines** | card `render_strategy.spatial_consistency.fg_bg_shared_anchor_rule` (6 required keys) + constraints[3] 가 동일 데이터 carry |
| `## Primary Subject Framing — 한 shot = 한 framing scale (Rule J) ★ 신규` | 218-267 | close/medium/wide framing 판정 + close framing 룰 + wide/medium 룰 + self-check 4-step + 4 ✓/✗ 예시 = **50 lines** | card `render_strategy.spatial_consistency.primary_framing_rule` (6 required keys + nested 3 sub-rule) + constraints[4] 가 동일 데이터 carry |

총 삭제 대상: **124 lines (system.md)** (33 + 41 + 50). detail_steps.py 변경
없음 (G4.4 의 inline prose 9 줄 삭제 패턴은 G4.5a 에 해당 사항 없음 — G4.5b
에서 재검토).

> **Section boundary inclusivity (G4.4 Override O-10 carry)**: Rule F
> inclusive 행 = v19 lines 144-176 (33 lines: heading 144 / blank 145 / intro
> 146 / blank 147 / 모순 패턴 sub-heading + 4 bullets + blank + 일관 패턴
> sub-heading + 4 bullets + blank + 자체 점검 sub-heading + 4 steps + blank +
> 핵심 원칙 sub-heading + 4 bullets). Rule G inclusive = v19 lines 177-217
> (41 lines). Rule J inclusive = v19 lines 218-267 (50 lines). **Blank lines
> 143 / 268** = 인접 섹션 (Background Binding 끝 / 구조화 입력 우선순위 시작)
> 보존 — Wave 1-B v20 drafting 에서 line-by-line diff 로 결정.
>
> **G4.5a trailing-blank policy (RO-10 binding)**: 삭제 섹션의 trailing blank
> 한 개 (line 142, 217, 268 트레일링 blank — Rule F end / Rule G end / Rule J
> end) 모두 drop. Inserted compact section starts on its own blank line.
> Net: 인접 섹션 사이 1 blank line 유지 (v19 §1-§2-§3 convention 일관).
> Wave 1-B mechanical 적용.

### 3.3 대체 section (v20 신규 — Override RO-2 ≤ 32 lines binding)

삭제된 3 prose section 자리에 다음 단일 섹션으로 교체. **Override RO-2
(G4.4 O-2 carry) — `wc -l` ≤ 32 lines strict gate** (통합 테스트 §6.2
`test_v20_spatial_consistency_section_line_count_under_32` 가 강제). 각
bullet 반드시 1 line — 80-col wrap 허용 안 됨 (wrap 줄도 line 1 로 카운트).
multi-line wrap trap 회피 (G4.4 R3R4-B3 carry). **RO-6 binding**: cross-card
paired-string substring `"see id_policy.body_part_focus_rule"` 가 v20 system.md
"Spatial consistency" compact section 에 1+ 등장 + card inject `close_framing_rules.body_part_focus_cross_ref`
와 `wide_medium_rules.body_part_focus_cross_ref` 모두 동일 substring 포함.

```markdown
## Spatial consistency (camera frame + fg/bg shared anchor + primary framing)

`[RenderPromptCard v1]` 의 `render_strategy.spatial_consistency` 가 이 shot 의 spatial 룰을 결정한다 (card 가 primary contract — 충돌 시 card 우선).

**핵심 원칙:**

- `camera_frame_rule.applies_when` 조건 (camera_direction 의 vertical 위치 + frame-edge surface 묘사) 만족 시 `forbidden_combinations` (낮은 카메라 + 가슴 높이 surface, overhead + 발 아래 바닥 등) 회피, `recommended_phrasings` 사용. `core_principles` 4 항 (낮은 카메라 / overhead / 허리 / full body) 으로 카메라 위치와 frame 안 요소 좌표 자연스럽게 작성.
- `fg_bg_shared_anchor_rule.applies_when` 조건 (두 인물 인터랙션 + fg/bg 분리 + 공유 surface/공간) 만족 시 `recommended_keywords` (`shared bench` / `the same X they both occupy` / `<prop> crosses the air between them` 류) 로 공유 anchor 명시. anchor 누락 시 모델이 두 인물을 다른 공간에 배치.
- `primary_framing_rule.framing_scale_keywords` (LLM hint) 로 close/wide/medium 판정 보조. card framing_scale 은 close/medium/insert (production enum, RO-4). close 이면 `close_framing_rules` 적용 — primary subject 만 fully visible, 다른 entity 는 partial/soft bg/absent. wide/medium 이면 `wide_medium_rules` 적용 — body-part close-up 금지 (close 는 별도 shot). 같은 인물 전신 + 부위 close-up 한 프레임 결합 금지 — see id_policy.body_part_focus_rule.
- fg/bg 분리 시 framing scale 무관 `fg_bg_shared_anchor_rule` 동시 적용.
- 카메라 1 위치 = 한 셔터 1/1000 초 = 한 framing scale. spatial mismatch 는 multi-camera 합성으로 모델이 해석해 인물 분열 / 부위 분리 / 좌표 모순 합성.

**충돌 해결**: 본 섹션 prose 는 reasoning summary; 구체 token list (forbidden_combinations / recommended_keywords / framing_scale_keywords / core_principles) 는 모두 `render_strategy.spatial_consistency` sub-field 가 single source. prose 가 sub-field 와 충돌하면 sub-field 우선.
```

실제 line 수 (Override RO-2 gate): heading 1 + blank 1 + intro 1 +
blank 1 + "**핵심 원칙:**" 1 + blank 1 + 5 bullet (각 1 line) + blank 1 +
"**충돌 해결**" 1 = **13 lines**. **32 lines 이내 strict** (wrap 발생 시
violation — Wave 1-B draft 작성 시 `wc -l` 자체 점검 필수).

### 3.4 Token 감소 추정

> **R1-M1 carry from G4.2/G4.3/G4.4**: 본 표의 token 수는 line 수 × 약 15
> tokens/line 의 1차 추정값 (`tiktoken cl100k_base` 미적용). canary 측정
> 단계 (§5.3) 가 `tiktoken cl100k_base` 로 system.md 실측 후 본 표를 갱신.
> exit criteria (§5.1 token delta gate) 는 **실측치 기준으로만 판정**.
>
> **R2-I4 carry from G4.2/G4.3/G4.4**: `_card_metadata.lift_status.spatial_*_lifted`
> 등 신규 dict key 가 user_prompt inject 시 +0 token 가정은 G4.2 producer-
> side 변경 (`detail_steps.py` inject 직전 strip — 14d14cb) 으로 보존. G4.5a
> 에서는 strip 로직 변경 0.

| 항목 | v19 | v20 (예상) | delta |
|---|---|---|---|
| Rule F section (system.md) | 33 lines ≈ 495 tokens | 0 (삭제) | -495 |
| Rule G section (system.md) | 41 lines ≈ 615 tokens | 0 (삭제) | -615 |
| Rule J section (system.md) | 50 lines ≈ 750 tokens | 0 (삭제) | -750 |
| Spatial consistency compact section (system.md, 신규 — Override O-2 ≤ 32 lines) | 0 | 13 lines ≈ 195 tokens (§3.3 enumerate) | +195 |
| **net system.md** | — | — | **-1665 tokens** (추정. 실측 = canary §5.3, **Override RO-1 binding gate `≤ -1000` 1차 estimate, system-only**) |
| card `render_strategy.spatial_consistency` user_prompt inject | 이미 있음 (G4.1 base 7 sibling) | 신규 nested dict (+~480 tokens — `camera_frame_rule` ~140 (core_principles 4-entry +~30) + `fg_bg_shared_anchor_rule` ~130 + `primary_framing_rule` ~240 (close/wide_medium body_part_focus_cross_ref +~30)) + constraints 2 → 5 (+3 strings ~80) | +590 |
| **net 합산 (system + inject)** | — | — | **-1075 tokens** (system -1665 + inject +590. Override RO-1 secondary report-only `≤ -800`, no gate) |

> **Override RO-1 binding token gate (R1+R2 Round 1 결정)**:
> `token_delta = candidate - baseline ≤ -1000` **system-only**. R2 분석 (G4.4
> estimate→actual realization ≈83%) 에 따르면 G4.5a estimate -1665 → actual
> ~-1380 → **28% margin over -1000 gate, sufficient** (이전 spec 의 "65%
> margin" claim 정정 — RO-1 binding). Wave 1-B tiktoken 실측 후 Round 2
> Override 로 downward tighten 가능 (e.g. -1300). **upward revise (e.g. -700)
> 는 re-audit 필요**. system+inject 합산 (≤ -800) = secondary report-only
> metric (canary JSON 보조 기록 / no gate).

> **Override RO-2 line count gate (G4.4 O-2 carry — binding)**: §3.3 compact
> section `wc -l` ≤ 32 lines strict — 통합 테스트 §6.2 가 강제. 본 §3.4
> estimate 의 13 lines 는 §3.3 enumerate 기준; Wave 1-B v20 draft 작성 후
> actual `wc -l` 측정.

> **G4.4 baseline reference**: G4.4 token canary delta -1010 (gate -540) 통과
> commit b1c5032. G4.5a 는 prose 분량 (124 lines) 이 G4.4 (75 lines) 보다 크고
> sub-field nested 깊이가 1 더 깊어 inject 부담 증가. system estimate -1665
> 에 G4.4 estimate→actual realization ≈83% 적용 시 actual ~-1380 → **28% margin
> over -1000 gate, sufficient** (RO-1 binding — 이전 "65% margin" claim 정정).

카나리 측정 시 실제 delta 를 `docs/canary/g4_5a_spatial_<timestamp>.json` 에
기록.

---

## 4. Lift Mapping Table

각 v19 prose section → G4.5a card field 1:1 대응:

| Prose source (v19 lines) | v19 prose 역할 | G4.5a card field | constraint key | lift 완료 판정 기준 |
|---|---|---|---|---|
| **Rule F — line 144 heading** | section title | (N/A heading 자체 삭제) | (N/A) | v20 system.md heading absence integration test |
| **Rule F — line 146 (intro)** | "camera_direction 의 vertical 위치와 frame 안 요소들의 vertical/depth 위치가 모순되면..." | (삭제 — card `applies_when` + `consistency_check_summary`) | constraints[2] | (N/A intro 자체) |
| **Rule F — line 148-153 (모순 패턴 4 ✗ bullets)** | 카메라 vs frame edge 4 forbidden combinations | `camera_frame_rule.forbidden_combinations` (4 entries) | constraints[2] | unit test `test_render_strategy_camera_frame_forbidden_combinations_count` (len == 4) + canary `g4_5a_camera_frame_consistency.py` |
| **Rule F — line 155-160 (일관 패턴 4 ✓ bullets)** | 권장 4 phrasings | `camera_frame_rule.recommended_phrasings` (4 entries) | constraints[2] | unit test `test_render_strategy_camera_frame_recommended_phrasings_count` (len == 4) |
| **Rule F — line 162-168 (자체 점검 4-step)** | 작성 후 self-check 4 단계 | `camera_frame_rule.self_check_steps` (4 entries) | constraints[2] | unit test |
| **Rule F — line 170-175 (핵심 원칙 4 bullets)** | 카메라 vs body parts vs frame edges 4 원칙 | `camera_frame_rule.core_principles` (**4-entry list**, RO-3 binding — v19 line 170-175 4 bullets verbatim carry; required keys 6 → 7) | constraints[2] | unit test `test_camera_frame_rule_core_principles_count == 4` + 4 substring assertions (`test_camera_frame_rule_core_principles_substring_*` × 4) |
| **Rule G — line 177 heading** | section title | (N/A heading 자체 삭제) | (N/A) | v20 missing heading |
| **Rule G — line 179 (intro)** | "한 인터랙션 두 인물을 fg/bg 로 분리해 묘사하면..." | (삭제 — card `applies_when` + `rationale_summary`) | constraints[3] | (N/A) |
| **Rule G — line 183-186 (적용 조건 3 bullets)** | 두 인물 인터랙션 + fg/bg 분리 + 공유 anchor 존재 | `fg_bg_shared_anchor_rule.applies_when` (single string carry) | constraints[3] | unit test substring assert |
| **Rule G — line 190-192 (✗ 3 forbidden)** | 공유 anchor 누락 3 패턴 | `fg_bg_shared_anchor_rule.forbidden_phrasings` (3 entries) | constraints[3] | unit test count + canary `g4_5a_fg_bg_shared_anchor.py` |
| **Rule G — line 198-200 (✓ 3 recommended)** | 공유 anchor 명시 3 권장 | (삭제 — `recommended_keywords` 가 keyword tier carry, 예시는 prose 측면) | constraints[3] | (N/A — keywords 가 generalization) |
| **Rule G — line 204-208 (표현 키워드 5)** | shared bench / same X / across the X / prop crosses / single line of sight | `fg_bg_shared_anchor_rule.recommended_keywords` (5 entries) | constraints[3] | unit test count |
| **Rule G — line 212-216 (자체 점검 4-step)** | 작성 후 self-check 4 단계 | `fg_bg_shared_anchor_rule.self_check_steps` (4 entries) | constraints[3] | unit test |
| **Rule J — line 218 heading** | section title | (N/A heading 자체 삭제) | (N/A) | v20 missing heading |
| **Rule J — line 220 (intro)** | "shot description 은 명시한 primary subject + framing scale 을 가진다..." | (삭제 — card `applies_when`) | constraints[4] | (N/A) |
| **Rule J — line 224-226 (close framing keywords 11)** | close-up / CU / MCU / ECU / XCU / 클로즈업 / 손가락이 등 | `primary_framing_rule.framing_scale_keywords.close` (11 entries) | constraints[4] | unit test count + canary `g4_5a_primary_framing.py` |
| **Rule J — line 226 (wide framing keywords 5)** | wide shot / establishing / aerial / 전경 / 전신 | `primary_framing_rule.framing_scale_keywords.wide` (5 entries) | constraints[4] | unit test count |
| **Rule J — line 226 (medium default)** | 그 외 default | `primary_framing_rule.framing_scale_keywords.medium_default` (string) | constraints[4] | unit test |
| **Rule J — line 230-237 (close framing 4 룰 + 옵션 3)** | primary only fully visible + 다른 entity 3 옵션 (partial / soft bg / absent) + 같은 인물 전신+close-up 금지 + 두 명 close-up 금지 | `primary_framing_rule.close_framing_rules` (3 sub-keys: primary_subject_only_fully_visible / other_entities_appearance_options 3 entries / forbidden_combinations 2 entries) | constraints[4] | unit test sub-shape + canary view-mixing 검증 |
| **Rule J — line 241-244 (wide/medium 룰 3)** | primary + secondary 등장 가능 + body-part close-up 금지 + fg/bg cross-ref | `primary_framing_rule.wide_medium_rules` (3 sub-keys) | constraints[4] | unit test sub-shape |
| **Rule J — line 248-252 (자체 점검 4-step)** | framing scale 식별 / close 시 주변 entity 확인 / 시점 혼합 부재 / 두 명 face 부재 | `primary_framing_rule.self_check_steps` (4 entries) | constraints[4] | unit test |
| **Rule J — line 256-266 (4 ✓/✗ 예시)** | close primary only / close 다른 인물 partial / close 시점 혼합 / close 두 명 fully visible | (삭제 — card sub-keys 가 enumeration) | constraints[4] | integration test v20 missing prose substring |

각 section 에 대해 "card 가 data 를 carry" + "prompt 가 1-paragraph reasoning
제공" 의 분리가 완성되면 lift 완료.

---

## 5. Exit Criteria + Canary Procedure

### 5.1 Exit criteria

G4.5a 는 다음 **모두** 충족 시 완료 선언 가능 (정확 수치는 R1+R2 audit 후
확정):

| criterion | 측정 방법 | threshold |
|---|---|---|
| camera-frame consistency violation 0 | canary script `scripts/canary/g4_5a_camera_frame_consistency.py` — module-level constant `_SPATIAL_CAMERA_LOW_TOKENS` (`low / ground level / quay level / floor level / 낮은`) + `_SPATIAL_CAMERA_HIGH_TOKENS` (`overhead / from above / high angle`) + `_SPATIAL_CAMERA_HIP_TOKENS` (`hip height / waist height / 허리 높이`) 와 frame-edge surface position token (`chest / waist / 가슴 / 허리 / floor / 바닥`) 동시 등장 패턴 검출. R1+R2 audit 가 false-positive 검증 + ground-truth list 확정 | **candidate count == 0 strict** |
| fg/bg shared-anchor missing 0 | canary script `scripts/canary/g4_5a_fg_bg_shared_anchor.py` — 두 인물 ID (C##/C##O##) + fg/bg separation token (`foreground / background / rear background / 전경 / 후경`) + 인터랙션 동사 (`facing / handing / dialogue / hold / 마주 / 건네`) 동시 등장하면서 module-level `_SPATIAL_SHARED_ANCHOR_KEYWORDS` (`shared bench / shared table / shared surface / the same X / across the X / single line of sight`) 부재 패턴 검출 | **candidate count == 0 strict** |
| primary framing violation 0 | canary script `scripts/canary/g4_5a_primary_framing.py` — close-framing shot scope filter 는 production `_CLOSE_FRAMING_RE` **9-entry alignment** 재사용 (`close-up`, `CU`, `MCU`, `ECU`, `XCU`, `extreme close-up`, `medium close-up`, `클로즈업`, `손가락이`, RO-5 binding — LLM-facing 11-entry list 와 분리). close-framing 등장 shot 의 t2i_prompt 에서 (a) **same-character-ID detection (RO-8 binding)**: per-shot scan, build `{character_id: [token_positions]}`. body-part close-up trigger (`focus on / close on / tight on / detail on`) token position 마다 nearest C##/C##O## within ±N=20 tokens → `cid_trigger`. full body keyword (`stands / seated / leaning / 전신`) position 마다 nearest character ID → `cid_body`. `cid_trigger == cid_body` AND both nearest-character-distance ≤ 20 → violation. (b) 두 인물 ID 의 face close-up (`face / 얼굴`) 동시 등장 검출. wide/medium framing shot 의 body-part close-up trigger 등장 검출 | **candidate count == 0 strict** |
| system.md token delta (tiktoken cl100k_base 실측) | canary script `scripts/canary/g4_5a_token_count.py` 가 v20 system.md 실측 vs v19 baseline 실측 | **`token_delta = candidate - baseline ≤ -1000` system-only binding gate (Override RO-1 binding)**. R2 분석: G4.4 estimate→actual realization ≈83% → G4.5a estimate -1665 → actual ~-1380 → 28% margin sufficient. system+inject 합산 (≤ -800) = secondary report-only metric. Wave 1-B 실측 후 Round 2 Override 로 downward (e.g. -1300) revise 가능, **upward revise 는 re-audit 필요**. |
| **v20 compact section line count gate** (Override RO-2 / G4.4 O-2 carry) | `wc -l` of `## Spatial consistency` heading → 다음 `##` heading 사이 lines | **≤ 32 lines strict** (각 bullet 1 line, wrap 줄도 카운트). 통합 테스트 §6.2 `test_v20_spatial_consistency_section_line_count_under_32` 가 강제. |
| pytest G4.5a test suite green | `backend/tests/unit/test_g4_5a_*.py` + `backend/tests/integration/test_g4_5a_*.py` all pass | 0 failures, 0 regressions |
| 기존 regression suite green | `pytest backend/tests/` | count ≥ G4.4 baseline (= 약 2179+ pass post-G4.4) |

> **strict 0 metric (3종)**: G4.5a 의 camera-frame / fg-bg shared-anchor /
> primary framing 위반 표현은 baseline 비0 이어도 candidate 0 강제 (G4.2
> R1-I4 / R2-B5 strict 패턴 carry, G4.3/G4.4 carry). baseline 비0 이면 baseline
> 도 별 production fix 대상 으로 마킹 (G4.5a scope 밖, P1 follow-up).

> **Behavior canary critical (G4.5a-unique)**: Rule F/G/J 가 visual quality
> critical (★ 신규 표시) — lift 후 LLM 행동 변화 가능성. Wave 5 production
> canary 결과는 G4.5b 진입 전 사용자 단계로 검증 필수. baseline 측정 시 도전
> shot subset (camera vertical 명시 / fg-bg 분리 / close framing) 최소량 확보
> (§5.2 참조).

> **R2-B4 carry from G4.2/G4.3/G4.4 — metric scope**: 본 3 canary 의 측정
> 대상 shot subset 은 명확히 분리:
> - `g4_5a_camera_frame_consistency.py`: camera_direction 안 vertical 위치
>   token (low / overhead / hip / from above / 높이 / 위에서) 1+ 존재 shot 만.
>   다른 shot 은 분모/numerator 모두 제외.
> - `g4_5a_fg_bg_shared_anchor.py`: 두 인물 ID + fg/bg separation token 동시
>   등장 shot 만.
> - `g4_5a_primary_framing.py`: 모든 shot. framing_scale 자체가 모든 shot 에
>   존재 (`render_strategy.framing_scale` 이 production enum
>   close/medium/insert 중 하나 — RO-4 binding, wide-class shots 는 medium 으로
>   매핑되어 wide_medium_rules 적용). close-framing shot scope filter
>   (subset detection 용) 는 production `_CLOSE_FRAMING_RE` **9-entry
>   alignment** (`close-up`, `CU`, `MCU`, `ECU`, `XCU`, `extreme close-up`,
>   `medium close-up`, `클로즈업`, `손가락이`, RO-5 binding — LLM-facing
>   11-entry list 와 분리).

> **regex view-mixing canary brittleness footnote (G4.4 carry)**: regex 는
> 1차 detector 만, baseline 비0 시 P1 follow-up 으로 LLM-validator (gpt-5.4-mini
> judge) 전환 검토. 의미 판단은 LLM-first (`feedback_no_regex_postprocessing.md`
> 참조). 본 3 canary 의 regex (camera height token / shared anchor keyword /
> framing scale keyword) 는 deterministic 패턴만.

G4.5a Exit criteria 가 충족되어야 G4.5b (Rule B/D/outfit/evidence) 진입 가능.

### 5.2 Baseline 측정 대상 PID / scene set

**Baseline 비교는 baseline / candidate 가 다음 변동 source 모두 동일할 때만
유효** (G4.2 R1-I5 + G4.3 R2-I2 + G4.4 carry pinning):

| pinning field | 값 결정 | 비교 시 enforce |
|---|---|---|
| `pid` | 단일 PID (G4.2/G4.3/G4.4 canary 와 동일 권장) | baseline.pid == candidate.pid |
| `scene_index_list` | 정확한 scene_index 정수 list | sorted 비교 동일 |
| `shot_index_list_per_scene` | scene 별 shot_index 정수 list dict | dict 동일 |
| `model_routing` | scene_detail step 의 LLM 모델 (e.g. `gemini-3.1-pro-preview`) | 동일 |
| `prompt_source_mode` | `file` (file-based prompt) vs `db` (DB prompt_template) | 동일 |
| `card_commit_hash` | RenderPromptCard helper / detail_steps 의 git HEAD commit hash 시점 (G4.4 = `b1c5032`) | baseline / candidate 모두 명시 |
| `render_strategy_card_snapshot_hash` | render_strategy field hash (loader 입력 변동 차단). G4.5a 추가 — **`compute_render_strategy_snapshot_hash(card) = sha256(json.dumps(card["render_strategy"], sort_keys=True, ensure_ascii=False))[:16]`**. G4.3 `compute_id_policy_snapshot_hash` / G4.4 `compute_continuity_snapshot_hash` 와 동일 패턴 carry. | 동일 |

**Baseline scene set 요건** (R1+R2 audit 시 정확 수치 정정):

- camera_direction 안 vertical 위치 token 1+ 존재 shot 최소 5개 (camera-frame
  metric 분모 보장)
- 두 인물 ID + fg/bg separation token 동시 등장 shot 최소 3개 (fg-bg shared-
  anchor metric 분모 보장)
- close framing keyword 등장 shot 최소 5개 (primary framing metric 분모 보장)
- 모두 `scene_index_list` / `shot_index_list_per_scene` 에 명시.

Baseline 측정 실행:
```
# 1. canary script 가 위 pinning field 모두 capture 후 baseline JSON 작성
# 2. v19 (G4.4) 로 force scene_detail 실행 — 명시된 scene/shot 만 force
# 3. 측정: 3 metric per shot + tiktoken cl100k_base system.md 실측 token count
# 4. 저장: docs/canary/g4_5a_spatial_baseline_<timestamp>.json
```

candidate 측정도 동일 pinning. 한 field 라도 다르면 비교 reject — baseline
재측정.

### 5.3 Canary 절차 (G4.2 §5.3 + G4.3 §5.3 + G4.4 §5.3 carry — DRY refactor 권장)

G4.5a canary 는 3 spatial-rules prose 삭제를 광범위 적용 전 1 PID / 1 selected
segment 로 검증한다:

> **DRY refactor 권장 (R1+R2 audit 시 명시 확정)**: G4.2 (4 metric) + G4.3 (5
> metric) + G4.4 (4 metric) + G4.5a (3 metric) 의 16 canary script 가 동일
> boilerplate (CP load + pinning capture + measurement_failures list +
> pinned-tuple seen_in_cp post-loop validation + sentinel pre-pass + gate fail
> exit 1) 을 가짐. **`scripts/canary/_canary_common.py` helper module 신설 +
> `_validate_scene_preflight()` 공통 함수** 추출 권장. G4.2 R3-I3 carry +
> G4.3/G4.4 잔여 follow-up carry. 본 권장은 G4.5a in-scope 가 아니라 G4.5c
> 또는 별 P1 follow-up 으로 이관.

**Step 1 — Baseline capture (v19)**
```
force scene_detail 실행 (v19 prompt, G4.4 card, 일반 PID)
capture → docs/canary/g4_5a_spatial_baseline_<timestamp>.json
```

JSON schema (G4.2 R1-I3 + R1-I5 pinning + R2-B6 pid 위치 + G4.3 R2-I2 hash
+ G4.4 carry):
```json
{
  "timestamp": "ISO8601",
  "prompt_version": "19.202605050814",
  "pinning": {
    "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": "b1c5032",
    "render_strategy_card_snapshot_hash": "..."
  },
  "scene_set": [
    {
      "scene_index": N,
      "shot_index": M,
      "has_camera_vertical_token": bool,
      "has_fg_bg_pattern": bool,
      "has_close_framing": bool
    }
  ],
  "metrics": {
    "total_shots": N,
    "shots_with_camera_vertical_token": N,
    "shots_with_fg_bg_pattern": N,
    "shots_with_close_framing": N,
    "camera_frame_violation_count": N,
    "camera_frame_violation_per_shot": {},
    "fg_bg_shared_anchor_missing_count": N,
    "fg_bg_shared_anchor_missing_per_shot": {},
    "primary_framing_violation_count": N,
    "primary_framing_violation_per_shot": {},
    "system_prompt_token_count_tiktoken_cl100k": N,
    "system_plus_inject_token_count_tiktoken_cl100k": N
  },
  "measurement_scripts": {
    "camera_frame_consistency": "scripts/canary/g4_5a_camera_frame_consistency.py (camera vertical token shots only)",
    "fg_bg_shared_anchor": "scripts/canary/g4_5a_fg_bg_shared_anchor.py (fg/bg pattern shots only)",
    "primary_framing": "scripts/canary/g4_5a_primary_framing.py (all shots)",
    "token_count": "scripts/canary/g4_5a_token_count.py (tiktoken cl100k_base)"
  }
}
```

**Step 2 — Candidate capture (v20)**
```
새 v20 prompt 적용 + 동일 PID 동일 scene_set force 재실행
capture → docs/canary/g4_5a_spatial_candidate_<timestamp>.json
```

동일 JSON schema (`prompt_version: "20.<timestamp>"` 로 변경).

**Step 3 — Comparison**:

선행 조건: `pinning` block 7 field 모두 baseline == candidate 일치 확인 (dict
equality 한 번. 불일치 시 재측정). 그 후 metric 비교:

- `camera_frame_violation_count`: **candidate == 0 strict**
- `fg_bg_shared_anchor_missing_count`: **candidate == 0 strict**
- `primary_framing_violation_count`: **candidate == 0 strict**
- `system_prompt_token_count_tiktoken_cl100k`: **`token_delta = candidate -
  baseline ≤ -1000` system-only binding gate (Override O-1 1차 estimate)**.
  Wave 1-B 실측 후 Round 2 Override 로 downward revise 가능, **upward revise
  는 re-audit 필요**.
- `system_plus_inject_token_count_tiktoken_cl100k`: secondary report-only
  metric. canary JSON 보조 기록만.

**Step 4 — 판정**
- 모두 통과 → G4.5a lift 적용 확정, 3 spatial-rules prose v20 삭제 확정.
- 실패 항목 있음 → constraints sub-field 또는 compact section 문구 수정 후
  재canary.
- canary 가 mix 되면 다음 phase (G4.5b) 와 분리 상태 유지
  (`_card_metadata.lift_status` 확인으로 구별 가능).

### 5.4 Canary script 공통 패턴 (G4.2 + G4.3 + G4.4 carry)

모든 3 metric script + token script 는 G4.2 + G4.3 + G4.4 의 silent-absorb 차단
패턴 답습:

- **CP shape**: `scene["render_prompt_card"]["render_strategy"]`
  (NOT 평면 `scene["render_strategy"]`). G4.2 함정 carry —
  `detail_steps.py:2124` authoritative. G4.3/G4.4 의 `_validate_scene_preflight()`
  helper carry — 3 metric scripts 동일 적용.
- **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.
- **`_DEFAULT_SENTINEL = object()` fixture pattern** (test 측): fixture 의
  default arg 와 None / [] 구분 필요할 때 sentinel. G4.1 R3-B2 fixture trap
  carry.
- **module-level constants single source**: `_SPATIAL_CAMERA_LOW_TOKENS` /
  `_SPATIAL_CAMERA_HIGH_TOKENS` / `_SPATIAL_CAMERA_HIP_TOKENS` /
  `_SPATIAL_FRAME_EDGE_POSITION_TOKENS` / `_SPATIAL_FG_BG_SEPARATION_TOKENS` /
  `_SPATIAL_INTERACTION_VERBS` / `_SPATIAL_SHARED_ANCHOR_KEYWORDS` /
  `_SPATIAL_FRAMING_CLOSE_KEYWORDS` / `_SPATIAL_FRAMING_WIDE_KEYWORDS`
  builder + canary + unit test 동일 import. drift 차단 (G4.3/G4.4 R1-I2/R2-I6
  carry).
- **body-part triggers cross-import (RO-11 binding)**: G4.5a 는 G4.3 의
  `_ID_BODY_PART_TRIGGERS` 를 alias-import 로 직접 재사용 (single source —
  drift 차단). G4.5a 내부에서 `_SPATIAL_BODY_PART_TRIGGERS` 별칭 도입 금지 —
  모든 import 는
  `from backend.app.core.steps.render_prompt_card import _ID_BODY_PART_TRIGGERS as _ID_BODY_PART_TRIGGERS_G4_3_CARRY`
  또는 직접 사용. 별칭 도입 X.

---

## 6. Test Scope

### 6.1 Unit tests

파일: `backend/tests/unit/test_g4_5a_spatial_lift.py`

| test | 설명 | assert |
|---|---|---|
| `test_render_strategy_has_spatial_consistency` | `build_render_strategy()` 반환에 `spatial_consistency` dict 존재 + 4 required keys | dict + 4 key assert |
| `test_spatial_consistency_required_keys_superset` | `spatial_consistency` 의 4 required key (`camera_frame_rule` / `fg_bg_shared_anchor_rule` / `primary_framing_rule` / `rationale_summary`) — superset | `assert set(returned_keys) >= EXPECTED_SET` |
| `test_camera_frame_rule_required_keys_superset` | `camera_frame_rule` 의 **7 required key** (`applies_when` / `consistency_check_summary` / `core_principles` / `forbidden_combinations` / `recommended_phrasings` / `self_check_steps` / `rationale_summary`, RO-3 binding) — superset | superset assert |
| `test_camera_frame_rule_core_principles_count` | `core_principles` list-len == 4 (RO-3 binding — v19 line 170-175 4 bullets carry) | len assert |
| `test_camera_frame_rule_core_principles_substring_low_camera` | `core_principles[0]` 안 `"low camera"` substring 존재 (낮은 카메라 룰) | substring assert |
| `test_camera_frame_rule_core_principles_substring_overhead` | `core_principles[1]` 안 `"overhead"` substring 존재 (머리 위 룰) | substring assert |
| `test_camera_frame_rule_core_principles_substring_hip_height` | `core_principles[2]` 안 `"hip-height"` substring 존재 (허리 룰) | substring assert |
| `test_camera_frame_rule_core_principles_substring_full_body` | `core_principles[3]` 안 `"full body"` substring 존재 (full body shot 룰) | substring assert |
| `test_camera_frame_rule_forbidden_combinations_count` | `forbidden_combinations` list-len == 4 | len assert |
| `test_camera_frame_rule_recommended_phrasings_count` | `recommended_phrasings` list-len == 4 | len assert |
| `test_camera_frame_rule_self_check_steps_count` | `self_check_steps` list-len == 4 | len assert |
| `test_fg_bg_shared_anchor_rule_required_keys_superset` | `fg_bg_shared_anchor_rule` 의 6 required key — superset | superset assert |
| `test_fg_bg_shared_anchor_rule_forbidden_phrasings_count` | `forbidden_phrasings` list-len == 3 | len assert |
| `test_fg_bg_shared_anchor_rule_recommended_keywords_count` | `recommended_keywords` list-len == 5 | len assert |
| `test_fg_bg_shared_anchor_rule_self_check_steps_count` | `self_check_steps` list-len == 4 | len assert |
| `test_primary_framing_rule_required_keys_superset` | `primary_framing_rule` 의 6 required key — superset | superset assert |
| `test_primary_framing_rule_framing_scale_keywords_required_subkeys` | `framing_scale_keywords` 의 3 required sub-key (`close` / `wide` / `medium_default`) — superset | superset assert |
| `test_primary_framing_rule_close_keywords_count` | `framing_scale_keywords.close` list-len == 11 | len assert |
| `test_primary_framing_rule_wide_keywords_count` | `framing_scale_keywords.wide` list-len == 5 | len assert |
| `test_primary_framing_rule_close_framing_rules_required_subkeys` | `close_framing_rules` 의 **4 required sub-key** (primary_subject_only_fully_visible / other_entities_appearance_options / forbidden_combinations / `body_part_focus_cross_ref`, RO-6 binding) — superset | superset assert |
| `test_primary_framing_rule_close_framing_other_entities_options_count` | `close_framing_rules.other_entities_appearance_options` list-len == 3 | len assert |
| `test_primary_framing_rule_close_framing_forbidden_combinations_count` | `close_framing_rules.forbidden_combinations` list-len == 2 | len assert |
| `test_primary_framing_rule_close_body_part_cross_ref_paired` | `close_framing_rules.body_part_focus_cross_ref` (string) paired-substring `"see id_policy.body_part_focus_rule"` 포함 (RO-6 binding — `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` reuse) | substring assert |
| `test_primary_framing_rule_wide_medium_rules_required_subkeys` | `wide_medium_rules` 의 **4 required sub-key** (primary_and_secondary_both_visible / body_part_close_up_forbidden / fg_bg_separation_requires_shared_anchor / `body_part_focus_cross_ref`, RO-6 binding) — superset | superset assert |
| `test_primary_framing_rule_wide_medium_body_part_cross_ref_paired` | `wide_medium_rules.body_part_focus_cross_ref` (string) paired-substring `"see id_policy.body_part_focus_rule"` 포함 (RO-6 binding) | substring assert |
| `test_primary_framing_rule_self_check_steps_count` | `self_check_steps` list-len == 4 | len assert |
| `test_render_strategy_constraints_grew_to_five_strings` | G4.4 base 2 strings → G4.5a 5 strings (1 still moment + 1 view-mix + 1 camera frame + 1 fg/bg anchor + 1 framing scale) | `len(constraints) == 5` |
| `test_render_strategy_constraints_string_1_still_moment_carry` | constraints[0] == G4.4 carry string (`"one prompt captures one still moment"`) | string equality |
| `test_render_strategy_constraints_string_2_view_mix_carry` | constraints[1] == G4.4 carry string (`"do not combine full-body framing and body-part close-up in one frame"`) | string equality |
| `test_render_strategy_constraints_string_3_camera_frame_inline` | constraints[2] 안 `"camera position and frame-edge"` substring + `"render_strategy.spatial_consistency.camera_frame_rule"` substring | 2 substring assert |
| `test_render_strategy_constraints_string_4_fg_bg_anchor_inline` | constraints[3] 안 `"fg/bg separation"` substring + `"render_strategy.spatial_consistency.fg_bg_shared_anchor_rule"` substring | 2 substring assert |
| `test_render_strategy_constraints_string_5_framing_scale_inline` | constraints[4] 안 `"framing_scale binds entity rendering"` + `"render_strategy.spatial_consistency.primary_framing_rule"` substring | 2 substring assert |
| `test_spatial_consistency_4_subfields_independent_of_input` | hardcoded `staging=None` (with `staging_not_applicable=True`) 입력에도 4 sub-field (camera_frame_rule / fg_bg_shared_anchor_rule / primary_framing_rule / rationale_summary) 모두 builder-static 출력 (Wave subagent hardcoded `[]` fixture trap 회피 — G4.4 R4-M6 carry) | 모든 sub-field deep equality vs ground-truth |
| `test_card_metadata_lift_status_includes_spatial_keys` | `build_render_prompt_card()` 반환의 `_card_metadata.lift_status` 에 `spatial_camera_frame_lifted=true` + `spatial_fg_bg_shared_anchor_lifted=true` + `spatial_primary_framing_lifted=true` + `spatial_constraints_extended=true` 모두 존재 | 4 key assert |
| `test_card_metadata_rule_source_includes_spatial_keys` | `_card_metadata.rule_source` 에 `camera_frame_rule` / `fg_bg_shared_anchor_rule` / `primary_framing_rule` 3 key 존재 | 3 key assert |
| `test_card_metadata_excluded_from_hash_after_g4_5a_keys_added` | G4.2 R1-B1 + G4.3/G4.4 carry — 신규 4 lift_status key 추가에도 hash 변경 0 (`_card_metadata` strip) | hash_before == hash_after |
| `test_card_hash_drifts_on_spatial_consistency_change` | `render_strategy.spatial_consistency.camera_frame_rule.forbidden_combinations[0]` 문자열 1 char 변경 → hash drift | hash_A != hash_B |
| `test_card_hash_stable_on_spatial_lift_status_only_change` | `_card_metadata.lift_status.spatial_camera_frame_lifted` 만 toggle → hash 동일 | hash_A == hash_B |
| `test_assert_render_strategy_shape_validates_spatial_consistency_shape` | `_assert_render_strategy_shape()` 가 `spatial_consistency` 누락 시 AppError | pytest.raises |
| `test_assert_spatial_consistency_shape_validates_camera_frame_rule_shape` | `_assert_spatial_consistency_shape()` nested helper 가 `camera_frame_rule` 누락 또는 **7 sub-key (incl core_principles, RO-3)** 미충족 시 AppError | pytest.raises |
| `test_assert_spatial_consistency_shape_validates_fg_bg_shared_anchor_rule_shape` | nested helper 가 `fg_bg_shared_anchor_rule` 누락 또는 6 sub-key 미충족 시 AppError | pytest.raises |
| `test_assert_spatial_consistency_shape_validates_primary_framing_rule_shape` | nested helper 가 `primary_framing_rule` 누락 또는 6 sub-key 미충족 시 AppError | pytest.raises |
| `test_assert_primary_framing_rule_shape_validates_framing_scale_keywords` | **separate further nested helper `_assert_primary_framing_rule_shape()`** (RO-9 binding — Q9 commit, sibling to `_assert_spatial_consistency_shape()`) 가 `framing_scale_keywords` 의 3 sub-key (close/wide/medium_default) 미충족 시 AppError | pytest.raises |
| `test_assert_primary_framing_rule_shape_validates_close_framing_rules` | separate nested helper 가 `close_framing_rules` 의 **4 sub-key (incl body_part_focus_cross_ref, RO-6)** 미충족 시 AppError | pytest.raises |
| `test_assert_primary_framing_rule_shape_validates_wide_medium_rules` | separate nested helper 가 `wide_medium_rules` 의 **4 sub-key (incl body_part_focus_cross_ref, RO-6)** 미충족 시 AppError | pytest.raises |
| `test_assert_primary_framing_rule_shape_validates_cross_ref_strings` | separate nested helper 가 `close_framing_rules.body_part_focus_cross_ref` + `wide_medium_rules.body_part_focus_cross_ref` 둘 다 paired-substring `"see id_policy.body_part_focus_rule"` 누락 시 AppError | pytest.raises |
| `test_g4_5a_canary_camera_frame_violation_filter_module_constants` | canary fixture 가 module-level `_SPATIAL_CAMERA_LOW_TOKENS` 와 `_SPATIAL_FRAME_EDGE_POSITION_TOKENS` 동시 등장 패턴 정확히 검출 (false-positive 0) | filter behavior assert |
| `test_g4_5a_canary_fg_bg_anchor_filter_module_constants` | canary fixture 가 fg/bg + 인터랙션 동시 등장 + shared anchor keyword 부재 패턴 정확 검출 (자연스러운 single-character 묘사 false-positive 0) | filter behavior assert |
| `test_g4_5a_canary_primary_framing_filter_close_keywords` | canary fixture 가 close framing keyword + same-character 전신/부위 close-up 동시 등장 정확 검출 (close framing 단순 partial visibility false-positive 0). **RO-8 same-character-ID detection algorithm**: per-shot scan, build `{character_id: [token_positions]}`, body-part trigger 와 full-body keyword 의 nearest character ID 가 동일 + 둘 다 ±20 tokens 이내 → violation. fixture 3 cases: (a) same-id co-occurrence → violation, (b) different-id → no violation, (c) > 20 tokens distance → no violation. | filter behavior assert |
| `test_canary_close_framing_filter_uses_production_close_framing_re` | canary smoke (RO-5 binding) — `g4_5a_primary_framing.py` 의 close-framing shot scope filter 가 production `_CLOSE_FRAMING_RE` **9-entry alignment** (`close-up`, `CU`, `MCU`, `ECU`, `XCU`, `extreme close-up`, `medium close-up`, `클로즈업`, `손가락이`) 재사용 — LLM-facing 11-entry list 와 분리 검증. fixture: `손이`/`눈이`/`얼굴이` 등 LLM-facing keyword 가 canary scope filter 에 포함되지 않음 (production scope filter 정확) | filter import + 9-entry alignment assert |
| `test_render_strategy_camera_direction_none_raises` | `staging=None` + `shot_info` 에 `staging_not_applicable=False` (default) → AppError (G4.1 R1-I1 carry — silent fallback ban) | pytest.raises |
| `test_render_strategy_camera_direction_empty_string_valid_with_static_subfields` | `staging` 의 `camera_direction=""` (explicit empty) → 4 sub-field 모두 present (builder-static 보장) | shape valid |
| `test_render_strategy_view_consistency_mixing_forbidden_is_bool_true` | (G4.4 carry) view_consistency 의 `mixing_forbidden` 가 `True` (bool, NOT bool int subclass: `assert mixing_forbidden is True and type(mixing_forbidden) is bool`) | identity + type assert (G4.4 carry — G4.5a 변경 0 검증) |

총 unit tests: **~38-43** (RO-12 binding — table 38 enumerate, +5 fix-loop
buffer for plan-R3+R4 audit additions). G4.4 32 carry pattern 위에 +6 신규
sub-field shape (4 sibling + nested 3 sub-rule + RO-3 core_principles + RO-6
cross-ref + RO-9 separate helper) 검증 + 4 canary filter behavior (RO-5 + RO-8
포함) + 1 G4.4 carry regression. 정확 count 는 plan drafting 시 R3+R4 audit
에서 확정.

### 6.2 Integration tests

파일: `backend/tests/integration/test_g4_5a_spatial_integration.py`

| test | 설명 | assert |
|---|---|---|
| `test_v20_system_prompt_missing_camera_frame_section` | v20 system.md 를 load 후 `"## 카메라 위치와 frame edge의 물리 일관성 (Rule F)"` heading 없음 | heading not present |
| `test_v20_system_prompt_missing_fg_bg_separation_section` | v20 system.md 에 `"## fg/bg 인물 분리 시 공유 공간 명시 (Rule G)"` heading 없음 | heading not present |
| `test_v20_system_prompt_missing_primary_subject_framing_section` | v20 system.md 에 `"## Primary Subject Framing — 한 shot = 한 framing scale (Rule J)"` heading 없음 | heading not present |
| `test_v20_system_prompt_has_spatial_consistency_section` | v20 system.md 에 `"## Spatial consistency"` heading 존재 | heading present |
| `test_v20_system_prompt_keeps_id_policy_section` | v20 system.md 에 G4.3 의 `"## ID Policy"` heading 여전히 존재 | heading present |
| `test_v20_system_prompt_keeps_background_binding_section` | v20 system.md 에 G4.2 의 `"## Background Binding"` heading 여전히 존재 | heading present |
| `test_v20_system_prompt_keeps_continuity_section` | v20 system.md 에 G4.4 의 `"## Continuity"` heading 여전히 존재 | heading present |
| `test_v20_prompt_token_reduction_vs_v19` | v20 system.md 줄 수 < v19 baseline 534 lines - 32 (compact section gate Override O-2) - 보수 buffer 5 = **497 lines 미만** (Override O-14 도출식) | `len(v20_lines) < 497` |
| `test_v20_spatial_consistency_section_line_count_under_32` | v20 system.md 의 `## Spatial consistency` heading 부터 다음 `##` heading 사이 lines `wc -l` ≤ 32 (Override O-2 binding gate, bullet 1 line 강제, wrap 줄도 카운트) | `assert spatial_section_line_count <= 32` |
| `test_spatial_consistency_compact_section_references_card_path` | v20 system.md 의 Spatial consistency compact section 안 `"render_strategy.spatial_consistency"` substring 1+ 등장 (cross-card reference) | substring present |
| `test_cross_card_paired_substring_in_v20_system_md_and_card_inject` | RO-6 binding — `"see id_policy.body_part_focus_rule"` paired substring 이 (a) v20 system.md "Spatial consistency" compact section 안 1+ 등장 + (b) card inject `close_framing_rules.body_part_focus_cross_ref` + `wide_medium_rules.body_part_focus_cross_ref` 모두 동일 substring 포함 — 양쪽 모두 paired (G4.4 `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` reuse, Override O-31 carry) | 3-way substring assertion |
| `test_render_strategy_card_carries_spatial_consistency` | `_collect_card_inputs()` + `build_render_prompt_card()` + inject 경로 통해 user_prompt 에 `render_strategy.spatial_consistency` dict 포함 확인 | JSON parse → dict valid |
| `test_camera_frame_rule_card_inject_carries_forbidden_combinations` | inject 된 user_prompt 에 `camera_frame_rule.forbidden_combinations` 4 entry 모두 substring 존재 | 4 substring in injected JSON |
| `test_fg_bg_shared_anchor_rule_card_inject_carries_keywords` | inject 된 user_prompt 에 `fg_bg_shared_anchor_rule.recommended_keywords` 5 entry 모두 substring 존재 | 5 substring in injected JSON |
| `test_primary_framing_rule_card_inject_carries_close_keywords` | inject 된 user_prompt 에 `primary_framing_rule.framing_scale_keywords.close` 11 entry 모두 substring 존재 | 11 substring in injected JSON |
| `test_card_hash_drift_on_spatial_consistency_change_in_verify` | spatial_consistency.camera_frame_rule.forbidden_combinations 변경 후 `verify_completion()` 실행 → hash drift detected → status partial | verify marks partial |
| `test_v19_checkpoint_resume_on_v20_escalates_force` | v19 cp (schema_version=7) 를 v20 consumer 로 resume → `_config_hash` mismatch → `mode="force"` escalate (G4.4 escalation test carry) | step_runner.escalate observed |
| `test_g3_2_sentinel_coexists_with_g4_5a_spatial` | 임의 shot → card `render_strategy.spatial_consistency` 신규 sub-field + variation-level `owned_validation` sentinel + `background_binding.constraints` + `id_policy` 4 신규 sub-field + `continuity_elements_used` 3 sibling policy dicts 모두 CP 에 존재 | five areas present in CP dict |
| `test_4_way_alignment_post_g4_5a_bump` | alignment test runner — v20 prompt dir + detail_steps.py:105 + version_registry.py:34 + version_registry.py:126 4 곳 lockstep 갱신 검증 | 4 locations match |

총 integration tests: **~10-15** (Override O-2 line count test + Override O-3
related migration tests).

> **`test_v19_checkpoint_resume_on_v20_escalates_force` (G4.2 + G4.3 + G4.4
> carry)**: G4.4 의 v18→v19 escalation test 와 같은 escalate path 가 v19→v20
> 에 자동 적용됨을 검증. `SCENE_DETAIL_PROMPT_VERSION` 변경이 `_config_hash`
> 에 reflect 되므로 step_runner 자동 force escalate.

### 6.3 Regression tests

G4.5a 구현 후 반드시 실행:

```
pytest backend/tests/ -x --timeout=120
```

기준: G4.4 baseline 대비 0 regressions. G4.5a test 추가 후 total count 증가만
허용. **RO-14 binding (test count baseline disambiguation)**: G4.4 끝
cumulative backend pytest count ≈ **2179**. G4.5a adds ~38 unit + ~13
integration → **≈2230 expected**. (G4.4 시 76/76 G4.4-specific 그대로 유지 —
§7.1 carry 변경 없음.)

G3.2 sentinel test suite (`test_g3_2_*.py`) + G4.1 (`test_g4_1_*.py`) + G4.2
(`test_g4_2_*.py`) + G4.3 (`test_g4_3_*.py`) + G4.4 (`test_g4_4_*.py`) 가
G4.5a 이후에도 모두 pass 해야 한다 (render_strategy 외 영역 변경 없음, 그리고
render_strategy 의 G4.1 base 7 sibling shape 도 변경 없음 — `spatial_consistency`
신규만).

### 6.4 alignment test (4-way sync)

`backend/tests/prompts/test_scene_detail_spatial_alignment.py`:

- `test_scene_detail_prompt_version_constant_matches_latest` —
  `SCENE_DETAIL_PROMPT_VERSION` 가 v20 latest 와 일치 (G4.5a 가 v19 → v20
  bump).
- `test_scene_detail_schema_version_synced_with_manifest` —
  `SCENE_DETAIL_SCHEMA_VERSION` 유지 (= 7) — render_strategy.spatial_consistency
  신규 sub-field 는 cp shape 변경 아님 (G4.2 R1-I6 carry).
- `test_scene_detail_version_registry_aligned` —
  `version_registry._MODULE_INFO['scene_detail_composer'].prompt_dependency`
  가 `scene_detail/v20` 로 갱신 (G4.3/G4.4 R3R4-B1 carry: dict access NOT
  attribute, major suffix only).
- `test_scene_detail_module_versions_aligned` —
  `MODULE_VERSIONS["scene_detail_composer"] == "1.20.0"` 검증 (silent fallback
  ban: `MODULE_VERSIONS.get(..., "")` 패턴 금지 — 명시적 키 존재 + 타입
  검증, G4.3/G4.4 R5-I2 carry).

> **4-way sync constraint (G4.3/G4.4 R3-I2 carry)**: v20 push 시 다음 4 곳
> lockstep 갱신 필수:
> 1. `prompts/_base/scene_detail/20.<TIMESTAMP>/system.md` 디렉토리 생성
> 2. `backend/app/core/steps/detail_steps.py:105` —
>    `SCENE_DETAIL_PROMPT_VERSION = "20.<TIMESTAMP>"`
> 3. `backend/app/core/version_registry.py:34` —
>    `MODULE_VERSIONS["scene_detail_composer"]` minor bump (1.19.0 → 1.20.0)
> 4. `backend/app/core/version_registry.py:126` —
>    `_MODULE_INFO["scene_detail_composer"].prompt_dependency = "scene_detail/v20"`
>    (`prompt_dependency` 값 = major suffix only `"scene_detail/v20"` —
>    G4.3/G4.4 R3R4-B1 carry)

---

## 7. G4.x Sequencing

### 7.1 G4.5a entry condition (= G4.4 exit criteria + commit b1c5032)

G4.4 exit criteria (G4.4 spec §5.1):
- G4.4 4 metric STRICT (double-description / zoom_in_detail entity 추가 /
  atmosphere_reference layout import / view-mixing) 통과 ✅ 또는 production
  canary 사용자 단계
- G4.4 token delta gate `≤ -540` system-only ✅ (실측 -1010)
- G4.4 v19 compact section line count gate `≤ 32 lines` ✅ (실측 14 lines)
- G4.4 unit + integration test green ✅ (commit `b1c5032`, 76/76)
- v19 prompt push ✅ (`prompts/_base/scene_detail/19.202605050814/`)
- 4-way sync ✅ (G4.4 Wave 2 complete)

> **R3-Entry-Note (G4.3/G4.4 carry — entry gate split)**: G4.4 production
> canary 미실행 시에도 G4.5a spec drafting / R1+R2 audit / plan drafting / Wave
> 1-5 구현 진행 가능. 단, **G4.5a commit gate 는 G4.4 production canary 6
> gate AND G4.5a self-canary 5 gate 둘 다 통과 필수**. canary 결과는 G4.5a v20
> push 직전 검증.

### 7.2 G4.5a 작업 순서

**Wave 1-A: render_prompt_card.py 변경**

1. `build_render_strategy()` 에 G4.5a **3 new sibling sub-rules + extended
   constraints list** (Option 3 — nested dict `spatial_consistency`) 작성
   로직 추가:
   - `camera_frame_rule` dict (6 required keys: applies_when /
     consistency_check_summary / forbidden_combinations / recommended_phrasings /
     self_check_steps / rationale_summary)
   - `fg_bg_shared_anchor_rule` dict (6 required keys: applies_when /
     shared_anchor_required / forbidden_phrasings / recommended_keywords /
     self_check_steps / rationale_summary)
   - `primary_framing_rule` dict (6 required keys: applies_when /
     framing_scale_keywords {3 sub-key: close / wide / medium_default} /
     close_framing_rules {3 sub-key} / wide_medium_rules {3 sub-key} /
     self_check_steps / rationale_summary)
   - `spatial_consistency.rationale_summary` (single string)
2. `render_strategy.constraints` G4.4 base 2 → G4.5a 5 (1 still moment + 1
   view-mix + 1 camera frame + 1 fg/bg anchor + 1 framing scale). 4 신규
   sub-rule 은 builder-static (G4.4 의 perception 분기와 달리 G4.5a 는 모든
   shot 동일).
3. `_assert_render_strategy_shape()` (`render_prompt_card.py` G4.1 location)
   에 `spatial_consistency` 누락 strict 검증 추가. 누락 시 AppError. 신규
   nested validator helper `_assert_spatial_consistency_shape()` 신설 (G4.4
   `_assert_ref_usage_constraints_shape()` 패턴 carry — nested 3 sub-rule
   각각 required key set 검증). **RO-9 binding (Q9 commit) — separate further
   nested helper `_assert_primary_framing_rule_shape()`** (sibling to
   `_assert_spatial_consistency_shape()`) 신설: primary_framing_rule 의 6 sub-key
   + framing_scale_keywords 3 sub-sub-key + close_framing_rules 4 sub-sub-key
   (incl `body_part_focus_cross_ref` RO-6) + wide_medium_rules 4 sub-sub-key
   (incl `body_part_focus_cross_ref` RO-6) + cross-ref string paired-substring
   `"see id_policy.body_part_focus_rule"` 모두 검증.
4. `build_render_prompt_card()` wrapper 에서 `_card_metadata.lift_status` 에
   **4 신규 key**: `spatial_camera_frame_lifted` / `spatial_fg_bg_shared_anchor_lifted`
   / `spatial_primary_framing_lifted` / `spatial_constraints_extended` 추가
   + `rule_source` 에 3 신규 key (`camera_frame_rule` / `fg_bg_shared_anchor_rule`
   / `primary_framing_rule`) 추가. 모든 mode 에서 `True` (spatial_consistency
   는 ref_usage / mode 무관 동일 적용).
5. module-level constants 도입 (G4.3/G4.4 R1-I2/R2-I6 carry — builder + canary +
   unit test 동일 import):
   - `_SPATIAL_CAMERA_LOW_TOKENS` (R1+R2 audit 시 ground-truth list 확정 —
     초기 후보: `("low", "ground level", "quay level", "floor level", "낮은
     관찰자", "발 높이")`)
   - `_SPATIAL_CAMERA_HIGH_TOKENS` — 초기 후보: `("overhead", "from above",
     "high angle", "위에서", "오버헤드")`
   - `_SPATIAL_CAMERA_HIP_TOKENS` — 초기 후보: `("hip height", "waist height",
     "허리 높이")`
   - `_SPATIAL_FRAME_EDGE_POSITION_TOKENS` — frame 안 vertical 좌표 token
     (`"chest"`, `"waist"`, `"가슴"`, `"허리"`, `"floor"`, `"바닥"`, `"ceiling"`,
     `"천장"`)
   - `_SPATIAL_FG_BG_SEPARATION_TOKENS` — 초기 후보: `("foreground",
     "background", "rear background", "전경", "후경")`
   - `_SPATIAL_INTERACTION_VERBS` — 초기 후보: `("facing", "handing", "dialogue",
     "hold", "across", "마주", "건네")`
   - `_SPATIAL_SHARED_ANCHOR_KEYWORDS` — 초기 후보: `("shared bench", "shared
     table", "shared surface", "the same", "across the", "single line of
     sight", "<prop> crosses", "같은 벤치", "같은 테이블")`
   - `_SPATIAL_FRAMING_CLOSE_KEYWORDS` — 11 entries (영어 6 + 한국어 5)
   - `_SPATIAL_FRAMING_WIDE_KEYWORDS` — 5 entries
   - `_SPATIAL_BODY_PART_TRIGGERS` — G4.3 의 `_ID_BODY_PART_TRIGGERS` 4 trigger
     alternation 재사용 (cross-import) — `("focus on", "close on", "tight on",
     "detail on")`

**Wave 1-B: v20 prompt 신규 디렉토리**

6. `prompts/_base/scene_detail/20.<TIMESTAMP>/` 디렉토리 생성:
   - `system.md` = v19 system.md - 124 lines [Rule F + Rule G + Rule J] +
     ~13-line "Spatial consistency" compact section (§3.3).
   - `detail_schema.json` = v19 verbatim copy (cp shape 변경 없음).
   - `user.md` = v19 verbatim copy (변경 없음).
7. v19 sealed (덮어쓰기 금지 — `feedback_prompt_versioning.md`).
8. tiktoken cl100k_base 실측 — Wave 1-B draft 완성 직후 §3.4 estimate vs
   실측 비교 + token gate threshold §5.1 보정 (G4.2 R2-M4 carry).

**Wave 2: detail_steps.py + version_registry 4-way sync**

9. `detail_steps.py:105` —
   `SCENE_DETAIL_PROMPT_VERSION = "19.202605050814"` →
   `"20.<TIMESTAMP>"`. SCENE_DETAIL_SCHEMA_VERSION 유지 (= 7).
10. `detail_steps.py` inject 직전 `_card_metadata` strip 로직 변경 0 (G4.2 +
    G4.3 + G4.4 carry 그대로 유지). `render_strategy.spatial_consistency` 본
    sub-field 는 inject 대상 (LLM 이 참조). `_card_metadata` 만 strip.
11. (G4.4 의 detail_steps.py inline prose 9 줄 삭제 같은 패턴은 G4.5a 에 없음
    — Wave 2 inline prose 삭제 작업 없음. G4.5b 에서 잔여 inline prose
    재검토.)
12. `version_registry.py:34` — `MODULE_VERSIONS["scene_detail_composer"]`
    `1.19.0` → `1.20.0`.
13. `version_registry.py:126` —
    `_MODULE_INFO["scene_detail_composer"].prompt_dependency =
    "scene_detail/v20"` (G4.3/G4.4 R3R4-B1 carry). `updated_at = "2026-05-05"`
    또는 신규 timestamp.
14. alignment test
    (`backend/tests/prompts/test_scene_detail_spatial_alignment.py`) green
    유지 검증 — silent fallback 제거 패턴 (`MODULE_VERSIONS.get(..., "")` /
    `_MODULE_INFO.get(..., {})` 금지) carry.

**Wave 3: canary scripts 4종 작성**

15. `scripts/canary/g4_5a_camera_frame_consistency.py` — camera vertical token
    1+ 존재 shot 만 scan, `_SPATIAL_CAMERA_LOW_TOKENS` 등 module-level constant
    + `_SPATIAL_FRAME_EDGE_POSITION_TOKENS` 동시 등장 + 모순 패턴 (low + chest
    height 등) 검출. **G4.2 + G4.3 + G4.4 silent-absorb 차단 패턴 carry**.
16. `scripts/canary/g4_5a_fg_bg_shared_anchor.py` — 두 인물 ID + fg/bg
    separation + 인터랙션 동시 등장 shot 만 scan, `_SPATIAL_SHARED_ANCHOR_KEYWORDS`
    부재 검출.
17. `scripts/canary/g4_5a_primary_framing.py` — 모든 shot scan, close-framing
    shot scope filter = production `_CLOSE_FRAMING_RE` **9-entry alignment**
    재사용 (RO-5 binding — LLM-facing 11-entry list 와 분리). 같은 인물 ID 의
    전신/부위 close-up 동시 등장 검출 + 두 인물 face close-up 동시 등장 검출 +
    wide/medium framing shot 의 body-part close-up trigger 등장 검출.
    **RO-8 same-character-ID detection algorithm**: per-shot scan, build dict
    `{character_id: [token_positions]}`. body-part close-up trigger
    (`focus on / close on / tight on / detail on`) token position 마다 nearest
    C##/C##O## within ±N=20 tokens → `cid_trigger`. full body keyword
    (`stands / seated / leaning / 전신`) position 마다 nearest character ID →
    `cid_body`. `cid_trigger == cid_body` AND both nearest-character-distance
    ≤ 20 → violation. body-part triggers 는 G4.3 의 `_ID_BODY_PART_TRIGGERS` 를
    alias-import 로 직접 재사용 (RO-11 binding — `_SPATIAL_BODY_PART_TRIGGERS`
    별칭 도입 금지). **Override O-17 / O-35 carry — regex brittleness**:
    false-positive 발견 시 LLM-validator 전환 follow-up.
18. `scripts/canary/g4_5a_token_count.py` — `tiktoken cl100k_base` 로 v20
    system.md 실측 + system+inject 합산 측정 (Override O-1 secondary report-only
    metric). CLI args = pinning 7 fields (G4.2 plan-R2-I4 carry).

**Wave 4: tests**

19. `backend/tests/unit/test_g4_5a_spatial_lift.py` — ~30-35 unit tests (§6.1).
20. `backend/tests/integration/test_g4_5a_spatial_integration.py` — ~10-15
    integration tests (§6.2 — line count gate test 포함).
21. `backend/tests/prompts/test_scene_detail_spatial_alignment.py` — 4-way
    alignment test (§6.4) + silent fallback 제거 패턴.
22. fixture pattern: `_DEFAULT_SENTINEL = object()` carry (G4.1 R3-B2 / G4.2 /
    G4.3 / G4.4 carry).
23. **Wave subagent dispatch 시 hardcoded `[]` fixture trap 회피 명시** (G4.1
    + G4.3 R4-M6 + G4.4 carry): 신규 nested `spatial_consistency` 4 sub-key
    (3 sub-rule + rationale_summary) 는 builder-static — `if camera_direction:`
    또는 `if shot_info:` 류 truthy guard 안 배치 절대 금지. test
    `test_spatial_consistency_4_subfields_independent_of_input` 가 강제.

**Wave 5: production canary execution (사용자 단계)**

24. Baseline canary capture (v19) — pinning 7 field 명시.
25. Candidate canary capture (v20) — 동일 pinning.
26. 3 metric + token + line count + pytest = 5 gate 판정 (Override O-1 token
    gate `≤ -1000` system-only binding + Override O-2 line count `≤ 32` +
    3 strict 0 metric).

**Wave 6: single commit + push**

27. 4 영역 (render_prompt_card.py + detail_steps.py + version_registry.py +
    prompts/_base/scene_detail/20.../) + canary scripts 4종 + tests 3 file
    (unit + integration + alignment) = single commit + push.
28. Codex + Claude 듀얼 리뷰 → 수정 → 재push (G4.2/G4.3/G4.4 패턴 — 3 iter
    dual review fix loop).

### 7.3 G4.5b entry condition (= G4.5a exit criteria)

G4.5b (Rule B/D/outfit/evidence lift) 는 다음 모두 충족 후 진입:

- G4.5a 3 metric STRICT (camera-frame violation / fg-bg shared-anchor missing /
  primary framing violation) 통과
- G4.5a token delta gate `≤ -1000` system-only 통과 (Override O-1 binding 1차
  estimate)
- G4.5a v20 compact section line count gate `≤ 32 lines` 통과 (Override O-2
  binding)
- G4.5a unit + integration test suite green
- v20 prompt push + 4-way sync 완료

G4.5b scope: Rule B (line 63-90 조명·색조 28L) + Rule D (line 386-408 큰 prop
23L) + outfit 묘사 (line 418-441 24L) + evidence and inference disclosure
(line 494-533 40L) lift. 5-field envelope contract 안 흡수 유지 (사용자 binding
invariant).

### 7.4 G4.5c entry condition (= G4.5b exit criteria)

G4.5c (dense compression / final 250-line target) 는 G4.5b exit criteria
충족 + 잔여 prose heading count ≤ 10 enforcement + 추가 token 절감 + 미 lift
된 reasoning prose 통합.

---

## 7.x Entry gate 명시 (G4.2 + G4.3 + G4.4 carry)

| G4.4 Exit gate | 현재 상태 (2026-05-05) |
|---|---|
| double-description count 0 STRICT | canary measurement pending (Wave 5 production 사용자 실행) |
| zoom_in_detail entity 추가 0 STRICT | canary measurement pending |
| atmosphere_reference layout import 0 STRICT | canary measurement pending |
| view-mixing pattern 0 STRICT | canary measurement pending |
| unit + integration green | ✅ (commit `b1c5032`, 76/76) |
| token delta ≤ -540 | ✅ dry-run -1010 |
| v19 prompt push | ✅ (`prompts/_base/scene_detail/19.202605050814/`) |
| 4-way sync (registry + alignment test) | ✅ (G4.4 Wave 2 complete) |

> **G4.5a spec drafting 진행 가능 (entry gate split carry from G4.3/G4.4 R3-I3)** —
> production canary 결과는 G4.5a commit gate 로만 필요 (spec / plan / Wave
> 1-5 작성은 무관). G4.5a commit gate = G4.4 production canary 6 gate AND
> G4.5a self-canary 5 gate.

---

## 8. Risk Register

### 8.1 G4.5a-unique risks

| risk | mitigation | measurable threshold |
|---|---|---|
| **Rule F/G/J 가 visual quality critical (★ 신규 표시) — lift 후 LLM 행동 변화** | 3 metric STRICT 0 + Wave 5 production canary 사용자 단계 (G4.4 carry) + R1+R2 audit 시 ground-truth list 정확 검증 + body-part trigger reuse from G4.3 | 3 candidate count == 0 STRICT |
| 5-field contract 안정성 — render_strategy 비대 (G4.5b 추가 lift 시 더 비대) | **Option 3 채택 — nested dict `spatial_consistency`** 로 render_strategy top 비대 방지. G4.4 의 `view_consistency` sibling dict 패턴 carry. G4.5b spatial 외 lift 는 다른 nested dict 또는 다른 field (id_policy 등) 으로 분산 | render_strategy top-level keys count ≤ 9 (mode + 5 sibling + spatial_consistency + constraints) |
| Rule G 매핑 분기 — render_strategy vs background_binding 의 semantic stretch | **Option 3 채택 — render_strategy.spatial_consistency 안 통합**: Rule G 가 "두 인물 spatial relationship" 이므로 background 보다 spatial 의미 강함. R1+R2 audit 가 final 결정. | mapping decision recorded in §2.2 |
| Token gate -1000+ 까지 줄지 검증 필요 (RO-1 binding) | Wave 1-B 실측 + behavior canary 동시. 1차 estimate -1665 system-only / -1075 합산 (§3.4). R2 분석: G4.4 estimate→actual realization ≈83% → G4.5a estimate -1665 → actual ~-1380 → **28% margin over -1000 gate, sufficient** (RO-1 binding — 이전 spec "65% margin" claim 정정). | Wave 1-B 실측 ≤ -1000 system-only |
| **Rule F core_principles 누락 (RO-3 binding)** | v19 line 170-175 의 4 핵심 원칙 (낮은 카메라 / overhead / 허리 / full body) 이 `consistency_check_summary` single string 에 묻혀 LLM 이 sub-rule 으로 따로 인지 못 할 위험 → `core_principles` (4-entry list) 별도 sub-key 로 lift. unit test 4 entries count + 4 substring assertions. required keys 6 → 7. | unit test 5 종 + integration test (camera_frame_rule.core_principles 4-entry inject 검증) |
| **framing_scale enum drift (RO-4 binding)** | `_derive_framing_scale()` producer enum 은 close/medium/insert (production) — wide-class shots 는 medium 으로 매핑. card-only classification 채택 — LLM 은 prose keyword 로 wide 추론 가능하나 reasoning hint only. spec §2.2 line 202 + §2.2 applies_when + §2.3 producer matrix + §5.1 metric scope + Glossary 모두 동기화 — producer 변경 = G4.5c/follow-up. | producer enum 변경 0 in G4.5a + spec 5 곳 동기화 |
| **Korean close-framing keyword conflict (RO-5 binding)** | card `framing_scale_keywords.close` LLM-facing 11-entry list (`손이`/`눈이`/`얼굴이` 포함) 그대로 carry — LLM reasoning hint. canary `g4_5a_primary_framing.py` close-framing shot scope filter 는 production `_CLOSE_FRAMING_RE` **9-entry alignment** (`close-up`/`CU`/`MCU`/`ECU`/`XCU`/`extreme close-up`/`medium close-up`/`클로즈업`/`손가락이`) 재사용 — LLM hint vs canary detector 분리 strict. unit test 11-count + canary smoke 9-entry alignment 둘 다. | unit test 2 종 + canary 9-entry alignment fixture |
| **Cross-card reference (RO-6 binding)** | `primary_framing_rule.close_framing_rules.body_part_focus_cross_ref` + `primary_framing_rule.wide_medium_rules.body_part_focus_cross_ref` 둘 다 paired-string `"see id_policy.body_part_focus_rule"` 포함. `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` constant 재사용 (G4.4 carry, rename = G4.5c follow-up — RO-13). 2 unit tests (close + wide_medium body_part cross_ref paired) + 1 integration test (3-way substring assertion). | unit test 2 종 + integration test 1 종 |
| **same-character-ID detection algorithm (RO-8 binding)** | `g4_5a_primary_framing.py` canary 의 algorithm 명시 — per-shot scan, `{character_id: [token_positions]}` dict, body-part trigger 와 full-body keyword 의 nearest character ID 가 동일 + 둘 다 ±20 tokens 이내 → violation. fixture 3 cases: same-id violation / different-id no-violation / > 20 tokens distance no-violation. Wave 3 implementer follows verbatim. | unit test fixture 3 cases + canary 구현 algorithm 일치 |
| **`_assert_primary_framing_rule_shape()` separate helper (RO-9 binding)** | nested 깊이 3 levels (rule → close_framing_rules → 4 sub-key) 검증을 `_assert_spatial_consistency_shape()` 안 inline 하면 가독성/유지보수 hurt. separate further nested helper `_assert_primary_framing_rule_shape()` 신설 — sibling to spatial helper. G4.4 `_assert_ref_usage_constraints_shape` 패턴 carry. unit test 4 종 (framing_scale_keywords / close_framing_rules / wide_medium_rules / cross-ref strings 검증). | separate helper 도입 + 4 unit tests |
| Staged execution coordination — G4.5a 결과가 G4.5b architecture 결정에 영향 | G4.5a spec §7.3 / §7.4 에 G4.5b/c 진입 조건 + scope 명시. spatial_consistency nested dict 패턴 검증 후 G4.5b 의 Rule B/D 매핑 (id_policy 분산 vs 다른 nested dict) 결정. | G4.5a complete 후 G4.5b spec drafting 시 carry traps 19개 + G4.5a-unique pattern 1개 추가 |
| `_SPATIAL_CAMERA_LOW_TOKENS` / `_SPATIAL_FRAME_EDGE_POSITION_TOKENS` ground-truth list 가 actual production prompt 의 vertical 표현 다양성을 cover 하지 않음 | R1+R2 audit 에서 ground-truth list 확정 + Rule F line 148-153 actual prose 의 표현 카탈로그 + Wave 5 production canary 에서 baseline scan time 에 missed phrase 자동 발견 시 list 확장 (P1 follow-up 별도 추적) | actual production canary baseline 에 등장한 vertical token ≥ 90% list cover |
| `_SPATIAL_SHARED_ANCHOR_KEYWORDS` 가 너무 narrow 해 자연스러운 shared anchor 표현 (e.g. `"on the same wooden bench"`) false-negative | R1+R2 audit 에서 keyword pattern 정정. `the same` substring + 가구/공간 명사 (bench/table/sofa/door/room) co-occurrence 도 anchor 로 인정. | unit test fixture 가 `"on the same wooden bench"` 류 anchor 인정 검증 |
| primary framing canary 가 wide framing 의 자연스러운 부분 묘사 (`"hand resting on the table"`) false-positive | regex 가 close framing keyword + same-character ID + body-part close-up trigger 셋 동시 등장 시에만 trigger. 단순 wide 의 hand 언급은 trigger 아님. R1+R2 audit 에서 regex 정정 | unit test fixture 가 wide framing 자연스러운 부분 묘사 false-positive 0 검증 |
| `_card_metadata.lift_status` 신규 4 key 가 hash 에 의도치 않게 포함되어 hash 가 매 build 마다 변경 | G4.2 R1-B1 + G4.3/G4.4 carry: `_card_metadata` 전체 가 `canonicalize_render_prompt_card()` 에서 strip. unit test `test_card_metadata_excluded_from_hash_after_g4_5a_keys_added` 가 강제 | `_card_metadata` not in canonicalized output (post G4.5a key 추가) |
| canary PID 의 camera vertical / fg-bg / close framing shot 부족 시 metric 분모 0 | baseline / candidate scene set 선정 시 camera vertical shot 최소 5 + fg-bg 최소 3 + close framing 최소 5 보장 (§5.2) | scene_set in canary JSON ≥ 5+3+5 분리 |
| canary baseline camera-frame / fg-bg / primary-framing count 가 비0 인 경우 | strict: candidate == 0 강제. baseline 비0 이면 P1 follow-up 으로 별 추적 (G4.5a scope 밖이지만 production 결함 시그널) | candidate count == 0 mandatory; baseline 비0 면 별 issue 등록 |
| Wave subagent fixture 가 신규 nested `spatial_consistency` (4 sub-key + 3 sub-rule × 6 sub-sub-key) 누락한 partial card 사용 → production drift cascade | `_DEFAULT_SENTINEL = object()` fixture pattern + `_make_full_card()` helper 가 항상 spatial_consistency 4 sub-key + 3 sub-rule 채움 + `_assert_render_strategy_shape()` strict + `_assert_spatial_consistency_shape()` nested helper (G4.4 R2-I5 carry, superset) | unit test `test_assert_*_shape` 7종 |
| nested dict 깊이 (3 sub-rule × 6 sub-sub-key, primary_framing_rule 안 추가 nested 3 sub-key × 3 sub-sub-sub-key) 가 `_assert_spatial_consistency_shape()` 검증 누락 위험 | nested validator helper `_assert_spatial_consistency_shape()` 신설 + `_assert_primary_framing_rule_shape()` further nested helper (G4.4 `_assert_ref_usage_constraints_shape()` 패턴 carry) + 7 unit test (`test_assert_*_shape`) | nested shape AppError on missing sub-key |
| **regex brittleness on 3 spatial canaries** (Override O-17 carry) | 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 등록 |
| **사용자 binding invariant 위반 — G4.5a 의 어떤 path 든 신규 top-level field 도입 위험** | spec §1.1 footnote + R1+R2 audit BLOCKING 최우선 + plan drafting 시 Wave 1-A producer 변경이 envelope 7-field 만 변경하는지 강제 검증 | `assert_card_shape()` envelope 7 field 동일 + integration test `test_envelope_top_level_keys_unchanged_post_g4_5a` |

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

| inherited risk | carry source | G4.5a enforcement |
|---|---|---|
| v20 로 resume 시 old v19 cp 가 자동 force escalate | G4.1 R1-B1 (`_config_hash` mismatch 검증) | §6.2 `test_v19_checkpoint_resume_on_v20_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.5a 변경 없음 |
| schema_version != 7 result 에서 card check bypass warning step 당 1회 emit | G4.1 cleanup I5 (commit c656ce3) | G4.5a 변경 없음 — 동일 동작 |
| `or [] / or {}` silent absorb pattern 차단 | G4.2 iter1 C3 + G4.3 R5-B1 + G4.4 carry | G4.5a canary scripts 4종 + 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 carry | G4.5a fixture (None / [] / default 셋 명시 구분) — Trap #2 |
| canary CP shape `scene["render_prompt_card"]["render_strategy"]` (NOT 평면) | G4.2 함정 + G4.3 `_validate_scene_preflight()` helper carry | G4.5a 3 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/G4.4 carry | G4.5a canary scripts 4종 동일 pattern — Trap #4 |
| `_MODULE_INFO` dict access (NOT attribute) + `prompt_dependency = "scene_detail/v20"` (major suffix only) | G4.3/G4.4 R3R4-B1 | G4.5a alignment test 동일 — Trap #5 |
| token gate signed convention `token_delta = candidate - baseline`, gate `<= -<value>` | G4.3/G4.4 R4-B4 | §5.1 token gate 수치는 R1+R2 audit 후 확정 — Trap #6 |
| compact section line count 정확 (multi-line wrap trap) — bullet 1줄 강제 | G4.3/G4.4 R3R4-B3 | §3.3 compact section 13 lines target, Wave 1-B draft 시 line count 자체 점검 — Trap #7 |
| module-level constants single source — builder + canary + unit test 동일 import. drift 차단 | G4.3/G4.4 R1-I2/R2-I6 | Wave 1-A step 5 — Trap #8 |
| 시나리오 의존 0 — 작품 고유명사 절대 금지. 예시 prose 도 generic Asian man / a young woman / a hand 등 | CLAUDE.md 절대 규칙 + `feedback_no_scenario_names.md` | §2.2 / §3.3 / §6.1 / §9 모든 예시 generic placeholder 사용 — Trap #9 |
| prompt versioning — NEVER overwrite v19, 새 dir `prompts/_base/scene_detail/20.<TIMESTAMP>/` | CLAUDE.md `feedback_prompt_versioning.md` | Wave 1-B step 6-7 — Trap #10 |
| 4-point sync lockstep — prompt dir + detail_steps.py:105 + version_registry.py:34 + version_registry.py:126 | G4.3/G4.4 R4-I6 | Wave 2 step 9-13 + alignment test — Trap #11 |
| `_card_metadata.rule_source` 키 명세 — namespace 분리되어 top-level field 와 동명 OK | G4.3 R5-I1 + G4.4 carry | §2.2 _card_metadata 표기 namespace 일관 — Trap #12 |
| alignment test silent fallback 제거 — `MODULE_VERSIONS.get(..., "")` / `_MODULE_INFO.get(..., {})` 금지 — 명시적 키 존재 + 타입 검증 | G4.3 R5-I2 + G4.4 carry | §6.4 4 alignment test — Trap #13 |
| Wave subagent hardcoded `[]` fixture trap — 신규 sub-field 는 builder-static, `if visible_entities:` truthy guard 안 배치 절대 금지 | G4.1 origin / G4.3 R4-M6 + G4.4 carry | §6.1 `test_spatial_consistency_4_subfields_independent_of_input` 강제 — Trap #14 |
| **silent-fallback 잔존 carry (G4.4 Override O-8)** | `backend/app/core/version_registry.py:148-155` 의 `MODULE_VERSIONS.get(..., "0.0.0")` / `_MODULE_INFO.get(..., {})` 잔존 = G4.1 inheritance, G4.5a scope 외 | **G4.5c 또는 별도 cleanup PR carry follow-up**. G4.5a 신규 코드 (Wave 1-A producer + canary + tests) 는 새로운 silent-fallback 도입 절대 금지 — Trap #15 |
| **`forward_zoom_targets` G4.4 lift coverage 0** (G4.4 Override O-4 Option A) | G4.4 carry — G4.5b/c 에서 재검토 | G4.5a scope 아님 — Trap #16 |
| **R3 Codex CLI 두 번 BLOCKED carry** (G4.4 진단) | G4.4 audit 단계 R*-Codex BLOCKED 시 fast manual fallback 전환 — tighter dimension scope (8-10 file reads cap) + explicit "emit final block then STOP" 지시 | G4.5a R*-audit 진행 시 동일 패턴 carry — Trap #17 |
| **Phase 4 / Phase 2 Task 번호 drift trap** (G4.4 R4-B1/B2) | plan body Task 순서 vs Implementation Checklist Summary vs Override carry 불일치 위험. plan 작성 시 numbering canonical 일치 강제 — drift 발생 시 즉시 patch | G4.5a plan drafting 시 동일 검증 — Trap #18 |
| **Plan agent 출력 truncation trap** (G4.4 진단) | 큰 plan (2500+ lines) 작성 시 agent 출력이 60KB persisted-output cap 에 truncate. 회수: SendMessage 로 missing prefix/suffix 회수 + 병합. 또는 task 분할 | G4.5a plan drafting 시 동일 회수 패턴 carry — Trap #19 |
| `t2i_variations` + per-variation `t2i_prompt` validation | G4.2 iter2 B2 + iter3 B3 + G4.4 carry | G4.5a 3 metric scripts 모두 검증 |
| LLM 입력 truncation 절대 금지 (`[:400]`, `[:500]` 등) | CLAUDE.md 절대 규칙 | G4.5a canary script 가 t2i_prompt 전문 scan, deepcopy lossless |
| regex post-processing 의존 금지 (`feedback_no_regex_postprocessing.md`) | CLAUDE.md 메모리 | G4.5a canary 의 regex 는 deterministic pattern 만, 의미 판단은 module-level constant token list (LLM validator 또는 prompt 우선) |

---

## 9. Glossary

| 용어 | 정의 |
|---|---|
| `render_strategy.spatial_consistency` | `RenderPromptCard` 의 5 semantic field 중 `render_strategy` 안 신규 sibling nested dict (G4.5a). camera vertical 일관성 + fg/bg 공유 anchor + primary framing scale 룰을 deterministic 하게 기술. 4 required sub-key (3 sub-rule + rationale_summary). |
| Camera frame consistency rule (Rule F, v19:144-176) | camera_direction 의 vertical 위치/높이와 frame 안 요소 vertical/depth 위치 모순 검출. 4 forbidden combination + 4 recommended phrasing + 4 self-check step. |
| fg/bg shared anchor rule (Rule G, v19:177-217) | 두 인물 인터랙션 + fg/bg 분리 + 공유 surface/공간 anchor 명시 강제. 3 forbidden + 5 recommended keyword + 4 self-check step. |
| Primary framing rule (Rule J, v19:218-267) | shot 의 primary subject + framing scale (close/medium/wide) 고정 + 다른 entity 등장 형태 제한. close framing 시 primary only fully visible / 다른 entity partial-soft bg-absent / 같은 인물 전신+부위 close-up 금지 / 두 명 face close-up 금지. wide/medium 시 body-part close-up 금지. |
| `camera_frame_rule` | render_strategy.spatial_consistency sub-key. 6 required keys: applies_when / consistency_check_summary / forbidden_combinations (4 entries) / recommended_phrasings (4 entries) / self_check_steps (4 entries) / rationale_summary. |
| `fg_bg_shared_anchor_rule` | render_strategy.spatial_consistency sub-key. 6 required keys: applies_when / shared_anchor_required / forbidden_phrasings (3 entries) / recommended_keywords (5 entries) / self_check_steps (4 entries) / rationale_summary. |
| `primary_framing_rule` | render_strategy.spatial_consistency sub-key. 6 required keys: applies_when / framing_scale_keywords (3 sub-key: close 11 + wide 5 + medium_default) / close_framing_rules (3 sub-key) / wide_medium_rules (3 sub-key) / self_check_steps (4 entries) / rationale_summary. |
| `render_strategy_card_snapshot_hash` | render_strategy field 의 partial canonicalize hash. canary pinning 7 field 중 1. baseline / candidate render_strategy shape 동일 검증. **helper `compute_render_strategy_snapshot_hash(card) = sha256(json.dumps(card["render_strategy"], sort_keys=True, ensure_ascii=False))[:16]`** (G4.3/G4.4 패턴 carry). |
| Camera vertical tokens **(R1+R2 ground-truth source)** | camera_frame_rule 검출 대상 vertical 위치 키워드 ground-truth list. 본 정의가 single source — §2.2 / §3.3 / §6.1 unit test / canary detector 모두 본 list 를 reference. R1+R2 audit 에서 정확 N entry 확정 (초기 후보: low / ground level / quay level / floor level / overhead / from above / high angle / hip height / waist height + 한국어 대응). |
| Frame edge position tokens **(R1+R2 ground-truth source)** | camera_frame_rule violation 검출 대상 frame 안 vertical 좌표 토큰. 본 정의가 single source. R1+R2 audit 에서 정확 N entry 확정 (초기 후보: chest / waist / floor / ceiling + 한국어 대응). |
| FG/BG separation tokens **(R1+R2 ground-truth source)** | fg_bg_shared_anchor_rule trigger 키워드 (foreground / background / rear background + 한국어 대응) ground-truth list. R1+R2 audit 시 정확 N entry 확정. |
| Interaction verbs **(R1+R2 ground-truth source)** | fg_bg_shared_anchor_rule applies_when 의 인터랙션 동사 (facing / handing / dialogue / hold / 마주 / 건네) ground-truth. |
| Shared anchor keywords **(R1+R2 ground-truth source)** | fg_bg_shared_anchor_rule.recommended_keywords. shared bench / shared table / the same X they both occupy / across the X / single line of sight. |
| Framing scale keywords **(R1+R2 ground-truth source)** | primary_framing_rule.framing_scale_keywords. close 11 entries (영어 close-up / CU / MCU / ECU / XCU / 한국어 클로즈업 / 손가락이 / 손이 / 눈이 / 얼굴이) + wide 5 entries (wide shot / establishing / aerial / 전경 / 전신) + medium_default. |
| Body part triggers | G4.3 의 `_ID_BODY_PART_TRIGGERS` 4 trigger alternation 재사용 (focus on / close on / tight on / detail on). G4.5a primary_framing canary cross-import. |
| canary | 광범위 적용 전 1 PID / 1 selected segment 로 baseline vs candidate 비교 검증 (G4.2/G4.3/G4.4 carry). 결과는 `docs/canary/g4_5a_spatial_<timestamp>.json` 에 저장. |
| G4.4 baseline | G4.4 commit `b1c5032` 기준의 4 metric (double-description / zoom_in_detail entity 추가 / atmosphere_reference layout import / view-mixing). G4.5a 가 4 metric 모두 carry — degradation 차단. |
| 4-way sync | v20 push 시 lockstep 갱신 4 곳: prompts/_base/scene_detail/20.../system.md 디렉토리 + detail_steps.py:105 SCENE_DETAIL_PROMPT_VERSION + version_registry.py:34 MODULE_VERSIONS + version_registry.py:126 _MODULE_INFO. |
| Option 3 (sub-shape mapping) | G4.5a 의 sub-shape 결정 — 3 룰 통합 nested dict `render_strategy.spatial_consistency` (sibling nested dict). G4.4 의 `view_consistency` 패턴 carry. Option 1 (3 룰 모두 render_strategy top-level sibling 흡수) / Option 2 (Rule G 만 background_binding 으로) 와 비교 후 채택. |
| Behavior canary | G4.5a Wave 5 production canary — Rule F/G/J 가 visual quality critical 이므로 별도 검증 단계. 3 metric STRICT 0 + token + line count + integration alignment 5 gate. |
| Staged execution (Option A') | G4.5 의 3 단계 진행 (G4.5a → G4.5b → G4.5c). 사용자 binding 핵심 원칙 (새 top-level field 금지 / 기존 5-field contract 안 흡수) 모든 단계에서 invariant. |

> **t2i_prompt 영어 가정 footnote (G4.4 carry)**: `_SPATIAL_CAMERA_LOW_TOKENS`
> / `_SPATIAL_FRAME_EDGE_POSITION_TOKENS` ground-truth list 가 영어/한국어
> 혼합 substring 포함 (e.g. `"low"` + `"낮은"`) 은 t2i_prompt 영어 가정 +
> shot description 한국어 가정에 의존 — `feedback_t2i_storyboard.md` carry.
> 한국어 t2i prompt 도입 시 G4.5c 에서 generic-noun 패턴 재검토.

---

## 10. Open Questions (R1+R2 audit 결정 대상)

R1+R2 audit 가 진행되면 본 §10 의 question 들은 §0 Round Override 표 row 로
이동. 본 §10 는 spec drafting 시점의 outstanding issues.

| ID | 질문 | 결정 |
|---|---|---|
| Q1 | sub-shape 선택 — Option 1 (render_strategy top-level 3 sibling) vs Option 2 (Rule G 만 background_binding) vs Option 3 (nested dict spatial_consistency) | **Option 3 채택** (Round 1 RO-N pre-existing user binding §1.1 + §12 G4.5a-binding row). |
| Q2 | token gate threshold system-only `≤ -1000` vs system+inject 합산 `≤ -800` 중 어느? | **Round 1 RO-1 에서 결정**: system-only `≤ -1000` binding gate. system+inject ≤ -800 secondary report-only. R2 분석 G4.4 realization ≈83% → 28% margin sufficient. Wave 1-B 실측 후 Round 2 Override 로 downward tighten 가능. |
| Q3 | sub-field 명명 — `spatial_consistency` vs `spatial_rules` vs `frame_consistency` | **spatial_consistency 채택** — G4.4 view_consistency 패턴 명명 일관성 + spatial 의미 명확 (Round 1 모든 RO 가 본 명명 reference). |
| Q4 | `_SPATIAL_CAMERA_LOW_TOKENS` ground-truth 정확 N entry | 초기 후보 6 entry 유지 (영어 4 + 한국어 2). Wave 1-B 시 v19 line 148-160 actual prose 검증 시 list 확장 가능 — R3+R4 audit 에서 fix. |
| Q5 | `_SPATIAL_FRAMING_CLOSE_KEYWORDS` 가 11 entry (영어 6 + 한국어 5) — 적정한가? | **Round 1 RO-5 에서 결정**: card `framing_scale_keywords.close` LLM-facing 11-entry list 그대로 carry (LLM reasoning hint). canary close-framing shot scope filter 는 production `_CLOSE_FRAMING_RE` **9-entry alignment** 재사용 — LLM hint vs canary detector 분리 strict. |
| Q6 | Rule G 매핑 — render_strategy.spatial_consistency vs background_binding (semantic stretch) | **render_strategy.spatial_consistency 채택** (Option 3 안 통합) — Round 1 모든 RO 가 본 매핑 reference. |
| Q7 | unit test count — G4.4 46 vs G4.5a 추정치 ~30-35? | **Round 1 RO-12 에서 결정**: ~38-43 (table 38 enumerate + 5 fix-loop buffer). |
| Q8 | `compute_render_strategy_snapshot_hash(card)` helper 신설 위치 | `render_prompt_card.py` 안 G4.3 `compute_id_policy_snapshot_hash` + G4.4 `compute_continuity_snapshot_hash` 옆 (Round 1 무관, §5.2 carry). |
| Q9 | `_assert_spatial_consistency_shape()` nested helper 안에 `_assert_primary_framing_rule_shape()` further nested 분리 vs 단일 helper 안 인라인 검증? | **Round 1 RO-9 에서 결정**: separate further nested helper 분리 채택 — primary_framing_rule 자체가 nested 깊이 3 levels (rule → close_framing_rules → 4 sub-key). G4.4 `_assert_ref_usage_constraints_shape` 패턴 carry. |
| Q10 | constraint string count — G4.4 7 strings vs G4.5a 5 strings? | **G4.5a 5 strings** (G4.4 base 2 carry + 3 신규 spatial). G4.4 7 = continuity 의 prose 6 sub-rule + 1 prop orientation, G4.5a 는 spatial 3 sub-rule 만 (Round 1 무관, §2.2 carry). |

**Round 1 Open Questions: 0 outstanding** (Q1, Q3, Q6, Q8, Q10 = pre-existing
user binding 또는 §body 본문 carry; Q2 = RO-1, Q5 = RO-5, Q7 = RO-12, Q9 = RO-9
모두 Round 1 RO-N에서 결정). Q4 = Wave 1-B 시 R3+R4 audit 에서 fix (운영 단계
이양). R3+R4 plan audit 에서 추가 발견 사항 시 §0 Round 2 Override row 로 이동.

---

## 11. Override 표 (Round audit 진행 시 채움)

본 섹션은 spec 상단 "## Round Override (audit-driven, applied before body)"
가 진실의 source. 각 round 의 BLOCKING / IMPORTANT / MINOR 결정은 상단 표에
기재되며 본문도 동기화 패치 적용. 본 §11 은 history 추적 용도.

### Round 1 — Codex (R1) + Claude (R2) dual audit 결과 (2026-05-05)

R1 verdict: `NEEDS_REVISION` (R1-B1 + R1-I1 + R1-I2 + R1-M1).
R2 verdict: `NEEDS_REVISION` (R2-B1 + R2-B2 + R2-B3 + R2-I1 + R2-I2 + R2-I3 +
R2-I4 + R2-I5 + R2-M1 + R2-M2 + R2-M3 + R2-M4).

| Round 1 결정 ID | summary | 적용 위치 |
|---|---|---|
| RO-1 | Token gate ≤ -1000 system-only binding (28% margin sufficient — R2 G4.4 realization ≈83% 분석 적용). 이전 "65% margin" claim 정정. | §3.4 + §5.1 + §5.3 + §8.1 |
| RO-2 | v20 `## Spatial consistency` ≤ 32 lines strict (G4.4 O-2 carry) | §3.3 + §6.2 |
| RO-3 | Rule F core_principles 4-entry list 추가 (camera_frame_rule required keys 6 → 7) | §2.2 + §4 + §6.1 + §8.1 |
| RO-4 | framing_scale enum drift fix — card-only classification (close/medium/insert producer enum). wide = LLM hint only. | §1.2 + §2.2 + §2.3 + §5.1 + §9 |
| RO-5 | Korean close-framing keyword 분리 — card LLM-facing 11-entry vs canary 9-entry production-aligned | §2.2 + §5.1 + §6.1 + §10 |
| RO-6 | Cross-card reference paired-string `"see id_policy.body_part_focus_rule"` — close + wide_medium 두 곳 모두 (`body_part_focus_cross_ref`) | §2.2 + §3.3 + §6.1 + §6.2 |
| RO-7 | Override numbering — Round 1 IDs prefix `RO-N`, §12 carry IDs prefix `O-N` | §0 + §3.4 + §5.1 + §6.2 |
| RO-8 | same-character-ID detection algorithm — per-shot `{cid: [token_pos]}` dict + nearest character ID within ±20 tokens | §5.1 + §6.1 + §7.2 |
| RO-9 | `_assert_primary_framing_rule_shape()` separate further nested helper (Q9 commit) | §6.1 + §7.2 + §8.1 |
| RO-10 | Section boundary trailing-blank policy — line 142/217/268 trailing blank 모두 drop, 1 blank line 유지 | §3.2 |
| RO-11 | `_SPATIAL_BODY_PART_TRIGGERS` 별칭 도입 금지 — G4.3 `_ID_BODY_PART_TRIGGERS` alias-import 직접 재사용 | §5.4 + §7.2 |
| RO-12 | Unit test count alignment ~30-35 → ~38-43 (table 38 enumerate + 5 buffer) | §6.1 + §10 |
| RO-13 | `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` namespace footnote — G4.5c rename follow-up | §12 (O-31) |
| RO-14 | Test count baseline disambiguation — G4.4 끝 ≈2179, G4.5a 추가 후 ≈2230 expected | §6.3 |

### Round 2 — (예정, plan R3+R4 audit 시점 또는 R5+R6 fix loop 후 추가 row)

### 메타룰

- 본 spec 의 진실 source = 상단 "## Round Override" 표.
- Codex 또는 Claude 리뷰에서 BLOCKING 발견 시 즉시 상단 표에 row 추가 + 본문
  동기화.
- 절대 규칙 위반 (LLM 입력 truncation, 시나리오 의존 고유명사, 새 top-level
  field 도입) 발견 시 BLOCKING 최우선.
- silent fallback 패턴 발견 시 BLOCKING (`feedback_no_silent_fallback.md`).
- regex post-processing 의존 발견 시 IMPORTANT (`feedback_no_regex_postprocessing.md`).

---

## 12. Override Table (G4.2 + G4.3 + G4.4 carry — propagate G4.5a)

본 표는 G4.2 + G4.3 + G4.4 audit 결과 중 G4.5a 에 동일 적용해야 하는 결정사항.
추가 G4.5a-specific row 는 R1+R2 audit 에서 발견 시 §0 Round Override 표에
추가.

| ID | Source | Override | Why |
|----|-------|----------|-----|
| O-1 | G4.2 R2-B4 | builder 의 None vs [] fail-fast (`build_render_strategy()` `staging=None` + `staging_not_applicable=False` → AppError) | silent fallback ban (`feedback_no_silent_fallback.md`) |
| O-2 | G4.2 R1-I12 / G4.3 R3-B2 fixture trap | `_DEFAULT_SENTINEL = object()` fixture pattern (test 측) | fixture default vs None vs [] 구분 — production drift 차단 |
| O-3 | G4.2 R2-I7 / iter2 B1 | canary pinned-tuple seen_in_cp post-loop 검증 | false STRICT pass 차단 — empty `data.scenes` / pinning 1:1 false-pass |
| O-4 | G4.2 iter2 B2 | `t2i_variations` 누락 silent absorb 차단 — `t2i_variations=[]` / missing → measurement_failures | gate fail 누락 차단 |
| O-5 | G4.2 iter3 B3 | variation 내부 `t2i_prompt` 누락 silent absorb 차단 — `var.get("t2i_prompt") or ""` 패턴 금지 | detail_schema.json variation.required = t2i_prompt — strict |
| O-6 | G4.2 iter1 C3 / G4.3 R5-B1 / G4.4 carry | `or [] / or {}` 패턴 전반 차단 — `(d or {})` / `(card or {})` / `(pair or {}).get(...)` 모두 fail-fast (AppError) 강제 | shots / scenes 흡수 → `total_shots=0` false-pass |
| O-7 | G4.2 iter1 C2 | gate fail 시 exit 1 강제 (return 0 금지) | CI 인지 가능 |
| O-8 | G4.2 iter1 I1 | producer-side `_g41_bg_owned = ... or []` 패턴 금지 — None 흡수 차단, builder fail-fast 보장 | builder fail-fast 무력화 차단 |
| O-9 | G4.2 iter1 I2 | hash 회귀 test gap 방지 — `_card_metadata` 변경 / `render_strategy.spatial_consistency` 변경 drift test 의무 | hash drift undetected 차단 |
| O-10 | G4.2 R1-B1 | `_card_metadata` envelope-sibling top-level only — nested 구조 도입 금지. `canonicalize_render_prompt_card()` top-level pop. | hash 격리 contract |
| O-11 | G4.2 R2-I5 | `_card_metadata` free-form (extra-key 허용, schema 검증 없음). assert_card_shape envelope 7 field 만 strict. | 미래 G4.x 신규 sub-key 추가 시 validator 변경 0 |
| O-12 | G4.2 R2-I4 | `detail_steps.py` inject 직전 `_card_metadata` strip 보존 | LLM 입력 token 격리 |
| O-13 | G4.2 R1-I6 | `CARD_SCHEMA_VERSION` 유지 (= 1) — render_strategy.spatial_consistency 신규 sub-field 는 envelope shape 변경 아님. `SCENE_DETAIL_SCHEMA_VERSION` 유지 (= 7) — cp shape 변경 아님. | backward compat |
| O-14 | G4.2 R2-B6 | canary JSON `pid` 가 `pinning` block 안 (top-level redundant 제거) | pinning dict equality 한 번 |
| O-15 | G4.2 R1-M1 | token measurement = `tiktoken cl100k_base` 실측 (line × 15 estimate 는 spec 참조용) | exit criteria 실측 기준 |
| O-16 | G4.2 R1-I4 / R2-B5 | strict 0 metric 은 baseline 비0 이어도 candidate 0 강제 | baseline 비0 → P1 follow-up 별 추적 |
| O-17 | G4.2 plan-R1-I4 | unit test fixture `_make_full_card()` 가 `_DEFAULT_SENTINEL = object()` 적용 — None / [] / default 구분 | G4.1 R3-B2 fixture trap 회피 |
| O-18 | G4.2 plan-R2-I5 | canary owned/sentinel scripts 가 sentinel 합산 전 `assert_owned_sentinel_shape()` 호출 (G4.5a 의 3 metric scripts 도 sentinel 검증 시 동일 pattern) | silent 0 false-pass 차단 |
| O-19 | G4.2 plan-R2-I6 | commit target 에 `version_registry.py` (4-way sync 일부) 의무 포함 | alignment test green 유지 |
| 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 | drift 차단 |
| O-21 | G4.3 R3-I2 4-way sync | v20 push lockstep 4 곳 (prompt 디렉토리 + detail_steps.py:105 + version_registry.py:34 + version_registry.py:126) — alignment test 가 강제 검증 | 4-way drift 차단 |
| O-22 | G4.3 R3-I3 entry gate split | spec drafting / plan / Wave 1-5 = canary 무관 진행 가능. **commit gate** = G4.4 production canary 6 gate AND G4.5a self-canary 5 gate 둘 다 필수. | 작업 차단 회피 + canary 결과의 commit-gate 역할 명확화 |
| O-23 | G4.3 R3R4-B1 | `_MODULE_INFO` dict access (NOT attribute) + `prompt_dependency` 값 = `"scene_detail/v20"` (major suffix only) | alignment test silent fallback 차단 |
| O-24 | G4.3 R3R4-B3 | compact section line count 정확 (multi-line wrap trap) — bullet 1줄 강제 | token estimate 보정 |
| O-25 | G4.3 R4-B4 | token gate signed convention `token_delta = candidate - baseline`, gate `<= -<value>` | 부호 혼동 차단 |
| O-26 | G4.3 R4-M6 / R5-I3 / G4.4 carry | Wave subagent hardcoded `[]` fixture trap — 신규 sub-field 는 builder-static, `if visible_entities:` 또는 `if shot_info:` truthy guard 안 배치 절대 금지 | production force-loop trap 차단 |
| O-27 | G4.3 R5-I1 / G4.4 carry | `_card_metadata.rule_source` 키 명세 — namespace 분리되어 top-level field 와 동명 OK | drift 차단 |
| O-28 | G4.3 R5-I2 / G4.4 carry | alignment test silent fallback 제거 — `MODULE_VERSIONS.get(..., "")` / `_MODULE_INFO.get(..., {})` 금지 — 명시적 키 존재 + 타입 검증 | silent 0 false-pass 차단 |
| O-29 | G4.4 Override O-2 | compact section line count gate `wc -l` ≤ 32 strict (binding) | token estimate 보정 + LLM 가독성 |
| O-30 | G4.4 Override O-5 | strict required-keys subset (set superset 검증) | G4.5b/c forward-compat |
| O-31 | G4.4 Override O-7 | cross-card reference paired-string substring (G4.5a 에서는 G4.3 body_part_focus_rule 와의 cross-ref). **RO-13 binding (G4.5a)**: G4.5a reuses `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` directly (G4.4 carry, **not renaming** in G4.5a scope). Future cleanup: rename to `_ID_POLICY_BODY_PART_FOCUS_CROSS_REF` in G4.5c or separate cleanup PR (follow-up). | drift 차단 |
| O-32 | G4.4 Override O-8 | silent-fallback 잔존 carry follow-up (G4.5c 또는 별도 PR) | G4.5a 신규 코드 silent-fallback 도입 절대 금지 |
| O-33 | G4.4 Override O-11 | canary multi-ref filter 패턴 (G4.5a 의 multi-rule shot — camera vertical + close framing co-presence 시 metric 분리 처리) | metric overlap 차단 |
| O-34 | G4.4 Override O-12 | `compute_render_strategy_snapshot_hash` 동작 명시 | pinning hash 격리 |
| O-35 | G4.4 Override O-17 | regex view-mixing canary brittleness footnote — LLM-validator 전환 follow-up | 의미 판단 LLM-first |
| O-36 | G4.4 진단 carry | R3 Codex CLI BLOCKED 시 fast manual fallback — tighter dimension scope (8-10 file reads cap) + explicit "emit final block then STOP" 지시 | audit 단계 차단 회피 |
| O-37 | G4.4 R4-B1/B2 | Phase / Task 번호 drift trap — plan body Task 순서 vs Implementation Checklist Summary vs Override carry 불일치 즉시 patch | numbering canonical 일치 강제 |
| O-38 | G4.4 진단 carry | Plan agent 출력 truncation trap — 큰 plan (2500+ lines) 작성 시 SendMessage 회수 + 병합 또는 task 분할 | 60KB persisted-output cap 회피 |
| **G4.5a-binding** | (사용자 directive) | **새 top-level field 도입 절대 금지** — 5-field envelope contract (`render_strategy` / `id_policy` / `background_binding` / `continuity_elements_used` / `asset_requirements`) 그대로 유지. G4.5a Wave 1-A producer 변경 시 envelope 7-field 만 변경 강제 검증. R1+R2 audit BLOCKING 최우선. | 사용자 binding invariant — G4.5a/b/c 모든 단계에서 적용 |
| **G4.5a-binding** | (사용자 directive) | **Sub-shape 선택 = Option 3 (nested dict `render_strategy.spatial_consistency`)**. G4.4 `view_consistency` 패턴 carry. | render_strategy top 비대 방지 + 의미 그룹화 |

#### G4.5a-new Round 1 RO-N entries (§0 Round Override 표 reference copy)

본 sub-table 은 §0 Round Override 결정의 history reference (RO- prefix 유지 —
RO-7 binding). 진실의 source = §0 표 + §11 Round 1 표.

| RO-ID | Source | Override summary | Why |
|---|---|---|---|
| RO-1 | R1-I2 + R2-I1 | Token gate ≤ -1000 system-only binding (28% margin) | RO-1 binding |
| RO-2 | G4.4 O-2 carry | v20 `## Spatial consistency` ≤ 32 lines strict | LLM 가독성 + token estimate 보정 |
| RO-3 | R1-B1 | Rule F core_principles 4-entry list (required keys 6 → 7) | v19 line 170-175 4 핵심 원칙 carry |
| RO-4 | R1-I1 + R2-B1 | framing_scale enum 정정 — card-only classification (close/medium/insert producer enum) | producer 변경 0 in G4.5a |
| RO-5 | R2-B2 | Korean close-framing keyword 분리 — card 11-entry LLM hint vs canary 9-entry production filter | LLM reasoning vs detector 분리 |
| RO-6 | R2-B3 + O-31 carry | Cross-card reference paired-string in close + wide_medium body_part_focus_cross_ref | drift 차단 + ID-policy 연결 |
| RO-7 | R2-I2 | Override numbering — RO-N (Round 1) vs O-N (§12 carry) | 명확 disambiguation |
| RO-8 | R2-I3 | same-character-ID detection algorithm — per-shot dict + ±20 tokens | canary 정확도 |
| RO-9 | R2-I4 (Q9) | `_assert_primary_framing_rule_shape()` separate further nested helper | nested 깊이 3 levels validator |
| RO-10 | R2-I5 | Section boundary trailing-blank policy — line 142/217/268 drop | v19 convention 일관 |
| RO-11 | R2-M1 | `_SPATIAL_BODY_PART_TRIGGERS` 별칭 도입 금지 — G4.3 alias-import | drift 차단 (single source) |
| RO-12 | R1-M1 + R2-M2 | Unit test count alignment ~38-43 (table 38 + 5 buffer) | enumerate vs prose 일치 |
| RO-13 | R2-M3 | `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` rename follow-up (G4.5c) | namespace cleanup |
| RO-14 | R2-M4 | Test count baseline disambiguation — G4.4 ≈2179 → G4.5a ≈2230 | 명확 baseline |

---
