# G4.2 Background-Binding Lift Design

본 문서는 G4 spec (`docs/superpowers/specs/2026-05-04-g4-render-prompt-card-strategy.md`)
§6 G4.2 섹션 (line 572-599) 의 detail sub-spec 이다. G4.1 (commit 5d045f3 +
c656ce3 + 86473df) 의 자연 후속.

- 작성일: 2026-05-04
- 상태: initial draft
- 선행 완료: G4.1 RenderPromptCard bootstrap (CARD_SCHEMA_VERSION=1, 5 field, hash,
  assert_card_shape, _analyze_one inject, verify_completion drift, _user_edited path)
- 목적: Rule A / Rule C / Rule E prose 를 `background_binding.constraints` 로 lift,
  system.md 에서 해당 long-form sections 제거 (token 감소), owned violation rate ≤
  G4.1 baseline 검증

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

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

### Round 1 — BLOCKING

| ID | 결정 | spec 영향 |
|---|---|---|
| **R1-B1** | `rule_source` 와 `lift_status` 둘 다 `_card_metadata` 하위로 격리, **hash payload 에서 제외**. G4.1 R1-I11 패턴 carry (debug-only 는 `_card_metadata` 통일). 본 spec 안에서 결정 — R-round 로 defer 안 함. | §2.2 (envelope JSON 예시 + sub-field 설명 모두 `_card_metadata` 하위로 이동), §8 risk register row 갱신 |

### Round 1 — IMPORTANT

| ID | 결정 | spec 영향 |
|---|---|---|
| **R1-I1** | §2.2 `background_ref_attached` constraints 가 builder 의 conditional 동작과 mismatch 였음. 결정: spec §2.2 를 conditional 로 명시 (`bg_owned` 비어있지 않을 때 owned 2 constraint 추가, `cam_ref` 존재할 때 camera 2 constraint 추가, 그 외 fallback "no specific binding constraints" 1 row). builder 코드 변경 X (Non-goals §1.2 유지). v17 prompt compact section 도 동일 conditional 처리 reflect. | §2.2 background_ref_attached row 재작성, §3.3 compact section 단락 조정 |
| **R1-I2** | Rule E forbidden wording = **6 ground-truth pattern** (spec 한 곳에서만 정의 — §9 Glossary 가 source). 6 패턴: `the existing X` / `from the reference` / `use the X from the reference` / `preserving the same room perspective` / `maintaining the reference's framing` / `do not generate a new X` + 추가 (Rule A close-framing 무효화) `same … angle as the reference` / `match the reference framing` 류. 모든 §2.2 / §3.3 / §6.1 / §9 가 이 ground-truth list 를 동일하게 reference. | §2.2 skipped_close_framing constraints / §3.3 compact / §6.1 unit test / §9 Glossary 동기화 |
| **R1-I3** | `camera_consistency_wording_present` 측정 = `t2i_variations[].t2i_prompt` 텍스트에서 deterministic regex/keyword set 검출. 검출 패턴: `match.*reference.*(camera|framing|position)` / `same.*(angle|position).*reference` / `(push in\|pull out\|deviation).*from.*reference` 등. spec §5.3 에 정확한 keyword/regex list 박음 + 검출 스크립트 경로 (`scripts/canary/g4_2_camera_wording.py`) 명시. | §5.3 canary procedure + §5.1 exit criteria 측정 방법 명시 |
| **R1-I4** | close-framing forbidden wording threshold = `candidate count == 0` **strict** (≤ baseline 아님). Rule E 는 close-framing 에서 절대 출력되면 안 되는 패턴이므로 baseline 비0 이어도 candidate 0 강제. | §5.1 / §5.3 비교 항목 strict 갱신 + §8 risk register |
| **R1-I5** | canary baseline / candidate scene-set pinning 강화. JSON 에 다음 필수 field: `pid`, `scene_index_list`, `shot_index_list_per_scene`, `model_routing` (예: `gemini-3.1-pro-preview`), `prompt_source_mode` (`file` vs `db`), `card_commit_hash` (G4.1 commit), `chain_bg_card_snapshot_hash`. baseline 과 candidate 동일 조건만 비교 허용. | §5.2 / §5.3 JSON schema 확장 |
| **R1-I6** | CARD_SCHEMA_VERSION = 1 **유지** (bump 안 함). 근거: R1-B1 결정으로 `lift_status` / `rule_source` 둘 다 `_card_metadata` 하위로 격리 → envelope shape (5 contract field) 변경 없음 + `_card_metadata` 자체가 hash 제외이므로 v1 backward compat 보장. SCENE_DETAIL_SCHEMA_VERSION 도 cp shape 변경 없으면 유지 (= 7) — `_card_metadata` 추가는 cp shape 변경 아님. spec §7.2 step 5 명시. | §7.2 step 5 재작성 |
| **R1-I7** | not_applicable mode test scope 확장. unit +2 (`test_not_applicable_constraints_no_reference_language`, `test_not_applicable_card_metadata_lift_status`), integration +1 (`test_not_applicable_shot_card_carries_no_owned_no_camera_no_reference`). | §6.1 / §6.2 test list 확장 |

### Round 1 — MINOR

| ID | 결정 | spec 영향 |
|---|---|---|
| **R1-M1** | token 추정 baseline = **tiktoken `cl100k_base` 기준** (또는 동급). spec §3.4 의 "v16 ≈ 9,300 tokens" 는 추정값임을 명시 + canary 가 실측 시 caveats 갱신. | §3.4 표 caption + §5.3 canary JSON `system_prompt_token_count` field 측정 도구 명시 |
| **R1-M2** | §8 risk register 를 **G4.2-unique** 와 **G4.1 carry / inherited checks** 두 subsection 으로 분리. v15 → v17 resume escalation 등 carry row 는 후자로 이동. | §8 두 subsection 으로 재구성 |

### Round 2 — BLOCKING

| ID | 결정 | spec 영향 |
|---|---|---|
| **R2-B1** | `_card_metadata` 격리는 envelope top-level only. mode-by-mode JSON snippet 의 `"_card_metadata.lift_status"` dotted-key 표기 모두 제거 — mode snippet 은 `background_binding.constraints` 만 보여주는 partial JSON 으로 명시. envelope-level `_card_metadata.lift_status` 별도 matrix 표로 정리. `canonicalize_render_prompt_card()` (`render_prompt_card.py:563-566`) 가 top-level pop 만 수행 — nested 구조 도입 금지. | §2.2 mode 별 JSON snippet 재작성 + `_card_metadata` matrix 표 별도 |
| **R2-B2** | §1.2 Non-goals 좁힘 (옵션 A 채택). 변경 후 정의: "Producer side — `background_prompt` step / `chain_bg` loader 변경 없음. **`build_background_binding()` 내부 로직 변경은 G4.2 의 자연 부산물로 in-scope** (`_card_metadata` 작성 / R1-I2 6-pattern + Rule A 4 forms / sub-case D fallback constraint 추가)." | §1.2 Non-goals 본문 좁힘 |
| **R2-B3** | Rule A close-framing 무효화 = **4 ground-truth forms** (§9 Glossary single source 강화): (1) `"matching the reference camera"` (2) `"deviation from reference"` (3) `"same … angle as the reference"` (4) `"match the reference framing"`. §2.2 `skipped_close_framing` constraints / §6.1 unit test assert / canary script `g4_2_close_forbidden.py` 모두 4 forms 동일 reference. | §2.2 / §6.1 / §9 sync |
| **R2-B4** | canary metric scope 분리 (overlap 차단). `camera_consistency_wording_present` = **non-close framing shot 의 `t2i_prompt` 만** 측정 (close framing 은 reference 자체 부재이므로 measurement 무의미). close framing shot 은 forbidden_wording 만 측정. §5.1 / §5.3 명시. | §5.1 / §5.3 measurement scope 명시 |
| **R2-B5** | R1-I4 close-framing forbidden wording strict (`== 0`) 를 §7.3 G4.3 entry 조건에 propagate. "close framing forbidden wording count ≤ baseline" → "**close framing forbidden wording count == 0 strict**" 로 갱신. | §7.3 G4.3 entry table 갱신 |
| **R2-B6** | pinning JSON schema 일관: §5.3 JSON 의 `pid` 를 top-level 에서 `pinning` block 안으로 이동 (§5.2 표 7 field 와 1:1 일치). top-level 에 redundant pid 두지 않음. | §5.3 JSON schema 갱신 |

