# G4.3 ID-Policy Lift Design

본 문서는 G4 spec (`docs/superpowers/specs/2026-05-04-g4-render-prompt-card-strategy.md`)
§7 G4.3 섹션의 detail sub-spec 이다. G4.2 background-binding lift (commit `14d14cb`)
의 자연 후속.

- 작성일: 2026-05-04
- 상태: initial draft
- 선행 완료: G4.1 RenderPromptCard bootstrap (5d045f3 + c656ce3 + 86473df) /
  G4.2 background-binding lift (14d14cb — `_card_metadata` envelope-sibling,
  `build_background_binding()` constraint reshape, v17 prompt with Background
  Binding compact section, 4 canary scripts, 25+10 tests)
- 목적: v17 system.md 의 4 ID-policy prose section (Rule H 포함, 약 109 lines — R1+R2 audit 정정)
  을 `id_policy` field 의 sub-field + `constraints` 로 lift, system.md 에서
  해당 long-form sections 제거 후 compact "ID Policy" reference section 으로
  교체. G4.2 token canary delta 동급 추가 감소 + composite-ID misuse rate ≤
  G4.2 baseline 검증.
  ※ R1+R2 audit 후 정정: 삭제 prose 합 정확히 **109 lines** (33+20+14+42),
    `약 ~140 lines` 표기는 stale.

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

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

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

R1 verdict: `NEEDS_REVISION` (3B + 5I + 2M). R2 verdict: `APPROVED_WITH_REVISIONS` (5B + 7I + 6M).

| ID | Source | 결정 (Override) | 적용 위치 |
|---|---|---|---|
| **R1R2-B1** | R1-B1 | v17 lines 32-64 의 face-identifiability gates (back-to-camera / silhouette / OTS-no-face / OTS-with-face exception) 가 schema 에서 누락 — **신규 sub-field `face_identifiability_rule` 추가** (§2.2). `_assert_id_policy_shape()` strict 검증 + 단위 test + Lift Mapping Table (§4) row 갱신 + canary detector 신설 보류 (해당 prose 직접 검출이 없으므로 v18 prompt 삭제 + 단위 test 만으로 lift 완료 판정). 본 sub-field 는 builder-static. | §2.2 + §2.3 producer matrix + §4 + §6.1 + §9 Glossary |
| **R1R2-B2** | R1-B2 | body-part canary regex 가 4 trigger 모두 (`focus on / close on / tight on / detail on`) match 하도록 alternation. §4 line 433 의 "close-framing shot" scope 표기는 §5.1 의 "모든 shot" 과 모순 — §4 표기를 "all shots (focus-on pattern 자체가 의도된 close-up 만 trigger)" 로 통일. 단일 module-level constant `_ID_BODY_PART_TRIGGERS = ("focus on", "close on", "tight on", "detail on")` 도입 후 canary + test 모두 import. | §4 + §5.1 + §9 + module constants |
| **R1R2-B3** | R1-B3 + R2-B5 | demographic canary contract 변경: denominator = **first-appearance per `(scene_index, shot_index, variation_index, id)` tuple** (id = bare `C##` OR composite `C##O##`, regex `r"\bC\d{2}(?:O\d{2})?\b"`). numerator = first-appearance 가 ±50 chars window 안에 ethnicity/age_band entry 1+ 결합. **sentence boundary regex 사용 금지** (LLM punctuation 가변성, `feedback_no_regex_postprocessing.md` carry). JSON metric 명: `demographic_descriptor_total_first_id_appearances` / `demographic_descriptor_present_first_appearances` / `demographic_descriptor_present_ratio`. 기존 `*_total_composite_appearances` 명 폐기. | §5.1 + §5.3 + §6 canary script |
| **R2-B1** | R2-B1 | §1.1 prelude `약 ~140 lines` 표기 → 정확한 `약 ~110 lines (33+20+14+42 = 109 lines verbatim)` 로 정정. §3.4 token 계산도 109 라인 기준 재산정. | §1.1 + §3.4 |
| **R2-B3** | R2-B3 + R1-I2 | 4 신규 sub-field 의 sub-field shape contract (`_assert_id_policy_shape()` strict scope) 명시. body_part_focus_rule.required_keys = `("trigger_phrases", "applies_to", "id_use")` / close_framing_face_phrasing.required_keys = `("forbidden_phrases", "recommended_phrasings", "rationale_summary")` / reproduction_surface_rule.required_keys = `("applies_to_surfaces", "id_use", "rationale_summary")` / demographic_descriptor_policy.required_keys = `("required_on_first_appearance", "applies_to_id_forms", "components")` (`components.required_keys = ("ethnicity", "gender", "age_band")`, role_hint_from_outfit / format_template_a/b / source_priority / scenario_dependency_ban / regional_consistency 는 optional/free-form). | §2.2 + §6.1 |
| **R2-B4** | R2-B4 | unit test `test_id_policy_*_phrases_exact` 등 ground-truth 검증 시 `assert set(returned) == EXACT_GROUND_TRUTH_SET and len(returned) == N` 강제 (substring 검증 금지). N = (body-part triggers: 4, close-framing forbidden: 6, reproduction surfaces: 9, ethnicity: 10 — Latina+Latino split 후, see R1-I1). | §6.1 |
| **R1R2-Q1** (= R1-I5 / R2-B2) | R1-I5 + R2-B2 | Q1 (v17 line 395 처분) → **결정으로 승격**: G4.3 v18 에서 line 395 `"C## 사용 금지 — 얼굴 참조 이미지가 강제 주입"` 부분을 `"id_policy.body_part_focus_rule 참조 — body-part Focus 시 C##O## 금지 (얼굴 ref 합성 차단)"` 로 1 line 인플레이스 교체. v17 line 390-407 단일 시점 섹션 자체는 G4.4 continuity scope — body 보존. integration test 가 v18 system.md 에 `"얼굴 참조 이미지가 강제 주입"` substring 부재 + `"id_policy.body_part_focus_rule"` substring 존재 검증. §10 Q1 → §3.2 결정. | §3.2 + §6.2 integration |
| **R2-I2 / Q5** | R2-I2 | Q5 (`id_policy_card_snapshot_hash` 산출법) → **결정으로 승격**: `id_policy_card_snapshot_hash = sha256(json.dumps(card["id_policy"], sort_keys=True, ensure_ascii=False))[:16]`. helper `compute_id_policy_snapshot_hash(card: dict) -> str` 신설 (`render_prompt_card.py`). canary scripts + Wave 3 모두 동일 helper 사용. §10 Q5 → §5.3 결정. | §5.2 + §5.3 |
| **R1-I1** | R1-I1 | ethnicity ground-truth count 정렬: v17 line 361 list `Asian / East Asian / South Asian / Southeast Asian / Black / Middle Eastern / Hispanic / Caucasian` (8 entry) + line 372/382 의 `Latina` / `Latina(o)` 사용 → **G4.3 ground-truth = 10 entry**: `Asian, East Asian, South Asian, Southeast Asian, Black, Middle Eastern, Hispanic, Latina, Latino, Caucasian`. §2.2 / §5.1 / §6.1 / §9 / §10 모두 10 entry 기준 통일. | §2.2 + §5.1 + §6.1 + §9 |
| **R1-I2 / R2-I6** | R1-I2 + R2-I6 | producer-side **module-level constants** 도입 (`render_prompt_card.py` top): `_ID_BODY_PART_TRIGGERS` (4) / `_ID_CLOSE_FACE_FORBIDDEN_PHRASES` (6) / `_ID_CLOSE_FACE_RECOMMENDED_PHRASINGS` (4) / `_ID_REPRODUCTION_SURFACES` (9) / `_ID_ETHNICITY_COMPONENTS` (10) / `_ID_AGE_BANDS` (6). 단위 test + canary detector 가 동일 import. 기존 `_VALID_RENDER_MODES` / `_FIXED_ELEMENT_REQUIRED_KEYS` 패턴 (render_prompt_card.py:56-60, 893-897) 답습. | §6.1 + Wave 1-A |
| **R1-I3 / R2-I3** | R1-I3 + R2-I3 | §3.3 compact section 실제 line count = 30 (예상치 25 → 정정). §3.4 token 표 갱신: `compact section = 30 lines ≈ 450 tokens` → net system.md `-1185 tokens` (`-495 -300 -210 -630 +450`) → net 합산 (system + inject) `-1065 tokens`. token gate `≥ -1050` 유지 (slack 15). G4.3 v18 draft 작성 시 30 lines 초과 금지. | §3.3 + §3.4 + §5.1 |
| **R1-I4 / R2-I5** | R1-I4 + R2-I5 | perception_mode (reflection / mirror / through_device / projection) constraint 가 reproduction_surface_rule 와 의도적 overlap (defense in depth). §8 Risk register 에 row 추가 + §6.1 `test_id_policy_perception_mode_reflection_does_not_contradict_reproduction_surface()` 추가. v18 compact section 에 "perception_mode constraint 가 mirror/projection shot 에서 우선" 명시. | §8 + §6.1 + §3.3 |
| **R2-I4** | R2-I4 | `test_id_policy_visible_entities_empty_list_valid` 강화: `assert "body_part_focus_rule" in result and "close_framing_face_phrasing" in result and "reproduction_surface_rule" in result and "demographic_descriptor_policy" in result` — 4 builder-static sub-field 가 빈 입력에서도 항상 present. | §6.1 |
| **R2-I7** | R2-I7 | alignment test 4-way sync 검증에 `MODULE_VERSIONS["scene_detail_composer"] == "1.18.0"` row 추가. (G4.2 Override O-19 carry — version_registry.py:34 의 dict 변경 누락 시 alignment fail.) | §6.4 |
| **R1-M1** | R1-M1 | close-framing forbidden 6번째 entry `"his face fills the frame"` 는 v17 prose 직접 등장 ❌ — masculine symmetry 변형 (defensive). §9 Glossary 에 `[v17-derived (symmetry)]` 라벨. canary detector 6 phrase 모두 검출 (false-positive 무관). | §9 |
| **R2-M2** | R2-M2 | hash drift test (`test_id_policy_constraints_hash_unchanged_across_perception_modes`) 가 `constraints[3]` 인덱스 의존 — 콘텐츠 selector 로 변경: `next(c for c in constraints if "face filling" in c)`. | §6.1 |
| **R2-M5** | R2-M5 | §11 vs §12 경계 명시: §11 = G4.3 R1/R2 audit findings (Round Override 동기화 history). §12 = G4.2 carry (immutable). R3+ audit 발견 사항 → §11 Round N row 추가. | §11 |
| **R2-M6** | R2-M6 | baseline `demographic_descriptor_present_ratio` 측정 안정성: composite-ID 등장 shot 5 → first-appearance 발생 횟수 최소 10 으로 변경. §5.2 baseline scene set 요건 갱신. | §5.2 |
| **R2-M3** | R2-M3 | Q4 (compact section 안 ✓/✗ 예시 inline 여부) → **결정**: 예시 미포함 (token gate 15 token slack 으로 tight). §3.3 draft 가 reasoning prose 만 carry. §10 Q4 → §3.3 결정. | §3.3 |
| **R2-M4** | R2-M4 | tiktoken 실측 timing — Wave 1-B 끝에서 v18 system.md draft 1차 실측 → §3.4 estimate 보정 → Wave 4-5 진입 전 estimate vs gate margin 재검증. | Wave 1-B |
| **R2-I1** | R2-I1 | `body_part_focus_rule.applies_to` ground-truth 문자열 `"any body part (face or non-face) when the framing is dominated by that part"` 는 §9 Glossary 에 R3-B4 row 추가 + `test_id_policy_body_part_focus_applies_to_exact()` 강제 동등 비교. | §9 + §6.1 |

### Round 2 — (예정, R3 audit 후)

Wave 1-4 dispatch 후 dual review fix loop 진행 시 추가 발견 사항 row 추가.

### 메타룰

- Round Override 표 가 §1-§9 본문보다 우선.
- 본 spec drafting 직후 Codex (R1) + Claude code-reviewer (R2) 듀얼 audit 진행.
  결정사항은 본 표에 row 추가 + 본문 inline 동기화 (G4.2 패턴 답습).
- 절대 규칙 위반 (LLM 입력 truncation, 시나리오 의존 고유명사, silent fallback)
  발견 시 BLOCKING 최우선.

---

## 1. Overview

### 1.1 Why now

G4.1 은 `RenderPromptCard.id_policy` 의 sub-shape 를 이미 빌드하고 inject 한다.
현재 `id_policy` (`render_prompt_card.py:217-275`) 가 carry 하는 field:

- `allowed_base_entity_ids` (list[str])
- `allowed_outlook_pairs` (list[dict] with character_id+outlook_id)
- `must_use_composite_character_ids` (bool — 항상 True)
- `common_noun_required_when` (list[str] — 3 entries)
- `demographic_fallback_required` (bool — 항상 True)
- `constraints` (list[str] — 3 base + 0~1 perception_mode 추가)

그러나 v17 system.md 에는 여전히 4 long-form prose section 이 있다:

- line 32-64: `## C## 사용 규칙 — 참조 이미지 연동` — C## 사용/금지 + 신체부위
  Focus 절대 금지 + 보통명사 substitution 가이드 + region-specific demographic
  examples (33 lines)
- line 66-85: `## 극단 클로즈업 표현 — 몸과 분리된 얼굴 방지` — face-filling-frame
  forbidden expressions + recommended head-and-shoulder phrasings (20 lines)
- line 133-146: `## 사진·포스터·화면·거울 속 인물 규칙` — photo/poster/screen/mirror/
  reflection 시 C##O## 금지 + reasoning + 7 ✓/✗ 예시 (14 lines)
- line 341-382: `## entity ID 인물의 demographic descriptor 명시 (Rule H) ★ 신규`
  — composite ID 첫 등장 시 demographic 1-2 token mandatory + region-specific
  examples + 판단 기준 (42 lines)

이 네 section 의 역할은 G4.1 이 card 에 deterministic 하게 넣어 주는 ID 사용
정책의 "어떻게 사용해야 하는가" 를 산문으로 풀어쓴 것이다. LLM 에게 남겨야
하는 것은 "card 의 id_policy.constraints 가 단일 출처이며 그 reminder 한
단락" 뿐이다.

G4.3 는 그 분리를 완성한다: long prose 제거 → single compact "ID Policy"
section 으로 교체. token 감소 quantified + composite-ID misuse rate ≤
baseline 검증 + close-framing forbidden-focus pattern 0 strict.

### 1.2 Non-goals (명시)

G4.3 는 다음을 **하지 않는다**:

- background-binding lift (G4.2) — 이미 완료. Rule A/C/E prose 변경 없음.
- continuity / render-strategy lift (G4.4) — fixed_elements / previous_shot_refs
  / forward_zoom_targets prose 는 건드리지 않는다.
- prompt slimming pass (G4.5) — heading count ≤ 10 enforcement 는 G4.5 까지 보류.
- Producer side 변경 — `entity_extractor` / `outlook_*` step / `chain_bg`
  loader 변경 없음. **그러나 `build_id_policy()` 내부 로직 변경은 G4.3 의
  자연 부산물로 in-scope** (R2-B2 carry from G4.2): id_policy 의 신규
  sub-field 작성 (forbidden_focus_patterns / close_framing_face_phrasings /
  reproduction_surface_rule / demographic_descriptor_policy) + 신규
  constraint strings + `_card_metadata.lift_status.rule_h_lifted` 등 신규
  rule key 추가.
- `_check_prompts()` / deterministic post-check 변경 없음.
- G3.2 owned sentinel 의 로직 변경 없음 (G4.2 와 무관).
- G4.2 `background_binding.constraints` 의 close-framing forbidden wording
  list 변경 없음 — Rule E 영역.
- scene_image_pipeline 또는 합성 로직 변경 없음.
- `id_policy.allowed_base_entity_ids` / `allowed_outlook_pairs` shape 변경
  없음 (G4.1 carry — 입력 producer 변경 X).

---

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

### 2.1 G4.1 현재 shape

`build_id_policy()` 가 반환하는 shape (`render_prompt_card.py:217-275`):

```json
{
  "allowed_base_entity_ids": ["C01", "C02", ...],
  "allowed_outlook_pairs": [{"character_id": "C01", "outlook_id": "O02"}, ...],
  "must_use_composite_character_ids": true,
  "common_noun_required_when": [
    "body-part close-up",
    "photo/poster/screen/mirror/reflection/projection depiction",
    "unregistered extra"
  ],
  "demographic_fallback_required": true,
  "constraints": [
    "visible real people with outfit binding use C##O## composite IDs",
    "do not use C##O## for a reproduced face inside a screen, photo, poster, mirror, or reflection — use a common noun instead",
    "do not use C##O## for isolated body-part focus — use a common noun (hand / eye / lip / etc.)"
  ]
}
```

`perception_mode` 가 reflection / mirror / through_device / projection 일 때
1 추가 constraint 가 들어간다 — 이 한 줄도 v17 prose 와 1:1 대응되는 reminder.

이 shape 는 ID 정책의 "policy summary" 는 carry 하지만, "어떤 wording 이
금지/권장인가" 의 구체 list 는 v17 prose 에만 존재한다. 결과 — 같은 룰의
이중 source (card constraints summary + prose detail) 가 G4.3 lift 대상.

### 2.2 G4.3 sub-shape 확정

G4.3 에서 `id_policy` 는 4 신규 sub-field + 확장된 `constraints` list 로
구성된다. envelope contract (5 semantic field) 는 변경하지 않고, debug-only
sub-field 는 `_card_metadata` 하위로 격리 (G4.2 R1-B1 carry — hash payload
에서 strip).