### Round 2 — IMPORTANT

| ID | 결정 | spec 영향 |
|---|---|---|
| **R2-I1** | §5.1 token delta gate `≥ -800 tokens` → **`≥ -850 tokens`** 로 tighter 조정 (§3.4 estimate -915 대비 65 token margin). caption 에 margin rationale 명시 — measurement variance 흡수용 65 token slack. | §5.1 + §3.4 caption |
| **R2-I2** | R2-B2 옵션 A 채택으로 sub-case D unit test (`test_background_ref_attached_neither_owned_nor_camera_fallback`) 자동 valid — 별 처리 불필요. test row 유지. | §6.1 (no change) |
| **R2-I3** | §4 Lift Mapping Table Rule E "lift 완료 판정 기준" `≤ baseline` → **`== 0 strict`** 로 갱신 (R1-I4 sync). | §4 Rule E row 갱신 |
| **R2-I4** | `_card_metadata` 가 user_prompt inject 시 token 영향 차단 (옵션 A 채택). detail_steps.py inject 직전 `_card_metadata` strip 추가 — G4.2 producer-side 변경 1건 (R2-B2 in-scope 와 일관). spec §3.4 가정 (`_card_metadata` token +0) 보존. §7.2 step 1 에 strip 추가 명시. | §3.4 caption + §7.2 step 1 |
| **R2-I5** | `assert_card_shape()` 는 envelope contract **7 field 만 strict 검증** (plan-R2-I3 정정 — `_REQUIRED_TOP_FIELDS` 가 7 entries: schema_version / shot_key / render_strategy / id_policy / background_binding / continuity_elements_used / asset_requirements). `_card_metadata` 는 **free-form (extra-key 허용, schema 검증 없음)** 명시. spec §2.2 본문 + §7.2 step 3 에 contract 박음. | §2.2 + §7.2 step 3 |

### Round 2 — MINOR

| ID | 결정 | spec 영향 |
|---|---|---|
| **R2-M1** | §10 Round 1 history entry trim — Override 표 의 11 row 결정을 §10 에서 다시 enumerate 하지 않음. status-only marker (날짜 / 결과 count / 완료 표시) 만. | §10 trim |

### 메타룰

- Round Override 표 가 §1-§9 본문보다 우선.
- 본 R1+R2 패치 = 표 채움 **+ 본문 inline 동기화** (G4.1 패턴 답습). cross-reference 불필요.
- R3 이후 round 는 추가 audit 결과 발생 시 본 표에 row 추가.

---

## 1. Overview

### 1.1 Why now

G4.1 은 `RenderPromptCard` 의 `background_binding` 필드를 이미 빌드하고 inject 한다.
현재 `background_binding` 은 다음을 carry 한다:

- `mode` (background_ref_attached / skipped_close_framing / background_mode_off /
  not_applicable)
- `bg_id` (string or null)
- `reference_usage` (exact_background / atmosphere_reference / skipped_close_framing / none)
- `owned_objects` (English canonical string list)
- `camera_reference` (camera_position / camera_height / lens_hint / framing_notes)
- `close_framing_skips_background_ref` (bool)
- `constraints` (list of reminder strings)

그러나 `system.md` (v16, `prompts/_base/scene_detail/16.202605041200/system.md`) 에는
여전히 세 long-form prose section 이 있다:

- line 197-222: `## chain_bg reference 카메라 일관성 (Rule A)` — 일치형/deviation형/절대 금지
  + 3 예시 블록
- line 223-246: `## chain_bg 객체 중복 묘사 금지 (Rule C — G3.2 contract)` — 사용 규칙
  / 위반 패턴 / 허용 패턴 (3개 항목)
- line 247-279: `## close framing 시 reference image 부재 가정 (Rule E)` — 적용 조건
  / 금지 표현 6개 / 권장 자체 묘사 / 판단 자체 점검

이 세 section 의 역할 — "어떤 데이터가 이 shot 에 적용되는가" — 은 G4.1 이 card 에
deterministic 하게 넣어 준다. LLM 에게 남겨야 하는 것은 "그 데이터를 어떻게 써야
하는가" 라는 짧은 reminder 한 단락뿐이다.

G4.2 는 그 분리를 완성한다: long prose 제거 → single compact "Background Binding" 섹션
으로 교체. token 감소 quantified + owned violation rate ≤ baseline 검증.

### 1.2 Non-goals (명시)

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

- ID-policy lift (G4.3) — C##O## composite ID, body-part close, photo/screen/reflection
  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 변경 — `background_prompt` step, `chain_bg` loader 변경 없음. **그러나
  `build_background_binding()` 내부 로직 변경은 G4.2 의 자연 부산물로 in-scope**
  (R2-B2 결정): `_card_metadata.lift_status` / `_card_metadata.rule_source` 작성,
  R1-I2 6 ground-truth pattern + R2-B3 Rule A 4 forms 추가, sub-case D fallback
  constraint 추가, detail_steps.py inject 직전 `_card_metadata` strip (R2-I4). 이 변경
  들은 spec §7.2 작업 순서에 포함된다.
- `_check_prompts()` / deterministic post-check 변경 없음.
- G3.2 owned sentinel (`owned_validation` CP field, LLM judge, `_OWNED_VALIDATOR_FULL`)
  의 로직 변경 없음. G4.2 는 이 sentinel 의 **rate measurement** 를 baseline 으로만 사용.
- scene_image_pipeline 또는 합성 로직 변경 없음.

---

## 2. Card Envelope Sub-shape: background_binding.constraints

### 2.1 G4.1 현재 shape

`build_background_binding()` 이 반환하는 shape (`render_prompt_card.py:272-378`) 는 이미
`constraints` list 를 포함한다. G4.1 에서 mode 별로 다음 constraint string 이 들어간다:

| mode | constraints (G4.1 현재) |
|---|---|
| background_mode_off | "background_mode is off — no chain_bg reference is attached and no owned-object preservation is required" |
| skipped_close_framing | "this shot is close framing — do not mention the reference image, the existing room, or any chain_bg owned object" + "the background ref is intentionally skipped (mode = skipped_close_framing); compose the close subject without anchoring to the wider room" |
| not_applicable | "no chain_bg is bound to this shot — describe the location freely without referencing a background image" |
| background_ref_attached | "do not create new objects in the owned_objects list — they are already drawn in the background reference" (bg_owned 존재 시) + "if focusing on an owned object, anchor it as 'from the reference'" (bg_owned 존재 시) + "match the reference image's camera_position and framing — do not rotate, change height, or zoom independently" (cam_ref 존재 시) |

이 constraints 는 Rule A / C / E 의 "what to do / not do" 를 deterministic data 로
표현하지만, "왜 하지 말아야 하는가" 의 reasoning prose 는 system.md 에 여전히 남아있다.

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

G4.2 에서 `background_binding.constraints` 는 Rule A/C/E 의 세 축을 mode 별
constraint list 로 명시 표현한다. envelope contract field (5개) 는 변경하지
않고, **debug-only sub-field 두 개 (`lift_status`, `rule_source`) 는
`_card_metadata` 하위로 격리 — hash payload 에서 제외** (R1-B1 결정, G4.1 R1-I11
패턴 carry).

```json
{
  "schema_version": 1,
  "shot_key": "S12_Shot4",
  "render_strategy": { "...": "..." },
  "id_policy": { "...": "..." },
  "background_binding": {
    "mode": "background_ref_attached | skipped_close_framing | background_mode_off | not_applicable",
    "bg_id": "cb_main_room_night_normal",
    "reference_usage": "exact_background | atmosphere_reference | skipped_close_framing | none",
    "owned_objects": ["door", "window", "TV"],
    "camera_reference": {
      "camera_position": "same southeast doorway angle",
      "camera_height": "eye-level standing",
      "lens_hint": "35mm wide",
      "framing_notes": "doorway and TV visible at frame edge"
    },
    "close_framing_skips_background_ref": false,
    "constraints": [
      "do not create new owned_objects — they are already drawn in the reference",
      "if focusing on an owned object, anchor it as 'from the reference'",
      "match the reference image's camera_position and framing — do not rotate, change height, or zoom independently",
      "if deviating from the reference framing (push-in, angle shift, ECU), state the deviation explicitly"
    ]
  },
  "continuity_elements_used": { "...": "..." },
  "asset_requirements": { "...": "..." },
  "_card_metadata": {
    "lift_status": {
      "rule_a_lifted": true,
      "rule_c_lifted": true,
      "rule_e_lifted": true
    },
    "rule_source": {
      "camera_rule": "A",
      "owned_rule": "C",
      "close_skip_rule": "E"
    }
  }
}
```

**`_card_metadata` 하위 두 debug-only sub-field 설명 (R1-B1 결정):**

- `_card_metadata.lift_status` (debug-only, hash 제외): Rule A/C/E 가 card 로 lift
  됐음을 version-tracking 목적으로 기록. v17 prompt 에서 prose 삭제 완료 후 이
  field 로 확인.
- `_card_metadata.rule_source` (debug-only, hash 제외): 각 constraint group 이 어느
  v16 Rule 에서 왔는지 역추적. spec audit 전용. 미래 Rule 추가/이름 변경 시 hash
  drift 발생 안 하도록 격리.

`_card_metadata` 자체는 G4.1 R1-I11 의 hash payload 제외 키 (
`canonicalize_render_prompt_card()` 가 strip 후 hash) 이므로 본 두 sub-field 추가는
hash 영향 0 + CARD_SCHEMA_VERSION = 1 backward compat 유지 (R1-I6 결정).

**mode 별 constraints 확정 (G4.2 이후):**

> **R2-B1 명시**: 다음 mode 별 JSON snippet 은 `background_binding.constraints` 만
> 보여주는 partial JSON (full envelope shape 아님). `_card_metadata` 는 envelope
> top-level sibling 으로 격리됨 (위 JSON 예시 line 156-168 참조 — `canonicalize_render_prompt_card()`
> 가 top-level pop 만 수행하므로 nested 표기 금지). mode 별 `lift_status` 값은 본
> 단락 끝의 envelope-level matrix 표 참조.

`background_mode_off`:
```json
{
  "constraints": [
    "background_mode is off — no chain_bg reference is attached and no owned-object preservation is required"
  ]
}
```

`skipped_close_framing` (Rule E 적용 — R1-I2 6 ground-truth pattern + R2-B3 Rule A 4 forms 모두 명시):
```json
{
  "constraints": [
    "this shot uses close framing — the background reference is intentionally omitted at composition time",
    "do not mention the existing room, the chain_bg reference image, or any item from the owned_objects list",
    "do not use any of: 'the existing X', 'from the reference', 'use the X from the reference', 'preserving the same room perspective', 'maintaining the reference's framing', 'do not generate a new X' — these phrases are invalid when the reference is absent",
    "do not use any of: 'matching the reference camera', 'deviation from reference', 'same … angle as the reference', 'match the reference framing' — Rule A close-framing invalidation: close framing has no reference camera to match or deviate from",
    "compose the close subject from its immediate surroundings only — use [L##: ...] block for frame-edge surface description"
  ]
}
```

> **§9 Glossary 가 R1-I2 6 ground-truth + R2-B3 Rule A 4 forms 의 single source.**
> 본 constraint 문자열, §3.3 v17 compact section, §6.1 unit test assert, canary
> detector script 모두 동일 list 를 reference.

`not_applicable` (chain_bg 없음):
```json
{
  "constraints": [
    "no chain_bg is bound to this shot — describe the location freely without referencing a background image",
    "do not use 'from the reference' / 'the existing room' / 'preserving the room' style phrasing — there is no reference image to refer to"
  ]
}
```

**Envelope-level `_card_metadata.lift_status` matrix (R2-B1 — mode 별 dict):**

| mode | rule_a_lifted | rule_c_lifted | rule_e_lifted |
|---|---|---|---|
| `background_mode_off` | true | true | true (vacuous — close framing 자체 적용 X) |
| `skipped_close_framing` | true | true | true |
| `not_applicable` | true | true | false (chain_bg 부재 — Rule E 적용 대상 아님) |
| `background_ref_attached` | true | true | false (close framing 아니면 Rule E 대상 아님) |

본 matrix 는 envelope `_card_metadata.lift_status` 에 그대로 작성. mode 별
`background_binding.constraints` 와 분리된 scope.

`background_ref_attached` (R1-I1 conditional — owned/camera 데이터 존재 여부에
따라 constraint set 분기):

| sub-case | builder 입력 | constraints (포함) |
|---|---|---|
| **A. owned + camera 둘 다 존재** | `bg_owned != [] and cam_ref != None` | "owned_objects lists all environment objects already drawn in the background reference — do not create new ones" + "when focusing on an owned object, anchor it explicitly: 'the door from the reference'" + "camera_reference specifies the framing under which the background was painted — match camera_position, camera_height, and framing" + "if deviating from the reference framing (push-in, angle shift, ECU), state the deviation explicitly so the compositor can account for the scale difference" |
| **B. owned 만 존재 (camera 부재)** | `bg_owned != [] and cam_ref == None` | A 의 owned 2 constraint 만. camera constraint omit. |
| **C. camera 만 존재 (owned 부재)** | `bg_owned == [] and cam_ref != None` | A 의 camera 2 constraint 만. owned constraint omit. |
| **D. 둘 다 부재** | `bg_owned == [] and cam_ref == None` | "the chain_bg reference is attached but carries no owned-object list and no camera metadata — describe the scene freely while staying consistent with the reference image overall mood" (fallback 1 row) |

**R2-B2 결정 (Non-goals 좁힘)**: builder 변경은 G4.2 in-scope. 현재 builder 의
conditional 동작 (`if bg_owned:` / `if cam_ref:`) 을 4 sub-case 로 documentation
하면서, sub-case D fallback constraint 는 G4.2 작업 순서 (§7.2 step 1) 에서
`build_background_binding()` 에 추가.

**`_card_metadata` shape 검증 contract (R2-I5)**:

`assert_card_shape()` 는 envelope contract **7 field (schema_version, shot_key,
render_strategy, id_policy, background_binding, continuity_elements_used,
asset_requirements) 만 strict 검증**. `_card_metadata` 는 **free-form** —
extra-key 허용, schema 검증 없음. 미래 G4.x 에서 새 debug sub-field 추가해도
validator 변경 불필요. `_card_metadata` 자체는 hash payload 에서 strip 되므로
contract 영향 0.