```json
{
  "schema_version": 1,
  "shot_key": {"scene_index": 12, "shot_index": 4},
  "render_strategy": { "...": "..." },
  "id_policy": {
    "allowed_base_entity_ids": ["C01", "C02"],
    "allowed_outlook_pairs": [{"character_id": "C01", "outlook_id": "O02"}],
    "must_use_composite_character_ids": true,
    "common_noun_required_when": [
      "body-part close-up",
      "photo/poster/screen/mirror/reflection/projection depiction",
      "unregistered extra"
    ],
    "demographic_fallback_required": true,

    "face_identifiability_rule": {
      "use_entity_id_when": [
        "face identifiable: front, profile, three-quarter, or eyes closed",
        "OTS framing with any visible face"
      ],
      "common_noun_required_when": [
        "back to camera with no visible face",
        "silhouette or blurred outline only",
        "OTS with only back of head/shoulder, face entirely hidden"
      ],
      "id_use_summary": "C##/C##O## allowed only when face is identifiable AND framing is not dominated by an isolated body part",
      "rationale_summary": "C## 사용 시 얼굴 ref 가 자동 결합 — 얼굴이 안 보이는 인물에 C## 부착하면 ref 와 본문 묘사가 충돌"
    },

    "body_part_focus_rule": {
      "trigger_phrases": ["focus on", "close on", "tight on", "detail on"],
      "applies_to": "any body part (face or non-face) when the framing is dominated by that part",
      "id_use": "forbidden — use a common noun + demographic descriptor instead",
      "common_noun_examples_generic": [
        "a trembling hand", "an exposed forearm", "the furrowed brow of a figure in shadow",
        "the clenched jaw of a figure in profile"
      ]
    },

    "close_framing_face_phrasing": {
      "forbidden_phrases": [
        "face filling the entire frame",
        "face filling nearly the entire frame",
        "her face fills the frame",
        "his face fills the frame",
        "shoulders only entering the frame",
        "head snapped sideways"
      ],
      "recommended_phrasings": [
        "tight framing on her face, upper body in frame",
        "close on his expression, shoulder visible at the edge",
        "head-and-shoulder composition, face occupying the center-right",
        "face in profile at the left, neck and upper chest visible"
      ],
      "rationale_summary": "T2I 모델은 얼굴이 프레임 전체를 가득 채우는 묘사를 받으면 몸과 분리된 얼굴만 공중에 떠 있는 합성을 낸다 — 어깨/상반신 일부를 프레임 안에 유지"
    },

    "reproduction_surface_rule": {
      "applies_to_surfaces": ["photograph", "poster", "painting", "portrait",
                              "monitor", "TV", "mirror", "window reflection",
                              "projection"],
      "id_use": "forbidden — common noun + demographic descriptor only",
      "rationale_summary": "C##O## 은 얼굴 reference 이미지를 원본 해상도로 inject 하므로 사진/화면 속 얼굴이 사진 밖 실물 크기로 합성됨"
    },

    "demographic_descriptor_policy": {
      "required_on_first_appearance": true,
      "applies_to_id_forms": ["C##", "C##O##"],
      "token_count_range": [1, 2],
      "components": {
        "ethnicity": ["Asian", "East Asian", "South Asian", "Southeast Asian",
                       "Black", "Middle Eastern", "Hispanic", "Latina",
                       "Latino", "Caucasian"],
        "gender": ["man", "woman", "figure"],
        "age_band": ["in his/her 20s", "in his/her 30s", "in his/her 40s",
                      "young", "middle-aged", "elderly"],
        "role_hint_from_outfit": ["in worker uniform", "in fisher workwear",
                                    "in detective coat", "in business suit"]
      },
      "format_template_a": "C##O## in <옷 1-3 단어>, <demographic descriptor>",
      "format_template_b": "C##O##, <demographic descriptor>, <자세 표현>",
      "source_priority": [
        "visual_world_rules.region",
        "visual_world_rules.era",
        "shot context outfit hint"
      ],
      "scenario_dependency_ban": "no work-specific proper nouns (character names, place names) — common nouns only",
      "regional_consistency": "한 작품 내 region 표기는 일관 — 선택된 세분 ethnicity (East Asian / Southeast Asian / South Asian / Caucasian / Black / Hispanic / Latina|o / Middle Eastern) 를 작품 단위 고정"
    },

    "constraints": [
      "visible real people with outfit binding use C##O## composite IDs",
      "do not use C##O## for a reproduced face inside a photograph, poster, painting, portrait, monitor, TV, mirror, window reflection, or projection — use a common noun + demographic descriptor instead",
      "do not use C##O## for isolated body-part focus (face or non-face) — when the framing is dominated by a single body part triggered by 'focus on / close on / tight on / detail on' phrasing, use a common noun (hand / eye / brow / jaw / etc.)",
      "for close-up of a face, do not use 'face filling the entire frame' / 'face filling nearly the entire frame' / 'her face fills the frame' / 'shoulders only entering the frame' / 'head snapped sideways' — keep upper body or shoulder edge visible in the frame ('tight framing on her face, upper body in frame')",
      "when introducing a C##O## (or C##) for the first time in this t2i_prompt, append a 1-2 token demographic descriptor (ethnicity + age band, or ethnicity + role hint) consistent with visual_world_rules.region — never use scenario-specific proper nouns",
      "use the format `C##O## in <옷 1-3 단어>, <demographic descriptor>` or `C##O##, <demographic descriptor>, <자세 표현>` — main characters with face refs may include this descriptor too (image inject takes precedence)"
    ]
  },
  "background_binding": { "...": "..." },
  "continuity_elements_used": { "...": "..." },
  "asset_requirements": { "...": "..." },
  "_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
    },
    "rule_source": {
      "camera_rule": "A",
      "owned_rule": "C",
      "close_skip_rule": "E",
      "demographic_rule": "H",
      "composite_id_rule": "C## 사용 규칙 — 참조 이미지 연동 (v17:32-64)",
      "close_framing_face_rule": "극단 클로즈업 표현 (v17:66-85)",
      "reproduction_surface_rule": "사진·포스터·화면·거울 속 인물 규칙 (v17:133-146)"
    }
  }
}
```

**`_card_metadata` 신규 sub-field 설명 (G4.2 R1-B1 carry — hash 제외):**

- `lift_status.rule_h_lifted` (bool, debug-only): Rule H prose 가 card 로 lift
  됐음을 version-tracking 목적으로 기록.
- `lift_status.id_policy_composite_lifted` (bool, debug-only): "C## 사용 규칙"
  prose section 이 card constraint 로 lift 완료.
- `lift_status.id_policy_close_framing_face_lifted` (bool, debug-only): "극단
  클로즈업 표현" prose 가 lift 완료.
- `lift_status.id_policy_reproduction_surface_lifted` (bool, debug-only):
  "사진·포스터·화면·거울 속 인물 규칙" prose 가 lift 완료.
- `rule_source.demographic_rule` / `composite_id_rule` /
  `close_framing_face_rule` / `reproduction_surface_rule` (str, debug-only):
  각 신규 lift 의 v17 prose source 위치 역추적.

`_card_metadata` 자체는 G4.2 에서 이미 `canonicalize_render_prompt_card()`
가 hash 입력에서 strip + `detail_steps.py` inject 직전 strip 처리 (commit
`14d14cb`). G4.3 에서는 dict key 추가만 — strip 로직 변경 0.

**id_policy `_card_metadata.lift_status` matrix (G4.3 — perception_mode 무관
모두 동일, 모든 shot 에 대해 lift 완료):**

| perception_mode | rule_h_lifted | id_policy_composite_lifted | id_policy_close_framing_face_lifted | id_policy_reproduction_surface_lifted |
|---|---|---|---|---|
| (default / unset) | true | true | true | true |
| `reflection` / `mirror` / `through_device` / `projection` | true | true | true | true |

(G4.2 의 background_binding 은 mode 별로 차등이지만, id_policy 는 perception
무관 — 모든 shot 에 동일 적용. matrix 는 single row 로 사실상 충분.)

**id_policy 신규 sub-field shape 검증 contract (R2-I5 carry from G4.2):**

`assert_card_shape()` 는 envelope contract **7 field 만 strict 검증**
유지 (`schema_version`, `shot_key`, `render_strategy`, `id_policy`,
`background_binding`, `continuity_elements_used`, `asset_requirements`).
**id_policy 내부 신규 4 sub-field (`body_part_focus_rule` /
`close_framing_face_phrasing` / `reproduction_surface_rule` /
`demographic_descriptor_policy`) 는 G4.3 strict 검증에 추가 — 누락 시
contract violation**. G4.1 의 `_assert_id_policy_shape()` (`render_prompt_card.py:844-872`)
가 base 2 field (allowed_base_entity_ids / allowed_outlook_pairs) 만 검증하므로
G4.3 에서 4 sub-field 검증 추가 필요.

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

### 2.3 Producer / consumer matrix

| field | producer | consumer (LLM) | consumer (post-parse judge) |
|---|---|---|---|
| `allowed_base_entity_ids` | `_collect_card_inputs()` → builder | C##/C##O## ID 후보 set | (없음 — V14 prompt_chars 에서 별 검증) |
| `allowed_outlook_pairs` | `_collect_card_inputs()` → builder | C##O## composite 후보 set | outfit_assignments validation |
| `body_part_focus_rule` | builder static (R3-B1) | "focus on body-part" 패턴 회피 | canary `g4_3_body_part_focus.py` (close-framing prompt 검출) |
| `close_framing_face_phrasing` | builder static (R3-B1) | face close-up 시 forbidden phrasing 회피 | canary `g4_3_close_framing_face_forbidden.py` |
| `reproduction_surface_rule` | builder static (R3-B1) | 사진/거울 속 얼굴 시 ID 회피 | canary `g4_3_reproduction_surface.py` |
| `demographic_descriptor_policy` | builder static (R3-B1) | composite ID 첫 등장 시 demographic descriptor token | canary `g4_3_demographic_descriptor_present.py` |
| `constraints` | builder per perception_mode | t2i_prompt 작성 시 constraint list 준수 | 위 4 canary 통합 |

### 2.4 정보 분리 원칙

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

| 정보 | G4.3 이후 위치 | 근거 |
|---|---|---|
| 어떤 base/composite ID 가 사용 가능한가 | card `id_policy.allowed_base_entity_ids` + `allowed_outlook_pairs` | deterministic — G4.1 이미 carry |
| body-part focus 시 trigger phrase set | card `id_policy.body_part_focus_rule.trigger_phrases` | deterministic list — card 로 |
| close-up 시 face 금지 표현 6개 | card `id_policy.close_framing_face_phrasing.forbidden_phrases` | deterministic list → card 로 |
| close-up 시 권장 head-and-shoulder 표현 4개 | card `id_policy.close_framing_face_phrasing.recommended_phrasings` | deterministic list → card 로 |
| 재현 surface (사진/거울 등) list | card `id_policy.reproduction_surface_rule.applies_to_surfaces` | deterministic list → card 로 |
| demographic descriptor 10 ethnicity options (R1-I1 carry — Latina+Latino split) | card `id_policy.demographic_descriptor_policy.components.ethnicity` | deterministic list → card 로 |
| "왜 신체부위 close-up 에 C##O## 금지인가" (얼굴 ref overlay 설명) | system.md "ID Policy" 한 단락 reminder | reasoning prose — card 로 표현 불가 |
| "왜 photo/screen 속 얼굴에 C##O## 금지인가" (실물 크기 합성 설명) | system.md "ID Policy" 한 단락 reminder | reasoning prose — card 로 표현 불가 |
| Rule H ✓/✗ 예시 7개 (region 별) | 삭제 (G4.3 prompt v18) | card constraints 가 대체; 예시는 중복 |
| "C## 사용 규칙" ✓/✗ 예시 8개 | 삭제 (G4.3 prompt v18) | card constraints 가 대체 |
| close-framing face 시 ✓/✗ 예시 8개 | 삭제 (G4.3 prompt v18) | card constraints 가 대체 |
| photo/poster/screen/mirror ✓/✗ 예시 6개 | 삭제 (G4.3 prompt v18) | card constraints 가 대체 |

---

## 3. Prompt Change Spec

### 3.1 대상 버전

- **Before**: `prompts/_base/scene_detail/17.202605042018/system.md` (v17, ~669
  lines, 4 ID-policy prose section 포함)
- **After**: `prompts/_base/scene_detail/18.<timestamp>/system.md` (v18, 4 ID
  prose sections removed, "ID Policy" compact section 1 단락 대체)

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

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

| section | lines (v17) | 내용 | 삭제 이유 |
|---|---|---|---|
| `## C## 사용 규칙 — 참조 이미지 연동` | 32-64 | C## 사용/금지 + 신체부위 Focus 절대 금지 + 보통명사 substitution + 8 ✓/✗ 예시 = 33 lines | card `body_part_focus_rule` + constraints 가 동일 데이터 carry |
| `## 극단 클로즈업 표현 — 몸과 분리된 얼굴 방지` | 66-85 | face-filling-frame 금지 6 + 권장 4 + 추가 원칙 2 + 8 examples = 20 lines | card `close_framing_face_phrasing` 가 동일 데이터 carry |
| `## 사진·포스터·화면·거울 속 인물 규칙` | 133-146 | reproduction surface 시 C##O## 금지 + reasoning + 6 ✓/✗ 예시 = 14 lines | card `reproduction_surface_rule` + constraints 가 동일 데이터 carry |
| `## entity ID 인물의 demographic descriptor 명시 (Rule H) ★ 신규` | 341-382 | composite ID 첫 등장 시 demographic mandatory + region examples + 판단 기준 = 42 lines | card `demographic_descriptor_policy` 가 동일 데이터 carry |