### 2.3 Producer / consumer matrix

| field | producer | consumer (LLM) | consumer (post-parse judge) |
|---|---|---|---|
| `mode` | `build_background_binding()` | card-wins branching | `verify_completion()` hash drift |
| `owned_objects` | `ctx.chain_bg_owned_by_shot` → builder | Rule C anchor constraint 준수 | G3.2 `owned_validation` sentinel judge |
| `camera_reference` | `ctx.chain_bg_camera_meta_by_shot` → builder | Rule A matching constraint 준수 | G4.2 canary prompt inspection |
| `close_framing_skips_background_ref` | `_CLOSE_FRAMING_RE` → builder | Rule E wording guard | close-framing forbidden wording check (§6) |
| `constraints` | builder per mode | t2i_prompt 작성 시 constraint list 준수 | LLM judge `_OWNED_VALIDATOR_FULL` (G3.2 carry) |
| `lift_status` | builder (hardcoded true after G4.2) | 무시 | spec audit only |

### 2.4 정보 분리 원칙

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

| 정보 | G4.2 이후 위치 | 근거 |
|---|---|---|
| 어떤 카메라 설정인가 (position / height / lens) | card `background_binding.camera_reference` | deterministic — G4.1 이미 carry |
| owned 객체 목록 | card `background_binding.owned_objects` | deterministic — G4.1 이미 carry |
| close framing 여부 | card `background_binding.mode=skipped_close_framing` | deterministic — G4.1 이미 carry |
| "왜 owned 객체를 다시 그리면 안 되는가" (이중 합성 설명) | system.md "Background Binding" 한 단락 reminder | reasoning prose — card 로 표현 불가 |
| "왜 close framing 에서 reference 언급 금지인가" (scale 충돌 설명) | system.md "Background Binding" 한 단락 reminder | reasoning prose — card 로 표현 불가 |
| Rule A/C/E 의 ✓/✗ 예시 문장들 | 삭제 (G4.2 prompt v17) | card constraints 가 대체; 예시는 중복 |
| close framing 금지 표현 6개 (the existing X 등) | card `skipped_close_framing.constraints` 에 통합 | deterministic list → card 로 |

---

## 3. Prompt Change Spec

### 3.1 대상 버전

- **Before**: `prompts/_base/scene_detail/16.202605041200/system.md` (v16, ~620 lines,
  Rule A/C/E prose 포함)
- **After**: `prompts/_base/scene_detail/17.<timestamp>/system.md` (v17, Rule A/C/E
  removed, "Background Binding" compact section 대체)

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

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

| section | lines (v16) | 내용 | 삭제 이유 |
|---|---|---|---|
| `## chain_bg reference 카메라 일관성 (Rule A)` | 197-222 | 일치형 3예시 + deviation형 3예시 + 절대 금지 2예시 = 26 lines | card `camera_reference` + constraints 가 동일 데이터 carry |
| `## chain_bg 객체 중복 묘사 금지 (Rule C — G3.2 contract)` | 223-246 | 사용 규칙 + 위반 패턴 3개 + 허용 패턴 3개 = 24 lines | card `owned_objects` + constraints 가 동일 데이터 carry |
| `## close framing 시 reference image 부재 가정 (Rule E)` | 247-279 | 적용 조건 + 금지 표현 6개 + 권장 자체 묘사 2예시 + 판단 자체 점검 3항목 = 33 lines | card `skipped_close_framing` mode + constraints 가 동일 데이터 carry |

총 삭제 대상: approximately 83 lines.

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

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

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

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

**mode 별 동작:**

- `background_ref_attached`: `owned_objects` 의 항목은 배경 PNG 에 이미 그려져 있다.
  새로 그리도록 지시 금지 — 이중 합성 발생. 집중하려면 "from the reference" 앵커 사용.
  `camera_reference` 가 지정한 카메라 설정에 맞춰 t2i_prompt 를 작성. deviation 은
  명시적으로 기술 (합성 scale 불일치 방지). owned/camera 둘 중 하나라도 부재면
  card constraints 가 conditional 로 표현하므로 prompt 도 그 분기 따른다.