총 삭제 대상: approximately 109 lines (33 + 20 + 14 + 42).

> **결정 (R1+R2 audit, R1R2-Q1)**: line 390-407 의 `## 절대 규칙 — 단일 시점
> (1인칭/3인칭 혼합 금지)` 섹션은 G4.4 continuity scope — body 보존. 단
> 본문 중 ID 룰 인용 (line 395 `"C## 사용 금지 — 얼굴 참조 이미지가 강제
> 주입"`) 부분만 G4.3 v18 에서 1 line 인플레이스 교체:
>
>   `"id_policy.body_part_focus_rule 참조 — body-part Focus 시 C##O## 금지 (얼굴 ref 합성 차단)"`
>
> integration test 가 v18 system.md 에 `"얼굴 참조 이미지가 강제 주입"`
> substring 부재 + `"id_policy.body_part_focus_rule"` substring 존재 검증
> (§6.2 통합 test).

### 3.3 대체 section (v18 신규)

삭제된 4 section 자리에 다음 단일 섹션으로 교체:

```markdown
## ID Policy (C##/C##O## composite + body-part / reproduction / demographic — Rule H)

`[RenderPromptCard v1]` 의 `id_policy` field 가 이 shot 의 character ID 사용
정책을 결정한다 (card 가 primary contract — 이 섹션 prose 와 충돌 시 card 우선).

**핵심 원칙:**

- `allowed_base_entity_ids` / `allowed_outlook_pairs` 안의 ID 만 사용 가능.
  그 외 인물은 보통명사 + demographic descriptor 로 묘사.
- `body_part_focus_rule.trigger_phrases` (`focus on / close on / tight on /
  detail on` + 신체부위) 패턴이 등장하면 **얼굴·비얼굴 가리지 않고 C##O##
  사용 금지** — 공통명사 + demographic descriptor 로 대체. 이유: 얼굴 ref
  가 신체부위 위에 겹쳐 그려지거나 몸 분리 합성.
- 얼굴 close-up 시 `close_framing_face_phrasing.forbidden_phrases` 표현 금지
  (`face filling the entire frame` 류) — `recommended_phrasings`
  (head-and-shoulder 구도) 권장. 이유: T2I 모델이 frame 가득 채운 얼굴을
  몸 분리 합성으로 처리.
- `reproduction_surface_rule.applies_to_surfaces` (사진·포스터·모니터·거울·
  반사·투영 등) 안의 인물은 **C##O## 절대 금지** — 보통명사로만. 이유: 얼굴
  ref 가 원본 해상도로 inject 되어 surface 위에 실물 크기 합성.
- `demographic_descriptor_policy.required_on_first_appearance=true` 일 때
  composite/base ID 첫 등장에 ethnicity + age (또는 + role hint) 1-2 token
  부착. format: `C##O## in <옷 1-3 단어>, <demographic descriptor>` 또는
  `C##O##, <demographic descriptor>, <자세 표현>`. region 은 visual_world_rules
  기반 — 작품 고유명사 (인물 이름·지명) 사용 금지.

**충돌 해결**: 본 섹션의 prose 는 reasoning summary 이며, 구체 token list (forbidden
phrases / surfaces / ethnicity options) 는 모두 `id_policy` sub-field 에서 가져온다.
prose 가 sub-field 와 충돌하면 sub-field 우선.
```

예상 line 수: **30 lines** (R1-I3 / R2-I3 carry — draft 실측 기준 정정).
30 lines 초과 금지 — Wave 1-B draft 작성 시 line count 자체 점검 필수.

### 3.4 Token 감소 추정

> **R1-M1 carry from G4.2**: 본 표의 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**: `_card_metadata.lift_status.rule_h_lifted` 등
> 신규 dict key 가 user_prompt inject 시 +0 token 가정은 G4.2 producer-side
> 변경 (`detail_steps.py` inject 직전 strip — 14d14cb) 으로 보존. G4.3 에서는
> strip 로직 변경 0.

| 항목 | v17 | v18 (예상) | delta |
|---|---|---|---|
| C## 사용 규칙 section | 33 lines ≈ 495 tokens | 0 (삭제) | -495 |
| 극단 클로즈업 표현 section | 20 lines ≈ 300 tokens | 0 (삭제) | -300 |
| 사진/포스터/화면/거울 section | 14 lines ≈ 210 tokens | 0 (삭제) | -210 |
| Rule H demographic descriptor section | 42 lines ≈ 630 tokens | 0 (삭제) | -630 |
| ID Policy compact section (신규) | 0 | 30 lines ≈ 450 tokens | +450 |
| **net system.md** | — | — | **-1185 tokens** (추정. 실측 = canary §5.3) |
| card `id_policy` user_prompt inject | 이미 있음 (G4.1) | 동일 sub-field 추가 (+~120 tokens — 5 sub-field dict 추가, face_identifiability_rule 포함) | +120 |
| **net 합산 (system + inject)** | — | — | **-1065 tokens** (추정) |

> **R1-I3 / R2-I3 carry**: §3.3 compact section actual line count 은 30 lines
> (예상치 25 → 정정). 30 lines 초과 금지 — Wave 1-B draft 작성 시 자체 line
> count + tiktoken 실측 후 §3.4 estimate 보정 필수 (R2-M4 carry).

**Token gate margin**: §5.1 의 token delta gate 는 `≥ -1050 tokens` —
estimate `-1065` 대비 15 token slack. tiktoken 실측 variance + estimate
오차 흡수용. tighter (`≥ -1100`) 으로 가면 estimate 오차로 candidate 가
실패할 위험, looser (`≥ -1000`) 로 가면 design intent 미달 가능. R2-M4 후
Wave 1-B 가 실측 기반 보정 — 부족 시 compact section line 수 추가 단축.

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

---

## 4. Lift Mapping Table

각 v17 prose section → G4.3 card field 1:1 대응:

| Prose source (v17 lines) | v17 prose 역할 | G4.3 card field | lift 완료 판정 기준 |
|---|---|---|---|
| **C## 사용 규칙 — 참조 이미지 연동 (face identifiability gate)** (32-44, 62-64) | C## 사용 (face identifiable: front/profile/3-quarter/eyes-closed/OTS-with-face) vs 보통명사 (back-to-camera / silhouette / OTS-no-face) 판단 | `face_identifiability_rule.use_entity_id_when` + `common_noun_required_when` + `id_use_summary` (R1R2-B1 신규 sub-field) | v17 line 32-44, 62-64 prose 삭제 후 `_assert_id_policy_shape()` strict 검증 + `test_id_policy_face_identifiability_*_exact()` ground-truth 동등 비교 + integration test 가 v18 system.md 에 face-identifiability prose 부재 검증 |
| **C## 사용 규칙 — body-part Focus 절대 금지** (45-61, 64) | 신체 부위 Focus 절대 금지 + 보통명사 substitution + 8 ✓/✗ 예시 | `body_part_focus_rule.trigger_phrases` (4 trigger — `_ID_BODY_PART_TRIGGERS`) + `body_part_focus_rule.id_use="forbidden"` + constraints[2] | system.md body-part 섹션 삭제 후 canary `g4_3_body_part_focus.py` 가 **모든 shot 의** t2i_prompt 에서 4 trigger alternation regex pattern 검출 == 0 strict (close-framing scope 아님 — focus-on pattern 자체가 의도된 close-up 만 trigger) |
| **극단 클로즈업 표현 — 몸과 분리된 얼굴 방지** (66-85) | face-filling-frame forbidden + head-and-shoulder 권장 | `close_framing_face_phrasing.forbidden_phrases` (6 entries) + `recommended_phrasings` (4 entries) + constraints[3] | canary `g4_3_close_framing_face_forbidden.py` 가 forbidden 6 phrase 검출 == 0 strict |
| **사진·포스터·화면·거울 속 인물 규칙** (133-146) | reproduction surface 시 C##O## 금지 | `reproduction_surface_rule.applies_to_surfaces` (9 surfaces) + constraints[1] | canary `g4_3_reproduction_surface.py` 가 reproduction surface (photo/poster/etc.) 와 인접한 위치에서 C##O## composite 사용 검출 == 0 strict |
| **entity ID 인물의 demographic descriptor 명시 (Rule H)** (341-382) | composite ID 첫 등장 시 demographic 1-2 token mandatory | `demographic_descriptor_policy` 전체 dict (required_on_first_appearance / token_count_range / components / format_template) + constraints[4..5] | canary `g4_3_demographic_descriptor_present.py` 가 composite ID 등장한 prompt 에서 demographic descriptor (ethnicity from `components.ethnicity` list) 결합 비율 ≥ G4.2 baseline rate |

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

---

## 5. Exit Criteria + Canary Procedure

### 5.1 Exit criteria

G4.3 는 다음 **모두** 충족 시 완료 선언 가능:

| criterion | 측정 방법 (R1-I3 deterministic carry from G4.2) | threshold |
|---|---|---|
| C## body-part focus prose 삭제 후 forbidden pattern 0 | canary script `scripts/canary/g4_3_body_part_focus.py` 가 **모든 shot 의 `t2i_variations[].t2i_prompt`** scan — module-level constant `_ID_BODY_PART_TRIGGERS = ("focus on", "close on", "tight on", "detail on")` import + regex `r"\b(?:" + "\|".join(map(re.escape, _ID_BODY_PART_TRIGGERS)) + r")\s+C\d{2}(?:O\d{2})?'s\s+\w+"` (IGNORECASE) 4 trigger alternation 검출 count | **candidate count == 0 strict** (≤ baseline 아님 — Rule 위반은 0 강제) |
| close-framing face forbidden phrasing 0 | canary script `scripts/canary/g4_3_close_framing_face_forbidden.py` 가 close-framing shot t2i_prompt 에서 module-level constant `_ID_CLOSE_FACE_FORBIDDEN_PHRASES` (6 entry — §9 Glossary single source) 검출 count | **candidate count == 0 strict** |
| reproduction surface 인물에 C##O## 미사용 | canary script `scripts/canary/g4_3_reproduction_surface.py` 가 t2i_prompt 에서 module-level constant `_ID_REPRODUCTION_SURFACES` (9 entry — `photograph` / `poster` / `painting` / `portrait` / `monitor` / `TV` / `mirror` / `window reflection` / `projection`) 와 동일 ±50 chars window 안의 `C##O##` composite 사용 검출 count (sentence boundary regex 사용 금지 — `feedback_no_regex_postprocessing.md` carry) | **candidate count == 0 strict** |
| demographic descriptor present rate ≥ baseline | canary script `scripts/canary/g4_3_demographic_descriptor_present.py` 가 t2i_prompt 에서 ID regex `r"\bC\d{2}(?:O\d{2})?\b"` (bare C## OR composite C##O## 모두) 의 **first-appearance per `(scene_index, shot_index, variation_index, id_token)` tuple** 검출 — 분모 = total first-appearance count. 분자 = first-appearance 가 ±50 chars window 안에 module-level `_ID_ETHNICITY_COMPONENTS` (10 entry) OR `_ID_AGE_BANDS` (6 entry) 1+ 결합. ratio = numerator / denominator. metric 명: `demographic_descriptor_total_first_id_appearances` / `demographic_descriptor_present_first_appearances` / `demographic_descriptor_present_ratio` | candidate ratio ≥ baseline ratio (degradation 차단) |
| system.md token delta (tiktoken cl100k_base 실측) | canary script `scripts/canary/g4_3_token_count.py` 가 v18 system.md 실측 vs v17 baseline 실측 | **net delta ≤ -1050 tokens** (estimate -1065 대비 15 token slack) |
| pytest G4.3 test suite green | `backend/tests/unit/test_g4_3_*.py` + `backend/tests/integration/test_g4_3_*.py` all pass | 0 failures, 0 regressions |
| 기존 regression suite green | `pytest backend/tests/` | count ≥ G4.2 baseline |

> **strict 0 metric (3종)**: G4.3 의 body-part focus / close-framing face /
> reproduction surface 위반 표현은 baseline 비0 이어도 candidate 0 강제
> (G4.2 R1-I4 / R2-B5 strict 패턴 carry). baseline 비0 이면 baseline 도 별
> production fix 대상으로 마킹 (G4.3 scope 밖, P1 follow-up).

> **R2-B4 carry from G4.2 — metric scope**: 본 4 canary 의 측정 대상 shot
> subset 은 명확히 분리:
> - `g4_3_body_part_focus.py`: 모든 shot (close-framing 포함). focus-on
>   pattern 자체가 의도된 close-up 만 trigger 하므로 framing 무관 scan.
> - `g4_3_close_framing_face_forbidden.py`: **close-framing shot 만**.
>   non-close 는 face-filling-frame phrasing 자체 자연 미발생.
> - `g4_3_reproduction_surface.py`: **모든 shot**. surface keyword 가
>   non-close 에도 등장 가능.
> - `g4_3_demographic_descriptor_present.py`: composite ID 등장한 모든 shot.
>   composite ID 미등장 shot 은 ratio 산출 분모에서 제외.

G4.3 Exit criteria 가 충족되어야 G4.4 (continuity lift) 진입 가능.

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

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

| pinning field | 값 결정 | 비교 시 enforce |
|---|---|---|
| `pid` | 단일 PID (G4.2 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.2 = `14d14cb`) | baseline / candidate 모두 명시 |
| `id_policy_card_snapshot_hash` | id_policy field hash (loader 입력 변동 차단). G4.3 추가 — `compute_card_hash()` 의 id_policy partial canonicalize | 동일 |

**Baseline scene set 요건**:
- composite ID (C##O##) 가 1+ 등장하는 shot 최소 5개.
- 그 중 close-framing shot 최소 2개 (face-filling-frame 검증 mandatory).
- 그 중 reproduction surface keyword (photograph / TV / mirror 등) 등장 shot
  최소 1개 (reproduction_surface 검증 mandatory).
- 모두 `scene_index_list` / `shot_index_list_per_scene` 에 명시.

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

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

### 5.3 Canary 절차 (G4.2 §5.3 carry)

G4.3 canary 는 4 ID-policy prose 삭제를 광범위 적용 전 1 PID / 1 selected
segment 로 검증한다:

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

JSON schema (R1-I3 + R1-I5 pinning + R2-B6 pid 위치 carry from G4.2 + G4.3
4 metric):
```json
{
  "timestamp": "ISO8601",
  "prompt_version": "17.202605042018",
  "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": "14d14cb",
    "id_policy_card_snapshot_hash": "..."
  },
  "scene_set": [{"scene_index": N, "shot_index": M, "is_close_framing": bool, "has_composite_id": bool, "has_reproduction_surface": bool}],
  "metrics": {
    "total_shots": N,
    "close_framing_shots": N,
    "shots_with_composite_id": N,
    "shots_with_reproduction_surface": N,
    "body_part_focus_pattern_count": N,
    "body_part_focus_per_shot": {},
    "close_framing_face_forbidden_count": N,
    "close_framing_face_forbidden_per_shot": {},
    "reproduction_surface_composite_id_count": N,
    "reproduction_surface_composite_id_per_shot": {},
    "demographic_descriptor_present_first_appearances": N,
    "demographic_descriptor_total_first_id_appearances": N,
    "demographic_descriptor_present_ratio": 0.0,
    "system_prompt_token_count_tiktoken_cl100k": N
  },
  "measurement_scripts": {
    "body_part_focus": "scripts/canary/g4_3_body_part_focus.py",
    "close_framing_face_forbidden": "scripts/canary/g4_3_close_framing_face_forbidden.py (close framing shots only)",
    "reproduction_surface": "scripts/canary/g4_3_reproduction_surface.py",
    "demographic_descriptor_present": "scripts/canary/g4_3_demographic_descriptor_present.py (composite ID shots only)",
    "token_count": "scripts/canary/g4_3_token_count.py (tiktoken cl100k_base)"
  }
}
```

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

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

**Step 3 — Comparison**:

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

- `body_part_focus_pattern_count`: **candidate == 0 strict**
- `close_framing_face_forbidden_count`: **candidate == 0 strict**
- `reproduction_surface_composite_id_count`: **candidate == 0 strict**
- `demographic_descriptor_present_ratio`: candidate ≥ baseline (degradation
  차단)
- `system_prompt_token_count_tiktoken_cl100k`: baseline - candidate ≥ **1050**
  (실측 token reduction gate)

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

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

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

- **CP shape**: `scene["render_prompt_card"]["id_policy"]` (NOT 평면
  `scene["id_policy"]`). G4.2 함정 carry — `detail_steps.py:2124` authoritative.
- **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.

---

## 6. Test Scope

### 6.1 Unit tests

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

| test | 설명 | assert |
|---|---|---|
| `test_id_policy_has_body_part_focus_rule` | `build_id_policy()` 반환에 `body_part_focus_rule` dict 존재 + `trigger_phrases` 4 entries (`focus on` / `close on` / `tight on` / `detail on`) + `id_use="forbidden"` | dict + str + list-len assert |
| `test_id_policy_has_close_framing_face_phrasing` | 반환에 `close_framing_face_phrasing` dict + `forbidden_phrases` 6 entries + `recommended_phrasings` 4 entries | list-len + content assert |
| `test_id_policy_close_framing_face_forbidden_phrases_exact` | `forbidden_phrases` 가 정확히 6개 ground-truth: `"face filling the entire frame"` / `"face filling nearly the entire frame"` / `"her face fills the frame"` / `"his face fills the frame"` / `"shoulders only entering the frame"` / `"head snapped sideways"` | 6 substring assert |
| `test_id_policy_close_framing_face_recommended_phrasings_exact` | `recommended_phrasings` 가 정확히 4개 ground-truth (head-and-shoulder 4 forms) | 4 substring assert |
| `test_id_policy_has_reproduction_surface_rule` | 반환에 `reproduction_surface_rule` dict + `applies_to_surfaces` 9 entries + `id_use="forbidden"` | dict + list-len assert |
| `test_id_policy_reproduction_surface_list_exact` | `applies_to_surfaces` 가 정확히 9: `photograph` / `poster` / `painting` / `portrait` / `monitor` / `TV` / `mirror` / `window reflection` / `projection` | 9 substring assert |
| `test_id_policy_has_demographic_descriptor_policy` | 반환에 `demographic_descriptor_policy` dict + `required_on_first_appearance=true` + `token_count_range=[1,2]` + `applies_to_id_forms=["C##","C##O##"]` | dict + bool + tuple + list assert |
| `test_id_policy_demographic_components_ethnicity_list` | `demographic_descriptor_policy.components.ethnicity` 가 9+ entries (East Asian / South Asian / Southeast Asian / Black / Middle Eastern / Hispanic / Latina / Latino / Caucasian) | list contains assert |
| `test_id_policy_demographic_components_gender_age_role` | `gender` (3) / `age_band` (6) / `role_hint_from_outfit` (4) entries 검증 | list-len + content assert |
| `test_id_policy_demographic_format_templates` | `format_template_a` + `format_template_b` 가 명세된 두 format string | str equality |
| `test_id_policy_demographic_no_scenario_proper_nouns` | `scenario_dependency_ban` field 가 "no work-specific proper nouns" 메시지 포함 + ethnicity entries 에 시나리오 고유명사 없음 | substring + list scan |
| `test_id_policy_constraints_grew_to_six_strings` | G4.1 base 3 strings → G4.3 6 strings (composite IDs / reproduction surface / body-part focus / close-framing face / demographic first-appearance / format template) | len(constraints) == 6 |
| `test_id_policy_constraints_string_1_composite_id_use` | constraints[0] 에 `"C##O## composite IDs"` substring | substring assert |
| `test_id_policy_constraints_string_2_reproduction_surface_listed` | constraints[1] 에 9 surface keyword 모두 inline 또는 enumerate | substring assert |
| `test_id_policy_constraints_string_3_body_part_focus_trigger` | constraints[2] 에 `"focus on / close on / tight on / detail on"` trigger phrase 명시 | substring assert |
| `test_id_policy_constraints_string_4_close_framing_face_forbidden_listed` | constraints[3] 에 6 forbidden phrasing 모두 inline 또는 enumerate | 6 substring assert |
| `test_id_policy_constraints_string_5_demographic_first_appearance` | constraints[4] 에 `"first time"` + `"1-2 token demographic descriptor"` substring | substring assert |
| `test_id_policy_constraints_string_6_format_template` | constraints[5] 에 `"in <옷"` 또는 `"<demographic descriptor>"` placeholder 명시 | substring assert |
| `test_id_policy_perception_mode_reflection_adds_constraint` | `perception_mode="reflection"` → constraints 가 base 6 + 1 reflection (G4.1 carry) = 7 | len(constraints) == 7 |
| `test_id_policy_perception_mode_default_no_extra` | `perception_mode=None` → 6 constraints | len(constraints) == 6 |
| `test_id_policy_visible_entities_none_raises` | `visible_entities=None` → AppError (G4.1/G4.2 silent-absorb ban carry) | pytest.raises |
| `test_id_policy_outlook_pairs_none_raises` | `outlook_pairs=None` → AppError | pytest.raises |
| `test_id_policy_visible_entities_empty_list_valid` | `visible_entities=[]` → return shape OK (no raise) | shape valid |
| `test_card_metadata_lift_status_includes_rule_h_and_id_keys` | `build_render_prompt_card()` 반환의 `_card_metadata.lift_status` 에 `rule_h_lifted=true` + `id_policy_composite_lifted=true` + `id_policy_close_framing_face_lifted=true` + `id_policy_reproduction_surface_lifted=true` 모두 존재 | 4 key assert |
| `test_card_metadata_rule_source_includes_demographic_and_composite_keys` | `_card_metadata.rule_source` 에 `demographic_rule="H"` + `composite_id_rule` / `close_framing_face_rule` / `reproduction_surface_rule` 4 key 존재 | 4 key assert |
| `test_card_metadata_excluded_from_hash_after_g4_3_keys_added` | G4.2 R1-I11 carry — 신규 4 lift_status key 추가에도 hash 변경 0 (`_card_metadata` strip) | hash_before == hash_after |
| `test_card_hash_drifts_on_id_policy_constraint_change` | `id_policy.constraints[3]` 문자열 1 char 변경 → hash drift | hash_A != hash_B |
| `test_card_hash_stable_on_id_policy_lift_status_only_change` | `_card_metadata.lift_status.rule_h_lifted` 만 toggle → hash 동일 | hash_A == hash_B |
| `test_assert_card_shape_validates_body_part_focus_rule_shape` | `_assert_id_policy_shape()` 가 `body_part_focus_rule` 누락 시 AppError | pytest.raises |
| `test_assert_card_shape_validates_close_framing_face_phrasing_shape` | `_assert_id_policy_shape()` 가 `close_framing_face_phrasing` 누락 시 AppError | pytest.raises |
| `test_assert_card_shape_validates_reproduction_surface_rule_shape` | `_assert_id_policy_shape()` 가 `reproduction_surface_rule` 누락 시 AppError | pytest.raises |
| `test_assert_card_shape_validates_demographic_descriptor_policy_shape` | `_assert_id_policy_shape()` 가 `demographic_descriptor_policy` 누락 시 AppError | pytest.raises |

총 unit tests: **31** (G4.2 25 carry pattern 위에 +6 sub-field shape +
demographic 분기 검증).

### 6.2 Integration tests

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

| test | 설명 | assert |
|---|---|---|
| `test_v18_system_prompt_missing_c_use_section` | v18 system.md 를 load 후 `"## C## 사용 규칙 — 참조 이미지 연동"` heading 없음 | heading not present |
| `test_v18_system_prompt_missing_close_face_section` | v18 system.md 에 `"## 극단 클로즈업 표현"` heading 없음 | heading not present |
| `test_v18_system_prompt_missing_reproduction_surface_section` | v18 system.md 에 `"## 사진·포스터·화면·거울 속 인물 규칙"` heading 없음 | heading not present |
| `test_v18_system_prompt_missing_rule_h_section` | v18 system.md 에 `"## entity ID 인물의 demographic descriptor"` heading 없음 | heading not present |
| `test_v18_system_prompt_has_id_policy_section` | v18 system.md 에 `"## ID Policy"` heading 존재 | heading present |
| `test_v18_prompt_token_reduction_vs_v17` | v18 system.md 줄 수 < v17 줄 수 (최소 80 lines 감소) | len(v18_lines) < len(v17_lines) - 80 |
| `test_composite_id_shot_card_carries_demographic_policy` | `_collect_card_inputs()` + `build_render_prompt_card()` + inject 경로 통해 user_prompt 에 `id_policy.demographic_descriptor_policy` dict 포함 확인 | JSON parse → dict valid |
| `test_close_framing_shot_card_carries_face_phrasing_rules` | close-framing path → user_prompt 에 `id_policy.close_framing_face_phrasing.forbidden_phrases` 6 entry 모두 포함 | 6 substring in injected JSON |
| `test_reproduction_surface_shot_card_lists_9_surfaces` | 모든 shot path → user_prompt 에 `id_policy.reproduction_surface_rule.applies_to_surfaces` 9 entry 모두 포함 | 9 substring in injected JSON |
| `test_card_hash_drift_on_id_policy_constraint_change_in_verify` | id_policy.constraints 변경 후 `verify_completion()` 실행 → hash drift detected → status partial | verify marks partial |
| `test_v17_checkpoint_resume_on_v18_escalates_force` | v17 cp (schema_version=7) 를 v18 consumer 로 resume → `_config_hash` mismatch → `mode="force"` escalate. G4.1 의 `test_g4_1_card_consumer_wiring.py::test_step_runner_actually_escalates_to_force_on_v15_resume` 와 동일 escalate path — G4.3 v18 prompt 가 동일하게 작동 | step_runner.escalate observed |
| `test_g3_2_sentinel_coexists_with_g4_3_id_policy` | 임의 shot → card `id_policy` 신규 sub-field + variation-level `owned_validation` sentinel + `background_binding.constraints` 셋 모두 CP 에 존재 | three present in CP dict |

총 integration tests: **12**.

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

### 6.3 Regression tests

G4.3 구현 후 반드시 실행:

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

기준: G4.2 baseline 대비 0 regressions. G4.3 test 추가 후 total count 증가만
허용. 예상 baseline = 2025 + 35 (G4.2) + 43 (G4.3 31 unit + 12 integration) =
약 2103+ pass.

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

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

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

- `test_scene_detail_prompt_version_constant_matches_latest` — `SCENE_DETAIL_PROMPT_VERSION`
  가 v18 latest 와 일치 (G4.3 가 v17 → v18 bump).
- `test_scene_detail_schema_version_synced_with_manifest` — `SCENE_DETAIL_SCHEMA_VERSION`
  유지 (= 7) — id_policy 신규 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/v18` 로 갱신.

> **4-way sync constraint**: v18 push 시 다음 4 곳 lockstep 갱신 필수:
> 1. `prompts/_base/scene_detail/18.<timestamp>/system.md` 디렉토리 생성
> 2. `backend/app/core/steps/detail_steps.py:105` —
>    `SCENE_DETAIL_PROMPT_VERSION = "18.<timestamp>"`
> 3. `backend/app/core/version_registry.py:34` —
>    `MODULE_VERSIONS["scene_detail_composer"]` minor bump (1.17.0 → 1.18.0)
> 4. `backend/app/core/version_registry.py:126` —
>    `_MODULE_INFO["scene_detail_composer"].prompt_dependency = "scene_detail/v18"`

---

## 7. G4.x Sequencing

### 7.1 G4.3 entry condition (= G4.2 exit criteria)

G4.2 exit criteria (G4.2 spec §5.1, R2-B5):
- G4.2 owned violation rate ≤ G4.1 baseline (canary 측정)
- G4.2 close framing forbidden wording count == 0 strict (canary 측정)
- G4.2 unit + integration test green ✅ (이미 만족 — commit `14d14cb`)
- v17 prompt push ✅ (이미 만족)

> **R3-Entry-Note**: Phase 6 production canary 미실행 시에도 G4.3 spec
> drafting / R1+R2 audit / plan drafting / Wave 1-4 구현 진행 가능. 단,
> **G4.3 commit gate 는 Phase 6 G4.2 production canary 통과 필수**. canary
> 결과는 G4.3 v18 push 직전 검증 — token + pytest gate 는 G4.2 commit 시점에
> 이미 dry-run 통과 (token canary delta -1454 < -850 ✓), 다른 4 gate (camera
> wording / close forbidden STRICT / owned violations / pytest) 는 production
> CP 데이터 필요.

### 7.2 G4.3 작업 순서

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

1. `build_id_policy()` 에 G4.3 신규 4 sub-field 작성 로직 추가 (R2-B2 in-scope
   carry — `_card_metadata` 작성 / 신규 constraint strings):
   - `body_part_focus_rule` dict (trigger_phrases / applies_to / id_use /
     common_noun_examples_generic)
   - `close_framing_face_phrasing` dict (forbidden_phrases 6 / recommended_phrasings
     4 / rationale_summary)
   - `reproduction_surface_rule` dict (applies_to_surfaces 9 / id_use /
     rationale_summary)
   - `demographic_descriptor_policy` dict (required_on_first_appearance /
     applies_to_id_forms / token_count_range / components / format_template_a/b
     / source_priority / scenario_dependency_ban / regional_consistency)
2. `id_policy.constraints` G4.1 base 3 → G4.3 6 (1 composite IDs + 1
   reproduction surface enumerated + 1 body-part focus trigger + 1 close-framing
   face forbidden enumerated + 1 demographic first-appearance + 1 format
   template). perception_mode reflection branch 는 G4.1 carry — base 6 위에
   +1 (총 7).
3. `_assert_id_policy_shape()` (`render_prompt_card.py:844-872`) 에 4 신규
   sub-field strict 검증 추가. 누락 시 AppError. G4.3 R2-I5 carry — `_card_metadata`
   는 free-form.
4. `build_render_prompt_card()` wrapper 에서 `_card_metadata.lift_status` 에
   4 신규 key (`rule_h_lifted` / `id_policy_composite_lifted` /
   `id_policy_close_framing_face_lifted` / `id_policy_reproduction_surface_lifted`)
   추가 + `rule_source` 에 4 신규 key (`demographic_rule="H"` /
   `composite_id_rule` / `close_framing_face_rule` /
   `reproduction_surface_rule`) 추가. 모든 mode 에서 `True` (id_policy 는
   perception 무관 동일 적용).

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

5. `prompts/_base/scene_detail/18.<timestamp>/` 디렉토리 생성:
   - `system.md` = v17 system.md - 109 lines [4 ID prose] + 25-line "ID Policy"
     compact section (§3.3).
   - `detail_schema.json` = v17 verbatim copy (cp shape 변경 없음).
   - `user.md` = v17 verbatim copy (변경 없음).
6. v17 sealed (덮어쓰기 금지 — `feedback_prompt_versioning.md`).

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

7. `detail_steps.py:105` —
   `SCENE_DETAIL_PROMPT_VERSION = "17.202605042018"` →
   `"18.<timestamp>"`. SCENE_DETAIL_SCHEMA_VERSION 유지 (= 7).
8. `detail_steps.py` inject 직전 `_card_metadata` strip 로직 변경 0 (G4.2
   `14d14cb` 그대로 유지) — id_policy 의 본 sub-field 는 inject 대상 (LLM 이
   참조). `_card_metadata` 만 strip.
9. `version_registry.py:34` — `MODULE_VERSIONS["scene_detail_composer"]`
   `1.17.0` → `1.18.0`.
10. `version_registry.py:126` —
    `_MODULE_INFO["scene_detail_composer"].prompt_dependency =
    "scene_detail/v18"`. `updated_at = "2026-05-04"` 또는 신규 timestamp.
11. alignment test (`backend/tests/prompts/test_scene_detail_id_policy_alignment.py`)
    green 유지 검증.

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

12. `scripts/canary/g4_3_body_part_focus.py` — 모든 shot t2i_prompt scan,
    `r"focus on\s+C\d{2}(O\d{2})?'s\s+\w+"` + `r"close on\s+C\d{2}(O\d{2})?'s\s+\w+"`
    검출. **G4.2 silent-absorb 차단 패턴 carry** (CP shape /
    measurement_failures / pinned tuple seen_in_cp post-loop).
13. `scripts/canary/g4_3_close_framing_face_forbidden.py` — close-framing
    shot 만 scan, 6 forbidden phrasing 검출.
14. `scripts/canary/g4_3_reproduction_surface.py` — 모든 shot scan, 9 surface
    keyword 와 same sentence/within-50-chars 안 C##O## composite 검출.
15. `scripts/canary/g4_3_demographic_descriptor_present.py` — composite ID
    등장 shot 만 scan, 10 ethnicity entry 중 1+ ±50 chars window
    결합 비율 산출.
16. `scripts/canary/g4_3_token_count.py` — `tiktoken cl100k_base` 로 v18
    system.md 실측. CLI args = pinning 7 fields (G4.2 plan-R2-I4 carry).

**Wave 4: tests (Phase 5 in plan)**

17. `backend/tests/unit/test_g4_3_id_policy_lift.py` — 31 unit tests (§6.1).
18. `backend/tests/integration/test_g4_3_id_policy_integration.py` — 12
    integration tests (§6.2).
19. fixture pattern: `_DEFAULT_SENTINEL = object()` carry (G4.1 R3-B2 / G4.2
    plan-R1-I4 carry — None vs [] vs default 구분).

**Wave 5: production canary execution (사용자 실행)**

20. Baseline canary capture (v17) — pinning 7 field 명시.
21. Candidate canary capture (v18) — 동일 pinning.
22. 4 metric + token + pytest = 6 gate 판정.

**Wave 6: commit**

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

### 7.3 G4.4 entry condition (= G4.3 exit criteria)

G4.4 (continuity lift) 는 다음 모두 충족 후 진입:

- G4.3 4 metric STRICT (body-part focus / close-framing face / reproduction
  surface / demographic descriptor) 통과
- G4.3 token delta gate `≥ -1050` 통과
- G4.3 unit + integration test suite green
- v18 prompt push + 4-way sync 완료

G4.4 가 다루는 scope: continuity lift (fixed_elements / previous_shot_refs /
forward_zoom_targets prose).

---

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

| G4.2 Exit gate | 현재 상태 (2026-05-04) |
|---|---|
| owned violation rate ≤ G4.1 baseline | canary measurement pending (Phase 6 production 사용자 실행) |
| close framing forbidden wording == 0 STRICT | canary measurement pending |
| unit + integration green | ✅ (commit `14d14cb`, 289 passed) |
| token delta ≤ -850 | ✅ dry-run -1454 |
| v17 prompt push | ✅ (`prompts/_base/scene_detail/17.202605042018/`) |
| 4-way sync (registry + alignment test) | ✅ (G4.2 Wave 2 complete) |

> **G4.3 spec drafting 진행 가능** — production canary 결과는 G4.3 commit
> gate 로만 필요 (spec / plan / Wave 1-4 작성은 무관).

---

## 8. Risk Register

### 8.1 G4.3-unique risks

| risk | mitigation | measurable threshold |
|---|---|---|
| v18 compact section 이 demographic descriptor 권장 token 가이드를 충분히 carry 하지 않아 LLM 이 default-race 인물 생성 | constraints[4..5] + `demographic_descriptor_policy.format_template_a/b` 명시 + canary `demographic_descriptor_present_ratio` ≥ baseline | candidate ratio ≥ baseline rate |
| 신체부위 focus prose 삭제 후 LLM 이 "focus on C##O##'s hand" 류 출력 | card `body_part_focus_rule.id_use="forbidden"` + constraints[2] + canary STRICT 0 | candidate count == 0 |
| close-framing face forbidden phrasing 6개가 card constraints 에 완전히 cover 되지 않음 | §2.2 `close_framing_face_phrasing.forbidden_phrases` 6 entry 명시 + unit test 6 phrase 검증 | all 6 phrases present in card — unit test green |
| reproduction surface 9 keyword 가 card list 에 누락 시 LLM 이 "C##O## on the photograph" 출력 | §2.2 `reproduction_surface_rule.applies_to_surfaces` 9 entry 명시 + unit test 9 keyword 검증 | all 9 surfaces present in card |
| `_card_metadata.lift_status` 신규 4 key 가 hash 에 의도치 않게 포함되어 hash 가 매 build 마다 변경 | G4.2 R1-B1 carry: `_card_metadata` 전체 가 `canonicalize_render_prompt_card()` 에서 strip. unit test `test_card_metadata_excluded_from_hash_after_g4_3_keys_added` 가 강제 | `_card_metadata` not in canonicalized output (post G4.3 key 추가) |
| canary PID 의 composite ID 등장 shot 이 부족하면 demographic_descriptor metric 분모 0 | baseline / candidate scene set 선정 시 composite ID 1+ 등장 shot 최소 5개 + close-framing shot 최소 2개 + reproduction surface keyword shot 최소 1개 보장 (§5.2) | scene_set in canary JSON ≥ 5 composite-ID shots |
| canary baseline body-part focus / close-framing face / reproduction surface count 가 비0 인 경우 | strict: candidate == 0 강제. baseline 비0 이면 P1 follow-up 으로 별 추적 (G4.3 scope 밖이지만 production 결함 시그널) | candidate count == 0 mandatory; baseline 비0 면 별 issue 등록 |
| Wave subagent fixture 가 신규 4 sub-field 누락한 partial card 사용 → production drift cascade | `_DEFAULT_SENTINEL = object()` fixture pattern + `_make_full_card()` helper 가 항상 4 신규 sub-field 채움 + `_assert_id_policy_shape()` strict (G4.3 R2-I5 carry) | unit test `test_assert_card_shape_validates_*_shape` 4종 |

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

| inherited risk | carry source | G4.3 enforcement |
|---|---|---|
| v18 로 resume 시 old v17 cp 가 자동 force escalate | G4.1 R1-B1 (`_config_hash` mismatch 검증) | §6.2 `test_v17_checkpoint_resume_on_v18_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.3 변경 없음 |
| schema_version != 7 result 에서 card check bypass warning step 당 1회 emit | G4.1 cleanup I5 (commit c656ce3) | G4.3 변경 없음 — 동일 동작 |
| `or [] / or {}` silent absorb pattern 차단 | G4.2 iter1 C3 (commit `14d14cb` cycle) | G4.3 canary scripts 5종 모두 동일 pattern carry |
| pinned tuple seen_in_cp post-loop validation | G4.2 iter2 B1 | G4.3 canary scripts 5종 모두 동일 pattern carry |
| t2i_variations + per-variation t2i_prompt validation | G4.2 iter2 B2 + iter3 B3 | G4.3 4 metric scripts 모두 검증 |
| Wave subagent hardcoded empty trap | G4.1 + G4.2 carry | G4.3 spec §7 wave decomposition 에서 fixture default 사용 시 `_DEFAULT_SENTINEL` 의무화 |

---

## 9. Glossary

| 용어 | 정의 |
|---|---|
| `id_policy` | `RenderPromptCard` 의 5 semantic field 중 하나. character ID (C##/C##O##) 사용 정책을 deterministic 하게 기술. |
| Composite ID rule (v17:32-64) | `scene_detail v17 system.md` C## 사용 규칙 — 얼굴이 식별 가능하면 C##/C##O## 사용, 신체부위 close-up / 뒷모습 / 실루엣 / OTS-no-face 시 보통명사. |
| Body-part focus rule | "Focus on C##O##'s <body-part>" 패턴 절대 금지. trigger phrases: `focus on / close on / tight on / detail on`. 얼굴·비얼굴 가리지 않고 보통명사 + demographic descriptor 로 대체. |
| Close-framing face phrasing rule (v17:66-85) | `face filling the entire frame` 류 6 forbidden phrasing — 어깨/상반신 일부를 프레임 안 유지하는 head-and-shoulder 표현 권장. |
| Reproduction surface rule (v17:133-146) | photograph / poster / painting / portrait / monitor / TV / mirror / window reflection / projection 안의 인물에 C##O## 절대 금지. 얼굴 ref 가 surface 위에 실물 크기 합성 방지. |
| Rule H (v17:341-382) | composite ID (`C##` / `C##O##`) 첫 등장 시 demographic descriptor (ethnicity + age band 또는 + role hint) 1-2 token 부착 mandatory. region 은 visual_world_rules 기반. |
| `body_part_focus_rule` | id_policy sub-field. trigger_phrases / applies_to / id_use / common_noun_examples_generic 4 key dict. |
| `close_framing_face_phrasing` | id_policy sub-field. forbidden_phrases (6) + recommended_phrasings (4) + rationale_summary 3 key dict. |
| `reproduction_surface_rule` | id_policy sub-field. applies_to_surfaces (9) + id_use + rationale_summary 3 key dict. |
| `demographic_descriptor_policy` | id_policy sub-field. required_on_first_appearance / applies_to_id_forms / token_count_range / components / format_template_a/b / source_priority / scenario_dependency_ban / regional_consistency 8+ key dict. |
| `id_policy_card_snapshot_hash` | id_policy field 의 partial canonicalize hash. canary pinning 7 field 중 1. baseline / candidate id_policy shape 동일 검증. |
| forbidden phrasing **(R3-B1 ground-truth source)** | close-framing face shot 의 t2i_prompt 에 등장하면 안 되는 6 ground-truth phrase. **본 정의가 single source — §2.2 / §3.3 / §6.1 unit test / canary detector script 모두 본 list 를 reference**. 6 phrases: (1) `"face filling the entire frame"`, (2) `"face filling nearly the entire frame"`, (3) `"her face fills the frame"`, (4) `"his face fills the frame"`, (5) `"shoulders only entering the frame"`, (6) `"head snapped sideways"`. |
| reproduction surface keywords **(R3-B2 ground-truth source)** | reproduction surface rule 적용 대상 9 surface keyword. 본 정의가 single source. 9 keywords: `photograph` / `poster` / `painting` / `portrait` / `monitor` / `TV` / `mirror` / `window reflection` / `projection`. |
| ethnicity components **(R3-B3 ground-truth source)** | demographic_descriptor_policy.components.ethnicity 의 9+ entry. region 별 권장. 본 정의가 single source. entries: `Asian` / `East Asian` / `South Asian` / `Southeast Asian` / `Black` / `Middle Eastern` / `Hispanic` / `Latina` / `Latino` / `Caucasian`. 한 작품 내 일관성 — 작품 단위 고정. |
| canary | 광범위 적용 전 1 PID / 1 selected segment 로 baseline vs candidate 비교 검증 (G4.2 carry). 결과는 `docs/canary/g4_id_policy_<timestamp>.json` 에 저장. |
| G4.2 baseline | G4.2 commit `14d14cb` 기준의 4 metric (body-part focus / close-framing face / reproduction surface / demographic descriptor present ratio). G4.3 exit criteria 비교 기준. |
| 4-way sync | v18 push 시 lockstep 갱신 4 곳: prompts/_base/scene_detail/18.../system.md 디렉토리 + detail_steps.py:105 SCENE_DETAIL_PROMPT_VERSION + version_registry.py:34 MODULE_VERSIONS + version_registry.py:126 _MODULE_INFO. |

---

## 10. Open Questions (R1+R2 audit 에서 해결 예정)

| ID | 질문 | 현재 stance |
|---|---|---|
| ~~Q1~~ | ~~v17 line 390-407 의 ID 룰 인용 처분~~ | **RESOLVED (R1R2-Q1)**: §3.2 결정 — line 395 1 line 인플레이스 교체. v17 line 390-407 자체는 G4.4 scope. |
| Q2 | `demographic_descriptor_policy.components.ethnicity` 에 `Native American` / `Pacific Islander` 등 추가 entry? | **stance**: 현재 10 entry (Asian / East Asian / South Asian / Southeast Asian / Black / Middle Eastern / Hispanic / Latina / Latino / Caucasian — Latina+Latino split 후) 로 v17 line 361+372+382 와 1:1. 추가는 별도 시나리오 region requirement 발생 시 P1 follow-up. G4.3 scope 외. |
| Q3 | `body_part_focus_rule.trigger_phrases` 에 한국어 trigger (`초점`, `클로즈업`) 추가? | **stance**: v17 prose 는 영어 patterns 만 명시. t2i_prompt 자체가 영어이므로 불필요 — G4.3 scope 외. |
| ~~Q4~~ | ~~v18 prompt compact section 안에 ✓/✗ 예시 inline 여부~~ | **RESOLVED (R2-M3)**: 미포함 (token gate 15 token slack tight). §3.3 draft 가 reasoning prose 만 carry. |
| ~~Q5~~ | ~~`id_policy_card_snapshot_hash` 산출 방법~~ | **RESOLVED (R2-I2)**: §5.2 + §5.3 결정 — `compute_id_policy_snapshot_hash(card) = sha256(json.dumps(card["id_policy"], sort_keys=True, ensure_ascii=False))[:16]`. helper `render_prompt_card.py` 신설. |
| Q6 | unit test count — G4.2 25 vs G4.3 추정치? | **stance**: face_identifiability_rule 추가로 31 → ~36 (4 신규 sub-field 각 ~6-8 test + alignment + integration). final count 는 plan drafting 시 fix. R3 (plan audit) 에서 검증. |

---

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

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

### Round 1 (예정) — 미진행

R1 dispatch 후 결정사항 summary 추가.

### Round 2 (예정) — 미진행

R1 결과 후 dispatch.

### 메타룰

- 본 spec 의 진실 source = 상단 "## Round Override" 표.
- Codex 또는 Claude 리뷰에서 BLOCKING 발견 시 즉시 상단 표에 row 추가 + 본문 동기화.
- 절대 규칙 위반 (LLM 입력 truncation, 시나리오 의존 고유명사 등) 발견 시 BLOCKING
  최우선.

---

## 12. Override Table (G4.2 carry — propagate into G4.3)

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

| ID | Source (G4.2 audit ref or new) | Override | Why |
|----|-------|----------|-----|
| O-1 | R2-B4 (G4.2) | builder 의 None vs [] fail-fast (`build_id_policy()` `visible_entities=None` → AppError) | silent fallback ban (`feedback_no_silent_fallback.md`) |
| O-2 | R1-I12 (G4.2) | `_DEFAULT_SENTINEL = object()` fixture pattern (test 측) | fixture default vs None vs [] 구분 — production drift 차단 |
| O-3 | R2-I7 / iter2 B1 (G4.2) | canary pinned-tuple seen_in_cp post-loop 검증 | false STRICT pass 차단 — empty `data.scenes` / pinning 1:1 false-pass |
| O-4 | iter2 B2 (G4.2) | `t2i_variations` 누락 silent absorb 차단 — `t2i_variations=[]` / missing → measurement_failures | gate fail 누락 차단 |
| O-5 | iter3 B3 (G4.2) | variation 내부 `t2i_prompt` 누락 silent absorb 차단 — `var.get("t2i_prompt") or ""` 패턴 금지 | detail_schema.json variation.required = t2i_prompt — strict |
| O-6 | iter1 C3 (G4.2) | `or [] / or {}` 패턴 전반 차단 | shots / scenes 흡수 → `total_shots=0` false-pass |
| O-7 | iter1 C2 (G4.2) | gate fail 시 exit 1 강제 (return 0 금지) | CI 인지 가능 |
| O-8 | iter1 I1 (G4.2) | producer-side `_g41_bg_owned = ... or []` 패턴 금지 — None 흡수 차단, builder fail-fast 보장 | builder fail-fast 무력화 차단 |
| O-9 | iter1 I2 (G4.2) | hash 회귀 test gap 방지 — `_card_metadata` 변경 / `id_policy.constraints` 변경 drift test 의무 | hash drift undetected 차단 |
| O-10 | R1-B1 (G4.2) | `_card_metadata` envelope-sibling top-level only — nested 구조 도입 금지. `canonicalize_render_prompt_card()` top-level pop. | hash 격리 contract |
| O-11 | R2-I5 (G4.2) | `_card_metadata` free-form (extra-key 허용, schema 검증 없음). assert_card_shape envelope 7 field 만 strict. | 미래 G4.x 신규 sub-key 추가 시 validator 변경 0 |
| O-12 | R2-I4 (G4.2) | `detail_steps.py` inject 직전 `_card_metadata` strip 보존 | LLM 입력 token 격리 |
| O-13 | R1-I6 (G4.2) | `CARD_SCHEMA_VERSION` 유지 (= 1) — id_policy 신규 sub-field 는 envelope shape 변경 아님. `SCENE_DETAIL_SCHEMA_VERSION` 유지 (= 7) — cp shape 변경 아님. | backward compat |
| O-14 | R2-B6 (G4.2) | canary JSON `pid` 가 `pinning` block 안 (top-level redundant 제거) | pinning dict equality 한 번 |
| O-15 | R1-M1 (G4.2) | token measurement = `tiktoken cl100k_base` 실측 (line × 15 estimate 는 spec 참조용) | exit criteria 실측 기준 |
| O-16 | R1-I4 / R2-B5 (G4.2) | strict 0 metric 은 baseline 비0 이어도 candidate 0 강제 | baseline 비0 → P1 follow-up 별 추적 |
| O-17 | plan-R1-I4 (G4.2) | unit test fixture `_make_full_card()` 가 `_DEFAULT_SENTINEL = object()` 적용 — None / [] / default 구분 | G4.1 R3-B2 fixture trap 회피 |
| O-18 | plan-R2-I5 (G4.2) | canary owned/sentinel scripts 가 sentinel 합산 전 `assert_owned_sentinel_shape()` 호출 (G4.3 의 4 metric scripts 도 sentinel 검증 시 동일 pattern) | silent 0 false-pass 차단 |
| O-19 | plan-R2-I6 (G4.2) | commit target 에 `version_registry.py` (4-way sync 일부) 의무 포함 | alignment test green 유지 |
| **G4.3-new** | (new) R3-B1 ground-truth | **6 forbidden phrasing** ground-truth — §9 Glossary single source. §2.2 / §3.3 / §6.1 unit test / canary detector 모두 동일 list reference. | G4.2 R1-I2 / R2-B3 패턴 carry — drift 차단 |
| **G4.3-new** | (new) R3-B2 ground-truth | **9 reproduction surface keywords** ground-truth — §9 Glossary single source. | G4.2 R1-I2 패턴 carry — drift 차단 |
| **G4.3-new** | (new) R3-B3 ground-truth | **10 ethnicity components** ground-truth (R1-I1 carry — Latina+Latino split) — §9 Glossary single source. region 별 작품 단위 고정. 시나리오 의존 0 (작품 고유명사 ban). | drift 차단 + 시나리오 의존 0 |
| **G4.3-new** | (new) R3-I1 strict scope | 4 metric STRICT scope: body-part focus = 모든 shot, close-framing face = close-framing only, reproduction surface = 모든 shot, demographic descriptor = composite-ID-등장 shot 만 (분모) | metric overlap / undefined denominator 차단 |
| **G4.3-new** | (new) R3-I2 4-way sync | v18 push lockstep 4 곳 (prompt 디렉토리 + detail_steps.py:105 + version_registry.py:34 + version_registry.py:126) — alignment test 가 강제 검증 | 4-way drift 차단 |
| **G4.3-new** | (new) R3-I3 entry gate split | spec drafting / plan / Wave 1-4 = canary 무관 진행 가능. **commit gate** = G4.2 production canary 6 gate 필수. | 작업 차단 회피 + canary 결과의 commit-gate 역할 명확화 |