- `skipped_close_framing`: 배경 reference 가 합성에서 제외된다. 다음 6개 표현은
  절대 출력 금지 — `the existing X` / `from the reference` / `use the X from the reference` /
  `preserving the same room perspective` / `maintaining the reference's framing` /
  `do not generate a new X`. Rule A 의 일치형/deviation형 표현도 close framing 에서는
  무효 (reference 자체 없음). [L##: ...] block 으로 frame edge surface 직접 묘사.
- `background_mode_off` / `not_applicable`: reference 자체 없음 — reference 관련
  표현 전부 금지, 공간을 직접 묘사.
```

예상 line 수: 약 22 lines (R1-I2 6 ground-truth pattern 명시 inline 으로 인해 +4).

### 3.4 Token 감소 추정

> **R1-M1 + R2-I4 caption**: 본 표의 token 수는 line 수 × 약 15 tokens/line 의
> 1차 추정값 (`tiktoken cl100k_base` 미적용). v16 total ≈ 9,300 tokens 도 동일
> 추정 방식. canary 측정 단계 (§5.3) 가 `tiktoken cl100k_base` 로 system.md 실측
> 후 본 표를 갱신. exit criteria (§5.1 token delta gate) 는 **실측치 기준으로만
> 판정**.
>
> **R2-I4 명시**: `_card_metadata` 가 user_prompt inject 시 +0 token 가정은 G4.2
> producer-side 변경 (`detail_steps.py` inject 직전 strip — §7.2 step 1) 으로
> 보존. strip 미구현 시 `_card_metadata` 가 그대로 inject 되어 +5~10 token 발생
> 가능 — 본 변경은 hash 격리와 inject 격리 일관성 확보를 위한 mandatory 변경.

| 항목 | v16 | v17 (예상) | delta |
|---|---|---|---|
| Rule A section | 26 lines ≈ 390 tokens | 0 (삭제) | -390 |
| Rule C section | 24 lines ≈ 360 tokens | 0 (삭제) | -360 |
| Rule E section | 33 lines ≈ 495 tokens | 0 (삭제) | -495 |
| Background Binding compact section (신규) | 0 | 22 lines ≈ 330 tokens | +330 |
| **net system.md** | — | — | **-915 tokens** (R1-I2 patterns 추가로 +60 보정. 추정. 실측 = canary §5.3) |
| card `background_binding` user_prompt inject | 이미 있음 (G4.1) | 동일 (`_card_metadata` strip 후 inject — R2-I4 보장) | +0 |

**Token gate margin (R2-I1)**: §5.1 의 token delta gate 는 `≥ -850 tokens` —
estimate `-915` 대비 65 token slack. tiktoken 실측 variance + estimate 의 lines×15
근사치 오차 흡수용. tighter (`≥ -900`) 으로 가면 estimate 오차로 candidate 가
실패할 위험, looser (`≥ -800`) 로 가면 design intent 미달 가능.

참고: card inject 는 G4.1 에서 이미 user_prompt 에 들어간다. v17 의 순 효과 = system.md
token 감소. user_prompt 는 G4.1 과 동일.

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

---

## 4. Lift Mapping Table

Rule 별 card field 1:1 대응:

| Rule | v16 prose 역할 | G4.2 card field | lift 완료 판정 기준 |
|---|---|---|---|
| **Rule A** — camera consistency | reference 카메라와 t2i 카메라 일치/deviation 명시 | `background_binding.camera_reference` (camera_position / camera_height / lens_hint / framing_notes) + constraints 마지막 항목 ("match the reference image's camera_position...") | system.md Rule A section 삭제 후 canary 1 PID 에서 camera consistency wording 검출 ≥ G4.1 baseline rate |
| **Rule C** — owned object no-redraw | owned 객체 이중 생성 금지 | `background_binding.owned_objects` (English canonical list) + constraints 첫 두 항목 ("do not create new owned_objects...", "anchor as 'from the reference'") | G3.2 `owned_validation.violations` rate ≤ G4.1 baseline |
| **Rule E** — close framing skip | close framing 시 reference 언급 금지 표현 6 patterns + Rule A 무효화 4 forms enumerate | `background_binding.mode=skipped_close_framing` + constraints (R1-I2 6 patterns + R2-B3 Rule A 4 forms 모두 포함) + `close_framing_skips_background_ref=true` | **close-framing shot 에서 forbidden wording 검출 count == 0 strict** (R1-I4 / R2-B5 — `≤ baseline` 아님, baseline 비0 이어도 candidate 0 강제) |

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

---

## 5. Exit Criteria + Canary Procedure

### 5.1 Exit criteria (G4.1 spec R1-I10 기준)

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

| criterion | 측정 방법 (R1-I3 deterministic + R2-B4 scope 분리) | threshold |
|---|---|---|
| Rule A section 삭제 후 camera consistency wording degradation 없음 | canary script `scripts/canary/g4_2_camera_wording.py` 가 **non-close framing shot 의 `t2i_variations[].t2i_prompt` 만 scan** (R2-B4 — close framing 은 reference 자체 부재이므로 measurement 무의미) — regex 검출 count: `r"match.*reference.*(camera\|framing\|position)"`, `r"same.*(angle\|position).*reference"`, `r"(push in\|pull out\|deviation).*from.*reference"`, `r"reference.*camera_position"` 합산 | candidate rate ≥ baseline rate (degradation 차단) |
| Rule C section 삭제 후 owned violation rate ≤ baseline | G3.2 sentinel `owned_validation.violations` list count: cp 의 모든 t2i_variation sentinel 합산 | candidate ≤ baseline |
| Rule E section 삭제 후 close-framing forbidden wording 0 (R1-I4 / R2-B5 strict) | canary script `scripts/canary/g4_2_close_forbidden.py` 가 **close framing shot 의 t2i_prompt 만 scan** (R2-B4 — non-close 는 reference 정상 사용 — 본 metric 무의미) — §9 Glossary 의 6 ground-truth pattern + R2-B3 Rule A 4 forms 검출 count | **candidate count == 0** (≤ baseline 아님 — Rule E 는 close framing 에서 절대 0 강제) |
| system.md token delta (tiktoken cl100k_base 실측) | canary script 가 `tiktoken cl100k_base` 로 v17 system.md 실측 vs baseline 실측 | **net delta ≤ -850 tokens** (R2-I1 — estimate -915 대비 65 token slack) |
| pytest G4.2 test suite green | `backend/tests/unit/test_g4_2_*.py` + `backend/tests/integration/test_g4_2_*.py` all pass | 0 failures, 0 regressions |
| 기존 regression suite green | `pytest backend/tests/` | count ≥ G4.1 baseline (2025 passed) |

> **R1-I4 / R2-B5 strict**: Rule E close-framing forbidden wording 은 baseline 비0
> 이어도 candidate 0 강제. baseline 이 비0 이면 baseline 도 별도 production fix
> 대상으로 마킹 (G4.2 scope 밖, P1 follow-up). G4.2 는 v17 로 전환 시 본 표현 출력
> 0 보장. §7.3 G4.3 entry 도 동일 strict propagate.

> **R2-B4 metric scope 분리**: camera wording metric 과 forbidden wording metric 의
> 측정 대상 shot subset 이 **상호 배타** — 동일 prompt 가 두 metric 에 동시 포함
> 되어 conflicting threshold 발생 차단. close framing shot 은 forbidden 만, non-close
> 는 camera 만 측정.

G4.2 Exit criteria 가 충족되어야 G4.3 (ID-policy lift) 진입 가능.

### 5.2 Baseline 측정 대상 PID / scene set (R1-I5 pinning)

**Baseline 비교는 baseline / candidate 가 다음 변동 source 모두 동일할 때만 유효.**
다음 field 가 baseline / candidate JSON 에 모두 명시되어야 한다:

| pinning field | 값 결정 | 비교 시 enforce |
|---|---|---|
| `pid` | 단일 PID (G4.1 canary 와 동일 권장 — `next_session` memory 의 마지막 검증된 PID) | baseline.pid == candidate.pid |
| `scene_index_list` | 정확한 scene_index 정수 list (e.g. `[3, 7, 12, 18, 24]`) | sorted 비교 동일 |
| `shot_index_list_per_scene` | scene 별 shot_index 정수 list dict (e.g. `{"3": [1,2], "7": [4]}`) | dict 동일 |
| `model_routing` | scene_detail step 의 LLM 모델 (e.g. `gemini-3.1-pro-preview`). settings.scene_detail_model 또는 동급. | baseline.model_routing == candidate.model_routing |
| `prompt_source_mode` | `file` (file-based prompt) vs `db` (DB prompt_template). settings.prompt_source 동급. | 동일 |
| `card_commit_hash` | RenderPromptCard helper / detail_steps 의 git HEAD commit hash 시점 (G4.1 = `5d045f3` 또는 그 후속) | baseline / candidate 모두 명시 |
| `chain_bg_card_snapshot_hash` | chain_bg cp + scene_consistency cp 의 hash (loader 입력 변동 차단) | 동일 |

**Baseline scene set 요건**:
- background_mode_on + chain_bg assigned shot 을 가진 씬 최소 5개.
- 그 중 close framing shot 최소 2개 (Rule E 검증 mandatory).
- 모두 `scene_index_list` / `shot_index_list_per_scene` 에 명시.

Baseline 측정 실행:
```
# 1. canary script 가 위 pinning field 모두 capture 후 baseline JSON 작성
# 2. v16 (G4.1) 로 force scene_detail 실행 — 명시된 scene/shot 만 force
# 3. 측정: owned_validation.violations count per shot + Rule E forbidden wording count
#    + camera_consistency_wording_present (R1-I3 regex 적용)
#    + tiktoken cl100k_base system.md 실측 token count
# 4. 저장: docs/canary/g4_bg_binding_baseline_<timestamp>.json
```

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

### 5.3 Canary 절차 (G4.1 spec line 591-597 mini-canary 형식)

G4.2 canary 는 Rule A/C/E prose 삭제를 광범위 적용 전 1 PID / 1 selected segment 로
검증한다:

**Step 1 — Baseline capture (v16)**
```
force scene_detail 실행 (v16 prompt, G4.1 card, background_mode_on PID)
capture → docs/canary/g4_bg_binding_baseline_<timestamp>.json
```

JSON schema (R1-I3 + R1-I5 pinning + R2-B6 pid 위치 일관 + R2-B4 metric scope):
```json
{
  "timestamp": "ISO8601",
  "prompt_version": "16.202605041200",
  "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": "5d045f3",
    "chain_bg_card_snapshot_hash": "..."
  },
  "scene_set": [{"scene_index": N, "shot_index": M, "is_close_framing": bool}],
  "metrics": {
    "total_shots": N,
    "non_close_framing_shots": N,
    "close_framing_shots": N,
    "owned_violations_total": N,
    "owned_violations_per_shot": {},
    "rule_e_forbidden_wording_count_close_only": N,
    "rule_e_forbidden_wording_per_shot": {},
    "camera_consistency_wording_present_non_close_only": N,
    "camera_consistency_wording_patterns_matched": [],
    "system_prompt_token_count_tiktoken_cl100k": N
  },
  "measurement_scripts": {
    "rule_e_forbidden": "scripts/canary/g4_2_close_forbidden.py (close framing shots only — R2-B4)",
    "camera_wording": "scripts/canary/g4_2_camera_wording.py (non-close framing shots only — R2-B4)",
    "owned_violations": "scripts/canary/g4_2_owned_violations.py (G3.2 sentinel CP read — plan-R2-B3 back-propagate)",
    "token_count": "scripts/canary/g4_2_token_count.py (tiktoken cl100k_base — R1-M1)"
  }
}
```

> **plan-R2-B3 back-propagate**: `card_token_count_avg_tiktoken_cl100k` field 제거 —
> 산출 책임 없음 + plan 4 split scripts 어느 것도 측정 안 함. token measurement 는
> `system_prompt_token_count_tiktoken_cl100k` 단일 metric 으로 충분. canary scripts
> 는 4종 (camera / forbidden / owned / token).

> **R2-B6 pid 위치**: `pid` 가 `pinning` block 안에 위치 (top-level redundant 제거).
> §5.2 표 7 field 와 1:1 일치. baseline / candidate JSON 비교 시 `pinning` dict
> 동일 검사 한 번으로 모든 pinning field equality 확인.

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

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

**Step 3 — Comparison** (R1-I4 strict applied):

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

- `owned_violations_total`: candidate ≤ baseline
- `rule_e_forbidden_wording_count_close_only`: **candidate == 0 strict** (baseline
  비0 이어도 candidate 0 강제 — R1-I4 / R2-B5 결정. baseline 비0 이면 P1 follow-up
  으로 별 추적)
- `camera_consistency_wording_present_non_close_only`: candidate ≥ baseline
  (degradation 차단 — R2-B4 scope 로 인해 close framing 제외)
- `system_prompt_token_count_tiktoken_cl100k`: baseline - candidate ≥ **850** (R2-I1
  실측 token reduction gate)

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

---

## 6. Test Scope

### 6.1 Unit tests

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

| test | 설명 | assert |
|---|---|---|
| `test_bg_ref_attached_constraints_cover_rule_a` | `build_background_binding(background_mode_on=True, is_close_framing=False, bg_owned=["door"], bg_camera_meta={...})` 반환 constraints 에 camera match 지시가 포함 | "match the reference image's camera_position" 포함 |
| `test_bg_ref_attached_constraints_cover_rule_c` | 동일 setup → owned list 비어있지 않으면 "do not create new" + "anchor as 'from the reference'" 포함 | both strings present |
| `test_skipped_close_framing_constraints_cover_rule_e_six_patterns` | R1-I2 6 ground-truth patterns: `"the existing X"` / `"from the reference"` / `"use the X from the reference"` / `"preserving the same room perspective"` / **`"maintaining the reference's framing"`** / `"do not generate a new X"` 모두 constraints 에 포함 | 6 substring assert |
| `test_skipped_close_framing_constraints_cover_rule_a_four_forms` | R2-B3 Rule A 4 forms: `"matching the reference camera"` / `"deviation from reference"` / **`"same … angle as the reference"`** / **`"match the reference framing"`** 모두 constraints 의 Rule A 무효화 단락에 포함 | 4 substring assert |
| `test_skipped_close_framing_no_owned_objects` | close framing shot 의 card `owned_objects=[]` | list empty |
| `test_skipped_close_framing_no_camera_reference` | close framing shot 의 card `camera_reference=None` | None |
| `test_background_mode_off_no_constraints_reference` | `background_mode_on=False` → constraints 에 camera/owned reference 언급 없음 | "camera_reference" / "owned_objects" 문자열 constraints 에 없음 |
| `test_not_applicable_no_bg_id` | bg_id=None, background_mode_on=True, is_close_framing=False → mode="not_applicable" | mode assert |
| `test_not_applicable_constraints_no_reference_language` | not_applicable mode 의 constraints 에 reference 관련 표현 (`"from the reference"`, `"the existing room"`, `"preserving the room"`) 차단 가이드 포함 | constraints 에 차단 가이드 string 검증 |
| `test_not_applicable_card_metadata_lift_status` | not_applicable mode 의 `_card_metadata.lift_status` = `{"rule_a_lifted": True, "rule_c_lifted": True, "rule_e_lifted": False}` | exact dict assert |
| `test_background_ref_attached_owned_only_no_camera_constraints` | R1-I1 sub-case B (`bg_owned!=[], cam_ref==None`) → owned 2 constraint 만, camera 0 | constraints len + content assert |
| `test_background_ref_attached_camera_only_no_owned_constraints` | R1-I1 sub-case C (`bg_owned==[], cam_ref!=None`) → camera 2 constraint 만, owned 0 | constraints len + content assert |
| `test_background_ref_attached_neither_owned_nor_camera_fallback` | R1-I1 sub-case D (`bg_owned==[], cam_ref==None`) → fallback "no owned-object list and no camera metadata" 1 row | fallback string assert |
| `test_owned_objects_english_canonical_only` | `bg_owned=["door", "window", "TV"]` → constraints 에 영어 표기 그대로 carry | list 1:1 preserve |
| `test_card_hash_drifts_on_owned_change` | owned=["door"] → hash_A; owned=["door","window"] → hash_B | hash_A != hash_B |
| `test_card_hash_drifts_on_camera_change` | cam={"camera_position":"A"} → hash_A; cam={"camera_position":"B"} → hash_B | hash_A != hash_B |
| `test_card_hash_stable_on_constraints_reorder` | constraints list 순서 바꿔도 canonicalize 후 hash 동일 (string list → sorted) | hash_A == hash_B |
| `test_non_close_owned_list_in_card_only` | non-close shot 에서 `background_binding.owned_objects` 에 list 있고, 같은 card inject 후 `[RenderPromptCard v1]` 블록에 owned list 포함 확인 | list in injected JSON |
| `test_close_framing_card_mode_forbids_reference_wording` | `skipped_close_framing` mode card constraints 에 "from the reference" 금지 명시 | assert string in constraints |
| `test_lift_status_in_card_metadata` | `build_render_prompt_card()` 가 `_card_metadata.lift_status` 에 mode 별 정확한 lift dict 작성 (R1-B1 격리 검증) | not_applicable: rule_e_lifted=False / 그 외: all true |
| `test_rule_source_in_card_metadata` | `_card_metadata.rule_source` 가 `{"camera_rule":"A","owned_rule":"C","close_skip_rule":"E"}` 포함 (R1-B1 격리) | dict assert |
| `test_card_metadata_excluded_from_hash` | `canonicalize_render_prompt_card()` 결과에 `_card_metadata` key 부재 (R1-B1 / R1-I6 hash 제외 검증) | "_card_metadata" not in canonicalized output |
| `test_card_hash_stable_across_lift_status_change` | 동일 envelope 에서 `_card_metadata.lift_status` 만 다르면 hash 동일 (envelope contract 만 hash 영향) | hash_A == hash_B |

### 6.2 Integration tests

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

| test | 설명 | assert |
|---|---|---|
| `test_v17_system_prompt_missing_rule_a_section` | v17 system.md 를 load 후 `"## chain_bg reference 카메라 일관성"` heading 없음 | heading not present |
| `test_v17_system_prompt_missing_rule_c_section` | v17 system.md 에 `"## chain_bg 객체 중복 묘사 금지"` heading 없음 | heading not present |
| `test_v17_system_prompt_missing_rule_e_section` | v17 system.md 에 `"## close framing 시 reference image 부재 가정"` heading 없음 | heading not present |
| `test_v17_system_prompt_has_background_binding_section` | v17 system.md 에 "## Background Binding" (또는 동급) heading 존재 | heading present |
| `test_v17_prompt_token_reduction_vs_v16` | v17 system.md 줄 수 < v16 줄 수 (최소 60 lines 감소) | len(v17_lines) < len(v16_lines) - 60 |
| `test_non_close_shot_card_carries_owned_and_camera` | `_collect_card_inputs()` + `build_render_prompt_card()` + inject 경로 통해 user_prompt 에 `owned_objects` list + `camera_reference` dict 포함 확인 | JSON parse → background_binding.owned_objects 비어있지 않음 |
| `test_close_framing_shot_card_skips_owned_and_camera` | close framing path → `mode=skipped_close_framing`, `owned_objects=[]`, `camera_reference=null` | all three asserts |
| `test_card_hash_drift_on_owned_change_in_verify` | owned 변경 후 `verify_completion()` 실행 → hash drift detected → status partial | verify marks partial |
| `test_g3_2_sentinel_coexists_with_g4_2_card` | background_ref_attached mode shot → card `background_binding` 과 variation-level `owned_validation` sentinel 둘 다 CP 에 존재 | both present in CP dict |
| `test_v15_checkpoint_escalates_on_v17_resume` (**G4.1 carry — G4.2 plan 에 신규 추가 X**) | v15 cp (schema_version=5) 를 v17 consumer 로 resume → schema/config mismatch → `mode="force"` escalate. G4.1 의 `test_g4_1_card_consumer_wiring.py::test_step_runner_actually_escalates_to_force_on_v15_resume` 가 이미 동일 contract 검증 — G4.2 의 v17 prompt 는 G4.1 force escalate path 위에 동일하게 작동 (plan-R1-I2 결정) | (G4.1 test 가 v17 resume 도 동일 검증 — 신규 작성 불필요) |
| `test_not_applicable_shot_card_carries_no_owned_no_camera_no_reference` | not_applicable mode shot path → `mode="not_applicable"`, `owned_objects=[]`, `camera_reference=null`, constraints 에 reference 차단 가이드 포함, `_card_metadata.lift_status.rule_e_lifted=False` | 4 assert 모두 |

### 6.3 Regression tests

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

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

기준: G4.1 baseline (2025 passed) 대비 0 regressions. G4.2 test 추가 후 total count
증가만 허용.

G3.2 sentinel test suite (`test_g3_2_*.py`) 가 G4.2 이후에도 모두 pass 해야 한다
(owned sentinel 로직 변경 없음 — G4.2 는 prompt side 만 lift).

---

## 7. G4.x Sequencing

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

G4.1 exit criteria (G4 spec R1-I10):
- card payload + hash CP 저장 확인 green
- drift detection (verify_completion + _user_edited path) green
- canary 1 PID prompt token delta < +30%

G4.1 commit 5d045f3 (+ c656ce3 + 86473df) 이 이미 이 criteria 를 충족함 (session
memory `session_20260504_g4_1_complete.md` 참조).

### 7.2 G4.2 작업 순서

1. `build_render_prompt_card()` / `build_background_binding()` 에 G4.2 변경 적용
   (R2-B2 in-scope):
   - envelope-level `_card_metadata.lift_status` / `_card_metadata.rule_source` 작성
     (R1-B1 격리 — top-level sibling, envelope contract field 외).
   - `build_background_binding()` 의 sub-case D fallback constraint 추가 (R1-I1 —
     `bg_owned==[]` and `cam_ref==None` and `mode=="background_ref_attached"` 일 때
     fallback row 1개).
   - `skipped_close_framing` constraints 에 R1-I2 6 ground-truth pattern + R2-B3 Rule A
     4 forms 모두 명시.
   - **`detail_steps.py` (현재 `:1937-1942`) 의 user_prompt inject 직전 `_card_metadata`
     key strip 추가** (R2-I4 — token +0 가정 보존). card 의 `_card_metadata` 는 LLM
     입력에서 제외, 다른 envelope contract field 만 inject.
2. `canonicalize_render_prompt_card()` 가 top-level `_card_metadata` 를 strip 후
   hash (G4.1 R1-I11 carry — `render_prompt_card.py:563-566` 이미 구현. 검증 unit test
   `test_card_metadata_excluded_from_hash` 추가).
3. `assert_card_shape()` 동작 확정 (R2-I5):
   - **envelope contract 7 field strict 검증 유지**: `schema_version`, `shot_key`,
     `render_strategy`, `id_policy`, `background_binding`, `continuity_elements_used`,
     `asset_requirements`.
   - **`_card_metadata` 는 free-form (extra-key 허용, schema 검증 없음)** — 미래 G4.x
     debug sub-field 추가 시 validator 변경 불필요.
   - `background_binding` 내부 constraint item 최소 count 검증은 G4.2 in-scope optional
     (mode 별 minimum). G4.2 R-round 후 추가 결정.
4. canary measurement script **4종** 작성 (plan-R2-B3 / plan-R2-B4 back-propagate):
   - `scripts/canary/g4_2_camera_wording.py` (R1-I3 regex set, non-close framing only)
   - `scripts/canary/g4_2_close_forbidden.py` (§9 Glossary 6 pattern + R2-B3 Rule A 4 forms, close framing only)
   - `scripts/canary/g4_2_owned_violations.py` (G3.2 sentinel CP read — plan-R2-B3 신설)
   - `scripts/canary/g4_2_token_count.py` (tiktoken cl100k_base — R1-M1)
5. Baseline canary capture (v16) — pinning field 7종 명시 (R1-I5):
   - `docs/canary/g4_bg_binding_baseline_<timestamp>.json` 작성.
6. v17 prompt 신규 디렉토리 생성: `prompts/_base/scene_detail/17.<timestamp>/`:
   - Rule A/C/E section 삭제 + "Background Binding" compact section 추가 (§3.3).
   - schema 변경 없음 (`detail_schema.json` 동일 유지).
7. `SCENE_DETAIL_PROMPT_VERSION` bump (v16 → v17). **`SCENE_DETAIL_SCHEMA_VERSION` 유지
   (= 7) — R1-I6 결정**: cp shape 변경 없음 (`_card_metadata` 추가는 hash 외 envelope 외
   debug-only — backward compat). **`CARD_SCHEMA_VERSION` 유지 (= 1) — R1-I6 결정**:
   envelope 5 contract field 변경 없음, `_card_metadata` 추가는 hash 외 v1 backward compat.
8. G4.2 unit test suite 작성 (§6.1 — R1-B1 격리 검증 4종 + R1-I1 4 sub-case + R1-I2 6 pattern
   + R1-I7 not_applicable 2종 포함).
9. G4.2 integration test suite 작성 (§6.2 — R1-I7 not_applicable 1종 포함).
10. Candidate canary capture (v17) — 동일 pinning → compare → exit criteria (§5.1) 판정.
11. 통과 시 단일 commit push.
12. Codex + Claude 듀얼 리뷰 → 수정 → 재push.

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

G4.3 은 다음 모두 충족 후 진입:
- G4.2 owned violation rate ≤ G4.1 baseline (canary JSON 기록)
- **G4.2 close framing forbidden wording count == 0 strict** (R2-B5 — R1-I4 strict
  propagate. baseline 비0 이어도 candidate 0 강제)
- G4.2 unit + integration test suite green
- v17 prompt push + tag 완료

G4.3 이 다루는 scope: ID-policy lift (C##O## composite, body-part, screen/photo/mirror,
demographic fallback).

---

## 8. Risk Register

### 8.1 G4.2-unique risks

| risk | mitigation | measurable threshold |
|---|---|---|
| v17 compact section 이 Rule A deviation 표현 guidance 를 충분히 carry 하지 않아 LLM 이 camera deviation 을 명시 안 함 | constraints 에 "if deviating, state deviation explicitly" 포함 (§2.2 sub-case A/B) + canary camera_consistency_wording_present rate ≥ baseline | candidate rate ≥ baseline rate |
| Rule C prose 삭제 후 LLM 이 owned object 를 새로 그리도록 prompt 작성 | G3.2 sentinel judge (LLM judge + violations sentinel) 는 유지 — G4.2 scope 밖. owned_objects list 가 constraints 에 명시적 반영 | owned_validation violations count candidate ≤ baseline |
| Rule E close framing 금지 표현 6개가 card constraints 에 완전히 cover 되지 않음 (R1-I2) | §2.2 `skipped_close_framing` constraints 에 6 ground-truth pattern + Rule A 무효화 모두 명시 + unit test 6 pattern 검증 | all 6 patterns + Rule A 무효화 present — unit test green |
| `_card_metadata` sub-field 가 hash 에 의도치 않게 포함되어 hash 가 매 build 마다 변경 | R1-B1 결정: `_card_metadata` 전체 가 `canonicalize_render_prompt_card()` 에서 strip (G4.1 R1-I11 carry). unit test `test_card_metadata_excluded_from_hash` 가 강제 | `_card_metadata` not in canonicalized output |
| §2.2 sub-case D (owned/camera 둘 다 부재) fallback constraint 부재 시 LLM 이 background_ref_attached 인데 가이드 없음 | builder step 1 에서 fallback row 추가 (§7.2 step 1) + unit test `test_background_ref_attached_neither_owned_nor_camera_fallback` | fallback constraint 1 row present |
| canary PID 의 `background_mode_on` 환경이 아닌 경우 Rule A/C 검증 불가 | baseline / candidate scene set 선정 시 background_mode_on + chain_bg assigned shot 최소 5개 + close framing 최소 2개 보장 (§5.2 R1-I5 pinning) | scene_set in canary JSON ≥ 5 background_mode shots |
| G3.2 sentinel judge false positive 가 owned violation rate 를 왜곡 | baseline / candidate 동일 pinning (R1-I5) 사용 — 동일 model_routing / prompt_source / scene set → 같은 judge 조건 → false positive 가 양쪽 동일하게 발생 (delta 만 비교 valid) | baseline / candidate pinning 7 field 동일 |
| canary close-framing forbidden wording baseline 이 비0 인 경우 | R1-I4 strict: candidate == 0 강제. baseline 비0 이면 P1 follow-up 으로 별 추적 (G4.2 scope 밖이지만 production 결함 시그널) | candidate count == 0 mandatory; baseline 비0 면 별 issue 등록 |

### 8.2 G4.1 carry / inherited checks (R1-M2 분리)

본 row 들은 G4.2 에 unique 하지 않음. G4.1 으로부터 carry 되어 G4.2 에서도 동일하게
적용되며, regression 차단용으로만 본 spec 이 reference.

| inherited risk | carry source | G4.2 enforcement |
|---|---|---|
| v17 로 resume 시 old v16 cp 가 자동 force escalate | G4.1 R1-B1 (`_config_hash` mismatch 검증) | §6.2 `test_v15_checkpoint_escalates_on_v17_resume` |
| `_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.2 변경 없음 |
| schema_version != 7 result 에서 card check bypass warning step 당 1회 emit | G4.1 cleanup I5 (commit c656ce3) | G4.2 변경 없음 — 동일 동작 |

---

## 9. Glossary

| 용어 | 정의 |
|---|---|
| `background_binding` | `RenderPromptCard` 의 5 field 중 하나. chain_bg reference 의 existence, ownership, camera 설정, close-framing skip 여부를 deterministic 하게 기술. |
| Rule A | `scene_detail v16 system.md` line 197-222 의 camera consistency rule. chain_bg PNG 를 그릴 때 사용한 카메라 설정과 t2i_prompt 카메라가 일치/deviation 명시해야 하는 rule. |
| Rule C | `scene_detail v16 system.md` line 223-246 의 owned object no-redraw rule. G3.2 contract 의 prompt-side expression. chain_bg PNG 에 이미 그려진 환경 객체를 t2i_prompt 가 새로 그리도록 지시하면 이중 합성. |
| Rule E | `scene_detail v16 system.md` line 247-279 의 close framing reference skip rule. close framing shot 은 chain_bg PNG 를 합성에서 자동 skip — t2i_prompt 가 reference 가정 표현을 쓰면 LLM 이 없는 reference 를 hallucinate. |
| owned_objects | chain_bg PNG 에 이미 포함된 환경 객체의 영어 canonical list. `background_prompt.objects_owned_by_background` (G3.2) 에서 load. |
| camera_reference | chain_bg PNG 를 그릴 때 사용한 카메라 설정 dict (camera_position / camera_height / lens_hint / framing_notes). |
| lift | system.md prose section 을 card field/constraints 로 이전 + prompt section 삭제. |
| lift_status | card `_card_metadata` sub-field. 각 Rule 이 G4.x 에서 card 로 lift 됐는지 추적. hash payload 에서 제외. |
| owned violation rate | G3.2 sentinel `owned_validation.violations` count / total shots. G4.2 exit criteria 의 primary metric. |
| close framing forbidden wording **(R1-I2 ground-truth source)** | close framing shot 의 t2i_prompt 에 등장하면 안 되는 6 ground-truth pattern. **본 정의가 single source — §2.2 / §3.3 / §6.1 unit test / canary detector script 모두 본 list 를 reference**. 6 patterns: (1) `"the existing X"` (e.g. "the existing door", "the existing room"), (2) `"from the reference"` / `"from the reference image"`, (3) `"use the X from the reference"`, (4) `"preserving the same room perspective"`, (5) `"maintaining the reference's framing"`, (6) `"do not generate a new X"`. **추가**: Rule A close-framing 무효화 표현 — `"matching the reference camera"`, `"deviation from reference"`, `"same … angle as the reference"`, `"match the reference framing"` 류 (Rule A 가 close framing 에 적용 불가하므로 동등하게 금지). |
| canary | 광범위 적용 전 1 PID / 1 selected segment 로 baseline vs candidate 비교 검증. 결과는 `docs/canary/g4_bg_binding_<timestamp>.json` 에 저장. |
| G4.1 baseline | G4.1 commit 5d045f3 + c656ce3 + 86473df 기준의 owned violation rate + close framing forbidden wording count. G4.2 exit criteria 비교 기준. |

---

## 10. Round Audit Override 표 (진행 시 채움)

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

### Round 1 (2026-05-04) — 완료

1 BLOCKING + 7 IMPORTANT + 2 MINOR. 결정 row 는 상단 "Round Override" 표 참조.

### Round 2 (2026-05-04) — 완료

6 BLOCKING + 5 IMPORTANT + 1 MINOR (cross-conflict 광범위 검출 — R1 결정의
다른 섹션 propagate 누락 + Non-goals 충돌 + builder code 직접 비교에서 producer-side
변경 필요 식별). 결정 row 는 상단 Round 2 Override 표 참조.

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

- Trigger: G4.2 plan drafting 후 plan ↔ spec drift 발생 시 / canary 실측 후
  estimate 와 큰 차이 발견 시 / 구현 단계 추가 BLOCKING 발견 시.
- Empty until R3 audit dispatched.

### 메타룰

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